web: patternfly hints as ak-web-component (#7120)
* web: patternfly hints as ak-web-component Patternfly 5's "Hints" React Component, but ported to web components. The discovery that CSS Custom Properties are still available in child components, even if they're within independent ShadowDOMs, made this fairly easy to port from Handlebars to Lit-HTML. Moving the definitions into `:host` and the applications into the root DIV of the component made duplicating the Patternfly 5 structure straightforward. Despite the [Patternfly Elements]documentation](https://patternflyelements.org/docs/develop/create/), there's a lot to Patternfly Elements that isn't well documented, such as their slot controller, which near as I can tell just makes it easy to determine if a slot with the given name is actually being used by the client code, but it's hard to tell why, other than that it provides an easy way to determine if some CSS should be included. * Pre-commit fixes. * web: fix some issues with styling found while testing. * web: separated the "with Title" and "without Title" stories. * Added footer story, fixed some CSS. * web: hint controller Add the `ShowHintController`. This ReactiveController takes a token in its constructor, and looks in LocalStorage for that token and an associated value. If that value is not `undefined`, it sets the field `this.host.showHint` to the value found. It also provides a `render()` method that provides an `ak-hint-footer` with a checkbox and the "Don't show this message again," and responds to clicks on the checkbox by setting the `this.hint.showHint` and LocalStorage values to "false". An example web component using it has been supplied. * web: support dark mode for hints. This was nifty. Still not entirely sure about the `theme="dark"` rippling through the product, but in this case it works quite well. All it took was defining the alternative dark mode values in a CSS entry, `:host([theme="dark"]) { ... }` and exploiting Patternfly's already intensely atomized CSS Custom Properties properly. * web: revise colors to use more of the Authentik dark-mode style. * Update web/src/components/ak-hint/ak-hint.ts Signed-off-by: Jens L. <jens@beryju.org> * remove any Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens L. <jens@beryju.org> Signed-off-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: Jens L <jens@goauthentik.io>
This commit is contained in:
parent
b503379319
commit
21e5441f92
|
@ -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` <section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<ak-hint>
|
||||
<ak-hint-body>
|
||||
<p>
|
||||
Authentik has a new Application Wizard that can configure both an
|
||||
application and its authentication provider at the same time.
|
||||
<a href="(link to docs)">Learn more about the wizard here.</a>
|
||||
</p>
|
||||
<ak-action-button
|
||||
class="pf-m-secondary"
|
||||
.apiRequest=${() => {
|
||||
showMessage({
|
||||
message: "This would have shown the wizard",
|
||||
level: MessageLevel.success,
|
||||
});
|
||||
}}
|
||||
>Create with Wizard</ak-action-button
|
||||
></ak-hint-body
|
||||
>
|
||||
${this.showHintController.render()}
|
||||
</ak-hint>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.showHint || this.forceHint ? this.renderHint() : nothing;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkApplicationWizardHint;
|
|
@ -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";
|
||||
|
|
|
@ -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<string, unknown> => {
|
||||
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`<ak-hint-footer
|
||||
><div style="text-align: right">
|
||||
<input type="checkbox" @input=${this.hideTheHint} />${msg(
|
||||
"Don't show this message again.",
|
||||
)}
|
||||
</div></ak-hint-footer
|
||||
>`;
|
||||
}
|
||||
}
|
|
@ -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`<div part="ak-hint-actions"><slot></slot></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkHintActions;
|
|
@ -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`<div part="ak-hint-body"><slot></slot></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkHintBody;
|
|
@ -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`<div id="host" part="ak-hint-footer"><slot></slot></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkHintFooter;
|
|
@ -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`<div id="host" part="ak-hint-title"><slot></slot></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkHintTitle;
|
|
@ -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<AkHint> = {
|
||||
title: "Components / Patternfly Hint",
|
||||
component: "ak-hint",
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: "A stylized hint box",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default metadata;
|
||||
|
||||
const container = (testItem: TemplateResult) =>
|
||||
html` <div style="background: #fff; padding: 2em">
|
||||
<style>
|
||||
li {
|
||||
display: block;
|
||||
}
|
||||
p {
|
||||
color: black;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
* {
|
||||
--ak-hint--Color: black !important;
|
||||
}
|
||||
ak-hint-title::part(ak-hint-title),
|
||||
ak-hint-footer::part(ak-hint-footer),
|
||||
slotted::(*) {
|
||||
color: black;
|
||||
}
|
||||
</style>
|
||||
|
||||
${testItem}
|
||||
|
||||
<ul id="radio-message-pad" style="margin-top: 1em"></ul>
|
||||
</div>`;
|
||||
|
||||
export const Default = () => {
|
||||
return container(
|
||||
html` <section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<ak-hint>
|
||||
<ak-hint-body>
|
||||
<p style="padding-bottom: 1rem;">
|
||||
Authentik has a new Application Wizard that can configure both an
|
||||
application and its authentication provider at the same time.
|
||||
<a href="(link to docs)">Learn more about the wizard here.</a>
|
||||
</p>
|
||||
<ak-action-button
|
||||
class="pf-m-secondary"
|
||||
.apiRequest=${() => {
|
||||
showMessage({
|
||||
message: "This would have shown the wizard",
|
||||
level: MessageLevel.success,
|
||||
});
|
||||
}}
|
||||
>Create with Wizard</ak-action-button
|
||||
></ak-hint-body
|
||||
>
|
||||
</ak-hint>
|
||||
</section>`,
|
||||
);
|
||||
};
|
||||
|
||||
export const WithTitle = () => {
|
||||
return container(
|
||||
html` <section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<ak-hint>
|
||||
<ak-hint-title>New Application Wizard</ak-hint-title>
|
||||
<ak-hint-body>
|
||||
<p style="padding-bottom: 1rem;">
|
||||
Authentik has a new Application Wizard that can configure both an
|
||||
application and its authentication provider at the same time.
|
||||
<a href="(link to docs)">Learn more about the wizard here.</a>
|
||||
</p>
|
||||
<ak-action-button
|
||||
class="pf-m-secondary"
|
||||
.apiRequest=${() => {
|
||||
showMessage({
|
||||
message: "This would have shown the wizard",
|
||||
level: MessageLevel.success,
|
||||
});
|
||||
}}
|
||||
>Create with Wizard</ak-action-button
|
||||
></ak-hint-body
|
||||
>
|
||||
</ak-hint>
|
||||
</section>`,
|
||||
);
|
||||
};
|
||||
|
||||
export const WithTitleAndFooter = () => {
|
||||
return container(
|
||||
html` <section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<ak-hint>
|
||||
<ak-hint-title>New Application Wizard</ak-hint-title>
|
||||
<ak-hint-body>
|
||||
<p style="padding-bottom: 1rem;">
|
||||
Authentik has a new Application Wizard that can configure both an
|
||||
application and its authentication provider at the same time.
|
||||
<a href="(link to docs)">Learn more about the wizard here.</a>
|
||||
</p>
|
||||
<ak-action-button
|
||||
class="pf-m-secondary"
|
||||
.apiRequest=${() => {
|
||||
showMessage({
|
||||
message: "This would have shown the wizard",
|
||||
level: MessageLevel.success,
|
||||
});
|
||||
}}
|
||||
>Create with Wizard</ak-action-button
|
||||
></ak-hint-body
|
||||
>
|
||||
<ak-hint-footer
|
||||
><div style="text-align: right">
|
||||
<input type="checkbox" /> Don't show this message again.
|
||||
</div></ak-hint-footer
|
||||
>
|
||||
</ak-hint>
|
||||
</section>`,
|
||||
);
|
||||
};
|
|
@ -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`<div part="ak-hint" id="host"><slot></slot></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default AkHint;
|
Reference in New Issue