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 { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
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/events/ObjectChangelog";
|
||||
import "@goauthentik/components/events/UserEvents";
|
||||
|
@ -137,165 +142,91 @@ export class UserViewPage extends AKElement {
|
|||
|
||||
const user = this.user;
|
||||
|
||||
const canImpersonate =
|
||||
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) &&
|
||||
this.user.pk !== this.me?.user.pk;
|
||||
// prettier-ignore
|
||||
const userInfo: DescriptionPair[] = [
|
||||
[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`
|
||||
<div class="pf-c-card__title">${msg("User Info")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
<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>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">${user.username}</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("Name")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">${user.name}</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("Email")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">${user.email || "-"}</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("Last login")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
${user.lastLogin?.toLocaleString()}
|
||||
</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("Active")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-status-label
|
||||
type="warning"
|
||||
?good=${user.isActive}
|
||||
></ak-status-label>
|
||||
</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("Type")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
${userTypeToLabel(user.type)}
|
||||
</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("Superuser")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-status-label
|
||||
type="warning"
|
||||
?good=${user.isSuperuser}
|
||||
></ak-status-label>
|
||||
</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("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">
|
||||
<div class="pf-c-card__body">${renderDescriptionList(userInfo)}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderActionButtons(user: User) {
|
||||
const canImpersonate =
|
||||
rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.CanImpersonate) &&
|
||||
user.pk !== this.me?.user.pk;
|
||||
|
||||
return html`<div class="ak-button-collection">
|
||||
<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> `;
|
||||
}
|
||||
|
||||
renderRecoveryButtons(user: User) {
|
||||
return html`<div class="ak-button-collection">
|
||||
<ak-forms-modal size=${PFSize.Medium} id="update-password-request">
|
||||
<span slot="submit">${msg("Update password")}</span>
|
||||
<span slot="header">${msg("Update password")}</span>
|
||||
|
|
94
web/src/components/DescriptionList.ts
Normal file
94
web/src/components/DescriptionList.ts
Normal file
|
@ -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 a new issue