core: user paths (#3085)

* init

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add user_path_template

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add to sources and flow

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add outposts & api

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* dark theme for treeview

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add search

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add docs and tests for validation

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add to user write stage

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add web ui

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* web: improve error handling

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens L
2022-06-15 12:12:26 +02:00
committed by GitHub
parent c4b4c7134d
commit 1c62a3db6e
37 changed files with 771 additions and 127 deletions

View File

@ -1,4 +1,4 @@
import { CoreApi, SessionUser } from "@goauthentik/api";
import { CoreApi, ResponseError, SessionUser } from "@goauthentik/api";
import { activateLocale } from "../interfaces/locale";
import { DEFAULT_CONFIG } from "./Config";
@ -21,7 +21,7 @@ export function me(): Promise<SessionUser> {
activateLocale(locale);
}
return user;
}).catch((ex) => {
}).catch((ex: ResponseError) => {
const defaultUser: SessionUser = {
user: {
pk: -1,

View File

@ -277,6 +277,12 @@ html > form > input {
.pf-c-select__menu-item:hover {
--pf-c-select__menu-item--hover--BackgroundColor: var(--ak-dark-background-lighter);
}
.pf-c-select__menu-wrapper:focus-within,
.pf-c-select__menu-wrapper.pf-m-focus,
.pf-c-select__menu-item:focus,
.pf-c-select__menu-item.pf-m-focus {
--pf-c-select__menu-item--focus--BackgroundColor: var(--ak-dark-background-light-ish);
}
.pf-c-button.pf-m-plain:hover {
color: var(--ak-dark-foreground);
}
@ -395,6 +401,14 @@ html > form > input {
.pf-c-wizard__nav-link::before {
--pf-c-wizard__nav-link--before--BackgroundColor: transparent;
}
/* tree view */
.pf-c-tree-view__node:focus {
--pf-c-tree-view__node--focus--BackgroundColor: var(--ak-dark-background-light-ish);
}
.pf-c-tree-view__content:hover,
.pf-c-tree-view__content:focus-within {
--pf-c-tree-view__node--hover--BackgroundColor: var(--ak-dark-background-light-ish);
}
}
.pf-c-data-list__item {

View File

@ -0,0 +1,206 @@
import { t } from "@lingui/macro";
import { CSSResult, LitElement, TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import AKGlobal from "../authentik.css";
import PFTreeView from "@patternfly/patternfly/components/TreeView/tree-view.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { EVENT_REFRESH } from "../constants";
import { setURLParams } from "./router/RouteMatch";
export interface TreeViewItem {
id: string;
label: string;
childItems: TreeViewItem[];
parent?: TreeViewItem;
level: number;
}
@customElement("ak-treeview-node")
export class TreeViewNode extends LitElement {
@property({ attribute: false })
item?: TreeViewItem;
@property({ type: Boolean })
open = false;
@property({ attribute: false })
host?: TreeView;
@property()
path = "";
@property()
separator = "";
get openable(): boolean {
return (this.item?.childItems || []).length > 0;
}
get fullPath(): string {
const pathItems = [];
let item = this.item;
while (item) {
pathItems.push(item.id);
item = item.parent;
}
return pathItems.reverse().join(this.separator);
}
protected createRenderRoot(): Element {
return this;
}
firstUpdated(): void {
const pathSegments = this.path.split(this.separator);
const level = this.item?.level || 0;
// Ignore the last item as that shouldn't be expanded
pathSegments.pop();
if (pathSegments[level] == this.item?.id) {
this.open = true;
}
if (this.path === this.fullPath && this.host !== undefined) {
this.host.activeNode = this;
}
}
render(): TemplateResult {
const shouldRenderChildren = (this.item?.childItems || []).length > 0 && this.open;
return html`
<li
class="pf-c-tree-view__list-item ${this.open ? "pf-m-expanded" : ""}"
role="treeitem"
tabindex="0"
>
<div class="pf-c-tree-view__content">
<button
class="pf-c-tree-view__node ${this.host?.activeNode === this
? "pf-m-current"
: ""}"
@click=${() => {
if (this.host) {
this.host.activeNode = this;
}
setURLParams({ path: this.fullPath });
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
}}
>
<div class="pf-c-tree-view__node-container">
${this.openable
? html` <button
class="pf-c-tree-view__node-toggle"
@click=${(e: Event) => {
if (this.openable) {
this.open = !this.open;
e.stopPropagation();
}
}}
>
<span class="pf-c-tree-view__node-toggle-icon">
<i class="fas fa-angle-right" aria-hidden="true"></i>
</span>
</button>`
: html``}
<span class="pf-c-tree-view__node-icon">
<i
class="fas ${this.open ? "fa-folder-open" : "fa-folder"}"
aria-hidden="true"
></i>
</span>
<span class="pf-c-tree-view__node-text">${this.item?.label}</span>
</div>
</button>
</div>
<ul class="pf-c-tree-view__list" role="group" ?hidden=${!shouldRenderChildren}>
${this.item?.childItems.map((item) => {
return html`<ak-treeview-node
.item=${item}
path=${this.path}
separator=${this.separator}
.host=${this.host}
></ak-treeview-node>`;
})}
</ul>
</li>
`;
}
}
@customElement("ak-treeview")
export class TreeView extends LitElement {
static get styles(): CSSResult[] {
return [PFBase, PFTreeView, AKGlobal];
}
@property({ type: Array })
items: string[] = [];
@property()
path = "";
@state()
activeNode?: TreeViewNode;
separator = "/";
createNode(path: string[], tree: TreeViewItem[], level: number): TreeViewItem {
const id = path.shift();
const idx = tree.findIndex((e: TreeViewItem) => {
return e.id == id;
});
if (idx < 0) {
const item: TreeViewItem = {
id: id || "",
label: id || "",
childItems: [],
level: level,
};
tree.push(item);
if (path.length !== 0) {
const child = this.createNode(path, tree[tree.length - 1].childItems, level + 1);
child.parent = item;
}
return item;
} else {
return this.createNode(path, tree[idx].childItems, level + 1);
}
}
parse(data: string[]): TreeViewItem[] {
const tree: TreeViewItem[] = [];
for (let i = 0; i < data.length; i++) {
const path: string = data[i];
const split: string[] = path.split(this.separator);
this.createNode(split, tree, 0);
}
return tree;
}
render(): TemplateResult {
const result = this.parse(this.items);
return html`<div class="pf-c-tree-view pf-m-guides">
<ul class="pf-c-tree-view__list" role="tree">
<!-- @ts-ignore -->
<ak-treeview-node
.item=${{
id: "",
label: t`Root`,
childItems: result,
level: -1,
} as TreeViewItem}
path=${this.path}
?open=${true}
separator=${this.separator}
.host=${this}
></ak-treeview-node>
</ul>
</div>`;
}
}

View File

@ -1,7 +1,7 @@
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { CoreApi } from "@goauthentik/api";
import { CoreApi, ResponseError } from "@goauthentik/api";
import { DEFAULT_CONFIG } from "../../api/Config";
import { ERROR_CLASS, SECONDARY_CLASS, SUCCESS_CLASS } from "../../constants";
@ -37,15 +37,15 @@ export class TokenCopyButton extends ActionButton {
this.buttonClass = SUCCESS_CLASS;
return token.key;
})
.catch((err: Error | Response | undefined) => {
.catch((err: Error | ResponseError | undefined) => {
this.buttonClass = ERROR_CLASS;
if (err instanceof Error) {
if (!(err instanceof ResponseError)) {
setTimeout(() => {
this.buttonClass = SECONDARY_CLASS;
}, 1500);
throw err;
}
return err?.json().then((errResp) => {
return err.response.json().then((errResp) => {
setTimeout(() => {
this.buttonClass = SECONDARY_CLASS;
}, 1500);
@ -92,15 +92,15 @@ export class TokenCopyButton extends ActionButton {
this.setDone(SUCCESS_CLASS);
});
})
.catch((err: Response | Error) => {
if (err instanceof Error) {
.catch((err: ResponseError | Error) => {
if (!(err instanceof ResponseError)) {
showMessage({
level: MessageLevel.error,
message: err.message,
});
return;
}
return err?.json().then((errResp) => {
return err.response.json().then((errResp) => {
this.setDone(ERROR_CLASS);
throw new Error(errResp["detail"]);
});

View File

@ -14,7 +14,7 @@ import PFFormControl from "@patternfly/patternfly/components/FormControl/form-co
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { ValidationError } from "@goauthentik/api";
import { ResponseError, ValidationError } from "@goauthentik/api";
import { EVENT_REFRESH } from "../../constants";
import { showMessage } from "../../elements/messages/MessageContainer";
@ -209,13 +209,13 @@ export class Form<T> extends LitElement {
);
return r;
})
.catch(async (ex: Response | Error) => {
if (ex instanceof Error) {
.catch(async (ex: Error | ResponseError) => {
if (!(ex instanceof ResponseError)) {
throw ex;
}
let msg = ex.statusText;
if (ex.status > 399 && ex.status < 500) {
const errorMessage: ValidationError = await ex.json();
let msg = ex.response.statusText;
if (ex.response.status > 399 && ex.response.status < 500) {
const errorMessage: ValidationError = await ex.response.json();
if (!errorMessage) return errorMessage;
if (errorMessage instanceof Error) {
throw errorMessage;

View File

@ -22,6 +22,7 @@ import {
FlowsApi,
LayoutEnum,
RedirectChallenge,
ResponseError,
ShellChallenge,
} from "@goauthentik/api";
@ -193,7 +194,7 @@ export class FlowExecutor extends LitElement implements StageHost {
}
return true;
})
.catch((e: Error | Response) => {
.catch((e: Error | ResponseError) => {
this.errorMessage(e);
return false;
})
@ -226,7 +227,7 @@ export class FlowExecutor extends LitElement implements StageHost {
this.setBackground(this.challenge.flowInfo.background);
}
})
.catch((e: Error | Response) => {
.catch((e: Error | ResponseError) => {
// Catch JSON or Update errors
this.errorMessage(e);
})
@ -235,9 +236,11 @@ export class FlowExecutor extends LitElement implements StageHost {
});
}
async errorMessage(error: Error | Response): Promise<void> {
async errorMessage(error: Error | ResponseError): Promise<void> {
let body = "";
if (error instanceof Error) {
if (error instanceof ResponseError) {
body = await error.response.text();
} else if (error instanceof Error) {
body = error.message;
}
this.challenge = {

View File

@ -15,6 +15,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import {
PlexAuthenticationChallenge,
PlexAuthenticationChallengeResponseRequest,
ResponseError,
} from "@goauthentik/api";
import { SourcesApi } from "@goauthentik/api";
@ -48,8 +49,8 @@ export class PlexLoginInit extends BaseStage<
.then((r) => {
window.location.assign(r.to);
})
.catch((r: Response) => {
r.json().then((body: { detail: string }) => {
.catch((r: ResponseError) => {
r.response.json().then((body: { detail: string }) => {
showMessage({
level: MessageLevel.error,
message: body.detail,

View File

@ -12,7 +12,7 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { Flow, FlowsApi } from "@goauthentik/api";
import { Flow, FlowsApi, ResponseError } from "@goauthentik/api";
import { AndNext, DEFAULT_CONFIG } from "../../api/Config";
import "../../elements/PageHeader";
@ -164,10 +164,13 @@ export class FlowViewPage extends LitElement {
)}`;
window.open(finalURL, "_blank");
})
.catch((exc: Response) => {
.catch((exc: ResponseError) => {
// This request can return a HTTP 400 when a flow
// is not applicable.
window.open(exc.url, "_blank");
window.open(
exc.response.url,
"_blank",
);
});
}}
>

View File

@ -209,7 +209,7 @@ export class PolicyBindingForm extends ModelForm<PolicyBinding, string> {
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Group[]> => {
const args: CoreGroupsListRequest = {
ordering: "username",
ordering: "name",
};
if (query !== undefined) {
args.search = query;

View File

@ -7,7 +7,9 @@ import { until } from "lit/directives/until.js";
import {
CoreApi,
CoreGroupsListRequest,
CryptoApi,
Group,
LDAPSource,
LDAPSourceRequest,
PropertymappingsApi,
@ -15,6 +17,7 @@ import {
} from "@goauthentik/api";
import { DEFAULT_CONFIG } from "../../../api/Config";
import "../../../elements/SearchSelect";
import "../../../elements/forms/FormGroup";
import "../../../elements/forms/HorizontalFormElement";
import { ModelForm } from "../../../elements/forms/ModelForm";
@ -301,31 +304,49 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
<span slot="header"> ${t`Additional settings`} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${t`Group`} name="syncParentGroup">
<select class="pf-c-form-control">
<option
value=""
?selected=${this.instance?.syncParentGroup === undefined}
>
---------
</option>
${until(
new CoreApi(DEFAULT_CONFIG).coreGroupsList({}).then((groups) => {
return groups.results.map((group) => {
return html`<option
value=${ifDefined(group.pk)}
?selected=${this.instance?.syncParentGroup === group.pk}
>
${group.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
<!-- @ts-ignore -->
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Group[]> => {
const args: CoreGroupsListRequest = {
ordering: "name",
};
if (query !== undefined) {
args.search = query;
}
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(
args,
);
return groups.results;
}}
.renderElement=${(group: Group): string => {
return group.name;
}}
.value=${(group: Group | undefined): string | undefined => {
return group ? group.pk : undefined;
}}
.selected=${(group: Group): boolean => {
return group.pk === this.instance?.syncParentGroup;
}}
?blankable=${true}
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${t`Parent group for all the groups imported from LDAP.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`User path`} name="userPathTemplate">
<input
type="text"
value="${first(
this.instance?.userPathTemplate,
"goauthentik.io/sources/%(slug)s",
)}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Addition User DN`}
name="additionalUserDn"

View File

@ -268,6 +268,19 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
</option>
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`User path`} name="userPathTemplate">
<input
type="text"
value="${first(
this.instance?.userPathTemplate,
"goauthentik.io/sources/%(slug)s",
)}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
</p>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header"> ${t`Protocol settings`} </span>

View File

@ -215,6 +215,19 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
</option>
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`User path`} name="userPathTemplate">
<input
type="text"
value="${first(
this.instance?.userPathTemplate,
"goauthentik.io/sources/%(slug)s",
)}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
</p>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header"> ${t`Protocol settings`} </span>

View File

@ -232,6 +232,19 @@ export class SAMLSourceForm extends ModelForm<SAMLSource, string> {
</option>
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`User path`} name="userPathTemplate">
<input
type="text"
value="${first(
this.instance?.userPathTemplate,
"goauthentik.io/sources/%(slug)s",
)}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${t`Path template for users created. Use placeholders like \`%(slug)s\` to insert the source slug.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Delete temporary users after`}
?required=${true}

View File

@ -3,11 +3,11 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { CoreApi, StagesApi, UserWriteStage } from "@goauthentik/api";
import { CoreApi, CoreGroupsListRequest, Group, StagesApi, UserWriteStage } from "@goauthentik/api";
import { DEFAULT_CONFIG } from "../../../api/Config";
import "../../../elements/SearchSelect";
import "../../../elements/forms/FormGroup";
import "../../../elements/forms/HorizontalFormElement";
import { ModelForm } from "../../../elements/forms/ModelForm";
@ -74,29 +74,44 @@ export class UserWriteStageForm extends ModelForm<UserWriteStage, string> {
${t`Mark newly created users as inactive.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="userPathTemplate">
<input
type="text"
value="${first(this.instance?.userPathTemplate, "")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${t`Path new users will be created under.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Group`} name="createUsersGroup">
<select class="pf-c-form-control">
<option
value=""
?selected=${this.instance?.createUsersGroup === undefined}
>
---------
</option>
${until(
new CoreApi(DEFAULT_CONFIG).coreGroupsList({}).then((groups) => {
return groups.results.map((group) => {
return html`<option
value=${ifDefined(group.pk)}
?selected=${this.instance?.createUsersGroup ===
group.pk}
>
${group.name}
</option>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</select>
<!-- @ts-ignore -->
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Group[]> => {
const args: CoreGroupsListRequest = {
ordering: "name",
};
if (query !== undefined) {
args.search = query;
}
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(
args,
);
return groups.results;
}}
.renderElement=${(group: Group): string => {
return group.name;
}}
.value=${(group: Group | undefined): string | undefined => {
return group ? group.pk : undefined;
}}
.selected=${(group: Group): boolean => {
return group.pk === this.instance?.createUsersGroup;
}}
?blankable=${true}
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${t`Newly created users are added to this group, if a group is selected.`}
</p>

View File

@ -7,7 +7,7 @@ import { until } from "lit/directives/until.js";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import { CapabilitiesEnum, CoreApi, User } from "@goauthentik/api";
import { CapabilitiesEnum, CoreApi, ResponseError, User } from "@goauthentik/api";
import { AKResponse } from "../../api/Client";
import { DEFAULT_CONFIG, config, tenant } from "../../api/Config";
@ -244,8 +244,8 @@ export class RelatedUserList extends Table<User> {
description: rec.link,
});
})
.catch((ex: Response) => {
ex.json().then(() => {
.catch((ex: ResponseError) => {
ex.response.json().then(() => {
showMessage({
level: MessageLevel.error,
message: t`No recovery flow is configured.`,

View File

@ -69,6 +69,14 @@ export class UserForm extends ModelForm<User, number> {
${t`User's primary identifier. 150 characters or fewer.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Path`} ?required=${true} name="path">
<input
type="text"
value="${first(this.instance?.path, "users")}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Name`} name="name">
<input
type="text"

View File

@ -4,10 +4,12 @@ import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { until } from "lit/directives/until.js";
import AKGlobal from "../../authentik.css";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import { CapabilitiesEnum, CoreApi, User } from "@goauthentik/api";
import { CapabilitiesEnum, CoreApi, ResponseError, User } from "@goauthentik/api";
import { AKResponse } from "../../api/Client";
import { DEFAULT_CONFIG, config, tenant } from "../../api/Config";
@ -15,12 +17,13 @@ import { me } from "../../api/Users";
import { uiConfig } from "../../common/config";
import { PFColor } from "../../elements/Label";
import { PFSize } from "../../elements/Spinner";
import "../../elements/TreeView";
import "../../elements/buttons/ActionButton";
import "../../elements/forms/DeleteBulkForm";
import "../../elements/forms/ModalForm";
import { MessageLevel } from "../../elements/messages/Message";
import { showMessage } from "../../elements/messages/MessageContainer";
import { getURLParam, updateURLParams } from "../../elements/router/RouteMatch";
import { getURLParam } from "../../elements/router/RouteMatch";
import { TableColumn } from "../../elements/table/Table";
import { TablePage } from "../../elements/table/TablePage";
import { first } from "../../utils";
@ -51,11 +54,11 @@ export class UserListPage extends TablePage<User> {
@property()
order = "last_login";
@property({ type: Boolean })
hideServiceAccounts = getURLParam<boolean>("hideServiceAccounts", true);
@property()
path = getURLParam<string>("path", "/");
static get styles(): CSSResult[] {
return super.styles.concat(PFDescriptionList, PFAlert);
return super.styles.concat(PFDescriptionList, PFCard, PFAlert, AKGlobal);
}
async apiEndpoint(page: number): Promise<AKResponse<User>> {
@ -64,11 +67,7 @@ export class UserListPage extends TablePage<User> {
page: page,
pageSize: (await uiConfig()).pagination.perPage,
search: this.search || "",
attributes: this.hideServiceAccounts
? JSON.stringify({
"goauthentik.io/user/service-account__isnull": true,
})
: undefined,
pathStartswith: getURLParam("path", ""),
});
}
@ -251,8 +250,8 @@ export class UserListPage extends TablePage<User> {
description: rec.link,
});
})
.catch((ex: Response) => {
ex.json().then(() => {
.catch((ex: ResponseError) => {
ex.response.json().then(() => {
showMessage({
level: MessageLevel.error,
message: t`No recovery flow is configured.`,
@ -320,33 +319,25 @@ export class UserListPage extends TablePage<User> {
`;
}
renderToolbarAfter(): TemplateResult {
return html`&nbsp;
<div class="pf-c-toolbar__group pf-m-filter-group">
<div class="pf-c-toolbar__item pf-m-search-filter">
<div class="pf-c-input-group">
<div class="pf-c-check">
<input
class="pf-c-check__input"
type="checkbox"
id="hide-service-accounts"
name="hide-service-accounts"
?checked=${this.hideServiceAccounts}
@change=${() => {
this.hideServiceAccounts = !this.hideServiceAccounts;
this.page = 1;
this.fetch();
updateURLParams({
hideServiceAccounts: this.hideServiceAccounts,
});
}}
/>
<label class="pf-c-check__label" for="hide-service-accounts">
${t`Hide service-accounts`}
</label>
</div>
</div>
renderSidebarBefore(): TemplateResult {
return html`<div class="pf-c-sidebar__panel pf-m-width-25">
<div class="pf-c-card">
<div class="pf-c-card__title">${t`User folders`}</div>
<div class="pf-c-card__body">
${until(
new CoreApi(DEFAULT_CONFIG)
.coreUsersPathsRetrieve({
search: this.search,
})
.then((paths) => {
return html`<ak-treeview
.items=${paths.paths}
path=${this.path}
></ak-treeview>`;
}),
)}
</div>
</div>`;
</div>
</div>`;
}
}

View File

@ -18,6 +18,7 @@ import {
FlowChallengeResponseRequest,
FlowsApi,
RedirectChallenge,
ResponseError,
ShellChallenge,
} from "@goauthentik/api";
@ -80,7 +81,7 @@ export class UserSettingsFlowExecutor extends LitElement implements StageHost {
}
return true;
})
.catch((e: Error | Response) => {
.catch((e: Error | ResponseError) => {
this.errorMessage(e);
return false;
})