diff --git a/web/paths/node.js b/web/paths/node.js index 14eb449b98..85f3d1718c 100644 --- a/web/paths/node.js +++ b/web/paths/node.js @@ -64,7 +64,7 @@ export const EntryPoint = /** @type {const} */ ({ in: resolve(PackageRoot, "src", "flow", "index.entrypoint.ts"), out: resolve(DistDirectory, "flow", "FlowInterface"), }, - Standalone: { + StandaloneAPI: { in: resolve(PackageRoot, "src", "standalone", "api-browser/index.entrypoint.ts"), out: resolve(DistDirectory, "standalone", "api-browser", "index"), }, diff --git a/web/src/admin/admin-overview/cards/RecentEventsCard.ts b/web/src/admin/admin-overview/cards/RecentEventsCard.ts index df9ceeb786..c66d89dec6 100644 --- a/web/src/admin/admin-overview/cards/RecentEventsCard.ts +++ b/web/src/admin/admin-overview/cards/RecentEventsCard.ts @@ -89,7 +89,7 @@ export class RecentEventsCard extends Table { return super.renderEmpty( html`${msg("No Events found.")} + >${msg("No Events found.")}
${msg("No matching events could be found.")}
`, ); diff --git a/web/src/admin/applications/ApplicationViewPage.ts b/web/src/admin/applications/ApplicationViewPage.ts index 480e231ff8..5f810200cc 100644 --- a/web/src/admin/applications/ApplicationViewPage.ts +++ b/web/src/admin/applications/ApplicationViewPage.ts @@ -112,7 +112,7 @@ export class ApplicationViewPage extends AKElement { renderApp(): TemplateResult { if (!this.application) { - return html` `; + return html``; } return html` ${this.missingOutpost diff --git a/web/src/admin/applications/entitlements/ApplicationEntitlementPage.ts b/web/src/admin/applications/entitlements/ApplicationEntitlementPage.ts index 5dcace54a6..816b5306a6 100644 --- a/web/src/admin/applications/entitlements/ApplicationEntitlementPage.ts +++ b/web/src/admin/applications/entitlements/ApplicationEntitlementPage.ts @@ -118,13 +118,12 @@ export class ApplicationEntitlementsPage extends Table { renderEmpty(): TemplateResult { return super.renderEmpty( - html` + html`${msg("No app entitlements created.")} +
${msg( - "This application does currently not have any application entitlement defined.", + "This application does currently not have any application entitlements defined.", )}
diff --git a/web/src/admin/applications/wizard/steps/ak-application-wizard-bindings-step.ts b/web/src/admin/applications/wizard/steps/ak-application-wizard-bindings-step.ts index e7285bce1c..877f0d2ff3 100644 --- a/web/src/admin/applications/wizard/steps/ak-application-wizard-bindings-step.ts +++ b/web/src/admin/applications/wizard/steps/ak-application-wizard-bindings-step.ts @@ -116,7 +116,7 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep { .content=${[]} > ${msg("No bound policies.")} + >${msg("No bound policies.")}
${msg("No policies are currently bound to this object.")}
+ ` + : nothing} + + `, +}; - render: ({ icon, loading, fullHeight, header }: IEmptyState) => - container( - html` - `, +export const Basic: Story = { + ...Template, + args: { + icon: "fa-folder-open", + headingText: "No files found", + bodyText: "This folder is empty. Upload some files to get started.", + }, +}; + +export const Empty: Story = { + ...Template, + args: { + icon: "", + }, + render: () => + html`

Note that a completely empty <ak-empty-state> is just that: empty.

+ `, +}; + +export const WithAction: Story = { + ...Template, + args: { + icon: "fa-users", + headingText: "No users yet", + bodyText: "Get started by creating your first user account.", + primaryButtonText: html``, + }, +}; + +export const Loading: Story = { + ...Template, + args: { + loading: true, + }, +}; + +export const LoadingWithCustomMessage: Story = { + ...Template, + args: { + loading: true, + headingText: html`I know it's here, somewhere...`, + }, +}; + +export const LoadingWithDefaultMessage: Story = { + ...Template, + args: { + defaultLabel: true, + }, +}; + +export const LoadingDefaultWithOverride: Story = { + ...Template, + args: { + defaultLabel: true, + headingText: html`Have they got a chance? Eh. It would take a miracle.`, + }, +}; + +export const LoadingDefaultWithButton: Story = { + ...Template, + args: { + defaultLabel: true, + primaryButtonText: html``, + }, +}; + +export const FullHeight: Story = { + ...Template, + args: { + icon: "fa-search", + headingText: "No search results", + bodyText: "Try adjusting your search criteria or browse our categories.", + fullHeight: true, + primaryButtonText: html``, + }, +}; + +export const ProgrammaticUsage: Story = { + ...Template, + args: { + icon: "fa-beer", + headingText: "Hold My Beer", + bodyText: "I saw this in a cartoon once. I'm sure I can pull it off.", + primaryButtonText: html``, + }, + render: (args) => + akEmptyState( + { + icon: args.icon, + }, + { + heading: args.headingText, + body: args.bodyText, + primary: args.primaryButtonText + ? html` + + ` + : undefined, + }, ), }; -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` - - This is the body content +export const IconShowcase: Story = { + args: {}, + render: () => html` +
+ + Users + No users found - `), -}; -export const WithBodyAndPrimarySlotsFilled = { - ...DefaultStory, - args: { - ...DefaultStory, - ...{ loading: false, icon: "fa-space-shuttle", header: "The final frontier" }, - }, - render: ({ icon, loading, fullHeight, header }: IEmptyState) => - container( - html` - This is the body content slot - This is the primary content slot - `, - ), + + Database + No records + + + + Messages + No messages + + + + Analytics + No data to display + + + + Settings + No configuration + + + + Security + No alerts + +
+ `, }; diff --git a/web/src/elements/stories/LoadingOverlay.docs.mdx b/web/src/elements/stories/LoadingOverlay.docs.mdx deleted file mode 100644 index 65d010fc72..0000000000 --- a/web/src/elements/stories/LoadingOverlay.docs.mdx +++ /dev/null @@ -1,36 +0,0 @@ -import { Canvas, Description, Meta, Story, Title } from "@storybook/blocks"; - -import * as LoadingOverlayStories from "./LoadingOverlay.stories"; - - - -# 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 - - This would display below the loading spinner - -``` - -## Demo - -### Default - - - -### With a message - - diff --git a/web/src/elements/stories/LoadingOverlay.stories.ts b/web/src/elements/stories/LoadingOverlay.stories.ts index aac589c6aa..02d129effc 100644 --- a/web/src/elements/stories/LoadingOverlay.stories.ts +++ b/web/src/elements/stories/LoadingOverlay.stories.ts @@ -1,74 +1,154 @@ import type { Meta, StoryObj } from "@storybook/web-components"; -import { LitElement, TemplateResult, css, html } from "lit"; -import { customElement, property } from "lit/decorators.js"; +import { html } from "lit"; +import { ifDefined } from "lit/directives/if-defined.js"; -import { type ILoadingOverlay, LoadingOverlay } from "../LoadingOverlay.js"; import "../LoadingOverlay.js"; +import { type ILoadingOverlay, LoadingOverlay, akLoadingOverlay } from "../LoadingOverlay.js"; -const metadata: Meta = { - title: "Elements/", +type StoryArgs = ILoadingOverlay & { + headingText?: string; + bodyText?: string; + noSpinner: boolean; +}; + +const metadata: Meta = { + title: "Elements/ ", component: "ak-loading-overlay", + tags: ["autodocs"], parameters: { docs: { - description: "Our empty state spinner", + description: { + component: ` +# Loading Overlay Component + +A full-screen overlay component that displays a loading state with optional heading and body content. + +A variant of the EmptyState component that includes a protective background for load or import +operations during which the user should be prevented from interacting with the page. + +It has two named slots, both optional: + +- **heading**: Main title (renders in an \`

\`) +- **body**: Any text to describe the state +`, + }, }, }, argTypes: { - topmost: { control: "boolean" }, - // @ts-ignore - message: { control: "text" }, + topmost: { + control: "boolean", + description: + "Whether this overlay should appear above all other overlays (z-index: 999)", + defaultValue: false, + }, + noSpinner: { + control: "boolean", + description: "Disable the loading spinner animation", + defaultValue: false, + }, + icon: { + control: "text", + description: "Icon name to display instead of the default loading spinner", + }, + headingText: { + control: "text", + description: "Heading text displayed above the loading indicator", + }, + bodyText: { + control: "text", + description: "Body text displayed below the loading indicator", + }, }, + decorators: [ + (story) => html` +
+
+

Content Behind Overlay

+

authentik is awesome (or will be if something were actually loading)

+ +
+ ${story()} +
+ `, + ], }; 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%; - } - `; - } +type Story = StoryObj; - @property({ type: Object, attribute: false }) - content!: TemplateResult; +export const Default: Story = { + render: () => html``, +}; - render() { - return html`
${this.content}
`; - } -} - -export const DefaultStory: StoryObj = { +export const WithHeading: Story = { args: { - topmost: undefined, - // @ts-ignore - message: undefined, - }, - - // @ts-ignore - render: ({ topmost, message }: ILoadingOverlay) => { - message = typeof message === "string" ? html`${message}` : message; - const content = html` ${message ?? ""} - `; - return html``; + headingText: "Loading Data", }, + render: (args) => + html` + ${args.headingText} + `, }; -export const WithAMessage: StoryObj = { - ...DefaultStory, - args: { ...DefaultStory.args, message: html`

Overlay with a message

` }, +export const WithHeadingAndBody: Story = { + args: { + headingText: "Loading Data", + bodyText: "Please wait while we fetch your information...", + }, + render: (args) => + html` + ${args.headingText} + ${args.bodyText} + `, +}; + +export const NoSpinner: Story = { + args: { + headingText: "Static Message", + bodyText: "This overlay shows without a spinner animation.", + }, + render: (args) => + html` + ${args.headingText} + ${args.bodyText} + `, +}; + +export const WithCustomIcon: Story = { + args: { + icon: "fa-info-circle", + headingText: "Processing", + bodyText: "Your request is being processed...", + }, + render: (args) => + html` + ${args.headingText} + ${args.bodyText} + `, +}; + +export const ProgrammaticUsage: Story = { + args: { + topmost: false, + noSpinner: false, + icon: "", + headingText: "Programmatic Loading", + bodyText: "This overlay was created using the akLoadingOverlay function.", + }, + render: (args) => + akLoadingOverlay( + { + topmost: args.topmost, + noSpinner: args.noSpinner, + icon: args.icon || undefined, + }, + { + heading: args.headingText, + body: args.bodyText, + }, + ), }; diff --git a/web/src/elements/table/Table.ts b/web/src/elements/table/Table.ts index 4a5a6b3f04..1afabd0d5a 100644 --- a/web/src/elements/table/Table.ts +++ b/web/src/elements/table/Table.ts @@ -299,9 +299,7 @@ export abstract class Table extends WithLicenseSummary(AKElement) implements return html`
- ${msg("Loading")} +
`; @@ -314,7 +312,7 @@ export abstract class Table extends WithLicenseSummary(AKElement) implements
${inner ?? html`${msg("No objects found.")} > + >${msg("No objects found.")}
${this.renderObjectCreate()}
`}
@@ -331,7 +329,7 @@ export abstract class Table extends WithLicenseSummary(AKElement) implements if (!this.error) return nothing; return html`${msg("Failed to fetch objects.")} + >${msg("Failed to fetch objects.")}
${pluckErrorDetail(this.error)}
`; } diff --git a/web/src/elements/table/TablePage.ts b/web/src/elements/table/TablePage.ts index b89a215fd0..0f42c65bf3 100644 --- a/web/src/elements/table/TablePage.ts +++ b/web/src/elements/table/TablePage.ts @@ -42,7 +42,8 @@ export abstract class TablePage extends Table { return super.renderEmpty(html` ${inner ? inner - : html` + : html`${msg("No objects found.")}
${this.searchEnabled() ? this.renderEmptyClearSearch() : html``}
diff --git a/web/src/elements/tests/EmptyState.test.ts b/web/src/elements/tests/EmptyState.test.ts index 542bcdf301..9660d9ab2d 100644 --- a/web/src/elements/tests/EmptyState.test.ts +++ b/web/src/elements/tests/EmptyState.test.ts @@ -19,11 +19,7 @@ describe("ak-empty-state", () => { }); it("should render the default loader", async () => { - render( - html`${msg("Loading")} - `, - ); + render(html``); const empty = await $("ak-empty-state").$(">>>.pf-c-empty-state__icon"); await expect(empty).toExist(); @@ -33,25 +29,17 @@ describe("ak-empty-state", () => { }); it("should handle standard boolean", async () => { - render( - html`${msg("Loading")} - `, - ); + render(html`Waiting`); const empty = await $("ak-empty-state").$(">>>.pf-c-empty-state__icon"); await expect(empty).toExist(); const header = await $("ak-empty-state").$(">>>.pf-c-title"); - await expect(header).toHaveText("Loading"); + await expect(header).toHaveText("Waiting"); }); it("should render a static empty state", async () => { - render( - html`${msg("No messages found")} - `, - ); + render(html`${msg("No messages found")} `); const empty = await $("ak-empty-state").$(">>>.pf-c-empty-state__icon"); await expect(empty).toExist(); @@ -64,7 +52,7 @@ describe("ak-empty-state", () => { it("should render a slotted message", async () => { render( html`${msg("No messages found")} + >${msg("No messages found")}

Try again with a different filter

`, ); diff --git a/web/src/elements/user/sources/SourceSettings.ts b/web/src/elements/user/sources/SourceSettings.ts index b7d368e4d2..f749fbc487 100644 --- a/web/src/elements/user/sources/SourceSettings.ts +++ b/web/src/elements/user/sources/SourceSettings.ts @@ -115,9 +115,9 @@ export class UserSourceSettingsPage extends AKElement { ${this.sourceSettings ? html` ${this.sourceSettings.length < 1 - ? html`` + ? html` + ${msg("No services available.")}` : html` ${this.sourceSettings.map((source) => { return html`
  • @@ -139,7 +139,7 @@ export class UserSourceSettingsPage extends AKElement { })} `} ` - : html` `} + : html``} `; } } diff --git a/web/src/flow/FlowExecutor.ts b/web/src/flow/FlowExecutor.ts index efba4f8bb5..4254ae49c4 100644 --- a/web/src/flow/FlowExecutor.ts +++ b/web/src/flow/FlowExecutor.ts @@ -304,7 +304,7 @@ export class FlowExecutor async renderChallenge(): Promise { if (!this.challenge) { - return html` `; + return html` `; } switch (this.challenge?.component) { case "ak-stage-access-denied": diff --git a/web/src/flow/providers/SessionEnd.ts b/web/src/flow/providers/SessionEnd.ts index 33ebecc185..edd526258d 100644 --- a/web/src/flow/providers/SessionEnd.ts +++ b/web/src/flow/providers/SessionEnd.ts @@ -24,7 +24,7 @@ export class SessionEnd extends BaseStage { render(): TemplateResult { if (!this.challenge) { - return html` `; + return html``; } return html`