From a3673906c79624888b1a678924c30db7a33944a1 Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Tue, 28 Nov 2023 11:11:35 -0800 Subject: [PATCH] web: provide three-tier sidebar with subcategories This commit implements a three-tier sidebar with subcategories. The first thing is that we've refactored the Sidebar into a holistic entity; rather than be built in pieces, it's constructed declaratively from a tree of entries, much in the same way routes are, and for much the same reason1. The AdminSidebar element only provides the list of entries to show and some of the controls necessary to show/hide the sidebar. Because the sidebar requires a rich collection of objects retrieved from the back-end, to avoid cluttering the AdminSidebar each of those sublists of TypeCreate have been isolated into their own controllers. The SidebarTypeController isn't even a strongly reactive controller; all it does is fetch the TypeCreate collection and notify the client object that the fetch has completed. The client can then call the `.entries()` method on the controller to get the sub-tree of entries for the TypeCreate object. The Sidebar has been slightly (!) refactored so that it's emphatic about what it does: it shows the brand, nav, and user sections of the sidebar. The styling has been moved to a separate file, the better to emphasize this. The SidebarItems file is where all the magic-- and a lot of ugly-- is hidden. The main purpose of the SidebarItems is to render the tree of entries passed to it. That's it. But it also has to be able to read the URL and highlight which entry is currently being shown by the router, and it has to be able to open up all the parent objects of the "current" link so that the user gets a clear sense of where they are navigationally. Most messy: the `reclick()` function intercepts clicks on anchors and, using the same logic as the router, decides if the router would *not* handle the navigation event. If the router would not, it takes on the responsibility for reaching into the currently visible table, modifying the filter and triggering a new `.fetch()`. Somewhere along the way I boyscoutted another `switch` statement or two into lookup expressions. --- 1  One of the reasons for this is that the Administrator Application's sidebar, routes, and command palette will all get their data from a single source of truth, and that single source will be independent of any of those. This is a step in that direction. --- web/src/admin/AdminInterface/AdminSidebar.ts | 14 +-- web/src/elements/sidebar/Sidebar.css.ts | 56 +++++++++ web/src/elements/sidebar/Sidebar.ts | 62 +--------- web/src/elements/sidebar/SidebarItems.css.ts | 86 ++++++++++++++ web/src/elements/sidebar/SidebarItems.ts | 117 ++++--------------- web/src/elements/sidebar/utils.ts | 28 +++-- 6 files changed, 200 insertions(+), 163 deletions(-) create mode 100644 web/src/elements/sidebar/Sidebar.css.ts create mode 100644 web/src/elements/sidebar/SidebarItems.css.ts diff --git a/web/src/admin/AdminInterface/AdminSidebar.ts b/web/src/admin/AdminInterface/AdminSidebar.ts index 42b381095..269c90598 100644 --- a/web/src/admin/AdminInterface/AdminSidebar.ts +++ b/web/src/admin/AdminInterface/AdminSidebar.ts @@ -192,11 +192,11 @@ export class AkAdminSidebar extends AKElement { ["/administration/dashboard/users", msg("User Statistics")], ["/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})$`], this.providerTypes.entries()], + ["/core/applications", msg("Applications"), [`^/core/applications(/(?${SLUG_REGEX}))?$`]], + ["/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/log", msg("Logs"), [`^/events/log(/(?${UUID_REGEX}))?$`], eventTypes], ["/events/rules", msg("Notification Rules")], ["/events/transports", msg("Notification Transports")]]], [null, msg("Customisation"), null, [ @@ -205,14 +205,14 @@ export class AkAdminSidebar extends AKElement { ["/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/flows", msg("Flows"), [`^/flow/flows(/(?${SLUG_REGEX}))?$`], flowTypes], ["/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/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})$`], this.sourceTypes.entries()], + ["/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, [ diff --git a/web/src/elements/sidebar/Sidebar.css.ts b/web/src/elements/sidebar/Sidebar.css.ts new file mode 100644 index 000000000..51663931e --- /dev/null +++ b/web/src/elements/sidebar/Sidebar.css.ts @@ -0,0 +1,56 @@ +import { css } from "lit"; + +import PFNav from "@patternfly/patternfly/components/Nav/nav.css"; +import PFPage from "@patternfly/patternfly/components/Page/page.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +export const sidebarStyles = [ + PFBase, + PFPage, + PFNav, + css` + :host { + z-index: 100; + } + .pf-c-nav__link.pf-m-current::after, + .pf-c-nav__link.pf-m-current:hover::after, + .pf-c-nav__item.pf-m-current:not(.pf-m-expanded) .pf-c-nav__link::after { + --pf-c-nav__link--m-current--after--BorderColor: #fd4b2d; + } + :host([theme="light"]) { + border-right-color: transparent !important; + } + + .pf-c-nav__section + .pf-c-nav__section { + --pf-c-nav__section--section--MarginTop: var(--pf-global--spacer--sm); + } + .pf-c-nav__list .sidebar-brand { + max-height: 82px; + margin-bottom: -0.5rem; + } + nav { + display: flex; + flex-direction: column; + max-height: 100vh; + height: 100%; + overflow-y: hidden; + } + + ak-sidebar-items { + flex-grow: 1; + overflow-y: auto; + } + + .pf-c-nav__link { + --pf-c-nav__link--PaddingTop: 0.5rem; + --pf-c-nav__link--PaddingRight: 0.5rem; + --pf-c-nav__link--PaddingBottom: 0.5rem; + } + .pf-c-nav__section-title { + font-size: 12px; + } + .pf-c-nav__item { + --pf-c-nav__item--MarginTop: 0px; + } + `, +]; diff --git a/web/src/elements/sidebar/Sidebar.ts b/web/src/elements/sidebar/Sidebar.ts index c07dcf828..7fe6fda92 100644 --- a/web/src/elements/sidebar/Sidebar.ts +++ b/web/src/elements/sidebar/Sidebar.ts @@ -3,15 +3,12 @@ import "@goauthentik/elements/sidebar/SidebarBrand"; import "@goauthentik/elements/sidebar/SidebarItems"; import "@goauthentik/elements/sidebar/SidebarUser"; -import { CSSResult, TemplateResult, css, html } from "lit"; +import { html } from "lit"; import { customElement, property } from "lit/decorators.js"; -import PFNav from "@patternfly/patternfly/components/Nav/nav.css"; -import PFPage from "@patternfly/patternfly/components/Page/page.css"; -import PFBase from "@patternfly/patternfly/patternfly-base.css"; - import { UiThemeEnum } from "@goauthentik/api"; +import { sidebarStyles } from "./Sidebar.css.js"; import type { SidebarEntry } from "./types"; @customElement("ak-sidebar") @@ -19,60 +16,11 @@ export class Sidebar extends AKElement { @property({ type: Array }) entries: SidebarEntry[] = []; - static get styles(): CSSResult[] { - return [ - PFBase, - PFPage, - PFNav, - css` - :host { - z-index: 100; - } - .pf-c-nav__link.pf-m-current::after, - .pf-c-nav__link.pf-m-current:hover::after, - .pf-c-nav__item.pf-m-current:not(.pf-m-expanded) .pf-c-nav__link::after { - --pf-c-nav__link--m-current--after--BorderColor: #fd4b2d; - } - :host([theme="light"]) { - border-right-color: transparent !important; - } - - .pf-c-nav__section + .pf-c-nav__section { - --pf-c-nav__section--section--MarginTop: var(--pf-global--spacer--sm); - } - .pf-c-nav__list .sidebar-brand { - max-height: 82px; - margin-bottom: -0.5rem; - } - nav { - display: flex; - flex-direction: column; - max-height: 100vh; - height: 100%; - overflow-y: hidden; - } - - ak-sidebar-items { - flex-grow: 1; - overflow-y: auto; - } - - .pf-c-nav__link { - --pf-c-nav__link--PaddingTop: 0.5rem; - --pf-c-nav__link--PaddingRight: 0.5rem; - --pf-c-nav__link--PaddingBottom: 0.5rem; - } - .pf-c-nav__section-title { - font-size: 12px; - } - .pf-c-nav__item { - --pf-c-nav__item--MarginTop: 0px; - } - `, - ]; + static get styles() { + return sidebarStyles; } - render(): TemplateResult { + render() { return html`