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:
Ken Sternberg
2024-10-14 10:30:09 -07:00
committed by GitHub
parent 6b79190f6c
commit 0a1d283ac8
29 changed files with 1060 additions and 75 deletions

View File

@ -1,8 +1,7 @@
import "@goauthentik/admin/applications/ApplicationForm"; import "@goauthentik/admin/applications/ApplicationForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; 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 MDApplication from "@goauthentik/docs/add-secure-apps/applications/index.md";
import "@goauthentik/elements/AppIcon.js";
import "@goauthentik/elements/Markdown"; import "@goauthentik/elements/Markdown";
import "@goauthentik/elements/buttons/SpinnerButton"; import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/DeleteBulkForm";
@ -16,6 +15,7 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFCard from "@patternfly/patternfly/components/Card/card.css"; import PFCard from "@patternfly/patternfly/components/Card/card.css";
@ -122,7 +122,10 @@ export class ApplicationListPage extends TablePage<Application> {
row(item: Application): TemplateResult[] { row(item: Application): TemplateResult[] {
return [ 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}"> html`<a href="#/core/applications/${item.slug}">
<div>${item.name}</div> <div>${item.name}</div>
${item.metaPublisher ? html`<small>${item.metaPublisher}</small>` : html``} ${item.metaPublisher ? html`<small>${item.metaPublisher}</small>` : html``}

View File

@ -5,8 +5,8 @@ import "@goauthentik/admin/policies/BoundPoliciesList";
import "@goauthentik/admin/rbac/ObjectPermissionsPage"; import "@goauthentik/admin/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PFSize } from "@goauthentik/common/enums.js"; import { PFSize } from "@goauthentik/common/enums.js";
import "@goauthentik/components/ak-app-icon";
import "@goauthentik/components/events/ObjectChangelog"; import "@goauthentik/components/events/ObjectChangelog";
import "@goauthentik/elements/AppIcon";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/EmptyState"; import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/PageHeader"; import "@goauthentik/elements/PageHeader";
@ -102,8 +102,9 @@ export class ApplicationViewPage extends AKElement {
> >
<ak-app-icon <ak-app-icon
size=${PFSize.Medium} size=${PFSize.Medium}
name=${ifDefined(this.application?.name || undefined)}
icon=${ifDefined(this.application?.metaIcon || undefined)}
slot="icon" slot="icon"
.app=${this.application}
></ak-app-icon> ></ak-app-icon>
</ak-page-header> </ak-page-header>
${this.renderApp()}`; ${this.renderApp()}`;

View File

@ -1,13 +1,13 @@
import { applicationListStyle } from "@goauthentik/admin/applications/ApplicationListPage"; import { applicationListStyle } from "@goauthentik/admin/applications/ApplicationListPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PFSize } from "@goauthentik/common/enums.js"; import "@goauthentik/elements/AppIcon";
import "@goauthentik/components/ak-app-icon";
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table"; import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit"; import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { Application, CoreApi, User } from "@goauthentik/api"; import { Application, CoreApi, User } from "@goauthentik/api";
@ -40,7 +40,10 @@ export class UserApplicationTable extends Table<Application> {
row(item: Application): TemplateResult[] { row(item: Application): TemplateResult[] {
return [ 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}"> html`<a href="#/core/applications/${item.slug}">
<div>${item.name}</div> <div>${item.name}</div>
${item.metaPublisher ? html`<small>${item.metaPublisher}</small>` : html``} ${item.metaPublisher ? html`<small>${item.metaPublisher}</small>` : html``}

View File

@ -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>`);
};

View File

@ -1,23 +1,30 @@
import { PFSize } from "@goauthentik/common/enums.js"; import { PFSize } from "@goauthentik/common/enums.js";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { P, match } from "ts-pattern";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js"; 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 PFFAIcons from "@patternfly/patternfly/base/patternfly-fa-icons.css";
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.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") @customElement("ak-app-icon")
export class AppIcon extends AKElement { export class AppIcon extends AKElement implements IAppIcon {
@property({ type: Object, attribute: false }) @property({ type: String })
app?: Application; name?: string;
@property({ type: String })
icon?: string;
@property() @property()
size?: PFSize; size: PFSize = PFSize.Medium;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return [
@ -39,6 +46,10 @@ export class AppIcon extends AKElement {
--icon-height: 1rem; --icon-height: 1rem;
--icon-border: 0.125rem; --icon-border: 0.125rem;
} }
:host([size="pf-m-xl"]) {
--icon-height: 6rem;
--icon-border: 0.25rem;
}
.pf-c-avatar { .pf-c-avatar {
--pf-c-avatar--BorderRadius: 0; --pf-c-avatar--BorderRadius: 0;
--pf-c-avatar--Height: calc( --pf-c-avatar--Height: calc(
@ -64,21 +75,17 @@ export class AppIcon extends AKElement {
} }
render(): TemplateResult { render(): TemplateResult {
if (!this.app) { // prettier-ignore
return html`<div><i class="icon fas fa-question-circle"></i></div>`; return match([this.name, this.icon])
} .with([undefined, undefined],
if (this.app?.metaIcon) { () => html`<div><i class="icon fas fa-question-circle"></i></div>`)
if (this.app.metaIcon.startsWith("fa://")) { .with([P._, P.string.startsWith("fa://")],
const icon = this.app.metaIcon.replaceAll("fa://", ""); ([_name, icon]) => html`<div><i class="icon fas ${icon.replaceAll("fa://", "")}"></i></div>`)
return html`<div><i class="icon fas ${icon}"></i></div>`; .with([P._, P.string],
} ([_name, icon]) => html`<img class="icon pf-c-avatar" src="${icon}" alt="${msg("Application Icon")}" />`)
return html`<img .with([P.string, undefined],
class="icon pf-c-avatar" ([name]) => html`<span class="icon">${name.charAt(0).toUpperCase()}</span>`)
src="${ifDefined(this.app.metaIcon)}" .exhaustive();
alt="${msg("Application Icon")}"
/>`;
}
return html`<span class="icon">${this.app?.name.charAt(0).toUpperCase()}</span>`;
} }
} }

View File

@ -8,7 +8,7 @@ import { customElement, property } from "lit/decorators.js";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
interface ILoadingOverlay { export interface ILoadingOverlay {
topmost?: boolean; topmost?: boolean;
} }
@ -41,7 +41,7 @@ export class LoadingOverlay extends AKElement implements ILoadingOverlay {
render() { render() {
return html`<ak-empty-state loading header=""> return html`<ak-empty-state loading header="">
<slot></slot> <span slot="body"><slot></slot></span>
</ak-empty-state>`; </ak-empty-state>`;
} }
} }

View File

@ -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} />

View File

@ -7,7 +7,7 @@ import "./ak-action-button";
import AKActionButton from "./ak-action-button"; import AKActionButton from "./ak-action-button";
const metadata: Meta<AKActionButton> = { const metadata: Meta<AKActionButton> = {
title: "Elements / Action Button", title: "Elements / <ak-action-button>",
component: "ak-action-button", component: "ak-action-button",
parameters: { parameters: {
docs: { docs: {

View 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} />

View 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} />

View 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} />

View 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} />

View 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!" } },
};

View 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} />

View 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>`,
),
};

View 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} />

View 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> `),
};

View 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} />

View 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>`,
),
};

View 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} />

View 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 } },
};

View 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} />

View 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>&nbsp;! 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?" },
},
};

View 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} />

View 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>` },
};

View File

@ -1,6 +1,6 @@
import { PFSize } from "@goauthentik/common/enums.js"; import { PFSize } from "@goauthentik/common/enums.js";
import { truncateWords } from "@goauthentik/common/utils"; import { truncateWords } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-app-icon"; import "@goauthentik/elements/AppIcon";
import { AKElement, rootInterface } from "@goauthentik/elements/Base"; import { AKElement, rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/Expand"; import "@goauthentik/elements/Expand";
import "@goauthentik/user/LibraryApplication/RACLaunchEndpointModal"; 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 classes = { "pf-m-selectable": this.selected, "pf-m-selected": this.selected };
const styles = this.background ? { background: this.background } : {}; const styles = this.background ? { background: this.background } : {};
return html` <div return html` <div
class="pf-c-card pf-m-hoverable pf-m-compact ${classMap(classes)}" class="pf-c-card pf-m-hoverable pf-m-compact ${classMap(classes)}"
style=${styleMap(styles)} style=${styleMap(styles)}
@ -125,7 +124,11 @@ export class LibraryApplication extends AKElement {
href="${ifDefined(this.application.launchUrl ?? "")}" href="${ifDefined(this.application.launchUrl ?? "")}"
target="${ifDefined(this.application.openInNewTab ? "_blank" : undefined)}" 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> </a>
</div> </div>
<div class="pf-c-card__title">${this.renderLaunch()}</div> <div class="pf-c-card__title">${this.renderLaunch()}</div>

View File

@ -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} />

View File

@ -1,9 +1,9 @@
import { html } from "lit"; import { html } from "lit";
import "./ak-library-application-empty-list"; import "../ak-library-application-empty-list";
export default { export default {
title: "Elements / Application Empty State", title: "Users / <ak-library-application-empty-list>",
}; };
export const OrdinaryUser = () => export const OrdinaryUser = () =>