Streamline TypeCreate lists. This commit removes the highly repetitive definitions for each

of the TypeCreate objects and replaces them with a single generic ReactiveController, which it
then instantiates six times, but at least they're shorter!
This commit is contained in:
Ken Sternberg 2023-11-17 11:15:58 -08:00
parent d5875a597b
commit 5386f0f4c3
8 changed files with 168 additions and 82 deletions

View File

@ -1,5 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_SIDEBAR_TOGGLE, VERSION } from "@goauthentik/common/constants"; import { EVENT_SIDEBAR_TOGGLE, VERSION } from "@goauthentik/common/constants";
import { eventActionLabels } from "@goauthentik/common/labels";
import { me } from "@goauthentik/common/users"; import { me } from "@goauthentik/common/users";
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts"; import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
@ -16,35 +17,41 @@ 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 { 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"; import { flowDesignationTable } from "../flows/utils";
import ConnectionTypesController from "./SidebarEntries/ConnectionTypesController";
import PolicyTypesController from "./SidebarEntries/PolicyTypesController";
import PropertyMappingsController from "./SidebarEntries/PropertyMappingsController";
import ProviderTypesController from "./SidebarEntries/ProviderTypesController";
import SourceTypesController from "./SidebarEntries/SourceTypesController";
import StageTypesController from "./SidebarEntries/StageTypesController";
/** /**
* AdminSidebar * AdminSidebar
* *
* Encapsulates the logic for the administration sidebar: what to show and, initially, when to show * The AdminSidebar has two responsibilities:
* it. Rendering decisions are left to the sidebar itself. *
* 1. Control the styling of the sidebar host, specifically when to show it and whether to show
* it as an overlay or as a push.
* 2. Control what content the sidebar will receive. The sidebar takes a tree, maximally three deep,
* of type SidebarEventHandler.
*/ */
type SidebarUrl = string;
export type LocalSidebarEntry = [ export type LocalSidebarEntry = [
string | SidebarEventHandler | null, // - null: This entry is not a link.
// - string: the url for the entry
// - SidebarEventHandler: a function to run if the entry is clicked.
SidebarUrl | SidebarEventHandler | null,
// The visible text of the entry.
string, string,
// Attributes to which the sidebar responds. See the sidebar for details.
(SidebarAttributes | string[] | null)?, // eslint-disable-line (SidebarAttributes | string[] | null)?, // eslint-disable-line
// Children of the entry
LocalSidebarEntry[]?, LocalSidebarEntry[]?,
]; ];
@ -55,12 +62,6 @@ const localToSidebarEntry = (l: LocalSidebarEntry): SidebarEntry => ({
...(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 }) @consume({ context: authentikConfigContext })
@ -75,23 +76,12 @@ export class AkAdminSidebar extends AKElement {
@state() @state()
impersonation: UserSelf["username"] | null = null; impersonation: UserSelf["username"] | null = null;
@state() private connectionTypes = new ConnectionTypesController(this);
providerTypes: TypeCreate[] = []; private policyTypes = new PolicyTypesController(this);
private propertyMapper = new PropertyMappingsController(this);
@state() private providerTypes = new ProviderTypesController(this);
stageTypes: TypeCreate[] = []; private sourceTypes = new SourceTypesController(this);
private stageTypes = new StageTypesController(this);
@state()
mappingTypes: TypeCreate[] = [];
@state()
sourceTypes: TypeCreate[] = [];
@state()
policyTypes: TypeCreate[] = [];
@state()
connectionTypes: TypeCreate[] = [];
constructor() { constructor() {
super(); super();
@ -101,25 +91,6 @@ export class AkAdminSidebar extends AKElement {
me().then((user: SessionUser) => { me().then((user: SessionUser) => {
this.impersonation = user.original ? user.user.username : null; this.impersonation = user.original ? user.user.username : null;
}); });
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.toggleOpen = this.toggleOpen.bind(this);
this.checkWidth = this.checkWidth.bind(this); this.checkWidth = this.checkWidth.bind(this);
} }
@ -183,27 +154,21 @@ export class AkAdminSidebar extends AKElement {
? [[reload, msg(str`You're currently impersonating ${this.impersonation}. Click to stop.`)]] ? [[reload, msg(str`You're currently impersonating ${this.impersonation}. Click to stop.`)]]
: []; : [];
// prettier-ignore const enterpriseMenu: LocalSidebarEntry[] = this.config?.capabilities.includes(
const enterpriseMenu: LocalSidebarEntry[] = this.config?.capabilities.includes(CapabilitiesEnum.IsEnterprise) CapabilitiesEnum.IsEnterprise
)
? [[null, msg("Enterprise"), null, [["/enterprise/licenses", msg("Licenses")]]]] ? [[null, msg("Enterprise"), null, [["/enterprise/licenses", msg("Licenses")]]]]
: []; : [];
// prettier-ignore const flowTypes: LocalSidebarEntry[] = flowDesignationTable.map(([_designation, label]) => [
const flowTypes: LocalSidebarEntry[] = flowDesignationTable.map(([_designation, label]) => `/flow/flows;${encodeURIComponent(JSON.stringify({ search: label }))}`,
([`/flow/flows;${encodeURIComponent(JSON.stringify({ search: label }))}`, label])); label,
]);
const eventTypes: LocalSidebarEntry[] = eventActionLabels.map(([_action, label]) => [
const eventTypes: LocalSidebarEntry[] = eventActionLabels.map(([_action, label]) => `/events/log;${encodeURIComponent(JSON.stringify({ search: label }))}`,
([`/events/log;${encodeURIComponent(JSON.stringify({ search: label }))}`, 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[] = [
@ -216,32 +181,32 @@ export class AkAdminSidebar extends AKElement {
["/administration/system-tasks", msg("System Tasks")]]], ["/administration/system-tasks", msg("System Tasks")]]],
[null, msg("Applications"), null, [ [null, msg("Applications"), null, [
["/core/applications", msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]], ["/core/applications", msg("Applications"), [`^/core/applications/(?<slug>${SLUG_REGEX})$`]],
["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`], providerTypes], ["/core/providers", msg("Providers"), [`^/core/providers/(?<id>${ID_REGEX})$`], this.providerTypes.entries()],
["/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})$`], eventTypes], ["/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"), null, policyTypes], ["/policy/policies", msg("Policies"), null, this.policyTypes.entries()],
["/core/property-mappings", msg("Property Mappings"), null, mappingTypes], ["/core/property-mappings", msg("Property Mappings"), null, this.propertyMapper.entries()],
["/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})$`], flowTypes], ["/flow/flows", msg("Flows"), [`^/flow/flows/(?<slug>${SLUG_REGEX})$`], flowTypes],
["/flow/stages", msg("Stages"), null, stageTypes], ["/flow/stages", msg("Stages"), null, this.stageTypes.entries()],
["/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})$`], sourceTypes], ["/core/sources", msg("Federation and Social login"), [`^/core/sources/(?<slug>${SLUG_REGEX})$`], this.sourceTypes.entries()],
["/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"), null, connectionTypes]]], ["/outpost/integrations", msg("Outpost Integrations"), null, this.connectionTypes.entries()]]],
...(enterpriseMenu) ...(enterpriseMenu)
]; ];

View File

@ -0,0 +1,12 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { OutpostsApi } from "@goauthentik/api";
import { createTypesController } from "./GenericTypesController";
export const ConnectionTypesController = createTypesController(
() => new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsAllTypesList(),
"/outpost/integrations"
);
export default ConnectionTypesController;

View File

@ -0,0 +1,49 @@
import { ReactiveControllerHost } from "lit";
import { TypeCreate } from "@goauthentik/api";
import { LocalSidebarEntry } from "../AdminSidebar";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Fetcher = () => Promise<TypeCreate[]>;
const typeCreateToSidebar = (baseUrl: string, tcreate: TypeCreate[]): LocalSidebarEntry[] =>
tcreate.map((t) => [
`${baseUrl};${encodeURIComponent(JSON.stringify({ search: t.name }))}`,
t.name,
]);
/**
* createTypesController
*
* The Sidebar accesses a number objects of `TypeCreate`, which all have the exact same type, just
* different accessors for generating the lists and different paths to which they respond. This
* function is a template for a (simple) reactive controller that fetches the data for that type on
* construction, then informs the host that the data is available.
*/
/**
* TODO (2023-11-17): This function is unlikely to survive in this form. It would be nice if it were more
* generic, able to take a converter that can handle more that TypeCreate[] as its inbound argument,
* since we need to refine what's displayed and on what the search is conducted.
*
*/
export function createTypesController(fetch: Fetcher, path: string, converter = typeCreateToSidebar) {
return class GenericTypesController {
createTypes: TypeCreate[] = [];
host: ReactiveControllerHost;
constructor(host: ReactiveControllerHost) {
this.host = host;
fetch().then((types) => {
this.createTypes = types;
host.requestUpdate();
});
}
entries(): LocalSidebarEntry[] {
return converter(path, this.createTypes);
}
};
}
export default createTypesController;

View File

@ -0,0 +1,12 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PoliciesApi } from "@goauthentik/api";
import { createTypesController } from "./GenericTypesController";
export const PolicyTypesController = createTypesController(
() => new PoliciesApi(DEFAULT_CONFIG).policiesAllTypesList(),
"/policy/policies"
);
export default PolicyTypesController;

View File

@ -0,0 +1,12 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PropertymappingsApi } from "@goauthentik/api";
import { createTypesController } from "./GenericTypesController";
export const PropertyMappingsController = createTypesController(
() => new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsAllTypesList(),
"/core/property-mappings"
);
export default PropertyMappingsController;

View File

@ -0,0 +1,12 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { ProvidersApi } from "@goauthentik/api";
import { createTypesController } from "./GenericTypesController";
export const ProviderTypesController = createTypesController(
() => new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList(),
"/core/providers"
);
export default ProviderTypesController;

View File

@ -0,0 +1,12 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { SourcesApi } from "@goauthentik/api";
import { createTypesController } from "./GenericTypesController";
export const SourceTypesController = createTypesController(
() => new SourcesApi(DEFAULT_CONFIG).sourcesAllTypesList(),
"/core/sources"
);
export default SourceTypesController;

View File

@ -0,0 +1,12 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { StagesApi } from "@goauthentik/api";
import { createTypesController } from "./GenericTypesController";
export const StageTypesController = createTypesController(
() => new StagesApi(DEFAULT_CONFIG).stagesAllTypesList(),
"/flow/stages"
);
export default StageTypesController;