web: Replace ad-hoc toggle control with ak-toggle-group

This commit replaces various ad-hoc implementations of the Patternfly Toggle Group HTML with a web
component that encapsulates all of the needed behavior and exposes a single API with a single event
handler, return the value of the option clicked.

The results are: Lots of visual clutter is eliminated.  A single link of:

```
<div class="pf-c-toggle-group__item">
  <button
      class="pf-c-toggle-group__button ${this.mode === ProxyMode.Proxy
          ? "pf-m-selected"
          : ""}"
      type="button"
      @click=${() => {
          this.mode = ProxyMode.Proxy;
      }}>
      <span class="pf-c-toggle-group__text">${msg("Proxy")}</span>
  </button>
</div>
<div class="pf-c-divider pf-m-vertical" role="separator"></div>
```

Now looks like:

```
<option value=${ProxyMode.Proxy}>${msg("Proxy")}</option>
```

This also means that the three pages that used the Patternfly Toggle Group could eliminate all of
their Patternfly PFToggleGroup needs, as well as the `justify-content: center` extension, which also
eliminated the `css` import.

The savings aren't as spectacular as I'd hoped: removed 178 lines, but added 123; total savings 55
lines of code.  I still count this a win: we need never write another toggle component again, and
any bugs, extensions or features we may want to add can be centralized or forked without risking the
whole edifice.
This commit is contained in:
Ken Sternberg 2023-08-03 14:43:31 -07:00
parent 9e34a74a48
commit df16dc3088
5 changed files with 136 additions and 173 deletions

View file

@ -9,12 +9,11 @@ import "@goauthentik/elements/forms/SearchSelect";
import YAML from "yaml"; import YAML from "yaml";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css"; import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css";
import { BlueprintFile, BlueprintInstance, ManagedApi } from "@goauthentik/api"; import { BlueprintFile, BlueprintInstance, ManagedApi } from "@goauthentik/api";
@ -51,15 +50,7 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
} }
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return super.styles.concat( return [...super.styles, PFContent];
PFToggleGroup,
PFContent,
css`
.pf-c-toggle-group {
justify-content: center;
}
`,
);
} }
async send(data: BlueprintInstance): Promise<BlueprintInstance> { async send(data: BlueprintInstance): Promise<BlueprintInstance> {
@ -105,52 +96,16 @@ export class BlueprintForm extends ModelForm<BlueprintInstance, string> {
</ak-form-element-horizontal> </ak-form-element-horizontal>
<div class="pf-c-card pf-m-selectable pf-m-selected"> <div class="pf-c-card pf-m-selectable pf-m-selected">
<div class="pf-c-card__body"> <div class="pf-c-card__body">
<div class="pf-c-toggle-group"> <ak-toggle-group
<div class="pf-c-toggle-group__item"> value=${this.source}
<button @ak-toggle=${(ev: CustomEvent<{ value: blueprintSource }>) => {
class="pf-c-toggle-group__button ${this.source === this.source = ev.detail.value;
blueprintSource.file
? "pf-m-selected"
: ""}"
type="button"
@click=${() => {
this.source = blueprintSource.file;
}} }}
> >
<span class="pf-c-toggle-group__text">${msg("Local path")}</span> <option value=${blueprintSource.file}>${msg("Local path")}</option>
</button> <option value=${blueprintSource.oci}>${msg("OCI Registry")}</option>
</div> <option value=${blueprintSource.internal}>${msg("Internal")}</option>
<div class="pf-c-divider pf-m-vertical" role="separator"></div> </ak-toggle-group>
<div class="pf-c-toggle-group__item">
<button
class="pf-c-toggle-group__button ${this.source ===
blueprintSource.oci
? "pf-m-selected"
: ""}"
type="button"
@click=${() => {
this.source = blueprintSource.oci;
}}
>
<span class="pf-c-toggle-group__text">${msg("OCI Registry")}</span>
</button>
</div>
<div class="pf-c-divider pf-m-vertical" role="separator"></div>
<div class="pf-c-toggle-group__item">
<button
class="pf-c-toggle-group__button ${this.source ===
blueprintSource.internal
? "pf-m-selected"
: ""}"
type="button"
@click=${() => {
this.source = blueprintSource.internal;
}}
>
<span class="pf-c-toggle-group__text">${msg("Internal")}</span>
</button>
</div>
</div>
</div> </div>
<div class="pf-c-card__footer"> <div class="pf-c-card__footer">
${this.source === blueprintSource.file ${this.source === blueprintSource.file

View file

@ -5,12 +5,11 @@ import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/SearchSelect"; import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, css } from "lit"; import { CSSResult } from "lit";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css"; import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css";
import { import {
CoreApi, CoreApi,
@ -70,15 +69,7 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
} }
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return super.styles.concat( return [...super.styles, PFContent];
PFToggleGroup,
PFContent,
css`
.pf-c-toggle-group {
justify-content: center;
}
`,
);
} }
send(data: PolicyBinding): Promise<unknown> { send(data: PolicyBinding): Promise<unknown> {
@ -112,55 +103,22 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
} }
renderModeSelector(): TemplateResult { renderModeSelector(): TemplateResult {
return html` <div class="pf-c-toggle-group__item"> return html` <ak-toggle-group
<button value=${this.policyGroupUser}
class="pf-c-toggle-group__button ${this.policyGroupUser === target.policy @ak-toggle=${(ev: CustomEvent<{ value: target }>) => {
? "pf-m-selected" this.policyGroupUser = ev.detail.value;
: ""}"
type="button"
@click=${() => {
this.policyGroupUser = target.policy;
}} }}
> >
<span class="pf-c-toggle-group__text">${msg("Policy")}</span> <option value=${target.policy}>${msg("Policy")}</option>
</button> <option value=${target.group}>${msg("Group")}</option>
</div> <option value=${target.user}>${msg("User")}</option>
<div class="pf-c-divider pf-m-vertical" role="separator"></div> </ak-toggle-group>`;
<div class="pf-c-toggle-group__item">
<button
class="pf-c-toggle-group__button ${this.policyGroupUser === target.group
? "pf-m-selected"
: ""}"
type="button"
@click=${() => {
this.policyGroupUser = target.group;
}}
>
<span class="pf-c-toggle-group__text">${msg("Group")}</span>
</button>
</div>
<div class="pf-c-divider pf-m-vertical" role="separator"></div>
<div class="pf-c-toggle-group__item">
<button
class="pf-c-toggle-group__button ${this.policyGroupUser === target.user
? "pf-m-selected"
: ""}"
type="button"
@click=${() => {
this.policyGroupUser = target.user;
}}
>
<span class="pf-c-toggle-group__text">${msg("User")}</span>
</button>
</div>`;
} }
renderForm(): TemplateResult { renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal"> return html`<form class="pf-c-form pf-m-horizontal">
<div class="pf-c-card pf-m-selectable pf-m-selected"> <div class="pf-c-card pf-m-selectable pf-m-selected">
<div class="pf-c-card__body"> <div class="pf-c-card__body">${this.renderModeSelector()}</div>
<div class="pf-c-toggle-group">${this.renderModeSelector()}</div>
</div>
<div class="pf-c-card__footer"> <div class="pf-c-card__footer">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Policy")} label=${msg("Policy")}

View file

@ -1,6 +1,7 @@
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search"; import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils"; import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/forms/FormGroup"; import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement"; import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm"; import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -8,14 +9,13 @@ import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/utils/TimeDeltaHelp"; import "@goauthentik/elements/utils/TimeDeltaHelp";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, css } from "lit"; import { CSSResult } from "lit";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css"; import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFList from "@patternfly/patternfly/components/List/list.css"; import PFList from "@patternfly/patternfly/components/List/list.css";
import PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css";
import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css"; import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css";
import { import {
@ -35,17 +35,7 @@ import {
@customElement("ak-provider-proxy-form") @customElement("ak-provider-proxy-form")
export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> { export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return super.styles.concat( return [...super.styles, PFContent, PFList, PFSpacing];
PFToggleGroup,
PFContent,
PFList,
PFSpacing,
css`
.pf-c-toggle-group {
justify-content: center;
}
`,
);
} }
async loadInstance(pk: number): Promise<ProxyProvider> { async loadInstance(pk: number): Promise<ProxyProvider> {
@ -137,51 +127,22 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
} }
renderModeSelector(): TemplateResult { renderModeSelector(): TemplateResult {
return html` <div class="pf-c-toggle-group__item"> return html`
<button <ak-toggle-group
class="pf-c-toggle-group__button ${this.mode === ProxyMode.Proxy value=${this.mode}
? "pf-m-selected" @ak-toggle=${(ev: CustomEvent<{ value: ProxyMode }>) => {
: ""}" this.mode = ev.detail.value;
type="button"
@click=${() => {
this.mode = ProxyMode.Proxy;
}} }}
> >
<span class="pf-c-toggle-group__text">${msg("Proxy")}</span> <option value=${ProxyMode.Proxy}>${msg("Proxy")}</option>
</button> <option value=${ProxyMode.ForwardSingle}>
</div> ${msg("Forward auth (single application)")}
<div class="pf-c-divider pf-m-vertical" role="separator"></div> </option>
<div class="pf-c-toggle-group__item"> <option value=${ProxyMode.ForwardDomain}>
<button ${msg("Forward auth (domain level)")}
class="pf-c-toggle-group__button ${this.mode === ProxyMode.ForwardSingle </option>
? "pf-m-selected" </ak-toggle-group>
: ""}" `;
type="button"
@click=${() => {
this.mode = ProxyMode.ForwardSingle;
}}
>
<span class="pf-c-toggle-group__text"
>${msg("Forward auth (single application)")}</span
>
</button>
</div>
<div class="pf-c-divider pf-m-vertical" role="separator"></div>
<div class="pf-c-toggle-group__item">
<button
class="pf-c-toggle-group__button ${this.mode === ProxyMode.ForwardDomain
? "pf-m-selected"
: ""}"
type="button"
@click=${() => {
this.mode = ProxyMode.ForwardDomain;
}}
>
<span class="pf-c-toggle-group__text"
>${msg("Forward auth (domain level)")}</span
>
</button>
</div>`;
} }
renderSettings(): TemplateResult { renderSettings(): TemplateResult {
@ -362,9 +323,7 @@ export class ProxyProviderFormPage extends ModelForm<ProxyProvider, number> {
</ak-form-element-horizontal> </ak-form-element-horizontal>
<div class="pf-c-card pf-m-selectable pf-m-selected"> <div class="pf-c-card pf-m-selectable pf-m-selected">
<div class="pf-c-card__body"> <div class="pf-c-card__body">${this.renderModeSelector()}</div>
<div class="pf-c-toggle-group">${this.renderModeSelector()}</div>
</div>
<div class="pf-c-card__footer">${this.renderSettings()}</div> <div class="pf-c-card__footer">${this.renderSettings()}</div>
</div> </div>
<ak-form-element-horizontal label=${msg("Token validity")} name="accessTokenValidity"> <ak-form-element-horizontal label=${msg("Token validity")} name="accessTokenValidity">

View file

@ -0,0 +1,90 @@
import { AKElement } from "@goauthentik/elements/Base";
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import PFToggleGroup from "@patternfly/patternfly/components/ToggleGroup/toggle-group.css";
type Pair = [string, string];
/**
* Toggle Group
*
* An implementation of the Patternfly Toggle Group as a LitElement
*
* @element ak-toggle-group
*
* @fires ak-toggle - Fired when someone clicks on a toggle option. Carries the value of the option.
*/
// MYNIS:
// A 'name' property so that the event carries *which* toggle group emitted the event.
@customElement("ak-toggle-group")
export class AkToggleGroup extends CustomEmitterElement(AKElement) {
static get styles() {
return [
PFToggleGroup,
css`
.pf-c-toggle-group {
justify-content: center;
}
`,
];
}
/*
* The value (causes highlighting, value is returned)
*
* @attr
*/
@property({ type: String, reflect: true })
value = "";
get rawOptions(): HTMLOptionElement[] {
return Array.from(this.querySelectorAll("option") ?? []);
}
get options(): Pair[] {
return Array.from(this.rawOptions).map(
(option: HTMLOptionElement): Pair => [
option.getAttribute("value") ?? "",
option.textContent ?? "",
],
);
}
render() {
const last = this.options.length - 1;
const mkClass = (v: string) => ({
"pf-c-toggle-group__button": true,
"pf-m-selected": this.value === v,
});
const mkClick = (v: string) => () => {
this.dispatchCustomEvent("ak-toggle", { value: v });
};
return html` <div class="pf-c-toggle-group">
${this.options.map(
([key, label], idx) =>
html`<div class="pf-c-toggle-group__item">
<button
class="${classMap(mkClass(key))}"
type="button"
@click=${mkClick(key)}
>
<span class="pf-c-toggle-group__text">${label}</span>
</button>
</div>
${idx < last
? html`<div class="pf-c-divider pf-m-vertical" role="separator"></div>`
: nothing} `,
)}
</div>`;
}
}
export default AkToggleGroup;

View file

@ -5,6 +5,7 @@
"@goauthentik/app/*": ["src/*"], "@goauthentik/app/*": ["src/*"],
"@goauthentik/admin/*": ["src/admin/*"], "@goauthentik/admin/*": ["src/admin/*"],
"@goauthentik/common/*": ["src/common/*"], "@goauthentik/common/*": ["src/common/*"],
"@goauthentik/components/*": ["src/components/*"],
"@goauthentik/docs/*": ["../website/docs/*"], "@goauthentik/docs/*": ["../website/docs/*"],
"@goauthentik/elements/*": ["src/elements/*"], "@goauthentik/elements/*": ["src/elements/*"],
"@goauthentik/flow/*": ["src/flow/*"], "@goauthentik/flow/*": ["src/flow/*"],