diff --git a/web/package.json b/web/package.json index 201ea934a87c..e7544493fd3b 100644 --- a/web/package.json +++ b/web/package.json @@ -57,6 +57,7 @@ "#styles/*.css": "./src/styles/*.css", "#styles/*": "./src/styles/*.js", "#common/*": "./src/common/*.js", + "#elements/dialogs": "./src/elements/dialogs/index.js", "#elements/*.css": "./src/elements/*.css", "#elements/*": "./src/elements/*.js", "#components/*.css": "./src/components/*.css", diff --git a/web/playwright.config.js b/web/playwright.config.js index c14f4029ff1b..863b1e6eaaa5 100644 --- a/web/playwright.config.js +++ b/web/playwright.config.js @@ -36,6 +36,7 @@ export default defineConfig({ testIdAttribute: "data-test-id", baseURL, trace: "on-first-retry", + colorScheme: "dark", launchOptions: { logger: { isEnabled() { diff --git a/web/src/admin/admin-settings/AdminSettingsFooterLinks.ts b/web/src/admin/admin-settings/AdminSettingsFooterLinks.ts index 5697abc24696..eb36949fb556 100644 --- a/web/src/admin/admin-settings/AdminSettingsFooterLinks.ts +++ b/web/src/admin/admin-settings/AdminSettingsFooterLinks.ts @@ -43,13 +43,16 @@ export class FooterLinkInput extends AKControlElement { @queryAll(".ak-form-control") controls?: HTMLInputElement[]; - json() { + @property({ type: String }) + public name: string | null = null; + + toJSON(): FooterLink { return Object.fromEntries( Array.from(this.controls ?? []).map((control) => [control.name, control.value]), ) as unknown as FooterLink; } - get isValid() { + get valid() { const href = this.json()?.href ?? ""; return hasLegalScheme(href) && URL.canParse(href); } diff --git a/web/src/admin/admin-settings/AdminSettingsForm.ts b/web/src/admin/admin-settings/AdminSettingsForm.ts index 36497c969987..84c47b9eb992 100644 --- a/web/src/admin/admin-settings/AdminSettingsForm.ts +++ b/web/src/admin/admin-settings/AdminSettingsForm.ts @@ -14,11 +14,12 @@ import { akFooterLinkInput, IFooterLinkInput } from "./AdminSettingsFooterLinks. import { DEFAULT_CONFIG } from "#common/api/config"; import { Form } from "#elements/forms/Form"; +import { SlottedTemplateResult } from "#elements/types"; import { AdminApi, FooterLink, Settings, SettingsRequest } from "@goauthentik/api"; import { msg } from "@lit/localize"; -import { css, CSSResult, html, TemplateResult } from "lit"; +import { css, CSSResult, html } from "lit"; import { customElement, property } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; @@ -56,7 +57,20 @@ export class AdminSettingsForm extends Form { return result; } - protected override renderForm(): TemplateResult { + public override submitLabel = msg("Save changes"); + + public override renderHeader() { + return html`
+

${msg("Edit Settings")}

+
${this.renderSubmitButton()}
+
`; + } + + public override renderActions(): SlottedTemplateResult { + return null; + } + + protected override renderForm(): SlottedTemplateResult { const { settings } = this; return html` diff --git a/web/src/admin/admin-settings/AdminSettingsPage.ts b/web/src/admin/admin-settings/AdminSettingsPage.ts index 2ddca9bd2413..bfc90f452c6d 100644 --- a/web/src/admin/admin-settings/AdminSettingsPage.ts +++ b/web/src/admin/admin-settings/AdminSettingsPage.ts @@ -66,19 +66,18 @@ export class AdminSettingsPage extends AKElement { if (!this.settings) return nothing; return html` -
+
-
+ `; } diff --git a/web/src/admin/admin-settings/stories/AdminSettingsFooterLinks.stories.ts b/web/src/admin/admin-settings/stories/AdminSettingsFooterLinks.stories.ts index c604110fac55..79ecccdcbc87 100644 --- a/web/src/admin/admin-settings/stories/AdminSettingsFooterLinks.stories.ts +++ b/web/src/admin/admin-settings/stories/AdminSettingsFooterLinks.stories.ts @@ -34,7 +34,7 @@ const metadata: Meta = { return; } const target = event.target as FooterLinkInput; - messages!.innerText = `${JSON.stringify(target.json(), null, 2)}\n\nValid: ${target.isValid ? "Yes" : "No"}`; + messages!.innerText = `${JSON.stringify(target.json(), null, 2)}\n\nValid: ${target.valid ? "Yes" : "No"}`; }); }, 250); diff --git a/web/src/admin/admin-settings/stories/ak-array-input.stories.ts b/web/src/admin/admin-settings/stories/ak-array-input.stories.ts index 416a387498f7..c3375bd89a94 100644 --- a/web/src/admin/admin-settings/stories/ak-array-input.stories.ts +++ b/web/src/admin/admin-settings/stories/ak-array-input.stories.ts @@ -40,7 +40,7 @@ const metadata: Meta> = { return; } const target = event.target as FooterLinkInput; - messages!.innerText = `${JSON.stringify(target.json(), null, 2)}\n\nValid: ${target.isValid ? "Yes" : "No"}`; + messages!.innerText = `${JSON.stringify(target.json(), null, 2)}\n\nValid: ${target.valid ? "Yes" : "No"}`; }); }, 250); diff --git a/web/src/admin/ak-about-modal.ts b/web/src/admin/ak-about-modal.ts index a3c671181b46..52f9461d03ae 100644 --- a/web/src/admin/ak-about-modal.ts +++ b/web/src/admin/ak-about-modal.ts @@ -1,72 +1,51 @@ -import "#elements/EmptyState"; +import "#elements/ak-progress-bar"; import { DEFAULT_CONFIG } from "#common/api/config"; import { globalAK } from "#common/global"; +import { asInvoker } from "#elements/dialogs"; +import { AKModal } from "#elements/dialogs/ak-modal"; import { WithBrandConfig } from "#elements/mixins/branding"; import { WithLicenseSummary } from "#elements/mixins/license"; -import { AKModal } from "#elements/modals/ak-modal"; -import { asInvoker } from "#elements/modals/utils"; +import { SlottedTemplateResult } from "#elements/types"; import { ThemedImage } from "#elements/utils/images"; -import { AdminApi, CapabilitiesEnum, LicenseSummaryStatusEnum } from "@goauthentik/api"; +import { + AdminApi, + CapabilitiesEnum, + LicenseSummaryStatusEnum, + SystemInfo, + Version, +} from "@goauthentik/api"; import { msg } from "@lit/localize"; -import { css, html, TemplateResult } from "lit"; +import { css, html } from "lit"; import { ref } from "lit-html/directives/ref.js"; import { styleMap } from "lit-html/directives/style-map.js"; +import { until } from "lit-html/directives/until.js"; import { customElement, state } from "lit/decorators.js"; import PFAbout from "@patternfly/patternfly/components/AboutModalBox/about-modal-box.css"; const DEFAULT_BRAND_IMAGE = "/static/dist/assets/images/flow_background.jpg"; -type AboutEntry = [label: string, content: string | TemplateResult]; +type AboutEntry = [label: string, content?: SlottedTemplateResult]; -async function fetchAboutDetails(): Promise { - const api = new AdminApi(DEFAULT_CONFIG); - - const [status, version] = await Promise.all([ - api.adminSystemRetrieve(), - api.adminVersionRetrieve(), - ]); - - let build: string | TemplateResult = msg("Release"); - - if (globalAK().config.capabilities.includes(CapabilitiesEnum.CanDebug)) { - build = msg("Development"); - } else if (version.buildHash) { - build = html`${version.buildHash}`; - } - - return [ - [msg("Version"), version.versionCurrent], - [msg("UI Version"), import.meta.env.AK_VERSION], - [msg("Build"), build], - [msg("Python version"), status.runtime.pythonVersion], - [msg("Platform"), status.runtime.platform], - [msg("Kernel"), status.runtime.uname], - [ - msg("OpenSSL"), - `${status.runtime.opensslVersion} ${status.runtime.opensslFipsEnabled ? "FIPS" : ""}`, - ], - ]; +function renderEntry([label, content = null]: AboutEntry): SlottedTemplateResult { + return html`
${label}
+
${content === null ? msg("Loading...") : content}
`; } @customElement("ak-about-modal") export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) { - public static override formatARIALabel = () => msg("About authentik"); + public override formatARIALabel = () => msg("About authentik"); public static hostStyles = [ + ...AKModal.hostStyles, css` - dialog.ak-c-modal:has(ak-about-modal) { - --ak-c-modal--BackgroundColor: var(--pf-global--palette--black-900); - --ak-c-modal--BorderColor: var(--pf-global--palette--black-600); + .ak-c-dialog:has(ak-about-modal) { + --ak-c-dialog--BackgroundColor: var(--pf-global--palette--black-900); + --ak-c-dialog--BorderColor: var(--pf-global--palette--black-600); } `, ]; @@ -80,7 +59,7 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) { } .pf-c-about-modal-box { - --pf-c-about-modal-box--BackgroundColor: var(--ak-c-modal--BackgroundColor); + --pf-c-about-modal-box--BackgroundColor: var(--ak-c-dialog--BackgroundColor); width: unset; height: 100%; max-height: unset; @@ -89,6 +68,17 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) { position: unset; box-shadow: unset; } + + [part="brand"] { + position: relative; + } + + [part="loading-bar"] { + position: absolute; + z-index: 1; + inset-block-start: 0; + inset-inline: 0; + } `, ]; @@ -96,26 +86,100 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) { public static open = asInvoker(AboutModal); + #api = new AdminApi(DEFAULT_CONFIG); + + protected canDebug = globalAK().config.capabilities.includes(CapabilitiesEnum.CanDebug); + @state() - protected entries: AboutEntry[] | null = null; + protected version: Version | null = null; - public refresh() { - return fetchAboutDetails().then((entries) => { - this.entries = entries; + @state() + protected systemInfo: SystemInfo | null = null; + + @state() + protected refreshPromise: Promise<[Version, SystemInfo]> | null = null; + + public refresh = (): void => { + const versionPromise = this.#api.adminVersionRetrieve(); + const systemInfoPromise = this.#api.adminSystemRetrieve(); + + this.refreshPromise = Promise.all([versionPromise, systemInfoPromise]).then((result) => { + this.version = result[0]; + this.systemInfo = result[1]; + + return result; }); - } + }; public connectedCallback(): void { super.connectedCallback(); this.refresh(); } + protected renderVersionInfo = () => { + const { version } = this; + + let build: SlottedTemplateResult = null; + + if (this.canDebug) { + build = msg("Development"); + } else if (version?.buildHash) { + build = html`${version.buildHash}`; + } else if (version) { + build = msg("Release"); + } + + const entries: AboutEntry[] = [ + [msg("Server Version"), version?.versionCurrent], + [msg("Build"), build], + ]; + + return entries.map(renderEntry); + }; + + protected renderSystemInfo = () => { + const { runtime } = this.systemInfo || {}; + + const sslLabel = runtime + ? `${runtime.opensslVersion} ${runtime.opensslFipsEnabled ? "FIPS" : ""}` + : null; + + const entries: AboutEntry[] = [ + [msg("Python version"), runtime?.pythonVersion], + [msg("Platform"), runtime?.platform], + [msg("OpenSSL"), sslLabel], + [ + msg("Kernel"), + runtime?.uname ?? html`
${msg("Loading...")}
`, + ], + ]; + + return entries.map(renderEntry); + }; + //#region Renderers protected override renderCloseButton() { return null; } + protected renderLoadingBar(): SlottedTemplateResult { + return until( + this.refreshPromise?.then(() => null), + html``, + ); + } + protected override render() { let product = this.brandingTitle; @@ -129,6 +193,7 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) { style=${styleMap({ "--pf-c-about-modal-box__hero--sm--BackgroundImage": `url(${DEFAULT_BRAND_IMAGE})`, })} + part="box" >
-
+
+ ${this.renderLoadingBar()} ${ThemedImage({ src: this.brandingFavicon, alt: msg("authentik Logo"), @@ -149,21 +215,25 @@ export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) { themedUrls: this.brandingFaviconThemedUrls, })}
-
-

${product}

+
+

${product}

- ${this.entries - ? html`
- ${this.entries.map(([label, value]) => { - return html`
${label}
-
${value}
`; - })} -
` - : html``} +
+
${msg("UI Version")}
+
${import.meta.env.AK_VERSION}
+ ${until( + this.refreshPromise?.then(this.renderVersionInfo), + this.renderVersionInfo(), + )} + ${until( + this.refreshPromise?.then(this.renderSystemInfo), + this.renderSystemInfo(), + )} +

diff --git a/web/src/admin/ak-interface-admin.ts b/web/src/admin/ak-interface-admin.ts index 4ef06816d936..1090fbdeefc0 100644 --- a/web/src/admin/ak-interface-admin.ts +++ b/web/src/admin/ak-interface-admin.ts @@ -4,6 +4,7 @@ import "#elements/sidebar/Sidebar"; import "#elements/sidebar/SidebarItem"; import "#elements/router/RouterOutlet"; import "#elements/commands/ak-command-palette"; +import "#elements/commands/ak-command-palette-user-modal"; import { createAdminSidebarEnterpriseEntries, @@ -24,10 +25,10 @@ import { PaletteCommandNamespace, } from "#elements/commands/shared"; import { listen } from "#elements/decorators/listen"; +import { renderDialog } from "#elements/dialogs"; import { WithCapabilitiesConfig } from "#elements/mixins/capabilities"; import { WithNotifications } from "#elements/mixins/notifications"; import { canAccessAdmin, WithSession } from "#elements/mixins/session"; -import { renderDialog } from "#elements/modals/utils"; import { AKDrawerChangeEvent } from "#elements/notifications/events"; import { DrawerState, @@ -36,6 +37,7 @@ import { renderNotificationDrawerPanel, } from "#elements/notifications/utils"; import { navigate } from "#elements/router/RouterOutlet"; +import { SlottedTemplateResult } from "#elements/types"; import Styles from "#admin/ak-interface-admin.css"; import { ROUTES } from "#admin/Routes"; @@ -46,6 +48,7 @@ import { LOCALE_STATUS_EVENT, LocaleStatusEventDetail, msg } from "@lit/localize import { CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit"; import { customElement, eventOptions, property, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; +import { guard } from "lit/directives/guard.js"; import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; @@ -266,40 +269,7 @@ export class AdminInterface extends WithCapabilitiesConfig( - + ${this.renderCommandPaletteButton()} @@ -344,6 +314,47 @@ export class AdminInterface extends WithCapabilitiesConfig( ${this.commandPalette}`; } + protected renderCommandPaletteButton(): SlottedTemplateResult { + return guard([this.commandPalette.showListener], () => { + const macOS = navigator.platform.toUpperCase().indexOf("MAC") >= 0; + + const primaryModifierKey = macOS ? "⌘" : "Ctrl"; + + return html``; + }); + } + //#endregion } diff --git a/web/src/admin/applications/ApplicationForm.ts b/web/src/admin/applications/ApplicationForm.ts index 4a14e92ec879..148fa905c0ce 100644 --- a/web/src/admin/applications/ApplicationForm.ts +++ b/web/src/admin/applications/ApplicationForm.ts @@ -27,7 +27,7 @@ import { policyEngineModes } from "#admin/policies/PolicyEngineModes"; import { Application, CoreApi, Provider, UsageEnum } from "@goauthentik/api"; import { msg } from "@lit/localize"; -import { html, nothing, TemplateResult } from "lit"; +import { html, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; @@ -40,8 +40,8 @@ import { ifDefined } from "lit/directives/if-defined.js"; export class ApplicationForm extends WithCapabilitiesConfig(ModelForm) { #api = new CoreApi(DEFAULT_CONFIG); - public override entitySingular = msg("Application"); - public override entityPlural = msg("Applications"); + public static override verboseName = msg("Application"); + public static override verboseNamePlural = msg("Applications"); protected override async loadInstance(pk: string): Promise { const app = await this.#api.coreApplicationsRetrieve({ @@ -54,7 +54,7 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm${alertMsg}`} + ${this.instance || this.provider + ? null + : html`${alertMsg}`} ) protected override searchEnabled = true; public pageTitle = msg("Applications"); + public searchLabel = msg("Applications search"); + public searchPlaceholder = msg("Search for application by name, group or provider..."); + public get pageDescription() { return msg( str`External applications that use ${this.brandingTitle} as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.`, @@ -69,7 +73,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage) super.firstUpdated(changed); if (getURLParam("createWizard", false)) { - AkApplicationWizard.showModal(); + AKApplicationWizard.showModal(); } else if (getURLParam("createForm", false)) { ApplicationForm.showModal(); } @@ -138,16 +142,8 @@ export class ApplicationListPage extends WithBrandConfig(TablePage) ` : html`-`, html`${item.providerObj?.verboseName || msg("-")}`, - html`
- + html`
+ ${IconEditButton(ApplicationForm, item.slug)} ${item.launchUrl ? html`) protected override renderObjectCreate(): TemplateResult { return html` - +
+ + + +
+
`; } - renderEmpty(): TemplateResult { + protected override renderEmpty(): SlottedTemplateResult { return super.renderEmpty( html`${msg("No app entitlements created.")} diff --git a/web/src/admin/applications/wizard/ApplicationWizardStep.ts b/web/src/admin/applications/wizard/ApplicationWizardStep.ts index 3805f3dcc2c8..5c16a37efa67 100644 --- a/web/src/admin/applications/wizard/ApplicationWizardStep.ts +++ b/web/src/admin/applications/wizard/ApplicationWizardStep.ts @@ -10,8 +10,8 @@ import { WizardStep } from "#components/ak-wizard/WizardStep"; import { ApplicationWizardStyles } from "#admin/applications/wizard/ApplicationWizardFormStepStyles.styles"; import { - type ApplicationWizardState, - type ApplicationWizardStateUpdate, + type ApplicationWizardContext, + type ApplicationWizardContextUpdate, } from "#admin/applications/wizard/steps/providers/shared"; import { ApplicationRequest } from "@goauthentik/api"; @@ -19,6 +19,12 @@ import { ApplicationRequest } from "@goauthentik/api"; import { msg } from "@lit/localize"; import { property } from "lit/decorators.js"; +export interface ApplicationDispatchInit { + update?: ApplicationWizardContextUpdate | null; + destination?: string | null; + details?: NavigationEventInit | null; +} + /** * Base class for application wizard steps. Provides common functionality such as form handling and wizard state management. * @@ -28,7 +34,7 @@ export abstract class ApplicationWizardStep> ext static styles = [...WizardStep.styles, ...ApplicationWizardStyles]; @property({ type: Object, attribute: false }) - public wizard!: ApplicationWizardState; + public wizard!: ApplicationWizardContext; protected override wizardTitle = msg("New application"); protected override wizardDescription = msg( @@ -78,19 +84,15 @@ export abstract class ApplicationWizardStep> ext // This pattern became visible during development, and the order is important: wizard updating // and validation must complete before navigation is attempted. - public handleUpdate( - update?: ApplicationWizardStateUpdate, - destination?: string, - enable?: NavigationEventInit, - ) { + public dispatchEvents({ update, destination, details }: ApplicationDispatchInit): void { // Inform ApplicationWizard of content state if (update) { this.dispatchEvent(new WizardUpdateEvent(update)); } // Inform WizardStepManager of steps state - if (destination || enable) { - this.dispatchEvent(new WizardNavigationEvent(destination, enable)); + if (destination || details) { + this.dispatchEvent(new WizardNavigationEvent(details, destination)); } } } diff --git a/web/src/admin/applications/wizard/ak-application-wizard-main.ts b/web/src/admin/applications/wizard/ak-application-wizard-main.ts deleted file mode 100644 index 020f96a792b2..000000000000 --- a/web/src/admin/applications/wizard/ak-application-wizard-main.ts +++ /dev/null @@ -1,140 +0,0 @@ -import "#components/ak-wizard/ak-wizard-steps"; -import "#admin/applications/wizard/steps/ak-application-wizard-application-step"; -import "#admin/applications/wizard/steps/ak-application-wizard-bindings-step"; -import "#admin/applications/wizard/steps/ak-application-wizard-edit-binding-step"; -import "#admin/applications/wizard/steps/ak-application-wizard-provider-choice-step"; -import "#admin/applications/wizard/steps/ak-application-wizard-provider-step"; -import "#admin/applications/wizard/steps/ak-application-wizard-submit-step"; - -import { DEFAULT_CONFIG } from "#common/api/config"; -import { assertEveryPresent } from "#common/utils"; - -import { AKElement } from "#elements/Base"; - -import { WizardUpdateEvent } from "#components/ak-wizard/events"; - -import { applicationWizardProvidersContext } from "#admin/applications/wizard/ContextIdentity"; -import { - type ApplicationWizardState, - type ApplicationWizardStateUpdate, -} from "#admin/applications/wizard/steps/providers/shared"; - -import type { TypeCreate } from "@goauthentik/api"; -import { ProviderModelEnum, ProvidersApi, ProxyMode } from "@goauthentik/api"; - -import { ContextProvider } from "@lit/context"; -import { html } from "lit"; -import { customElement, state } from "lit/decorators.js"; - -const freshWizardState = (): ApplicationWizardState => ({ - providerModel: "", - currentBinding: -1, - app: {}, - provider: {}, - proxyMode: ProxyMode.Proxy, - bindings: [], - errors: {}, -}); - -type ExtractProviderName = T extends `${string}.${infer Name}` ? Name : never; - -type ProviderModelNameEnum = ExtractProviderName | "samlproviderimportmodel"; - -export const providerTypePriority: ProviderModelNameEnum[] = [ - "oauth2provider", - "samlprovider", - "samlproviderimportmodel", - "racprovider", - "proxyprovider", - "radiusprovider", - "ldapprovider", - "scimprovider", - "wsfederationprovider", -]; - -@customElement("ak-application-wizard-main") -export class AkApplicationWizardMain extends AKElement { - protected createRenderRoot(): HTMLElement | DocumentFragment { - return this; - } - - @state() - protected wizard: ApplicationWizardState = freshWizardState(); - - protected wizardProviderProvider = new ContextProvider(this, { - context: applicationWizardProvidersContext, - initialValue: [], - }); - - constructor() { - super(); - this.addEventListener(WizardUpdateEvent.eventName, this.handleUpdate); - } - - public override connectedCallback() { - super.connectedCallback(); - - new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((providerTypes) => { - const providerNameToProviderMap = new Map( - providerTypes.map((providerType) => [providerType.modelName, providerType]), - ); - const providersInOrder = providerTypePriority.map((name) => - providerNameToProviderMap.get(name), - ); - assertEveryPresent( - providersInOrder, - "Provider priority list includes name for which no provider model was returned.", - ); - this.wizardProviderProvider.setValue(providersInOrder); - }); - } - - // This is the actual top of the Wizard; so this is where we accept the update information and - // incorporate it into the wizard. - handleUpdate(ev: WizardUpdateEvent) { - ev.stopPropagation(); - const update = ev.content; - - if (typeof update !== "undefined") { - this.wizard = { - ...this.wizard, - ...update, - }; - } - } - - render() { - return html` - - - - - - - `; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ak-application-wizard-main": AkApplicationWizardMain; - } -} diff --git a/web/src/admin/applications/wizard/ak-application-wizard.ts b/web/src/admin/applications/wizard/ak-application-wizard.ts index 003ed250c754..5701d34630c1 100644 --- a/web/src/admin/applications/wizard/ak-application-wizard.ts +++ b/web/src/admin/applications/wizard/ak-application-wizard.ts @@ -1,46 +1,147 @@ -import "#admin/applications/wizard/ak-application-wizard-main"; +import "#components/ak-wizard/ak-wizard-steps"; +import "#admin/applications/wizard/steps/ak-application-wizard-application-step"; +import "#admin/applications/wizard/steps/ak-application-wizard-bindings-step"; +import "#admin/applications/wizard/steps/ak-application-wizard-edit-binding-step"; +import "#admin/applications/wizard/steps/ak-application-wizard-provider-choice-step"; +import "#admin/applications/wizard/steps/ak-application-wizard-provider-step"; +import "#admin/applications/wizard/steps/ak-application-wizard-submit-step"; -import { AKModal } from "#elements/modals/ak-modal"; -import { SlottedTemplateResult } from "#elements/types"; +import { DEFAULT_CONFIG } from "#common/api/config"; +import { assertEveryPresent } from "#common/utils"; -import { WizardCloseEvent } from "#components/ak-wizard/events"; +import { listen } from "#elements/decorators/listen"; +import { CreateWizard } from "#elements/wizard/CreateWizard"; +import { WizardUpdateEvent } from "#components/ak-wizard/events"; + +import { applicationWizardProvidersContext } from "#admin/applications/wizard/ContextIdentity"; +import { + type ApplicationWizardContext, + type ApplicationWizardContextUpdate, +} from "#admin/applications/wizard/steps/providers/shared"; + +import type { TypeCreate } from "@goauthentik/api"; +import { ProviderModelEnum, ProvidersApi, ProxyMode } from "@goauthentik/api"; + +import { ContextProvider } from "@lit/context"; import { msg } from "@lit/localize"; -import { css, CSSResult, html } from "lit"; -import { customElement } from "lit/decorators.js"; +import { html } from "lit"; +import { customElement, state } from "lit/decorators.js"; + +const createWizardContextValue = (): ApplicationWizardContext => ({ + providerModel: "", + currentBinding: -1, + app: {}, + provider: {}, + proxyMode: ProxyMode.Proxy, + bindings: [], + errors: {}, +}); + +type ExtractProviderName = T extends `${string}.${infer Name}` ? Name : never; + +type ProviderModelNameEnum = ExtractProviderName | "samlproviderimportmodel"; + +export const providerTypePriority: ProviderModelNameEnum[] = [ + "oauth2provider", + "samlprovider", + "samlproviderimportmodel", + "racprovider", + "proxyprovider", + "radiusprovider", + "ldapprovider", + "scimprovider", + "wsfederationprovider", +]; @customElement("ak-application-wizard") -export class AkApplicationWizard extends AKModal { - public static override formatARIALabel?(): string { - return msg("New Application Wizard"); +export class AKApplicationWizard extends CreateWizard { + #api = new ProvidersApi(DEFAULT_CONFIG); + + public static override verboseName = msg("Application"); + public static override verboseNamePlural = msg("Applications"); + + protected createRenderRoot(): HTMLElement | DocumentFragment { + return this; } - public static override styles: CSSResult[] = [ - ...super.styles, - css` - [part="main"] { - display: block; - } - `, - ]; + @state() + protected context: ApplicationWizardContext = createWizardContextValue(); - constructor() { - super(); + protected wizardProviderProvider = new ContextProvider(this, { + context: applicationWizardProvidersContext, + initialValue: [], + }); - this.addEventListener(WizardCloseEvent.eventName, this.closeListener); - } + public override refresh = (): Promise => { + return this.#api.providersAllTypesList().then((providerTypes) => { + const providerNameToProviderMap = new Map( + providerTypes.map((providerType) => [providerType.modelName, providerType]), + ); + + const providersInOrder = providerTypePriority.map((name) => + providerNameToProviderMap.get(name), + ); + + assertEveryPresent( + providersInOrder, + "Provider priority list includes name for which no provider model was returned.", + ); + + this.wizardProviderProvider.setValue(providersInOrder); + }); + }; + + // This is the actual top of the Wizard; so this is where we accept the update information and + // incorporate it into the wizard. + /** + * Handles updates to the wizard context, which are emitted by the individual steps when their data changes. + */ + @listen(WizardUpdateEvent) + handleUpdate(ev: WizardUpdateEvent) { + ev.stopPropagation(); + const update = ev.content; - protected renderCloseButton(): SlottedTemplateResult { - return null; + if (update) { + this.context = { + ...this.context, + ...update, + }; + } } - render() { - return html``; + protected override render() { + return html` + + + + + + + `; } } declare global { interface HTMLElementTagNameMap { - "ak-application-wizard": AkApplicationWizard; + "ak-application-wizard": AKApplicationWizard; } } diff --git a/web/src/admin/applications/wizard/ak-wizard-title.ts b/web/src/admin/applications/wizard/ak-wizard-title.ts deleted file mode 100644 index 1a358bc1831c..000000000000 --- a/web/src/admin/applications/wizard/ak-wizard-title.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { AKElement } from "#elements/Base"; - -import { css, html } from "lit"; -import { customElement } from "lit/decorators.js"; - -import PFContent from "@patternfly/patternfly/components/Content/content.css"; -import PFTitle from "@patternfly/patternfly/components/Title/title.css"; - -@customElement("ak-wizard-title") -export class AkWizardTitle extends AKElement { - static styles = [ - PFContent, - PFTitle, - css` - .ak-bottom-spacing { - padding-bottom: var(--pf-global--spacer--lg); - } - `, - ]; - - render() { - return html`
-

-
`; - } -} - -export default AkWizardTitle; - -declare global { - interface HTMLElementTagNameMap { - "ak-wizard-title": AkWizardTitle; - } -} diff --git a/web/src/admin/applications/wizard/steps/SubmitStepOverviewRenderers.ts b/web/src/admin/applications/wizard/steps/SubmitStepOverviewRenderers.ts index 4e32cef5c5d4..18b6a7ed8ef1 100644 --- a/web/src/admin/applications/wizard/steps/SubmitStepOverviewRenderers.ts +++ b/web/src/admin/applications/wizard/steps/SubmitStepOverviewRenderers.ts @@ -50,8 +50,8 @@ const renderSAMLImportOverview: ProviderOverview { return renderSummary("SAML", provider.name, [ - [msg("Authorization flow"), provider.authorizationFlow ?? "-"], - [msg("Invalidation flow"), provider.invalidationFlow ?? "-"], + [msg("Authorization Flow"), provider.authorizationFlow ?? "-"], + [msg("Invalidation Flow"), provider.invalidationFlow ?? "-"], ]); }; @@ -153,7 +153,7 @@ const renderOAuth2Overview: ProviderOverview = (provider) => { const label = provider.clientType ? clientTypeToLabel[provider.clientType]() : ""; return renderSummary("OAuth2", provider.name, [ - [msg("Client type"), label], + [msg("Client Type"), label], [msg("Client ID"), provider.clientId], [msg("Redirect URIs"), formatRedirectUris(provider.redirectUris)], ]); diff --git a/web/src/admin/applications/wizard/steps/ak-application-wizard-application-step.ts b/web/src/admin/applications/wizard/steps/ak-application-wizard-application-step.ts index 3f6552efae38..d1e2181ac2af 100644 --- a/web/src/admin/applications/wizard/steps/ak-application-wizard-application-step.ts +++ b/web/src/admin/applications/wizard/steps/ak-application-wizard-application-step.ts @@ -1,4 +1,3 @@ -import "#admin/applications/wizard/ak-wizard-title"; import "#components/ak-file-search-input"; import "#components/ak-radio-input"; import "#components/ak-slug-input"; @@ -16,7 +15,7 @@ import { type NavigableButton, type WizardButton } from "#components/ak-wizard/s import { ApplicationWizardStep } from "#admin/applications/wizard/ApplicationWizardStep"; import { - ApplicationWizardStateUpdate, + ApplicationWizardContextUpdate, WizardValidationRecord, } from "#admin/applications/wizard/steps/providers/shared"; import { policyEngineModes } from "#admin/policies/PolicyEngineModes"; @@ -61,13 +60,11 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep { : (this.wizard.errors?.app?.[name] ?? this.wizard.errors?.app?.[snakeCase(name)] ?? []); } - get buttons(): WizardButton[] { - return [ - // --- - { kind: "cancel" }, - { kind: "next", destination: "provider-choice" }, - ]; - } + protected buttons: WizardButton[] = [ + // --- + { kind: "cancel" }, + { kind: "next", destination: "provider-choice" }, + ]; get valid() { this.errors = new Map(); @@ -95,7 +92,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep { } if (!this.valid) { - this.handleEnabling({ + this.dispatchNavigationEvent({ disabled: ["provider-choice", "provider", "bindings", "submit"], }); @@ -104,24 +101,26 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep { const app = { ...this.formValues }; - const payload: ApplicationWizardStateUpdate = { + const update: ApplicationWizardContextUpdate = { app, errors: omitKeys(this.wizard.errors, "app"), }; if (!this.wizard.provider?.name?.trim() && app.name) { - payload.provider = { + update.provider = { name: `Provider for ${app.name}`, }; } - this.handleUpdate(payload, button.destination, { - enable: "provider-choice", + return this.dispatchEvents({ + update, + destination: button.destination, + details: { enable: "provider-choice" }, }); } protected renderForm(app: Partial, errors: WizardValidationRecord = {}) { - return html` ${msg("Configure the Application")} + return html`

${msg("Configure the Application")}

{ const { order, enabled, timeout } = binding; + const isSet = P.union(P.string.minLength(1), P.number); const policy = match(binding) .with({ policy: isSet }, (v) => msg(str`Policy ${v.policyObj?.name}`)) @@ -89,26 +87,32 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep { // TODO Fix those dispatches so that we handle them here, in this component, and *choose* how to // forward them. onBindingEvent(binding?: number) { - this.handleUpdate({ currentBinding: binding ?? -1 }, "edit-binding", { - enable: "edit-binding", + this.dispatchEvents({ + update: { currentBinding: binding ?? -1 }, + destination: "edit-binding", + details: { enable: "edit-binding" }, }); } - onDeleteBindings() { + protected onDeleteBindings() { const toDelete = this.selectTable .json() .map((i) => (typeof i === "string" ? parseInt(i, 10) : i)); const bindings = this.wizard.bindings.filter((binding, index) => !toDelete.includes(index)); - this.handleUpdate({ bindings }, "bindings"); + + return this.dispatchEvents({ + update: { bindings }, + destination: "bindings", + }); } - renderEmptyCollection() { - return html`${msg("Configure Policy/User/Group Bindings")} -
+ protected renderEmptyCollection() { + return html`

+ ${msg("Configure Policy/User/Group Bindings")} +

+

${msg("These policies control which users can access this application.")} -

+
this.onBindingEvent()} @@ -136,11 +140,11 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep {
`; } - renderCollection() { - return html` ${msg("Configure Policy Bindings")} -
+ protected renderCollection() { + return html`

${msg("Configure Policy Bindings")}

+

${msg("These policies control which users can access this application.")} -

+ this.onBindingEvent()} @clickDelete=${() => this.onDeleteBindings()} @@ -155,7 +159,7 @@ export class ApplicationWizardBindingsStep extends ApplicationWizardStep { >`; } - renderMain() { + protected renderMain() { if ((this.wizard.bindings ?? []).length === 0) { return this.renderEmptyCollection(); } diff --git a/web/src/admin/applications/wizard/steps/ak-application-wizard-edit-binding-step.ts b/web/src/admin/applications/wizard/steps/ak-application-wizard-edit-binding-step.ts index 65a4e60f6845..539c51248944 100644 --- a/web/src/admin/applications/wizard/steps/ak-application-wizard-edit-binding-step.ts +++ b/web/src/admin/applications/wizard/steps/ak-application-wizard-edit-binding-step.ts @@ -1,5 +1,4 @@ import "#components/ak-number-input"; -import "#admin/applications/wizard/ak-wizard-title"; import "#components/ak-radio-input"; import "#components/ak-switch-input"; import "#components/ak-text-input"; @@ -27,7 +26,7 @@ import { ApplicationWizardStep } from "#admin/applications/wizard/ApplicationWiz import { CoreApi, Group, PoliciesApi, Policy, PolicyBinding, User } from "@goauthentik/api"; -import { msg } from "@lit/localize"; +import { msg, str } from "@lit/localize"; import { html, nothing } from "lit"; import { customElement, query, state } from "lit/decorators.js"; @@ -50,17 +49,15 @@ export class ApplicationWizardEditBindingStep extends ApplicationWizardStep `; } - renderForm(instance?: PolicyBinding) { - return html`${msg("Create a Policy/User/Group Binding")} + protected renderForm(instance?: PolicyBinding | null) { + return html`

+ ${msg("Create a Policy/User/Group Binding")} +

@@ -222,16 +224,18 @@ export class ApplicationWizardEditBindingStep extends ApplicationWizardStep`; } - renderMain() { + protected renderMain() { if (!(this.wizard.bindings && this.wizard.errors)) { throw new Error("Application Step received uninitialized wizard context."); } + const currentBinding = this.wizard.currentBinding ?? -1; + if (this.instanceId !== currentBinding) { this.instanceId = currentBinding; - this.instance = - this.instanceId === -1 ? undefined : this.wizard.bindings[this.instanceId]; + this.instance = this.instanceId === -1 ? null : this.wizard.bindings[this.instanceId]; } + return this.renderForm(this.instance); } } diff --git a/web/src/admin/applications/wizard/steps/ak-application-wizard-provider-choice-step.ts b/web/src/admin/applications/wizard/steps/ak-application-wizard-provider-choice-step.ts index 1fd5cc7975da..5e80a25623a2 100644 --- a/web/src/admin/applications/wizard/steps/ak-application-wizard-provider-choice-step.ts +++ b/web/src/admin/applications/wizard/steps/ak-application-wizard-provider-choice-step.ts @@ -1,4 +1,3 @@ -import "#admin/applications/wizard/ak-wizard-title"; import "#elements/EmptyState"; import "#elements/forms/FormGroup"; import "#elements/forms/HorizontalFormElement"; @@ -6,8 +5,8 @@ import "#elements/wizard/TypeCreateWizardPage"; import { applicationWizardProvidersContext } from "../ContextIdentity.js"; -import { bound } from "#elements/decorators/bound"; import { WithLicenseSummary } from "#elements/mixins/license"; +import { SlottedTemplateResult } from "#elements/types"; import { TypeCreateWizardPageLayouts } from "#elements/wizard/TypeCreateWizardPage"; import type { NavigableButton, WizardButton } from "#components/ak-wizard/shared"; @@ -19,6 +18,7 @@ import type { TypeCreate } from "@goauthentik/api"; import { consume } from "@lit/context"; import { msg } from "@lit/localize"; import { html } from "lit"; +import { guard } from "lit-html/directives/guard.js"; import { customElement, state } from "lit/decorators.js"; /** @@ -30,65 +30,69 @@ export class ApplicationWizardProviderChoiceStep extends WithLicenseSummary(Appl label = msg("Choose a Provider"); @state() - failureMessage = ""; + protected failureMessage = ""; @consume({ context: applicationWizardProvidersContext, subscribe: true }) public providerModelsList!: TypeCreate[]; - get buttons(): WizardButton[] { - return [ - { kind: "cancel" }, - { kind: "back", destination: "application" }, - { kind: "next", destination: "provider" }, - ]; - } + protected buttons: WizardButton[] = [ + { kind: "cancel" }, + { kind: "back", destination: "application" }, + { kind: "next", destination: "provider" }, + ]; public override handleButton(button: NavigableButton) { this.failureMessage = ""; + if (button.kind === "next") { if (!this.wizard.providerModel) { this.failureMessage = msg("Please choose a provider type before proceeding."); - this.handleEnabling({ disabled: ["provider", "bindings", "submit"] }); + this.dispatchNavigationEvent({ disabled: ["provider", "bindings", "submit"] }); + return; } - this.handleUpdate(undefined, button.destination, { enable: "provider" }); - return; + + return this.dispatchEvents({ + destination: button.destination, + details: { enable: "provider" }, + }); } - super.handleButton(button); - } - @bound - onSelect(ev: CustomEvent) { - ev.stopPropagation(); - const detail: TypeCreate = ev.detail; - this.handleUpdate({ providerModel: detail.modelName }); + return super.handleButton(button); } - renderMain() { - const selectedTypes = this.providerModelsList.filter( - (t) => t.modelName === this.wizard.providerModel, - ); - - return this.providerModelsList.length > 0 - ? html` ${msg("Choose a Provider Type")} - - 0 ? selectedTypes[0] : undefined} - @select=${(ev: CustomEvent) => { - this.handleUpdate( - { - ...this.wizard, - providerModel: ev.detail.modelName, - }, - undefined, - { enable: "provider" }, - ); - }} - > - ` - : html``; + protected typeSelectListener = (event: CustomEvent) => { + return this.dispatchEvents({ + update: { + ...this.wizard, + providerModel: event.detail.modelName, + }, + details: { enable: "provider" }, + }); + }; + + protected renderMain(): SlottedTemplateResult { + const { providerModelsList } = this; + + return guard([providerModelsList], () => { + if (!providerModelsList.length) { + return html``; + } + + const selectedTypes = providerModelsList.filter( + (t) => t.modelName === this.wizard.providerModel, + ); + + return html`

${msg("Choose a Provider Type")}

+
+ 0 ? selectedTypes[0] : null} + @ak-type-create-select=${this.typeSelectListener} + > +
`; + }); } } diff --git a/web/src/admin/applications/wizard/steps/ak-application-wizard-provider-step.ts b/web/src/admin/applications/wizard/steps/ak-application-wizard-provider-step.ts index 8131727d9698..1522dc0b3da3 100644 --- a/web/src/admin/applications/wizard/steps/ak-application-wizard-provider-step.ts +++ b/web/src/admin/applications/wizard/steps/ak-application-wizard-provider-step.ts @@ -75,11 +75,12 @@ export class ApplicationWizardProviderStep extends ApplicationWizardStep { public override handleButton(button: NavigableButton) { if (button.kind === "next") { if (!this.valid) { - this.handleEnabling({ + this.dispatchNavigationEvent({ disabled: ["bindings", "submit"], }); return; } + const payload = { provider: { ...this.formValues, @@ -87,22 +88,23 @@ export class ApplicationWizardProviderStep extends ApplicationWizardStep { }, errors: omitKeys(this.wizard.errors, "provider"), }; - this.handleUpdate(payload, button.destination, { - enable: ["bindings", "submit"], + + return this.dispatchEvents({ + update: payload, + destination: button.destination, + details: { enable: ["bindings", "submit"] }, }); - return; } - super.handleButton(button); - } - get buttons(): WizardButton[] { - return [ - { kind: "cancel" }, - { kind: "back", destination: "provider-choice" }, - { kind: "next", destination: "bindings" }, - ]; + return super.handleButton(button); } + protected buttons: WizardButton[] = [ + { kind: "cancel" }, + { kind: "back", destination: "provider-choice" }, + { kind: "next", destination: "bindings" }, + ]; + renderMain() { if (!this.wizard.providerModel) { throw new Error("Attempted to access provider page without providing a provider type."); diff --git a/web/src/admin/applications/wizard/steps/ak-application-wizard-submit-step.ts b/web/src/admin/applications/wizard/steps/ak-application-wizard-submit-step.ts index 89b0cfe5281c..f856c4c8a7de 100644 --- a/web/src/admin/applications/wizard/steps/ak-application-wizard-submit-step.ts +++ b/web/src/admin/applications/wizard/steps/ak-application-wizard-submit-step.ts @@ -1,5 +1,3 @@ -import "#admin/applications/wizard/ak-wizard-title"; - import { DEFAULT_CONFIG } from "#common/api/config"; import { EVENT_REFRESH } from "#common/constants"; import { parseAPIResponseError } from "#common/errors/network"; @@ -154,7 +152,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio return; } - this.handleUpdate({ errors: parsedError }); + this.dispatchEvents({ update: { errors: parsedError } }); this.state = "reviewing"; } } @@ -243,7 +241,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio } } - this.handleUpdate({ errors: parsedError }); + this.dispatchEvents({ update: { errors: parsedError } }); this.state = "reviewing"; }); } @@ -269,19 +267,22 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio }); } - get buttons(): WizardButton[] { - const forReview: WizardButton[] = [ - { kind: "cancel" }, - { kind: "back", destination: "bindings" }, - { kind: "next", label: msg("Create Application"), destination: "here" }, - ]; - - const forSubmit: WizardButton[] = [{ kind: "close" }]; - + protected get buttons(): WizardButton[] { return match(this.state) - .with("submitted", () => forSubmit) + .with("submitted", () => { + return [ + { kind: "close" }, + { kind: "finish", destination: "close" }, + ] satisfies WizardButton[]; + }) + .with("reviewing", () => { + return [ + { kind: "cancel" }, + { kind: "back", destination: "bindings" }, + { kind: "next", label: msg("Create Application"), destination: "here" }, + ] satisfies WizardButton[]; + }) .with("running", () => []) - .with("reviewing", () => forReview) .exhaustive(); } @@ -377,36 +378,53 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio const metaLaunchUrl = app.metaLaunchUrl?.trim(); - return html` -
- ${msg("Review the Application and Provider")} -

${msg("Application")}

-
-
-
${msg("Name")}
-
${app.name}
-
-
-
${msg("Group")}
-
${app.group || msg("-")}
-
-
-
${msg("Policy engine mode")}
-
- ${app.policyEngineMode?.toUpperCase()} -
-
- ${metaLaunchUrl - ? html`
-
${msg("Launch URL")}
-
${metaLaunchUrl}
-
` - : nothing} -
- ${renderer - ? html`

${msg("Provider")}

- ${renderer(provider)}` - : nothing} + return html`

+ ${msg("Review the Application and Provider")} +

+
+ ${msg("Application Details")} +
+
+
${msg("Application Name")}
+
${app.name}
+
+
+
${msg("Group")}
+
+ ${app.group || msg("-")} +
+
+
+
+ ${msg("Policy engine mode")} +
+
+ ${app.policyEngineMode?.toUpperCase()} +
+
+ ${ + metaLaunchUrl + ? html`
+
+ ${msg("Launch URL")} +
+
+ ${metaLaunchUrl} +
+
` + : nothing + } +
+
+ + ${ + renderer + ? html`
+ ${msg("Provider Details")} + ${renderer(provider)} +
` + : null + }
`; } diff --git a/web/src/admin/applications/wizard/steps/providers/ApplicationWizardProviderForm.ts b/web/src/admin/applications/wizard/steps/providers/ApplicationWizardProviderForm.ts index cacf3b3f1dbb..ab215652c8c7 100644 --- a/web/src/admin/applications/wizard/steps/providers/ApplicationWizardProviderForm.ts +++ b/web/src/admin/applications/wizard/steps/providers/ApplicationWizardProviderForm.ts @@ -11,7 +11,7 @@ import { serializeForm } from "#elements/forms/serialization"; import { ApplicationWizardStyles } from "#admin/applications/wizard/ApplicationWizardFormStepStyles.styles"; import { ApplicationTransactionValidationError, - type ApplicationWizardState, + type ApplicationWizardContext, ApplicationWizardStateError, type OneOfProvider, } from "#admin/applications/wizard/steps/providers/shared"; @@ -30,7 +30,7 @@ export abstract class ApplicationWizardProviderForm< public abstract label: string; @property({ type: Object, attribute: false }) - public wizard!: ApplicationWizardState; + public wizard!: ApplicationWizardContext; @property({ type: Object, attribute: false }) public errors: E = {} as E; diff --git a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-ldap.ts b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-ldap.ts index b0623f509a04..f8217371bc08 100644 --- a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-ldap.ts +++ b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-ldap.ts @@ -1,5 +1,3 @@ -import "#admin/applications/wizard/ak-wizard-title"; - import { WithBrandConfig } from "#elements/mixins/branding"; import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm"; @@ -22,12 +20,10 @@ export class ApplicationWizardLdapProviderForm extends WithBrandConfig( label = msg("Configure LDAP Provider"); renderForm(provider: LDAPProvider, errors: WizardValidationRecord) { - return html` - ${this.label} + return html`

${this.label}

${renderForm({ provider, errors, brand: this.brand })} -
- `; + `; } render() { diff --git a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-oauth.ts b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-oauth.ts index 08b7e5509dee..d4a11985c2b4 100644 --- a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-oauth.ts +++ b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-oauth.ts @@ -1,5 +1,3 @@ -import "#admin/applications/wizard/ak-wizard-title"; - import { DEFAULT_CONFIG } from "#common/api/config"; import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm"; @@ -44,7 +42,7 @@ export class ApplicationWizardOauth2ProviderForm extends ApplicationWizardProvid const showLogoutMethodCallback = (show: boolean) => { this.showLogoutMethod = show; }; - return html` ${this.label} + return html`

${this.label}

${renderForm({ provider, diff --git a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-proxy.ts b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-proxy.ts index c5c35c071349..570455cd2b5a 100644 --- a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-proxy.ts +++ b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-proxy.ts @@ -1,5 +1,3 @@ -import "#admin/applications/wizard/ak-wizard-title"; - import { WizardUpdateEvent } from "#components/ak-wizard/events"; import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm"; @@ -39,7 +37,7 @@ export class ApplicationWizardProxyProviderForm extends ApplicationWizardProvide this.showHttpBasic = el.checked; }; - return html` ${this.label} + return html`

${this.label}

${renderForm({ provider, diff --git a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-rac.ts b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-rac.ts index 91b550a181d8..9f64f2d73e54 100644 --- a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-rac.ts +++ b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-rac.ts @@ -1,4 +1,3 @@ -import "#admin/applications/wizard/ak-wizard-title"; import "#admin/common/ak-crypto-certificate-search"; import "#admin/common/ak-flow-search/ak-flow-search"; import "#components/ak-text-input"; @@ -23,8 +22,7 @@ export class ApplicationWizardRACProviderForm extends ApplicationWizardProviderF label = msg("Configure Remote Access Provider"); renderForm(provider: RACProvider) { - return html` - ${this.label} + return html`

${this.label}

- - `; + `; } render() { diff --git a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-radius.ts b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-radius.ts index 191f89ad100e..c2341019a183 100644 --- a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-radius.ts +++ b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-radius.ts @@ -1,5 +1,3 @@ -import "#admin/applications/wizard/ak-wizard-title"; - import { WithBrandConfig } from "#elements/mixins/branding"; import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm"; @@ -19,7 +17,7 @@ export class ApplicationWizardRadiusProviderForm extends WithBrandConfig( label = msg("Configure Radius Provider"); renderForm(provider: RadiusProvider, errors: WizardValidationRecord = {}) { - return html` ${this.label} + return html`

${this.label}

${renderForm({ provider, errors, brand: this.brand })}
`; diff --git a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-saml-metadata.ts b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-saml-metadata.ts index 20728914104b..ed14d0db6bcf 100644 --- a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-saml-metadata.ts +++ b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-saml-metadata.ts @@ -1,5 +1,3 @@ -import "#admin/applications/wizard/ak-wizard-title"; - import { createFileMap } from "#elements/utils/inputs"; import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm"; @@ -30,12 +28,10 @@ export class ApplicationWizardProviderSamlMetadataForm extends ApplicationWizard } renderForm() { - return html` - ${this.label} + return html`

${this.label}

${renderForm(this.wizard.provider)} -
- `; + `; } render() { diff --git a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-saml.ts b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-saml.ts index bb0d2c757f4c..f8192b29a6ec 100644 --- a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-saml.ts +++ b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-saml.ts @@ -1,4 +1,3 @@ -import "#admin/applications/wizard/ak-wizard-title"; import "#elements/forms/FormGroup"; import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm"; @@ -80,7 +79,7 @@ export class ApplicationWizardProviderSamlForm extends ApplicationWizardProvider this.logoutMethod = target.value; }; - return html` ${this.label} + return html`

${this.label}

${renderForm({ provider: this.wizard.provider, diff --git a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-scim.ts b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-scim.ts index d0ef6f219baf..d89a2adb189e 100644 --- a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-scim.ts +++ b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-scim.ts @@ -1,4 +1,3 @@ -import "#admin/applications/wizard/ak-wizard-title"; import "#elements/forms/FormGroup"; import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm"; @@ -18,7 +17,7 @@ export class ApplicationWizardSCIMProvider extends ApplicationWizardProviderForm propertyMappings?: PaginatedSCIMMappingList; render() { - return html`${this.label} + return html`

${this.label}

${renderForm({ update: this.requestUpdate.bind(this), diff --git a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-wsfed.ts b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-wsfed.ts index f6676824f38c..029567de2db4 100644 --- a/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-wsfed.ts +++ b/web/src/admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-wsfed.ts @@ -1,4 +1,3 @@ -import "#admin/applications/wizard/ak-wizard-title"; import "#elements/forms/FormGroup"; import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js"; @@ -30,7 +29,7 @@ export class ApplicationWizardProviderWSFedForm extends ApplicationWizardProvide this.signingKeyType = target.selectedKeypair?.keyType ?? KeyTypeEnum.Rsa; }; - return html` ${this.label} + return html`

${this.label}

${renderForm({ provider: this.wizard.provider as WSFederationProvider, diff --git a/web/src/admin/applications/wizard/steps/providers/shared.ts b/web/src/admin/applications/wizard/steps/providers/shared.ts index 9f3ea694d028..1ccd5f274fba 100644 --- a/web/src/admin/applications/wizard/steps/providers/shared.ts +++ b/web/src/admin/applications/wizard/steps/providers/shared.ts @@ -67,7 +67,7 @@ export type ApplicationWizardStateError = ValidationError | ApplicationTransacti // configured bindings" page in the wizard. The PolicyBinding is converted into a // PolicyBindingRequest during the submission phase. -export interface ApplicationWizardState< +export interface ApplicationWizardContext< P extends OneOfProvider = OneOfProvider, E = ApplicationTransactionValidationError, > { @@ -80,7 +80,7 @@ export interface ApplicationWizardState< errors: E; } -export interface ApplicationWizardStateUpdate { +export interface ApplicationWizardContextUpdate { app?: Partial; providerModel?: string; provider?: OneOfProvider; diff --git a/web/src/admin/blueprints/BlueprintForm.ts b/web/src/admin/blueprints/BlueprintForm.ts index 6b2b23ea57a3..99c3e2a53c48 100644 --- a/web/src/admin/blueprints/BlueprintForm.ts +++ b/web/src/admin/blueprints/BlueprintForm.ts @@ -31,6 +31,9 @@ export enum BlueprintSource { @customElement("ak-blueprint-form") export class BlueprintForm extends ModelForm { + public static override verboseName = msg("Blueprint"); + public static override verboseNamePlural = msg("Blueprints"); + @state() protected source: BlueprintSource = BlueprintSource.File; diff --git a/web/src/admin/blueprints/BlueprintImportForm.ts b/web/src/admin/blueprints/BlueprintImportForm.ts index 66965af04a45..6efd31d32ad9 100644 --- a/web/src/admin/blueprints/BlueprintImportForm.ts +++ b/web/src/admin/blueprints/BlueprintImportForm.ts @@ -4,6 +4,7 @@ import "#elements/forms/HorizontalFormElement"; import "#components/ak-toggle-group"; import { DEFAULT_CONFIG } from "#common/api/config"; +import { PFSize } from "#common/enums"; import { Form } from "#elements/forms/Form"; import { PreventFormSubmit } from "#elements/forms/helpers"; @@ -34,6 +35,14 @@ import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList export class BlueprintImportForm extends Form { static styles: CSSResult[] = [...super.styles, PFDescriptionList, PFBanner]; + public static override verboseName = msg("Flow Blueprint"); + public static override verboseNamePlural = msg("Flow Blueprints"); + public static override createLabel = msg("Import"); + public static override submitVerb = msg("Import"); + public static override submittingVerb = msg("Importing"); + + public override size = PFSize.Medium; + @state() protected result: BlueprintImportResult | null = null; @@ -98,6 +107,11 @@ export class BlueprintImportForm extends Form ${this.source === BlueprintSource.Upload ? html` + ${this.findSlotted("banner-warning") + ? html`
+ +
` + : null} ${AKLabel( { @@ -123,24 +137,20 @@ export class BlueprintImportForm extends Form - ${this.hasSlotted("read-more-link") + ${this.findSlotted("read-more-link") ? html`

${msg("Read more about")} 

` - : nothing} + : null}
- ${this.hasSlotted("banner-warning") - ? html`
- -
` - : nothing} ` - : nothing} + : null} ${this.source === BlueprintSource.File ? html` => { const items = await new ManagedApi( DEFAULT_CONFIG, diff --git a/web/src/admin/blueprints/BlueprintListPage.ts b/web/src/admin/blueprints/BlueprintListPage.ts index 84fea183d029..fc3d54d6800b 100644 --- a/web/src/admin/blueprints/BlueprintListPage.ts +++ b/web/src/admin/blueprints/BlueprintListPage.ts @@ -14,10 +14,14 @@ import { DEFAULT_CONFIG } from "#common/api/config"; import { EVENT_REFRESH } from "#common/constants"; import { docLink } from "#common/global"; +import { IconEditButton, modalInvoker, ModalInvokerButton } from "#elements/dialogs"; +import { IconPermissionButton } from "#elements/dialogs/components/IconPermissionButton"; import { PaginatedResponse, TableColumn, Timestamp } from "#elements/table/Table"; import { TablePage } from "#elements/table/TablePage"; import { SlottedTemplateResult } from "#elements/types"; +import { BlueprintForm } from "#admin/blueprints/BlueprintForm"; + import { BlueprintInstance, BlueprintInstanceStatusEnum, @@ -26,8 +30,9 @@ import { } from "@goauthentik/api"; import { msg, str } from "@lit/localize"; -import { CSSResult, html, nothing, TemplateResult } from "lit"; -import { customElement, property } from "lit/decorators.js"; +import { CSSResult, html, nothing } from "lit"; +import { guard } from "lit-html/directives/guard.js"; +import { customElement } from "lit/decorators.js"; import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; @@ -58,27 +63,28 @@ export function formatBlueprintDescription(item: BlueprintInstance): string | nu @customElement("ak-blueprint-list") export class BlueprintListPage extends TablePage { + static styles: CSSResult[] = [...super.styles, PFDescriptionList]; + protected override searchEnabled = true; + public pageTitle = msg("Blueprints"); public pageDescription = msg("Automate and template configuration within authentik."); public pageIcon = "pf-icon pf-icon-blueprint"; - expandable = true; - checkbox = true; - clearOnRefresh = true; + public override expandable = true; + public override checkbox = true; + public override clearOnRefresh = true; + public override searchPlaceholder = msg("Search for a blueprint by name or path..."); - @property() - order = "name"; + public override order = "name"; - static styles: CSSResult[] = [...super.styles, PFDescriptionList]; - - async apiEndpoint(): Promise> { + protected override async apiEndpoint(): Promise> { return new ManagedApi(DEFAULT_CONFIG).managedBlueprintsList( await this.defaultEndpointConfig(), ); } - protected columns: TableColumn[] = [ + protected override columns: TableColumn[] = [ [msg("Name"), "name"], [msg("Status"), "status"], [msg("Last applied"), "last_applied"], @@ -86,7 +92,7 @@ export class BlueprintListPage extends TablePage { [msg("Actions"), null, msg("Row Actions")], ]; - renderToolbarSelected(): TemplateResult { + protected override renderToolbarSelected(): SlottedTemplateResult { const disabled = this.selectedElements.length < 1; return html` { `; } - renderExpanded(item: BlueprintInstance): TemplateResult { + protected override renderExpanded(item: BlueprintInstance): SlottedTemplateResult { const [appLabel, modelName] = ModelEnum.AuthentikBlueprintsBlueprintinstance.split("."); return html`
@@ -144,7 +150,7 @@ export class BlueprintListPage extends TablePage {
`; } - row(item: BlueprintInstance): SlottedTemplateResult[] { + protected override row(item: BlueprintInstance): SlottedTemplateResult[] { const description = formatBlueprintDescription(item); return [ @@ -152,30 +158,16 @@ export class BlueprintListPage extends TablePage { ${description ? html`` : nothing}`, - html`${BlueprintStatus(item)}`, + BlueprintStatus(item), Timestamp(item.lastApplied), html``, - html`
- - ${msg("Save Changes")} - ${msg("Update Blueprint")} - - - - - + html`
+ ${IconEditButton(BlueprintForm, item.pk, item.name)} + ${IconPermissionButton(item.name, { + model: ModelEnum.AuthentikBlueprintsBlueprintinstance, + objectPk: item.pk, + })} + { ]; } - renderObjectCreate(): TemplateResult { - return html` - - ${msg("Create")} - ${msg("Create Blueprint Instance")} - - - - - ${msg("Import")} - ${msg("Import Blueprint")} - - ${msg("Flow Examples")} - - ${msg( - "Warning: Blueprint files may contain objects such as users, policies and expression.", - )}
${msg( - "You should only import files from trusted sources and review blueprints before importing them.", - )} -
- - - - `; + protected override renderObjectCreate(): SlottedTemplateResult { + return guard([], () => { + return [ + ModalInvokerButton(BlueprintForm), + html``, + ]; + }); } } diff --git a/web/src/admin/brands/BrandForm.ts b/web/src/admin/brands/BrandForm.ts index 0c70670d2b95..7590cf73ada3 100644 --- a/web/src/admin/brands/BrandForm.ts +++ b/web/src/admin/brands/BrandForm.ts @@ -36,6 +36,9 @@ import { customElement } from "lit/decorators.js"; @customElement("ak-brand-form") export class BrandForm extends ModelForm { + public static override verboseName = msg("Brand"); + public static override verboseNamePlural = msg("Brands"); + loadInstance(pk: string): Promise { return new CoreApi(DEFAULT_CONFIG).coreBrandsRetrieve({ brandUuid: pk, @@ -192,7 +195,7 @@ export class BrandForm extends ModelForm {
{

{ [msg("Actions"), null, msg("Row Actions")], ]; - renderToolbarSelected(): TemplateResult { + protected override renderToolbarSelected(): SlottedTemplateResult { const disabled = this.selectedElements.length < 1; + return html` { `; } - row(item: Brand): SlottedTemplateResult[] { + protected override row(item: Brand): SlottedTemplateResult[] { return [ - html`${item.domain}`, - html`${item.brandingTitle}`, + item.domain, + item.brandingTitle || msg("-"), html``, - html`
- - ${msg("Save Changes")} - ${msg("Update Brand")} - - - + html`
+ ${IconEditButton(BrandForm, item.brandUuid, item.brandingTitle)} { ]; } - renderObjectCreate(): TemplateResult { - return html` - - ${msg("Create Brand")} - ${msg("New Brand")} - - - - `; + protected override renderObjectCreate(): SlottedTemplateResult { + return ModalInvokerButton(BrandForm); } } diff --git a/web/src/admin/crypto/CertificateGenerateForm.ts b/web/src/admin/crypto/CertificateGenerateForm.ts index d47686314361..898501cd7226 100644 --- a/web/src/admin/crypto/CertificateGenerateForm.ts +++ b/web/src/admin/crypto/CertificateGenerateForm.ts @@ -1,3 +1,5 @@ +import "#components/ak-text-input"; +import "#components/ak-number-input"; import "#elements/forms/Radio"; import "#elements/forms/HorizontalFormElement"; @@ -17,7 +19,12 @@ import { html, TemplateResult } from "lit"; import { customElement } from "lit/decorators.js"; @customElement("ak-crypto-certificate-generate-form") -export class CertificateKeyPairForm extends Form { +export class CryptoCertificateGenerateForm extends Form { + public static override verboseName = msg("Certificate-Key Pair"); + public static override verboseNamePlural = msg("Certificate-Key Pairs"); + public static override createLabel = msg("Generate"); + public static override submitVerb = msg("Generate"); + getSuccessMessage(): string { return msg("Successfully generated certificate-key pair."); } @@ -29,22 +36,31 @@ export class CertificateKeyPairForm extends Form { } protected override renderForm(): TemplateResult { - return html` - - - - -

- ${msg("Optional, comma-separated SubjectAlt Names.")} -

-
- - - + placeholder=${msg("Type a name for this certificate...")} + autofocus + autocomplete="off" + spellcheck="false" + > + + + + { declare global { interface HTMLElementTagNameMap { - "ak-crypto-certificate-generate-form": CertificateKeyPairForm; + "ak-crypto-certificate-generate-form": CryptoCertificateGenerateForm; } } diff --git a/web/src/admin/crypto/CertificateKeyPairForm.ts b/web/src/admin/crypto/CertificateKeyPairForm.ts index e9d56b6b3707..5a19e1431cb2 100644 --- a/web/src/admin/crypto/CertificateKeyPairForm.ts +++ b/web/src/admin/crypto/CertificateKeyPairForm.ts @@ -1,4 +1,5 @@ import "#components/ak-secret-textarea-input"; +import "#components/ak-text-input"; import "#elements/CodeMirror"; import "#elements/forms/HorizontalFormElement"; @@ -14,7 +15,13 @@ import { customElement } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; @customElement("ak-crypto-certificate-form") -export class CertificateKeyPairForm extends ModelForm { +export class CryptoCertificateForm extends ModelForm { + public static override verboseName = msg("Certificate-Key Pair"); + public static override verboseNamePlural = msg("Certificate-Key Pairs"); + public static override createLabel = msg("Import Existing"); + public static override submitVerb = msg("Import"); + public static override submittingVerb = msg("Importing"); + loadInstance(pk: string): Promise { return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsRetrieve({ kpUuid: pk, @@ -40,14 +47,16 @@ export class CertificateKeyPairForm extends ModelForm - - + return html` { - expandable = true; - checkbox = true; - clearOnRefresh = true; + static styles: CSSResult[] = [...super.styles, PFDescriptionList]; + + public override expandable = true; + public override checkbox = true; + public override clearOnRefresh = true; + public override searchPlaceholder = msg("Search for a certificate or key name..."); protected override searchEnabled = true; + public pageTitle = msg("Certificate-Key Pairs"); public pageDescription = msg( "Import certificates of external providers or create certificates to sign requests with.", ); public pageIcon = "pf-icon pf-icon-key"; - @property() - order = "name"; - - static styles: CSSResult[] = [...super.styles, PFDescriptionList]; + public override order = "name"; async apiEndpoint(): Promise> { return new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsList({ @@ -53,7 +58,7 @@ export class CertificateKeyPairListPage extends TablePage { [msg("Actions"), null, msg("Row Actions")], ]; - renderToolbarSelected(): TemplateResult { + protected override renderToolbarSelected(): SlottedTemplateResult { const disabled = this.selectedElements.length < 1; const count = this.selectedElements.length; return html` { `; } - row(item: CertificateKeyPair): SlottedTemplateResult[] { + protected override row(item: CertificateKeyPair): SlottedTemplateResult[] { let managedSubText = msg("Managed by authentik"); if (item.managed && item.managed.startsWith("goauthentik.io/crypto/discovered")) { managedSubText = msg("Managed by authentik (Discovered)"); @@ -130,7 +135,7 @@ export class CertificateKeyPairListPage extends TablePage { ]; } - renderExpanded(item: CertificateKeyPair): TemplateResult { + protected override renderExpanded(item: CertificateKeyPair): SlottedTemplateResult { return html`
@@ -188,24 +193,13 @@ export class CertificateKeyPairListPage extends TablePage {
`; } - renderObjectCreate(): TemplateResult { - return html` - - ${msg("Import")} - ${msg("Import Existing Certificate-Key Pair")} - - - - - ${msg("Generate")} - ${msg("Generate New Certificate-Key Pair")} - - - - - `; + protected override renderObjectCreate(): SlottedTemplateResult { + return [ + ModalInvokerButton(CryptoCertificateForm), + ModalInvokerButton(CryptoCertificateGenerateForm, null, { + kind: "secondary", + }), + ]; } } diff --git a/web/src/admin/endpoints/DeviceAccessGroupForm.ts b/web/src/admin/endpoints/DeviceAccessGroupForm.ts index d2994b7a8b89..1870f98868df 100644 --- a/web/src/admin/endpoints/DeviceAccessGroupForm.ts +++ b/web/src/admin/endpoints/DeviceAccessGroupForm.ts @@ -2,6 +2,7 @@ import "#components/ak-text-input"; import "#elements/forms/HorizontalFormElement"; import { DEFAULT_CONFIG } from "#common/api/config"; +import { PFSize } from "#common/enums"; import { ModelForm } from "#elements/forms/ModelForm"; import { WithBrandConfig } from "#elements/mixins/branding"; @@ -20,35 +21,42 @@ import { ifDefined } from "lit/directives/if-defined.js"; */ @customElement("ak-endpoints-device-access-groups-form") export class DeviceAccessGroupForm extends WithBrandConfig(ModelForm) { - loadInstance(pk: string): Promise { + public static override verboseName = msg("Device Access Group"); + public static override verboseNamePlural = msg("Device Access Groups"); + + public override size = PFSize.Small; + + protected override loadInstance(pk: string): Promise { return new EndpointsApi(DEFAULT_CONFIG).endpointsDeviceAccessGroupsRetrieve({ pbmUuid: pk, }); } - getSuccessMessage(): string { + public override getSuccessMessage(): string { return this.instance ? msg("Successfully updated group.") : msg("Successfully created group."); } - async send(data: DeviceAccessGroup): Promise { + protected override async send(data: DeviceAccessGroup): Promise { if (this.instance) { return new EndpointsApi(DEFAULT_CONFIG).endpointsDeviceAccessGroupsPartialUpdate({ pbmUuid: this.instance.pbmUuid, patchedDeviceAccessGroupRequest: data, }); } + return new EndpointsApi(DEFAULT_CONFIG).endpointsDeviceAccessGroupsCreate({ deviceAccessGroupRequest: data as unknown as DeviceAccessGroupRequest, }); } - renderForm() { + protected override renderForm() { return html``; diff --git a/web/src/admin/endpoints/DeviceAccessGroupsListPage.ts b/web/src/admin/endpoints/DeviceAccessGroupsListPage.ts index a218b9eb2844..f3e92aa06a36 100644 --- a/web/src/admin/endpoints/DeviceAccessGroupsListPage.ts +++ b/web/src/admin/endpoints/DeviceAccessGroupsListPage.ts @@ -6,10 +6,13 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; import { DEFAULT_CONFIG } from "#common/api/config"; +import { IconEditButton, ModalInvokerButton } from "#elements/dialogs"; import { PaginatedResponse, TableColumn } from "#elements/table/Table"; import { TablePage } from "#elements/table/TablePage"; import { SlottedTemplateResult } from "#elements/types"; +import { DeviceAccessGroupForm } from "#admin/endpoints/DeviceAccessGroupForm"; + import { DeviceAccessGroup, EndpointsApi } from "@goauthentik/api"; import { msg } from "@lit/localize"; @@ -18,59 +21,44 @@ import { customElement } from "lit/decorators.js"; @customElement("ak-endpoints-device-access-groups-list") export class DeviceAccessGroupsListPage extends TablePage { - public pageIcon = "pf-icon pf-icon-server-group "; - public pageTitle = msg("Device access groups"); - public pageDescription = msg("Create groups of devices to manage access."); - protected searchEnabled: boolean = true; protected columns: TableColumn[] = [ [msg("Name"), "name"], [msg("Actions"), null, msg("Row Actions")], ]; - checkbox = true; - expandable = true; + public override pageIcon = "pf-icon pf-icon-server-group "; + public override pageTitle = msg("Device access groups"); + public override pageDescription = msg("Create groups of devices to manage access."); + public override searchPlaceholder = msg("Search device groups by name..."); + + public override checkbox = true; + public override expandable = true; - async apiEndpoint(): Promise> { + protected override async apiEndpoint(): Promise> { return new EndpointsApi(DEFAULT_CONFIG).endpointsDeviceAccessGroupsList( await this.defaultEndpointConfig(), ); } - row(item: DeviceAccessGroup): SlottedTemplateResult[] { + protected override row(item: DeviceAccessGroup): SlottedTemplateResult[] { return [ - html`${item.name}`, - html`
- - ${msg("Save Changes")} - ${msg("Update Group")} - - - - + // --- + item.name, + html`
+ ${IconEditButton(DeviceAccessGroupForm, item.pbmUuid)}
`, ]; } - renderExpanded(item: DeviceAccessGroup) { + protected override renderExpanded(item: DeviceAccessGroup) { return html`
`; } - renderObjectCreate() { - return html` - ${msg("Create")} - ${msg("Create Device Group")} - - - `; + protected override renderObjectCreate(): SlottedTemplateResult { + return ModalInvokerButton(DeviceAccessGroupForm); } renderToolbarSelected() { diff --git a/web/src/admin/endpoints/ak-endpoints-device-group-search.ts b/web/src/admin/endpoints/ak-endpoints-device-group-search.ts index faf16a6b2006..aa07fcf69ede 100644 --- a/web/src/admin/endpoints/ak-endpoints-device-group-search.ts +++ b/web/src/admin/endpoints/ak-endpoints-device-group-search.ts @@ -10,6 +10,7 @@ import { EndpointsDeviceAccessGroupsListRequest, } from "@goauthentik/api"; +import { msg } from "@lit/localize"; import { html } from "lit"; import { customElement, property, query } from "lit/decorators.js"; @@ -85,6 +86,7 @@ export class EndpointsDeviceAccessGroupSearch extends CustomListenerElement(AKEl render() { return html` { - this.connectorTypes = types; - }); - } + protected apiEndpoint = (requestInit?: RequestInit): Promise => { + return this.#api.endpointsConnectorsTypesList(requestInit); + }; - render(): TemplateResult { - return html` - - ) => { - if (!this.wizard) return; - const idx = this.wizard.steps.indexOf("initial") + 1; - // Exclude all current steps starting with type-, - // this happens when the user selects a type and then goes back - this.wizard.steps = this.wizard.steps.filter( - (step) => !step.startsWith("type-"), - ); - this.wizard.steps.splice( - idx, - 0, - `type-${ev.detail.component}-${ev.detail.modelName}`, - ); - this.wizard.isValid = true; - }} - > -
-

- ${msg( - "Connectors are required to create devices. Depending on connector type, agents either directly talk to them or they talk to and external API to create devices.", - )} -

-
-
- ${this.connectorTypes.map((type) => { - return html` - - ${StrictUnsafe(type.component)} - - `; - })} - -
- `; + protected override renderInitialPageContent(): SlottedTemplateResult { + return msg( + "Connectors are required to create devices. Depending on connector type, agents either directly talk to them or they talk to and external API to create devices.", + ); } } declare global { interface HTMLElementTagNameMap { - "ak-endpoint-connector-wizard": EndpointConnectorWizard; + "ak-endpoint-connector-wizard": AKEndpointConnectorWizard; } } diff --git a/web/src/admin/endpoints/connectors/ConnectorsListPage.ts b/web/src/admin/endpoints/connectors/ConnectorsListPage.ts index 442748ba120d..1ab16fe31515 100644 --- a/web/src/admin/endpoints/connectors/ConnectorsListPage.ts +++ b/web/src/admin/endpoints/connectors/ConnectorsListPage.ts @@ -7,70 +7,58 @@ import "#elements/forms/ModalForm"; import { DEFAULT_CONFIG } from "#common/api/config"; -import { CustomFormElementTagName } from "#elements/forms/unsafe"; +import { IconEditButtonByTagName, ModalInvokerButton } from "#elements/dialogs"; import { PaginatedResponse, TableColumn } from "#elements/table/Table"; import { TablePage } from "#elements/table/TablePage"; import { SlottedTemplateResult } from "#elements/types"; -import { StrictUnsafe } from "#elements/utils/unsafe"; + +import { AKEndpointConnectorWizard } from "#admin/endpoints/connectors/ConnectorWizard"; import { Connector, EndpointsApi } from "@goauthentik/api"; -import { msg, str } from "@lit/localize"; +import { msg } from "@lit/localize"; import { html } from "lit"; import { customElement } from "lit/decorators.js"; @customElement("ak-endpoints-connectors-list") export class ConnectorsListPage extends TablePage { - public pageIcon = "pf-icon pf-icon-data-source"; - public pageTitle = msg("Connectors"); - public pageDescription = msg( + public override searchPlaceholder = msg("Search connectors by name or type..."); + public override pageIcon = "pf-icon pf-icon-data-source"; + public override pageTitle = msg("Connectors"); + public override pageDescription = msg( "Configure how devices connect with authentik and ingest external device data.", ); - protected searchEnabled: boolean = true; - protected columns: TableColumn[] = [ + protected override searchEnabled: boolean = true; + protected override columns: TableColumn[] = [ [msg("Name"), "name"], [msg("Type")], [msg("Actions"), null, msg("Row Actions")], ]; - checkbox = true; + public override checkbox = true; - async apiEndpoint(): Promise> { + protected override async apiEndpoint(): Promise> { return new EndpointsApi(DEFAULT_CONFIG).endpointsConnectorsList( await this.defaultEndpointConfig(), ); } - row(item: Connector): SlottedTemplateResult[] { + protected override row(item: Connector): SlottedTemplateResult[] { return [ html`${item.name}`, - html`${item.verboseName}`, - html`
- - ${StrictUnsafe(item.component, { - slot: "form", - instancePk: item.connectorUuid, - submitLabel: msg("Save Changes"), - headline: msg(str`Update ${item.verboseName}`, { - id: "form.headline.update", - }), - })} - - + item.verboseName, + html`
+ ${IconEditButtonByTagName(item.component, item.connectorUuid, item.verboseName)}
`, ]; } - renderObjectCreate() { - return html` `; + protected override renderObjectCreate(): SlottedTemplateResult { + return ModalInvokerButton(AKEndpointConnectorWizard); } - renderToolbarSelected() { + protected override renderToolbarSelected(): SlottedTemplateResult { const disabled = this.selectedElements.length < 1; return html`
diff --git a/web/src/admin/endpoints/connectors/agent/EnrollmentTokenForm.ts b/web/src/admin/endpoints/connectors/agent/EnrollmentTokenForm.ts index fa718ff1be88..78dd70805ce0 100644 --- a/web/src/admin/endpoints/connectors/agent/EnrollmentTokenForm.ts +++ b/web/src/admin/endpoints/connectors/agent/EnrollmentTokenForm.ts @@ -29,13 +29,18 @@ const EXPIRATION_DURATION = 30 * 60 * 1000; // 30 minutes */ @customElement("ak-endpoints-agent-enrollment-token-form") export class EnrollmentTokenForm extends WithBrandConfig(ModelForm) { + #api = new EndpointsApi(DEFAULT_CONFIG); + + public static override verboseName = msg("Enrollment Token"); + public static override verboseNamePlural = msg("Enrollment Tokens"); + protected expirationMinimumDate = new Date(); @state() protected expiresAt: Date | null = new Date(Date.now() + EXPIRATION_DURATION); @property({ type: String, attribute: "connector-id" }) - public connectorID?: string; + public connectorID: string | null = null; public override reset(): void { super.reset(); @@ -57,25 +62,25 @@ export class EnrollmentTokenForm extends WithBrandConfig(ModelForm { + protected override async send(data: EnrollmentToken): Promise { if (!this.instance) { data.connector = this.connectorID || ""; } else { data.connector = this.instance.connector; } if (this.instance) { - return new EndpointsApi(DEFAULT_CONFIG).endpointsAgentsEnrollmentTokensPartialUpdate({ + return this.#api.endpointsAgentsEnrollmentTokensPartialUpdate({ tokenUuid: this.instance.tokenUuid, patchedEnrollmentTokenRequest: data, }); } - return new EndpointsApi(DEFAULT_CONFIG).endpointsAgentsEnrollmentTokensCreate({ + return this.#api.endpointsAgentsEnrollmentTokensCreate({ enrollmentTokenRequest: data as unknown as EnrollmentTokenRequest, }); } @@ -102,7 +107,7 @@ export class EnrollmentTokenForm extends WithBrandConfig(ModelForm - `; + `; } //#endregion diff --git a/web/src/admin/endpoints/connectors/agent/EnrollmentTokenListPage.ts b/web/src/admin/endpoints/connectors/agent/EnrollmentTokenListPage.ts index 18dd83e86680..17d75c75ff17 100644 --- a/web/src/admin/endpoints/connectors/agent/EnrollmentTokenListPage.ts +++ b/web/src/admin/endpoints/connectors/agent/EnrollmentTokenListPage.ts @@ -1,6 +1,5 @@ import "#admin/rbac/ObjectPermissionModal"; import "#admin/endpoints/connectors/agent/EnrollmentTokenForm"; -import "#admin/endpoints/connectors/agent/ak-enrollment-token-copy-button"; import "#elements/buttons/SpinnerButton/index"; import "#elements/forms/DeleteBulkForm"; import "#elements/forms/ModalForm"; @@ -9,9 +8,14 @@ import "#components/ak-status-label"; import { DEFAULT_CONFIG } from "#common/api/config"; +import { IconEnrollmentTokenCopyButton } from "#elements/buttons/IconEnrollmentTokenCopyButton"; +import { IconEditButton, ModalInvokerButton } from "#elements/dialogs"; +import { IconPermissionButton } from "#elements/dialogs/components/IconPermissionButton"; import { PaginatedResponse, Table, TableColumn, Timestamp } from "#elements/table/Table"; import { SlottedTemplateResult } from "#elements/types"; +import { EnrollmentTokenForm } from "#admin/endpoints/connectors/agent/EnrollmentTokenForm"; + import { AgentConnector, EndpointsApi, EnrollmentToken, ModelEnum } from "@goauthentik/api"; import { msg } from "@lit/localize"; @@ -20,19 +24,23 @@ import { customElement, property } from "lit/decorators.js"; @customElement("ak-endpoints-agent-enrollment-token-list") export class EnrollmentTokenListPage extends Table { - checkbox = true; - clearOnRefresh = true; + #api = new EndpointsApi(DEFAULT_CONFIG); protected override searchEnabled = true; + protected emptyStateMessage = msg("No enrollment tokens found for this connector."); + + public override checkbox = true; + public override clearOnRefresh = true; + + public override searchPlaceholder = msg("Search for an enrollment token..."); - @property() - order = "name"; + public override order = "name"; @property({ attribute: false }) - connector?: AgentConnector; + public connector: AgentConnector | null = null; - async apiEndpoint(): Promise> { - return new EndpointsApi(DEFAULT_CONFIG).endpointsAgentsEnrollmentTokensList({ + protected override async apiEndpoint(): Promise> { + return this.#api.endpointsAgentsEnrollmentTokensList({ ...(await this.defaultEndpointConfig()), connector: this.connector?.connectorUuid, }); @@ -46,8 +54,9 @@ export class EnrollmentTokenListPage extends Table { [msg("Actions"), null, msg("Row Actions")], ]; - renderToolbarSelected(): TemplateResult { + protected override renderToolbarSelected(): TemplateResult { const disabled = this.selectedElements.length < 1; + return html` { ]; }} .usedBy=${(item: EnrollmentToken) => { - return new EndpointsApi(DEFAULT_CONFIG).endpointsAgentsEnrollmentTokensUsedByList({ + return this.#api.endpointsAgentsEnrollmentTokensUsedByList({ tokenUuid: item.tokenUuid, }); }} .delete=${(item: EnrollmentToken) => { - return new EndpointsApi(DEFAULT_CONFIG).endpointsAgentsEnrollmentTokensDestroy({ + return this.#api.endpointsAgentsEnrollmentTokensDestroy({ tokenUuid: item.tokenUuid, }); }} @@ -74,54 +83,27 @@ export class EnrollmentTokenListPage extends Table { `; } - row(item: EnrollmentToken): SlottedTemplateResult[] { + protected override row(item: EnrollmentToken): SlottedTemplateResult[] { return [ - html`${item.name}`, - html`${item.deviceGroupObj?.name || "-"}`, + item.name, + item.deviceGroupObj?.name || msg("-"), html``, Timestamp(item.expires && item.expiring ? item.expires : null), - html`
- - ${msg("Save Changes")} - ${msg("Update Enrollment Token")} - - - - - - - - - - - + html`
+ ${IconEditButton(EnrollmentTokenForm, item.tokenUuid, item.name)} + ${IconPermissionButton(item.name, { + model: ModelEnum.AuthentikEndpointsConnectorsAgentEnrollmenttoken, + objectPk: item.tokenUuid, + })} + ${IconEnrollmentTokenCopyButton(item.tokenUuid)}
`, ]; } - renderObjectCreate(): TemplateResult { - return html` - - ${msg("Create")} - ${msg("Create Enrollment Token")} - - - - - `; + protected override renderObjectCreate(): SlottedTemplateResult { + return ModalInvokerButton(EnrollmentTokenForm, { + connectorID: this.connector?.connectorUuid, + }); } } diff --git a/web/src/admin/endpoints/connectors/agent/ak-enrollment-token-copy-button.ts b/web/src/admin/endpoints/connectors/agent/ak-enrollment-token-copy-button.ts deleted file mode 100644 index 0ef9bb85144e..000000000000 --- a/web/src/admin/endpoints/connectors/agent/ak-enrollment-token-copy-button.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { DEFAULT_CONFIG } from "#common/api/config"; -import { writeToClipboard } from "#common/clipboard"; - -import TokenCopyButton from "#elements/buttons/TokenCopyButton/ak-token-copy-button"; - -import { EndpointsApi } from "@goauthentik/api"; - -import { msg } from "@lit/localize"; -import { customElement } from "lit/decorators.js"; - -@customElement("ak-enrollment-token-copy-button") -export class EnrollmentTokenCopyButton extends TokenCopyButton { - public override entityLabel = msg("Enrollment Token"); - - public override callAction(): Promise { - if (!this.identifier) { - throw new TypeError("No `identifier` set for `EnrollmentTokenCopyButton`"); - } - - // Safari permission hack. - const text = new ClipboardItem({ - "text/plain": new EndpointsApi(DEFAULT_CONFIG) - .endpointsAgentsEnrollmentTokensViewKeyRetrieve({ - tokenUuid: this.identifier, - }) - .then((tokenView) => new Blob([tokenView.key], { type: "text/plain" })), - }); - - return writeToClipboard(text, this.entityLabel).then(() => null); - } -} - -declare global { - interface HTMLElementTagNameMap { - "ak-enrollment-token-copy-button": EnrollmentTokenCopyButton; - } -} diff --git a/web/src/admin/endpoints/connectors/fleet/FleetConnectorForm.ts b/web/src/admin/endpoints/connectors/fleet/FleetConnectorForm.ts index 804688f9803d..79ec4fa37564 100644 --- a/web/src/admin/endpoints/connectors/fleet/FleetConnectorForm.ts +++ b/web/src/admin/endpoints/connectors/fleet/FleetConnectorForm.ts @@ -42,7 +42,8 @@ export class FleetConnectorForm extends ModelForm { renderForm() { return html` { { diff --git a/web/src/admin/endpoints/connectors/gdtc/GoogleChromeConnectorForm.ts b/web/src/admin/endpoints/connectors/gdtc/GoogleChromeConnectorForm.ts index ea2108669c50..6e83d3786cc4 100644 --- a/web/src/admin/endpoints/connectors/gdtc/GoogleChromeConnectorForm.ts +++ b/web/src/admin/endpoints/connectors/gdtc/GoogleChromeConnectorForm.ts @@ -43,7 +43,8 @@ export class GoogleChromeConnectorForm extends ModelForm { - public pageTitle = msg("Devices"); - public pageDescription = ""; - public pageIcon = "fa fa-laptop"; - - checkbox = true; - - static styles: CSSResult[] = [ + public static styles: CSSResult[] = [ ...super.styles, PFGrid, PFBanner, @@ -37,6 +31,13 @@ export class DeviceListPage extends TablePage { } `, ]; + public override pageTitle = msg("Devices"); + public override pageDescription = ""; + public override pageIcon = "fa fa-laptop"; + + public override checkbox = true; + + public override searchPlaceholder = msg("Search devices by name, OS, or group..."); protected searchEnabled: boolean = true; protected columns: TableColumn[] = [ @@ -59,7 +60,7 @@ export class DeviceListPage extends TablePage { ); } - protected renderEmpty(inner?: TemplateResult): TemplateResult { + protected renderEmpty(inner?: TemplateResult): SlottedTemplateResult { return super.renderEmpty(html` ${inner ? inner diff --git a/web/src/admin/enterprise/EnterpriseLicenseForm.ts b/web/src/admin/enterprise/EnterpriseLicenseForm.ts index 48cc6b2b60ba..d07eeeeb8c9e 100644 --- a/web/src/admin/enterprise/EnterpriseLicenseForm.ts +++ b/web/src/admin/enterprise/EnterpriseLicenseForm.ts @@ -1,4 +1,5 @@ import "#components/ak-secret-textarea-input"; +import "#components/ak-text-input"; import "#elements/CodeMirror"; import "#elements/forms/HorizontalFormElement"; @@ -6,16 +7,24 @@ import { DEFAULT_CONFIG } from "#common/api/config"; import { EVENT_REFRESH_ENTERPRISE } from "#common/constants"; import { ModelForm } from "#elements/forms/ModelForm"; +import { SlottedTemplateResult } from "#elements/types"; import { ifPresent } from "#elements/utils/attributes"; import { EnterpriseApi, License } from "@goauthentik/api"; import { msg } from "@lit/localize"; -import { html, TemplateResult } from "lit"; +import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; @customElement("ak-enterprise-license-form") export class EnterpriseLicenseForm extends ModelForm { + public static override verboseName = msg("Enterprise License"); + public static override verboseNamePlural = msg("Enterprise Licenses"); + public static override createLabel = msg("Install"); + public static override submitVerb = msg("Install"); + + #api = new EnterpriseApi(DEFAULT_CONFIG); + @state() protected installID: string | null = null; @@ -26,7 +35,7 @@ export class EnterpriseLicenseForm extends ModelForm { } loadInstance(pk: string): Promise { - return new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseRetrieve({ + return this.#api.enterpriseLicenseRetrieve({ licenseUuid: pk, }); } @@ -38,19 +47,17 @@ export class EnterpriseLicenseForm extends ModelForm { } async load(): Promise { - this.installID = ( - await new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseInstallIdRetrieve() - ).installId; + this.installID = (await this.#api.enterpriseLicenseInstallIdRetrieve()).installId; } async send(data: License): Promise { return ( this.instance - ? new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicensePartialUpdate({ + ? this.#api.enterpriseLicensePartialUpdate({ licenseUuid: this.instance.licenseUuid || "", patchedLicenseRequest: data, }) - : new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseCreate({ + : this.#api.enterpriseLicenseCreate({ licenseRequest: data, }) ).then((data) => { @@ -59,19 +66,21 @@ export class EnterpriseLicenseForm extends ModelForm { }); } - protected override renderForm(): TemplateResult { - return html` - - + protected override renderForm(): SlottedTemplateResult { + return html` + { - checkbox = true; - clearOnRefresh = true; - - protected override searchEnabled = true; - public pageTitle = msg("Licenses"); - public pageDescription = msg("Manage enterprise licenses"); - public pageIcon = "pf-icon pf-icon-key"; - - @property() - order = "name"; - - @state() - forecast?: LicenseForecast; - - @state() - summary?: LicenseSummary; - - @state() - installID?: string; - - static styles: CSSResult[] = [ + public static styles: CSSResult[] = [ ...super.styles, PFGrid, PFBanner, @@ -74,6 +57,25 @@ export class EnterpriseLicenseListPage extends TablePage { `, ]; + public override checkbox = true; + public override clearOnRefresh = true; + + protected override searchEnabled = true; + public override pageTitle = msg("Licenses"); + public override pageDescription = msg("Manage enterprise licenses"); + public override pageIcon = "pf-icon pf-icon-key"; + public override searchPlaceholder = msg("Search for a license by name..."); + public override order = "name"; + + @state() + protected forecast?: LicenseForecast; + + @state() + protected summary?: LicenseSummary; + + @state() + protected installID?: string; + async apiEndpoint(): Promise> { this.forecast = await new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseForecastRetrieve(); this.summary = await new EnterpriseApi(DEFAULT_CONFIG).enterpriseLicenseSummaryRetrieve({ @@ -96,7 +98,7 @@ export class EnterpriseLicenseListPage extends TablePage { // TODO: Make this more generic, maybe automatically get the plural name // of the object to use in the renderEmpty - renderEmpty(inner?: TemplateResult): TemplateResult { + protected override renderEmpty(inner?: SlottedTemplateResult): SlottedTemplateResult { return super.renderEmpty(html` ${inner ? inner @@ -110,7 +112,7 @@ export class EnterpriseLicenseListPage extends TablePage { `); } - renderToolbarSelected(): TemplateResult { + protected override renderToolbarSelected(): SlottedTemplateResult { const disabled = this.selectedElements.length < 1; return html` { `; } - renderSectionBefore(): TemplateResult { + protected override renderSectionBefore(): SlottedTemplateResult { const { externalUsers = 0, internalUsers = 0, @@ -219,18 +221,9 @@ export class EnterpriseLicenseListPage extends TablePage { html`
${msg(str`Internal: ${item.internalUsers}`)}
${msg(str`External: ${item.externalUsers}`)}
`, html` ${item.expiry?.toLocaleString()} `, - html`
- - ${msg("Save Changes")} - ${msg("Update License")} - - - - + html`
+ ${IconEditButton(EnterpriseLicenseForm, item.licenseUuid, item.name)} + { ]; } - renderGetLicenseCard() { + protected renderGetLicenseCard() { const renderSpinner = () => html`
@@ -277,15 +270,8 @@ export class EnterpriseLicenseListPage extends TablePage {
`; } - renderObjectCreate(): TemplateResult { - return html` - - ${msg("Install")} - ${msg("Install License")} - - - - `; + protected override renderObjectCreate(): SlottedTemplateResult { + return ModalInvokerButton(EnterpriseLicenseForm); } } diff --git a/web/src/admin/events/DataExportListPage.ts b/web/src/admin/events/DataExportListPage.ts index ffae823f076e..e4ab7f69cc3b 100644 --- a/web/src/admin/events/DataExportListPage.ts +++ b/web/src/admin/events/DataExportListPage.ts @@ -120,7 +120,7 @@ export class DataExportListPage extends TablePage { `; } - protected renderEmpty(_inner?: TemplateResult): TemplateResult { + protected renderEmpty(_inner?: TemplateResult): SlottedTemplateResult { return super.renderEmpty( html` { ]; } - renderExpanded(item: Event): TemplateResult { + renderExpanded(item: Event): SlottedTemplateResult { return html``; } - renderEmpty(): TemplateResult { + renderEmpty(): SlottedTemplateResult { return super.renderEmpty( html`${msg("No Events found.")} diff --git a/web/src/admin/events/RuleForm.ts b/web/src/admin/events/RuleForm.ts index 36f0f8a6f652..e8e6be509960 100644 --- a/web/src/admin/events/RuleForm.ts +++ b/web/src/admin/events/RuleForm.ts @@ -1,4 +1,5 @@ import "#components/ak-switch-input"; +import "#components/ak-text-input"; import "#elements/ak-dual-select/ak-dual-select-dynamic-selected-provider"; import "#elements/forms/HorizontalFormElement"; import "#elements/forms/Radio"; @@ -29,6 +30,9 @@ import { ifDefined } from "lit/directives/if-defined.js"; @customElement("ak-event-rule-form") export class RuleForm extends ModelForm { + public static verboseName = msg("Notification Rule"); + public static verboseNamePlural = msg("Notification Rules"); + eventTransports?: PaginatedNotificationTransportList; loadInstance(pk: string): Promise { @@ -62,23 +66,24 @@ export class RuleForm extends ModelForm { } protected override renderForm(): TemplateResult { - return html` - - + return html` => { const args: CoreGroupsListRequest = { ordering: "name", includeUsers: false, }; - if (query !== undefined) { + if (typeof query !== "undefined") { args.search = query; } diff --git a/web/src/admin/events/RuleListPage.ts b/web/src/admin/events/RuleListPage.ts index 0333fd59e0d0..43bef45f1bf6 100644 --- a/web/src/admin/events/RuleListPage.ts +++ b/web/src/admin/events/RuleListPage.ts @@ -11,10 +11,13 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; import { DEFAULT_CONFIG } from "#common/api/config"; import { severityToLabel } from "#common/labels"; +import { IconEditButton, ModalInvokerButton } from "#elements/dialogs"; import { PaginatedResponse, TableColumn } from "#elements/table/Table"; import { TablePage } from "#elements/table/TablePage"; import { SlottedTemplateResult } from "#elements/types"; +import { RuleForm } from "#admin/events/RuleForm"; + import { EventsApi, ModelEnum, NotificationRule } from "@goauthentik/api"; import { msg } from "@lit/localize"; @@ -23,9 +26,12 @@ import { customElement, property } from "lit/decorators.js"; @customElement("ak-event-rule-list") export class RuleListPage extends TablePage { - expandable = true; - checkbox = true; - clearOnRefresh = true; + public override expandable = true; + public override checkbox = true; + public override clearOnRefresh = true; + public override searchPlaceholder = msg( + "Search for a notification rule by name, severity or group...", + ); protected override searchEnabled = true; public pageTitle = msg("Notification Rules"); @@ -35,9 +41,9 @@ export class RuleListPage extends TablePage { public pageIcon = "pf-icon pf-icon-attention-bell"; @property() - order = "name"; + public order = "name"; - async apiEndpoint(): Promise> { + protected override async apiEndpoint(): Promise> { return new EventsApi(DEFAULT_CONFIG).eventsRulesList(await this.defaultEndpointConfig()); } @@ -49,7 +55,7 @@ export class RuleListPage extends TablePage { [msg("Actions"), null, msg("Row Actions")], ]; - renderToolbarSelected(): TemplateResult { + protected override renderToolbarSelected(): TemplateResult { const disabled = this.selectedElements.length < 1; return html` { `; } - row(item: NotificationRule): SlottedTemplateResult[] { + protected override row(item: NotificationRule): SlottedTemplateResult[] { const enabled = !!item.destinationGroupObj || item.destinationEventUser; return [ html``, @@ -82,17 +88,8 @@ export class RuleListPage extends TablePage { >${item.destinationGroupObj.name}` : msg("-")}`, - html`
- - ${msg("Save Changes")} - ${msg("Update Notification Rule")} - - - + html`
+ ${IconEditButton(RuleForm, item.pk, item.name)} { ]; } - renderObjectCreate(): TemplateResult { - return html` - - ${msg("Create")} - ${msg("Create Notification Rule")} - - - - `; + protected override renderObjectCreate(): SlottedTemplateResult { + return ModalInvokerButton(RuleForm); } - renderExpanded(item: NotificationRule): TemplateResult { + protected override renderExpanded(item: NotificationRule): TemplateResult { const [appLabel, modelName] = ModelEnum.AuthentikEventsNotificationrule.split("."); + return html`

${msg( `These bindings control upon which events this rule triggers. diff --git a/web/src/admin/events/SimpleEventTable.ts b/web/src/admin/events/SimpleEventTable.ts index 71e0359a1086..30448a32fe6a 100644 --- a/web/src/admin/events/SimpleEventTable.ts +++ b/web/src/admin/events/SimpleEventTable.ts @@ -5,13 +5,14 @@ import { EventWithContext } from "#common/events"; import { actionToLabel } from "#common/labels"; import { PaginatedResponse, RowType, Table, TableColumn, Timestamp } from "#elements/table/Table"; +import { SlottedTemplateResult } from "#elements/types"; import { EventGeo, renderEventUser } from "#admin/events/utils"; import { Event, EventsApi, EventsEventsListRequest } from "@goauthentik/api"; import { msg } from "@lit/localize"; -import { html, TemplateResult } from "lit-html"; +import { html } from "lit-html"; import { property } from "lit/decorators.js"; export abstract class SimpleEventTable extends Table { @@ -57,11 +58,11 @@ export abstract class SimpleEventTable extends Table { ]; } - renderExpanded(item: Event): TemplateResult { + protected override renderExpanded(item: Event): SlottedTemplateResult { return html``; } - renderEmpty(): TemplateResult { + protected override renderEmpty(): SlottedTemplateResult { return super.renderEmpty( html`${msg("No Events found.")} diff --git a/web/src/admin/events/TransportForm.ts b/web/src/admin/events/TransportForm.ts index 181f4510d197..8656dddfaae7 100644 --- a/web/src/admin/events/TransportForm.ts +++ b/web/src/admin/events/TransportForm.ts @@ -1,5 +1,6 @@ import "#components/ak-hidden-text-input"; import "#components/ak-switch-input"; +import "#components/ak-text-input"; import "#elements/forms/HorizontalFormElement"; import "#elements/forms/Radio"; import "#elements/forms/SearchSelect/index"; @@ -27,6 +28,9 @@ import { ifDefined } from "lit/directives/if-defined.js"; @customElement("ak-event-transport-form") export class TransportForm extends ModelForm { + public static override verboseName = msg("Notification Transport"); + public static override verboseNamePlural = msg("Notification Transports"); + loadInstance(pk: string): Promise { return new EventsApi(DEFAULT_CONFIG) .eventsTransportsRetrieve({ @@ -88,15 +92,16 @@ export class TransportForm extends ModelForm { } protected override renderForm(): TemplateResult { - return html` - - - + return html` { `; })} - - `; + `; } } diff --git a/web/src/admin/events/TransportListPage.ts b/web/src/admin/events/TransportListPage.ts index 105031020f3b..8cd1ad7bf8bb 100644 --- a/web/src/admin/events/TransportListPage.ts +++ b/web/src/admin/events/TransportListPage.ts @@ -9,15 +9,18 @@ import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; import { DEFAULT_CONFIG } from "#common/api/config"; +import { IconEditButton, ModalInvokerButton } from "#elements/dialogs"; import { PaginatedResponse, TableColumn } from "#elements/table/Table"; import { TablePage } from "#elements/table/TablePage"; import { SlottedTemplateResult } from "#elements/types"; +import { TransportForm } from "#admin/events/TransportForm"; + import { EventsApi, ModelEnum, NotificationTransport } from "@goauthentik/api"; import { msg } from "@lit/localize"; -import { html, TemplateResult } from "lit"; -import { customElement, property } from "lit/decorators.js"; +import { html } from "lit"; +import { customElement } from "lit/decorators.js"; @customElement("ak-event-transport-list") export class TransportListPage extends TablePage { @@ -28,26 +31,28 @@ export class TransportListPage extends TablePage { ); public pageIcon = "pf-icon pf-icon-export"; - checkbox = true; - clearOnRefresh = true; - expandable = true; + public override checkbox = true; + public override clearOnRefresh = true; + public override expandable = true; + public override searchPlaceholder = msg( + "Search for a notification transport by name or mode...", + ); - @property() - order = "name"; + public override order = "name"; - async apiEndpoint(): Promise> { + protected override async apiEndpoint(): Promise> { return new EventsApi(DEFAULT_CONFIG).eventsTransportsList( await this.defaultEndpointConfig(), ); } - protected columns: TableColumn[] = [ + protected override columns: TableColumn[] = [ [msg("Name"), "name"], [msg("Mode"), "mode"], [msg("Actions"), null, msg("Row Actions")], ]; - renderToolbarSelected(): TemplateResult { + protected override renderToolbarSelected(): SlottedTemplateResult { const disabled = this.selectedElements.length < 1; return html` { `; } - row(item: NotificationTransport): SlottedTemplateResult[] { + protected override row(item: NotificationTransport): SlottedTemplateResult[] { return [ - html`${item.name}`, - html`${item.modeVerbose}`, - html`

- - ${msg("Save Changes")} - ${msg("Update Notification Transport")} - - - - + item.name, + item.modeVerbose, + html`
+ ${IconEditButton(TransportForm, item.pk, item.name)} { ]; } - renderExpanded(item: NotificationTransport): TemplateResult { + protected override renderExpanded(item: NotificationTransport): SlottedTemplateResult { const [appLabel, modelName] = ModelEnum.AuthentikEventsNotificationtransport.split("."); return html`
@@ -127,15 +122,8 @@ export class TransportListPage extends TablePage {
`; } - renderObjectCreate(): TemplateResult { - return html` - - ${msg("Create")} - ${msg("Create Notification Transport")} - - - - `; + protected override renderObjectCreate(): SlottedTemplateResult { + return ModalInvokerButton(TransportForm); } } diff --git a/web/src/admin/files/FileListPage.ts b/web/src/admin/files/FileListPage.ts index 3d0ecb5fe7e0..35ff0762cb34 100644 --- a/web/src/admin/files/FileListPage.ts +++ b/web/src/admin/files/FileListPage.ts @@ -37,6 +37,7 @@ export class FileListPage extends WithCapabilitiesConfig(TablePage) { public override pageTitle = msg("Files"); public override pageDescription = msg("Manage uploaded files."); public override pageIcon = "pf-icon pf-icon-folder-open"; + public override searchPlaceholder = msg("Search for a file by name..."); @property({ type: String, useDefault: true }) public order: FileListOrderKey = "name"; diff --git a/web/src/admin/flows/BoundStagesList.ts b/web/src/admin/flows/BoundStagesList.ts index b8f980de80f0..6f317d38d230 100644 --- a/web/src/admin/flows/BoundStagesList.ts +++ b/web/src/admin/flows/BoundStagesList.ts @@ -1,24 +1,27 @@ import "#admin/flows/StageBindingForm"; import "#admin/policies/BoundPoliciesList"; import "#admin/rbac/ObjectPermissionModal"; -import "#admin/stages/StageWizard"; import "#elements/Tabs"; import "#elements/forms/DeleteBulkForm"; import "#elements/forms/ModalForm"; import { DEFAULT_CONFIG } from "#common/api/config"; +import { modalInvoker } from "#elements/dialogs"; +import { IconPermissionButton } from "#elements/dialogs/components/IconPermissionButton"; import { CustomFormElementTagName } from "#elements/forms/unsafe"; import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table"; import { SlottedTemplateResult } from "#elements/types"; import { StrictUnsafe } from "#elements/utils/unsafe"; +import { StageBindingForm } from "#admin/flows/StageBindingForm"; +import { AKStageWizard } from "#admin/stages/ak-stage-wizard"; + import { FlowsApi, FlowStageBinding, ModelEnum } from "@goauthentik/api"; -import { msg, str } from "@lit/localize"; +import { msg } from "@lit/localize"; import { html, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; -import { ifDefined } from "lit/directives/if-defined.js"; @customElement("ak-bound-stages-list") export class BoundStagesList extends Table { @@ -80,39 +83,56 @@ export class BoundStagesList extends Table { row(item: FlowStageBinding): SlottedTemplateResult[] { return [ html`
${item.order}
`, - html`${item.stageObj?.name}`, - html`${item.stageObj?.verboseName}`, - html` - ${StrictUnsafe(item.stageObj?.component, { - slot: "form", - instancePk: item.stageObj?.pk, - submitLabel: msg("Save Changes"), - headline: msg(str`Update ${item.stageObj?.verboseName}`, { - id: "form.headline.update", + item.stageObj?.name, + item.stageObj?.verboseName, + html`
+ - - - ${msg("Save Changes")} - ${msg("Update Stage binding")} - - - - - + ${msg("Edit Stage")} + + + + ${IconPermissionButton(item.stageObj?.name || "", { + model: ModelEnum.AuthentikFlowsFlowstagebinding, + objectPk: item.pk, + })} +
`, ]; } - renderExpanded(item: FlowStageBinding): TemplateResult { + protected renderActions(): SlottedTemplateResult { + return html` + `; + } + + protected override renderExpanded(item: FlowStageBinding): TemplateResult { return html`

${msg("These bindings control if this stage will be applied to the flow.")}

{
`; } - renderEmpty(): TemplateResult { + protected override renderEmpty(): SlottedTemplateResult { return super.renderEmpty( html` ${msg("No Stages bound")}
${msg("No stages are currently bound to this flow.")}
-
- - - ${msg("Create")} - ${msg("Create Stage binding")} - - - - -
+
${this.renderActions()}
`, ); } - renderToolbar(): TemplateResult { - return html` - - - ${msg("Create")} - ${msg("Create Stage binding")} - - - - - ${super.renderToolbar()} - `; + protected override renderToolbar(): SlottedTemplateResult { + return [this.renderActions(), super.renderToolbar()]; } } diff --git a/web/src/admin/flows/FlowForm.ts b/web/src/admin/flows/FlowForm.ts index 17077e617bda..ee45e937c03f 100644 --- a/web/src/admin/flows/FlowForm.ts +++ b/web/src/admin/flows/FlowForm.ts @@ -1,5 +1,6 @@ import "#components/ak-file-search-input"; import "#components/ak-slug-input"; +import "#components/ak-text-input"; import "#components/ak-switch-input"; import "#elements/forms/FormGroup"; import "#elements/forms/HorizontalFormElement"; @@ -10,6 +11,8 @@ import { DEFAULT_CONFIG } from "#common/api/config"; import { ModelForm } from "#elements/forms/ModelForm"; import { WithCapabilitiesConfig } from "#elements/mixins/capabilities"; +import { AKLabel } from "#components/ak-label"; + import { DesignationToLabel, LayoutToLabel } from "#admin/flows/utils"; import { policyEngineModes } from "#admin/policies/PolicyEngineModes"; @@ -35,62 +38,80 @@ import { ifDefined } from "lit/directives/if-defined.js"; */ @customElement("ak-flow-form") export class FlowForm extends WithCapabilitiesConfig(ModelForm) { - async loadInstance(pk: string): Promise { - return new FlowsApi(DEFAULT_CONFIG).flowsInstancesRetrieve({ + public static override verboseName = msg("Flow"); + public static override verboseNamePlural = msg("Flows"); + + #api = new FlowsApi(DEFAULT_CONFIG); + + protected override async loadInstance(pk: string): Promise { + return this.#api.flowsInstancesRetrieve({ slug: pk, }); } - getSuccessMessage(): string { + public override getSuccessMessage(): string { return this.instance ? msg("Successfully updated flow.") : msg("Successfully created flow."); } - async send(data: Flow): Promise { + protected override async send(data: Flow): Promise { if (this.instance) { - return new FlowsApi(DEFAULT_CONFIG).flowsInstancesUpdate({ + return this.#api.flowsInstancesUpdate({ slug: this.instance.slug, flowRequest: data, }); } - return new FlowsApi(DEFAULT_CONFIG).flowsInstancesCreate({ + + return this.#api.flowsInstancesCreate({ flowRequest: data, }); } protected override renderForm(): TemplateResult { - return html` - - - - -

${msg("Shown as the Title in Flow pages.")}

-
+ return html` + - - + - -