web: replace handmade list in Admin Overview with generator, storybook generator, fix storybook, fix bug in list's parent component (#9726)

* web: fix esbuild issue with style sheets

Getting ESBuild, Lit, and Storybook to all agree on how to read and parse stylesheets is a serious
pain. This fix better identifies the value types (instances) being passed from various sources in
the repo to the three *different* kinds of style processors we're using (the native one, the
polyfill one, and whatever the heck Storybook does internally).

Falling back to using older CSS instantiating techniques one era at a time seems to do the trick.
It's ugly, but in the face of the aggressive styling we use to avoid Flashes of Unstyled Content
(FLoUC), it's the logic with which we're left.

In standard mode, the following warning appears on the console when running a Flow:

```
Autofocus processing was blocked because a document already has a focused element.
```

In compatibility mode, the following **error** appears on the console when running a Flow:

```
crawler-inject.js:1106 Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
    at initDomMutationObservers (crawler-inject.js:1106:18)
    at crawler-inject.js:1114:24
    at Array.forEach (<anonymous>)
    at initDomMutationObservers (crawler-inject.js:1114:10)
    at crawler-inject.js:1549:1
initDomMutationObservers @ crawler-inject.js:1106
(anonymous) @ crawler-inject.js:1114
initDomMutationObservers @ crawler-inject.js:1114
(anonymous) @ crawler-inject.js:1549
```

Despite this error, nothing seems to be broken and flows work as anticipated.

* web: provide a test framework

As is typical of a system where a new build engine is involved, this thing is sadly fragile. Use the
wrong import style in wdio.conf.js and it breaks; there are several notes in tsconfig.test.conf and
wdio.conf.ts to tell eslint or tsc not to complain, it's just a different build with different
criteria, the native criteria don't apply.

On the other hand, writing tests is easy and predictable. We can test behaviors at the unit and
component scale in a straightforward manner, and validate our expectations that things work the way
we believe they should.

* Rolling back a reversion.

* web: update storybook, storybook a few things, fix a few things

After examining how people like Adobe and Salesforce do things, I have updated the storybook
configuration to provide run-time configuration of light/dark mode (although right now nothing
happens), inject the correct styling into the page, and update the preview handling so that we can
see the components better.  We'll see how this pans out.

I have provided stories for the AggregateCard, AggregatePromiseCard, and a new QuickActionsCard. I
also fixed a bug in AggregatePromiseCard where it would fail to report a fetch error. It will only
report that "the operation falied," but it will give the full error into the console.

**As an experiment**, I have changed the interpreter for `lint:precommit` and `build:watch` to use
[Bun](https://bun.sh/) instead of NodeJS. We have observed significant speed-ups and much better
memory management with Bun for these two operations. Those are both developer-facing operations, the
behavior of the system undur current CI/CD should not change.

And finally, I've switched the QuickActionsCard view in Admin-Overview to use the new component.
Looks the same.  Reads *way* easier.  :-)

* Slight revision in exception logic.

* Added a ton of documentation; made the failure message configurable.

* A few documentation changes.

* Adjusting paths to work with tests.

* add ci to test

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* linting shenanigans

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* web: patch spotlight on the fly to fix syntax issue that blocked storybook build

This should be a temporary hack.  I have an [open
issue](https://github.com/getsentry/spotlight/issues/419) and [pull
request](https://github.com/getsentry/spotlight/pull/420) with the
Spotlight people already to fix the issue.

* Somehow missed these in the merge.

* Merge missed something.

* Fix for incorrect path to patch file; fix for running patch multiple times.

* Prettier is still havin' opinions.

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Ken Sternberg
2024-07-15 10:54:09 -07:00
committed by GitHub
parent 1dec9bde3c
commit 1f2654f25f
15 changed files with 783 additions and 469 deletions

View File

@ -1,7 +1,9 @@
import { create } from "@storybook/theming/create";
const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
export default create({
base: "light",
base: isDarkMode ? "dark" : "light",
brandTitle: "authentik Storybook",
brandUrl: "https://goauthentik.io",
brandImage: "https://goauthentik.io/img/icon_left_brand_colour.svg",

View File

@ -19,6 +19,20 @@ const config: StorybookConfig = {
"@jeysal/storybook-addon-css-user-preferences",
"storybook-addon-mock",
],
staticDirs: [
{
from: "../node_modules/@patternfly/patternfly/patternfly-base.css",
to: "@patternfly/patternfly/patternfly-base.css",
},
{
from: "../src/common/styles/authentik.css",
to: "@goauthentik/common/styles/authentik.css",
},
{
from: "../src/common/styles/theme-dark.css",
to: "@goauthentik/common/styles/theme-dark.css",
},
],
framework: {
name: "@storybook/web-components-vite",
options: {},

View File

@ -0,0 +1,60 @@
<link rel="stylesheet" href="@patternfly/patternfly/patternfly-base.css" />
<link rel="stylesheet" href="@goauthentik/common/styles/authentik.css" />
<style>
body {
overflow-y: scroll;
margin: 0;
}
.sb-main-padded {
padding: 0 !important;
}
.story-shadow-container {
background-color: #fff;
box-shadow: 0 0 0.25em #ddd;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 1em;
margin: 0 auto;
max-width: 72em;
padding: 1em;
width: 100%;
}
.docs-story .story-shadow-container {
box-shadow: none;
}
.story-shadow-container[display-mode="flex-wrap"] {
flex-wrap: wrap;
flex-direction: row;
}
.title {
border-bottom: 1px solid #ccc;
color: #333;
font-size: 13px;
font-weight: bold;
margin: 2rem -1rem 1rem;
padding-bottom: 0.25rem;
padding-left: 1rem;
}
.title code {
background-color: #f5f5f5;
border-radius: 0.25rem;
font-weight: bold;
padding: 0.1rem 0.25rem;
}
.sbdocs-preview .hljs {
color: #fff !important;
white-space: pre-line;
}
.sbdocs-pre > div {
margin: 1em 0;
}
</style>

View File

@ -9,6 +9,11 @@ import "@patternfly/patternfly/patternfly-base.css";
const preview: Preview = {
parameters: {
options: {
storySort: {
method: "alphabetical",
},
},
actions: { argTypesRegex: "^on[A-Z].*" },
cssUserPrefs: {
"prefers-color-scheme": "light",

View File

@ -29,6 +29,7 @@ const eslintConfig = {
sourceType: "module",
},
plugins: ["@typescript-eslint", "lit", "custom-elements", "sonarjs"],
ignorePatterns: ["!./.storybook/**/*.ts"],
rules: {
"indent": "off",
"linebreak-style": ["error", "unix"],
@ -60,9 +61,14 @@ const modified = (s) => isModified.test(s);
const isCheckable = /\.(ts|js|mjs)$/;
const checkable = (s) => isCheckable.test(s);
const ignored = /\/\.storybook\//;
const notIgnored = (s) => !ignored.test(s);
const updated = statuses.reduce(
(acc, [status, filename]) =>
modified(status) && checkable(filename) ? [...acc, path.join(projectRoot, filename)] : acc,
modified(status) && checkable(filename) && notIgnored(filename)
? [...acc, path.join(projectRoot, filename)]
: acc,
[],
);

63
web/scripts/eslint.mjs Normal file
View File

@ -0,0 +1,63 @@
#!/usr/bin/env node --max_old_space_size=65536
import { execFileSync } from "child_process";
import { ESLint } from "eslint";
import path from "path";
import process from "process";
// Code assumes this script is in the './web/scripts' folder.
const projectRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
encoding: "utf8",
}).replace("\n", "");
process.chdir(path.join(projectRoot, "./web"));
const eslintConfig = {
fix: true,
overrideConfig: {
env: {
browser: true,
es2021: true,
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:lit/recommended",
"plugin:custom-elements/recommended",
"plugin:storybook/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
project: true,
},
plugins: ["@typescript-eslint", "lit", "custom-elements"],
ignorePatterns: ["authentik-live-tests/**"],
rules: {
"indent": "off",
"linebreak-style": ["error", "unix"],
"quotes": ["error", "double", { avoidEscape: true }],
"semi": ["error", "always"],
"@typescript-eslint/ban-ts-comment": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
},
},
};
const eslint = new ESLint(eslintConfig);
const results = await eslint.lintFiles(".");
const formatter = await eslint.loadFormatter("stylish");
const resultText = formatter.format(results);
const errors = results.reduce((acc, result) => acc + result.errorCount, 0);
// eslint-disable-next-line no-console
console.log(resultText);
process.exit(errors > 1 ? 1 : 0);

View File

@ -1,9 +1,15 @@
#!/usr/bin/env bash
TARGET="./node_modules/@spotlightjs/overlay/dist/index-"[0-9a-f]*.js
if [[ $(grep -L "QX2" "$TARGET" > /dev/null 2&>1) ]]; then
patch --forward -V none --no-backup-if-mismatch -p0 $TARGET <<EOF
TARGET=$(find "./node_modules/@spotlightjs/overlay/dist/" -name "index-[0-9a-f]*.js");
if ! grep -GL 'QX2 = ' "$TARGET" > /dev/null ; then
patch --forward --no-backup-if-mismatch -p0 "$TARGET" <<EOF
>>>>>>> main
--- a/index-5682ce90.js 2024-06-13 16:19:28
+++ b/index-5682ce90.js 2024-06-13 16:20:23
@@ -4958,11 +4958,10 @@
@ -21,6 +27,7 @@ patch --forward --no-backup-if-mismatch -p0 "$TARGET" <<EOF
clipboardEnabled: o = !1,
displayDataTypes: c = !1,
EOF
else
echo "spotlight overlay.js patch already applied"
fi

View File

@ -14,6 +14,8 @@ import { AKElement } from "@goauthentik/elements/Base";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js";
import "@goauthentik/elements/PageHeader";
import "@goauthentik/elements/cards/AggregatePromiseCard";
import "@goauthentik/elements/cards/QuickActionsCard.js";
import type { QuickAction } from "@goauthentik/elements/cards/QuickActionsCard.js";
import { paramURL } from "@goauthentik/elements/router/RouterOutlet";
import { msg, str } from "@lit/localize";
@ -25,7 +27,6 @@ import { when } from "lit/directives/when.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFDivider from "@patternfly/patternfly/components/Divider/divider.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@ -38,6 +39,11 @@ export function versionFamily(): string {
return parts.join(".");
}
const RELEASE = `${VERSION.split(".").slice(0, -1).join(".")}#fixed-in-${VERSION.replaceAll(
".",
"",
)}`;
const AdminOverviewBase = WithLicenseSummary(AKElement);
type Renderer = () => TemplateResult | typeof nothing;
@ -50,7 +56,6 @@ export class AdminOverviewPage extends AdminOverviewBase {
PFGrid,
PFPage,
PFContent,
PFList,
PFDivider,
css`
.pf-l-grid__item {
@ -73,6 +78,14 @@ export class AdminOverviewPage extends AdminOverviewBase {
];
}
quickActions: QuickAction[] = [
[msg("Create a new application"), paramURL("/core/applications", { createForm: true })],
[msg("Check the logs"), paramURL("/events/log")],
[msg("Explore integrations"), "https://goauthentik.io/integrations/", true],
[msg("Manage users"), paramURL("/identity/users")],
[msg("Check the release notes"), `https://goauthentik.io/docs/releases/${RELEASE}`, true],
];
@state()
user?: SessionUser;
@ -93,15 +106,8 @@ export class AdminOverviewPage extends AdminOverviewBase {
class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl pf-l-grid pf-m-gutter"
>
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl">
<ak-aggregate-card
icon="fa fa-share"
header=${msg("Quick actions")}
.isCenter=${false}
>
<ul class="pf-c-list">
${this.renderActions()}
</ul>
</ak-aggregate-card>
<ak-quick-actions-card .actions=${this.quickActions}>
</ak-quick-actions-card>
</div>
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl">
<ak-aggregate-card

View File

@ -8,22 +8,65 @@ import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
export interface IAggregateCard {
icon?: string;
header?: string;
headerLink?: string;
subtext?: string;
leftJustified?: boolean;
}
/**
* class AggregateCard
* element ak-aggregate-card
*
* @slot - The main content of the card
*
* Card component with a specific layout for quick informational blurbs
*/
@customElement("ak-aggregate-card")
export class AggregateCard extends AKElement {
export class AggregateCard extends AKElement implements IAggregateCard {
/**
* If this contains an `fa-` style string, the FontAwesome icon specified will be shown next to
* the header.
*
* @attr
*/
@property()
icon?: string;
/**
* The title of the card.
*
* @attr
*/
@property()
header?: string;
/**
* If this is non-empty, a link icon will be shown in the upper-right corner of the card.
*
* @attr
*/
@property()
headerLink?: string;
/**
* If this is non-empty, a small-text footer will be shown at the bottom of the card
*
* @attr
*/
@property()
subtext?: string;
@property({ type: Boolean })
isCenter = true;
/**
* If this is set, the contents of the card will be left-justified; otherwise they will be
* centered by default.
*
* @attr
*/
@property({ type: Boolean, attribute: "left-justified" })
leftJustified = false;
static get styles(): CSSResult[] {
return [PFBase, PFCard, PFFlex].concat([
@ -80,7 +123,7 @@ export class AggregateCard extends AKElement {
</div>
${this.renderHeaderLink()}
</div>
<div class="pf-c-card__body ${this.isCenter ? "center-value" : ""}">
<div class="pf-c-card__body ${this.leftJustified ? "" : "center-value"}">
${this.renderInner()}
${this.subtext ? html`<p class="subtext">${this.subtext}</p>` : html``}
</div>
@ -88,3 +131,9 @@ export class AggregateCard extends AKElement {
</div>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-aggregate-card": AggregateCard;
}
}

View File

@ -1,22 +1,56 @@
import { PFSize } from "@goauthentik/common/enums.js";
import "@goauthentik/elements/Spinner";
import { AggregateCard } from "@goauthentik/elements/cards/AggregateCard";
import { AggregateCard, type IAggregateCard } from "@goauthentik/elements/cards/AggregateCard";
import { TemplateResult, html } from "lit";
import { msg } from "@lit/localize";
import { TemplateResult, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { until } from "lit/directives/until.js";
export interface IAggregatePromiseCard extends IAggregateCard {
promise?: Promise<Record<string, unknown>>;
failureMessage?: string;
}
/**
* class AggregatePromiseCard
* element ak-aggregate-card-promise
*
* Card component with a specific layout for quick informational blurbs, fills in its main content
* with the results of a promise; shows a spinner when the promise has not yet resolved. Inherits
* from [AggregateCard](./AggregateCard.ts).
*/
@customElement("ak-aggregate-card-promise")
export class AggregatePromiseCard extends AggregateCard {
export class AggregatePromiseCard extends AggregateCard implements IAggregatePromiseCard {
/**
* If this contains an `fa-` style string, the FontAwesome icon specified will be shown next to
* the header.
*
* @attr
*/
@property({ attribute: false })
promise?: Promise<Record<string, unknown>>;
async promiseProxy(): Promise<TemplateResult> {
/**
* The error message if the promise is rejected or throws an exception.
*
* @attr
*/
@property()
failureMessage = msg("Operation failed to complete");
async promiseProxy(): Promise<TemplateResult | typeof nothing> {
if (!this.promise) {
return html``;
return nothing;
}
try {
const value = await this.promise;
return html`<i class="fa fa-check-circle"></i>&nbsp;${value.toString()}`;
} catch (error: unknown) {
console.warn(error);
return html`<i class="fa fa-exclamation-circle"></i>&nbsp;${this.failureMessage}`;
}
}
renderInner(): TemplateResult {
@ -25,3 +59,9 @@ export class AggregatePromiseCard extends AggregateCard {
</p>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-aggregate-card-promise": AggregatePromiseCard;
}
}

View File

@ -0,0 +1,72 @@
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/cards/AggregateCard.js";
import { msg } from "@lit/localize";
import { html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { map } from "lit/directives/map.js";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
export type QuickAction = [label: string, url: string, isExternal?: boolean];
export interface IQuickActionsCard {
title?: string;
actions: QuickAction[];
}
/**
* class QuickActionsCard
* element ak-quick-actions-card
*
* Specialized card for navigation.
*/
@customElement("ak-quick-actions-card")
export class QuickActionsCard extends AKElement implements IQuickActionsCard {
static get styles() {
return [PFBase, PFList];
}
/**
* Card title
*
* @attr
*/
@property()
title = msg("Quick actions");
/**
* Card contents. An array of [label, url, isExternal]. External links will
* be rendered with an external link icon and will always open in a new tab.
*
* @attr
*/
@property({ type: Array })
actions: QuickAction[] = [];
render() {
const renderItem = ([label, url, external]: QuickAction) =>
html` <li>
<a class="pf-u-mb-xl" href=${url} ${external ? 'target="_blank"' : ""}>
${external
? html`${label}&nbsp;<i
class="fas fa-external-link-alt ak-external-link"
></i>`
: label}
</a>
</li>`;
return html` <ak-aggregate-card icon="fa fa-share" header=${this.title} left-justified>
<ul class="pf-c-list">
${map(this.actions, renderItem)}
</ul>
</ak-aggregate-card>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-quick-actions-card": QuickActionsCard;
}
}

View File

@ -0,0 +1,62 @@
import type { Meta, StoryObj } from "@storybook/web-components";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { AggregateCard, type IAggregateCard } from "../AggregateCard.js";
import "../AggregateCard.js";
const metadata: Meta<AggregateCard> = {
title: "Elements/<ak-aggregate-card>",
component: "ak-aggregate-card",
parameters: {
docs: {
description: "A specialized card for displaying collections",
},
},
argTypes: {
icon: { control: "text" },
header: { control: "text" },
headerLink: { control: "text" },
subtext: { control: "text" },
leftJustified: { control: "boolean" },
},
};
export default metadata;
export const DefaultStory: StoryObj = {
args: {
icon: undefined,
header: "Default",
headerLink: undefined,
subtext: undefined,
isCenter: false,
},
render: ({ icon, header, headerLink, subtext, leftJustified }: IAggregateCard) => {
return html` <div style="background-color: #f0f0f0; padding: 1rem;">
<style>
ak-aggregate-card {
display: inline-block;
width: 32rem;
max-width: 32rem;
}
</style>
<ak-aggregate-card
header=${ifDefined(header)}
headerLink=${ifDefined(headerLink)}
subtext=${ifDefined(subtext)}
icon=${ifDefined(icon)}
?left-justified=${leftJustified}
>
<p>
Form without content style without meaning quick-win, for that is a good problem
to have, so this is our north star design. Can you champion this cross sabers
run it up the flagpole, ping the boss and circle back race without a finish line
in an ideal world. Price point innovation is hot right now, nor it's not hard
guys, but race without a finish line, nor thought shower.
</p>
</ak-aggregate-card>
</div>`;
},
};

View File

@ -0,0 +1,105 @@
import type { Meta, StoryObj } from "@storybook/web-components";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { AggregatePromiseCard, type IAggregatePromiseCard } from "../AggregatePromiseCard.js";
import "../AggregatePromiseCard.js";
const metadata: Meta<AggregatePromiseCard> = {
title: "Elements/<ak-aggregate-card-promise>",
component: "ak-aggregate-card-promise",
parameters: {
docs: {
description: "A specialized card for displaying information after a fetch",
},
},
argTypes: {
icon: { control: "text" },
header: { control: "text" },
headerLink: { control: "text" },
subtext: { control: "text" },
leftJustified: { control: "boolean" },
failureMessage: { control: "text" },
},
};
export default metadata;
const text =
"Curl up and sleep on the freshly laundered towels mew, but make meme, make cute face growl at dogs in my sleep. Scratch me there, elevator butt humans, humans, humans oh how much they love us felines we are the center of attention they feed, they clean hopped up on catnip mice. Kitty time flop over, for see owner, run in terror";
export const DefaultStory: StoryObj = {
args: {
icon: undefined,
header: "Default",
headerLink: undefined,
subtext: "Demo has an eight second delay until resolution",
leftJustified: false,
},
render: ({ icon, header, headerLink, subtext, leftJustified }: IAggregatePromiseCard) => {
const runThis = (timeout: number, value: string) =>
new Promise((resolve) => setTimeout(resolve, timeout, value));
return html` <div style="background-color: #f0f0f0; padding: 1rem;">
<style>
ak-aggregate-card-promise {
display: inline-block;
width: 32rem;
max-width: 32rem;
}
</style>
<ak-aggregate-card-promise
header=${ifDefined(header)}
headerLink=${ifDefined(headerLink)}
subtext=${ifDefined(subtext)}
icon=${ifDefined(icon)}
?left-justified=${leftJustified}
.promise=${runThis(8000, text)}
>
</ak-aggregate-card-promise>
</div>`;
},
};
export const PromiseRejected: StoryObj = {
args: {
icon: undefined,
header: "Default",
headerLink: undefined,
subtext: "Demo has an eight second delay until rejection",
leftJustified: false,
failureMessage: undefined,
},
render: ({
icon,
header,
headerLink,
subtext,
leftJustified,
failureMessage,
}: IAggregatePromiseCard) => {
const runThis = (timeout: number, value: string) =>
new Promise((_resolve, reject) => setTimeout(reject, timeout, value));
return html` <div style="background-color: #f0f0f0; padding: 1rem;">
<style>
ak-aggregate-card-promise {
display: inline-block;
width: 32rem;
max-width: 32rem;
}
</style>
<ak-aggregate-card-promise
header=${ifDefined(header)}
headerLink=${ifDefined(headerLink)}
subtext=${ifDefined(subtext)}
icon=${ifDefined(icon)}
failureMessage=${ifDefined(failureMessage)}
?left-justified=${leftJustified}
.promise=${runThis(8000, text)}
>
</ak-aggregate-card-promise>
</div>`;
},
};

View File

@ -0,0 +1,47 @@
import type { Meta, StoryObj } from "@storybook/web-components";
import { html } from "lit";
import "../QuickActionsCard.js";
import { QuickAction, QuickActionsCard } from "../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],
];
const metadata: Meta<QuickActionsCard> = {
title: "Elements/<ak-quick-action-card>",
component: "ak-quick-action-card",
parameters: {
docs: {
description: "A specialized card for a list of navigation links",
},
},
argTypes: {
title: { control: "text" },
},
};
export default metadata;
export const DefaultStory: StoryObj = {
args: {
title: "Quick actions",
},
render: ({ title }) => {
return html` <div style="background-color: #f0f0f0; padding: 1rem;">
<style>
ak-quick-actions-card {
display: inline-block;
width: 16rem;
max-width: 16rem;
}
</style>
<ak-quick-actions-card title=${title} .actions=${ACTIONS}></ak-quick-actions-card>
</div>`;
},
};

File diff suppressed because it is too large Load Diff