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 "@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``}
|
||||||
|
@ -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()}`;
|
||||||
|
@ -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``}
|
||||||
|
@ -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 { 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>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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";
|
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: {
|
||||||
|
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 { 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>
|
||||||
|
@ -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 { 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 = () =>
|
Reference in New Issue
Block a user