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/AdminInterface.ts similarity index 50% rename from web/src/admin/AdminInterface.ts rename to web/src/admin/AdminInterface/AdminInterface.ts index e59793706..69e07d84e 100644 --- a/web/src/admin/AdminInterface.ts +++ b/web/src/admin/AdminInterface/AdminInterface.ts @@ -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`
- - ${this.renderSidebarItems()} - +
-
+
@@ -177,120 +176,4 @@ export class AdminInterface extends Interface {
`; } - - 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/AdminSidebar.ts b/web/src/admin/AdminInterface/AdminSidebar.ts new file mode 100644 index 000000000..f922bbeff --- /dev/null +++ b/web/src/admin/AdminInterface/AdminSidebar.ts @@ -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` + + ${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/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..f8e588a05 --- /dev/null +++ b/web/src/admin/AdminInterface/index.ts @@ -0,0 +1,3 @@ +import { AdminInterface } from "./AdminInterface"; +import "./AdminInterface"; +export { AdminInterface }; diff --git a/web/src/elements/AuthentikContexts.ts b/web/src/elements/AuthentikContexts.ts new file mode 100644 index 000000000..a4b7d4f84 --- /dev/null +++ b/web/src/elements/AuthentikContexts.ts @@ -0,0 +1,8 @@ +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..f9efc1228 100644 --- a/web/src/elements/Base.ts +++ b/web/src/elements/Base.ts @@ -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; @@ -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(); 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()}
  • `; } }