From ff78f2f00a236b170f358319c3655f0ba15e949d Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Thu, 16 Nov 2023 14:59:02 -0800 Subject: [PATCH] web: almost there with the sidebar The actual behavior is more or less what I expected. What's missing is: - Persistence of location (the hover effect fades with a click anywhere else) - Proper testing of the oddities - Full (or any!) responsiveness when moving between third-tier links in the same category Stretch goal: - Remembering the state of the sidebar when transitioning between the user and the admin (this will require using some localstorage, I suspect). I also think that in my rush there's a bit of lost internal coherency. I'd like to figure out what's wiggling around my brain and solve that discomfort. --- web/src/admin/AdminInterface/AdminSidebar.ts | 107 +++++++++++++++---- web/src/admin/flows/utils.ts | 57 +++++----- web/src/common/labels.ts | 8 +- web/src/elements/sidebar/SidebarItems.ts | 17 +++ 4 files changed, 137 insertions(+), 52 deletions(-) diff --git a/web/src/admin/AdminInterface/AdminSidebar.ts b/web/src/admin/AdminInterface/AdminSidebar.ts index 991d5fa66..b717954fb 100644 --- a/web/src/admin/AdminInterface/AdminSidebar.ts +++ b/web/src/admin/AdminInterface/AdminSidebar.ts @@ -5,18 +5,35 @@ import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts" import { AKElement } from "@goauthentik/elements/Base"; import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route"; import "@goauthentik/elements/sidebar/Sidebar"; -import { SidebarAttributes, SidebarEntry, SidebarEventHandler } from "@goauthentik/elements/sidebar/SidebarItems"; +import { + SidebarAttributes, + SidebarEntry, + SidebarEventHandler, +} from "@goauthentik/elements/sidebar/SidebarItems"; import { getRootStyle } from "@goauthentik/elements/utils/getRootStyle"; 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, Version } from "@goauthentik/api"; +import { + AdminApi, + CapabilitiesEnum, + CoreApi, + OutpostsApi, + PoliciesApi, + PropertymappingsApi, + SourcesApi, + StagesApi, + Version, +} from "@goauthentik/api"; import type { Config, SessionUser, UserSelf } from "@goauthentik/api"; +import { flowDesignationTable } from "../flows/utils"; + /** * AdminSidebar * @@ -24,7 +41,7 @@ import type { Config, SessionUser, UserSelf } from "@goauthentik/api"; * it. Rendering decisions are left to the sidebar itself. */ -type LocalSidebarEntry = [ +export type LocalSidebarEntry = [ string | SidebarEventHandler | null, string, (SidebarAttributes | string[] | null)?, // eslint-disable-line @@ -34,12 +51,21 @@ type LocalSidebarEntry = [ const localToSidebarEntry = (l: LocalSidebarEntry): SidebarEntry => ({ path: l[0], label: l[1], - ...(l[2]? { attributes: Array.isArray(l[2]) ? { activeWhen: l[2] } : l[2] } : {}), + ...(l[2] ? { attributes: Array.isArray(l[2]) ? { activeWhen: l[2] } : l[2] } : {}), ...(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 }) + public config!: Config; + @property({ type: Boolean, reflect: true }) open = true; @@ -52,8 +78,20 @@ export class AkAdminSidebar extends AKElement { @state() providerTypes: TypeCreate[] = []; - @consume({ context: authentikConfigContext }) - public config!: Config; + @state() + stageTypes: TypeCreate[] = []; + + @state() + mappingTypes: TypeCreate[] = []; + + @state() + sourceTypes: TypeCreate[] = []; + + @state() + policyTypes: TypeCreate[] = []; + + @state() + connectionTypes: TypeCreate[] = []; constructor() { super(); @@ -66,6 +104,22 @@ export class AkAdminSidebar extends AKElement { 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); } @@ -79,7 +133,9 @@ export class AkAdminSidebar extends AKElement { checkWidth() { // This works just fine, but it assumes that the `--ak-sidebar--minimum-auto-width` is in // REMs. If that changes, this code will have to be adjusted as well. - const minWidth = parseFloat(getRootStyle("--ak-sidebar--minimum-auto-width")) * parseFloat(getRootStyle("font-size")); + const minWidth = + parseFloat(getRootStyle("--ak-sidebar--minimum-auto-width")) * + parseFloat(getRootStyle("font-size")); this.open = window.innerWidth >= minWidth; } @@ -133,8 +189,21 @@ export class AkAdminSidebar extends AKElement { : []; // prettier-ignore - const providerTypes: LocalSidebarEntry[] = this.providerTypes.map((ptype) => - ([`/core/providers;${encodeURIComponent(JSON.stringify({ search: ptype.modelName.replace(/provider$/, "") }))}`, ptype.name])); + 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), + ]; // prettier-ignore const localSidebar: LocalSidebarEntry[] = [ @@ -150,36 +219,38 @@ export class AkAdminSidebar extends AKElement { ["/core/providers", msg("Providers"), [`^/core/providers/(?${ID_REGEX})$`], providerTypes], ["/outpost/outposts", msg("Outposts")]]], [null, msg("Events"), null, [ - ["/events/log", msg("Logs"), [`^/events/log/(?${UUID_REGEX})$`]], + ["/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")], - ["/core/property-mappings", msg("Property Mappings")], + ["/policy/policies", msg("Policies"), null, policyTypes], + ["/core/property-mappings", msg("Property Mappings"), null, mappingTypes], ["/blueprints/instances", msg("Blueprints")], ["/policy/reputation", msg("Reputation scores")]]], [null, msg("Flows and Stages"), null, [ - ["/flow/flows", msg("Flows"), [`^/flow/flows/(?${SLUG_REGEX})$`]], - ["/flow/stages", msg("Stages")], + ["/flow/flows", msg("Flows"), [`^/flow/flows/(?${SLUG_REGEX})$`], flowTypes], + ["/flow/stages", msg("Stages"), null, stageTypes], ["/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})$`]], + ["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?${SLUG_REGEX})$`], sourceTypes], ["/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")]]], + ["/outpost/integrations", msg("Outpost Integrations"), null, connectionTypes]]], ...(enterpriseMenu) ]; - return localSidebar.map(localToSidebarEntry); + return localSidebar.map(localToSidebarEntry); } render() { - return html` `; + return html` + + `; } } diff --git a/web/src/admin/flows/utils.ts b/web/src/admin/flows/utils.ts index 3c5ae8f29..dde724d8c 100644 --- a/web/src/admin/flows/utils.ts +++ b/web/src/admin/flows/utils.ts @@ -6,40 +6,33 @@ export function RenderFlowOption(flow: Flow): string { return `${flow.slug} (${flow.name})`; } +type Pair = [FlowDesignationEnum, string]; + +export const flowDesignationTable: Pair[] = [ + [FlowDesignationEnum.Authentication, msg("Authentication")], + [FlowDesignationEnum.Authorization, msg("Authorization")], + [FlowDesignationEnum.Enrollment, msg("Enrollment")], + [FlowDesignationEnum.Invalidation, msg("Invalidation")], + [FlowDesignationEnum.Recovery, msg("Recovery")], + [FlowDesignationEnum.StageConfiguration, msg("Stage Configuration")], + [FlowDesignationEnum.Unenrollment, msg("Unenrollment")], +] + +// prettier-ignore +const flowDesignations = new Map(flowDesignationTable); + export function DesignationToLabel(designation: FlowDesignationEnum): string { - switch (designation) { - case FlowDesignationEnum.Authentication: - return msg("Authentication"); - case FlowDesignationEnum.Authorization: - return msg("Authorization"); - case FlowDesignationEnum.Enrollment: - return msg("Enrollment"); - case FlowDesignationEnum.Invalidation: - return msg("Invalidation"); - case FlowDesignationEnum.Recovery: - return msg("Recovery"); - case FlowDesignationEnum.StageConfiguration: - return msg("Stage Configuration"); - case FlowDesignationEnum.Unenrollment: - return msg("Unenrollment"); - case FlowDesignationEnum.UnknownDefaultOpenApi: - return msg("Unknown designation"); - } + return flowDesignations.get(designation) ?? msg("Unknown designation"); } +const layoutToLabel = new Map([ + [LayoutEnum.Stacked, msg("Stacked")], + [LayoutEnum.ContentLeft, msg("Content left")], + [LayoutEnum.ContentRight, msg("Content right")], + [LayoutEnum.SidebarLeft, msg("Sidebar left")], + [LayoutEnum.SidebarRight, msg("Sidebar right")], +]); + export function LayoutToLabel(layout: LayoutEnum): string { - switch (layout) { - case LayoutEnum.Stacked: - return msg("Stacked"); - case LayoutEnum.ContentLeft: - return msg("Content left"); - case LayoutEnum.ContentRight: - return msg("Content right"); - case LayoutEnum.SidebarLeft: - return msg("Sidebar left"); - case LayoutEnum.SidebarRight: - return msg("Sidebar right"); - case LayoutEnum.UnknownDefaultOpenApi: - return msg("Unknown layout"); - } + return layoutToLabel.get(layout) ?? msg("Unknown layout"); } diff --git a/web/src/common/labels.ts b/web/src/common/labels.ts index d63c4a38a..fc76406b1 100644 --- a/web/src/common/labels.ts +++ b/web/src/common/labels.ts @@ -2,6 +2,8 @@ import { msg } from "@lit/localize"; import { Device, EventActions, IntentEnum, SeverityEnum, UserTypeEnum } from "@goauthentik/api"; +type Pair = [T, string]; + /* Various tables in the API for which we need to supply labels */ export const intentEnumToLabel = new Map([ @@ -14,7 +16,7 @@ export const intentEnumToLabel = new Map([ export const intentToLabel = (intent: IntentEnum) => intentEnumToLabel.get(intent); -export const eventActionToLabel = new Map([ +export const eventActionLabels: Pair[] = [ [EventActions.Login, msg("Login")], [EventActions.LoginFailed, msg("Failed login")], [EventActions.Logout, msg("Logout")], @@ -43,7 +45,9 @@ export const eventActionToLabel = new Map([ [EventActions.ModelDeleted, msg("Model deleted")], [EventActions.EmailSent, msg("Email sent")], [EventActions.UpdateAvailable, msg("Update available")], -]); +] + +export const eventActionToLabel = new Map(eventActionLabels); export const actionToLabel = (action?: EventActions): string => eventActionToLabel.get(action) ?? action ?? ""; diff --git a/web/src/elements/sidebar/SidebarItems.ts b/web/src/elements/sidebar/SidebarItems.ts index e5dec02ea..ba04acb8b 100644 --- a/web/src/elements/sidebar/SidebarItems.ts +++ b/web/src/elements/sidebar/SidebarItems.ts @@ -69,6 +69,12 @@ export class SidebarItems extends AKElement { max-height: 82px; margin-bottom: -0.5rem; } + .pf-c-nav__toggle { + width: calc( + var(--pf-c-nav__toggle--FontSize) + calc(2 * var(--pf-global--spacer--md)) + ); + } + nav { display: flex; flex-direction: column; @@ -86,6 +92,17 @@ export class SidebarItems extends AKElement { --pf-c-nav__link--PaddingRight: 0.5rem; --pf-c-nav__link--PaddingBottom: 0.5rem; } + + .pf-c-nav__link a { +flex: 1 0 max-content; + color: var(--pf-c-nav__link--Color); + } + + a.pf-c-nav__link:hover { + color: var(--pf-c-nav__link--Color); + text-decoration: var(--pf-global--link--TextDecoration--hover); + } + .pf-c-nav__section-title { font-size: 12px; }