web/admin: user details few tooltip buttons (#6899)

* Web: Detangling some circular dependencies in Admin and User

Admin, User, and Flow should not dependend upon each other, at least
not in a circular way.  If Admin and User depend on Flow, that's
fine, but Flow should not correspondingly depend upon elements of
either; if they have something in common, let's put them in
`@goauthentik/common` or find some other smart place to store them.

This commit refactors the intentToLabel and actionToLabel functions
into `@goauthentik/common/labels` and converts them to static tables
for maintenance purposes.

* web: "Consistency is the hobgoblin of small minds" - Ralph Waldo Emerson

* web: I found these confusing to look at, so I added comments.

* web: remove admin-to-user component reference(s)

There was only one: AppIcon.  This has been moved to `components`.

Touching the LibraryApplications page triggered a cyclomatic
complexity check.  Extracting the expansion block and streamlining
the class and style declarations with lit directives helped.

* web: remove admin from elements

This commit removes the two references from `elements` to `admin`: the list of UserEvents and a
reference to the FlowSearch type, used by the Forms manager to decide how to extract a value.
For FlowSearch, a different convention for detecting the type was implemented (instances of the
object have a unique fieldname for the value holder).  UserEvents and ObjectChangelog have been
moved to `components` as they're clearly dependent upon the API.

This defers work on removing Admin from Components, as that is (again) references going the
wrong way, but that can happen later.

* web: remove admin-to-user component reference(s) (#6856)

There was only one: AppIcon.  This has been moved to `components`.

Touching the LibraryApplications page triggered a cyclomatic
complexity check.  Extracting the expansion block and streamlining
the class and style declarations with lit directives helped.

* This was supposed to be merged.

* web: remove `./element`⇢`./user` references

The offender here is UserDevicesList, which despite being in `elements` is only
used by the admin/user/UserViewPage.  The problem is that UserDevicesList,
despite being in `admin`, inherits from `user`, so moving it would have created
a new admin⇢user reference, and the whole point of this exercise is to get rid
of references that point "up" from the foundational pieces to the views, or
that refer to components in sibling applications.

After examining UserDevicesList, I realized that *every feature* of MFADevicesList
had been overridden: the rows, the columns, the toolbar, and the endpoint all had
custom overrides.  Nothing was left of MFADevicesList after that.   Even the
property that the web component used had been completely changed.  The only thing
they had in common was that they both inherited from `Table<Device>`.

Refactoring UserDevicesList so that it inherited directly from `Table<Device>` and
then moving it into `./admin/users` was the obvious and correct step.

Both used the same label table, so that went into the `common/labels` folder.

Along the way, I cleaned up a few minor details. Just little things, like the repeated invocation
of:

```
new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorAdminMETHODDestroy({ id: device.pk });
```

This is repeated five times, once for each Method.  By creating these:

```
        const api = new AuthenticatorsApi(DEFAULT_CONFIG);
        const id = { id: device.pk };
```

The method invocation could be just `api.authenticatorsMETHODDestroy(id)`, which is easier on the
eyes.  See the MFADevicesPage for the full example.

Similarly,

```
return [
   new TableColumn(msg("Name"), ""),
   new TableColumn(msg("Type"), ""),
   new TableColumn("")
];
   ```

is more straightforward as:

```
const headers = [msg("Name"), msg("Type"), ""];
return headers.map((th) => new TableColumn(th, ""));
```

We've labeled what we're working with, and web developers ought to know that `th` is the HTML code
for `table header`.

I've had to alter what files are scanned in pre-commit mode; it doesn't handle renamed files very well,
and at the moment a file that is renamed is not scanned, as its "new" name is not straightforwardly
displayed, not even by `git porcelain`.

* web: make the table of column headers look like a table

* web: detangle `common` from `elements`.

And just like that, `common` no longer has a reference to `elements`.   I don't mind this little bit of
code duplication if it removes a cycle.  What it does point out is that there are bits of `common` that
are predicated on the presence of the browser, and that there are bits of `elements` that, if they rely
on `common`, can't be disentangled from the application as a whole.  Which seems to me that we have two
different things going on in common: things about an application, and things about elements that are
independent of the application.

I'll think about those later.

```
$ rg 'import.*@goauthentik' ./common/ | perl -ne 'm{"(@goauthentik[^"]*)"} && print "$1\n"' | sort | cut -d '/' -f1-2 | uniq | sort
@goauthentik/api
@goauthentik/common
$
```

* web: odd bug; merge-related?  Gonna investigate.

* web: build failure thanks to local cache; fixed

* web: detangle `components` from `admin`.

This was the last inappropriate reference: something from `./components` referencing something in
`./admin`, in this case the `ak-event-info` component.  Used by both Users and Admin, moving it
into `./components` was the obvious correct step.

`ak-event-info` is a lookup table relating specific events in the event log to rich, textual
representations; in the special case of model changes and email info, even more rich content is
available in a dl/dt format. I've tableized the model changes and email info renderer, and I've
extracted every event's textual representation into its own method, converting the `switch/case`
rendering statement into a `switch/case` dispatch switch. This has the virtue of isolating each
unique case and making the dispatch switch short and coherent.

The conversion was done mechanistically; I gave the refactorer (Tide, in this case) instructions to
duplicate the switch block and then convert every case into a method with a name patterned on the
`case`. Going back to the original switch block, it was easy to duplicate the pattern matching and
convert it into a dispatch switch.

And with this, there are zero cycles in the references between the different "packageable" sections
of the UI.  The only thing left to do is figure out how to redistribute `./elements` and `./components`
in a way that makes sense for each.

* Changed function name from 'emailMessageBody' to 'githubIssueMessageBody' to better reflect its usage.

* web: added comments about length and purpose of githubIssueMessageBody.

* Update web/src/common/labels.ts

Co-authored-by: Jens L. <jens@goauthentik.io>
Signed-off-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>

* Unwanted change.

* web/add tooltip buttons to user details page

This commit wraps the command buttons on the UserDetailsPage with tooltips providing greater copy
explaining what each button does. It also ensures that every button is a minimum of 11ems in width
(The longest phrase, 'Reset Password', results in a width of 10.75ems; this makes them all
consistent.)

The technique for giving the `ak-action-button` objects a mininum width uses the CSS `::part()`
syntax, which is new. CanIUse shows that it's at 95.3% of global usage; our weak points remain Opera
Mini and UC Browser for Android.

Oh, and IE. But the various Powers That Be™ agree we're no longer tracking or caring about IE.

* I added some text, so it's my responibility to add the language files.

* fix text

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* rework

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* web: enforce a max-width on the container for the buttons so that they don't look funky on ultrawide monitors.

* wbe: re-ran and confirmed prettier.

---------

Signed-off-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
This commit is contained in:
Ken Sternberg 2023-09-18 10:27:13 -07:00 committed by GitHub
parent 70630aab3a
commit 42c3cfa65d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 227 additions and 181 deletions

View File

@ -23,7 +23,7 @@ import "@goauthentik/elements/user/SessionList";
import "@goauthentik/elements/user/UserConsentList"; import "@goauthentik/elements/user/UserConsentList";
import { msg, str } from "@lit/localize"; import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -69,6 +69,15 @@ export class UserViewPage extends AKElement {
PFCard, PFCard,
PFDescriptionList, PFDescriptionList,
PFSizing, PFSizing,
css`
.pf-c-description-list__description ak-action-button {
margin-right: 6px;
margin-bottom: 6px;
}
.ak-button-collection {
max-width: 12em;
}
`,
]; ];
} }
@ -90,6 +99,221 @@ export class UserViewPage extends AKElement {
${this.renderBody()}`; ${this.renderBody()}`;
} }
renderUserCard(): TemplateResult {
if (!this.user) {
return html``;
}
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">${this.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">${this.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">${this.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">
${this.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-label
color=${this.user.isActive ? PFColor.Green : PFColor.Orange}
></ak-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("Superuser")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ak-label
color=${this.user.isSuperuser ? PFColor.Green : PFColor.Orange}
></ak-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=${this.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=${this.user}
objectLabel=${msg("User")}
.delete=${() => {
return new CoreApi(DEFAULT_CONFIG).coreUsersPartialUpdate({
id: this.user?.pk || 0,
patchedUserRequest: {
isActive: !this.user?.isActive,
},
});
}}
>
<button
slot="trigger"
class="pf-c-button pf-m-warning pf-m-block"
>
<pf-tooltip
position="top"
content=${this.user.isActive
? msg("Lock the user out of this system")
: msg(
"Allow the user to log in and use this system",
)}
>
${this.user.isActive
? msg("Deactivate")
: msg("Activate")}
</pf-tooltip>
</button>
</ak-user-active-form>
${rootInterface()?.config?.capabilities.includes(
CapabilitiesEnum.CanImpersonate,
)
? html`
<ak-action-button
class="pf-m-secondary pf-m-block"
id="impersonate-user-button"
.apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersImpersonateCreate({
id: this.user?.pk || 0,
})
.then(() => {
window.location.href = "/";
});
}}
>
<pf-tooltip
position="top"
content=${msg(
"Temporarily assume the identity of this user",
)}
>
${msg("Impersonate")}
</pf-tooltip>
</ak-action-button>
`
: html``}
</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}>
<span slot="submit">${msg("Update password")}</span>
<span slot="header">${msg("Update password")}</span>
<ak-user-password-form
slot="form"
.instancePk=${this.user?.pk}
></ak-user-password-form>
<button
slot="trigger"
class="pf-c-button pf-m-secondary pf-m-block"
>
<pf-tooltip
position="top"
content=${msg("Enter a new password for this user")}
>
${msg("Set password")}
</pf-tooltip>
</button>
</ak-forms-modal>
<ak-action-button
id="reset-password-button"
class="pf-m-secondary pf-m-block"
.apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersRecoveryRetrieve({
id: this.user?.pk || 0,
})
.then((rec) => {
showMessage({
level: MessageLevel.success,
message: msg(
"Successfully generated recovery link",
),
description: rec.link,
});
})
.catch(() => {
showMessage({
level: MessageLevel.error,
message: msg(
"To create a recovery link, the current tenant needs to have a recovery flow configured.",
),
description: "",
});
});
}}
>
<pf-tooltip
position="top"
content=${msg(
"Create a link for this user to reset their password",
)}
>
${msg("Reset Password")}
</pf-tooltip>
</ak-action-button>
</div>
</dd>
</div>
</dl>
</div>
`;
}
renderBody(): TemplateResult { renderBody(): TemplateResult {
if (!this.user) { if (!this.user) {
return html``; return html``;
@ -104,186 +328,7 @@ export class UserViewPage extends AKElement {
<div <div
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-3-col-on-xl pf-m-3-col-on-2xl" class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-3-col-on-xl pf-m-3-col-on-2xl"
> >
<div class="pf-c-card__title">${msg("User Info")}</div> ${this.renderUserCard()}
<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-2-col">
<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">
${this.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">
${this.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">
${this.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">
${this.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-label
color=${this.user.isActive
? PFColor.Green
: PFColor.Orange}
></ak-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("Superuser")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ak-label
color=${this.user.isSuperuser
? PFColor.Green
: PFColor.Orange}
></ak-label>
</div>
</dd>
</div>
</dl>
</div>
<div class="pf-c-card__footer">
<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update User")} </span>
<ak-user-form slot="form" .instancePk=${this.user.pk}>
</ak-user-form>
<button slot="trigger" class="pf-m-primary pf-c-button">
${msg("Edit")}
</button>
</ak-forms-modal>
</div>
${rootInterface()?.config?.capabilities.includes(
CapabilitiesEnum.CanImpersonate,
)
? html`
<div class="pf-c-card__footer">
<ak-action-button
class="pf-m-tertiary"
.apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersImpersonateCreate({
id: this.user?.pk || 0,
})
.then(() => {
window.location.href = "/";
});
}}
>
${msg("Impersonate")}
</ak-action-button>
</div>
`
: html``}
<div class="pf-c-card__footer">
<ak-user-active-form
.obj=${this.user}
objectLabel=${msg("User")}
.delete=${() => {
return new CoreApi(DEFAULT_CONFIG).coreUsersPartialUpdate({
id: this.user?.pk || 0,
patchedUserRequest: {
isActive: !this.user?.isActive,
},
});
}}
>
<button slot="trigger" class="pf-c-button pf-m-warning">
${this.user.isActive ? msg("Deactivate") : msg("Activate")}
</button>
</ak-user-active-form>
</div>
<div class="pf-c-card__footer">
<ak-action-button
class="pf-m-secondary"
.apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersRecoveryRetrieve({
id: this.user?.pk || 0,
})
.then((rec) => {
showMessage({
level: MessageLevel.success,
message: msg(
"Successfully generated recovery link",
),
description: rec.link,
});
})
.catch(() => {
showMessage({
level: MessageLevel.error,
message: msg(
"To create a recovery link, the current tenant needs to have a recovery flow configured.",
),
description: "",
});
});
}}
>
${msg("Reset Password")}
</ak-action-button>
</div>
<div class="pf-c-card__footer">
<ak-forms-modal size=${PFSize.Medium}>
<span slot="submit">${msg("Update password")}</span>
<span slot="header">${msg("Update password")}</span>
<ak-user-password-form
slot="form"
.instancePk=${this.user?.pk}
></ak-user-password-form>
<button slot="trigger" class="pf-c-button pf-m-secondary">
${msg("Set password")}
</button>
</ak-forms-modal>
</div>
</div> </div>
<div <div
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-9-col-on-xl pf-m-9-col-on-2xl" class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-9-col-on-xl pf-m-9-col-on-2xl"

View File

@ -118,6 +118,7 @@ export abstract class BaseTaskButton extends CustomEmitterElement(AKElement) {
render() { render() {
return html`<button return html`<button
id="spinner-button" id="spinner-button"
part="spinner-button"
class="pf-c-button pf-m-progress ${this.buttonClasses}" class="pf-c-button pf-m-progress ${this.buttonClasses}"
@click=${this.onClick} @click=${this.onClick}
> >