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 { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route";
|
import { ID_REGEX, SLUG_REGEX, UUID_REGEX } from "@goauthentik/elements/router/Route";
|
||||||
import "@goauthentik/elements/sidebar/Sidebar";
|
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 { getRootStyle } from "@goauthentik/elements/utils/getRootStyle";
|
||||||
|
|
||||||
import { consume } from "@lit-labs/context";
|
import { consume } from "@lit-labs/context";
|
||||||
import { msg, str } from "@lit/localize";
|
import { msg, str } from "@lit/localize";
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
|
import { eventActionLabels } from "@goauthentik/common/labels";
|
||||||
|
|
||||||
import { ProvidersApi, TypeCreate } from "@goauthentik/api";
|
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 type { Config, SessionUser, UserSelf } from "@goauthentik/api";
|
||||||
|
|
||||||
|
import { flowDesignationTable } from "../flows/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AdminSidebar
|
* AdminSidebar
|
||||||
*
|
*
|
||||||
|
@ -24,7 +41,7 @@ import type { Config, SessionUser, UserSelf } from "@goauthentik/api";
|
||||||
* it. Rendering decisions are left to the sidebar itself.
|
* it. Rendering decisions are left to the sidebar itself.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type LocalSidebarEntry = [
|
export type LocalSidebarEntry = [
|
||||||
string | SidebarEventHandler | null,
|
string | SidebarEventHandler | null,
|
||||||
string,
|
string,
|
||||||
(SidebarAttributes | string[] | null)?, // eslint-disable-line
|
(SidebarAttributes | string[] | null)?, // eslint-disable-line
|
||||||
|
@ -34,12 +51,21 @@ type LocalSidebarEntry = [
|
||||||
const localToSidebarEntry = (l: LocalSidebarEntry): SidebarEntry => ({
|
const localToSidebarEntry = (l: LocalSidebarEntry): SidebarEntry => ({
|
||||||
path: l[0],
|
path: l[0],
|
||||||
label: l[1],
|
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) } : {}),
|
...(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")
|
@customElement("ak-admin-sidebar")
|
||||||
export class AkAdminSidebar extends AKElement {
|
export class AkAdminSidebar extends AKElement {
|
||||||
|
@consume({ context: authentikConfigContext })
|
||||||
|
public config!: Config;
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true })
|
@property({ type: Boolean, reflect: true })
|
||||||
open = true;
|
open = true;
|
||||||
|
|
||||||
|
@ -52,8 +78,20 @@ export class AkAdminSidebar extends AKElement {
|
||||||
@state()
|
@state()
|
||||||
providerTypes: TypeCreate[] = [];
|
providerTypes: TypeCreate[] = [];
|
||||||
|
|
||||||
@consume({ context: authentikConfigContext })
|
@state()
|
||||||
public config!: Config;
|
stageTypes: TypeCreate[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
mappingTypes: TypeCreate[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
sourceTypes: TypeCreate[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
policyTypes: TypeCreate[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
connectionTypes: TypeCreate[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
@ -66,6 +104,22 @@ export class AkAdminSidebar extends AKElement {
|
||||||
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((types) => {
|
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((types) => {
|
||||||
this.providerTypes = 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.toggleOpen = this.toggleOpen.bind(this);
|
||||||
this.checkWidth = this.checkWidth.bind(this);
|
this.checkWidth = this.checkWidth.bind(this);
|
||||||
}
|
}
|
||||||
|
@ -79,7 +133,9 @@ export class AkAdminSidebar extends AKElement {
|
||||||
checkWidth() {
|
checkWidth() {
|
||||||
// This works just fine, but it assumes that the `--ak-sidebar--minimum-auto-width` is in
|
// 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.
|
// 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;
|
this.open = window.innerWidth >= minWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,8 +189,21 @@ export class AkAdminSidebar extends AKElement {
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const providerTypes: LocalSidebarEntry[] = this.providerTypes.map((ptype) =>
|
const flowTypes: LocalSidebarEntry[] = flowDesignationTable.map(([_designation, label]) =>
|
||||||
([`/core/providers;${encodeURIComponent(JSON.stringify({ search: ptype.modelName.replace(/provider$/, "") }))}`, ptype.name]));
|
([`/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
|
// prettier-ignore
|
||||||
const localSidebar: LocalSidebarEntry[] = [
|
const localSidebar: LocalSidebarEntry[] = [
|
||||||
|
@ -150,36 +219,38 @@ export class AkAdminSidebar extends AKElement {
|
||||||
["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`], providerTypes],
|
["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`], providerTypes],
|
||||||
["/outpost/outposts", msg("Outposts")]]],
|
["/outpost/outposts", msg("Outposts")]]],
|
||||||
[null, msg("Events"), null, [
|
[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/rules", msg("Notification Rules")],
|
||||||
["/events/transports", msg("Notification Transports")]]],
|
["/events/transports", msg("Notification Transports")]]],
|
||||||
[null, msg("Customisation"), null, [
|
[null, msg("Customisation"), null, [
|
||||||
["/policy/policies", msg("Policies")],
|
["/policy/policies", msg("Policies"), null, policyTypes],
|
||||||
["/core/property-mappings", msg("Property Mappings")],
|
["/core/property-mappings", msg("Property Mappings"), null, mappingTypes],
|
||||||
["/blueprints/instances", msg("Blueprints")],
|
["/blueprints/instances", msg("Blueprints")],
|
||||||
["/policy/reputation", msg("Reputation scores")]]],
|
["/policy/reputation", msg("Reputation scores")]]],
|
||||||
[null, msg("Flows and Stages"), null, [
|
[null, msg("Flows and Stages"), null, [
|
||||||
["/flow/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`]],
|
["/flow/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`], flowTypes],
|
||||||
["/flow/stages", msg("Stages")],
|
["/flow/stages", msg("Stages"), null, stageTypes],
|
||||||
["/flow/stages/prompts", msg("Prompts")]]],
|
["/flow/stages/prompts", msg("Prompts")]]],
|
||||||
[null, msg("Directory"), null, [
|
[null, msg("Directory"), null, [
|
||||||
["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]],
|
["/identity/users", msg("Users"), [`^/identity/users/(?<id>${ID_REGEX})$`]],
|
||||||
["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]],
|
["/identity/groups", msg("Groups"), [`^/identity/groups/(?<id>${UUID_REGEX})$`]],
|
||||||
["/identity/roles", msg("Roles"), [`^/identity/roles/(?<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")],
|
["/core/tokens", msg("Tokens and App passwords")],
|
||||||
["/flow/stages/invitations", msg("Invitations")]]],
|
["/flow/stages/invitations", msg("Invitations")]]],
|
||||||
[null, msg("System"), null, [
|
[null, msg("System"), null, [
|
||||||
["/core/tenants", msg("Tenants")],
|
["/core/tenants", msg("Tenants")],
|
||||||
["/crypto/certificates", msg("Certificates")],
|
["/crypto/certificates", msg("Certificates")],
|
||||||
["/outpost/integrations", msg("Outpost Integrations")]]],
|
["/outpost/integrations", msg("Outpost Integrations"), null, connectionTypes]]],
|
||||||
...(enterpriseMenu)
|
...(enterpriseMenu)
|
||||||
];
|
];
|
||||||
|
|
||||||
return localSidebar.map(localToSidebarEntry);
|
return localSidebar.map(localToSidebarEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
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})`;
|
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 {
|
export function DesignationToLabel(designation: FlowDesignationEnum): string {
|
||||||
switch (designation) {
|
return flowDesignations.get(designation) ?? msg("Unknown 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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
export function LayoutToLabel(layout: LayoutEnum): string {
|
||||||
switch (layout) {
|
return layoutToLabel.get(layout) ?? msg("Unknown 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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { msg } from "@lit/localize";
|
||||||
|
|
||||||
import { Device, EventActions, IntentEnum, SeverityEnum, UserTypeEnum } from "@goauthentik/api";
|
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 */
|
/* Various tables in the API for which we need to supply labels */
|
||||||
|
|
||||||
export const intentEnumToLabel = new Map<IntentEnum, string>([
|
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 intentToLabel = (intent: IntentEnum) => intentEnumToLabel.get(intent);
|
||||||
|
|
||||||
export const eventActionToLabel = new Map<EventActions | undefined, string>([
|
export const eventActionLabels: Pair<EventActions>[] = [
|
||||||
[EventActions.Login, msg("Login")],
|
[EventActions.Login, msg("Login")],
|
||||||
[EventActions.LoginFailed, msg("Failed login")],
|
[EventActions.LoginFailed, msg("Failed login")],
|
||||||
[EventActions.Logout, msg("Logout")],
|
[EventActions.Logout, msg("Logout")],
|
||||||
|
@ -43,7 +45,9 @@ export const eventActionToLabel = new Map<EventActions | undefined, string>([
|
||||||
[EventActions.ModelDeleted, msg("Model deleted")],
|
[EventActions.ModelDeleted, msg("Model deleted")],
|
||||||
[EventActions.EmailSent, msg("Email sent")],
|
[EventActions.EmailSent, msg("Email sent")],
|
||||||
[EventActions.UpdateAvailable, msg("Update available")],
|
[EventActions.UpdateAvailable, msg("Update available")],
|
||||||
]);
|
]
|
||||||
|
|
||||||
|
export const eventActionToLabel = new Map<EventActions | undefined, string>(eventActionLabels);
|
||||||
|
|
||||||
export const actionToLabel = (action?: EventActions): string =>
|
export const actionToLabel = (action?: EventActions): string =>
|
||||||
eventActionToLabel.get(action) ?? action ?? "";
|
eventActionToLabel.get(action) ?? action ?? "";
|
||||||
|
|
|
@ -69,6 +69,12 @@ export class SidebarItems extends AKElement {
|
||||||
max-height: 82px;
|
max-height: 82px;
|
||||||
margin-bottom: -0.5rem;
|
margin-bottom: -0.5rem;
|
||||||
}
|
}
|
||||||
|
.pf-c-nav__toggle {
|
||||||
|
width: calc(
|
||||||
|
var(--pf-c-nav__toggle--FontSize) + calc(2 * var(--pf-global--spacer--md))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -86,6 +92,17 @@ export class SidebarItems extends AKElement {
|
||||||
--pf-c-nav__link--PaddingRight: 0.5rem;
|
--pf-c-nav__link--PaddingRight: 0.5rem;
|
||||||
--pf-c-nav__link--PaddingBottom: 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 {
|
.pf-c-nav__section-title {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue