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:
@ -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",
|
||||
|
@ -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: {},
|
||||
|
60
web/.storybook/preview-head.html
Normal file
60
web/.storybook/preview-head.html
Normal 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>
|
@ -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",
|
||||
|
@ -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
63
web/scripts/eslint.mjs
Normal 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);
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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> ${value.toString()}`;
|
||||
} catch (error: unknown) {
|
||||
console.warn(error);
|
||||
return html`<i class="fa fa-exclamation-circle"></i> ${this.failureMessage}`;
|
||||
}
|
||||
}
|
||||
|
||||
renderInner(): TemplateResult {
|
||||
@ -25,3 +59,9 @@ export class AggregatePromiseCard extends AggregateCard {
|
||||
</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-aggregate-card-promise": AggregatePromiseCard;
|
||||
}
|
||||
}
|
||||
|
72
web/src/elements/cards/QuickActionsCard.ts
Normal file
72
web/src/elements/cards/QuickActionsCard.ts
Normal 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} <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;
|
||||
}
|
||||
}
|
62
web/src/elements/cards/stories/AggregateCard.stories.ts
Normal file
62
web/src/elements/cards/stories/AggregateCard.stories.ts
Normal 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>`;
|
||||
},
|
||||
};
|
105
web/src/elements/cards/stories/AggregatePromiseCard.stories.ts
Normal file
105
web/src/elements/cards/stories/AggregatePromiseCard.stories.ts
Normal 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>`;
|
||||
},
|
||||
};
|
47
web/src/elements/cards/stories/QuickActionCard.stories.ts
Normal file
47
web/src/elements/cards/stories/QuickActionCard.stories.ts
Normal 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>`;
|
||||
},
|
||||
};
|
666
website/package-lock.json
generated
666
website/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user