mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
web: Fix user list default paths. (#23062)
This commit is contained in:
@@ -98,9 +98,12 @@ export class UserListPage extends WithLicenseSummary(
|
|||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
public order = "-last_login";
|
public order = "-last_login";
|
||||||
|
|
||||||
@property({ type: String, useDefault: true })
|
@property({ type: String, attribute: "active-path", useDefault: true })
|
||||||
public activePath: string = DefaultUIConfig.defaults.userPath;
|
public activePath: string = DefaultUIConfig.defaults.userPath;
|
||||||
|
|
||||||
|
@property({ type: String, attribute: "default-active-path", useDefault: true })
|
||||||
|
public defaultActivePath: string = DefaultUIConfig.defaults.userPath;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
protected hideDeactivated = getURLParam<boolean>("hideDeactivated", false);
|
protected hideDeactivated = getURLParam<boolean>("hideDeactivated", false);
|
||||||
|
|
||||||
@@ -109,18 +112,39 @@ export class UserListPage extends WithLicenseSummary(
|
|||||||
|
|
||||||
protected canImpersonate = false;
|
protected canImpersonate = false;
|
||||||
|
|
||||||
public override connectedCallback(): void {
|
//#region Lifecycle
|
||||||
super.connectedCallback();
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronizes `activePath` and `defaultActivePath` from three sources in priority order:
|
||||||
|
*
|
||||||
|
* 1. URL param (explicit navigation)
|
||||||
|
* 2. Brand default user path (admin-configured override)
|
||||||
|
* 3. Compiled-in `DefaultUIConfig` default (fallback)
|
||||||
|
*
|
||||||
|
* `activePath` is set to `""` (show all users) when neither a URL param nor a
|
||||||
|
* brand-level override is present, avoiding silent list filtering.
|
||||||
|
* `defaultActivePath` always resolves to a value via the fallback chain.
|
||||||
|
*/
|
||||||
|
protected synchronizeUserPaths(): void {
|
||||||
this.canImpersonate = this.can(CapabilitiesEnum.CanImpersonate);
|
this.canImpersonate = this.can(CapabilitiesEnum.CanImpersonate);
|
||||||
|
|
||||||
const initialDefaultUserPath = DefaultUIConfig.defaults.userPath;
|
const initialDefaultUserPath = DefaultUIConfig.defaults.userPath;
|
||||||
const brandDefaultUserPath = this.uiConfig.defaults.userPath;
|
const brandDefaultUserPath = this.uiConfig.defaults.userPath;
|
||||||
|
const defaultUserPath = brandDefaultUserPath || initialDefaultUserPath;
|
||||||
|
const userPathParam = getURLParam<string>("path", "");
|
||||||
|
|
||||||
this.activePath = getURLParam<string>(
|
const pathPresent =
|
||||||
"path",
|
(userPathParam && userPathParam !== "") || defaultUserPath !== initialDefaultUserPath;
|
||||||
brandDefaultUserPath || initialDefaultUserPath,
|
|
||||||
);
|
const resolvedUserPath = pathPresent ? userPathParam || defaultUserPath : "";
|
||||||
|
|
||||||
|
this.activePath = resolvedUserPath;
|
||||||
|
this.defaultActivePath = userPathParam || defaultUserPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override connectedCallback(): void {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.synchronizeUserPaths();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async apiEndpoint(): Promise<PaginatedResponse<User>> {
|
protected override async apiEndpoint(): Promise<PaginatedResponse<User>> {
|
||||||
@@ -138,6 +162,17 @@ export class UserListPage extends WithLicenseSummary(
|
|||||||
return users;
|
return users;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region Event Listeners
|
||||||
|
|
||||||
|
protected treeViewRefreshListener = (ev: CustomEvent<{ path: string }>) => {
|
||||||
|
this.activePath = ev.detail.path;
|
||||||
|
this.defaultActivePath = ev.detail.path;
|
||||||
|
};
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
protected buildExportParams = async (): Promise<CoreUsersExportCreateRequest> => {
|
protected buildExportParams = async (): Promise<CoreUsersExportCreateRequest> => {
|
||||||
return {
|
return {
|
||||||
...(await this.defaultEndpointConfig()),
|
...(await this.defaultEndpointConfig()),
|
||||||
@@ -346,15 +381,15 @@ export class UserListPage extends WithLicenseSummary(
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected renderObjectCreate(): SlottedTemplateResult {
|
protected renderObjectCreate(): SlottedTemplateResult {
|
||||||
const { activePath } = this;
|
const { defaultActivePath } = this;
|
||||||
|
|
||||||
return guard([activePath], () => {
|
return guard([defaultActivePath], () => {
|
||||||
return [
|
return [
|
||||||
html`<button
|
html`<button
|
||||||
class="pf-c-button pf-m-primary"
|
class="pf-c-button pf-m-primary"
|
||||||
type="button"
|
type="button"
|
||||||
${modalInvoker(AKUserWizard, {
|
${modalInvoker(AKUserWizard, {
|
||||||
defaultPath: activePath,
|
defaultPath: defaultActivePath,
|
||||||
})}
|
})}
|
||||||
aria-description=${msg("Open the new user wizard")}
|
aria-description=${msg("Open the new user wizard")}
|
||||||
>
|
>
|
||||||
@@ -383,10 +418,8 @@ export class UserListPage extends WithLicenseSummary(
|
|||||||
<ak-treeview
|
<ak-treeview
|
||||||
label=${msg("User paths")}
|
label=${msg("User paths")}
|
||||||
.items=${this.userPaths?.paths || []}
|
.items=${this.userPaths?.paths || []}
|
||||||
activePath=${this.activePath}
|
default-active-path=${this.activePath}
|
||||||
@ak-refresh=${(ev: CustomEvent<{ path: string }>) => {
|
@ak-refresh=${this.treeViewRefreshListener}
|
||||||
this.activePath = ev.detail.path;
|
|
||||||
}}
|
|
||||||
></ak-treeview>
|
></ak-treeview>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export const DefaultUIConfig = {
|
|||||||
},
|
},
|
||||||
locale: "",
|
locale: "",
|
||||||
defaults: {
|
defaults: {
|
||||||
userPath: "",
|
userPath: "users",
|
||||||
},
|
},
|
||||||
} as const satisfies UIConfig;
|
} as const satisfies UIConfig;
|
||||||
|
|
||||||
|
|||||||
+117
-102
@@ -2,16 +2,19 @@ import { EVENT_REFRESH } from "#common/constants";
|
|||||||
|
|
||||||
import { AKElement } from "#elements/Base";
|
import { AKElement } from "#elements/Base";
|
||||||
import { setURLParams } from "#elements/router/RouteMatch";
|
import { setURLParams } from "#elements/router/RouteMatch";
|
||||||
|
import { SlottedTemplateResult } from "#elements/types";
|
||||||
import { ifPresent } from "#elements/utils/attributes";
|
import { ifPresent } from "#elements/utils/attributes";
|
||||||
|
|
||||||
import { msg, str } from "@lit/localize";
|
import { msg, str } from "@lit/localize";
|
||||||
import { CSSResult, html, nothing, TemplateResult } from "lit";
|
import { CSSResult, html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
import PFTreeView from "@patternfly/patternfly/components/TreeView/tree-view.css";
|
import PFTreeView from "@patternfly/patternfly/components/TreeView/tree-view.css";
|
||||||
|
|
||||||
|
//#region Tree View Node
|
||||||
|
|
||||||
export interface TreeViewItem {
|
export interface TreeViewItem {
|
||||||
id?: string;
|
id: string | null;
|
||||||
label: string;
|
label: string;
|
||||||
childItems: TreeViewItem[];
|
childItems: TreeViewItem[];
|
||||||
parent?: TreeViewItem;
|
parent?: TreeViewItem;
|
||||||
@@ -21,33 +24,35 @@ export interface TreeViewItem {
|
|||||||
@customElement("ak-treeview-node")
|
@customElement("ak-treeview-node")
|
||||||
export class TreeViewNode extends AKElement {
|
export class TreeViewNode extends AKElement {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
item?: TreeViewItem;
|
public item: TreeViewItem | null = null;
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
open = false;
|
public open = false;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
host?: TreeView;
|
public host: TreeView | null = null;
|
||||||
|
|
||||||
@property()
|
@property({ type: String, attribute: "active-path" })
|
||||||
activePath = "";
|
public activePath = "";
|
||||||
|
|
||||||
@property()
|
@property({ type: String })
|
||||||
separator = "";
|
public separator = "";
|
||||||
|
|
||||||
get openable(): boolean {
|
public get openable(): boolean {
|
||||||
return (this.item?.childItems || []).length > 0;
|
return (this.item?.childItems || []).length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
get fullPath(): string {
|
public get fullPath(): string {
|
||||||
const pathItems = [];
|
const pathItems = [];
|
||||||
let item = this.item;
|
let item = this.item;
|
||||||
|
|
||||||
while (item) {
|
while (item) {
|
||||||
if (item.id) {
|
if (item.id) {
|
||||||
pathItems.push(item.id);
|
pathItems.push(item.id);
|
||||||
}
|
}
|
||||||
item = item.parent;
|
item = item.parent || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return pathItems.reverse().join(this.separator);
|
return pathItems.reverse().join(this.separator);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +60,7 @@ export class TreeViewNode extends AKElement {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
firstUpdated(): void {
|
protected override firstUpdated(): void {
|
||||||
const pathSegments = this.activePath.split(this.separator);
|
const pathSegments = this.activePath.split(this.separator);
|
||||||
const level = this.item?.level || 0;
|
const level = this.item?.level || 0;
|
||||||
// Ignore the last item as that shouldn't be expanded
|
// Ignore the last item as that shouldn't be expanded
|
||||||
@@ -63,7 +68,7 @@ export class TreeViewNode extends AKElement {
|
|||||||
if (pathSegments[level] === this.item?.id) {
|
if (pathSegments[level] === this.item?.id) {
|
||||||
this.open = true;
|
this.open = true;
|
||||||
}
|
}
|
||||||
if (this.activePath === this.fullPath && this.host !== undefined) {
|
if (this.activePath === this.fullPath && this.host) {
|
||||||
this.host.activeNode = this;
|
this.host.activeNode = this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,85 +89,87 @@ export class TreeViewNode extends AKElement {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
render(): TemplateResult {
|
protected override render(): SlottedTemplateResult {
|
||||||
const shouldRenderChildren = (this.item?.childItems || []).length > 0 && this.open;
|
const shouldRenderChildren = (this.item?.childItems || []).length > 0 && this.open;
|
||||||
const itemLabel = this.item?.label || msg("Unnamed");
|
const itemLabel = this.item?.label || msg("Unnamed");
|
||||||
const current = this.host?.activeNode === this;
|
const current = this.host?.activeNode === this;
|
||||||
|
|
||||||
return html`
|
return html`<li
|
||||||
<li
|
class="pf-c-tree-view__list-item ${this.open ? "pf-m-expanded" : ""}"
|
||||||
class="pf-c-tree-view__list-item ${this.open ? "pf-m-expanded" : ""}"
|
role="treeitem"
|
||||||
role="treeitem"
|
aria-expanded=${ifPresent(this.openable, this.open ? "true" : "false")}
|
||||||
aria-expanded=${ifPresent(this.openable, this.open ? "true" : "false")}
|
aria-label=${itemLabel}
|
||||||
aria-label=${itemLabel}
|
aria-selected=${current ? "true" : "false"}
|
||||||
aria-selected=${current ? "true" : "false"}
|
tabindex="0"
|
||||||
tabindex="0"
|
>
|
||||||
>
|
<div class="pf-c-tree-view__content">
|
||||||
<div class="pf-c-tree-view__content">
|
<div
|
||||||
<div
|
class="pf-c-tree-view__node ${current ? "pf-m-current" : ""}"
|
||||||
class="pf-c-tree-view__node ${current ? "pf-m-current" : ""}"
|
@click=${this.#selectionListener}
|
||||||
@click=${this.#selectionListener}
|
>
|
||||||
>
|
<div class="pf-c-tree-view__node-container">
|
||||||
<div class="pf-c-tree-view__node-container">
|
${this.openable
|
||||||
${this.openable
|
? html` <button
|
||||||
? html` <button
|
type="button"
|
||||||
type="button"
|
aria-label=${ifPresent(
|
||||||
aria-label=${ifPresent(
|
this.openable,
|
||||||
this.openable,
|
this.open
|
||||||
this.open
|
? msg(str`Collapse "${itemLabel}"`)
|
||||||
? msg(str`Collapse "${itemLabel}"`)
|
: msg(str`Expand "${itemLabel}"`),
|
||||||
: msg(str`Expand "${itemLabel}"`),
|
)}
|
||||||
)}
|
class="pf-c-tree-view__node-toggle"
|
||||||
class="pf-c-tree-view__node-toggle"
|
@click=${(e: Event) => {
|
||||||
@click=${(e: Event) => {
|
if (this.openable) {
|
||||||
if (this.openable) {
|
this.open = !this.open;
|
||||||
this.open = !this.open;
|
e.stopPropagation();
|
||||||
e.stopPropagation();
|
}
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<span class="pf-c-tree-view__node-toggle-icon">
|
||||||
<span class="pf-c-tree-view__node-toggle-icon">
|
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
</span>
|
||||||
</span>
|
</button>`
|
||||||
</button>`
|
: null}
|
||||||
: nothing}
|
<span class="pf-c-tree-view__node-icon">
|
||||||
<span class="pf-c-tree-view__node-icon">
|
<i
|
||||||
<i
|
class="fas ${this.open ? "fa-folder-open" : "fa-folder"}"
|
||||||
class="fas ${this.open ? "fa-folder-open" : "fa-folder"}"
|
aria-hidden="true"
|
||||||
aria-hidden="true"
|
></i>
|
||||||
></i>
|
</span>
|
||||||
</span>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
aria-label=${msg(str`Select "${itemLabel}"`)}
|
||||||
aria-label=${msg(str`Select "${itemLabel}"`)}
|
@click=${this.#selectionListener}
|
||||||
@click=${this.#selectionListener}
|
class="pf-c-tree-view__node-text"
|
||||||
class="pf-c-tree-view__node-text"
|
>
|
||||||
>
|
${itemLabel}
|
||||||
${itemLabel}
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
</div>
|
||||||
class="pf-c-tree-view__list"
|
<ul
|
||||||
?hidden=${!shouldRenderChildren}
|
class="pf-c-tree-view__list"
|
||||||
role="group"
|
?hidden=${!shouldRenderChildren}
|
||||||
aria-label=${msg(str`Items of "${itemLabel}"`)}
|
role="group"
|
||||||
>
|
aria-label=${msg(str`Items of "${itemLabel}"`)}
|
||||||
${this.item?.childItems.map((item) => {
|
>
|
||||||
return html`<ak-treeview-node
|
${this.item?.childItems.map((item) => {
|
||||||
.item=${item}
|
return html`<ak-treeview-node
|
||||||
activePath=${this.activePath}
|
.item=${item}
|
||||||
separator=${this.separator}
|
active-path=${this.activePath}
|
||||||
.host=${this.host}
|
separator=${this.separator}
|
||||||
></ak-treeview-node>`;
|
.host=${this.host}
|
||||||
})}
|
></ak-treeview-node>`;
|
||||||
</ul>
|
})}
|
||||||
</li>
|
</ul>
|
||||||
`;
|
</li> `;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region Tree View
|
||||||
|
|
||||||
@customElement("ak-treeview")
|
@customElement("ak-treeview")
|
||||||
export class TreeView extends AKElement {
|
export class TreeView extends AKElement {
|
||||||
static styles: CSSResult[] = [PFTreeView];
|
static styles: CSSResult[] = [PFTreeView];
|
||||||
@@ -171,61 +178,67 @@ export class TreeView extends AKElement {
|
|||||||
public label: string | null = null;
|
public label: string | null = null;
|
||||||
|
|
||||||
@property({ type: Array })
|
@property({ type: Array })
|
||||||
items: string[] = [];
|
public items: string[] = [];
|
||||||
|
|
||||||
@property()
|
@property({ type: String, attribute: "default-active-path", useDefault: true })
|
||||||
activePath = "";
|
public defaultActivePath = "";
|
||||||
|
|
||||||
@state()
|
@property({ attribute: false })
|
||||||
activeNode?: TreeViewNode;
|
public activeNode: TreeViewNode | null = null;
|
||||||
|
|
||||||
separator = "/";
|
protected separator = "/";
|
||||||
|
|
||||||
|
public createNode(path: string[], parentItem: TreeViewItem, level: number): TreeViewItem {
|
||||||
|
const id = path.shift() || null;
|
||||||
|
const idx = parentItem.childItems.findIndex((item) => item.id === id);
|
||||||
|
|
||||||
createNode(path: string[], parentItem: TreeViewItem, level: number): TreeViewItem {
|
|
||||||
const id = path.shift();
|
|
||||||
const idx = parentItem.childItems.findIndex((e: TreeViewItem) => {
|
|
||||||
return e.id === id;
|
|
||||||
});
|
|
||||||
if (idx < 0) {
|
if (idx < 0) {
|
||||||
const item: TreeViewItem = {
|
const item: TreeViewItem = {
|
||||||
id: id,
|
id,
|
||||||
label: id || "",
|
label: id || "",
|
||||||
childItems: [],
|
childItems: [],
|
||||||
level: level,
|
level: level,
|
||||||
parent: parentItem,
|
parent: parentItem,
|
||||||
};
|
};
|
||||||
|
|
||||||
parentItem.childItems.push(item);
|
parentItem.childItems.push(item);
|
||||||
if (path.length !== 0) {
|
|
||||||
|
if (path.length) {
|
||||||
const child = this.createNode(path, item, level + 1);
|
const child = this.createNode(path, item, level + 1);
|
||||||
child.parent = item;
|
child.parent = item;
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.createNode(path, parentItem.childItems[idx], level + 1);
|
return this.createNode(path, parentItem.childItems[idx], level + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(data: string[]): TreeViewItem {
|
protected parse(data: string[]): TreeViewItem {
|
||||||
const rootItem: TreeViewItem = {
|
const rootItem: TreeViewItem = {
|
||||||
id: undefined,
|
id: null,
|
||||||
label: msg("Root"),
|
label: msg("Root"),
|
||||||
childItems: [],
|
childItems: [],
|
||||||
level: -1,
|
level: -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
const path: string = data[i];
|
const path: string = data[i];
|
||||||
const split: string[] = path.split(this.separator);
|
const split: string[] = path.split(this.separator);
|
||||||
|
|
||||||
this.createNode(split, rootItem, 0);
|
this.createNode(split, rootItem, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return rootItem;
|
return rootItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
protected override render(): SlottedTemplateResult {
|
||||||
const rootItem = this.parse(this.items);
|
const rootItem = this.parse(this.items);
|
||||||
|
|
||||||
return html`<div class="pf-c-tree-view pf-m-guides">
|
return html`<div class="pf-c-tree-view pf-m-guides">
|
||||||
<ul class="pf-c-tree-view__list" role="tree" aria-label=${ifPresent(this.label)}>
|
<ul class="pf-c-tree-view__list" role="tree" aria-label=${ifPresent(this.label)}>
|
||||||
<ak-treeview-node
|
<ak-treeview-node
|
||||||
.item=${rootItem}
|
.item=${rootItem}
|
||||||
activePath=${this.activePath}
|
active-path=${this.defaultActivePath}
|
||||||
open
|
open
|
||||||
separator=${this.separator}
|
separator=${this.separator}
|
||||||
.host=${this}
|
.host=${this}
|
||||||
@@ -235,6 +248,8 @@ export class TreeView extends AKElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"ak-treeview": TreeView;
|
"ak-treeview": TreeView;
|
||||||
|
|||||||
Reference in New Issue
Block a user