From b88d0829477c5e539646cbf4c5f1ce99aac03731 Mon Sep 17 00:00:00 2001 From: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com> Date: Wed, 25 Mar 2026 07:07:29 +0100 Subject: [PATCH] web/a11y: Modals, Command Palette (Merge branch) (#17812) * Use project relative paths. * Fix tests. * Fix types. * Clean up admin imports. * Move admin import. * Remove or replace references to admin. * Typo fix. * Flesh out ak-modal, about modal. * Flesh out lazy modal. * Fix portal elements not using dialog scope. * Fix url parameters, wizards. * Fix invokers, lazy load. * Fix theming. * Add placeholders, help. * Flesh out command palette. Flesh out styles, command invokers. Continue clean up. Allow slotted content. Flesh out. * Flesh out edit invoker. Prep groups. * Fix odd labeling, legacy situations. * Prepare deprecation of table modal. Clean up serialization. * Tidy types. * Port provider select modal. * Port member select form. * Flesh out role modal. Fix loading state. * Port user group form. * Fix spellcheck. * Fix dialog detection. * Revise types. * Port rac launch modal. * Remove deprecated table modal. * Consistent form action placement. * Consistent casing. * Consistent alignment. * Use more appropriate description. * Flesh out icon. Fix alignment, colors. * Flesh out user search. * Consistent save button. * Clean up labels. * Reduce warning noise. * Clean up label. * Use attribute e2e expects. * Use directive. Fix lifecycle * Fix frequent un-memoized entries. * Fix up closedBy detection. * Tidy alignment. * Fix types, composition. * Fix labels, tests. * Fix up impersonation, labels. * Flesh out. Fix refresh after submit. * Flesh out basic modal test. * Fix ARIA. * Flesh out roles test. * Revise selectors. * Clean up selectors. * Fix impersonation labels, form references. * Fix messages appearing under modals. * Ensure reason is parsed. * Flesh out impersonation test. * Flesh out impersonate test. * Flesh out application tests. Clean up toolbar header, ARIA. * Flesh out wizard test. * Refine weight, order. * Fix up initial values, selectors. * Fix tests. * Fix selector. --- web/e2e/fixtures/FormFixture.ts | 11 +- web/e2e/fixtures/PointerFixture.ts | 4 +- web/src/admin/AdminInterface/AboutModal.ts | 236 +++--- .../admin/AdminInterface/index.entrypoint.css | 6 + .../admin/AdminInterface/index.entrypoint.ts | 257 +++++-- web/src/admin/Routes.ts | 13 - .../admin/admin-overview/SystemTasksPage.ts | 1 + .../AdminSettingsFooterLinks.ts | 4 +- .../admin/admin-settings/AdminSettingsPage.ts | 2 +- .../stories/ak-array-input.stories.ts | 4 +- web/src/admin/applications/ApplicationForm.ts | 22 +- .../applications/ApplicationListPage.css | 15 + .../admin/applications/ApplicationListPage.ts | 150 ++-- .../admin/applications/ApplicationViewPage.ts | 6 +- .../admin/applications/ProviderSelectForm.ts | 57 ++ .../admin/applications/ProviderSelectModal.ts | 91 --- .../components/ak-backchannel-input.ts | 41 +- .../components/ak-provider-search-input.ts | 6 +- .../ApplicationEntitlementPage.ts | 5 +- .../ApplicationWizardFormStepStyles.styles.ts | 2 +- .../wizard/ApplicationWizardStep.ts | 54 +- .../wizard/ak-application-wizard-main.ts | 37 +- .../wizard/ak-application-wizard.ts | 37 +- .../steps/SubmitStepOverviewRenderers.ts | 137 ++-- .../ak-application-wizard-application-step.ts | 52 +- .../ak-application-wizard-bindings-step.ts | 14 +- ...ak-application-wizard-edit-binding-step.ts | 23 +- ...application-wizard-provider-choice-step.ts | 12 +- .../ak-application-wizard-provider-step.ts | 90 ++- .../ak-application-wizard-submit-step.ts | 80 +- .../ak-application-wizard-bindings-toolbar.ts | 2 +- .../ApplicationWizardProviderForm.ts | 45 +- ...ak-application-wizard-provider-for-ldap.ts | 17 +- ...k-application-wizard-provider-for-oauth.ts | 16 +- ...k-application-wizard-provider-for-proxy.ts | 12 +- .../ak-application-wizard-provider-for-rac.ts | 5 +- ...-application-wizard-provider-for-radius.ts | 12 +- ...ation-wizard-provider-for-saml-metadata.ts | 3 +- ...ak-application-wizard-provider-for-saml.ts | 5 +- ...ak-application-wizard-provider-for-scim.ts | 5 +- .../{types.ts => steps/providers/shared.ts} | 46 +- web/src/admin/blueprints/BlueprintListPage.ts | 2 +- web/src/admin/brands/BrandListPage.ts | 2 +- .../crypto/CertificateKeyPairListPage.ts | 2 +- .../endpoints/DeviceAccessGroupsListPage.ts | 2 +- .../endpoints/connectors/ConnectorWizard.ts | 2 +- .../connectors/ConnectorsListPage.ts | 2 +- .../agent/AgentConnectorViewPage.ts | 2 +- .../endpoints/connectors/agent/ConfigModal.ts | 29 +- .../agent/EnrollmentTokenListPage.ts | 2 +- .../fleet/FleetConnectorViewPage.ts | 3 +- .../gdtc/GoogleChromeConnectorViewPage.ts | 3 +- .../endpoints/devices/BoundDeviceUsersList.ts | 2 +- .../admin/endpoints/devices/DeviceAddHowTo.ts | 5 +- .../admin/endpoints/devices/DeviceListPage.ts | 2 +- .../devices/DeviceUserBindingForm.ts | 2 +- .../admin/endpoints/devices/DeviceViewPage.ts | 2 +- .../enterprise/EnterpriseLicenseListPage.ts | 2 +- .../events/ObjectChangelog.ts | 0 web/src/admin/events/RuleListPage.ts | 2 +- web/src/admin/events/TransportListPage.ts | 2 +- .../events/UserEvents.ts | 0 web/src/admin/flows/BoundStagesList.ts | 4 +- web/src/admin/flows/FlowListPage.ts | 20 +- web/src/admin/flows/FlowViewPage.ts | 6 +- web/src/admin/groups/GroupForm.ts | 5 +- web/src/admin/groups/GroupListPage.ts | 48 +- web/src/admin/groups/GroupViewPage.ts | 4 +- ...mberSelectModal.ts => MemberSelectForm.ts} | 61 +- web/src/admin/groups/RelatedGroupList.ts | 49 +- web/src/admin/groups/RelatedUserList.ts | 380 +++++----- web/src/admin/lifecycle/LifecycleRuleForm.ts | 6 +- .../admin/lifecycle/LifecycleRuleListPage.ts | 2 +- web/src/admin/lifecycle/ObjectReviewForm.ts | 6 +- .../admin/outposts/OutpostDeploymentModal.ts | 5 +- web/src/admin/outposts/OutpostListPage.ts | 2 +- .../outposts/ServiceConnectionListPage.ts | 2 +- web/src/admin/policies/BoundPoliciesList.ts | 30 +- web/src/admin/policies/PolicyBindingForm.ts | 9 +- web/src/admin/policies/PolicyListPage.ts | 4 +- .../PropertyMappingListPage.ts | 2 +- web/src/admin/providers/ProviderListPage.ts | 2 +- web/src/admin/providers/ProviderWizard.ts | 2 +- .../GoogleWorkspaceProviderGroupList.ts | 1 + .../GoogleWorkspaceProviderUserList.ts | 1 + .../GoogleWorkspaceProviderViewPage.ts | 5 +- .../providers/ldap/LDAPProviderViewPage.ts | 4 +- .../MicrosoftEntraProviderGroupList.ts | 1 + .../MicrosoftEntraProviderUserList.ts | 1 + .../MicrosoftEntraProviderViewPage.ts | 5 +- .../oauth2/OAuth2ProviderRedirectURI.ts | 4 +- .../oauth2/OAuth2ProviderViewPage.ts | 5 +- .../providers/proxy/ProxyProviderViewPage.ts | 4 +- web/src/admin/providers/rac/EndpointList.ts | 2 +- .../providers/rac/RACProviderViewPage.ts | 4 +- .../radius/RadiusProviderFormForm.ts | 2 +- .../radius/RadiusProviderViewPage.ts | 4 +- .../providers/saml/SAMLProviderViewPage.ts | 4 +- .../providers/scim/SCIMProviderFormForm.ts | 2 +- .../providers/scim/SCIMProviderGroupList.ts | 1 + .../providers/scim/SCIMProviderUserList.ts | 1 + .../providers/scim/SCIMProviderViewPage.ts | 5 +- .../providers/ssf/SSFProviderViewPage.ts | 5 +- .../wsfed/WSFederationProviderViewPage.ts | 4 +- .../admin/rbac/InitialPermissionsListPage.ts | 2 +- web/src/admin/rbac/PermissionSelectForm.ts | 52 ++ web/src/admin/rbac/PermissionSelectModal.ts | 94 --- web/src/admin/reports/ExportButton.ts | 2 +- web/src/admin/roles/RelatedRoleList.ts | 115 +-- web/src/admin/roles/RoleForm.ts | 23 +- web/src/admin/roles/RoleListPage.ts | 53 +- web/src/admin/roles/RolePermissionForm.ts | 38 +- web/src/admin/roles/RoleViewPage.ts | 6 +- web/src/admin/sources/SourceListPage.ts | 4 +- .../kerberos/KerberosSourceViewPage.ts | 4 +- .../admin/sources/ldap/LDAPSourceViewPage.ts | 4 +- .../sources/oauth/OAuthSourceViewPage.ts | 7 +- .../admin/sources/plex/PlexSourceViewPage.ts | 7 +- .../admin/sources/saml/SAMLSourceViewPage.ts | 7 +- .../admin/sources/scim/SCIMSourceViewPage.ts | 4 +- .../telegram/TelegramSourceViewPage.ts | 9 +- web/src/admin/stages/StageListPage.ts | 2 +- web/src/admin/stages/StageWizard.ts | 2 +- .../stages/invitation/InvitationListPage.ts | 2 +- web/src/admin/stages/prompt/PromptForm.ts | 2 +- web/src/admin/stages/prompt/PromptListPage.ts | 2 +- web/src/admin/tokens/TokenListPage.ts | 2 +- web/src/admin/users/GroupSelectModal.ts | 104 --- web/src/admin/users/RoleSelectModal.ts | 70 +- web/src/admin/users/ServiceAccountForm.ts | 9 +- web/src/admin/users/UserActiveForm.ts | 23 +- web/src/admin/users/UserApplicationTable.ts | 2 +- .../admin/users/UserBulkRevokeSessionsForm.ts | 23 +- web/src/admin/users/UserForm.ts | 5 +- web/src/admin/users/UserGroupSelectForm.ts | 74 ++ web/src/admin/users/UserImpersonateForm.ts | 23 +- web/src/admin/users/UserListPage.ts | 168 ++-- web/src/admin/users/UserPasswordForm.ts | 22 +- web/src/admin/users/UserViewPage.ts | 60 +- web/src/common/api/middleware.ts | 21 +- web/src/common/collections.ts | 13 + web/src/common/errors/network.ts | 8 +- web/src/common/objects.ts | 59 ++ web/src/{admin => common}/policies/utils.ts | 0 web/src/common/theme.ts | 17 - web/src/common/users.ts | 4 +- web/src/components/ak-multi-select.ts | 4 +- web/src/components/ak-nav-buttons.ts | 13 +- web/src/components/ak-page-navbar.css | 2 +- web/src/components/ak-page-navbar.ts | 1 + web/src/components/ak-search-ql/index.ts | 20 +- web/src/components/ak-slug-input.ts | 5 + .../components/ak-wizard/WizardContexts.ts | 2 +- web/src/components/ak-wizard/WizardStep.ts | 180 ++--- .../components/ak-wizard/ak-wizard-steps.ts | 71 +- web/src/components/ak-wizard/shared.ts | 54 ++ web/src/components/ak-wizard/types.ts | 22 - web/src/elements/Base.ts | 47 +- ...{AkControlElement.ts => ControlElement.ts} | 22 +- web/src/elements/Interface.ts | 31 +- .../LicenseNotice.ts} | 0 web/src/elements/Tabs.ts | 64 +- web/src/elements/ak-array-input.ts | 8 +- .../ak-checkbox-group/ak-checkbox-group.ts | 4 +- .../ak-dual-select/ak-dual-select-provider.ts | 4 +- .../commands/ak-command-palette-modal.css | 261 +++++++ .../commands/ak-command-palette-modal.ts | 715 ++++++++++++++++++ .../commands/ak-command-palette-user-modal.ts | 156 ++++ .../elements/commands/ak-command-palette.css | 17 + .../elements/commands/ak-command-palette.ts | 78 ++ web/src/elements/commands/events.ts | 28 + web/src/elements/commands/shared.ts | 165 ++++ .../controllers/SessionContextController.ts | 155 +++- web/src/elements/forms/ConfirmationForm.ts | 26 +- web/src/elements/forms/DeleteBulkForm.ts | 25 +- web/src/elements/forms/DeleteForm.ts | 24 +- web/src/elements/forms/Form.css | 14 + web/src/elements/forms/Form.ts | 512 +++++++------ .../elements/forms/HorizontalFormElement.ts | 6 +- web/src/elements/forms/ModalForm.ts | 6 +- web/src/elements/forms/ModelForm.ts | 38 +- .../forms/SearchSelect/SearchSelect.ts | 4 +- .../elements/forms/SearchSelect/ak-portal.ts | 9 +- .../SearchSelect/ak-search-select-view.ts | 8 +- web/src/elements/forms/errors.ts | 42 + .../elements/forms/form-associated-element.ts | 50 +- web/src/elements/forms/serialization.ts | 122 +++ web/src/elements/messages/MessageContainer.ts | 44 +- web/src/elements/messages/styles.css | 21 + web/src/elements/modals/ak-modal.ts | 587 ++++++++++++++ web/src/elements/modals/shared.ts | 40 + web/src/elements/modals/styles.css | 148 ++++ web/src/elements/modals/utils.ts | 332 ++++++++ web/src/elements/router/builders.ts | 12 + web/src/elements/sidebar/SidebarItem.ts | 7 +- web/src/elements/sidebar/SidebarVersion.ts | 69 +- web/src/{admin => elements}/sources/utils.ts | 2 +- web/src/elements/sync/SyncObjectForm.ts | 1 - web/src/elements/table/Table.ts | 52 +- web/src/elements/table/TableModal.ts | 128 ---- web/src/elements/table/TablePage.css | 16 + web/src/elements/table/TablePage.ts | 20 +- web/src/elements/table/TableSearch.ts | 1 + web/src/elements/tasks/ScheduleList.ts | 2 +- web/src/elements/tasks/TaskList.ts | 1 - web/src/elements/types.ts | 18 +- .../elements/user/sources/SourceSettings.ts | 3 +- web/src/elements/utils/eventEmitter.ts | 13 +- web/src/elements/utils/getRootStyle.ts | 5 - web/src/elements/utils/render-roots.ts | 18 + web/src/elements/utils/unsafe.ts | 4 +- .../elements/wizard/TypeCreateWizardPage.ts | 2 +- web/src/elements/wizard/Wizard.ts | 45 +- web/src/flow/stages/email/EmailStage.ts | 2 +- .../identification/IdentificationStage.ts | 10 +- web/src/styles/authentik/base.css | 2 +- web/src/styles/authentik/base/common.css | 42 + web/src/styles/authentik/base/modal.css | 249 ++++++ .../authentik/components/Button/button.css | 7 + .../styles/authentik/components/Card/card.css | 6 +- .../components/Dropdown/dropdown.css | 1 + .../styles/authentik/components/Form/form.css | 49 +- .../styles/authentik/components/Icon/icon.css | 20 + .../authentik/components/Modal/modal.css | 14 - .../authentik/components/Wizard/wizard.css | 33 +- web/src/styles/authentik/interface.global.css | 1 + .../RACLaunchEndpointModal.ts | 41 +- web/src/user/LibraryApplication/index.ts | 18 +- web/src/user/LibraryPage/ApplicationList.css | 2 +- web/src/user/LibraryPage/ApplicationList.ts | 3 +- web/src/user/index.entrypoint.ts | 10 +- .../user/user-settings/mfa/MFADevicesPage.ts | 2 +- .../user-settings/tokens/UserTokenList.ts | 2 +- web/test/browser/applications.test.ts | 190 +++++ web/test/browser/groups.test.ts | 53 +- web/test/browser/modals.test.ts | 55 ++ web/test/browser/providers.test.ts | 12 +- web/test/browser/roles.test.ts | 110 +++ web/test/browser/users.test.ts | 115 ++- web/types/dom.d.ts | 24 + 240 files changed, 6717 insertions(+), 2531 deletions(-) rename web/src/{elements => admin/admin-settings}/stories/ak-array-input.stories.ts (97%) create mode 100644 web/src/admin/applications/ApplicationListPage.css create mode 100644 web/src/admin/applications/ProviderSelectForm.ts delete mode 100644 web/src/admin/applications/ProviderSelectModal.ts rename web/src/admin/applications/wizard/{types.ts => steps/providers/shared.ts} (67%) rename web/src/{components => admin}/events/ObjectChangelog.ts (100%) rename web/src/{components => admin}/events/UserEvents.ts (100%) rename web/src/admin/groups/{MemberSelectModal.ts => MemberSelectForm.ts} (67%) create mode 100644 web/src/admin/rbac/PermissionSelectForm.ts delete mode 100644 web/src/admin/rbac/PermissionSelectModal.ts delete mode 100644 web/src/admin/users/GroupSelectModal.ts create mode 100644 web/src/admin/users/UserGroupSelectForm.ts create mode 100644 web/src/common/collections.ts create mode 100644 web/src/common/objects.ts rename web/src/{admin => common}/policies/utils.ts (100%) create mode 100644 web/src/components/ak-wizard/shared.ts delete mode 100644 web/src/components/ak-wizard/types.ts rename web/src/elements/{AkControlElement.ts => ControlElement.ts} (60%) rename web/src/{admin/common/ak-license-notice.ts => elements/LicenseNotice.ts} (100%) create mode 100644 web/src/elements/commands/ak-command-palette-modal.css create mode 100644 web/src/elements/commands/ak-command-palette-modal.ts create mode 100644 web/src/elements/commands/ak-command-palette-user-modal.ts create mode 100644 web/src/elements/commands/ak-command-palette.css create mode 100644 web/src/elements/commands/ak-command-palette.ts create mode 100644 web/src/elements/commands/events.ts create mode 100644 web/src/elements/commands/shared.ts create mode 100644 web/src/elements/forms/Form.css create mode 100644 web/src/elements/forms/errors.ts create mode 100644 web/src/elements/forms/serialization.ts create mode 100644 web/src/elements/messages/styles.css create mode 100644 web/src/elements/modals/ak-modal.ts create mode 100644 web/src/elements/modals/shared.ts create mode 100644 web/src/elements/modals/styles.css create mode 100644 web/src/elements/modals/utils.ts create mode 100644 web/src/elements/router/builders.ts rename web/src/{admin => elements}/sources/utils.ts (94%) delete mode 100644 web/src/elements/table/TableModal.ts create mode 100644 web/src/elements/table/TablePage.css delete mode 100644 web/src/elements/utils/getRootStyle.ts create mode 100644 web/src/elements/utils/render-roots.ts create mode 100644 web/src/styles/authentik/base/modal.css delete mode 100644 web/src/styles/authentik/components/Modal/modal.css create mode 100644 web/test/browser/applications.test.ts create mode 100644 web/test/browser/modals.test.ts create mode 100644 web/test/browser/roles.test.ts create mode 100644 web/types/dom.d.ts diff --git a/web/e2e/fixtures/FormFixture.ts b/web/e2e/fixtures/FormFixture.ts index 950150ea71..2b47d2aa85 100644 --- a/web/e2e/fixtures/FormFixture.ts +++ b/web/e2e/fixtures/FormFixture.ts @@ -27,6 +27,7 @@ export class FormFixture extends PageFixture { .filter({ hasNot: context.getByRole("presentation"), }) + .and(context.locator(":not(button)")) .or( context.getByRole("textbox", { name: fieldName, @@ -191,17 +192,17 @@ export class FormFixture extends PageFixture { // Find the search select input control and activate it. await control.click(); + if (typeof pattern === "string") { + this.fill(control, pattern, parent); + } + const button = this.page // --- .locator(`div[data-managed-for*="${fieldName}"] button`, { hasText: pattern, }); - if (!button) { - throw new Error( - `Unable to find an ak-search-select entry matching ${fieldLabel}:${pattern.toString()}`, - ); - } + await expect(button, `Search select entry (${pattern}) should be visible`).toBeVisible(); await button.click(); await this.page.keyboard.press("Tab"); diff --git a/web/e2e/fixtures/PointerFixture.ts b/web/e2e/fixtures/PointerFixture.ts index d37d1d2e59..18fc117dfa 100644 --- a/web/e2e/fixtures/PointerFixture.ts +++ b/web/e2e/fixtures/PointerFixture.ts @@ -26,7 +26,9 @@ export class PointerFixture extends PageFixture { context: LocatorContext = this.page, ): Promise => { if (typeof optionsOrRole === "string") { - return context.getByRole(optionsOrRole, { name }).first().click(); + const target = context.getByRole(optionsOrRole, { name }).first(); + + return target.click(); } const options = { diff --git a/web/src/admin/AdminInterface/AboutModal.ts b/web/src/admin/AdminInterface/AboutModal.ts index 3693c90c99..a3c671181b 100644 --- a/web/src/admin/AdminInterface/AboutModal.ts +++ b/web/src/admin/AdminInterface/AboutModal.ts @@ -3,137 +3,175 @@ import "#elements/EmptyState"; import { DEFAULT_CONFIG } from "#common/api/config"; import { globalAK } from "#common/global"; -import { ModalButton } from "#elements/buttons/ModalButton"; 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 { ThemedImage } from "#elements/utils/images"; import { AdminApi, CapabilitiesEnum, LicenseSummaryStatusEnum } from "@goauthentik/api"; import { msg } from "@lit/localize"; import { css, html, TemplateResult } from "lit"; -import { customElement } from "lit/decorators.js"; -import { createRef, ref } from "lit/directives/ref.js"; -import { until } from "lit/directives/until.js"; +import { ref } from "lit-html/directives/ref.js"; +import { styleMap } from "lit-html/directives/style-map.js"; +import { customElement, state } from "lit/decorators.js"; import PFAbout from "@patternfly/patternfly/components/AboutModalBox/about-modal-box.css"; -@customElement("ak-about-modal") -export class AboutModal extends WithLicenseSummary(WithBrandConfig(ModalButton)) { - static styles = [ - ...ModalButton.styles, - PFAbout, - css` - .pf-c-about-modal-box { - --pf-c-about-modal-box--BackgroundColor: var(--pf-global--palette--black-900); - } +const DEFAULT_BRAND_IMAGE = "/static/dist/assets/images/flow_background.jpg"; - .pf-c-about-modal-box__hero { - background-image: url("/static/dist/assets/images/flow_background.jpg"); - } - .pf-c-about-modal-box__brand { - --pf-c-about-modal-box__brand-image--Height: 6.25rem; - } - .pf-c-about-modal-box__brand i { - font-size: var(--pf-c-about-modal-box__brand-image--Height); +type AboutEntry = [label: string, content: string | TemplateResult]; + +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" : ""}`, + ], + ]; +} + +@customElement("ak-about-modal") +export class AboutModal extends WithLicenseSummary(WithBrandConfig(AKModal)) { + public static override formatARIALabel = () => msg("About authentik"); + + public static 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); } `, ]; - async getAboutEntries(): Promise<[string, string | TemplateResult][]> { - const status = await new AdminApi(DEFAULT_CONFIG).adminSystemRetrieve(); - const version = await new AdminApi(DEFAULT_CONFIG).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" : ""}`, - ], - ]; + public static styles = [ + ...AKModal.styles, + PFAbout, + css` + :host { + height: 100%; + } + + .pf-c-about-modal-box { + --pf-c-about-modal-box--BackgroundColor: var(--ak-c-modal--BackgroundColor); + width: unset; + height: 100%; + max-height: unset; + max-width: unset; + z-index: unset; + position: unset; + box-shadow: unset; + } + `, + ]; + + public static ariaLabel = msg("About authentik"); + + public static open = asInvoker(AboutModal); + + @state() + protected entries: AboutEntry[] | null = null; + + public refresh() { + return fetchAboutDetails().then((entries) => { + this.entries = entries; + }); } - #contentRef = createRef(); + public connectedCallback(): void { + super.connectedCallback(); + this.refresh(); + } - #backdropListener = (event: PointerEvent) => { - // We only want to close the modal when the backdrop is clicked, not when it's children are clicked. + //#region Renderers - if (this.#contentRef.value?.contains(event.target as Node)) { - return; - } - this.close(); - }; + protected override renderCloseButton() { + return null; + } - protected override renderModal() { + protected override render() { let product = this.brandingTitle; if (this.licenseSummary?.status !== LicenseSummaryStatusEnum.Unlicensed) { product += ` ${msg("Enterprise")}`; } - return html`
-
- `; } + + //#endregion } declare global { diff --git a/web/src/admin/AdminInterface/index.entrypoint.css b/web/src/admin/AdminInterface/index.entrypoint.css index d5fb6420c2..783e80255b 100644 --- a/web/src/admin/AdminInterface/index.entrypoint.css +++ b/web/src/admin/AdminInterface/index.entrypoint.css @@ -34,3 +34,9 @@ ak-sidebar-item:active ak-sidebar-item::part(list-item) { .pf-c-drawer__panel { background-color: transparent !important; } + +.command-palette-trigger { + cursor: pointer; + + padding-inline-end: var(--pf-global--spacer--sm); +} diff --git a/web/src/admin/AdminInterface/index.entrypoint.ts b/web/src/admin/AdminInterface/index.entrypoint.ts index 651aedc7f3..ca73f280f4 100644 --- a/web/src/admin/AdminInterface/index.entrypoint.ts +++ b/web/src/admin/AdminInterface/index.entrypoint.ts @@ -1,15 +1,16 @@ -import "#admin/AdminInterface/AboutModal"; import "#elements/banner/EnterpriseStatusBanner"; import "#elements/banner/VersionBanner"; import "#elements/messages/MessageContainer"; import "#elements/router/RouterOutlet"; import "#elements/sidebar/Sidebar"; import "#elements/sidebar/SidebarItem"; +import "#elements/commands/ak-command-palette-user-modal"; import { createAdminSidebarEnterpriseEntries, createAdminSidebarEntries, renderSidebarItems, + SidebarEntry, } from "./AdminSidebar.js"; import { isAPIResultReady } from "#common/api/responses"; @@ -18,10 +19,16 @@ import { isGuest } from "#common/users"; import { WebsocketClient } from "#common/ws/WebSocketClient"; import { AuthenticatedInterface } from "#elements/AuthenticatedInterface"; +import { + CommandPrefix, + PaletteCommandDefinitionInit, + PaletteCommandNamespace, +} from "#elements/commands/shared"; import { listen } from "#elements/decorators/listen"; 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, @@ -29,18 +36,19 @@ import { readDrawerParams, renderNotificationDrawerPanel, } from "#elements/notifications/utils"; +import { navigate } from "#elements/router/RouterOutlet"; -import type { AboutModal } from "#admin/AdminInterface/AboutModal"; import Styles from "#admin/AdminInterface/index.entrypoint.css"; import { ROUTES } from "#admin/Routes"; import { CapabilitiesEnum } from "@goauthentik/api"; -import { msg } from "@lit/localize"; +import { LOCALE_STATUS_EVENT, LocaleStatusEventDetail, msg } from "@lit/localize"; import { CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit"; -import { customElement, eventOptions, property, query, state } from "lit/decorators.js"; +import { customElement, eventOptions, property, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; +import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css"; import PFNav from "@patternfly/patternfly/components/Nav/nav.css"; @@ -56,18 +64,25 @@ export class AdminInterface extends WithCapabilitiesConfig( ) { //#region Styles - public static readonly styles: CSSResult[] = [PFPage, PFButton, PFDrawer, PFNav, Styles]; + public static readonly styles: CSSResult[] = [ + PFPage, + PFButton, + PFDrawer, + PFNav, + PFBanner, + Styles, + ]; //#endregion - //#region Properties - - @query("ak-about-modal") - public aboutModal?: AboutModal; + //#region Public Properties @property({ type: Boolean, reflect: true, attribute: "sidebar" }) public sidebarOpen = false; + @property({ type: Array }) + public entries: readonly SidebarEntry[] = createAdminSidebarEntries(); + //#endregion //#region Public Methods @@ -76,9 +91,14 @@ export class AdminInterface extends WithCapabilitiesConfig( this.sidebarOpen = !this.sidebarOpen; }; + public synchronizeSidebarEntries = () => { + this.logger.debug("Synchronizing sidebar entries with current locale"); + this.entries = createAdminSidebarEntries(); + }; + //#endregion - //#region Lifecycle + //#region Event Listeners #sidebarMatcher: MediaQueryList; #sidebarMediaQueryListener = (event: MediaQueryListEvent) => { @@ -99,6 +119,17 @@ export class AdminInterface extends WithCapabilitiesConfig( persistDrawerParams(event.drawer); }; + @listen(LOCALE_STATUS_EVENT) + localeStatusListener = (event: CustomEvent) => { + if (event.detail.status === "ready") { + this.synchronizeSidebarEntries(); + } + }; + + //#endregion + + //#region Lifecycle + constructor() { configureSentry(); @@ -110,6 +141,66 @@ export class AdminInterface extends WithCapabilitiesConfig( this.sidebarOpen = this.#sidebarMatcher.matches; } + #refreshCommandsFrameID = -1; + + #refreshCommands = () => { + const commands: PaletteCommandDefinitionInit[] = [ + { + label: msg("Create a new application..."), + action: () => navigate("/core/applications", { createWizard: true }), + group: msg("Applications"), + }, + { + namespace: PaletteCommandNamespace.Navigation, + label: msg("Check the logs"), + action: () => navigate("/events/log"), + group: msg("Events"), + }, + { + namespace: PaletteCommandNamespace.Navigation, + label: msg("Manage users"), + action: () => navigate("/identity/users"), + group: msg("Users"), + }, + ...this.entries.flatMap(([, label, , children]) => [ + ...(children ?? []).map( + ([path, childLabel]): PaletteCommandDefinitionInit => ({ + namespace: PaletteCommandNamespace.Navigation, + label: childLabel, + group: label, + action: () => { + navigate(path!); + }, + }), + ), + ]), + { + namespace: PaletteCommandNamespace.Search, + label: msg("Username or email address..."), + prefix: CommandPrefix.SearchFor(), + group: msg("Users"), + keywords: [msg("search"), msg("find")], + action: async (data, event) => { + event?.stopPropagation(); + + const userPalette = this.ownerDocument.createElement( + "ak-command-palette-user-modal", + ); + + renderDialog(userPalette, { + parentElement: this, + }); + + userPalette.show(); + }, + }, + ]; + + this.commandPalette.modal.setCommands( + commands.map((command) => ({ namespace: PaletteCommandNamespace.Action, ...command })), + ); + }; + public connectedCallback() { super.connectedCallback(); @@ -121,11 +212,19 @@ export class AdminInterface extends WithCapabilitiesConfig( public disconnectedCallback(): void { super.disconnectedCallback(); + cancelAnimationFrame(this.#refreshCommandsFrameID); + this.#sidebarMatcher.removeEventListener("change", this.#sidebarMediaQueryListener); WebsocketClient.close(); } + public firstUpdated(changedProperties: PropertyValues): void { + super.firstUpdated(changedProperties); + + this.#refreshCommandsFrameID = requestAnimationFrame(this.#refreshCommands); + } + public override updated(changedProperties: PropertyValues): void { super.updated(changedProperties); @@ -158,62 +257,96 @@ export class AdminInterface extends WithCapabilitiesConfig( }; return html`
- - + + - - - - - ${renderSidebarItems(createAdminSidebarEntries())} - ${this.can(CapabilitiesEnum.IsEnterprise) - ? renderSidebarItems(createAdminSidebarEnterpriseEntries()) - : nothing} - - -
-
-
-
-
- - +
- ${renderNotificationDrawerPanel(this.drawer)} - -
-
-
+ + + + + + + + ${renderSidebarItems(this.entries)} + ${this.can(CapabilitiesEnum.IsEnterprise) + ? renderSidebarItems(createAdminSidebarEnterpriseEntries()) + : nothing} + + +
+
+
+
+
+ + +
+
+ ${renderNotificationDrawerPanel(this.drawer)} +
+
+ +
+
-
`; + ${this.commandPalette}`; } //#endregion diff --git a/web/src/admin/Routes.ts b/web/src/admin/Routes.ts index fd4bf49e17..21bc532f9b 100644 --- a/web/src/admin/Routes.ts +++ b/web/src/admin/Routes.ts @@ -1,7 +1,5 @@ import "#admin/admin-overview/AdminOverviewPage"; -import { globalAK } from "#common/global"; - import { ID_REGEX, Route, SLUG_REGEX, UUID_REGEX } from "#elements/router/Route"; import { html } from "lit"; @@ -198,14 +196,3 @@ export const ROUTES: Route[] = [ return html``; }), ]; - -/** - * Application route helpers. - * - * @TODO: This API isn't quite right yet. Revisit after the hash router is replaced. - */ -export const ApplicationRoute = { - EditURL(slug: string, base = globalAK().api.base) { - return `${base}if/admin/#/core/applications/${slug}`; - }, -} as const; diff --git a/web/src/admin/admin-overview/SystemTasksPage.ts b/web/src/admin/admin-overview/SystemTasksPage.ts index ef3b6ceb1a..82242eccd2 100644 --- a/web/src/admin/admin-overview/SystemTasksPage.ts +++ b/web/src/admin/admin-overview/SystemTasksPage.ts @@ -4,6 +4,7 @@ import "#elements/buttons/SpinnerButton/index"; import "#elements/events/LogViewer"; import "#elements/tasks/ScheduleList"; import "#elements/tasks/TaskList"; +import "#admin/rbac/ObjectPermissionModal"; import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; import { AKElement } from "#elements/Base"; diff --git a/web/src/admin/admin-settings/AdminSettingsFooterLinks.ts b/web/src/admin/admin-settings/AdminSettingsFooterLinks.ts index 0f036c32ab..5697abc246 100644 --- a/web/src/admin/admin-settings/AdminSettingsFooterLinks.ts +++ b/web/src/admin/admin-settings/AdminSettingsFooterLinks.ts @@ -1,4 +1,4 @@ -import { AkControlElement } from "#elements/AkControlElement"; +import { AKControlElement } from "#elements/ControlElement"; import { type Spread } from "#elements/types"; import { ifPresent } from "#elements/utils/attributes"; @@ -22,7 +22,7 @@ const hasLegalScheme = (url: string) => LEGAL_SCHEMES.some((scheme) => url.substr(0, scheme.length).toLowerCase() === scheme); @customElement("ak-admin-settings-footer-link") -export class FooterLinkInput extends AkControlElement { +export class FooterLinkInput extends AKControlElement { static styles = [ PFInputGroup, PFFormControl, diff --git a/web/src/admin/admin-settings/AdminSettingsPage.ts b/web/src/admin/admin-settings/AdminSettingsPage.ts index 556fdb9c22..2ddca9bd24 100644 --- a/web/src/admin/admin-settings/AdminSettingsPage.ts +++ b/web/src/admin/admin-settings/AdminSettingsPage.ts @@ -1,5 +1,5 @@ import "#admin/admin-settings/AdminSettingsForm"; -import "#components/events/ObjectChangelog"; +import "#admin/events/ObjectChangelog"; import "#elements/CodeMirror"; import "#elements/EmptyState"; import "#elements/Tabs"; diff --git a/web/src/elements/stories/ak-array-input.stories.ts b/web/src/admin/admin-settings/stories/ak-array-input.stories.ts similarity index 97% rename from web/src/elements/stories/ak-array-input.stories.ts rename to web/src/admin/admin-settings/stories/ak-array-input.stories.ts index f5f030c81f..416a387498 100644 --- a/web/src/elements/stories/ak-array-input.stories.ts +++ b/web/src/admin/admin-settings/stories/ak-array-input.stories.ts @@ -1,8 +1,8 @@ import "#admin/admin-settings/AdminSettingsFooterLinks"; import "#elements/messages/MessageContainer"; -import "../ak-array-input.js"; +import "#elements/ak-array-input"; -import { IArrayInput } from "../ak-array-input.js"; +import { IArrayInput } from "#elements/ak-array-input"; import { FooterLinkInput } from "#admin/admin-settings/AdminSettingsFooterLinks"; diff --git a/web/src/admin/applications/ApplicationForm.ts b/web/src/admin/applications/ApplicationForm.ts index e03470670f..0254e27123 100644 --- a/web/src/admin/applications/ApplicationForm.ts +++ b/web/src/admin/applications/ApplicationForm.ts @@ -1,4 +1,4 @@ -import "#admin/applications/ProviderSelectModal"; +import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; import "#components/ak-file-search-input"; import "#components/ak-radio-input"; import "#components/ak-slug-input"; @@ -11,9 +11,9 @@ import "#elements/forms/HorizontalFormElement"; import "#elements/forms/ModalForm"; import "#elements/forms/Radio"; import "#elements/forms/SearchSelect/ak-search-select"; -import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; -import "./components/ak-backchannel-input.js"; -import "./components/ak-provider-search-input.js"; +import "#admin/applications/ProviderSelectForm"; +import "#admin/applications/components/ak-backchannel-input"; +import "#admin/applications/components/ak-provider-search-input"; import { DEFAULT_CONFIG } from "#common/api/config"; @@ -40,6 +40,9 @@ 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"); + protected override async loadInstance(pk: string): Promise { const app = await this.#api.coreApplicationsRetrieve({ slug: pk, @@ -104,6 +107,8 @@ export class ApplicationForm extends WithCapabilitiesConfig(ModelForm
`; } + + //#endregion } declare global { diff --git a/web/src/admin/applications/ApplicationListPage.css b/web/src/admin/applications/ApplicationListPage.css new file mode 100644 index 0000000000..98a6e4ea5d --- /dev/null +++ b/web/src/admin/applications/ApplicationListPage.css @@ -0,0 +1,15 @@ +/* Fix alignment issues with images in tables */ +.pf-c-table tbody > tr > * { + vertical-align: middle; +} + +tr td:first-child { + width: auto; + min-width: 0px; + text-align: center; + vertical-align: middle; +} + +.pf-c-sidebar.pf-m-gutter > .pf-c-sidebar__main > * + * { + margin-left: calc(var(--pf-c-sidebar__main--child--MarginLeft) / 2); +} diff --git a/web/src/admin/applications/ApplicationListPage.ts b/web/src/admin/applications/ApplicationListPage.ts index 5933ba19de..dc73c06e58 100644 --- a/web/src/admin/applications/ApplicationListPage.ts +++ b/web/src/admin/applications/ApplicationListPage.ts @@ -1,12 +1,13 @@ +import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; import "#elements/forms/ConfirmationForm"; -import "#admin/applications/ApplicationForm"; import "#elements/AppIcon"; import "#elements/ak-mdx/ak-mdx"; import "#elements/buttons/SpinnerButton/ak-spinner-button"; import "#elements/forms/DeleteBulkForm"; import "#elements/forms/ModalForm"; -import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; -import "./ApplicationWizardHint.js"; +import "#elements/modals/ak-modal"; +import "#admin/applications/ApplicationForm"; +import "#admin/applications/ApplicationWizardHint"; import { DEFAULT_CONFIG } from "#common/api/config"; @@ -17,34 +18,31 @@ import { TablePage } from "#elements/table/TablePage"; import { SlottedTemplateResult } from "#elements/types"; import { ifPresent } from "#elements/utils/attributes"; +import { ApplicationForm } from "#admin/applications/ApplicationForm"; +import Styles from "#admin/applications/ApplicationListPage.css"; +import { AkApplicationWizard } from "#admin/applications/wizard/ak-application-wizard"; + import { Application, CoreApi, PoliciesApi } from "@goauthentik/api"; import MDApplication from "~docs/add-secure-apps/applications/index.md"; import { msg, str } from "@lit/localize"; -import { css, CSSResult, html, nothing, TemplateResult } from "lit"; +import { css, CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; import PFCard from "@patternfly/patternfly/components/Card/card.css"; -export const applicationListStyle = css` - /* Fix alignment issues with images in tables */ - .pf-c-table tbody > tr > * { - vertical-align: middle; - } - tr td:first-child { - width: auto; - min-width: 0px; - text-align: center; - vertical-align: middle; - } - .pf-c-sidebar.pf-m-gutter > .pf-c-sidebar__main > * + * { - margin-left: calc(var(--pf-c-sidebar__main--child--MarginLeft) / 2); - } -`; +export const applicationListStyle = css``; @customElement("ak-application-list") export class ApplicationListPage extends WithBrandConfig(TablePage) { + public static styles: CSSResult[] = [ + // --- + ...TablePage.styles, + PFCard, + Styles, + ]; + protected override searchEnabled = true; public pageTitle = msg("Applications"); public get pageDescription() { @@ -54,11 +52,11 @@ export class ApplicationListPage extends WithBrandConfig(TablePage) } public pageIcon = "pf-icon pf-icon-applications"; - checkbox = true; - clearOnRefresh = true; + public override checkbox = true; + public override clearOnRefresh = true; @property() - order = "name"; + public order = "name"; async apiEndpoint(): Promise> { return new CoreApi(DEFAULT_CONFIG).coreApplicationsList({ @@ -67,7 +65,15 @@ export class ApplicationListPage extends WithBrandConfig(TablePage) }); } - static styles: CSSResult[] = [...TablePage.styles, PFCard, applicationListStyle]; + public override firstUpdated(changed: PropertyValues): void { + super.firstUpdated(changed); + + if (getURLParam("createWizard", false)) { + AkApplicationWizard.showModal(); + } else if (getURLParam("createForm", false)) { + ApplicationForm.showModal(); + } + } protected columns: TableColumn[] = [ ["", undefined, msg("Application Icon")], @@ -91,7 +97,7 @@ export class ApplicationListPage extends WithBrandConfig(TablePage) `; } - renderToolbarSelected(): TemplateResult { + protected override renderToolbarSelected(): TemplateResult { const disabled = this.selectedElements.length < 1; return html`) `; } - row(item: Application): SlottedTemplateResult[] { + protected row(item: Application): SlottedTemplateResult[] { return [ html`) : html`-`, html`${item.providerObj?.verboseName || msg("-")}`, html`
- - ${msg("Update")} - ${msg("Update Application")} - - - - + ${item.launchUrl ? html`) ]; } - renderObjectCreate(): TemplateResult { - return html` - - - - ${msg("Create")} - ${msg("Create Application")} - - - `; + protected override renderObjectCreate(): TemplateResult { + return html` + + + `; } - renderToolbar(): TemplateResult { - return html` ${super.renderToolbar()} + protected override renderToolbar(): TemplateResult { + return html`${super.renderToolbar()} { return new PoliciesApi(DEFAULT_CONFIG).policiesAllCacheClearCreate(); }} diff --git a/web/src/admin/applications/ApplicationViewPage.ts b/web/src/admin/applications/ApplicationViewPage.ts index d9d77a6a9b..c2b160ae6a 100644 --- a/web/src/admin/applications/ApplicationViewPage.ts +++ b/web/src/admin/applications/ApplicationViewPage.ts @@ -5,7 +5,7 @@ import "#admin/applications/entitlements/ApplicationEntitlementPage"; import "#admin/policies/BoundPoliciesList"; import "#admin/rbac/ObjectPermissionsPage"; import "#admin/lifecycle/ObjectLifecyclePage"; -import "#components/events/ObjectChangelog"; +import "#admin/events/ObjectChangelog"; import "#elements/AppIcon"; import "#elements/EmptyState"; import "#elements/Tabs"; @@ -238,7 +238,9 @@ export class ApplicationViewPage extends WithLicenseSummary(AKElement) {
- ${msg("Update")} + ${msg("Save Changes")} ${msg("Update Application")} diff --git a/web/src/admin/applications/ProviderSelectForm.ts b/web/src/admin/applications/ProviderSelectForm.ts new file mode 100644 index 0000000000..1434c9d773 --- /dev/null +++ b/web/src/admin/applications/ProviderSelectForm.ts @@ -0,0 +1,57 @@ +import "#elements/buttons/SpinnerButton/index"; + +import { DEFAULT_CONFIG } from "#common/api/config"; + +import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table"; +import { SlottedTemplateResult } from "#elements/types"; + +import { Provider, ProvidersApi } from "@goauthentik/api"; + +import { msg } from "@lit/localize"; +import { html } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +@customElement("ak-provider-select-form") +export class ProviderSelectForm extends Table { + public override checkbox = true; + public override checkboxChip = true; + + protected override searchEnabled = true; + + @property({ type: Boolean }) + public backchannel = false; + + public override order = "name"; + + protected async apiEndpoint(): Promise> { + return new ProvidersApi(DEFAULT_CONFIG).providersAllList({ + ...(await this.defaultEndpointConfig()), + backchannel: this.backchannel, + }); + } + + protected columns: TableColumn[] = [ + // --- + [msg("Name"), "username"], + [msg("Type")], + ]; + + protected row(item: Provider): SlottedTemplateResult[] { + return [ + html`
+
${item.name}
+
`, + html`${item.verboseName}`, + ]; + } + + protected renderSelectedChip(item: Provider): SlottedTemplateResult { + return item.name; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ak-provider-select-form": ProviderSelectForm; + } +} diff --git a/web/src/admin/applications/ProviderSelectModal.ts b/web/src/admin/applications/ProviderSelectModal.ts deleted file mode 100644 index d09b964087..0000000000 --- a/web/src/admin/applications/ProviderSelectModal.ts +++ /dev/null @@ -1,91 +0,0 @@ -import "#elements/buttons/SpinnerButton/index"; - -import { DEFAULT_CONFIG } from "#common/api/config"; - -import { PaginatedResponse, TableColumn } from "#elements/table/Table"; -import { TableModal } from "#elements/table/TableModal"; -import { SlottedTemplateResult } from "#elements/types"; - -import { Provider, ProvidersApi } from "@goauthentik/api"; - -import { msg } from "@lit/localize"; -import { html, TemplateResult } from "lit"; -import { customElement, property } from "lit/decorators.js"; - -@customElement("ak-provider-select-table") -export class ProviderSelectModal extends TableModal { - public override checkbox = true; - public override checkboxChip = true; - - protected override searchEnabled = true; - - @property({ type: Boolean }) - public backchannel = false; - - @property({ attribute: false }) - public confirm!: (selectedItems: Provider[]) => Promise; - - public override order = "name"; - - async apiEndpoint(): Promise> { - return new ProvidersApi(DEFAULT_CONFIG).providersAllList({ - ...(await this.defaultEndpointConfig()), - backchannel: this.backchannel, - }); - } - - protected columns: TableColumn[] = [ - // --- - [msg("Name"), "username"], - [msg("Type")], - ]; - - row(item: Provider): SlottedTemplateResult[] { - return [ - html`
-
${item.name}
-
`, - html`${item.verboseName}`, - ]; - } - - renderSelectedChip(item: Provider): TemplateResult { - return html`${item.name}`; - } - - renderModalInner(): TemplateResult { - return html`
-
-

- ${msg("Select providers to add to application")} -

-
-
-
${this.renderTable()}
-
- { - await this.confirm(this.selectedElements); - this.open = false; - }} - class="pf-m-primary" - > - ${msg("Add")}   - { - this.open = false; - }} - class="pf-m-secondary" - > - ${msg("Cancel")} - -
`; - } -} - -declare global { - interface HTMLElementTagNameMap { - "ak-provider-select-table": ProviderSelectModal; - } -} diff --git a/web/src/admin/applications/components/ak-backchannel-input.ts b/web/src/admin/applications/components/ak-backchannel-input.ts index fbc1e369e5..cc1260973c 100644 --- a/web/src/admin/applications/components/ak-backchannel-input.ts +++ b/web/src/admin/applications/components/ak-backchannel-input.ts @@ -1,12 +1,17 @@ -import "#admin/applications/ProviderSelectModal"; +import "#admin/applications/ProviderSelectForm"; +import "#elements/forms/HorizontalFormElement"; import "#elements/chips/Chip"; import "#elements/chips/ChipGroup"; +import "#elements/forms/Form"; import { AKElement } from "#elements/Base"; +import { AKFormSubmitEvent } from "#elements/forms/Form"; +import { renderModal } from "#elements/modals/utils"; import { SlottedTemplateResult } from "#elements/types"; import { Provider } from "@goauthentik/api"; +import { msg } from "@lit/localize"; import { html, nothing } from "lit"; import { customElement, property } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; @@ -56,6 +61,24 @@ export class AkBackchannelProvidersInput extends AKElement { @property({ type: String }) public help = ""; + protected openSelectBackchannelProvidersModal = () => { + return renderModal(html` + ) => { + const providers = event.target.toJSON(); + + this.confirm(providers); + }} + > + ${this.help ? html`

${this.help}

` : nothing} + + +
+ `); + }; + render() { const renderOneChip = (provider: Provider) => html`
- - - +
- ${map(this.providers, renderOneChip)} + ${map(this.providers, renderOneChip)}
${this.help ? html`

${this.help}

` : nothing} diff --git a/web/src/admin/applications/components/ak-provider-search-input.ts b/web/src/admin/applications/components/ak-provider-search-input.ts index 120fe9cb99..24457d1f75 100644 --- a/web/src/admin/applications/components/ak-provider-search-input.ts +++ b/web/src/admin/applications/components/ak-provider-search-input.ts @@ -5,6 +5,7 @@ import { DEFAULT_CONFIG } from "#common/api/config"; import { groupBy } from "#common/utils"; import { AKElement } from "#elements/Base"; +import { ifPresent } from "#elements/utils/attributes"; import { AKLabel } from "#components/ak-label"; @@ -12,6 +13,7 @@ import { IDGenerator } from "#packages/core/id"; import { Provider, ProvidersAllListRequest, ProvidersApi } from "@goauthentik/api"; +import { msg } from "@lit/localize/init/install"; import { html, nothing } from "lit"; import { customElement, property } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; @@ -91,7 +93,7 @@ export class AkProviderInput extends AKElement { render() { const readOnlyValue = this.readOnly && typeof this.value === "number"; - return html` + return html` ${AKLabel( { slot: "label", @@ -105,6 +107,7 @@ export class AkProviderInput extends AKElement { ? html`` : nothing} ${this.help ? html`

${this.help}

` : nothing} diff --git a/web/src/admin/applications/entitlements/ApplicationEntitlementPage.ts b/web/src/admin/applications/entitlements/ApplicationEntitlementPage.ts index 20029517c8..a40d3e8658 100644 --- a/web/src/admin/applications/entitlements/ApplicationEntitlementPage.ts +++ b/web/src/admin/applications/entitlements/ApplicationEntitlementPage.ts @@ -8,12 +8,11 @@ import "#elements/forms/ModalForm"; import { DEFAULT_CONFIG } from "#common/api/config"; import { PFSize } from "#common/enums"; +import { PolicyBindingCheckTarget } from "#common/policies/utils"; import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table"; import { SlottedTemplateResult } from "#elements/types"; -import { PolicyBindingCheckTarget } from "#admin/policies/utils"; - import { ApplicationEntitlement, CoreApi, @@ -77,7 +76,7 @@ export class ApplicationEntitlementsPage extends Table { return [ html`${item.name}`, html` - ${msg("Update")} + ${msg("Save Changes")} ${msg("Update Entitlement")} > extends WizardStep { - static styles = [...WizardStep.styles, ...styles]; +/** + * Base class for application wizard steps. Provides common functionality such as form handling and wizard state management. + * + * @prop wizard - The current state of the application wizard, shared across all steps. + */ +export abstract class ApplicationWizardStep> extends WizardStep { + static styles = [...WizardStep.styles, ...ApplicationWizardStyles]; @property({ type: Object, attribute: false }) - wizard!: ApplicationWizardState; + public wizard!: ApplicationWizardState; - // As recommended in [WizardStep](../../../components/ak-wizard/WizardStep.ts), we override - // these fields and provide them to all the child classes. - protected wizardTitle = msg("New application"); - protected wizardDescription = msg("Create a new application and configure a provider for it."); + protected override wizardTitle = msg("New application"); + protected override wizardDescription = msg( + "Create a new application and configure a provider for it.", + ); public canCancel = true; // This should be overridden in the children for more precise targeting. @@ -73,25 +76,6 @@ export class ApplicationWizardStep> extends Wiza ]); } - protected removeErrors( - keyToDelete: keyof ApplicationTransactionValidationError, - ): ValidationError | undefined { - if (!this.wizard.errors) { - return undefined; - } - const empty = {}; - const errors = Object.entries(this.wizard.errors).reduce( - (acc, [key, value]) => - key === keyToDelete || - value === undefined || - (Array.isArray(this.wizard?.errors?.[key]) && this.wizard.errors[key].length === 0) - ? acc - : { ...acc, [key]: value }, - empty, - ); - return errors; - } - // This pattern became visible during development, and the order is important: wizard updating // and validation must complete before navigation is attempted. public handleUpdate( diff --git a/web/src/admin/applications/wizard/ak-application-wizard-main.ts b/web/src/admin/applications/wizard/ak-application-wizard-main.ts index 16389b7d6f..c521662370 100644 --- a/web/src/admin/applications/wizard/ak-application-wizard-main.ts +++ b/web/src/admin/applications/wizard/ak-application-wizard-main.ts @@ -1,13 +1,10 @@ import "#components/ak-wizard/ak-wizard-steps"; -import "./steps/ak-application-wizard-application-step.js"; -import "./steps/ak-application-wizard-bindings-step.js"; -import "./steps/ak-application-wizard-edit-binding-step.js"; -import "./steps/ak-application-wizard-provider-choice-step.js"; -import "./steps/ak-application-wizard-provider-step.js"; -import "./steps/ak-application-wizard-submit-step.js"; - -import { applicationWizardProvidersContext } from "./ContextIdentity.js"; -import { type ApplicationWizardState, type ApplicationWizardStateUpdate } from "./types.js"; +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"; @@ -16,6 +13,12 @@ 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"; @@ -50,10 +53,14 @@ export const providerTypePriority: ProviderModelNameEnum[] = [ @customElement("ak-application-wizard-main") export class AkApplicationWizardMain extends AKElement { - @state() - wizard: ApplicationWizardState = freshWizardState(); + protected createRenderRoot(): HTMLElement | DocumentFragment { + return this; + } - wizardProviderProvider = new ContextProvider(this, { + @state() + protected wizard: ApplicationWizardState = freshWizardState(); + + protected wizardProviderProvider = new ContextProvider(this, { context: applicationWizardProvidersContext, initialValue: [], }); @@ -63,8 +70,9 @@ export class AkApplicationWizardMain extends AKElement { this.addEventListener(WizardUpdateEvent.eventName, this.handleUpdate); } - connectedCallback() { + public override connectedCallback() { super.connectedCallback(); + new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((providerTypes) => { const providerNameToProviderMap = new Map( providerTypes.map((providerType) => [providerType.modelName, providerType]), @@ -85,7 +93,8 @@ export class AkApplicationWizardMain extends AKElement { handleUpdate(ev: WizardUpdateEvent) { ev.stopPropagation(); const update = ev.content; - if (update !== undefined) { + + if (typeof update !== "undefined") { this.wizard = { ...this.wizard, ...update, diff --git a/web/src/admin/applications/wizard/ak-application-wizard.ts b/web/src/admin/applications/wizard/ak-application-wizard.ts index 7bf4e90523..003ed250c7 100644 --- a/web/src/admin/applications/wizard/ak-application-wizard.ts +++ b/web/src/admin/applications/wizard/ak-application-wizard.ts @@ -1,28 +1,41 @@ -import "./ak-application-wizard-main.js"; +import "#admin/applications/wizard/ak-application-wizard-main"; -import { ModalButton } from "#elements/buttons/ModalButton"; -import { bound } from "#elements/decorators/bound"; +import { AKModal } from "#elements/modals/ak-modal"; +import { SlottedTemplateResult } from "#elements/types"; import { WizardCloseEvent } from "#components/ak-wizard/events"; -import { html } from "lit"; +import { msg } from "@lit/localize"; +import { css, CSSResult, html } from "lit"; import { customElement } from "lit/decorators.js"; @customElement("ak-application-wizard") -export class AkApplicationWizard extends ModalButton { +export class AkApplicationWizard extends AKModal { + public static override formatARIALabel?(): string { + return msg("New Application Wizard"); + } + + public static override styles: CSSResult[] = [ + ...super.styles, + css` + [part="main"] { + display: block; + } + `, + ]; + constructor() { super(); - this.addEventListener(WizardCloseEvent.eventName, this.onCloseEvent); + + this.addEventListener(WizardCloseEvent.eventName, this.closeListener); } - @bound - onCloseEvent(ev: WizardCloseEvent) { - ev.stopPropagation(); - this.open = false; + protected renderCloseButton(): SlottedTemplateResult { + return null; } - renderModalInner() { - return html` `; + render() { + return html``; } } diff --git a/web/src/admin/applications/wizard/steps/SubmitStepOverviewRenderers.ts b/web/src/admin/applications/wizard/steps/SubmitStepOverviewRenderers.ts index 0680c6426c..2cb768db3c 100644 --- a/web/src/admin/applications/wizard/steps/SubmitStepOverviewRenderers.ts +++ b/web/src/admin/applications/wizard/steps/SubmitStepOverviewRenderers.ts @@ -1,9 +1,11 @@ import "#components/ak-status-label"; -import { OneOfProvider } from "../types.js"; +import { SlottedTemplateResult } from "#elements/types"; import { type DescriptionPair, renderDescriptionList } from "#components/DescriptionList"; +import { OneOfProvider } from "#admin/applications/wizard/steps/providers/shared"; + import { ClientTypeEnum, LDAPProvider, @@ -30,39 +32,38 @@ const renderSummary = (type: string, name: string, fields: DescriptionPair[]) => threecolumn: true, }); -function renderSAMLOverview(rawProvider: OneOfProvider) { - const provider = rawProvider as SAMLProvider; +export type ProviderOverview = ( + provider: T, +) => SlottedTemplateResult; +const renderSAMLOverview: ProviderOverview = (provider) => { return renderSummary("SAML", provider.name, [ [msg("ACS URL"), provider.acsUrl], [msg("Audience"), provider.audience || "-"], [msg("Issuer"), provider.issuer], ]); -} - -function renderSAMLImportOverview(rawProvider: OneOfProvider) { - const provider = rawProvider as ProvidersSamlImportMetadataCreateRequest; +}; +const renderSAMLImportOverview: ProviderOverview = ( + provider, +) => { return renderSummary("SAML", provider.name, [ [msg("Authorization flow"), provider.authorizationFlow ?? "-"], [msg("Invalidation flow"), provider.invalidationFlow ?? "-"], ]); -} +}; -function renderSCIMOverview(rawProvider: OneOfProvider) { - const provider = rawProvider as SCIMProvider; +const renderSCIMOverview: ProviderOverview = (provider) => { return renderSummary("SCIM", provider.name, [[msg("URL"), provider.url]]); -} +}; -function renderRadiusOverview(rawProvider: OneOfProvider) { - const provider = rawProvider as RadiusProvider; +const renderRadiusOverview: ProviderOverview = (provider) => { return renderSummary("Radius", provider.name, [ [msg("Client Networks"), provider.clientNetworks], ]); -} +}; -function renderRACOverview(rawProvider: OneOfProvider) { - const provider = rawProvider as RACProvider; +const renderRACOverview: ProviderOverview = (provider) => { return renderSummary("RAC", provider.name, [ [msg("Connection expiry"), provider.connectionExpiry ?? "-"], [ @@ -72,7 +73,7 @@ function renderRACOverview(rawProvider: OneOfProvider) { : msg("None"), ], ]); -} +}; function formatRedirectUris(uris: RedirectURI[] = []) { return uris.length > 0 @@ -90,76 +91,76 @@ function formatRedirectUris(uris: RedirectURI[] = []) { : "-"; } -const proxyModeToLabel = new Map([ - [ProxyMode.Proxy, msg("Proxy")], - [ProxyMode.ForwardSingle, msg("Forward auth (single application)")], - [ProxyMode.ForwardDomain, msg("Forward auth (domain-level)")], - [ProxyMode.UnknownDefaultOpenApi, msg("Unknown proxy mode")], -]); +const proxyModeToLabel = { + [ProxyMode.Proxy]: () => msg("Proxy"), + [ProxyMode.ForwardSingle]: () => msg("Forward auth (single application)"), + [ProxyMode.ForwardDomain]: () => msg("Forward auth (domain-level)"), + [ProxyMode.UnknownDefaultOpenApi]: () => msg("Unknown proxy mode"), +} as const satisfies Record string>; + +const renderProxyOverview: ProviderOverview = (provider) => { + const proxyHostMappings: DescriptionPair[] = match( + provider.mode, + ) + .with(ProxyMode.Proxy, () => { + return [ + [msg("Internal Host"), provider.internalHost], + [msg("External Host"), provider.externalHost], + ]; + }) + .with(ProxyMode.ForwardSingle, () => { + return [[msg("External Host"), provider.externalHost]]; + }) + .with(ProxyMode.ForwardDomain, () => { + return [ + [msg("Authentication URL"), provider.externalHost], + [msg("Cookie domain"), provider.cookieDomain], + ]; + }) + .otherwise(() => { + throw new Error( + `Unrecognized proxy mode: ${provider.mode?.toString() ?? "-- undefined __"}`, + ); + }); + + const label = proxyModeToLabel[provider.mode ?? ProxyMode.Proxy]; -function renderProxyOverview(rawProvider: OneOfProvider) { - const provider = rawProvider as ProxyProvider; return renderSummary("Proxy", provider.name, [ - [msg("Mode"), proxyModeToLabel.get(provider.mode ?? ProxyMode.Proxy)], - ...match(provider.mode) - .with( - ProxyMode.Proxy, - () => - [ - [msg("Internal Host"), provider.internalHost], - [msg("External Host"), provider.externalHost], - ] as DescriptionPair[], - ) - .with( - ProxyMode.ForwardSingle, - () => [[msg("External Host"), provider.externalHost]] as DescriptionPair[], - ) - .with( - ProxyMode.ForwardDomain, - () => - [ - [msg("Authentication URL"), provider.externalHost], - [msg("Cookie domain"), provider.cookieDomain], - ] as DescriptionPair[], - ) - .otherwise(() => { - throw new Error( - `Unrecognized proxy mode: ${provider.mode?.toString() ?? "-- undefined __"}`, - ); - }), + [msg("Mode"), label()], + ...proxyHostMappings, [ msg("Basic-Auth"), - html` `, ], ]); -} +}; -const clientTypeToLabel = new Map([ - [ClientTypeEnum.Confidential, msg("Confidential")], - [ClientTypeEnum.Public, msg("Public")], - [ClientTypeEnum.UnknownDefaultOpenApi, msg("Unknown type")], -]); +const clientTypeToLabel = { + [ClientTypeEnum.Confidential]: () => msg("Confidential"), + [ClientTypeEnum.Public]: () => msg("Public"), + [ClientTypeEnum.UnknownDefaultOpenApi]: () => msg("Unknown type"), +} as const satisfies Record string>; + +const renderOAuth2Overview: ProviderOverview = (provider) => { + const label = provider.clientType ? clientTypeToLabel[provider.clientType]() : ""; -function renderOAuth2Overview(rawProvider: OneOfProvider) { - const provider = rawProvider as OAuth2Provider; return renderSummary("OAuth2", provider.name, [ - [msg("Client type"), provider.clientType ? clientTypeToLabel.get(provider.clientType) : ""], + [msg("Client type"), label], [msg("Client ID"), provider.clientId], [msg("Redirect URIs"), formatRedirectUris(provider.redirectUris)], ]); -} +}; -function renderLDAPOverview(rawProvider: OneOfProvider) { - const provider = rawProvider as LDAPProvider; +const renderLDAPOverview: ProviderOverview = (provider) => { return renderSummary("Proxy", provider.name, [[msg("Base DN"), provider.baseDn]]); -} +}; const providerName = (p: ProviderModelEnum): string => p.toString().split(".")[1]; -export const providerRenderers = new Map([ +export const providerRenderers = new Map>([ [providerName(ProviderModelEnum.AuthentikProvidersSamlSamlprovider), renderSAMLOverview], ["samlproviderimportmodel", renderSAMLImportOverview], [providerName(ProviderModelEnum.AuthentikProvidersScimScimprovider), renderSCIMOverview], @@ -168,4 +169,4 @@ export const providerRenderers = new Map([ [providerName(ProviderModelEnum.AuthentikProvidersProxyProxyprovider), renderProxyOverview], [providerName(ProviderModelEnum.AuthentikProvidersOauth2Oauth2provider), renderOAuth2Overview], [providerName(ProviderModelEnum.AuthentikProvidersLdapLdapprovider), renderLDAPOverview], -]); +] satisfies [string, ProviderOverview][] as [string, ProviderOverview][]); 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 b16ae97a8e..565f038527 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 @@ -8,13 +8,17 @@ import "#components/ak-textarea-input"; import "#elements/forms/FormGroup"; import "#elements/forms/HorizontalFormElement"; -import { ApplicationWizardStateUpdate, ValidationRecord } from "../types.js"; +import { omitKeys, trimMany } from "#common/objects"; import { isSlug } from "#elements/router/utils"; -import { type NavigableButton, type WizardButton } from "#components/ak-wizard/types"; +import { type NavigableButton, type WizardButton } from "#components/ak-wizard/shared"; import { ApplicationWizardStep } from "#admin/applications/wizard/ApplicationWizardStep"; +import { + ApplicationWizardStateUpdate, + WizardValidationRecord, +} from "#admin/applications/wizard/steps/providers/shared"; import { policyEngineModes } from "#admin/policies/PolicyEngineModes"; import { AdminFileListUsageEnum, type ApplicationRequest } from "@goauthentik/api"; @@ -26,18 +30,14 @@ import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; -function trimMany(target: T, keys: K[]): Pick { - const output = {} as Record; - - for (const key of keys) { - const value = target[key]; - - output[key] = typeof value === "string" ? value.trim() : value; - } - - return output as Pick; -} - +/** + * The first step of the application wizard, responsible for collecting + * basic application information such as name, slug, group, and UI settings. + * + * This step performs validation on the form inputs and updates the wizard state accordingly when the "Next" button is clicked. + * + * @prop wizard - The current state of the application wizard, shared across all steps. + */ @customElement("ak-application-wizard-application-step") export class ApplicationWizardApplicationStep extends ApplicationWizardStep { label = msg("Application"); @@ -62,13 +62,17 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep { } get buttons(): WizardButton[] { - return [{ kind: "next", destination: "provider-choice" }, { kind: "cancel" }]; + return [ + // --- + { kind: "cancel" }, + { kind: "next", destination: "provider-choice" }, + ]; } get valid() { this.errors = new Map(); - const values = trimMany(this.formValues, ["metaLaunchUrl", "name", "slug"]); + const values = trimMany(this.formValues, "metaLaunchUrl", "name", "slug"); if (!values.name) { this.errors.set("name", msg("An application name is required")); @@ -85,7 +89,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep { return this.errors.size === 0; } - override handleButton(button: NavigableButton) { + public override handleButton(button: NavigableButton) { if (button.kind !== "next") { return super.handleButton(button); } @@ -102,7 +106,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep { const payload: ApplicationWizardStateUpdate = { app, - errors: this.removeErrors("app"), + errors: omitKeys(this.wizard.errors, "app"), }; if (!this.wizard.provider?.name?.trim() && app.name) { @@ -116,7 +120,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep { }); } - renderForm(app: Partial, errors: ValidationRecord) { + protected renderForm(app: Partial, errors: WizardValidationRecord = {}) { return html` ${msg("Configure the Application")}
@@ -214,10 +223,7 @@ export class ApplicationWizardApplicationStep extends ApplicationWizardStep { if (!(this.wizard.app && this.wizard.errors)) { throw new Error("Application Step received uninitialized wizard context."); } - return this.renderForm( - this.wizard.app as ApplicationRequest, - this.wizard.errors?.app ?? {}, - ); + return this.renderForm(this.wizard.app, this.wizard.errors?.app); } } diff --git a/web/src/admin/applications/wizard/steps/ak-application-wizard-bindings-step.ts b/web/src/admin/applications/wizard/steps/ak-application-wizard-bindings-step.ts index 9afa985ac4..1e416bb896 100644 --- a/web/src/admin/applications/wizard/steps/ak-application-wizard-bindings-step.ts +++ b/web/src/admin/applications/wizard/steps/ak-application-wizard-bindings-step.ts @@ -8,15 +8,14 @@ import "#components/ak-text-input"; import "#elements/ak-table/ak-select-table"; import "#elements/forms/FormGroup"; import "#elements/forms/HorizontalFormElement"; -import "./bindings/ak-application-wizard-bindings-toolbar.js"; - -import { makeEditButton } from "./bindings/ak-application-wizard-bindings-edit-button.js"; +import "#admin/applications/wizard/steps/bindings/ak-application-wizard-bindings-toolbar"; import { SelectTable } from "#elements/ak-table/ak-select-table"; -import { type WizardButton } from "#components/ak-wizard/types"; +import { type WizardButton } from "#components/ak-wizard/shared"; import { ApplicationWizardStep } from "#admin/applications/wizard/ApplicationWizardStep"; +import { makeEditButton } from "#admin/applications/wizard/steps/bindings/ak-application-wizard-bindings-edit-button"; import { match, P } from "ts-pattern"; @@ -34,15 +33,18 @@ const COLUMNS = [ [msg("Actions"), null, msg("Row Actions")], ]; +/** + * @prop wizard - The current state of the application wizard, shared across all steps. + */ @customElement("ak-application-wizard-bindings-step") export class ApplicationWizardBindingsStep extends ApplicationWizardStep { label = msg("Configure Bindings"); get buttons(): WizardButton[] { return [ - { kind: "next", destination: "submit" }, - { kind: "back", destination: "provider" }, { kind: "cancel" }, + { kind: "back", destination: "provider" }, + { kind: "next", destination: "submit" }, ]; } 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 502dd47bd2..180ef481ab 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 @@ -10,19 +10,19 @@ import "#elements/forms/SearchSelect/ak-search-select-ez"; import "#elements/forms/SearchSelect/index"; import { DEFAULT_CONFIG } from "#common/api/config"; +import { + createPassFailOptions, + PolicyBindingCheckTarget, + PolicyObjectKeys, +} from "#common/policies/utils"; import { groupBy } from "#common/utils"; import { ISearchSelectConfig } from "#elements/forms/SearchSelect/ak-search-select-ez"; import { type SearchSelectBase } from "#elements/forms/SearchSelect/SearchSelect"; -import { type NavigableButton, type WizardButton } from "#components/ak-wizard/types"; +import { type NavigableButton, type WizardButton } from "#components/ak-wizard/shared"; import { ApplicationWizardStep } from "#admin/applications/wizard/ApplicationWizardStep"; -import { - createPassFailOptions, - PolicyBindingCheckTarget, - PolicyObjectKeys, -} from "#admin/policies/utils"; import { CoreApi, Group, PoliciesApi, Policy, PolicyBinding, User } from "@goauthentik/api"; @@ -32,8 +32,11 @@ import { customElement, query, state } from "lit/decorators.js"; const withQuery = (search: string | undefined, args: T) => (search ? { ...args, search } : args); +/** + * @prop wizard - The current state of the application wizard, shared across all steps. + */ @customElement("ak-application-wizard-edit-binding-step") -export class ApplicationWizardEditBindingStep extends ApplicationWizardStep { +export class ApplicationWizardEditBindingStep extends ApplicationWizardStep { label = msg("Edit Binding"); hide = true; @@ -54,13 +57,13 @@ export class ApplicationWizardEditBindingStep extends ApplicationWizardStep { get buttons(): WizardButton[] { return [ + { kind: "cancel" }, { kind: "next", label: msg("Save Binding"), destination: "bindings" }, { kind: "back", destination: "bindings" }, - { kind: "cancel" }, ]; } - override handleButton(button: NavigableButton) { + public override handleButton(button: NavigableButton) { if (button.kind === "next") { if (!this.form?.checkValidity()) { return; @@ -69,7 +72,7 @@ export class ApplicationWizardEditBindingStep extends ApplicationWizardStep { const policyObject = this.searchSelect.selectedObject; const policyKey = PolicyObjectKeys[this.policyGroupUser]; const newBinding: PolicyBinding = { - ...(this.formValues as unknown as PolicyBinding), + ...this.formValues, [policyKey]: policyObject, }; 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 06c15761b3..1fd5cc7975 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 @@ -10,7 +10,7 @@ import { bound } from "#elements/decorators/bound"; import { WithLicenseSummary } from "#elements/mixins/license"; import { TypeCreateWizardPageLayouts } from "#elements/wizard/TypeCreateWizardPage"; -import type { NavigableButton, WizardButton } from "#components/ak-wizard/types"; +import type { NavigableButton, WizardButton } from "#components/ak-wizard/shared"; import { ApplicationWizardStep } from "#admin/applications/wizard/ApplicationWizardStep"; @@ -21,6 +21,10 @@ import { msg } from "@lit/localize"; import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; +/** + * + * @prop wizard - The current state of the application wizard, shared across all steps. + */ @customElement("ak-application-wizard-provider-choice-step") export class ApplicationWizardProviderChoiceStep extends WithLicenseSummary(ApplicationWizardStep) { label = msg("Choose a Provider"); @@ -33,13 +37,13 @@ export class ApplicationWizardProviderChoiceStep extends WithLicenseSummary(Appl get buttons(): WizardButton[] { return [ - { kind: "next", destination: "provider" }, - { kind: "back", destination: "application" }, { kind: "cancel" }, + { kind: "back", destination: "application" }, + { kind: "next", destination: "provider" }, ]; } - override handleButton(button: NavigableButton) { + public override handleButton(button: NavigableButton) { this.failureMessage = ""; if (button.kind === "next") { if (!this.wizard.providerModel) { 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 dc84178fdb..64b91dbed6 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 @@ -1,38 +1,46 @@ -import "./providers/ak-application-wizard-provider-for-ldap.js"; -import "./providers/ak-application-wizard-provider-for-oauth.js"; -import "./providers/ak-application-wizard-provider-for-proxy.js"; -import "./providers/ak-application-wizard-provider-for-rac.js"; -import "./providers/ak-application-wizard-provider-for-radius.js"; -import "./providers/ak-application-wizard-provider-for-saml.js"; -import "./providers/ak-application-wizard-provider-for-saml-metadata.js"; -import "./providers/ak-application-wizard-provider-for-scim.js"; +import "#admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-ldap"; +import "#admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-oauth"; +import "#admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-proxy"; +import "#admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-rac"; +import "#admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-radius"; +import "#admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-saml"; +import "#admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-saml-metadata"; +import "#admin/applications/wizard/steps/providers/ak-application-wizard-provider-for-scim"; -import { ApplicationWizardStep } from "../ApplicationWizardStep.js"; -import { OneOfProvider } from "../types.js"; -import { ApplicationWizardProviderForm } from "./providers/ApplicationWizardProviderForm.js"; +import { omitKeys } from "#common/objects"; -import { type NavigableButton, type WizardButton } from "#components/ak-wizard/types"; +import { StrictUnsafe } from "#elements/utils/unsafe"; + +import { type NavigableButton, type WizardButton } from "#components/ak-wizard/shared"; + +import { ApplicationWizardStep } from "#admin/applications/wizard/ApplicationWizardStep"; +import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm"; +import { OneOfProvider } from "#admin/applications/wizard/steps/providers/shared"; import { msg } from "@lit/localize"; import { nothing, PropertyValues } from "lit"; import { customElement, query, state } from "lit/decorators.js"; -import { html, unsafeStatic } from "lit/static-html.js"; -const providerToTag = new Map([ - ["ldapprovider", "ak-application-wizard-provider-for-ldap"], - ["oauth2provider", "ak-application-wizard-provider-for-oauth"], - ["proxyprovider", "ak-application-wizard-provider-for-proxy"], - ["racprovider", "ak-application-wizard-provider-for-rac"], - ["radiusprovider", "ak-application-wizard-provider-for-radius"], - ["samlprovider", "ak-application-wizard-provider-for-saml"], - ["samlproviderimportmodel", "ak-application-wizard-provider-for-saml-metadata"], - ["scimprovider", "ak-application-wizard-provider-for-scim"], -]); +const providerToTag = { + ldapprovider: "ak-application-wizard-provider-for-ldap", + oauth2provider: "ak-application-wizard-provider-for-oauth", + proxyprovider: "ak-application-wizard-provider-for-proxy", + racprovider: "ak-application-wizard-provider-for-rac", + radiusprovider: "ak-application-wizard-provider-for-radius", + samlprovider: "ak-application-wizard-provider-for-saml", + samlproviderimportmodel: "ak-application-wizard-provider-for-saml-metadata", + scimprovider: "ak-application-wizard-provider-for-scim", +} as const satisfies Record; +type ProviderModel = keyof typeof providerToTag; + +/** + * @prop wizard - The current state of the application wizard, shared across all steps. + */ @customElement("ak-application-wizard-provider-step") export class ApplicationWizardProviderStep extends ApplicationWizardStep { @state() - label = msg("Configure Provider"); + public override label = msg("Configure Provider"); @query("#providerform") protected element!: ApplicationWizardProviderForm; @@ -62,7 +70,7 @@ export class ApplicationWizardProviderStep extends ApplicationWizardStep { return this.element.formValues; } - override handleButton(button: NavigableButton) { + public override handleButton(button: NavigableButton) { if (button.kind === "next") { if (!this.valid) { this.handleEnabling({ @@ -75,7 +83,7 @@ export class ApplicationWizardProviderStep extends ApplicationWizardStep { ...this.formValues, mode: this.wizard.proxyMode, }, - errors: this.removeErrors("provider"), + errors: omitKeys(this.wizard.errors, "provider"), }; this.handleUpdate(payload, button.destination, { enable: ["bindings", "submit"], @@ -87,9 +95,9 @@ export class ApplicationWizardProviderStep extends ApplicationWizardStep { get buttons(): WizardButton[] { return [ - { kind: "next", destination: "bindings" }, - { kind: "back", destination: "provider-choice" }, { kind: "cancel" }, + { kind: "back", destination: "provider-choice" }, + { kind: "next", destination: "bindings" }, ]; } @@ -101,19 +109,21 @@ export class ApplicationWizardProviderStep extends ApplicationWizardStep { // This is, I'm afraid, some rather esoteric bit of Lit-ing, and it makes ESLint // sad. It does allow us to get away with specifying very little about the // provider here. - const tag = providerToTag.get(this.wizard.providerModel); - return tag - ? // eslint-disable-next-line lit/binding-positions,lit/no-invalid-html - html`<${unsafeStatic(tag)} - id="providerform" - .wizard=${this.wizard} - .errors=${this.wizard.errors?.provider ?? {}} + const tag = providerToTag[this.wizard.providerModel as ProviderModel]; - >` - : nothing; + if (!tag) { + this.logger.warn( + `No provider form found for provider model ${this.wizard.providerModel}`, + ); + + return nothing; + } + + return StrictUnsafe>(tag, { + wizard: this.wizard, + id: "providerform", + errors: this.wizard.errors?.provider ?? {}, + }); } updated(changed: PropertyValues) { 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 b7548db7f8..89b0cfe528 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,18 +1,22 @@ import "#admin/applications/wizard/ak-wizard-title"; -import { ApplicationWizardStep } from "../ApplicationWizardStep.js"; -import { isApplicationTransactionValidationError, OneOfProvider } from "../types.js"; -import { providerRenderers } from "./SubmitStepOverviewRenderers.js"; - import { DEFAULT_CONFIG } from "#common/api/config"; import { EVENT_REFRESH } from "#common/constants"; import { parseAPIResponseError } from "#common/errors/network"; import { showAPIErrorMessage } from "#elements/messages/MessageContainer"; +import { SlottedTemplateResult } from "#elements/types"; import { CustomEmitterElement } from "#elements/utils/eventEmitter"; import { WizardNavigationEvent } from "#components/ak-wizard/events"; -import { type WizardButton } from "#components/ak-wizard/types"; +import { type WizardButton } from "#components/ak-wizard/shared"; + +import { ApplicationWizardStep } from "#admin/applications/wizard/ApplicationWizardStep"; +import { + isApplicationTransactionValidationError, + OneOfProvider, +} from "#admin/applications/wizard/steps/providers/shared"; +import { providerRenderers } from "#admin/applications/wizard/steps/SubmitStepOverviewRenderers"; import { type ApplicationRequest, @@ -26,7 +30,6 @@ import { type ProvidersSamlImportMetadataCreateRequest, ProxyMode, type ProxyProviderRequest, - type SAMLProvider, type TransactionApplicationRequest, type TransactionApplicationResponse, type TransactionPolicyBindingRequest, @@ -35,11 +38,10 @@ import { import { match, P } from "ts-pattern"; import { msg } from "@lit/localize"; -import { css, html, nothing, TemplateResult } from "lit"; +import { css, html, nothing } from "lit"; import { customElement, state } from "lit/decorators.js"; import { classMap } from "lit/directives/class-map.js"; -// import { map } from "lit/directives/map.js"; import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css"; import PFProgressStepper from "@patternfly/patternfly/components/ProgressStepper/progress-stepper.css"; @@ -51,19 +53,22 @@ type SubmitStates = (typeof _submitStates)[number]; type StrictProviderModelEnum = Exclude; -const providerMap: Map = Object.values(ProviderModelEnum) - .filter((value) => /^authentik_providers_/.test(value) && /provider$/.test(value)) - .reduce((acc: Map, value) => { - acc.set(value.split(".")[1], value); +const providerMap: Map = Object.values(ProviderModelEnum) + .filter((value): value is StrictProviderModelEnum => { + return /^authentik_providers_/.test(value) && /provider$/.test(value); + }) + .reduce((acc: Map, value) => { + const key = value.split(".")[1]; + acc.set(key, value); + return acc; }, new Map()); type NonEmptyArray = [T, ...T[]]; -type MaybeTemplateResult = TemplateResult | typeof nothing; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const isNotEmpty = (arr: any): arr is NonEmptyArray => Array.isArray(arr) && arr.length > 0; +function isNotEmpty(arr: T[] | undefined): arr is NonEmptyArray { + return Array.isArray(arr) && arr.length > 0; +} const cleanApplication = (app: Partial): ApplicationRequest => ({ name: "", @@ -99,10 +104,10 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio `, ]; - label = msg("Review and Submit Application"); + public override label = msg("Review and Submit Application"); @state() - state: SubmitStates = "reviewing"; + protected state: SubmitStates = "reviewing"; async sendSAMLMetadataImport() { const providerData = this.wizard.provider as ProvidersSamlImportMetadataCreateRequest; @@ -112,12 +117,12 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio try { // Step 1: Import SAML metadata to create the provider - const createdProvider = (await providersApi.providersSamlImportMetadataCreate({ + const createdProvider = await providersApi.providersSamlImportMetadataCreate({ file: providerData.file, name: providerData.name, authorizationFlow: providerData.authorizationFlow || "", invalidationFlow: providerData.invalidationFlow || "", - })) as unknown as SAMLProvider; + }); // Step 2: Create the application linked to the provider const appData = cleanApplication(this.wizard.app); @@ -158,12 +163,12 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio const app = this.wizard.app; const provider = this.wizard.provider as ModelRequest; - if (app === undefined) { - throw new Error("Reached the submit state with the app undefined"); + if (!app) { + throw new Error("Reached the submit state without the application initialized"); } - if (provider === undefined) { - throw new Error("Reached the submit state with the provider undefined"); + if (!provider) { + throw new Error("Reached the submit state without the provider initialized"); } this.state = "running"; @@ -176,14 +181,21 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio // Stringly-based API. Not the best, but it works. Just be aware that it is // stringly-based. - const providerModel = providerMap.get(this.wizard.providerModel) as StrictProviderModelEnum; + const providerModel = providerMap.get(this.wizard.providerModel); + + if (!providerModel) { + throw new TypeError("Unrecognized provider model: " + this.wizard.providerModel); + } + provider.providerModel = providerModel; // Special case for the Proxy provider. if (this.wizard.providerModel === "proxyprovider") { - (provider as ProxyProviderRequest).mode = this.wizard.proxyMode; - if ((provider as ProxyProviderRequest).mode !== ProxyMode.ForwardDomain) { - (provider as ProxyProviderRequest).cookieDomain = ""; + const proxyProviderRequest = provider as ProxyProviderRequest; + proxyProviderRequest.mode = this.wizard.proxyMode; + + if (proxyProviderRequest.mode !== ProxyMode.ForwardDomain) { + proxyProviderRequest.cookieDomain = ""; } } @@ -236,7 +248,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio }); } - override handleButton(button: WizardButton) { + public override handleButton(button: WizardButton) { match([button.kind, this.state]) .with([P.union("back", "cancel"), P._], () => { super.handleButton(button); @@ -259,9 +271,9 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio get buttons(): WizardButton[] { const forReview: WizardButton[] = [ - { kind: "next", label: msg("Submit"), destination: "here" }, - { kind: "back", destination: "bindings" }, { kind: "cancel" }, + { kind: "back", destination: "bindings" }, + { kind: "next", label: msg("Create Application"), destination: "here" }, ]; const forSubmit: WizardButton[] = [{ kind: "close" }]; @@ -277,7 +289,7 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio state: string, label: string, icons: string[], - extraInfo: MaybeTemplateResult = nothing, + extraInfo: SlottedTemplateResult = nothing, ) { const icon = classMap(icons.reduce((acc, icon) => ({ ...acc, [icon]: true }), {})); @@ -385,14 +397,14 @@ export class ApplicationWizardSubmitStep extends CustomEmitterElement(Applicatio
${metaLaunchUrl - ? html`
+ ? html`
${msg("Launch URL")}
${metaLaunchUrl}
` : nothing} ${renderer - ? html`

${msg("Provider")}

+ ? html`

${msg("Provider")}

${renderer(provider)}` : nothing}
diff --git a/web/src/admin/applications/wizard/steps/bindings/ak-application-wizard-bindings-toolbar.ts b/web/src/admin/applications/wizard/steps/bindings/ak-application-wizard-bindings-toolbar.ts index 47a078ae84..ea87a9c17d 100644 --- a/web/src/admin/applications/wizard/steps/bindings/ak-application-wizard-bindings-toolbar.ts +++ b/web/src/admin/applications/wizard/steps/bindings/ak-application-wizard-bindings-toolbar.ts @@ -17,7 +17,7 @@ export class ApplicationWizardBindingsToolbar extends AKElement { static styles = [PFButton, PFToolbar]; @property({ type: Boolean, attribute: "can-delete", reflect: true }) - canDelete = false; + public canDelete = false; notify(eventName: string) { this.dispatchEvent(new Event(eventName, { bubbles: true, composed: true })); diff --git a/web/src/admin/applications/wizard/steps/providers/ApplicationWizardProviderForm.ts b/web/src/admin/applications/wizard/steps/providers/ApplicationWizardProviderForm.ts index eb154bad1a..cacf3b3f1d 100644 --- a/web/src/admin/applications/wizard/steps/providers/ApplicationWizardProviderForm.ts +++ b/web/src/admin/applications/wizard/steps/providers/ApplicationWizardProviderForm.ts @@ -5,27 +5,35 @@ import "#components/ak-text-input"; import "#elements/forms/FormGroup"; import "#elements/forms/HorizontalFormElement"; -import { styles as ApplicationWizardStyles } from "../../ApplicationWizardFormStepStyles.styles.js"; -import { type ApplicationWizardState, type OneOfProvider } from "../../types.js"; - import { AKElement } from "#elements/Base"; -import { serializeForm } from "#elements/forms/Form"; +import { serializeForm } from "#elements/forms/serialization"; + +import { ApplicationWizardStyles } from "#admin/applications/wizard/ApplicationWizardFormStepStyles.styles"; +import { + ApplicationTransactionValidationError, + type ApplicationWizardState, + ApplicationWizardStateError, + type OneOfProvider, +} from "#admin/applications/wizard/steps/providers/shared"; import { snakeCase } from "change-case"; import { CSSResult } from "lit"; import { property, query } from "lit/decorators.js"; -export abstract class ApplicationWizardProviderForm extends AKElement { +export abstract class ApplicationWizardProviderForm< + P extends OneOfProvider, + E extends ApplicationWizardStateError = ApplicationTransactionValidationError, +> extends AKElement { static styles: CSSResult[] = [...ApplicationWizardStyles]; - label = ""; + public abstract label: string; @property({ type: Object, attribute: false }) - wizard!: ApplicationWizardState; + public wizard!: ApplicationWizardState; @property({ type: Object, attribute: false }) - errors: Record = {}; + public errors: E = {} as E; @query("form#providerform") public form!: HTMLFormElement | null; @@ -42,23 +50,20 @@ export abstract class ApplicationWizardProviderForm ext } get valid() { - this.errors = {}; + this.errors = {} as E; return !!this.form?.checkValidity(); } - errorMessages(name: string) { - return name in this.errors - ? [this.errors[name]] - : (this.wizard.errors?.provider?.[name] ?? - this.wizard.errors?.provider?.[snakeCase(name)] ?? - []); - } + errorMessages>(name: T): Array { + if (name in this.errors) { + return [this.errors[name]]; + } - isValid(name: keyof T) { - return !( - (this.wizard.errors?.provider?.[name as string] ?? []).length > 0 || - this.errors?.[name] !== undefined + return ( + this.wizard.errors?.provider?.[name] ?? + this.wizard.errors?.provider?.[snakeCase(name) as keyof 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 bbca20e1e7..b0623f509a 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,10 +1,12 @@ import "#admin/applications/wizard/ak-wizard-title"; -import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js"; - import { WithBrandConfig } from "#elements/mixins/branding"; -import { ValidationRecord } from "#admin/applications/wizard/types"; +import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm"; +import { + ApplicationTransactionValidationError, + WizardValidationRecord, +} from "#admin/applications/wizard/steps/providers/shared"; import { renderForm } from "#admin/providers/ldap/LDAPProviderFormForm"; import type { LDAPProvider } from "@goauthentik/api"; @@ -15,11 +17,11 @@ import { customElement } from "lit/decorators.js"; @customElement("ak-application-wizard-provider-for-ldap") export class ApplicationWizardLdapProviderForm extends WithBrandConfig( - ApplicationWizardProviderForm, + ApplicationWizardProviderForm, ) { label = msg("Configure LDAP Provider"); - renderForm(provider: LDAPProvider, errors: ValidationRecord) { + renderForm(provider: LDAPProvider, errors: WizardValidationRecord) { return html` ${this.label} @@ -32,10 +34,7 @@ export class ApplicationWizardLdapProviderForm extends WithBrandConfig( if (!(this.wizard.provider && this.wizard.errors)) { throw new Error("LDAP Provider Step received uninitialized wizard context."); } - return this.renderForm( - this.wizard.provider as LDAPProvider, - this.wizard.errors.provider ?? {}, - ); + return this.renderForm(this.wizard.provider, this.wizard.errors.provider ?? {}); } } 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 58914d4fee..08b7e5509d 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,25 +1,19 @@ import "#admin/applications/wizard/ak-wizard-title"; -import { ApplicationTransactionValidationError } from "../../types.js"; -import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js"; - import { DEFAULT_CONFIG } from "#common/api/config"; +import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm"; +import { ApplicationTransactionValidationError } from "#admin/applications/wizard/steps/providers/shared"; import { renderForm } from "#admin/providers/oauth2/OAuth2ProviderFormForm"; -import { - type OAuth2Provider, - OAuth2ProviderRequest, - type PaginatedOAuthSourceList, - SourcesApi, -} from "@goauthentik/api"; +import { type OAuth2Provider, type PaginatedOAuthSourceList, SourcesApi } from "@goauthentik/api"; import { msg } from "@lit/localize"; import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; @customElement("ak-application-wizard-provider-for-oauth") -export class ApplicationWizardOauth2ProviderForm extends ApplicationWizardProviderForm { +export class ApplicationWizardOauth2ProviderForm extends ApplicationWizardProviderForm { label = msg("Configure OAuth2 Provider"); @state() @@ -67,7 +61,7 @@ export class ApplicationWizardOauth2ProviderForm extends ApplicationWizardProvid if (!(this.wizard.provider && this.wizard.errors)) { throw new Error("Oauth2 Provider Step received uninitialized wizard context."); } - return this.renderForm(this.wizard.provider as OAuth2Provider, this.wizard.errors); + return this.renderForm(this.wizard.provider, this.wizard.errors); } } 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 625dede6d6..c5c35c0713 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,10 +1,9 @@ import "#admin/applications/wizard/ak-wizard-title"; -import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js"; - import { WizardUpdateEvent } from "#components/ak-wizard/events"; -import { ValidationRecord } from "#admin/applications/wizard/types"; +import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm"; +import { WizardValidationRecord } from "#admin/applications/wizard/steps/providers/shared"; import { ProxyModeValue, renderForm, @@ -25,7 +24,7 @@ export class ApplicationWizardProxyProviderForm extends ApplicationWizardProvide @state() showHttpBasic = true; - renderForm(provider: ProxyProvider, errors: ValidationRecord) { + protected renderForm(provider: ProxyProvider, errors: WizardValidationRecord = {}) { const onSetMode: SetMode = (ev: CustomEvent) => { this.dispatchEvent( new WizardUpdateEvent({ ...this.wizard, proxyMode: ev.detail.value }), @@ -59,10 +58,7 @@ export class ApplicationWizardProxyProviderForm extends ApplicationWizardProvide if (!(this.wizard.provider && this.wizard.errors)) { throw new Error("Proxy Provider Step received uninitialized wizard context."); } - return this.renderForm( - this.wizard.provider as ProxyProvider, - this.wizard.errors?.provider ?? {}, - ); + return this.renderForm(this.wizard.provider, this.wizard.errors?.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 46a149e5c7..21db048c98 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 @@ -5,8 +5,7 @@ import "#components/ak-text-input"; import "#elements/CodeMirror"; import "#elements/ak-dual-select/ak-dual-select-dynamic-selected-provider"; -import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js"; - +import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm"; import { propertyMappingsProvider, propertyMappingsSelector, @@ -84,7 +83,7 @@ export class ApplicationWizardRACProviderForm extends ApplicationWizardProviderF if (!(this.wizard.provider && this.wizard.errors)) { throw new Error("RAC Provider Step received uninitialized wizard context."); } - return this.renderForm(this.wizard.provider as RACProvider); + return this.renderForm(this.wizard.provider); } } 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 f6f0164b70..191f89ad10 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,10 +1,9 @@ import "#admin/applications/wizard/ak-wizard-title"; -import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js"; - import { WithBrandConfig } from "#elements/mixins/branding"; -import { ValidationRecord } from "#admin/applications/wizard/types"; +import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm"; +import { WizardValidationRecord } from "#admin/applications/wizard/steps/providers/shared"; import { renderForm } from "#admin/providers/radius/RadiusProviderFormForm"; import { RadiusProvider } from "@goauthentik/api"; @@ -19,7 +18,7 @@ export class ApplicationWizardRadiusProviderForm extends WithBrandConfig( ) { label = msg("Configure Radius Provider"); - renderForm(provider: RadiusProvider, errors: ValidationRecord) { + renderForm(provider: RadiusProvider, errors: WizardValidationRecord = {}) { return html` ${this.label} ${renderForm({ provider, errors, brand: this.brand })} @@ -30,10 +29,7 @@ export class ApplicationWizardRadiusProviderForm extends WithBrandConfig( if (!(this.wizard.provider && this.wizard.errors)) { throw new Error("RAC Provider Step received uninitialized wizard context."); } - return this.renderForm( - this.wizard.provider as RadiusProvider, - this.wizard.errors?.provider ?? {}, - ); + return this.renderForm(this.wizard.provider, this.wizard.errors?.provider); } } 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 db031b5503..2072891410 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,9 +1,8 @@ import "#admin/applications/wizard/ak-wizard-title"; -import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js"; - import { createFileMap } from "#elements/utils/inputs"; +import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm"; import { renderForm } from "#admin/providers/saml/SAMLProviderImportFormForm"; import type { ProvidersSamlImportMetadataCreateRequest } from "@goauthentik/api"; 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 474f4348af..bb0d2c757f 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,8 +1,7 @@ import "#admin/applications/wizard/ak-wizard-title"; import "#elements/forms/FormGroup"; -import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js"; - +import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm"; import { type AkCryptoCertificateSearch } from "#admin/common/ak-crypto-certificate-search"; import { renderForm } from "#admin/providers/saml/SAMLProviderFormForm"; @@ -84,7 +83,7 @@ export class ApplicationWizardProviderSamlForm extends ApplicationWizardProvider return html` ${this.label} ${renderForm({ - provider: this.wizard.provider as SAMLProvider, + provider: this.wizard.provider, errors: this.wizard.errors?.provider, setHasSigningKp, hasSigningKp: this.hasSigningKp, 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 04e99d49b8..d0ef6f219b 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,8 +1,7 @@ import "#admin/applications/wizard/ak-wizard-title"; import "#elements/forms/FormGroup"; -import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js"; - +import { ApplicationWizardProviderForm } from "#admin/applications/wizard/steps/providers/ApplicationWizardProviderForm"; import { renderForm } from "#admin/providers/scim/SCIMProviderFormForm"; import { PaginatedSCIMMappingList, type SCIMProvider } from "@goauthentik/api"; @@ -23,7 +22,7 @@ export class ApplicationWizardSCIMProvider extends ApplicationWizardProviderForm ${renderForm({ update: this.requestUpdate.bind(this), - provider: this.wizard.provider as SCIMProvider, + provider: this.wizard.provider, errors: this.wizard.errors.provider, })} `; diff --git a/web/src/admin/applications/wizard/types.ts b/web/src/admin/applications/wizard/steps/providers/shared.ts similarity index 67% rename from web/src/admin/applications/wizard/types.ts rename to web/src/admin/applications/wizard/steps/providers/shared.ts index 5e20ffe612..100a4c955a 100644 --- a/web/src/admin/applications/wizard/types.ts +++ b/web/src/admin/applications/wizard/steps/providers/shared.ts @@ -13,27 +13,34 @@ import { type ValidationError, } from "@goauthentik/api"; -export type OneOfProvider = - | Partial - | Partial - | Partial - | Partial - | Partial - | Partial - | Partial - | Partial; +export type OneOfProvider = Partial< + | SCIMProviderRequest + | SAMLProviderRequest + | ProvidersSamlImportMetadataCreateRequest + | RACProviderRequest + | RadiusProviderRequest + | ProxyProviderRequest + | OAuth2ProviderRequest + | LDAPProviderRequest +>; -export type ValidationRecord = { [key: string]: string[] }; +export type WizardValidationRecord = { + [key in K]: string[] | undefined; +}; /** * An error that occurs during the creation or modification of an application. * * @todo (Elf) Extend this type to include all possible errors that can occur during the creation or modification of an application. */ -export interface ApplicationTransactionValidationError extends ValidationError { - app?: ValidationRecord; - provider?: ValidationRecord; - bindings?: ValidationRecord; +export interface ApplicationTransactionValidationError extends Pick< + ValidationError, + "code" | "nonFieldErrors" +> { + app?: WizardValidationRecord; + name?: WizardValidationRecord; + provider?: WizardValidationRecord; + bindings?: WizardValidationRecord; detail?: unknown; } @@ -50,20 +57,25 @@ export function isApplicationTransactionValidationError( return false; } +export type ApplicationWizardStateError = ValidationError | ApplicationTransactionValidationError; + // We use the PolicyBinding instead of the PolicyBindingRequest here, because that gives us a slot // in which to preserve the retrieved policy, group, or user object from the SearchSelect used to // find it, which in turn allows us to create a user-friendly display of bindings on the "List of // configured bindings" page in the wizard. The PolicyBinding is converted into a // PolicyBindingRequest during the submission phase. -export interface ApplicationWizardState { +export interface ApplicationWizardState< + P extends OneOfProvider = OneOfProvider, + E = ApplicationTransactionValidationError, +> { app: Partial; providerModel: string; - provider: OneOfProvider; + provider: P; proxyMode: ProxyMode; bindings: PolicyBinding[]; currentBinding: number; - errors: ValidationError | ApplicationTransactionValidationError; + errors: E; } export interface ApplicationWizardStateUpdate { diff --git a/web/src/admin/blueprints/BlueprintListPage.ts b/web/src/admin/blueprints/BlueprintListPage.ts index a47318baee..818a99447f 100644 --- a/web/src/admin/blueprints/BlueprintListPage.ts +++ b/web/src/admin/blueprints/BlueprintListPage.ts @@ -156,7 +156,7 @@ export class BlueprintListPage extends TablePage { html``, html`
- ${msg("Update")} + ${msg("Save Changes")} ${msg("Update Blueprint")} { @@ -107,16 +112,10 @@ export class ConfigModal extends ModalButton { > ${msg("Copy")} -   - - `; + + ${msg("Download")} + + `; } } diff --git a/web/src/admin/endpoints/connectors/agent/EnrollmentTokenListPage.ts b/web/src/admin/endpoints/connectors/agent/EnrollmentTokenListPage.ts index c28c40d5c8..6153d852dc 100644 --- a/web/src/admin/endpoints/connectors/agent/EnrollmentTokenListPage.ts +++ b/web/src/admin/endpoints/connectors/agent/EnrollmentTokenListPage.ts @@ -87,7 +87,7 @@ export class EnrollmentTokenListPage extends Table { Timestamp(item.expires && item.expiring ? item.expires : null), html`
- ${msg("Update")} + ${msg("Save Changes")} ${msg("Update Enrollment Token")} `}
-
+
`; + `; } } diff --git a/web/src/admin/endpoints/devices/DeviceListPage.ts b/web/src/admin/endpoints/devices/DeviceListPage.ts index f7becaf59f..d821755f28 100644 --- a/web/src/admin/endpoints/devices/DeviceListPage.ts +++ b/web/src/admin/endpoints/devices/DeviceListPage.ts @@ -132,7 +132,7 @@ export class DeviceListPage extends TablePage { html`${item.accessGroupObj?.name || "-"}`, item.facts.created ? Timestamp(item.facts.created) : html`-`, html` - ${msg("Update")} + ${msg("Save Changes")} ${msg("Update Device")} diff --git a/web/src/admin/endpoints/devices/DeviceUserBindingForm.ts b/web/src/admin/endpoints/devices/DeviceUserBindingForm.ts index eda0ae9aa5..413a928802 100644 --- a/web/src/admin/endpoints/devices/DeviceUserBindingForm.ts +++ b/web/src/admin/endpoints/devices/DeviceUserBindingForm.ts @@ -1,9 +1,9 @@ import "#components/ak-switch-input"; import { DEFAULT_CONFIG } from "#common/api/config"; +import { PolicyBindingCheckTarget } from "#common/policies/utils"; import { PolicyBindingForm } from "#admin/policies/PolicyBindingForm"; -import { PolicyBindingCheckTarget } from "#admin/policies/utils"; import { DeviceUserBinding, EndpointsApi, PolicyBinding } from "@goauthentik/api"; diff --git a/web/src/admin/endpoints/devices/DeviceViewPage.ts b/web/src/admin/endpoints/devices/DeviceViewPage.ts index 697e7bb9f6..2b3c1aae64 100644 --- a/web/src/admin/endpoints/devices/DeviceViewPage.ts +++ b/web/src/admin/endpoints/devices/DeviceViewPage.ts @@ -120,7 +120,7 @@ export class DeviceViewPage extends AKElement { [ msg("Actions"), html` - ${msg("Update")} + ${msg("Save Changes")} ${msg("Update Device")} { html` ${item.expiry?.toLocaleString()} `, html`
- ${msg("Update")} + ${msg("Save Changes")} ${msg("Update License")} diff --git a/web/src/components/events/ObjectChangelog.ts b/web/src/admin/events/ObjectChangelog.ts similarity index 100% rename from web/src/components/events/ObjectChangelog.ts rename to web/src/admin/events/ObjectChangelog.ts diff --git a/web/src/admin/events/RuleListPage.ts b/web/src/admin/events/RuleListPage.ts index efee4b211e..1d100b5971 100644 --- a/web/src/admin/events/RuleListPage.ts +++ b/web/src/admin/events/RuleListPage.ts @@ -89,7 +89,7 @@ export class RuleListPage extends TablePage { : msg("-")}`, html`
- ${msg("Update")} + ${msg("Save Changes")} ${msg("Update Notification Rule")} - ${msg("Update")} + ${msg("Save Changes")} ${msg("Update Stage binding")} diff --git a/web/src/admin/flows/FlowListPage.ts b/web/src/admin/flows/FlowListPage.ts index 7c565dcc11..e524da7907 100644 --- a/web/src/admin/flows/FlowListPage.ts +++ b/web/src/admin/flows/FlowListPage.ts @@ -25,6 +25,8 @@ import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; @customElement("ak-flow-list") export class FlowListPage extends TablePage { + static styles = [...super.styles, PFBanner]; + protected override searchEnabled = true; public pageTitle = msg("Flows"); public pageDescription = msg( @@ -38,8 +40,6 @@ export class FlowListPage extends TablePage { @property() order = "slug"; - static styles = [...super.styles, PFBanner]; - async apiEndpoint(): Promise> { return new FlowsApi(DEFAULT_CONFIG).flowsInstancesList(await this.defaultEndpointConfig()); } @@ -86,12 +86,12 @@ export class FlowListPage extends TablePage { ${item.slug} ${item.title}`, - html`${item.name}`, - html`${Array.from(item.stages || []).length}`, - html`${Array.from(item.policies || []).length}`, + item.name, + Array.from(item.stages || []).length, + Array.from(item.policies || []).length, html`
- ${msg("Update")} + ${msg("Save Changes")} ${msg("Update Flow")} + ${msg("Import")} @@ -161,7 +161,7 @@ export class FlowListPage extends TablePage { { return new FlowsApi(DEFAULT_CONFIG).flowsInstancesCacheClearCreate(); }} diff --git a/web/src/admin/flows/FlowViewPage.ts b/web/src/admin/flows/FlowViewPage.ts index 508aa671b5..9ab3670a62 100644 --- a/web/src/admin/flows/FlowViewPage.ts +++ b/web/src/admin/flows/FlowViewPage.ts @@ -3,7 +3,7 @@ import "#admin/flows/FlowDiagram"; import "#admin/flows/FlowForm"; import "#admin/policies/BoundPoliciesList"; import "#admin/rbac/ObjectPermissionsPage"; -import "#components/events/ObjectChangelog"; +import "#admin/events/ObjectChangelog"; import "#elements/Tabs"; import "#elements/buttons/SpinnerButton/ak-spinner-button"; @@ -127,7 +127,9 @@ export class FlowViewPage extends AKElement {
- ${msg("Update")} + ${msg("Save Changes")} ${msg("Update Flow")} diff --git a/web/src/admin/groups/GroupForm.ts b/web/src/admin/groups/GroupForm.ts index 17d73f40e0..9529a838ef 100644 --- a/web/src/admin/groups/GroupForm.ts +++ b/web/src/admin/groups/GroupForm.ts @@ -1,4 +1,4 @@ -import "#admin/groups/MemberSelectModal"; +import "#admin/groups/MemberSelectForm"; import "#elements/CodeMirror"; import "#elements/ak-dual-select/ak-dual-select-provider"; import "#elements/chips/Chip"; @@ -43,6 +43,9 @@ export class GroupForm extends ModelForm { `, ]; + public entitySingular = msg("Group"); + public entityPlural = msg("Groups"); + #fetchGroups = (page: number, search?: string): Promise => { return new CoreApi(DEFAULT_CONFIG) .coreGroupsList({ diff --git a/web/src/admin/groups/GroupListPage.ts b/web/src/admin/groups/GroupListPage.ts index ff6f4fd1f2..e086c579e0 100644 --- a/web/src/admin/groups/GroupListPage.ts +++ b/web/src/admin/groups/GroupListPage.ts @@ -11,6 +11,8 @@ import { PaginatedResponse, TableColumn } from "#elements/table/Table"; import { TablePage } from "#elements/table/TablePage"; import { SlottedTemplateResult } from "#elements/types"; +import { GroupForm } from "#admin/groups/GroupForm"; + import { CoreApi, Group } from "@goauthentik/api"; import { msg, str } from "@lit/localize"; @@ -19,9 +21,11 @@ import { customElement, property } from "lit/decorators.js"; @customElement("ak-group-list") export class GroupListPage extends TablePage { - checkbox = true; - clearOnRefresh = true; protected override searchEnabled = true; + + public override checkbox = true; + public override clearOnRefresh = true; + public searchPlaceholder = msg("Search for a group by name…"); public searchLabel = msg("Group Search"); public pageTitle = msg("Groups"); @@ -32,9 +36,9 @@ export class GroupListPage extends TablePage { public supportsQL = true; @property() - order = "name"; + public order = "name"; - async apiEndpoint(): Promise> { + protected async apiEndpoint(): Promise> { return new CoreApi(DEFAULT_CONFIG).coreGroupsList({ ...(await this.defaultEndpointConfig()), includeUsers: false, @@ -48,7 +52,7 @@ export class GroupListPage extends TablePage { [msg("Actions"), null, msg("Row Actions")], ]; - renderToolbarSelected(): TemplateResult { + protected renderToolbarSelected(): TemplateResult { const disabled = this.selectedElements.length < 1; return html` { `; } - row(item: Group): SlottedTemplateResult[] { + protected row(item: Group): SlottedTemplateResult[] { return [ html` { html`${Array.from(item.users || []).length}`, html``, html`
- - ${msg("Update")} - ${msg("Update Group")} - - - +
`, ]; } - renderObjectCreate(): TemplateResult { - return html` - - ${msg("Create Group")} - ${msg("New Group")} - - - - `; + protected renderObjectCreate(): TemplateResult { + return html``; } } diff --git a/web/src/admin/groups/GroupViewPage.ts b/web/src/admin/groups/GroupViewPage.ts index 3bfa30b401..0e21250351 100644 --- a/web/src/admin/groups/GroupViewPage.ts +++ b/web/src/admin/groups/GroupViewPage.ts @@ -5,7 +5,7 @@ import "#admin/roles/RelatedRoleList"; import "#components/ak-object-attributes-card"; import "#admin/lifecycle/ObjectLifecyclePage"; import "#components/ak-status-label"; -import "#components/events/ObjectChangelog"; +import "#admin/events/ObjectChangelog"; import "#elements/CodeMirror"; import "#elements/Tabs"; import "#elements/buttons/ActionButton/index"; @@ -181,7 +181,7 @@ export class GroupViewPage extends WithLicenseSummary(AKElement) {