web: provide storybook demos and docs for existing tests (#11651)
* Added tests and refinements as tests indicate. * Building out the test suite. * web: test the simple things. Fix what the tests revealed. - Move `EmptyState.test.ts` into the `./tests` folder. - Provide unit tests for: - Alert - Divider - Expand - Label - LoadingOverlay - Give all tested items an Interface and a functional variant for rendering - Give Label an alternative syntax for declaring alert levels - Remove the slot name in LoadingOverlay - Change the slot call in `./enterprise/rac/index.ts` to not need the slot name as well - Change the attribute names `topMost`, `textOpen`, and `textClosed` to `topmost`, `text-open`, and `text-closed`, respectively. - Change locations in the code where those are used to correspond ** Why interfaces: ** Provides another check on the input/output boundaries of our elements, gives Storybook and WebdriverIO another validation to check, and guarantees any rendering functions cannot be passed invalid property names. ** Why functions for rendering: ** Providing functions for rendering gets us one step closer to dynamically defining our forms-in-code at runtime without losing any type safety. ** Why rename the attributes: ** A *very* subtle bug: [Element:setAttribute()](https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute) automatically "converts an attribute name to all lower-case when called on an HTML element in an HTML document." The three attributes renamed are all treated *as* attributes, either classic boolean or stringly-typed attributes, and attempting to manipulate them with `setAttribute()` will fail. All of these attributes are presentational; none of them end up in a transaction with the back-end, so kebab-to-camel conversions are not a concern. Also, ["topmost" is one word](https://www.merriam-webster.com/dictionary/topmost). ** Why remove the slot name: ** Because there was only one slot. A name is not needed. * Fix minor spelling error. * First pass at a custom, styled input object. * . * web: Demo the simple things. Fix things the Demo says need fixing. - Move the Element's stories into a `./stories` folder - Provide stories for (these are the same ones "provided tests for" in the [previous PR](https://github.com/goauthentik/authentik/pull/11633)) - Alert - Divider - Expand - Label - LoadingOverlay - Provide Storybook documentation for: - AppIcon - ActionButton - AggregateCard - AggregatePromiseCard - QuickActionsCard - Alert - Divider - EmptyState - Expand - Label - LoadingOverlay - ApplicationEmptyState - Fix a bug in LoadingOverlay; naming error in nested slots caused any message attached to the overlay to not sow up correctly. - Revise AppIcon to be independent of authentik; it just cares if the data has a name or an icon reference, it does not need to know about `Application` objects. As such, it's an *element*, not a *component*, and I've moved it into the right location, and updated the few places it is used to match. * Prettier has opinions with which I sometimes diverge. * Found a bug! Although pf-m-xl was defined as a legal size, there was no code to handle drawing something XL! * Found a few typos and incorrect API descriptions.
This commit is contained in:
		| @ -1,8 +1,7 @@ | ||||
| import "@goauthentik/admin/applications/ApplicationForm"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { PFSize } from "@goauthentik/common/enums.js"; | ||||
| import "@goauthentik/components/ak-app-icon"; | ||||
| import MDApplication from "@goauthentik/docs/add-secure-apps/applications/index.md"; | ||||
| import "@goauthentik/elements/AppIcon.js"; | ||||
| import "@goauthentik/elements/Markdown"; | ||||
| import "@goauthentik/elements/buttons/SpinnerButton"; | ||||
| import "@goauthentik/elements/forms/DeleteBulkForm"; | ||||
| @ -16,6 +15,7 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; | ||||
| import { msg } from "@lit/localize"; | ||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import PFCard from "@patternfly/patternfly/components/Card/card.css"; | ||||
|  | ||||
| @ -122,7 +122,10 @@ export class ApplicationListPage extends TablePage<Application> { | ||||
|  | ||||
|     row(item: Application): TemplateResult[] { | ||||
|         return [ | ||||
|             html`<ak-app-icon size=${PFSize.Medium} .app=${item}></ak-app-icon>`, | ||||
|             html`<ak-app-icon | ||||
|                 name=${item.name} | ||||
|                 icon=${ifDefined(item.metaIcon || undefined)} | ||||
|             ></ak-app-icon>`, | ||||
|             html`<a href="#/core/applications/${item.slug}"> | ||||
|                 <div>${item.name}</div> | ||||
|                 ${item.metaPublisher ? html`<small>${item.metaPublisher}</small>` : html``} | ||||
|  | ||||
| @ -5,8 +5,8 @@ import "@goauthentik/admin/policies/BoundPoliciesList"; | ||||
| import "@goauthentik/admin/rbac/ObjectPermissionsPage"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { PFSize } from "@goauthentik/common/enums.js"; | ||||
| import "@goauthentik/components/ak-app-icon"; | ||||
| import "@goauthentik/components/events/ObjectChangelog"; | ||||
| import "@goauthentik/elements/AppIcon"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/EmptyState"; | ||||
| import "@goauthentik/elements/PageHeader"; | ||||
| @ -102,8 +102,9 @@ export class ApplicationViewPage extends AKElement { | ||||
|             > | ||||
|                 <ak-app-icon | ||||
|                     size=${PFSize.Medium} | ||||
|                     name=${ifDefined(this.application?.name || undefined)} | ||||
|                     icon=${ifDefined(this.application?.metaIcon || undefined)} | ||||
|                     slot="icon" | ||||
|                     .app=${this.application} | ||||
|                 ></ak-app-icon> | ||||
|             </ak-page-header> | ||||
|             ${this.renderApp()}`; | ||||
|  | ||||
| @ -1,13 +1,13 @@ | ||||
| import { applicationListStyle } from "@goauthentik/admin/applications/ApplicationListPage"; | ||||
| import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; | ||||
| import { PFSize } from "@goauthentik/common/enums.js"; | ||||
| import "@goauthentik/components/ak-app-icon"; | ||||
| import "@goauthentik/elements/AppIcon"; | ||||
| import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table"; | ||||
| import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; | ||||
|  | ||||
| import { msg } from "@lit/localize"; | ||||
| import { CSSResult, TemplateResult, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { Application, CoreApi, User } from "@goauthentik/api"; | ||||
|  | ||||
| @ -40,7 +40,10 @@ export class UserApplicationTable extends Table<Application> { | ||||
|  | ||||
|     row(item: Application): TemplateResult[] { | ||||
|         return [ | ||||
|             html`<ak-app-icon size=${PFSize.Medium} .app=${item}></ak-app-icon>`, | ||||
|             html`<ak-app-icon | ||||
|                 name=${item.name} | ||||
|                 icon=${ifDefined(item.metaIcon || undefined)} | ||||
|             ></ak-app-icon>`, | ||||
|             html`<a href="#/core/applications/${item.slug}"> | ||||
|                 <div>${item.name}</div> | ||||
|                 ${item.metaPublisher ? html`<small>${item.metaPublisher}</small>` : html``} | ||||
|  | ||||
| @ -1,38 +0,0 @@ | ||||
| import "@goauthentik/elements/messages/MessageContainer"; | ||||
| import { Meta } from "@storybook/web-components"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
|  | ||||
| import "../ak-app-icon"; | ||||
| import AkAppIcon from "../ak-app-icon"; | ||||
|  | ||||
| const metadata: Meta<AkAppIcon> = { | ||||
|     title: "Components / App Icon", | ||||
|     component: "ak-app-icon", | ||||
|     parameters: { | ||||
|         docs: { | ||||
|             description: { | ||||
|                 component: "A small card displaying an application icon", | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default metadata; | ||||
|  | ||||
| const container = (testItem: TemplateResult) => | ||||
|     html` <div style="background: #000; padding: 2em"> | ||||
|         <style> | ||||
|             li { | ||||
|                 display: block; | ||||
|             } | ||||
|             p { | ||||
|                 margin-top: 1em; | ||||
|             } | ||||
|         </style> | ||||
|         ${testItem} | ||||
|     </div>`; | ||||
|  | ||||
| export const AppIcon = () => { | ||||
|     return container(html`<ak-app-icon .app=${{ name: "Demo app" }} size="pf-m-md"></ak-app-icon>`); | ||||
| }; | ||||
| @ -1,23 +1,30 @@ | ||||
| import { PFSize } from "@goauthentik/common/enums.js"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import { P, match } from "ts-pattern"; | ||||
| 
 | ||||
| import { msg } from "@lit/localize"; | ||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
| 
 | ||||
| import PFFAIcons from "@patternfly/patternfly/base/patternfly-fa-icons.css"; | ||||
| import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css"; | ||||
| 
 | ||||
| import { Application } from "@goauthentik/api"; | ||||
| export interface IAppIcon { | ||||
|     name?: string; | ||||
|     icon?: string; | ||||
|     size?: PFSize; | ||||
| } | ||||
| 
 | ||||
| @customElement("ak-app-icon") | ||||
| export class AppIcon extends AKElement { | ||||
|     @property({ type: Object, attribute: false }) | ||||
|     app?: Application; | ||||
| export class AppIcon extends AKElement implements IAppIcon { | ||||
|     @property({ type: String }) | ||||
|     name?: string; | ||||
| 
 | ||||
|     @property({ type: String }) | ||||
|     icon?: string; | ||||
| 
 | ||||
|     @property() | ||||
|     size?: PFSize; | ||||
|     size: PFSize = PFSize.Medium; | ||||
| 
 | ||||
|     static get styles(): CSSResult[] { | ||||
|         return [ | ||||
| @ -39,6 +46,10 @@ export class AppIcon extends AKElement { | ||||
|                     --icon-height: 1rem; | ||||
|                     --icon-border: 0.125rem; | ||||
|                 } | ||||
|                 :host([size="pf-m-xl"]) { | ||||
|                     --icon-height: 6rem; | ||||
|                     --icon-border: 0.25rem; | ||||
|                 } | ||||
|                 .pf-c-avatar { | ||||
|                     --pf-c-avatar--BorderRadius: 0; | ||||
|                     --pf-c-avatar--Height: calc( | ||||
| @ -64,21 +75,17 @@ export class AppIcon extends AKElement { | ||||
|     } | ||||
| 
 | ||||
|     render(): TemplateResult { | ||||
|         if (!this.app) { | ||||
|             return html`<div><i class="icon fas fa-question-circle"></i></div>`; | ||||
|         } | ||||
|         if (this.app?.metaIcon) { | ||||
|             if (this.app.metaIcon.startsWith("fa://")) { | ||||
|                 const icon = this.app.metaIcon.replaceAll("fa://", ""); | ||||
|                 return html`<div><i class="icon fas ${icon}"></i></div>`; | ||||
|             } | ||||
|             return html`<img
 | ||||
|                 class="icon pf-c-avatar" | ||||
|                 src="${ifDefined(this.app.metaIcon)}" | ||||
|                 alt="${msg("Application Icon")}" | ||||
|             />`; | ||||
|         } | ||||
|         return html`<span class="icon">${this.app?.name.charAt(0).toUpperCase()}</span>`; | ||||
|         // prettier-ignore
 | ||||
|         return match([this.name, this.icon]) | ||||
|             .with([undefined, undefined], | ||||
|                 () => html`<div><i class="icon fas fa-question-circle"></i></div>`) | ||||
|             .with([P._, P.string.startsWith("fa://")], | ||||
|                 ([_name, icon]) => html`<div><i class="icon fas ${icon.replaceAll("fa://", "")}"></i></div>`) | ||||
|             .with([P._, P.string], | ||||
|                 ([_name, icon]) => html`<img class="icon pf-c-avatar" src="${icon}" alt="${msg("Application Icon")}" />`) | ||||
|             .with([P.string, undefined], | ||||
|                 ([name]) => html`<span class="icon">${name.charAt(0).toUpperCase()}</span>`) | ||||
|             .exhaustive(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -8,7 +8,7 @@ import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
|  | ||||
| interface ILoadingOverlay { | ||||
| export interface ILoadingOverlay { | ||||
|     topmost?: boolean; | ||||
| } | ||||
|  | ||||
| @ -41,7 +41,7 @@ export class LoadingOverlay extends AKElement implements ILoadingOverlay { | ||||
|  | ||||
|     render() { | ||||
|         return html`<ak-empty-state loading header=""> | ||||
|             <slot></slot> | ||||
|             <span slot="body"><slot></slot></span> | ||||
|         </ak-empty-state>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,33 @@ | ||||
| import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; | ||||
|  | ||||
| import * as ActionButtonStories from "./ak-action-button.stories"; | ||||
|  | ||||
| <Meta of={ActionButtonStories} /> | ||||
|  | ||||
| # Action Button | ||||
|  | ||||
| An `<ak-action-button>` takes a zero-arity function (a function that takes no argument) that returns | ||||
| a promise. Pressing the button runs the function and the results of the promise drive the behavior | ||||
| of the button. | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| ```Typescript | ||||
| import "@goauthentik/elements/buttons/ActionButton/ak-action-button.js"; | ||||
| ``` | ||||
|  | ||||
| ```html | ||||
| <ak-action-button .apiRequest=${somePromise}">Your message here</ak-action-button> | ||||
| ``` | ||||
|  | ||||
| ## Demo | ||||
|  | ||||
| ### Success: button with "promise revolved" animation | ||||
|  | ||||
| <Story of={ActionButtonStories.ButtonWithSuccess} /> | ||||
|  | ||||
| ### Failure: button with "promise rejected" animation | ||||
|  | ||||
| This shows how the button behaves if the promise rejects. | ||||
|  | ||||
| <Story of={ActionButtonStories.ButtonWithError} /> | ||||
| @ -7,7 +7,7 @@ import "./ak-action-button"; | ||||
| import AKActionButton from "./ak-action-button"; | ||||
|  | ||||
| const metadata: Meta<AKActionButton> = { | ||||
|     title: "Elements / Action Button", | ||||
|     title: "Elements / <ak-action-button>", | ||||
|     component: "ak-action-button", | ||||
|     parameters: { | ||||
|         docs: { | ||||
|  | ||||
							
								
								
									
										24
									
								
								web/src/elements/cards/stories/AggregateCard.docs.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								web/src/elements/cards/stories/AggregateCard.docs.mdx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; | ||||
|  | ||||
| import * as AggregateCardStories from "./AggregateCard.stories"; | ||||
|  | ||||
| <Meta of={AggregateCardStories} /> | ||||
|  | ||||
| # Aggregate Cards | ||||
|  | ||||
| Aggregate Cards are in-page elements to display isolated elements in a consistent, card-like format. | ||||
| Cards are used in dashboards and as asides for specific information. | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| ```Typescript | ||||
| import "@goauthentik/elements/cards/AggregateCard.js"; | ||||
| ``` | ||||
|  | ||||
| ```html | ||||
| <ak-aggregate-card header="Some title"><p>This is the content of your card!</p></ak-aggregate-card> | ||||
| ``` | ||||
|  | ||||
| ## Demo | ||||
|  | ||||
| <Story of={AggregateCardStories.DefaultStory} /> | ||||
							
								
								
									
										35
									
								
								web/src/elements/cards/stories/AggregatePromiseCard.docs.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								web/src/elements/cards/stories/AggregatePromiseCard.docs.mdx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; | ||||
|  | ||||
| import * as AggregatePromiseCardStories from "./AggregatePromiseCard.stories"; | ||||
|  | ||||
| <Meta of={AggregatePromiseCardStories} /> | ||||
|  | ||||
| # Aggregate Promise Cards | ||||
|  | ||||
| Aggregate Promise Cards are Aggregate Cards that take a promise from client code and either display | ||||
| the contents of that promise or a pre-configured failure notice. The contents must be compliant with | ||||
| and produce a meaningful result via the `.toString()` API. HTML in the string will currently be | ||||
| escaped. | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| ```Typescript | ||||
| import "@goauthentik/elements/cards/AggregatePromiseCard.js"; | ||||
| ``` | ||||
|  | ||||
| ```html | ||||
| <ak-aggregate-card-promise | ||||
|     header="Some title" | ||||
|     .promise="${somePromise}" | ||||
| ></ak-aggregate-card-promise> | ||||
| ``` | ||||
|  | ||||
| ## Demo | ||||
|  | ||||
| ### Success | ||||
|  | ||||
| <Story of={AggregatePromiseCardStories.DefaultStory} /> | ||||
|  | ||||
| ### Failure | ||||
|  | ||||
| <Story of={AggregatePromiseCardStories.PromiseRejected} /> | ||||
							
								
								
									
										36
									
								
								web/src/elements/cards/stories/QuickActionsCard.docs.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								web/src/elements/cards/stories/QuickActionsCard.docs.mdx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | ||||
| import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; | ||||
|  | ||||
| import * as QuickActionsCardStories from "./QuickActionsCard.stories"; | ||||
|  | ||||
| <Meta of={QuickActionsCardStories} /> | ||||
|  | ||||
| # Quick Action Cards | ||||
|  | ||||
| A Quick Action Card displays a list of navigation links. It is used on our dashboards to provide | ||||
| easy access to basic operations implied by the dashboard. The example here is from the home page | ||||
| dashboard. | ||||
|  | ||||
| The QuickAction type has three fields: the string to display, the URL to navigate to, and a flag | ||||
| indicating if the browser should open the link in a new tab. | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| ```Typescript | ||||
| import "@goauthentik/elements/cards/QuickActionsCard.js"; | ||||
|  | ||||
| const ACTIONS: QuickAction[] = [ | ||||
|     ["Create a new application", "/core/applications"], | ||||
|     ["Check the logs", "/events/log"], | ||||
|     ["Explore integrations", "https://goauthentik.io/integrations/", true], | ||||
|     ["Manage users", "/identity/users"], | ||||
|     ["Check the release notes", "https://goauthentik.io/docs/releases/", true], | ||||
| ]; | ||||
| ``` | ||||
|  | ||||
| ```html | ||||
| <ak-quick-actions-card title="Some title" .actions=${ACTIONS}></ak-aggregate-card> | ||||
| ``` | ||||
|  | ||||
| ## Demo | ||||
|  | ||||
| <Story of={QuickActionsCardStories.DefaultStory} /> | ||||
							
								
								
									
										46
									
								
								web/src/elements/stories/Alert.docs.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								web/src/elements/stories/Alert.docs.mdx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; | ||||
|  | ||||
| import * as AlertStories from "./Alert.stories"; | ||||
|  | ||||
| <Meta of={AlertStories} /> | ||||
|  | ||||
| # Alerts | ||||
|  | ||||
| Alerts are in-page elements intended to draw the user's attention and alert them to important | ||||
| details. Alerts are used alongside form elements to warn users of potential mistakes they can | ||||
| make, as well as in in-line documentation. | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| ```Typescript | ||||
| import "@goauthentik/elements/Alert.js"; | ||||
| ``` | ||||
|  | ||||
| Note that the content of an alert _must_ be a valid HTML component; plain text does not work here. | ||||
|  | ||||
| ```html | ||||
| <ak-alert><p>This is the content of your alert!</p></ak-alert> | ||||
| ``` | ||||
|  | ||||
| ## Demo | ||||
|  | ||||
| ### Default | ||||
|  | ||||
| The default state of an alert is _warning_. | ||||
|  | ||||
| <Story of={AlertStories.DefaultStory} /> | ||||
|  | ||||
| ### Info | ||||
|  | ||||
| The icon can be changed via the `icon` attribute. It takes the name of a valid `fas`-class Font | ||||
| Awesome icon. Changing the icon can be helpful, as not everyone can see the color changes. | ||||
|  | ||||
| <Story of={AlertStories.InfoAlert} /> | ||||
|  | ||||
| ### Success | ||||
|  | ||||
| <Story of={AlertStories.SuccessAlert} /> | ||||
|  | ||||
| ### Danger | ||||
|  | ||||
| <Story of={AlertStories.DangerAlert} /> | ||||
							
								
								
									
										69
									
								
								web/src/elements/stories/Alert.stories.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								web/src/elements/stories/Alert.stories.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | ||||
| import type { Meta, StoryObj } from "@storybook/web-components"; | ||||
|  | ||||
| import { html } from "lit"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { Alert, type IAlert } from "../Alert.js"; | ||||
| import "../Alert.js"; | ||||
|  | ||||
| type IAlertForTesting = IAlert & { message: string }; | ||||
|  | ||||
| const metadata: Meta<Alert> = { | ||||
|     title: "Elements/<ak-alert>", | ||||
|     component: "ak-alert", | ||||
|     parameters: { | ||||
|         docs: { | ||||
|             description: "An alert", | ||||
|         }, | ||||
|     }, | ||||
|     argTypes: { | ||||
|         inline: { control: "boolean" }, | ||||
|         level: { control: "text" }, | ||||
|         icon: { control: "text" }, | ||||
|         // @ts-ignore | ||||
|         message: { control: "text" }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default metadata; | ||||
|  | ||||
| export const DefaultStory: StoryObj = { | ||||
|     args: { | ||||
|         inline: false, | ||||
|         message: "You should be alarmed.", | ||||
|     }, | ||||
|  | ||||
|     // @ts-ignore | ||||
|     render: ({ inline, level, icon, message }: IAlertForTesting) => { | ||||
|         return html` <div style="background-color: #f0f0f0; padding: 1rem;"> | ||||
|             <style> | ||||
|                 ak-alert { | ||||
|                     display: inline-block; | ||||
|                     width: 32rem; | ||||
|                     max-width: 32rem; | ||||
|                 } | ||||
|             </style> | ||||
|             <ak-alert level=${ifDefined(level)} ?inline=${inline} icon=${ifDefined(icon)}> | ||||
|                 <p>${message}</p> | ||||
|             </ak-alert> | ||||
|         </div>`; | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export const SuccessAlert = { | ||||
|     ...DefaultStory, | ||||
|     args: { ...DefaultStory, ...{ level: "success", message: "He's a tribute to your genius!" } }, | ||||
| }; | ||||
|  | ||||
| export const InfoAlert = { | ||||
|     ...DefaultStory, | ||||
|     args: { | ||||
|         ...DefaultStory, | ||||
|         ...{ level: "info", icon: "fa-coffee", message: "It is time for coffee." }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export const DangerAlert = { | ||||
|     ...DefaultStory, | ||||
|     args: { ...DefaultStory, ...{ level: "danger", message: "Danger, Will Robinson!  Danger!" } }, | ||||
| }; | ||||
							
								
								
									
										46
									
								
								web/src/elements/stories/AppIcon.docs.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								web/src/elements/stories/AppIcon.docs.mdx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; | ||||
|  | ||||
| import * as AppIconStories from "./AppIcon.stories"; | ||||
|  | ||||
| <Meta of={AppIconStories} /> | ||||
|  | ||||
| # Application Icon | ||||
|  | ||||
| AppIcon displays an icon associated with an authentik application on the User Library page. It takes | ||||
| an API "Application" object and a size, with a default size of "medium." | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| Note that the variables passed in are how they are used in authentik. Any string and any FontAwesome | ||||
| icon supported by the current theme can be referenced. | ||||
|  | ||||
| ```Typescript | ||||
| import "@goauthentik/components/ak-app-icon.js"; | ||||
| ``` | ||||
|  | ||||
| ```html | ||||
| <ak-app-icon name=${app.name} icon=${app.metaIcon}></ak-ak-app-icon> | ||||
| ``` | ||||
|  | ||||
| ## Demo | ||||
|  | ||||
| ### Standard App Icon | ||||
|  | ||||
| In this example, the app has no icon reference and is just named "Default." The first letter is used | ||||
| as the icon. | ||||
|  | ||||
| <Story of={AppIconStories.DefaultStory} /> | ||||
|  | ||||
| ### App Icon with Icon | ||||
|  | ||||
| In this example, the app contains an icon reference: `{ metaIcon: "fa://fa-yin-yang" }`, which is | ||||
| preferred to just using the first letter. | ||||
|  | ||||
| <Story of={AppIconStories.WithIcon} /> | ||||
|  | ||||
| ### App Icon with Missing Data | ||||
|  | ||||
| This is what is shown if both the name and icon fields of an application are `undefined`. In practice, | ||||
| you should never see this. | ||||
|  | ||||
| <Story of={AppIconStories.AllDataUndefined} /> | ||||
							
								
								
									
										83
									
								
								web/src/elements/stories/AppIcon.stories.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								web/src/elements/stories/AppIcon.stories.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | ||||
| import { PFSize } from "@goauthentik/common/enums.js"; | ||||
| import type { Meta, StoryObj } from "@storybook/web-components"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import "../AppIcon"; | ||||
| import { AppIcon } from "../AppIcon"; | ||||
|  | ||||
| const sizeOptions = Array.from(Object.values(PFSize)); | ||||
|  | ||||
| const metadata: Meta<AppIcon> = { | ||||
|     title: "Elements / <ak-app-icon>", | ||||
|     component: "ak-app-icon", | ||||
|     parameters: { | ||||
|         docs: { | ||||
|             description: { | ||||
|                 component: "A small card displaying an application icon", | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
|     argTypes: { | ||||
|         name: { control: "text" }, | ||||
|         icon: { control: "text" }, | ||||
|         size: { options: sizeOptions, control: "select" }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default metadata; | ||||
|  | ||||
| const container = (testItem: TemplateResult) => | ||||
|     html` <div style="background: #f0f0f0; padding: 1em"> | ||||
|         <style> | ||||
|             li { | ||||
|                 display: block; | ||||
|             } | ||||
|             p { | ||||
|                 margin-top: 1em; | ||||
|             } | ||||
|         </style> | ||||
|         ${testItem} | ||||
|     </div>`; | ||||
|  | ||||
| export const DefaultStory: StoryObj = { | ||||
|     args: { | ||||
|         name: "Demo App", | ||||
|     }, | ||||
|     render: ({ name, icon, size }) => | ||||
|         container( | ||||
|             html`<ak-app-icon | ||||
|                 size=${size} | ||||
|                 name=${ifDefined(name)} | ||||
|                 icon=${ifDefined(icon)} | ||||
|             ></ak-app-icon>`, | ||||
|         ), | ||||
| }; | ||||
|  | ||||
| export const WithIcon: StoryObj = { | ||||
|     args: { | ||||
|         name: "Iconic App", | ||||
|         icon: "fa://fa-yin-yang", | ||||
|     }, | ||||
|     render: ({ name, icon, size }) => | ||||
|         container( | ||||
|             html`<ak-app-icon | ||||
|                 size=${size} | ||||
|                 name=${ifDefined(name)} | ||||
|                 icon=${ifDefined(icon || undefined)} | ||||
|             ></ak-app-icon>`, | ||||
|         ), | ||||
| }; | ||||
|  | ||||
| export const AllDataUndefined: StoryObj = { | ||||
|     args: {}, | ||||
|     render: ({ name, icon, size }) => | ||||
|         container( | ||||
|             html`<ak-app-icon | ||||
|                 size=${size} | ||||
|                 name=${ifDefined(name)} | ||||
|                 icon=${ifDefined(icon)} | ||||
|             ></ak-app-icon>`, | ||||
|         ), | ||||
| }; | ||||
							
								
								
									
										51
									
								
								web/src/elements/stories/Divider.docs.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								web/src/elements/stories/Divider.docs.mdx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | ||||
| import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; | ||||
|  | ||||
| import * as DividerStories from "./Divider.stories"; | ||||
|  | ||||
| <Meta of={DividerStories} /> | ||||
|  | ||||
| # Divider | ||||
|  | ||||
| Divider is a horizontal rule, an in-page element to separate displayed items. | ||||
|  | ||||
| It has no configurable attributes. It does have a single unnamed slot, which is displayed in-line in | ||||
| the center of the rule. If the CSS Base in loaded into the parent context, icons defined in the base | ||||
| can be used here. | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| ```Typescript | ||||
| import "@goauthentik/elements/Divider.js"; | ||||
| ``` | ||||
|  | ||||
| ```html | ||||
| <ak-divider></ak-divider> | ||||
| ``` | ||||
|  | ||||
| With content: | ||||
|  | ||||
| ```html | ||||
| <ak-divider><p>Time for bed!</p></ak-divider> | ||||
| ``` | ||||
|  | ||||
| With an icon: | ||||
|  | ||||
| ```html | ||||
| <ak-divider><i class="fa fa-bed"></i></ak-divider> | ||||
| ``` | ||||
|  | ||||
| ## Demo | ||||
|  | ||||
| Note that the Divider inherits its background from its parent component. | ||||
|  | ||||
| ### Default Horizontal Rule | ||||
|  | ||||
| <Story of={DividerStories.DefaultStory} /> | ||||
|  | ||||
| ### With A Message | ||||
|  | ||||
| <Story of={DividerStories.DividerWithSlottedContent} /> | ||||
|  | ||||
| ### With an Icon | ||||
|  | ||||
| <Story of={DividerStories.DividerWithSlottedIcon} /> | ||||
							
								
								
									
										41
									
								
								web/src/elements/stories/Divider.stories.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								web/src/elements/stories/Divider.stories.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| import type { Meta, StoryObj } from "@storybook/web-components"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
|  | ||||
| import { Divider } from "../Divider.js"; | ||||
| import "../Divider.js"; | ||||
|  | ||||
| const metadata: Meta<Divider> = { | ||||
|     title: "Elements/<ak-divider>", | ||||
|     component: "ak-divider", | ||||
|     parameters: { | ||||
|         docs: { | ||||
|             description: "our most simple divider", | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default metadata; | ||||
|  | ||||
| const container = (content: TemplateResult) => | ||||
|     html` <div style="background-color: #f0f0f0; padding: 1rem;"> | ||||
|         <style> | ||||
|             ak-divider { | ||||
|                 display: inline-block; | ||||
|                 width: 32rem; | ||||
|                 max-width: 32rem; | ||||
|             }</style | ||||
|         >${content} | ||||
|     </div>`; | ||||
|  | ||||
| export const DefaultStory: StoryObj = { | ||||
|     render: () => container(html` <ak-divider> </ak-divider> `), | ||||
| }; | ||||
|  | ||||
| export const DividerWithSlottedContent: StoryObj = { | ||||
|     render: () => container(html` <ak-divider><p>Time for bed!</p></ak-divider> `), | ||||
| }; | ||||
|  | ||||
| export const DividerWithSlottedIcon: StoryObj = { | ||||
|     render: () => container(html` <ak-divider><i class="fa fa-bed"></i></ak-divider> `), | ||||
| }; | ||||
							
								
								
									
										59
									
								
								web/src/elements/stories/EmptyState.docs.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								web/src/elements/stories/EmptyState.docs.mdx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | ||||
| import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; | ||||
|  | ||||
| import * as EmptyStateStories from "./EmptyState.stories"; | ||||
|  | ||||
| <Meta of={EmptyStateStories} /> | ||||
|  | ||||
| # EmptyState | ||||
|  | ||||
| The EmptyState is an in-page element to indicate that something is either loading or unavailable. | ||||
| When "loading" is true it displays a spinner, otherwise it displays a static icon. The default | ||||
| icon is a question mark in a circle. | ||||
|  | ||||
| It has two named slots, `body` and `primary`, to communicate further details about the current state | ||||
| this element is meant to display. | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| ```Typescript | ||||
| import "@goauthentik/elements/EmptyState.js"; | ||||
| ``` | ||||
|  | ||||
| Note that the content of an alert _must_ be a valid HTML component; plain text does not work here. | ||||
|  | ||||
| ```html | ||||
| <ak-empty-state icon="fa-eject" | ||||
|     ><span slot="primary">This would display in the "primary" slot</span></ak-empty-state | ||||
| > | ||||
| ``` | ||||
|  | ||||
| ## Demo | ||||
|  | ||||
| ### Default: Loading | ||||
|  | ||||
| The default state is _loading_ | ||||
|  | ||||
| <Story of={EmptyStateStories.DefaultStory} /> | ||||
|  | ||||
| ### Done | ||||
|  | ||||
| <Story of={EmptyStateStories.DefaultAndLoadingDone} /> | ||||
|  | ||||
| ### Alternative "Done" Icon | ||||
|  | ||||
| This also shows the "header" attribute filled, which is rendered in a large, dark typeface. | ||||
|  | ||||
| <Story of={EmptyStateStories.DoneWithAlternativeIcon} /> | ||||
|  | ||||
| ### The Body Slot Filled | ||||
|  | ||||
| The body content slot is rendered in a lighter typeface at default size. | ||||
|  | ||||
| <Story of={EmptyStateStories.WithBodySlotFilled} /> | ||||
|  | ||||
| ### The Body and Primary Slot Filled | ||||
|  | ||||
| The primary content is rendered in the normal dark typeface at default size. It is also spaced | ||||
| significantly below the spinner itself. | ||||
|  | ||||
| <Story of={EmptyStateStories.WithBodyAndPrimarySlotsFilled} /> | ||||
							
								
								
									
										108
									
								
								web/src/elements/stories/EmptyState.stories.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								web/src/elements/stories/EmptyState.stories.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,108 @@ | ||||
| import type { Meta, StoryObj } from "@storybook/web-components"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { EmptyState, type IEmptyState } from "../EmptyState.js"; | ||||
| import "../EmptyState.js"; | ||||
|  | ||||
| const metadata: Meta<EmptyState> = { | ||||
|     title: "Elements/<ak-empty-state>", | ||||
|     component: "ak-empty-state", | ||||
|     parameters: { | ||||
|         docs: { | ||||
|             description: "Our empty state spinner", | ||||
|         }, | ||||
|     }, | ||||
|     argTypes: { | ||||
|         icon: { control: "text" }, | ||||
|         loading: { control: "boolean" }, | ||||
|         fullHeight: { control: "boolean" }, | ||||
|         header: { control: "text" }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default metadata; | ||||
|  | ||||
| const container = (content: TemplateResult) => | ||||
|     html` <div style="background-color: #f0f0f0; padding: 1rem;"> | ||||
|         <style> | ||||
|             ak-divider { | ||||
|                 display: inline-block; | ||||
|                 width: 32rem; | ||||
|                 max-width: 32rem; | ||||
|             }</style | ||||
|         >${content} | ||||
|     </div>`; | ||||
|  | ||||
| export const DefaultStory: StoryObj = { | ||||
|     args: { | ||||
|         icon: undefined, | ||||
|         loading: true, | ||||
|         fullHeight: false, | ||||
|         header: undefined, | ||||
|     }, | ||||
|  | ||||
|     render: ({ icon, loading, fullHeight, header }: IEmptyState) => | ||||
|         container( | ||||
|             html` <ak-empty-state | ||||
|                 ?loading=${loading} | ||||
|                 ?fullHeight=${fullHeight} | ||||
|                 icon=${ifDefined(icon)} | ||||
|                 header=${ifDefined(header)} | ||||
|             > | ||||
|             </ak-empty-state>`, | ||||
|         ), | ||||
| }; | ||||
|  | ||||
| export const DefaultAndLoadingDone = { | ||||
|     ...DefaultStory, | ||||
|     args: { ...DefaultStory, ...{ loading: false } }, | ||||
| }; | ||||
|  | ||||
| export const DoneWithAlternativeIcon = { | ||||
|     ...DefaultStory, | ||||
|     args: { | ||||
|         ...DefaultStory, | ||||
|         ...{ loading: false, icon: "fa-space-shuttle", header: "The final frontier" }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export const WithBodySlotFilled = { | ||||
|     ...DefaultStory, | ||||
|     args: { | ||||
|         ...DefaultStory, | ||||
|         ...{ loading: false, icon: "fa-space-shuttle", header: "The final frontier" }, | ||||
|     }, | ||||
|     render: ({ icon, loading, fullHeight, header }: IEmptyState) => | ||||
|         container(html` | ||||
|             <ak-empty-state | ||||
|                 ?loading=${loading} | ||||
|                 ?fullHeight=${fullHeight} | ||||
|                 icon=${ifDefined(icon)} | ||||
|                 header=${ifDefined(header)} | ||||
|             > | ||||
|                 <span slot="body">This is the body content</span> | ||||
|             </ak-empty-state> | ||||
|         `), | ||||
| }; | ||||
|  | ||||
| export const WithBodyAndPrimarySlotsFilled = { | ||||
|     ...DefaultStory, | ||||
|     args: { | ||||
|         ...DefaultStory, | ||||
|         ...{ loading: false, icon: "fa-space-shuttle", header: "The final frontier" }, | ||||
|     }, | ||||
|     render: ({ icon, loading, fullHeight, header }: IEmptyState) => | ||||
|         container( | ||||
|             html` <ak-empty-state | ||||
|                 ?loading=${loading} | ||||
|                 ?fullHeight=${fullHeight} | ||||
|                 icon=${ifDefined(icon)} | ||||
|                 header=${ifDefined(header)} | ||||
|             > | ||||
|                 <span slot="body">This is the body content slot</span> | ||||
|                 <span slot="primary">This is the primary content slot</span> | ||||
|             </ak-empty-state>`, | ||||
|         ), | ||||
| }; | ||||
							
								
								
									
										38
									
								
								web/src/elements/stories/Expand.docs.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								web/src/elements/stories/Expand.docs.mdx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | ||||
| import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; | ||||
|  | ||||
| import * as ExpandStories from "./Expand.stories"; | ||||
|  | ||||
| <Meta of={ExpandStories} /> | ||||
|  | ||||
| # Expand | ||||
|  | ||||
| Expand is an in-page element used to hide cluttering details that a user may wish to reveal, such as raw | ||||
| details of an alert or event. | ||||
|  | ||||
| It has one unnamed slot for the content to be displayed. | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| ```Typescript | ||||
| import "@goauthentik/elements/Expand.js"; | ||||
| ``` | ||||
|  | ||||
| ```html | ||||
| <ak-expand><p>Your primary content goes here</p></ak-expand> | ||||
| ``` | ||||
|  | ||||
| To show the expanded content on initial render: | ||||
|  | ||||
| ```html | ||||
| <ak-expand expanded><p>Your primary content goes here</p></ak-expand> | ||||
| ``` | ||||
|  | ||||
| ## Demo | ||||
|  | ||||
| ### Default: The content is hidden | ||||
|  | ||||
| <Story of={ExpandStories.DefaultStory} /> | ||||
|  | ||||
| ### Expanded | ||||
|  | ||||
| <Story of={ExpandStories.Expanded} /> | ||||
							
								
								
									
										60
									
								
								web/src/elements/stories/Expand.stories.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								web/src/elements/stories/Expand.stories.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | ||||
| import type { Meta, StoryObj } from "@storybook/web-components"; | ||||
|  | ||||
| import { TemplateResult, html } from "lit"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { Expand, type IExpand } from "../Expand.js"; | ||||
| import "../Expand.js"; | ||||
|  | ||||
| const metadata: Meta<Expand> = { | ||||
|     title: "Elements/<ak-expand>", | ||||
|     component: "ak-expand", | ||||
|     parameters: { | ||||
|         docs: { | ||||
|             description: "Our accordion component", | ||||
|         }, | ||||
|     }, | ||||
|     argTypes: { | ||||
|         expanded: { control: "boolean" }, | ||||
|         textOpen: { control: "text" }, | ||||
|         textClosed: { control: "text" }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default metadata; | ||||
|  | ||||
| const container = (content: TemplateResult) => | ||||
|     html` <div style="background-color: #f0f0f0; padding: 1rem;"> | ||||
|         <style> | ||||
|             ak-divider { | ||||
|                 display: inline-block; | ||||
|                 width: 32rem; | ||||
|                 max-width: 32rem; | ||||
|             }</style | ||||
|         >${content} | ||||
|     </div>`; | ||||
|  | ||||
| export const DefaultStory: StoryObj = { | ||||
|     args: { | ||||
|         expanded: false, | ||||
|         textOpen: undefined, | ||||
|         textClosed: undefined, | ||||
|     }, | ||||
|  | ||||
|     render: ({ expanded, textOpen, textClosed }: IExpand) => | ||||
|         container( | ||||
|             html` <ak-expand | ||||
|                 ?expanded=${expanded} | ||||
|                 textOpen=${ifDefined(textOpen)} | ||||
|                 textClosed=${ifDefined(textClosed)} | ||||
|                 ><div> | ||||
|                     <p>Μήτ᾽ ἔμοι μέλι μήτε μέλισσα</p> | ||||
|                     <p>"Neither the bee nor the honey for me." - Sappho, 600 BC</p> | ||||
|                 </div> | ||||
|             </ak-expand>`, | ||||
|         ), | ||||
| }; | ||||
| export const Expanded = { | ||||
|     ...DefaultStory, | ||||
|     args: { ...DefaultStory, ...{ expanded: true } }, | ||||
| }; | ||||
							
								
								
									
										53
									
								
								web/src/elements/stories/Label.docs.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								web/src/elements/stories/Label.docs.mdx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,53 @@ | ||||
| import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; | ||||
|  | ||||
| import * as LabelStories from "./Label.stories"; | ||||
|  | ||||
| <Meta of={LabelStories} /> | ||||
|  | ||||
| # Labels | ||||
|  | ||||
| Labels are in-page elements that provide pointers or guidance. Frequently called "chips" in other | ||||
| design systems. Labels are used alongside other elements to warn users of conditions or status that | ||||
| they might want to be aware of | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| ```Typescript | ||||
| import "@goauthentik/elements/Label.js"; | ||||
| ``` | ||||
|  | ||||
| Note that the content of a label _must_ be a valid HTML component; plain text does not work here. The | ||||
| default label is informational: | ||||
|  | ||||
| ```html | ||||
| <ak-label><p>This is the content of your alert!</p></ak-label> | ||||
| ``` | ||||
|  | ||||
| ## Demo | ||||
|  | ||||
| ### Default: Info | ||||
|  | ||||
| The default state of an alert is _info_. | ||||
|  | ||||
| <Story of={LabelStories.DefaultStory} /> | ||||
|  | ||||
| ### Warning | ||||
|  | ||||
| The icon can be changed via the `icon` attribute. It takes the name of a valid `fas`-class Font | ||||
| Awesome icon. Changing the icon can be helpful, as not everyone can see the color changes. It has | ||||
| also been set "compact" to show the difference. | ||||
|  | ||||
| <Story of={LabelStories.CompactWarningLabel} /> | ||||
|  | ||||
| ### Success | ||||
|  | ||||
| This label is illustrated using the PFColor enum, rather than the attribute name. Note that the content | ||||
| is _slotted_, and can be styled. | ||||
|  | ||||
| <Story of={LabelStories.SuccessLabel} /> | ||||
|  | ||||
| ### Danger | ||||
|  | ||||
| This label is illustrated using the PFColor enum, rather than the attribute name. | ||||
|  | ||||
| <Story of={LabelStories.DangerLabel} /> | ||||
							
								
								
									
										83
									
								
								web/src/elements/stories/Label.stories.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								web/src/elements/stories/Label.stories.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | ||||
| import type { Meta, StoryObj } from "@storybook/web-components"; | ||||
|  | ||||
| import { html } from "lit"; | ||||
| import { ifDefined } from "lit/directives/if-defined.js"; | ||||
|  | ||||
| import { type ILabel, Label, PFColor } from "../Label.js"; | ||||
| import "../Label.js"; | ||||
|  | ||||
| type ILabelForTesting = ILabel & { message: string }; | ||||
|  | ||||
| const metadata: Meta<Label> = { | ||||
|     title: "Elements/<ak-label>", | ||||
|     component: "ak-label", | ||||
|     parameters: { | ||||
|         docs: { | ||||
|             description: "An alert", | ||||
|         }, | ||||
|     }, | ||||
|     argTypes: { | ||||
|         compact: { control: "boolean" }, | ||||
|         color: { control: "text" }, | ||||
|         icon: { control: "text" }, | ||||
|         // @ts-ignore | ||||
|         message: { control: "text" }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default metadata; | ||||
|  | ||||
| export const DefaultStory: StoryObj = { | ||||
|     args: { | ||||
|         compact: false, | ||||
|         message: "Eat at Joe's.", | ||||
|     }, | ||||
|  | ||||
|     // @ts-ignore | ||||
|     render: ({ compact, color, icon, message }: ILabelForTesting) => { | ||||
|         return html` <div style="background-color: #f0f0f0; padding: 1rem;"> | ||||
|             <style> | ||||
|                 ak-label { | ||||
|                     display: inline-block; | ||||
|                     width: 48rem; | ||||
|                     max-width: 48rem; | ||||
|                 } | ||||
|             </style> | ||||
|             <ak-label color=${ifDefined(color)} ?compact=${compact} icon=${ifDefined(icon)}> | ||||
|                 <p>${message}</p> | ||||
|             </ak-label> | ||||
|         </div>`; | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export const SuccessLabel = { | ||||
|     ...DefaultStory, | ||||
|     args: { | ||||
|         ...DefaultStory, | ||||
|         ...{ | ||||
|             color: PFColor.Green, | ||||
|             message: html`I'll show them! I'll show them <i>all</i> ! Mwahahahahaha!`, | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export const CompactWarningLabel = { | ||||
|     ...DefaultStory, | ||||
|     args: { | ||||
|         ...DefaultStory, | ||||
|         ...{ | ||||
|             compact: true, | ||||
|             color: "warning", | ||||
|             icon: "fa-coffee", | ||||
|             message: "It is time for coffee.", | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export const DangerLabel = { | ||||
|     ...DefaultStory, | ||||
|     args: { | ||||
|         ...DefaultStory, | ||||
|         ...{ color: "danger", message: "Grave danger? Is there another kind?" }, | ||||
|     }, | ||||
| }; | ||||
							
								
								
									
										36
									
								
								web/src/elements/stories/LoadingOverlay.docs.mdx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								web/src/elements/stories/LoadingOverlay.docs.mdx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | ||||
| import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; | ||||
|  | ||||
| import * as LoadingOverlayStories from "./LoadingOverlay.stories"; | ||||
|  | ||||
| <Meta of={LoadingOverlayStories} /> | ||||
|  | ||||
| # LoadingOverlay | ||||
|  | ||||
| The LoadingOverlay is meant to cover the container element completely, hiding the content behind a | ||||
| dimming filter, while content loads. | ||||
|  | ||||
| It has a single named slot, "body" into which messages about the loading process can be included. | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| ```Typescript | ||||
| import "@goauthentik/elements/LoadingOverlay.js"; | ||||
| ``` | ||||
|  | ||||
| Note that the content of an alert _must_ be a valid HTML component; plain text does not work here. | ||||
|  | ||||
| ```html | ||||
| <ak-loading-overlay topmost> | ||||
|     <span>This would display below the loading spinner</span> | ||||
| </ak-loading-overlay> | ||||
| ``` | ||||
|  | ||||
| ## Demo | ||||
|  | ||||
| ### Default | ||||
|  | ||||
| <Story of={LoadingOverlayStories.DefaultStory} /> | ||||
|  | ||||
| ### With a message | ||||
|  | ||||
| <Story of={LoadingOverlayStories.WithAMessage} /> | ||||
							
								
								
									
										74
									
								
								web/src/elements/stories/LoadingOverlay.stories.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								web/src/elements/stories/LoadingOverlay.stories.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,74 @@ | ||||
| import type { Meta, StoryObj } from "@storybook/web-components"; | ||||
|  | ||||
| import { LitElement, TemplateResult, css, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import { type ILoadingOverlay, LoadingOverlay } from "../LoadingOverlay.js"; | ||||
| import "../LoadingOverlay.js"; | ||||
|  | ||||
| const metadata: Meta<LoadingOverlay> = { | ||||
|     title: "Elements/<ak-loading-overlay>", | ||||
|     component: "ak-loading-overlay", | ||||
|     parameters: { | ||||
|         docs: { | ||||
|             description: "Our empty state spinner", | ||||
|         }, | ||||
|     }, | ||||
|     argTypes: { | ||||
|         topmost: { control: "boolean" }, | ||||
|         // @ts-ignore | ||||
|         message: { control: "text" }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export default metadata; | ||||
|  | ||||
| @customElement("ak-storybook-demo-container") | ||||
| export class Container extends LitElement { | ||||
|     static get styles() { | ||||
|         return css` | ||||
|             :host { | ||||
|                 display: block; | ||||
|                 position: relative; | ||||
|                 height: 25vh; | ||||
|                 width: 75vw; | ||||
|             } | ||||
|             #main-container { | ||||
|                 position: relative; | ||||
|                 width: 100%; | ||||
|                 height: 100%; | ||||
|             } | ||||
|         `; | ||||
|     } | ||||
|  | ||||
|     @property({ type: Object, attribute: false }) | ||||
|     content!: TemplateResult; | ||||
|  | ||||
|     render() { | ||||
|         return html` <div id="main-container">${this.content}</div>`; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export const DefaultStory: StoryObj = { | ||||
|     args: { | ||||
|         topmost: undefined, | ||||
|         // @ts-ignore | ||||
|         message: undefined, | ||||
|     }, | ||||
|  | ||||
|     // @ts-ignore | ||||
|     render: ({ topmost, message }: ILoadingOverlay) => { | ||||
|         message = typeof message === "string" ? html`<span>${message}</span>` : message; | ||||
|         const content = html` <ak-loading-overlay ?topmost=${topmost} | ||||
|             >${message ?? ""} | ||||
|         </ak-loading-overlay>`; | ||||
|         return html`<ak-storybook-demo-container | ||||
|             .content=${content} | ||||
|         ></ak-storybook-demo-container>`; | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export const WithAMessage: StoryObj = { | ||||
|     ...DefaultStory, | ||||
|     args: { ...DefaultStory.args, message: html`<p>Overlay with a message</p>` }, | ||||
| }; | ||||
| @ -1,6 +1,6 @@ | ||||
| import { PFSize } from "@goauthentik/common/enums.js"; | ||||
| import { truncateWords } from "@goauthentik/common/utils"; | ||||
| import "@goauthentik/components/ak-app-icon"; | ||||
| import "@goauthentik/elements/AppIcon"; | ||||
| import { AKElement, rootInterface } from "@goauthentik/elements/Base"; | ||||
| import "@goauthentik/elements/Expand"; | ||||
| import "@goauthentik/user/LibraryApplication/RACLaunchEndpointModal"; | ||||
| @ -115,7 +115,6 @@ export class LibraryApplication extends AKElement { | ||||
|  | ||||
|         const classes = { "pf-m-selectable": this.selected, "pf-m-selected": this.selected }; | ||||
|         const styles = this.background ? { background: this.background } : {}; | ||||
|  | ||||
|         return html` <div | ||||
|             class="pf-c-card pf-m-hoverable pf-m-compact ${classMap(classes)}" | ||||
|             style=${styleMap(styles)} | ||||
| @ -125,7 +124,11 @@ export class LibraryApplication extends AKElement { | ||||
|                     href="${ifDefined(this.application.launchUrl ?? "")}" | ||||
|                     target="${ifDefined(this.application.openInNewTab ? "_blank" : undefined)}" | ||||
|                 > | ||||
|                     <ak-app-icon size=${PFSize.Large} .app=${this.application}></ak-app-icon> | ||||
|                     <ak-app-icon | ||||
|                         size=${PFSize.Large} | ||||
|                         name=${this.application.name} | ||||
|                         icon=${ifDefined(this.application.metaIcon || undefined)} | ||||
|                     ></ak-app-icon> | ||||
|                 </a> | ||||
|             </div> | ||||
|             <div class="pf-c-card__title">${this.renderLaunch()}</div> | ||||
|  | ||||
| @ -0,0 +1,31 @@ | ||||
| import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; | ||||
|  | ||||
| import * as ApplicationEmptyStateStories from "./ApplicationEmptyState.stories"; | ||||
|  | ||||
| <Meta of={ApplicationEmptyStateStories} /> | ||||
|  | ||||
| # Application List Empty State Indicator | ||||
|  | ||||
| A custom component for informing the user that they have no applications. If the user is | ||||
| an administrator (set via an attribute), a link to the "Create a new application" button | ||||
| will be provided | ||||
|  | ||||
| ## Usage | ||||
|  | ||||
| ```Typescript | ||||
| import "@goauthentik/user/LibraryPage/ApplicationEmptyState.js"; | ||||
| ``` | ||||
|  | ||||
| ```html | ||||
| <ak-library-application-empty-list></ak-library-application-empty-list> | ||||
| ``` | ||||
|  | ||||
| ## Demo | ||||
|  | ||||
| ### What an ordinary user sees | ||||
|  | ||||
| <Story of={ApplicationEmptyStateStories.OrdinaryUser} /> | ||||
|  | ||||
| ### What an Admin sees | ||||
|  | ||||
| <Story of={ApplicationEmptyStateStories.AdminUser} /> | ||||
| @ -1,9 +1,9 @@ | ||||
| import { html } from "lit"; | ||||
| 
 | ||||
| import "./ak-library-application-empty-list"; | ||||
| import "../ak-library-application-empty-list"; | ||||
| 
 | ||||
| export default { | ||||
|     title: "Elements / Application Empty State", | ||||
|     title: "Users / <ak-library-application-empty-list>", | ||||
| }; | ||||
| 
 | ||||
| export const OrdinaryUser = () => | ||||
		Reference in New Issue
	
	Block a user
	 Ken Sternberg
					Ken Sternberg