web: refactor sidebar capabilities for categorical subsections

The project "Change Admin UI lists to have sublists per type" requires some initial changes to the
UI to facilitate this request. The AdminSidebar is the principle target of this project, and it is
embedded in the AdminInterface. To facilitate editing the AdminSidebar as an independent entity,
AdminInterface has been moved into its own folder and the AdminSidebar extracted as a standalone Web
Component. This removes, oh, about half the code from AdminInterface. A little cleanup with
`classMap` was also committed.

The rollup config was adjusted to find the new AdminInterface location.

The Sidebar uses the global `config: Config` object to check for Enterprise capabilities. Rather
than plumb all the way down through the Interface => AdminInterface -> AdminSidebar, I chose to make
provide an alternative way of reaching the `config` object, as a *context*. Other configuration
objects (Me, UiConfig, Tenant) interfaces will be contextualized as demand warrants.

Demand will warrant.  Just not yet. <sup>1</sup>

The Sidebar has been refactored only slightly; the renderers are entirely the same as they were
prior to extraction. What has been changed is the source of information: when we retrieve the
current version we story *only* the information, and use type information to ensure that the version
we store is the version we care about. The same is true of `impersonation`; we care only about the
name of the person being impersonated being present, so we don't store anything else.

Fetches have been moved from `firstUpdated` to the constructor.  No reason to have the sidebar
render twice if the network returns before the render is scheduled.

Because the path used to identify the user being impersonated has changed, the `str()` references in
the XLIFF files had to be adjusted. **This change is to a variable only and does not require
translation.**

---
<sup>1</sup> The code is littered with checks to `me()?`, `uiConfig?`, `config?`, etc. In the
*context* of being logged in as an administrator those should never be in doubt. I intend to make
our interfaces not have any doubt.
This commit is contained in:
Ken Sternberg 2023-11-07 14:13:20 -08:00
parent bdd92f63d8
commit 19e8b675ae
20 changed files with 479 additions and 359 deletions

View File

@ -148,7 +148,7 @@ export default [
}, },
// Admin interface // Admin interface
{ {
input: "./src/admin/AdminInterface.ts", input: "./src/admin/AdminInterface/AdminInterface.ts",
output: [ output: [
{ {
format: "es", format: "es",

View File

@ -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<void> {
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` <ak-locale-context>
<div class="pf-c-page">
<ak-sidebar
class="pf-c-page__sidebar ${this.sidebarOpen
? "pf-m-expanded"
: "pf-m-collapsed"} ${this.activeTheme === UiThemeEnum.Light
? "pf-m-light"
: ""}"
>
${this.renderSidebarItems()}
</ak-sidebar>
<div class="pf-c-page__drawer">
<div
class="pf-c-drawer ${this.notificationDrawerOpen || this.apiDrawerOpen
? "pf-m-expanded"
: "pf-m-collapsed"}"
>
<div class="pf-c-drawer__main">
<div class="pf-c-drawer__content">
<div class="pf-c-drawer__body">
<main class="pf-c-page__main">
<ak-router-outlet
role="main"
class="pf-c-page__main"
tabindex="-1"
id="main-content"
defaultUrl="/administration/overview"
.routes=${ROUTES}
>
</ak-router-outlet>
</main>
</div>
</div>
<ak-notification-drawer
class="pf-c-drawer__panel pf-m-width-33 ${this
.notificationDrawerOpen
? ""
: "display-none"}"
?hidden=${!this.notificationDrawerOpen}
></ak-notification-drawer>
<ak-api-drawer
class="pf-c-drawer__panel pf-m-width-33 ${this.apiDrawerOpen
? ""
: "display-none"}"
?hidden=${!this.apiDrawerOpen}
></ak-api-drawer>
</div>
</div>
</div></div
></ak-locale-context>`;
}
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, any> | 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>${ID_REGEX})$`]],
["/core/applications", msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]],
["/outpost/outposts", msg("Outposts")]]],
[null, msg("Events"), null, [
["/events/log", msg("Logs"), [`^/events/log/(?<id>${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>${SLUG_REGEX})$`]],
["/flow/stages", msg("Stages")],
["/flow/stages/prompts", msg("Prompts")]]],
[null, msg("Directory"), null, [
["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]],
["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]],
["/identity/roles", msg("Roles"), [`^/identity/roles/(?<id>${UUID_REGEX})$`]],
["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${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`<ak-sidebar-item ${spread(properties)}>
${label ? html`<span slot="label">${label}</span>` : nothing}
${map(children, renderOneSidebarItem)}
</ak-sidebar-item>`;
};
// prettier-ignore
return html`
${this.renderNewVersionMessage()}
${this.renderImpersonationMessage()}
${map(sidebarContent, renderOneSidebarItem)}
${this.renderEnterpriseMessage()}
`;
}
renderNewVersionMessage() {
return this.version && this.version.versionCurrent !== VERSION
? html`
<ak-sidebar-item ?highlight=${true}>
<span slot="label"
>${msg("A newer version of the frontend is available.")}</span
>
</ak-sidebar-item>
`
: nothing;
}
renderImpersonationMessage() {
return this.user?.original
? html`<ak-sidebar-item
?highlight=${true}
@click=${() => {
new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve().then(() => {
window.location.reload();
});
}}
>
<span slot="label"
>${msg(
str`You're currently impersonating ${this.user.user.username}. Click to stop.`,
)}</span
>
</ak-sidebar-item>`
: nothing;
}
renderEnterpriseMessage() {
return this.config?.capabilities.includes(CapabilitiesEnum.IsEnterprise)
? html`
<ak-sidebar-item>
<span slot="label">${msg("Enterprise")}</span>
<ak-sidebar-item path="/enterprise/licenses">
<span slot="label">${msg("Licenses")}</span>
</ak-sidebar-item>
</ak-sidebar-item>
`
: nothing;
}
}

View File

@ -0,0 +1,174 @@
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,
} 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 })
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<void> {
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-expanded": this.sidebarOpen,
"pf-m-collapsed": !this.sidebarOpen,
"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` <ak-locale-context>
<div class="pf-c-page">
<ak-admin-sidebar
?open=${this.sidebarOpen}
class="pf-c-page__sidebar ${classMap(sidebarClasses)}"
></ak-admin-sidebar>
<div class="pf-c-page__drawer">
<div class="pf-c-drawer ${classMap(drawerClasses)}">
<div class="pf-c-drawer__main">
<div class="pf-c-drawer__content">
<div class="pf-c-drawer__body">
<main class="pf-c-page__main">
<ak-router-outlet
role="main"
class="pf-c-page__main"
tabindex="-1"
id="main-content"
defaultUrl="/administration/overview"
.routes=${ROUTES}
>
</ak-router-outlet>
</main>
</div>
</div>
<ak-notification-drawer
class="pf-c-drawer__panel pf-m-width-33 ${this
.notificationDrawerOpen
? ""
: "display-none"}"
?hidden=${!this.notificationDrawerOpen}
></ak-notification-drawer>
<ak-api-drawer
class="pf-c-drawer__panel pf-m-width-33 ${this.apiDrawerOpen
? ""
: "display-none"}"
?hidden=${!this.apiDrawerOpen}
></ak-api-drawer>
</div>
</div>
</div></div
></ak-locale-context>`;
}
}

View File

@ -0,0 +1,176 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { 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 { 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,
type Config,
CoreApi,
type SessionUser,
UiThemeEnum,
type UserSelf,
Version,
} from "@goauthentik/api";
@customElement("ak-admin-sidebar")
export class AkAdminSidebar extends AKElement {
@property({ type: Boolean })
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;
});
}
render() {
return html`
<ak-sidebar
class="pf-c-page__sidebar ${this.open ? "pf-m-expanded" : "pf-m-collapsed"} ${this
.activeTheme === UiThemeEnum.Light
? "pf-m-light"
: ""}"
>
${this.renderSidebarItems()}
</ak-sidebar>
`;
}
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, any> | 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>${SLUG_REGEX})$`]],
["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`]],
["/outpost/outposts", msg("Outposts")]]],
[null, msg("Events"), null, [
["/events/log", msg("Logs"), [`^/events/log/(?<id>${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>${SLUG_REGEX})$`]],
["/flow/stages", msg("Stages")],
["/flow/stages/prompts", msg("Prompts")]]],
[null, msg("Directory"), null, [
["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]],
["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]],
["/identity/roles", msg("Roles"), [`^/identity/roles/(?<id>${UUID_REGEX})$`]],
["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${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`<ak-sidebar-item ${spread(properties)}>
${label ? html`<span slot="label">${label}</span>` : nothing}
${map(children, renderOneSidebarItem)}
</ak-sidebar-item>`;
};
// prettier-ignore
return html`
${this.renderNewVersionMessage()}
${this.renderImpersonationMessage()}
${map(sidebarContent, renderOneSidebarItem)}
${this.renderEnterpriseMessage()}
`;
}
renderNewVersionMessage() {
return this.version && this.version !== VERSION
? html`
<ak-sidebar-item ?highlight=${true}>
<span slot="label"
>${msg("A newer version of the frontend is available.")}</span
>
</ak-sidebar-item>
`
: nothing;
}
renderImpersonationMessage() {
const reload = () =>
new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve().then(() => {
window.location.reload();
});
return this.impersonation
? html`<ak-sidebar-item ?highlight=${true} @click=${reload}>
<span slot="label"
>${msg(
str`You're currently impersonating ${this.impersonation}. Click to stop.`,
)}</span
>
</ak-sidebar-item>`
: nothing;
}
renderEnterpriseMessage() {
return this.config?.capabilities.includes(CapabilitiesEnum.IsEnterprise)
? html`
<ak-sidebar-item>
<span slot="label">${msg("Enterprise")}</span>
<ak-sidebar-item path="/enterprise/licenses">
<span slot="label">${msg("Licenses")}</span>
</ak-sidebar-item>
</ak-sidebar-item>
`
: nothing;
}
}

View File

@ -0,0 +1,5 @@
import { AdminInterface } from "./AdminInterface";
import "./AdminInterface";
export { AdminInterface };
export default AdminInterface;

View File

@ -0,0 +1,7 @@
import { createContext } from "@lit-labs/context";
import { type Config } from "@goauthentik/api";
export const authentikConfigContext = createContext<Config>(Symbol("authentik-config-context"));
export default authentikConfigContext;

View File

@ -2,7 +2,9 @@ import { config, tenant } from "@goauthentik/common/api/config";
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants"; import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
import { UIConfig, uiConfig } from "@goauthentik/common/ui/config"; import { UIConfig, uiConfig } from "@goauthentik/common/ui/config";
import { adaptCSS } from "@goauthentik/common/utils"; import { adaptCSS } from "@goauthentik/common/utils";
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import { ContextProvider } from "@lit-labs/context";
import { localized } from "@lit/localize"; import { localized } from "@lit/localize";
import { CSSResult, LitElement } from "lit"; import { CSSResult, LitElement } from "lit";
import { state } from "lit/decorators.js"; import { state } from "lit/decorators.js";
@ -181,8 +183,23 @@ export class Interface extends AKElement implements AkInterface {
@state() @state()
uiConfig?: UIConfig; uiConfig?: UIConfig;
_configContext = new ContextProvider(this, {
context: authentikConfigContext,
initialValue: undefined,
});
_config?: Config;
@state() @state()
config?: Config; set config(c: Config) {
this._config = c;
this._configContext.setValue(c);
this.requestUpdate();
}
get config(): Config | undefined {
return this._config;
}
constructor() { constructor() {
super(); super();

View File

@ -144,8 +144,7 @@ export class SidebarItem extends AKElement {
return this.renderInner(); return this.renderInner();
} }
renderInner(): TemplateResult { renderWithChildren() {
if (this.childItems.length > 0) {
return html`<li return html`<li
class="pf-c-nav__item ${this.expanded ? "pf-m-expandable pf-m-expanded" : ""}" class="pf-c-nav__item ${this.expanded ? "pf-m-expandable pf-m-expanded" : ""}"
> >
@ -170,21 +169,59 @@ export class SidebarItem extends AKElement {
</section> </section>
</li>`; </li>`;
} }
return html`<li class="pf-c-nav__item">
${this.path renderWithPathAndChildren() {
? html` return html`<li
class="pf-c-nav__item ${this.expanded ? "pf-m-expandable pf-m-expanded" : ""}"
>
<slot name="label"></slot>
<button
class="pf-c-nav__link"
aria-expanded="true"
@click=${() => {
this.expanded = !this.expanded;
}}
>
<span class="pf-c-nav__toggle">
<span class="pf-c-nav__toggle-icon">
<i class="fas fa-angle-right" aria-hidden="true"></i>
</span>
</span>
</button>
<section class="pf-c-nav__subnav" ?hidden=${!this.expanded}>
<ul class="pf-c-nav__list">
<slot></slot>
</ul>
</section>
</li>`;
}
renderWithPath() {
return html`
<a <a
href="${this.isAbsoluteLink ? "" : "#"}${this.path}" href="${this.isAbsoluteLink ? "" : "#"}${this.path}"
class="pf-c-nav__link ${this.isActive ? "pf-m-current" : ""}" class="pf-c-nav__link ${this.isActive ? "pf-m-current" : ""}"
> >
<slot name="label"></slot> <slot name="label"></slot>
</a> </a>
` `;
: html` }
renderWithLabel() {
html`
<span class="pf-c-nav__link"> <span class="pf-c-nav__link">
<slot name="label"></slot> <slot name="label"></slot>
</span> </span>
`} `;
}
renderInner() {
if (this.childItems.length > 0) {
return this.path ? this.renderWithPathAndChildren() : this.renderWithChildren();
}
return html`<li class="pf-c-nav__item">
${this.path ? this.renderWithPath() : this.renderWithLabel()}
</li>`; </li>`;
} }
} }

View File

@ -5005,9 +5005,9 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>Eine neuere Version des Frontends ist verfügbar.</target> <target>Eine neuere Version des Frontends ist verfügbar.</target>
</trans-unit> </trans-unit>
<trans-unit id="s96b3cddf33e1c853"> <trans-unit id="s96b3cddf33e1c853">
<source>You're currently impersonating <x id="0" equiv-text="${this.user.user.username}"/>. Click to stop.</source> <source>You're currently impersonating <x id="0" equiv-text="${this.impersonation}"/>. Click to stop.</source>
<target>Sie geben sich gerade als <target>Sie geben sich gerade als
<x id="0" equiv-text="${this.user.user.username}"/>aus. Klicken Sie zum Stoppen.</target> <x id="0" equiv-text="${this.impersonation}"/>aus. Klicken Sie zum Stoppen.</target>
</trans-unit> </trans-unit>
<trans-unit id="s7031e6928c44cedd"> <trans-unit id="s7031e6928c44cedd">
<source>User interface</source> <source>User interface</source>

View File

@ -5268,9 +5268,9 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>A newer version of the frontend is available.</target> <target>A newer version of the frontend is available.</target>
</trans-unit> </trans-unit>
<trans-unit id="s96b3cddf33e1c853"> <trans-unit id="s96b3cddf33e1c853">
<source>You're currently impersonating <x id="0" equiv-text="${this.user.user.username}"/>. Click to stop.</source> <source>You're currently impersonating <x id="0" equiv-text="${this.impersonation}"/>. Click to stop.</source>
<target>You're currently impersonating <target>You're currently impersonating
<x id="0" equiv-text="${this.user.user.username}"/>. Click to stop.</target> <x id="0" equiv-text="${this.impersonation}"/>. Click to stop.</target>
</trans-unit> </trans-unit>
<trans-unit id="s7031e6928c44cedd"> <trans-unit id="s7031e6928c44cedd">
<source>User interface</source> <source>User interface</source>

View File

@ -4929,9 +4929,9 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>Está disponible una versión más reciente de la interfaz.</target> <target>Está disponible una versión más reciente de la interfaz.</target>
</trans-unit> </trans-unit>
<trans-unit id="s96b3cddf33e1c853"> <trans-unit id="s96b3cddf33e1c853">
<source>You're currently impersonating <x id="0" equiv-text="${this.user.user.username}"/>. Click to stop.</source> <source>You're currently impersonating <x id="0" equiv-text="${this.impersonation}"/>. Click to stop.</source>
<target>Estás suplantando a <target>Estás suplantando a
<x id="0" equiv-text="${this.user.user.username}"/>. Haga clic para parar.</target> <x id="0" equiv-text="${this.impersonation}"/>. Haga clic para parar.</target>
</trans-unit> </trans-unit>
<trans-unit id="s7031e6928c44cedd"> <trans-unit id="s7031e6928c44cedd">
<source>User interface</source> <source>User interface</source>

View File

@ -6583,9 +6583,9 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
</trans-unit> </trans-unit>
<trans-unit id="s96b3cddf33e1c853"> <trans-unit id="s96b3cddf33e1c853">
<source>You're currently impersonating <x id="0" equiv-text="${this.user.user.username}"/>. Click to stop.</source> <source>You're currently impersonating <x id="0" equiv-text="${this.impersonation}"/>. Click to stop.</source>
<target>Vous vous faites actuellement passer pour <target>Vous vous faites actuellement passer pour
<x id="0" equiv-text="${this.user.user.username}"/>. Cliquer pour arrêter.</target> <x id="0" equiv-text="${this.impersonation}"/>. Cliquer pour arrêter.</target>
</trans-unit> </trans-unit>
<trans-unit id="s7031e6928c44cedd"> <trans-unit id="s7031e6928c44cedd">

View File

@ -6797,8 +6797,8 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
</trans-unit> </trans-unit>
<trans-unit id="s96b3cddf33e1c853"> <trans-unit id="s96b3cddf33e1c853">
<source>You're currently impersonating <x id="0" equiv-text="${this.user.user.username}"/>. Click to stop.</source> <source>You're currently impersonating <x id="0" equiv-text="${this.impersonation}"/>. Click to stop.</source>
<target>Je doet momenteel alsof je <x id="0" equiv-text="${this.user.user.username}"/> bent. Klik om te stoppen.</target> <target>Je doet momenteel alsof je <x id="0" equiv-text="${this.impersonation}"/> bent. Klik om te stoppen.</target>
</trans-unit> </trans-unit>
<trans-unit id="s7031e6928c44cedd"> <trans-unit id="s7031e6928c44cedd">

View File

@ -5119,9 +5119,9 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>Dostępna jest nowsza wersja frontendu.</target> <target>Dostępna jest nowsza wersja frontendu.</target>
</trans-unit> </trans-unit>
<trans-unit id="s96b3cddf33e1c853"> <trans-unit id="s96b3cddf33e1c853">
<source>You're currently impersonating <x id="0" equiv-text="${this.user.user.username}"/>. Click to stop.</source> <source>You're currently impersonating <x id="0" equiv-text="${this.impersonation}"/>. Click to stop.</source>
<target>Obecnie podszywasz się pod <target>Obecnie podszywasz się pod
<x id="0" equiv-text="${this.user.user.username}"/>. Kliknij, aby zatrzymać.</target> <x id="0" equiv-text="${this.impersonation}"/>. Kliknij, aby zatrzymać.</target>
</trans-unit> </trans-unit>
<trans-unit id="s7031e6928c44cedd"> <trans-unit id="s7031e6928c44cedd">
<source>User interface</source> <source>User interface</source>

View File

@ -6540,8 +6540,8 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s96b3cddf33e1c853"> <trans-unit id="s96b3cddf33e1c853">
<source>You're currently impersonating <x id="0" equiv-text="${this.user.user.username}"/>. Click to stop.</source> <source>You're currently impersonating <x id="0" equiv-text="${this.impersonation}"/>. Click to stop.</source>
<target>Ŷōũ'ŕē ćũŕŕēńţĺŷ ĩmƥēŕśōńàţĩńĝ <x id="0" equiv-text="${this.user.user.username}"/>. Ćĺĩćķ ţō śţōƥ.</target> <target>Ŷōũ'ŕē ćũŕŕēńţĺŷ ĩmƥēŕśōńàţĩńĝ <x id="0" equiv-text="${this.impersonation}"/>. Ćĺĩćķ ţō śţōƥ.</target>
</trans-unit> </trans-unit>
<trans-unit id="s7031e6928c44cedd"> <trans-unit id="s7031e6928c44cedd">

View File

@ -4922,9 +4922,9 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>Ön yüzün daha yeni bir sürümü mevcuttur.</target> <target>Ön yüzün daha yeni bir sürümü mevcuttur.</target>
</trans-unit> </trans-unit>
<trans-unit id="s96b3cddf33e1c853"> <trans-unit id="s96b3cddf33e1c853">
<source>You're currently impersonating <x id="0" equiv-text="${this.user.user.username}"/>. Click to stop.</source> <source>You're currently impersonating <x id="0" equiv-text="${this.impersonation}"/>. Click to stop.</source>
<target>Şu anda <target>Şu anda
<x id="0" equiv-text="${this.user.user.username}"/>kimliğine bürünüyorsunuz. Durdurmak için tıklayın.</target> <x id="0" equiv-text="${this.impersonation}"/>kimliğine bürünüyorsunuz. Durdurmak için tıklayın.</target>
</trans-unit> </trans-unit>
<trans-unit id="s7031e6928c44cedd"> <trans-unit id="s7031e6928c44cedd">
<source>User interface</source> <source>User interface</source>

View File

@ -6585,9 +6585,9 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s96b3cddf33e1c853"> <trans-unit id="s96b3cddf33e1c853">
<source>You're currently impersonating <x id="0" equiv-text="${this.user.user.username}"/>. Click to stop.</source> <source>You're currently impersonating <x id="0" equiv-text="${this.impersonation}"/>. Click to stop.</source>
<target>您目前正在模拟 <target>您目前正在模拟
<x id="0" equiv-text="${this.user.user.username}"/>的身份。点击以停止。</target> <x id="0" equiv-text="${this.impersonation}"/>的身份。点击以停止。</target>
</trans-unit> </trans-unit>
<trans-unit id="s7031e6928c44cedd"> <trans-unit id="s7031e6928c44cedd">

View File

@ -4966,9 +4966,9 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>有较新版本的前端可用。</target> <target>有较新版本的前端可用。</target>
</trans-unit> </trans-unit>
<trans-unit id="s96b3cddf33e1c853"> <trans-unit id="s96b3cddf33e1c853">
<source>You're currently impersonating <x id="0" equiv-text="${this.user.user.username}"/>. Click to stop.</source> <source>You're currently impersonating <x id="0" equiv-text="${this.impersonation}"/>. Click to stop.</source>
<target>你目前正在模拟 <target>你目前正在模拟
<x id="0" equiv-text="${this.user.user.username}"/>。单击停止。</target> <x id="0" equiv-text="${this.impersonation}"/>。单击停止。</target>
</trans-unit> </trans-unit>
<trans-unit id="s7031e6928c44cedd"> <trans-unit id="s7031e6928c44cedd">
<source>User interface</source> <source>User interface</source>

View File

@ -6585,9 +6585,9 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="s96b3cddf33e1c853"> <trans-unit id="s96b3cddf33e1c853">
<source>You're currently impersonating <x id="0" equiv-text="${this.user.user.username}"/>. Click to stop.</source> <source>You're currently impersonating <x id="0" equiv-text="${this.impersonation}"/>. Click to stop.</source>
<target>您目前正在模拟 <target>您目前正在模拟
<x id="0" equiv-text="${this.user.user.username}"/>的身份。点击以停止。</target> <x id="0" equiv-text="${this.impersonation}"/>的身份。点击以停止。</target>
</trans-unit> </trans-unit>
<trans-unit id="s7031e6928c44cedd"> <trans-unit id="s7031e6928c44cedd">

View File

@ -4965,9 +4965,9 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>有较新版本的前端可用。</target> <target>有较新版本的前端可用。</target>
</trans-unit> </trans-unit>
<trans-unit id="s96b3cddf33e1c853"> <trans-unit id="s96b3cddf33e1c853">
<source>You're currently impersonating <x id="0" equiv-text="${this.user.user.username}"/>. Click to stop.</source> <source>You're currently impersonating <x id="0" equiv-text="${this.impersonation}"/>. Click to stop.</source>
<target>你目前正在模拟 <target>你目前正在模拟
<x id="0" equiv-text="${this.user.user.username}"/>。单击停止。</target> <x id="0" equiv-text="${this.impersonation}"/>。单击停止。</target>
</trans-unit> </trans-unit>
<trans-unit id="s7031e6928c44cedd"> <trans-unit id="s7031e6928c44cedd">
<source>User interface</source> <source>User interface</source>