Due for amendment

This commit is contained in:
Ken Sternberg 2023-11-07 13:29:53 -08:00
parent 58639a5d03
commit 829ad5d3f2
7 changed files with 310 additions and 177 deletions

View File

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

View File

@ -26,6 +26,7 @@ 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 { classMap } from "lit/directives/class-map.js";
import { map } from "lit/directives/map.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -33,14 +34,9 @@ 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";
import { AdminApi, CoreApi, SessionUser, UiThemeEnum, Version } from "@goauthentik/api";
import "./AdminSidebar";
@customElement("ak-interface-admin")
export class AdminInterface extends Interface {
@ -126,23 +122,26 @@ export class AdminInterface extends Interface {
}
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-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>
<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 ${this.notificationDrawerOpen || this.apiDrawerOpen
? "pf-m-expanded"
: "pf-m-collapsed"}"
>
<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">
@ -177,120 +176,4 @@ export class AdminInterface extends Interface {
</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,183 @@
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,
ProvidersApi,
type SessionUser,
UiThemeEnum,
type UserSelf,
TypeCreate,
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;
@state()
providerTypes: TypeCreate[] = [];
@consume({ context: authentikConfigContext })
public config!: Config;
firstUpdated() {
new AdminApi(DEFAULT_CONFIG).adminVersionRetrieve().then((version) => {
this.version = version.versionCurrent;
});
me().then((user: SessionUser) => {
this.impersonation = user.original ? user.user.username : null;
});
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((types) => {
this.providerTypes = types;
});
}
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,3 @@
import { AdminInterface } from "./AdminInterface";
import "./AdminInterface";
export { AdminInterface };

View File

@ -0,0 +1,8 @@
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

@ -1,6 +1,7 @@
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 { ContextProvider } from "@lit-labs/context";
import { adaptCSS } from "@goauthentik/common/utils";
import { localized } from "@lit/localize";
@ -12,6 +13,7 @@ import ThemeDark from "@goauthentik/common/styles/theme-dark.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { Config, CurrentTenant, UiThemeEnum } from "@goauthentik/api";
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
type AkInterface = HTMLElement & {
getTheme: () => Promise<UiThemeEnum>;
@ -181,8 +183,25 @@ 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();

View File

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