diff --git a/web/src/admin/applications/ApplicationWizardHint.ts b/web/src/admin/applications/ApplicationWizardHint.ts new file mode 100644 index 000000000..9f0a22e88 --- /dev/null +++ b/web/src/admin/applications/ApplicationWizardHint.ts @@ -0,0 +1,69 @@ +import { MessageLevel } from "@goauthentik/common/messages"; +import { + ShowHintController, + ShowHintControllerHost, +} from "@goauthentik/components/ak-hint/ShowHintController"; +import "@goauthentik/components/ak-hint/ak-hint"; +import "@goauthentik/components/ak-hint/ak-hint-body"; +import { AKElement } from "@goauthentik/elements/Base"; +import "@goauthentik/elements/buttons/ActionButton/ak-action-button"; +import { showMessage } from "@goauthentik/elements/messages/MessageContainer"; + +import { html, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; + +import PFPage from "@patternfly/patternfly/components/Page/page.css"; + +@customElement("ak-application-wizard-hint") +export class AkApplicationWizardHint extends AKElement implements ShowHintControllerHost { + static get styles() { + return [PFPage]; + } + + @property({ type: Boolean, attribute: "show-hint" }) + forceHint: boolean = false; + + @state() + showHint: boolean = true; + + showHintController: ShowHintController; + + constructor() { + super(); + this.showHintController = new ShowHintController( + this, + "202310-application-wizard-announcement", + ); + } + + renderHint() { + return html`
+ + +

+ Authentik has a new Application Wizard that can configure both an + application and its authentication provider at the same time. + Learn more about the wizard here. +

+ { + showMessage({ + message: "This would have shown the wizard", + level: MessageLevel.success, + }); + }} + >Create with Wizard
+ ${this.showHintController.render()} +
+
`; + } + + render() { + return this.showHint || this.forceHint ? this.renderHint() : nothing; + } +} + +export default AkApplicationWizardHint; diff --git a/web/src/common/constants.ts b/web/src/common/constants.ts index ca6ce33a1..1e58a838a 100644 --- a/web/src/common/constants.ts +++ b/web/src/common/constants.ts @@ -22,3 +22,5 @@ export const EVENT_THEME_CHANGE = "ak-theme-change"; export const WS_MSG_TYPE_MESSAGE = "message"; export const WS_MSG_TYPE_REFRESH = "refresh"; + +export const LOCALSTORAGE_AUTHENTIK_KEY = "authentik-local-settings"; diff --git a/web/src/components/ak-hint/ShowHintController.ts b/web/src/components/ak-hint/ShowHintController.ts new file mode 100644 index 000000000..b3eb1393c --- /dev/null +++ b/web/src/components/ak-hint/ShowHintController.ts @@ -0,0 +1,63 @@ +import { LOCALSTORAGE_AUTHENTIK_KEY } from "@goauthentik/common/constants"; + +import { msg } from "@lit/localize"; +import { LitElement, ReactiveController, ReactiveControllerHost, html } from "lit"; + +type ReactiveLitElement = LitElement & ReactiveControllerHost; + +export interface ShowHintControllerHost extends ReactiveLitElement { + showHint: boolean; + + showHintController: ShowHintController; +} + +const getCurrentStorageValue = (): Record => { + try { + return JSON.parse(window?.localStorage.getItem(LOCALSTORAGE_AUTHENTIK_KEY) ?? "{}"); + } catch (_err: unknown) { + return {}; + } +}; + +export class ShowHintController implements ReactiveController { + host: ShowHintControllerHost; + + hintToken: string; + + constructor(host: ShowHintControllerHost, hintToken: string) { + (this.host = host).addController(this); + this.hintToken = hintToken; + this.hideTheHint = this.hideTheHint.bind(this); + } + + hideTheHint() { + window?.localStorage.setItem( + LOCALSTORAGE_AUTHENTIK_KEY, + JSON.stringify({ + ...getCurrentStorageValue(), + [this.hintToken]: false, + }), + ); + this.host.showHint = false; + } + + hostConnected() { + const localStores = getCurrentStorageValue(); + if (!(this.hintToken in localStores)) { + return; + } + // Note that we only do this IF the field exists and is defined. `undefined` means "do the + // default thing of showing the hint." + this.host.showHint = localStores[this.hintToken] as boolean; + } + + render() { + return html`
+ ${msg( + "Don't show this message again.", + )} +
`; + } +} diff --git a/web/src/components/ak-hint/ak-hint-actions.ts b/web/src/components/ak-hint/ak-hint-actions.ts new file mode 100644 index 000000000..8af341965 --- /dev/null +++ b/web/src/components/ak-hint/ak-hint-actions.ts @@ -0,0 +1,32 @@ +import { AKElement } from "@goauthentik/app/elements/Base"; + +import { css, html } from "lit"; +import { customElement } from "lit/decorators.js"; + +const style = css` + div { + display: inline-grid; + grid-row: 1; + grid-column: 2; + grid-auto-flow: column; + margin-left: var(--ak-hint__actions--MarginLeft); + text-align: right; + } + + ::slotted(ak-hint-body) { + grid-column: 1; + } +`; + +@customElement("ak-hint-actions") +export class AkHintActions extends AKElement { + static get styles() { + return [style]; + } + + render() { + return html`
`; + } +} + +export default AkHintActions; diff --git a/web/src/components/ak-hint/ak-hint-body.ts b/web/src/components/ak-hint/ak-hint-body.ts new file mode 100644 index 000000000..f5c122eb5 --- /dev/null +++ b/web/src/components/ak-hint/ak-hint-body.ts @@ -0,0 +1,24 @@ +import { AKElement } from "@goauthentik/app/elements/Base"; + +import { css, html } from "lit"; +import { customElement } from "lit/decorators.js"; + +const style = css` + div { + grid-column: 1 / -1; + font-size: var(--ak-hint__body--FontSize); + } +`; + +@customElement("ak-hint-body") +export class AkHintBody extends AKElement { + static get styles() { + return [style]; + } + + render() { + return html`
`; + } +} + +export default AkHintBody; diff --git a/web/src/components/ak-hint/ak-hint-footer.ts b/web/src/components/ak-hint/ak-hint-footer.ts new file mode 100644 index 000000000..a08197d11 --- /dev/null +++ b/web/src/components/ak-hint/ak-hint-footer.ts @@ -0,0 +1,26 @@ +import { AKElement } from "@goauthentik/app/elements/Base"; + +import { css, html } from "lit"; +import { customElement } from "lit/decorators.js"; + +const style = css` + #host { + grid-column: 1 / -1; + } + ::slotted(div#host > *:not(:last-child)) { + margin-right: var(--ak-hint__footer--child--MarginRight); + } +`; + +@customElement("ak-hint-footer") +export class AkHintFooter extends AKElement { + static get styles() { + return [style]; + } + + render() { + return html`
`; + } +} + +export default AkHintFooter; diff --git a/web/src/components/ak-hint/ak-hint-title.ts b/web/src/components/ak-hint/ak-hint-title.ts new file mode 100644 index 000000000..accc881d0 --- /dev/null +++ b/web/src/components/ak-hint/ak-hint-title.ts @@ -0,0 +1,23 @@ +import { AKElement } from "@goauthentik/app/elements/Base"; + +import { css, html } from "lit"; +import { customElement } from "lit/decorators.js"; + +const style = css` + #host { + font-size: var(--ak-hint__title--FontSize); + } +`; + +@customElement("ak-hint-title") +export class AkHintTitle extends AKElement { + static get styles() { + return [style]; + } + + render() { + return html`
`; + } +} + +export default AkHintTitle; diff --git a/web/src/components/ak-hint/ak-hint.stories.ts b/web/src/components/ak-hint/ak-hint.stories.ts new file mode 100644 index 000000000..281913761 --- /dev/null +++ b/web/src/components/ak-hint/ak-hint.stories.ts @@ -0,0 +1,137 @@ +import { MessageLevel } from "@goauthentik/common/messages"; +import "@goauthentik/elements/buttons/ActionButton/ak-action-button"; +import { showMessage } from "@goauthentik/elements/messages/MessageContainer"; +import { Meta } from "@storybook/web-components"; + +import { TemplateResult, html } from "lit"; + +import "../ak-radio-input"; +import "./ak-hint"; +import AkHint from "./ak-hint"; +import "./ak-hint-body"; +import "./ak-hint-title"; + +const metadata: Meta = { + title: "Components / Patternfly Hint", + component: "ak-hint", + parameters: { + docs: { + description: { + component: "A stylized hint box", + }, + }, + }, +}; + +export default metadata; + +const container = (testItem: TemplateResult) => + html`
+ + + ${testItem} + +
    +
    `; + +export const Default = () => { + return container( + html`
    + + +

    + Authentik has a new Application Wizard that can configure both an + application and its authentication provider at the same time. + Learn more about the wizard here. +

    + { + showMessage({ + message: "This would have shown the wizard", + level: MessageLevel.success, + }); + }} + >Create with Wizard
    +
    +
    `, + ); +}; + +export const WithTitle = () => { + return container( + html`
    + + New Application Wizard + +

    + Authentik has a new Application Wizard that can configure both an + application and its authentication provider at the same time. + Learn more about the wizard here. +

    + { + showMessage({ + message: "This would have shown the wizard", + level: MessageLevel.success, + }); + }} + >Create with Wizard
    +
    +
    `, + ); +}; + +export const WithTitleAndFooter = () => { + return container( + html`
    + + New Application Wizard + +

    + Authentik has a new Application Wizard that can configure both an + application and its authentication provider at the same time. + Learn more about the wizard here. +

    + { + showMessage({ + message: "This would have shown the wizard", + level: MessageLevel.success, + }); + }} + >Create with Wizard
    +
    + Don't show this message again. +
    +
    +
    `, + ); +}; diff --git a/web/src/components/ak-hint/ak-hint.ts b/web/src/components/ak-hint/ak-hint.ts new file mode 100644 index 000000000..731dbe0ca --- /dev/null +++ b/web/src/components/ak-hint/ak-hint.ts @@ -0,0 +1,72 @@ +import { AKElement } from "@goauthentik/app/elements/Base"; + +import { css, html } from "lit"; +import { customElement } from "lit/decorators.js"; + +const styles = css` + :host { + --ak-hint--GridRowGap: var(--pf-global--spacer--md); + --ak-hint--PaddingTop: var(--pf-global--spacer--md); + --ak-hint--PaddingRight: var(--pf-global--spacer--lg); + --ak-hint--PaddingBottom: var(--pf-global--spacer--md); + --ak-hint--PaddingLeft: var(--pf-global--spacer--lg); + --ak-hint--BackgroundColor: var(--pf-global--palette--blue-50); + --ak-hint--BorderColor: var(--pf-global--palette--blue-100); + --ak-hint--BorderWidth: var(--pf-global--BorderWidth--sm); + --ak-hint--BoxShadow: var(--pf-global--BoxShadow--sm); + --ak-hint--Color: var(--pf-global--Color--100); + + /* Hint Title */ + --ak-hint__title--FontSize: var(--pf-global--FontSize--lg); + + /* Hint Body */ + --ak-hint__body--FontSize: var(--pf-global--FontSize--md); + + /* Hint Footer */ + --ak-hint__footer--child--MarginRight: var(--pf-global--spacer--md); + + /* Hint Actions */ + --ak-hint__actions--MarginLeft: var(--pf-global--spacer--2xl); + --ak-hint__actions--c-dropdown--MarginTop: calc( + var(--pf-global--spacer--form-element) * -1 + ); + } + + :host([theme="dark"]) { + --ak-hint--BackgroundColor: var(--ak-dark-background-darker); + --ak-hint--BorderColor: var(--ak-dark-background-lighter); + --ak-hint--Color: var(--ak-dark-foreground); + } + + div#host { + display: flex; + flex-direction: column; + gap: var(--ak-hint--GridRowGap); + background-color: var(--ak-hint--BackgroundColor); + color: var(--ak-hint--Color); + border: var(--ak-hint--BorderWidth) solid var(--ak-hint--BorderColor); + box-shadow: var(--ak-hint--BoxShadow); + padding: var(--ak-hint--PaddingTop) var(--ak-hint--PaddingRight) + var(--ak-hint--PaddingBottom) var(--ak-hint--PaddingLeft); + } + + ::slotted(ak-hint-title), + ::slotted(ak-hint-body) { + display: grid; + grid-template-columns: 1fr auto; + grid-row-gap: var(--ak-hint--GridRowGap); + } +`; + +@customElement("ak-hint") +export class AkHint extends AKElement { + static get styles() { + return [styles]; + } + + render() { + return html`
    `; + } +} + +export default AkHint;