diff --git a/web/src/admin/AdminInterface/AdminSidebar.ts b/web/src/admin/AdminInterface/AdminSidebar.ts index b717954fb..9cc8148ef 100644 --- a/web/src/admin/AdminInterface/AdminSidebar.ts +++ b/web/src/admin/AdminInterface/AdminSidebar.ts @@ -1,5 +1,6 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { EVENT_SIDEBAR_TOGGLE, VERSION } from "@goauthentik/common/constants"; +import { eventActionLabels } from "@goauthentik/common/labels"; import { me } from "@goauthentik/common/users"; import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts"; import { AKElement } from "@goauthentik/elements/Base"; @@ -16,35 +17,41 @@ import { consume } from "@lit-labs/context"; import { msg, str } from "@lit/localize"; import { html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; -import { eventActionLabels } from "@goauthentik/common/labels"; -import { ProvidersApi, TypeCreate } from "@goauthentik/api"; -import { - AdminApi, - CapabilitiesEnum, - CoreApi, - OutpostsApi, - PoliciesApi, - PropertymappingsApi, - SourcesApi, - StagesApi, - Version, -} from "@goauthentik/api"; +import { AdminApi, CapabilitiesEnum, CoreApi, Version } from "@goauthentik/api"; import type { Config, SessionUser, UserSelf } from "@goauthentik/api"; import { flowDesignationTable } from "../flows/utils"; +import ConnectionTypesController from "./SidebarEntries/ConnectionTypesController"; +import PolicyTypesController from "./SidebarEntries/PolicyTypesController"; +import PropertyMappingsController from "./SidebarEntries/PropertyMappingsController"; +import ProviderTypesController from "./SidebarEntries/ProviderTypesController"; +import SourceTypesController from "./SidebarEntries/SourceTypesController"; +import StageTypesController from "./SidebarEntries/StageTypesController"; /** * AdminSidebar * - * Encapsulates the logic for the administration sidebar: what to show and, initially, when to show - * it. Rendering decisions are left to the sidebar itself. - */ + * The AdminSidebar has two responsibilities: + * + * 1. Control the styling of the sidebar host, specifically when to show it and whether to show + * it as an overlay or as a push. + * 2. Control what content the sidebar will receive. The sidebar takes a tree, maximally three deep, + * of type SidebarEventHandler. + */ + +type SidebarUrl = string; export type LocalSidebarEntry = [ - string | SidebarEventHandler | null, + // - null: This entry is not a link. + // - string: the url for the entry + // - SidebarEventHandler: a function to run if the entry is clicked. + SidebarUrl | SidebarEventHandler | null, + // The visible text of the entry. string, + // Attributes to which the sidebar responds. See the sidebar for details. (SidebarAttributes | string[] | null)?, // eslint-disable-line + // Children of the entry LocalSidebarEntry[]?, ]; @@ -55,12 +62,6 @@ const localToSidebarEntry = (l: LocalSidebarEntry): SidebarEntry => ({ ...(l[3] ? { children: l[3].map(localToSidebarEntry) } : {}), }); -const typeCreateToSidebar = (baseUrl: string, tcreate: TypeCreate[]): LocalSidebarEntry[] => - tcreate.map((t) => [ - `${baseUrl};${encodeURIComponent(JSON.stringify({ search: t.name }))}`, - t.name, - ]); - @customElement("ak-admin-sidebar") export class AkAdminSidebar extends AKElement { @consume({ context: authentikConfigContext }) @@ -75,23 +76,12 @@ export class AkAdminSidebar extends AKElement { @state() impersonation: UserSelf["username"] | null = null; - @state() - providerTypes: TypeCreate[] = []; - - @state() - stageTypes: TypeCreate[] = []; - - @state() - mappingTypes: TypeCreate[] = []; - - @state() - sourceTypes: TypeCreate[] = []; - - @state() - policyTypes: TypeCreate[] = []; - - @state() - connectionTypes: TypeCreate[] = []; + private connectionTypes = new ConnectionTypesController(this); + private policyTypes = new PolicyTypesController(this); + private propertyMapper = new PropertyMappingsController(this); + private providerTypes = new ProviderTypesController(this); + private sourceTypes = new SourceTypesController(this); + private stageTypes = new StageTypesController(this); constructor() { super(); @@ -101,25 +91,6 @@ export class AkAdminSidebar extends AKElement { me().then((user: SessionUser) => { this.impersonation = user.original ? user.user.username : null; }); - new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((types) => { - this.providerTypes = types; - }); - new StagesApi(DEFAULT_CONFIG).stagesAllTypesList().then((types) => { - this.stageTypes = types; - }); - new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsAllTypesList().then((types) => { - this.mappingTypes = types; - }); - new SourcesApi(DEFAULT_CONFIG).sourcesAllTypesList().then((types) => { - this.sourceTypes = types; - }); - new PoliciesApi(DEFAULT_CONFIG).policiesAllTypesList().then((types) => { - this.policyTypes = types; - }); - new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsAllTypesList().then((types) => { - this.connectionTypes = types; - }); - this.toggleOpen = this.toggleOpen.bind(this); this.checkWidth = this.checkWidth.bind(this); } @@ -183,27 +154,21 @@ export class AkAdminSidebar extends AKElement { ? [[reload, msg(str`You're currently impersonating ${this.impersonation}. Click to stop.`)]] : []; - // prettier-ignore - const enterpriseMenu: LocalSidebarEntry[] = this.config?.capabilities.includes(CapabilitiesEnum.IsEnterprise) + const enterpriseMenu: LocalSidebarEntry[] = this.config?.capabilities.includes( + CapabilitiesEnum.IsEnterprise + ) ? [[null, msg("Enterprise"), null, [["/enterprise/licenses", msg("Licenses")]]]] : []; - // prettier-ignore - const flowTypes: LocalSidebarEntry[] = flowDesignationTable.map(([_designation, label]) => - ([`/flow/flows;${encodeURIComponent(JSON.stringify({ search: label }))}`, label])); + const flowTypes: LocalSidebarEntry[] = flowDesignationTable.map(([_designation, label]) => [ + `/flow/flows;${encodeURIComponent(JSON.stringify({ search: label }))}`, + label, + ]); - - const eventTypes: LocalSidebarEntry[] = eventActionLabels.map(([_action, label]) => - ([`/events/log;${encodeURIComponent(JSON.stringify({ search: label }))}`, label])); - - const [mappingTypes, providerTypes, sourceTypes, stageTypes, connectionTypes, policyTypes] = [ - typeCreateToSidebar("/core/property-mappings", this.mappingTypes), - typeCreateToSidebar("/core/providers", this.providerTypes), - typeCreateToSidebar("/core/sources", this.sourceTypes), - typeCreateToSidebar("/flow/stages", this.stageTypes), - typeCreateToSidebar("/outpost/integrations", this.connectionTypes), - typeCreateToSidebar("/policy/policies", this.policyTypes), - ]; + const eventTypes: LocalSidebarEntry[] = eventActionLabels.map(([_action, label]) => [ + `/events/log;${encodeURIComponent(JSON.stringify({ search: label }))}`, + label, + ]); // prettier-ignore const localSidebar: LocalSidebarEntry[] = [ @@ -216,32 +181,32 @@ export class AkAdminSidebar extends AKElement { ["/administration/system-tasks", msg("System Tasks")]]], [null, msg("Applications"), null, [ ["/core/applications", msg("Applications"), [`^/core/applications/(?${SLUG_REGEX})$`]], - ["/core/providers", msg("Providers"), [`^/core/providers/(?${ID_REGEX})$`], providerTypes], + ["/core/providers", msg("Providers"), [`^/core/providers/(?${ID_REGEX})$`], this.providerTypes.entries()], ["/outpost/outposts", msg("Outposts")]]], [null, msg("Events"), null, [ ["/events/log", msg("Logs"), [`^/events/log/(?${UUID_REGEX})$`], eventTypes], ["/events/rules", msg("Notification Rules")], ["/events/transports", msg("Notification Transports")]]], [null, msg("Customisation"), null, [ - ["/policy/policies", msg("Policies"), null, policyTypes], - ["/core/property-mappings", msg("Property Mappings"), null, mappingTypes], + ["/policy/policies", msg("Policies"), null, this.policyTypes.entries()], + ["/core/property-mappings", msg("Property Mappings"), null, this.propertyMapper.entries()], ["/blueprints/instances", msg("Blueprints")], ["/policy/reputation", msg("Reputation scores")]]], [null, msg("Flows and Stages"), null, [ ["/flow/flows", msg("Flows"), [`^/flow/flows/(?${SLUG_REGEX})$`], flowTypes], - ["/flow/stages", msg("Stages"), null, stageTypes], + ["/flow/stages", msg("Stages"), null, this.stageTypes.entries()], ["/flow/stages/prompts", msg("Prompts")]]], [null, msg("Directory"), null, [ ["/identity/users", msg("Users"), [`^/identity/users/(?${ID_REGEX})$`]], ["/identity/groups", msg("Groups"), [`^/identity/groups/(?${UUID_REGEX})$`]], ["/identity/roles", msg("Roles"), [`^/identity/roles/(?${UUID_REGEX})$`]], - ["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?${SLUG_REGEX})$`], sourceTypes], + ["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?${SLUG_REGEX})$`], this.sourceTypes.entries()], ["/core/tokens", msg("Tokens and App passwords")], ["/flow/stages/invitations", msg("Invitations")]]], [null, msg("System"), null, [ ["/core/tenants", msg("Tenants")], ["/crypto/certificates", msg("Certificates")], - ["/outpost/integrations", msg("Outpost Integrations"), null, connectionTypes]]], + ["/outpost/integrations", msg("Outpost Integrations"), null, this.connectionTypes.entries()]]], ...(enterpriseMenu) ]; diff --git a/web/src/admin/AdminInterface/SidebarEntries/ConnectionTypesController.ts b/web/src/admin/AdminInterface/SidebarEntries/ConnectionTypesController.ts new file mode 100644 index 000000000..49c0bd67b --- /dev/null +++ b/web/src/admin/AdminInterface/SidebarEntries/ConnectionTypesController.ts @@ -0,0 +1,12 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; + +import { OutpostsApi } from "@goauthentik/api"; + +import { createTypesController } from "./GenericTypesController"; + +export const ConnectionTypesController = createTypesController( + () => new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsAllTypesList(), + "/outpost/integrations" +); + +export default ConnectionTypesController; diff --git a/web/src/admin/AdminInterface/SidebarEntries/GenericTypesController.ts b/web/src/admin/AdminInterface/SidebarEntries/GenericTypesController.ts new file mode 100644 index 000000000..84b1f8f5c --- /dev/null +++ b/web/src/admin/AdminInterface/SidebarEntries/GenericTypesController.ts @@ -0,0 +1,49 @@ +import { ReactiveControllerHost } from "lit"; +import { TypeCreate } from "@goauthentik/api"; +import { LocalSidebarEntry } from "../AdminSidebar"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Fetcher = () => Promise; + +const typeCreateToSidebar = (baseUrl: string, tcreate: TypeCreate[]): LocalSidebarEntry[] => + tcreate.map((t) => [ + `${baseUrl};${encodeURIComponent(JSON.stringify({ search: t.name }))}`, + t.name, + ]); + +/** + * createTypesController + * + * The Sidebar accesses a number objects of `TypeCreate`, which all have the exact same type, just + * different accessors for generating the lists and different paths to which they respond. This + * function is a template for a (simple) reactive controller that fetches the data for that type on + * construction, then informs the host that the data is available. + */ + +/** + * TODO (2023-11-17): This function is unlikely to survive in this form. It would be nice if it were more + * generic, able to take a converter that can handle more that TypeCreate[] as its inbound argument, + * since we need to refine what's displayed and on what the search is conducted. + * + */ + +export function createTypesController(fetch: Fetcher, path: string, converter = typeCreateToSidebar) { + return class GenericTypesController { + createTypes: TypeCreate[] = []; + host: ReactiveControllerHost; + + constructor(host: ReactiveControllerHost) { + this.host = host; + fetch().then((types) => { + this.createTypes = types; + host.requestUpdate(); + }); + } + + entries(): LocalSidebarEntry[] { + return converter(path, this.createTypes); + } + }; +} + +export default createTypesController; diff --git a/web/src/admin/AdminInterface/SidebarEntries/PolicyTypesController.ts b/web/src/admin/AdminInterface/SidebarEntries/PolicyTypesController.ts new file mode 100644 index 000000000..ad40fe19f --- /dev/null +++ b/web/src/admin/AdminInterface/SidebarEntries/PolicyTypesController.ts @@ -0,0 +1,12 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; + +import { PoliciesApi } from "@goauthentik/api"; + +import { createTypesController } from "./GenericTypesController"; + +export const PolicyTypesController = createTypesController( + () => new PoliciesApi(DEFAULT_CONFIG).policiesAllTypesList(), + "/policy/policies" +); + +export default PolicyTypesController; diff --git a/web/src/admin/AdminInterface/SidebarEntries/PropertyMappingsController.ts b/web/src/admin/AdminInterface/SidebarEntries/PropertyMappingsController.ts new file mode 100644 index 000000000..6f0c47f41 --- /dev/null +++ b/web/src/admin/AdminInterface/SidebarEntries/PropertyMappingsController.ts @@ -0,0 +1,12 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; + +import { PropertymappingsApi } from "@goauthentik/api"; + +import { createTypesController } from "./GenericTypesController"; + +export const PropertyMappingsController = createTypesController( + () => new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsAllTypesList(), + "/core/property-mappings" +); + +export default PropertyMappingsController; diff --git a/web/src/admin/AdminInterface/SidebarEntries/ProviderTypesController.ts b/web/src/admin/AdminInterface/SidebarEntries/ProviderTypesController.ts new file mode 100644 index 000000000..bdb98f8f5 --- /dev/null +++ b/web/src/admin/AdminInterface/SidebarEntries/ProviderTypesController.ts @@ -0,0 +1,12 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; + +import { ProvidersApi } from "@goauthentik/api"; + +import { createTypesController } from "./GenericTypesController"; + +export const ProviderTypesController = createTypesController( + () => new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList(), + "/core/providers" +); + +export default ProviderTypesController; diff --git a/web/src/admin/AdminInterface/SidebarEntries/SourceTypesController.ts b/web/src/admin/AdminInterface/SidebarEntries/SourceTypesController.ts new file mode 100644 index 000000000..04b7cb611 --- /dev/null +++ b/web/src/admin/AdminInterface/SidebarEntries/SourceTypesController.ts @@ -0,0 +1,12 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; + +import { SourcesApi } from "@goauthentik/api"; + +import { createTypesController } from "./GenericTypesController"; + +export const SourceTypesController = createTypesController( + () => new SourcesApi(DEFAULT_CONFIG).sourcesAllTypesList(), + "/core/sources" +); + +export default SourceTypesController; diff --git a/web/src/admin/AdminInterface/SidebarEntries/StageTypesController.ts b/web/src/admin/AdminInterface/SidebarEntries/StageTypesController.ts new file mode 100644 index 000000000..a497eaf3e --- /dev/null +++ b/web/src/admin/AdminInterface/SidebarEntries/StageTypesController.ts @@ -0,0 +1,12 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; + +import { StagesApi } from "@goauthentik/api"; + +import { createTypesController } from "./GenericTypesController"; + +export const StageTypesController = createTypesController( + () => new StagesApi(DEFAULT_CONFIG).stagesAllTypesList(), + "/flow/stages" +); + +export default StageTypesController;