web: replace 'description-list' with list of descriptions (#7392)
* web: break circular dependency between AKElement & Interface. This commit changes the way the root node of the web application shell is discovered by child components, such that the base class shared by both no longer results in a circular dependency between the two models. I've run this in isolation and have seen no failures of discovery; the identity token exists as soon as the Interface is constructed and is found by every item on the page. * web: fix broken typescript references This built... and then it didn't? Anyway, the current fix is to provide type information the AkInterface for the data that consumers require. * web: description lists as functions One thing I hate is clutter. Just tell me what you're going to do. "Description Lists" in our code are renderings of Patternfly's DescriptionList; we use only four of their idioms: horizontal, compact, 2col, and 3col. With that in mind, I've stripped out the DescriptionList rendering code from UserViewPage and replaced it with a list of "Here's what to render" and a function call to render them. The calling code is still responsible for having the right styles available, as this is not a component or an attempt at isolation; it is *just* a function (at this point). * web: fix issue that prevented the classMap from being rendered properly * web: added comments to the description list. * web: analyze & prettier had opinions * web: Fix description-list demo This commit re-instals the demo for the "description list" of user fields. * web: prettier had opinions. * any -> unknown Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
b181c551a5
commit
026e80bd10
|
@ -14,6 +14,11 @@ import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||||
import { userTypeToLabel } from "@goauthentik/common/labels";
|
import { userTypeToLabel } from "@goauthentik/common/labels";
|
||||||
|
import "@goauthentik/components/DescriptionList";
|
||||||
|
import {
|
||||||
|
type DescriptionPair,
|
||||||
|
renderDescriptionList,
|
||||||
|
} from "@goauthentik/components/DescriptionList";
|
||||||
import "@goauthentik/components/ak-status-label";
|
import "@goauthentik/components/ak-status-label";
|
||||||
import "@goauthentik/components/events/ObjectChangelog";
|
import "@goauthentik/components/events/ObjectChangelog";
|
||||||
import "@goauthentik/components/events/UserEvents";
|
import "@goauthentik/components/events/UserEvents";
|
||||||
|
@ -137,165 +142,91 @@ export class UserViewPage extends AKElement {
|
||||||
|
|
||||||
const user = this.user;
|
const user = this.user;
|
||||||
|
|
||||||
const canImpersonate =
|
// prettier-ignore
|
||||||
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) &&
|
const userInfo: DescriptionPair[] = [
|
||||||
this.user.pk !== this.me?.user.pk;
|
[msg("Username"), user.username],
|
||||||
|
[msg("Name"), user.name],
|
||||||
|
[msg("Email"), user.email || "-"],
|
||||||
|
[msg("Last login"), user.lastLogin?.toLocaleString()],
|
||||||
|
[msg("Active"), html`<ak-status-label type="warning" ?good=${user.isActive}></ak-status-label>`],
|
||||||
|
[msg("Type"), userTypeToLabel(user.type)],
|
||||||
|
[msg("Superuser"), html`<ak-status-label type="warning" ?good=${user.isSuperuser}></ak-status-label>`],
|
||||||
|
[msg("Actions"), this.renderActionButtons(user)],
|
||||||
|
[msg("Recovery"), this.renderRecoveryButtons(user)],
|
||||||
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="pf-c-card__title">${msg("User Info")}</div>
|
<div class="pf-c-card__title">${msg("User Info")}</div>
|
||||||
<div class="pf-c-card__body">
|
<div class="pf-c-card__body">${renderDescriptionList(userInfo)}</div>
|
||||||
<dl class="pf-c-description-list">
|
`;
|
||||||
<div class="pf-c-description-list__group">
|
}
|
||||||
<dt class="pf-c-description-list__term">
|
|
||||||
<span class="pf-c-description-list__text">${msg("Username")}</span>
|
renderActionButtons(user: User) {
|
||||||
</dt>
|
const canImpersonate =
|
||||||
<dd class="pf-c-description-list__description">
|
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) &&
|
||||||
<div class="pf-c-description-list__text">${user.username}</div>
|
user.pk !== this.me?.user.pk;
|
||||||
</dd>
|
|
||||||
</div>
|
return html`<div class="ak-button-collection">
|
||||||
<div class="pf-c-description-list__group">
|
<ak-forms-modal>
|
||||||
<dt class="pf-c-description-list__term">
|
<span slot="submit"> ${msg("Update")} </span>
|
||||||
<span class="pf-c-description-list__text">${msg("Name")}</span>
|
<span slot="header"> ${msg("Update User")} </span>
|
||||||
</dt>
|
<ak-user-form slot="form" .instancePk=${user.pk}> </ak-user-form>
|
||||||
<dd class="pf-c-description-list__description">
|
<button slot="trigger" class="pf-m-primary pf-c-button pf-m-block">
|
||||||
<div class="pf-c-description-list__text">${user.name}</div>
|
${msg("Edit")}
|
||||||
</dd>
|
</button>
|
||||||
</div>
|
</ak-forms-modal>
|
||||||
<div class="pf-c-description-list__group">
|
<ak-user-active-form
|
||||||
<dt class="pf-c-description-list__term">
|
.obj=${user}
|
||||||
<span class="pf-c-description-list__text">${msg("Email")}</span>
|
objectLabel=${msg("User")}
|
||||||
</dt>
|
.delete=${() => {
|
||||||
<dd class="pf-c-description-list__description">
|
return new CoreApi(DEFAULT_CONFIG).coreUsersPartialUpdate({
|
||||||
<div class="pf-c-description-list__text">${user.email || "-"}</div>
|
id: user.pk,
|
||||||
</dd>
|
patchedUserRequest: {
|
||||||
</div>
|
isActive: !user.isActive,
|
||||||
<div class="pf-c-description-list__group">
|
},
|
||||||
<dt class="pf-c-description-list__term">
|
});
|
||||||
<span class="pf-c-description-list__text">${msg("Last login")}</span>
|
}}
|
||||||
</dt>
|
>
|
||||||
<dd class="pf-c-description-list__description">
|
<button slot="trigger" class="pf-c-button pf-m-warning pf-m-block">
|
||||||
<div class="pf-c-description-list__text">
|
<pf-tooltip
|
||||||
${user.lastLogin?.toLocaleString()}
|
position="top"
|
||||||
</div>
|
content=${user.isActive
|
||||||
</dd>
|
? msg("Lock the user out of this system")
|
||||||
</div>
|
: msg("Allow the user to log in and use this system")}
|
||||||
<div class="pf-c-description-list__group">
|
>
|
||||||
<dt class="pf-c-description-list__term">
|
${user.isActive ? msg("Deactivate") : msg("Activate")}
|
||||||
<span class="pf-c-description-list__text">${msg("Active")}</span>
|
</pf-tooltip>
|
||||||
</dt>
|
</button>
|
||||||
<dd class="pf-c-description-list__description">
|
</ak-user-active-form>
|
||||||
<div class="pf-c-description-list__text">
|
${canImpersonate
|
||||||
<ak-status-label
|
? html`
|
||||||
type="warning"
|
<ak-action-button
|
||||||
?good=${user.isActive}
|
class="pf-m-secondary pf-m-block"
|
||||||
></ak-status-label>
|
id="impersonate-user-button"
|
||||||
</div>
|
.apiRequest=${() => {
|
||||||
</dd>
|
return new CoreApi(DEFAULT_CONFIG)
|
||||||
</div>
|
.coreUsersImpersonateCreate({
|
||||||
<div class="pf-c-description-list__group">
|
id: user.pk,
|
||||||
<dt class="pf-c-description-list__term">
|
})
|
||||||
<span class="pf-c-description-list__text">${msg("Type")}</span>
|
.then(() => {
|
||||||
</dt>
|
window.location.href = "/";
|
||||||
<dd class="pf-c-description-list__description">
|
});
|
||||||
<div class="pf-c-description-list__text">
|
}}
|
||||||
${userTypeToLabel(user.type)}
|
>
|
||||||
</div>
|
<pf-tooltip
|
||||||
</dd>
|
position="top"
|
||||||
</div>
|
content=${msg("Temporarily assume the identity of this user")}
|
||||||
<div class="pf-c-description-list__group">
|
>
|
||||||
<dt class="pf-c-description-list__term">
|
${msg("Impersonate")}
|
||||||
<span class="pf-c-description-list__text">${msg("Superuser")}</span>
|
</pf-tooltip>
|
||||||
</dt>
|
</ak-action-button>
|
||||||
<dd class="pf-c-description-list__description">
|
`
|
||||||
<div class="pf-c-description-list__text">
|
: nothing}
|
||||||
<ak-status-label
|
</div> `;
|
||||||
type="warning"
|
}
|
||||||
?good=${user.isSuperuser}
|
|
||||||
></ak-status-label>
|
renderRecoveryButtons(user: User) {
|
||||||
</div>
|
return html`<div class="ak-button-collection">
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div class="pf-c-description-list__group">
|
|
||||||
<dt class="pf-c-description-list__term">
|
|
||||||
<span class="pf-c-description-list__text">${msg("Actions")}</span>
|
|
||||||
</dt>
|
|
||||||
<dd class="pf-c-description-list__description ak-button-collection">
|
|
||||||
<div class="pf-c-description-list__text">
|
|
||||||
<ak-forms-modal>
|
|
||||||
<span slot="submit"> ${msg("Update")} </span>
|
|
||||||
<span slot="header"> ${msg("Update User")} </span>
|
|
||||||
<ak-user-form slot="form" .instancePk=${user.pk}>
|
|
||||||
</ak-user-form>
|
|
||||||
<button
|
|
||||||
slot="trigger"
|
|
||||||
class="pf-m-primary pf-c-button pf-m-block"
|
|
||||||
>
|
|
||||||
${msg("Edit")}
|
|
||||||
</button>
|
|
||||||
</ak-forms-modal>
|
|
||||||
<ak-user-active-form
|
|
||||||
.obj=${user}
|
|
||||||
objectLabel=${msg("User")}
|
|
||||||
.delete=${() => {
|
|
||||||
return new CoreApi(DEFAULT_CONFIG).coreUsersPartialUpdate({
|
|
||||||
id: user.pk,
|
|
||||||
patchedUserRequest: {
|
|
||||||
isActive: !user.isActive,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
slot="trigger"
|
|
||||||
class="pf-c-button pf-m-warning pf-m-block"
|
|
||||||
>
|
|
||||||
<pf-tooltip
|
|
||||||
position="top"
|
|
||||||
content=${user.isActive
|
|
||||||
? msg("Lock the user out of this system")
|
|
||||||
: msg(
|
|
||||||
"Allow the user to log in and use this system",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
${user.isActive ? msg("Deactivate") : msg("Activate")}
|
|
||||||
</pf-tooltip>
|
|
||||||
</button>
|
|
||||||
</ak-user-active-form>
|
|
||||||
${canImpersonate
|
|
||||||
? html`
|
|
||||||
<ak-action-button
|
|
||||||
class="pf-m-secondary pf-m-block"
|
|
||||||
id="impersonate-user-button"
|
|
||||||
.apiRequest=${() => {
|
|
||||||
return new CoreApi(DEFAULT_CONFIG)
|
|
||||||
.coreUsersImpersonateCreate({
|
|
||||||
id: user.pk,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
window.location.href = "/";
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<pf-tooltip
|
|
||||||
position="top"
|
|
||||||
content=${msg(
|
|
||||||
"Temporarily assume the identity of this user",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
${msg("Impersonate")}
|
|
||||||
</pf-tooltip>
|
|
||||||
</ak-action-button>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div class="pf-c-description-list__group">
|
|
||||||
<dt class="pf-c-description-list__term">
|
|
||||||
<span class="pf-c-description-list__text">${msg("Recovery")}</span>
|
|
||||||
</dt>
|
|
||||||
<dd class="pf-c-description-list__description">
|
|
||||||
<div class="pf-c-description-list__text ak-button-collection">
|
|
||||||
<ak-forms-modal size=${PFSize.Medium} id="update-password-request">
|
<ak-forms-modal size=${PFSize.Medium} id="update-password-request">
|
||||||
<span slot="submit">${msg("Update password")}</span>
|
<span slot="submit">${msg("Update password")}</span>
|
||||||
<span slot="header">${msg("Update password")}</span>
|
<span slot="header">${msg("Update password")}</span>
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
import { TemplateResult, html, nothing } from "lit";
|
||||||
|
import { classMap } from "lit/directives/class-map.js";
|
||||||
|
import { map } from "lit/directives/map.js";
|
||||||
|
|
||||||
|
export type DescriptionDesc = string | TemplateResult | undefined | typeof nothing;
|
||||||
|
export type DescriptionPair = [string, DescriptionDesc];
|
||||||
|
export type DescriptionRecord = { term: string; desc: DescriptionDesc };
|
||||||
|
|
||||||
|
interface DescriptionConfig {
|
||||||
|
horizontal: boolean;
|
||||||
|
compact: boolean;
|
||||||
|
twocolumn: boolean;
|
||||||
|
threecolumn: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDescriptionRecordCollection = (v: Array<unknown>): v is DescriptionRecord[] =>
|
||||||
|
v.length > 0 && typeof v[0] === "object" && !Array.isArray(v[0]);
|
||||||
|
|
||||||
|
function renderDescriptionGroup([term, description]: DescriptionPair) {
|
||||||
|
return html` <div class="pf-c-description-list__group">
|
||||||
|
<dt class="pf-c-description-list__term">
|
||||||
|
<span class="pf-c-description-list__text">${term}</span>
|
||||||
|
</dt>
|
||||||
|
<dd class="pf-c-description-list__description">
|
||||||
|
<div class="pf-c-description-list__text">${description ?? nothing}</div>
|
||||||
|
</dd>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordToPair({ term, desc }: DescriptionRecord): DescriptionPair {
|
||||||
|
return [term, desc];
|
||||||
|
}
|
||||||
|
|
||||||
|
function alignTermType(terms: DescriptionRecord[] | DescriptionPair[] = []) {
|
||||||
|
if (isDescriptionRecordCollection(terms)) {
|
||||||
|
return terms.map(recordToPair);
|
||||||
|
}
|
||||||
|
return terms ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* renderDescriptionList
|
||||||
|
*
|
||||||
|
* This function renders the most common form of the PatternFly description list used in our code.
|
||||||
|
* It expects either an array of term/description pairs or an array of `{ term: string, description:
|
||||||
|
* string | TemplateResult }`.
|
||||||
|
*
|
||||||
|
* An optional dictionary of configuration options is available. These enable the Patternfly
|
||||||
|
* "horizontal," "compact", "2 column on large," or "3 column on large" layouts that are (so far)
|
||||||
|
* the layouts used in Authentik's (and Gravity's, for that matter) code.
|
||||||
|
*
|
||||||
|
* This is not a web component and it does not bring its own styling ; calling code will still have
|
||||||
|
* to provide the styling necessary. It is only a function to replace the repetitious boilerplate of
|
||||||
|
* routine description lists. Its output is a standard TemplateResult that will be fully realized
|
||||||
|
* within the context of the DOM or ShadowDOM in which it is called.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
horizontal: false,
|
||||||
|
compact: false,
|
||||||
|
twocolumn: false,
|
||||||
|
threecolumn: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function renderDescriptionList(
|
||||||
|
terms: DescriptionRecord[],
|
||||||
|
config?: DescriptionConfig,
|
||||||
|
): TemplateResult;
|
||||||
|
|
||||||
|
export function renderDescriptionList(
|
||||||
|
terms: DescriptionPair[],
|
||||||
|
config?: DescriptionConfig,
|
||||||
|
): TemplateResult;
|
||||||
|
|
||||||
|
export function renderDescriptionList(
|
||||||
|
terms: DescriptionRecord[] | DescriptionPair[] = [],
|
||||||
|
config: DescriptionConfig = defaultConfig,
|
||||||
|
) {
|
||||||
|
const checkedTerms = alignTermType(terms);
|
||||||
|
const classes = classMap({
|
||||||
|
"pf-m-horizontal": config.horizontal,
|
||||||
|
"pf-m-compact": config.compact,
|
||||||
|
"pf-m-2-col-on-lg": config.twocolumn,
|
||||||
|
"pf-m-3-col-on-lg": config.threecolumn,
|
||||||
|
});
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<dl class="pf-c-description-list ${classes}">
|
||||||
|
${map(checkedTerms, renderDescriptionGroup)}
|
||||||
|
</dl>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default renderDescriptionList;
|
Reference in New Issue