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:
Ken Sternberg 2023-12-13 07:16:57 -08:00 committed by GitHub
parent b181c551a5
commit 026e80bd10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 181 additions and 156 deletions

View File

@ -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>

View 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;