web: almost there with the sidebar

The actual behavior is more or less what I expected.  What's missing is:

- Persistence of location (the hover effect fades with a click anywhere else)
- Proper testing of the oddities
- Full (or any!) responsiveness when moving between third-tier links in the same category

Stretch goal:
- Remembering the state of the sidebar when transitioning between the user and the admin (this will require using some localstorage, I suspect).

I also think that in my rush there's a bit of lost internal coherency.  I'd like to figure out what's wiggling around my brain and solve that discomfort.
This commit is contained in:
Ken Sternberg 2023-11-16 14:59:02 -08:00
parent 3c277f14c8
commit ff78f2f00a
4 changed files with 137 additions and 52 deletions

View File

@ -5,18 +5,35 @@ 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 "@goauthentik/elements/sidebar/Sidebar";
import { SidebarAttributes, SidebarEntry, SidebarEventHandler } from "@goauthentik/elements/sidebar/SidebarItems";
import {
SidebarAttributes,
SidebarEntry,
SidebarEventHandler,
} from "@goauthentik/elements/sidebar/SidebarItems";
import { getRootStyle } from "@goauthentik/elements/utils/getRootStyle";
import { consume } from "@lit-labs/context";
import { msg, str } from "@lit/localize";
import { html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { eventActionLabels } from "@goauthentik/common/labels";
import { ProvidersApi, TypeCreate } from "@goauthentik/api";
import { AdminApi, CapabilitiesEnum, CoreApi, Version } from "@goauthentik/api";
import {
AdminApi,
CapabilitiesEnum,
CoreApi,
OutpostsApi,
PoliciesApi,
PropertymappingsApi,
SourcesApi,
StagesApi,
Version,
} from "@goauthentik/api";
import type { Config, SessionUser, UserSelf } from "@goauthentik/api";
import { flowDesignationTable } from "../flows/utils";
/**
* AdminSidebar
*
@ -24,7 +41,7 @@ import type { Config, SessionUser, UserSelf } from "@goauthentik/api";
* it. Rendering decisions are left to the sidebar itself.
*/
type LocalSidebarEntry = [
export type LocalSidebarEntry = [
string | SidebarEventHandler | null,
string,
(SidebarAttributes | string[] | null)?, // eslint-disable-line
@ -34,12 +51,21 @@ type LocalSidebarEntry = [
const localToSidebarEntry = (l: LocalSidebarEntry): SidebarEntry => ({
path: l[0],
label: l[1],
...(l[2]? { attributes: Array.isArray(l[2]) ? { activeWhen: l[2] } : l[2] } : {}),
...(l[2] ? { attributes: Array.isArray(l[2]) ? { activeWhen: l[2] } : l[2] } : {}),
...(l[3] ? { children: l[3].map(localToSidebarEntry) } : {}),
});
const typeCreateToSidebar = (baseUrl: string, tcreate: TypeCreate[]): LocalSidebarEntry[] =>
tcreate.map((t) => [
`${baseUrl};${encodeURIComponent(JSON.stringify({ search: t.name }))}`,
t.name,
]);
@customElement("ak-admin-sidebar")
export class AkAdminSidebar extends AKElement {
@consume({ context: authentikConfigContext })
public config!: Config;
@property({ type: Boolean, reflect: true })
open = true;
@ -52,8 +78,20 @@ export class AkAdminSidebar extends AKElement {
@state()
providerTypes: TypeCreate[] = [];
@consume({ context: authentikConfigContext })
public config!: Config;
@state()
stageTypes: TypeCreate[] = [];
@state()
mappingTypes: TypeCreate[] = [];
@state()
sourceTypes: TypeCreate[] = [];
@state()
policyTypes: TypeCreate[] = [];
@state()
connectionTypes: TypeCreate[] = [];
constructor() {
super();
@ -66,6 +104,22 @@ export class AkAdminSidebar extends AKElement {
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((types) => {
this.providerTypes = types;
});
new StagesApi(DEFAULT_CONFIG).stagesAllTypesList().then((types) => {
this.stageTypes = types;
});
new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsAllTypesList().then((types) => {
this.mappingTypes = types;
});
new SourcesApi(DEFAULT_CONFIG).sourcesAllTypesList().then((types) => {
this.sourceTypes = types;
});
new PoliciesApi(DEFAULT_CONFIG).policiesAllTypesList().then((types) => {
this.policyTypes = types;
});
new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsAllTypesList().then((types) => {
this.connectionTypes = types;
});
this.toggleOpen = this.toggleOpen.bind(this);
this.checkWidth = this.checkWidth.bind(this);
}
@ -79,7 +133,9 @@ export class AkAdminSidebar extends AKElement {
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"));
const minWidth =
parseFloat(getRootStyle("--ak-sidebar--minimum-auto-width")) *
parseFloat(getRootStyle("font-size"));
this.open = window.innerWidth >= minWidth;
}
@ -133,8 +189,21 @@ export class AkAdminSidebar extends AKElement {
: [];
// prettier-ignore
const providerTypes: LocalSidebarEntry[] = this.providerTypes.map((ptype) =>
([`/core/providers;${encodeURIComponent(JSON.stringify({ search: ptype.modelName.replace(/provider$/, "") }))}`, ptype.name]));
const flowTypes: LocalSidebarEntry[] = flowDesignationTable.map(([_designation, label]) =>
([`/flow/flows;${encodeURIComponent(JSON.stringify({ search: label }))}`, label]));
const eventTypes: LocalSidebarEntry[] = eventActionLabels.map(([_action, label]) =>
([`/events/log;${encodeURIComponent(JSON.stringify({ search: label }))}`, label]));
const [mappingTypes, providerTypes, sourceTypes, stageTypes, connectionTypes, policyTypes] = [
typeCreateToSidebar("/core/property-mappings", this.mappingTypes),
typeCreateToSidebar("/core/providers", this.providerTypes),
typeCreateToSidebar("/core/sources", this.sourceTypes),
typeCreateToSidebar("/flow/stages", this.stageTypes),
typeCreateToSidebar("/outpost/integrations", this.connectionTypes),
typeCreateToSidebar("/policy/policies", this.policyTypes),
];
// prettier-ignore
const localSidebar: LocalSidebarEntry[] = [
@ -150,36 +219,38 @@ export class AkAdminSidebar extends AKElement {
["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`], providerTypes],
["/outpost/outposts", msg("Outposts")]]],
[null, msg("Events"), null, [
["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]],
["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`], eventTypes],
["/events/rules", msg("Notification Rules")],
["/events/transports", msg("Notification Transports")]]],
[null, msg("Customisation"), null, [
["/policy/policies", msg("Policies")],
["/core/property-mappings", msg("Property Mappings")],
["/policy/policies", msg("Policies"), null, policyTypes],
["/core/property-mappings", msg("Property Mappings"), null, mappingTypes],
["/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/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`], flowTypes],
["/flow/stages", msg("Stages"), null, stageTypes],
["/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/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`], sourceTypes],
["/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")]]],
["/outpost/integrations", msg("Outpost Integrations"), null, connectionTypes]]],
...(enterpriseMenu)
];
return localSidebar.map(localToSidebarEntry);
return localSidebar.map(localToSidebarEntry);
}
render() {
return html` <ak-sidebar class="pf-c-page__sidebar" .entries=${this.sidebarItems}></ak-sidebar> `;
return html`
<ak-sidebar class="pf-c-page__sidebar" .entries=${this.sidebarItems}></ak-sidebar>
`;
}
}

View File

@ -6,40 +6,33 @@ export function RenderFlowOption(flow: Flow): string {
return `${flow.slug} (${flow.name})`;
}
type Pair = [FlowDesignationEnum, string];
export const flowDesignationTable: Pair[] = [
[FlowDesignationEnum.Authentication, msg("Authentication")],
[FlowDesignationEnum.Authorization, msg("Authorization")],
[FlowDesignationEnum.Enrollment, msg("Enrollment")],
[FlowDesignationEnum.Invalidation, msg("Invalidation")],
[FlowDesignationEnum.Recovery, msg("Recovery")],
[FlowDesignationEnum.StageConfiguration, msg("Stage Configuration")],
[FlowDesignationEnum.Unenrollment, msg("Unenrollment")],
]
// prettier-ignore
const flowDesignations = new Map(flowDesignationTable);
export function DesignationToLabel(designation: FlowDesignationEnum): string {
switch (designation) {
case FlowDesignationEnum.Authentication:
return msg("Authentication");
case FlowDesignationEnum.Authorization:
return msg("Authorization");
case FlowDesignationEnum.Enrollment:
return msg("Enrollment");
case FlowDesignationEnum.Invalidation:
return msg("Invalidation");
case FlowDesignationEnum.Recovery:
return msg("Recovery");
case FlowDesignationEnum.StageConfiguration:
return msg("Stage Configuration");
case FlowDesignationEnum.Unenrollment:
return msg("Unenrollment");
case FlowDesignationEnum.UnknownDefaultOpenApi:
return msg("Unknown designation");
}
return flowDesignations.get(designation) ?? msg("Unknown designation");
}
const layoutToLabel = new Map([
[LayoutEnum.Stacked, msg("Stacked")],
[LayoutEnum.ContentLeft, msg("Content left")],
[LayoutEnum.ContentRight, msg("Content right")],
[LayoutEnum.SidebarLeft, msg("Sidebar left")],
[LayoutEnum.SidebarRight, msg("Sidebar right")],
]);
export function LayoutToLabel(layout: LayoutEnum): string {
switch (layout) {
case LayoutEnum.Stacked:
return msg("Stacked");
case LayoutEnum.ContentLeft:
return msg("Content left");
case LayoutEnum.ContentRight:
return msg("Content right");
case LayoutEnum.SidebarLeft:
return msg("Sidebar left");
case LayoutEnum.SidebarRight:
return msg("Sidebar right");
case LayoutEnum.UnknownDefaultOpenApi:
return msg("Unknown layout");
}
return layoutToLabel.get(layout) ?? msg("Unknown layout");
}

View File

@ -2,6 +2,8 @@ import { msg } from "@lit/localize";
import { Device, EventActions, IntentEnum, SeverityEnum, UserTypeEnum } from "@goauthentik/api";
type Pair<T> = [T, string];
/* Various tables in the API for which we need to supply labels */
export const intentEnumToLabel = new Map<IntentEnum, string>([
@ -14,7 +16,7 @@ export const intentEnumToLabel = new Map<IntentEnum, string>([
export const intentToLabel = (intent: IntentEnum) => intentEnumToLabel.get(intent);
export const eventActionToLabel = new Map<EventActions | undefined, string>([
export const eventActionLabels: Pair<EventActions>[] = [
[EventActions.Login, msg("Login")],
[EventActions.LoginFailed, msg("Failed login")],
[EventActions.Logout, msg("Logout")],
@ -43,7 +45,9 @@ export const eventActionToLabel = new Map<EventActions | undefined, string>([
[EventActions.ModelDeleted, msg("Model deleted")],
[EventActions.EmailSent, msg("Email sent")],
[EventActions.UpdateAvailable, msg("Update available")],
]);
]
export const eventActionToLabel = new Map<EventActions | undefined, string>(eventActionLabels);
export const actionToLabel = (action?: EventActions): string =>
eventActionToLabel.get(action) ?? action ?? "";

View File

@ -69,6 +69,12 @@ export class SidebarItems extends AKElement {
max-height: 82px;
margin-bottom: -0.5rem;
}
.pf-c-nav__toggle {
width: calc(
var(--pf-c-nav__toggle--FontSize) + calc(2 * var(--pf-global--spacer--md))
);
}
nav {
display: flex;
flex-direction: column;
@ -86,6 +92,17 @@ export class SidebarItems extends AKElement {
--pf-c-nav__link--PaddingRight: 0.5rem;
--pf-c-nav__link--PaddingBottom: 0.5rem;
}
.pf-c-nav__link a {
flex: 1 0 max-content;
color: var(--pf-c-nav__link--Color);
}
a.pf-c-nav__link:hover {
color: var(--pf-c-nav__link--Color);
text-decoration: var(--pf-global--link--TextDecoration--hover);
}
.pf-c-nav__section-title {
font-size: 12px;
}