Skip to content

Commit

Permalink
web: provide storybook demos and docs for existing tests (#11651)
Browse files Browse the repository at this point in the history
* 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](#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.
  • Loading branch information
kensternberg-authentik authored Oct 14, 2024
1 parent 6b79190 commit 0a1d283
Show file tree
Hide file tree
Showing 29 changed files with 1,060 additions and 75 deletions.
9 changes: 6 additions & 3 deletions web/src/admin/applications/ApplicationListPage.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import "@goauthentik/admin/applications/ApplicationForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PFSize } from "@goauthentik/common/enums.js";
import "@goauthentik/components/ak-app-icon";
import MDApplication from "@goauthentik/docs/add-secure-apps/applications/index.md";
import "@goauthentik/elements/AppIcon.js";
import "@goauthentik/elements/Markdown";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm";
Expand All @@ -16,6 +15,7 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";

import PFCard from "@patternfly/patternfly/components/Card/card.css";

Expand Down Expand Up @@ -122,7 +122,10 @@ export class ApplicationListPage extends TablePage<Application> {

row(item: Application): TemplateResult[] {
return [
html`<ak-app-icon size=${PFSize.Medium} .app=${item}></ak-app-icon>`,
html`<ak-app-icon
name=${item.name}
icon=${ifDefined(item.metaIcon || undefined)}
></ak-app-icon>`,
html`<a href="#/core/applications/${item.slug}">
<div>${item.name}</div>
${item.metaPublisher ? html`<small>${item.metaPublisher}</small>` : html``}
Expand Down
5 changes: 3 additions & 2 deletions web/src/admin/applications/ApplicationViewPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import "@goauthentik/admin/policies/BoundPoliciesList";
import "@goauthentik/admin/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PFSize } from "@goauthentik/common/enums.js";
import "@goauthentik/components/ak-app-icon";
import "@goauthentik/components/events/ObjectChangelog";
import "@goauthentik/elements/AppIcon";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/PageHeader";
Expand Down Expand Up @@ -102,8 +102,9 @@ export class ApplicationViewPage extends AKElement {
>
<ak-app-icon
size=${PFSize.Medium}
name=${ifDefined(this.application?.name || undefined)}
icon=${ifDefined(this.application?.metaIcon || undefined)}
slot="icon"
.app=${this.application}
></ak-app-icon>
</ak-page-header>
${this.renderApp()}`;
Expand Down
9 changes: 6 additions & 3 deletions web/src/admin/users/UserApplicationTable.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { applicationListStyle } from "@goauthentik/admin/applications/ApplicationListPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PFSize } from "@goauthentik/common/enums.js";
import "@goauthentik/components/ak-app-icon";
import "@goauthentik/elements/AppIcon";
import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/table/Table";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";

import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";

import { Application, CoreApi, User } from "@goauthentik/api";

Expand Down Expand Up @@ -40,7 +40,10 @@ export class UserApplicationTable extends Table<Application> {

row(item: Application): TemplateResult[] {
return [
html`<ak-app-icon size=${PFSize.Medium} .app=${item}></ak-app-icon>`,
html`<ak-app-icon
name=${item.name}
icon=${ifDefined(item.metaIcon || undefined)}
></ak-app-icon>`,
html`<a href="#/core/applications/${item.slug}">
<div>${item.name}</div>
${item.metaPublisher ? html`<small>${item.metaPublisher}</small>` : html``}
Expand Down
38 changes: 0 additions & 38 deletions web/src/components/stories/ak-app-icon.stories.ts

This file was deleted.

49 changes: 28 additions & 21 deletions web/src/components/ak-app-icon.ts → web/src/elements/AppIcon.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
import { PFSize } from "@goauthentik/common/enums.js";
import { AKElement } from "@goauthentik/elements/Base";
import { P, match } from "ts-pattern";

import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";

import PFFAIcons from "@patternfly/patternfly/base/patternfly-fa-icons.css";
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";

import { Application } from "@goauthentik/api";
export interface IAppIcon {
name?: string;
icon?: string;
size?: PFSize;
}

@customElement("ak-app-icon")
export class AppIcon extends AKElement {
@property({ type: Object, attribute: false })
app?: Application;
export class AppIcon extends AKElement implements IAppIcon {
@property({ type: String })
name?: string;

@property({ type: String })
icon?: string;

@property()
size?: PFSize;
size: PFSize = PFSize.Medium;

static get styles(): CSSResult[] {
return [
Expand All @@ -39,6 +46,10 @@ export class AppIcon extends AKElement {
--icon-height: 1rem;
--icon-border: 0.125rem;
}
:host([size="pf-m-xl"]) {
--icon-height: 6rem;
--icon-border: 0.25rem;
}
.pf-c-avatar {
--pf-c-avatar--BorderRadius: 0;
--pf-c-avatar--Height: calc(
Expand All @@ -64,21 +75,17 @@ export class AppIcon extends AKElement {
}

render(): TemplateResult {
if (!this.app) {
return html`<div><i class="icon fas fa-question-circle"></i></div>`;
}
if (this.app?.metaIcon) {
if (this.app.metaIcon.startsWith("fa://")) {
const icon = this.app.metaIcon.replaceAll("fa://", "");
return html`<div><i class="icon fas ${icon}"></i></div>`;
}
return html`<img
class="icon pf-c-avatar"
src="${ifDefined(this.app.metaIcon)}"
alt="${msg("Application Icon")}"
/>`;
}
return html`<span class="icon">${this.app?.name.charAt(0).toUpperCase()}</span>`;
// prettier-ignore
return match([this.name, this.icon])
.with([undefined, undefined],
() => html`<div><i class="icon fas fa-question-circle"></i></div>`)
.with([P._, P.string.startsWith("fa://")],
([_name, icon]) => html`<div><i class="icon fas ${icon.replaceAll("fa://", "")}"></i></div>`)
.with([P._, P.string],
([_name, icon]) => html`<img class="icon pf-c-avatar" src="${icon}" alt="${msg("Application Icon")}" />`)
.with([P.string, undefined],
([name]) => html`<span class="icon">${name.charAt(0).toUpperCase()}</span>`)
.exhaustive();
}
}

Expand Down
4 changes: 2 additions & 2 deletions web/src/elements/LoadingOverlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { customElement, property } from "lit/decorators.js";

import PFBase from "@patternfly/patternfly/patternfly-base.css";

interface ILoadingOverlay {
export interface ILoadingOverlay {
topmost?: boolean;
}

Expand Down Expand Up @@ -41,7 +41,7 @@ export class LoadingOverlay extends AKElement implements ILoadingOverlay {

render() {
return html`<ak-empty-state loading header="">
<slot></slot>
<span slot="body"><slot></slot></span>
</ak-empty-state>`;
}
}
Expand Down
33 changes: 33 additions & 0 deletions web/src/elements/buttons/ActionButton/ak-action-button.docs.mdx
Original file line number Diff line number Diff line change
@@ -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} />
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import "./ak-action-button";
import AKActionButton from "./ak-action-button";

const metadata: Meta<AKActionButton> = {
title: "Elements / Action Button",
title: "Elements / <ak-action-button>",
component: "ak-action-button",
parameters: {
docs: {
Expand Down
24 changes: 24 additions & 0 deletions web/src/elements/cards/stories/AggregateCard.docs.mdx
Original file line number Diff line number Diff line change
@@ -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 changes: 35 additions & 0 deletions web/src/elements/cards/stories/AggregatePromiseCard.docs.mdx
Original file line number Diff line number Diff line change
@@ -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 changes: 36 additions & 0 deletions web/src/elements/cards/stories/QuickActionsCard.docs.mdx
Original file line number Diff line number Diff line change
@@ -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} />
Loading

0 comments on commit 0a1d283

Please sign in to comment.