diff --git a/web/rollup.config.mjs b/web/rollup.config.mjs index db9caf6ee..49825a29d 100644 --- a/web/rollup.config.mjs +++ b/web/rollup.config.mjs @@ -148,7 +148,7 @@ export default [ }, // Admin interface { - input: "./src/admin/AdminInterface.ts", + input: "./src/admin/AdminInterface/AdminInterface.ts", output: [ { format: "es", diff --git a/web/src/admin/AdminInterface.ts b/web/src/admin/AdminInterface.ts deleted file mode 100644 index e59793706..000000000 --- a/web/src/admin/AdminInterface.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { ROUTES } from "@goauthentik/admin/Routes"; -import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { - EVENT_API_DRAWER_TOGGLE, - EVENT_NOTIFICATION_DRAWER_TOGGLE, - EVENT_SIDEBAR_TOGGLE, - VERSION, -} from "@goauthentik/common/constants"; -import { configureSentry } from "@goauthentik/common/sentry"; -import { me } from "@goauthentik/common/users"; -import { WebsocketClient } from "@goauthentik/common/ws"; -import { Interface } from "@goauthentik/elements/Base"; -import "@goauthentik/elements/ak-locale-context"; -import "@goauthentik/elements/enterprise/EnterpriseStatusBanner"; -import "@goauthentik/elements/messages/MessageContainer"; -import "@goauthentik/elements/messages/MessageContainer"; -import "@goauthentik/elements/notifications/APIDrawer"; -import "@goauthentik/elements/notifications/NotificationDrawer"; -import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route"; -import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch"; -import "@goauthentik/elements/router/RouterOutlet"; -import "@goauthentik/elements/sidebar/Sidebar"; -import "@goauthentik/elements/sidebar/SidebarItem"; -import { spread } from "@open-wc/lit-helpers"; - -import { msg, str } from "@lit/localize"; -import { CSSResult, TemplateResult, css, html, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; -import { map } from "lit/directives/map.js"; - -import PFButton from "@patternfly/patternfly/components/Button/button.css"; -import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css"; -import PFPage from "@patternfly/patternfly/components/Page/page.css"; -import PFBase from "@patternfly/patternfly/patternfly-base.css"; - -import { - AdminApi, - CapabilitiesEnum, - CoreApi, - SessionUser, - UiThemeEnum, - Version, -} from "@goauthentik/api"; - -@customElement("ak-interface-admin") -export class AdminInterface extends Interface { - @property({ type: Boolean }) - sidebarOpen = true; - - @property({ type: Boolean }) - notificationDrawerOpen = getURLParam("notificationDrawerOpen", false); - - @property({ type: Boolean }) - apiDrawerOpen = getURLParam("apiDrawerOpen", false); - - ws: WebsocketClient; - - @state() - version?: Version; - - @state() - user?: SessionUser; - - static get styles(): CSSResult[] { - return [ - PFBase, - PFPage, - PFButton, - PFDrawer, - css` - .pf-c-page__main, - .pf-c-drawer__content, - .pf-c-page__drawer { - z-index: auto !important; - background-color: transparent; - } - .display-none { - display: none; - } - .pf-c-page { - background-color: var(--pf-c-page--BackgroundColor) !important; - } - /* Global page background colour */ - :host([theme="dark"]) .pf-c-page { - --pf-c-page--BackgroundColor: var(--ak-dark-background); - } - `, - ]; - } - - constructor() { - super(); - this.ws = new WebsocketClient(); - this.sidebarOpen = window.innerWidth >= 1280; - window.addEventListener("resize", () => { - this.sidebarOpen = window.innerWidth >= 1280; - }); - window.addEventListener(EVENT_SIDEBAR_TOGGLE, () => { - this.sidebarOpen = !this.sidebarOpen; - }); - window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => { - this.notificationDrawerOpen = !this.notificationDrawerOpen; - updateURLParams({ - notificationDrawerOpen: this.notificationDrawerOpen, - }); - }); - window.addEventListener(EVENT_API_DRAWER_TOGGLE, () => { - this.apiDrawerOpen = !this.apiDrawerOpen; - updateURLParams({ - apiDrawerOpen: this.apiDrawerOpen, - }); - }); - } - - async firstUpdated(): Promise { - configureSentry(true); - this.version = await new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve(); - this.user = await me(); - const canAccessAdmin = - this.user.user.isSuperuser || - // TODO: somehow add `access_admin_interface` to the API schema - this.user.user.systemPermissions.includes("access_admin_interface"); - if (!canAccessAdmin && this.user.user.pk > 0) { - window.location.assign("/if/user/"); - } - } - - render(): TemplateResult { - return html` -
- - ${this.renderSidebarItems()} - -
-
-
-
-
-
- - -
-
-
- - -
-
-
`; - } - - renderSidebarItems(): TemplateResult { - // The second attribute type is of string[] to help with the 'activeWhen' control, which was - // commonplace and singular enough to merit its own handler. - type SidebarEntry = [ - path: string | null, - label: string, - attributes?: Record | string[] | null, // eslint-disable-line - children?: SidebarEntry[], - ]; - - // prettier-ignore - const sidebarContent: SidebarEntry[] = [ - ["/if/user/", msg("User interface"), { "?isAbsoluteLink": true, "?highlight": true }], - [null, msg("Dashboards"), { "?expanded": true }, [ - ["/administration/overview", msg("Overview")], - ["/administration/dashboard/users", msg("User Statistics")], - ["/administration/system-tasks", msg("System Tasks")]]], - [null, msg("Applications"), null, [ - ["/core/providers", msg("Providers"), [`^/core/providers/(?${ID_REGEX})$`]], - ["/core/applications", msg("Applications"), [`^/core/applications/(?${SLUG_REGEX})$`]], - ["/outpost/outposts", msg("Outposts")]]], - [null, msg("Events"), null, [ - ["/events/log", msg("Logs"), [`^/events/log/(?${UUID_REGEX})$`]], - ["/events/rules", msg("Notification Rules")], - ["/events/transports", msg("Notification Transports")]]], - [null, msg("Customisation"), null, [ - ["/policy/policies", msg("Policies")], - ["/core/property-mappings", msg("Property Mappings")], - ["/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/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/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")]]] - ]; - - // Typescript requires the type here to correctly type the recursive path - type SidebarRenderer = (_: SidebarEntry) => TemplateResult; - - const renderOneSidebarItem: SidebarRenderer = ([path, label, attributes, children]) => { - const properties = Array.isArray(attributes) - ? { ".activeWhen": attributes } - : attributes ?? {}; - if (path) { - properties["path"] = path; - } - return html` - ${label ? html`${label}` : nothing} - ${map(children, renderOneSidebarItem)} - `; - }; - - // prettier-ignore - return html` - ${this.renderNewVersionMessage()} - ${this.renderImpersonationMessage()} - ${map(sidebarContent, renderOneSidebarItem)} - ${this.renderEnterpriseMessage()} - `; - } - - renderNewVersionMessage() { - return this.version && this.version.versionCurrent !== VERSION - ? html` - - ${msg("A newer version of the frontend is available.")} - - ` - : nothing; - } - - renderImpersonationMessage() { - return this.user?.original - ? html` { - new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve().then(() => { - window.location.reload(); - }); - }} - > - ${msg( - str`You're currently impersonating ${this.user.user.username}. Click to stop.`, - )} - ` - : nothing; - } - - renderEnterpriseMessage() { - return this.config?.capabilities.includes(CapabilitiesEnum.IsEnterprise) - ? html` - - ${msg("Enterprise")} - - ${msg("Licenses")} - - - ` - : nothing; - } -} diff --git a/web/src/admin/AdminInterface/AdminInterface.ts b/web/src/admin/AdminInterface/AdminInterface.ts new file mode 100644 index 000000000..834c98f37 --- /dev/null +++ b/web/src/admin/AdminInterface/AdminInterface.ts @@ -0,0 +1,160 @@ +import { ROUTES } from "@goauthentik/admin/Routes"; +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { + EVENT_API_DRAWER_TOGGLE, + EVENT_NOTIFICATION_DRAWER_TOGGLE, +} from "@goauthentik/common/constants"; +import { configureSentry } from "@goauthentik/common/sentry"; +import { me } from "@goauthentik/common/users"; +import { WebsocketClient } from "@goauthentik/common/ws"; +import { Interface } from "@goauthentik/elements/Base"; +import "@goauthentik/elements/ak-locale-context"; +import "@goauthentik/elements/enterprise/EnterpriseStatusBanner"; +import "@goauthentik/elements/messages/MessageContainer"; +import "@goauthentik/elements/messages/MessageContainer"; +import "@goauthentik/elements/notifications/APIDrawer"; +import "@goauthentik/elements/notifications/NotificationDrawer"; +import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch"; +import "@goauthentik/elements/router/RouterOutlet"; +import "@goauthentik/elements/sidebar/Sidebar"; +import "@goauthentik/elements/sidebar/SidebarItem"; + +import { CSSResult, TemplateResult, css, html } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { classMap } from "lit/directives/class-map.js"; + +import PFButton from "@patternfly/patternfly/components/Button/button.css"; +import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css"; +import PFPage from "@patternfly/patternfly/components/Page/page.css"; +import PFBase from "@patternfly/patternfly/patternfly-base.css"; + +import { AdminApi, SessionUser, UiThemeEnum, Version } from "@goauthentik/api"; + +import "./AdminSidebar"; + +@customElement("ak-interface-admin") +export class AdminInterface extends Interface { + @property({ type: Boolean }) + notificationDrawerOpen = getURLParam("notificationDrawerOpen", false); + + @property({ type: Boolean }) + apiDrawerOpen = getURLParam("apiDrawerOpen", false); + + ws: WebsocketClient; + + @state() + version?: Version; + + @state() + user?: SessionUser; + + static get styles(): CSSResult[] { + return [ + PFBase, + PFPage, + PFButton, + PFDrawer, + css` + .pf-c-page__main, + .pf-c-drawer__content, + .pf-c-page__drawer { + z-index: auto !important; + background-color: transparent; + } + .display-none { + display: none; + } + .pf-c-page { + background-color: var(--pf-c-page--BackgroundColor) !important; + } + /* Global page background colour */ + :host([theme="dark"]) .pf-c-page { + --pf-c-page--BackgroundColor: var(--ak-dark-background); + } + `, + ]; + } + + constructor() { + super(); + this.ws = new WebsocketClient(); + window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, () => { + this.notificationDrawerOpen = !this.notificationDrawerOpen; + updateURLParams({ + notificationDrawerOpen: this.notificationDrawerOpen, + }); + }); + window.addEventListener(EVENT_API_DRAWER_TOGGLE, () => { + this.apiDrawerOpen = !this.apiDrawerOpen; + updateURLParams({ + apiDrawerOpen: this.apiDrawerOpen, + }); + }); + } + + async firstUpdated(): Promise { + configureSentry(true); + this.version = await new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve(); + this.user = await me(); + const canAccessAdmin = + this.user.user.isSuperuser || + // TODO: somehow add `access_admin_interface` to the API schema + this.user.user.systemPermissions.includes("access_admin_interface"); + if (!canAccessAdmin && this.user.user.pk > 0) { + window.location.assign("/if/user/"); + } + } + + render(): TemplateResult { + const sidebarClasses = { + "pf-m-light": this.activeTheme === UiThemeEnum.Light, + }; + + const drawerOpen = this.notificationDrawerOpen || this.apiDrawerOpen; + const drawerClasses = { + "pf-m-expanded": drawerOpen, + "pf-m-collapsed": !drawerOpen, + }; + + return html` +
+ +
+
+
+
+
+
+ + +
+
+
+ + +
+
+
`; + } +} diff --git a/web/src/admin/AdminInterface/AdminSidebar.ts b/web/src/admin/AdminInterface/AdminSidebar.ts new file mode 100644 index 000000000..2f973ca7e --- /dev/null +++ b/web/src/admin/AdminInterface/AdminSidebar.ts @@ -0,0 +1,214 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { EVENT_SIDEBAR_TOGGLE, VERSION } from "@goauthentik/common/constants"; +import { me } from "@goauthentik/common/users"; +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 { getRootStyle } from "@goauthentik/elements/utils/getRootStyle"; +import { spread } from "@open-wc/lit-helpers"; + +import { consume } from "@lit-labs/context"; +import { msg, str } from "@lit/localize"; +import { TemplateResult, html, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { map } from "lit/directives/map.js"; + +import { AdminApi, CapabilitiesEnum, CoreApi, UiThemeEnum, Version } from "@goauthentik/api"; +import type { Config, SessionUser, UserSelf } from "@goauthentik/api"; + +@customElement("ak-admin-sidebar") +export class AkAdminSidebar extends AKElement { + @property({ type: Boolean, reflect: true }) + open = true; + + @state() + version: Version["versionCurrent"] | null = null; + + @state() + impersonation: UserSelf["username"] | null = null; + + @consume({ context: authentikConfigContext }) + public config!: Config; + + constructor() { + super(); + new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve().then((version) => { + this.version = version.versionCurrent; + }); + me().then((user: SessionUser) => { + this.impersonation = user.original ? user.user.username : null; + }); + this.toggleOpen = this.toggleOpen.bind(this); + this.checkWidth = this.checkWidth.bind(this); + } + + // This has to be a bound method so the event listener can be removed on disconnection as + // needed. + toggleOpen() { + this.open = !this.open; + } + + 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")); + this.open = window.innerWidth >= minWidth; + } + + connectedCallback() { + super.connectedCallback(); + window.addEventListener(EVENT_SIDEBAR_TOGGLE, this.toggleOpen); + window.addEventListener("resize", this.checkWidth); + // After connecting to the DOM, we can now perform this check to see if the sidebar should + // be open by default. + this.checkWidth(); + } + + // The symmetry (☟, ☝) here is critical in that you want to start adding these handlers after + // connection, and removing them before disconnection. + + disconnectedCallback() { + window.removeEventListener(EVENT_SIDEBAR_TOGGLE, this.toggleOpen); + window.removeEventListener("resize", this.checkWidth); + super.disconnectedCallback(); + } + + render() { + return html` + + ${this.renderSidebarItems()} + + `; + } + + updated() { + // This is permissible as`:host.classList` is not one of the properties Lit uses as a + // scheduling trigger. This sort of shenanigans can trigger an loop, in that it will trigger + // a browser reflow, which may trigger some other styling the application is monitoring, + // triggering a re-render which triggers a browser reflow, ad infinitum. But we've been + // living with that since jQuery, and it's both well-known and fortunately rare. + this.classList.remove("pf-m-expanded", "pf-m-collapsed"); + this.classList.add(this.open ? "pf-m-expanded" : "pf-m-collapsed"); + } + + renderSidebarItems(): TemplateResult { + // The second attribute type is of string[] to help with the 'activeWhen' control, which was + // commonplace and singular enough to merit its own handler. + type SidebarEntry = [ + path: string | null, + label: string, + attributes?: Record | string[] | null, // eslint-disable-line + children?: SidebarEntry[], + ]; + + // prettier-ignore + const sidebarContent: SidebarEntry[] = [ + ["/if/user/", msg("User interface"), { "?isAbsoluteLink": true, "?highlight": true }], + [null, msg("Dashboards"), { "?expanded": true }, [ + ["/administration/overview", msg("Overview")], + ["/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})$`]], + ["/outpost/outposts", msg("Outposts")]]], + [null, msg("Events"), null, [ + ["/events/log", msg("Logs"), [`^/events/log/(?${UUID_REGEX})$`]], + ["/events/rules", msg("Notification Rules")], + ["/events/transports", msg("Notification Transports")]]], + [null, msg("Customisation"), null, [ + ["/policy/policies", msg("Policies")], + ["/core/property-mappings", msg("Property Mappings")], + ["/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/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/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")]]] + ]; + + // Typescript requires the type here to correctly type the recursive path + type SidebarRenderer = (_: SidebarEntry) => TemplateResult; + + const renderOneSidebarItem: SidebarRenderer = ([path, label, attributes, children]) => { + const properties = Array.isArray(attributes) + ? { ".activeWhen": attributes } + : attributes ?? {}; + if (path) { + properties["path"] = path; + } + return html` + ${label ? html`${label}` : nothing} + ${map(children, renderOneSidebarItem)} + `; + }; + + // prettier-ignore + return html` + ${this.renderNewVersionMessage()} + ${this.renderImpersonationMessage()} + ${map(sidebarContent, renderOneSidebarItem)} + ${this.renderEnterpriseMessage()} + `; + } + + renderNewVersionMessage() { + return this.version && this.version !== VERSION + ? html` + + ${msg("A newer version of the frontend is available.")} + + ` + : nothing; + } + + renderImpersonationMessage() { + const reload = () => + new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve().then(() => { + window.location.reload(); + }); + + return this.impersonation + ? html` + ${msg( + str`You're currently impersonating ${this.impersonation}. Click to stop.`, + )} + ` + : nothing; + } + + renderEnterpriseMessage() { + return this.config?.capabilities.includes(CapabilitiesEnum.IsEnterprise) + ? html` + + ${msg("Enterprise")} + + ${msg("Licenses")} + + + ` + : nothing; + } +} diff --git a/web/src/admin/AdminInterface/index.ts b/web/src/admin/AdminInterface/index.ts new file mode 100644 index 000000000..570b87e3b --- /dev/null +++ b/web/src/admin/AdminInterface/index.ts @@ -0,0 +1,5 @@ +import { AdminInterface } from "./AdminInterface"; +import "./AdminInterface"; + +export { AdminInterface }; +export default AdminInterface; diff --git a/web/src/common/styles/authentik.css b/web/src/common/styles/authentik.css index a46ec8eb1..6a0000e4e 100644 --- a/web/src/common/styles/authentik.css +++ b/web/src/common/styles/authentik.css @@ -12,6 +12,9 @@ /* PatternFly likes to override global variables for some reason */ --ak-global--Color--100: var(--pf-global--Color--100); + + /* Minimum width after which the sidebar becomes automatic */ + --ak-sidebar--minimum-auto-width: 80rem; } ::-webkit-scrollbar { diff --git a/web/src/elements/AuthentikContexts.ts b/web/src/elements/AuthentikContexts.ts new file mode 100644 index 000000000..97a89a881 --- /dev/null +++ b/web/src/elements/AuthentikContexts.ts @@ -0,0 +1,7 @@ +import { createContext } from "@lit-labs/context"; + +import { type Config } from "@goauthentik/api"; + +export const authentikConfigContext = createContext(Symbol("authentik-config-context")); + +export default authentikConfigContext; diff --git a/web/src/elements/Base.ts b/web/src/elements/Base.ts index 7b2420454..46c983aad 100644 --- a/web/src/elements/Base.ts +++ b/web/src/elements/Base.ts @@ -2,7 +2,9 @@ import { config, tenant } from "@goauthentik/common/api/config"; import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants"; import { UIConfig, uiConfig } from "@goauthentik/common/ui/config"; import { adaptCSS } from "@goauthentik/common/utils"; +import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts"; +import { ContextProvider } from "@lit-labs/context"; import { localized } from "@lit/localize"; import { CSSResult, LitElement } from "lit"; import { state } from "lit/decorators.js"; @@ -181,8 +183,23 @@ export class Interface extends AKElement implements AkInterface { @state() uiConfig?: UIConfig; + _configContext = new ContextProvider(this, { + context: authentikConfigContext, + initialValue: undefined, + }); + + _config?: Config; + @state() - config?: Config; + set config(c: Config) { + this._config = c; + this._configContext.setValue(c); + this.requestUpdate(); + } + + get config(): Config | undefined { + return this._config; + } constructor() { super(); diff --git a/web/src/elements/sidebar/SidebarItem.ts b/web/src/elements/sidebar/SidebarItem.ts index 9d5374736..26cdb975e 100644 --- a/web/src/elements/sidebar/SidebarItem.ts +++ b/web/src/elements/sidebar/SidebarItem.ts @@ -144,47 +144,84 @@ export class SidebarItem extends AKElement { return this.renderInner(); } - renderInner(): TemplateResult { - if (this.childItems.length > 0) { - return html`
  • + -
    -
      - -
    -
    -
  • `; + + +
    +
      + +
    +
    + `; + } + + renderWithPathAndChildren() { + return html`
  • + + +
    +
      + +
    +
    +
  • `; + } + + renderWithPath() { + return html` + + + + `; + } + + renderWithLabel() { + html` + + + + `; + } + + renderInner() { + if (this.childItems.length > 0) { + return this.path ? this.renderWithPathAndChildren() : this.renderWithChildren(); } + return html`
  • - ${this.path - ? html` - - - - ` - : html` - - - - `} + ${this.path ? this.renderWithPath() : this.renderWithLabel()}
  • `; } } diff --git a/web/src/elements/utils/getRootStyle.ts b/web/src/elements/utils/getRootStyle.ts new file mode 100644 index 000000000..f91d63e5f --- /dev/null +++ b/web/src/elements/utils/getRootStyle.ts @@ -0,0 +1,5 @@ +export function getRootStyle(selector: string, element: HTMLElement = document.documentElement) { + return getComputedStyle(element, null).getPropertyValue(selector); +} + +export default getRootStyle; diff --git a/web/xliff/de.xlf b/web/xliff/de.xlf index 38ac63d46..cacf5580c 100644 --- a/web/xliff/de.xlf +++ b/web/xliff/de.xlf @@ -4989,9 +4989,9 @@ Bindings to groups/users are checked against the user of the event. Eine neuere Version des Frontends ist verfügbar. - You're currently impersonating . Click to stop. + You're currently impersonating . Click to stop. Sie geben sich gerade als - aus. Klicken Sie zum Stoppen. + aus. Klicken Sie zum Stoppen. User interface diff --git a/web/xliff/en.xlf b/web/xliff/en.xlf index fd6274b44..11d8ee1a5 100644 --- a/web/xliff/en.xlf +++ b/web/xliff/en.xlf @@ -5248,9 +5248,9 @@ Bindings to groups/users are checked against the user of the event. A newer version of the frontend is available. - You're currently impersonating . Click to stop. + You're currently impersonating . Click to stop. You're currently impersonating - . Click to stop. + . Click to stop. User interface diff --git a/web/xliff/es.xlf b/web/xliff/es.xlf index e6f6fc899..b5708c7ac 100644 --- a/web/xliff/es.xlf +++ b/web/xliff/es.xlf @@ -4914,9 +4914,9 @@ Bindings to groups/users are checked against the user of the event. Está disponible una versión más reciente de la interfaz. - You're currently impersonating . Click to stop. + You're currently impersonating . Click to stop. Estás suplantando a - . Haga clic para parar. + . Haga clic para parar. User interface diff --git a/web/xliff/fr.xlf b/web/xliff/fr.xlf index 4343e0ab3..eb5cbb2f1 100644 --- a/web/xliff/fr.xlf +++ b/web/xliff/fr.xlf @@ -6558,9 +6558,9 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti - You're currently impersonating . Click to stop. + You're currently impersonating . Click to stop. Vous vous faites actuellement passer pour - . Cliquer pour arrêter. + . Cliquer pour arrêter. diff --git a/web/xliff/nl.xlf b/web/xliff/nl.xlf index c6ac85543..f2ceeb89c 100644 --- a/web/xliff/nl.xlf +++ b/web/xliff/nl.xlf @@ -6797,8 +6797,8 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de - You're currently impersonating . Click to stop. - Je doet momenteel alsof je bent. Klik om te stoppen. + You're currently impersonating . Click to stop. + Je doet momenteel alsof je bent. Klik om te stoppen. diff --git a/web/xliff/pl.xlf b/web/xliff/pl.xlf index d50ab62c2..0c8118ae8 100644 --- a/web/xliff/pl.xlf +++ b/web/xliff/pl.xlf @@ -5104,9 +5104,9 @@ Bindings to groups/users are checked against the user of the event. Dostępna jest nowsza wersja frontendu. - You're currently impersonating . Click to stop. + You're currently impersonating . Click to stop. Obecnie podszywasz się pod - . Kliknij, aby zatrzymać. + . Kliknij, aby zatrzymać. User interface diff --git a/web/xliff/pseudo-LOCALE.xlf b/web/xliff/pseudo-LOCALE.xlf index bd038e1be..78b560613 100644 --- a/web/xliff/pseudo-LOCALE.xlf +++ b/web/xliff/pseudo-LOCALE.xlf @@ -6515,8 +6515,8 @@ Bindings to groups/users are checked against the user of the event. - You're currently impersonating . Click to stop. - Ŷōũ'ŕē ćũŕŕēńţĺŷ ĩmƥēŕśōńàţĩńĝ . Ćĺĩćķ ţō śţōƥ. + You're currently impersonating . Click to stop. + Ŷōũ'ŕē ćũŕŕēńţĺŷ ĩmƥēŕśōńàţĩńĝ . Ćĺĩćķ ţō śţōƥ. diff --git a/web/xliff/tr.xlf b/web/xliff/tr.xlf index dd82e3e39..f2eb13ba5 100644 --- a/web/xliff/tr.xlf +++ b/web/xliff/tr.xlf @@ -4907,9 +4907,9 @@ Bindings to groups/users are checked against the user of the event. Ön yüzün daha yeni bir sürümü mevcuttur. - You're currently impersonating . Click to stop. + You're currently impersonating . Click to stop. Şu anda - kimliğine bürünüyorsunuz. Durdurmak için tıklayın. + kimliğine bürünüyorsunuz. Durdurmak için tıklayın. User interface diff --git a/web/xliff/zh-Hans.xlf b/web/xliff/zh-Hans.xlf index aa8d0b510..f314f44a2 100644 --- a/web/xliff/zh-Hans.xlf +++ b/web/xliff/zh-Hans.xlf @@ -6560,9 +6560,9 @@ Bindings to groups/users are checked against the user of the event. - You're currently impersonating . Click to stop. + You're currently impersonating . Click to stop. 您目前正在模拟 - 的身份。点击以停止。 + 的身份。点击以停止。 diff --git a/web/xliff/zh-Hant.xlf b/web/xliff/zh-Hant.xlf index b19eae4ca..e836104f2 100644 --- a/web/xliff/zh-Hant.xlf +++ b/web/xliff/zh-Hant.xlf @@ -4951,9 +4951,9 @@ Bindings to groups/users are checked against the user of the event. 有较新版本的前端可用。 - You're currently impersonating . Click to stop. + You're currently impersonating . Click to stop. 你目前正在模拟 - 。单击停止。 + 。单击停止。 User interface diff --git a/web/xliff/zh_CN.xlf b/web/xliff/zh_CN.xlf index e11e7e4ad..e661cbaa4 100644 --- a/web/xliff/zh_CN.xlf +++ b/web/xliff/zh_CN.xlf @@ -6560,9 +6560,9 @@ Bindings to groups/users are checked against the user of the event. - You're currently impersonating . Click to stop. + You're currently impersonating . Click to stop. 您目前正在模拟 - 的身份。点击以停止。 + 的身份。点击以停止。 diff --git a/web/xliff/zh_TW.xlf b/web/xliff/zh_TW.xlf index 2367c4581..bc39005a9 100644 --- a/web/xliff/zh_TW.xlf +++ b/web/xliff/zh_TW.xlf @@ -4950,9 +4950,9 @@ Bindings to groups/users are checked against the user of the event. 有较新版本的前端可用。 - You're currently impersonating . Click to stop. + You're currently impersonating . Click to stop. 你目前正在模拟 - 。单击停止。 + 。单击停止。 User interface