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:
parent
3c277f14c8
commit
ff78f2f00a
|
@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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 ?? "";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Reference in a new issue