web/admin: revamped rbac and user settings tabs (#8299)

* web/admin: fix duplicate RBAC preview banner on permission modal

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

* switch non-embedded permission page to use vertical tabs

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

* fix some leftover html?

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

* move stuff into vertical subtab

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

* show all of users permission tabs on one main tab

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

* rework role page to match user page

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

* use separate tabs

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

* rename role permission tables to match user tables

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

* rename to credentials and tokens

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

* add country icon to session list

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

* add oauth access token list

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

* add helper to get relative time

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

* use pfdivider

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

* replace plain hr with pf-c-divider

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

* use new logic for showing relative time in charts

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

* use consistent relative time for event display

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

* remove more leftovers

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

* fix some alignment issues on the admin dashboard

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

* update storybook map

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

* add sanity check to event app lookup

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

* make api drawer header fixed

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

* fix table padding for toggle

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

* fix notification drawer for user interface

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

* enable system task search

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

* fix formatting, exclude generated script from formatting

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

* web: minor fixes

There's a renderer (it's not a component, not yet) for producing definition lists without
the risk of missing a class or tag.

Breaking conditionally rendered components out to make their use easier to identify.

* fix prettier

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

* fix outpost form

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

* fix more flaky tests

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

* re-create locale

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

* add some description for different permission views

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

* fix system task search

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

* update docs

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Ken Sternberg <ken@goauthentik.io>
This commit is contained in:
Jens L
2024-01-26 18:01:03 +01:00
committed by GitHub
parent 85a8768424
commit 11ca358242
48 changed files with 838 additions and 456 deletions

View File

@ -19,6 +19,7 @@ import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFDivider from "@patternfly/patternfly/components/Divider/divider.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
@ -41,15 +42,12 @@ export class AdminOverviewPage extends AKElement {
PFPage,
PFContent,
PFList,
PFDivider,
css`
.row-divider {
margin-top: -4px;
margin-bottom: -4px;
.pf-l-grid__item {
height: 100%;
}
.graph-container {
height: 20em;
}
.big-graph-container {
.pf-l-grid__item.big-graph-container {
height: 35em;
}
.card-container {
@ -82,9 +80,7 @@ export class AdminOverviewPage extends AKElement {
<div class="pf-l-grid pf-m-gutter">
<!-- row 1 -->
<div class="pf-l-grid__item pf-m-6-col pf-l-grid pf-m-gutter">
<div
class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl graph-container"
>
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl">
<ak-aggregate-card
icon="fa fa-share"
header=${msg("Quick actions")}
@ -136,9 +132,7 @@ export class AdminOverviewPage extends AKElement {
</ul>
</ak-aggregate-card>
</div>
<div
class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl graph-container"
>
<div class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-4-col-on-2xl">
<ak-aggregate-card
icon="pf-icon pf-icon-zone"
header=${msg("Outpost status")}
@ -148,14 +142,14 @@ export class AdminOverviewPage extends AKElement {
</ak-aggregate-card>
</div>
<div
class="pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-4-col-on-2xl graph-container"
class="pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-4-col-on-2xl"
>
<ak-aggregate-card icon="fa fa-sync-alt" header=${msg("Sync status")}>
<ak-admin-status-chart-sync></ak-admin-status-chart-sync>
</ak-aggregate-card>
</div>
<div class="pf-l-grid__item pf-m-12-col row-divider">
<hr />
<div class="pf-l-grid__item pf-m-12-col">
<hr class="pf-c-divider" />
</div>
<div
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-md pf-m-4-col-on-xl card-container"
@ -176,8 +170,8 @@ export class AdminOverviewPage extends AKElement {
<div class="pf-l-grid__item pf-m-6-col">
<ak-recent-events pageSize="6"></ak-recent-events>
</div>
<div class="pf-l-grid__item pf-m-12-col row-divider">
<hr />
<div class="pf-l-grid__item pf-m-12-col">
<hr class="pf-c-divider" />
</div>
<!-- row 3 -->
<div

View File

@ -8,6 +8,7 @@ import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFDivider from "@patternfly/patternfly/components/Divider/divider.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
@ -22,14 +23,8 @@ export class DashboardUserPage extends AKElement {
PFPage,
PFContent,
PFList,
PFDivider,
css`
.row-divider {
margin-top: -4px;
margin-bottom: -4px;
}
.graph-container {
height: 20em;
}
.big-graph-container {
height: 35em;
}
@ -59,8 +54,8 @@ export class DashboardUserPage extends AKElement {
</ak-charts-admin-model-per-day>
</ak-aggregate-card>
</div>
<div class="pf-l-grid__item pf-m-12-col row-divider">
<hr />
<div class="pf-l-grid__item pf-m-12-col">
<hr class="pf-c-divider" />
</div>
<!-- row 2 -->
<div

View File

@ -1,4 +1,5 @@
import { EventGeo, EventUser } from "@goauthentik/admin/events/utils";
import { getRelativeTime } from "@goauthentik/app/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EventWithContext } from "@goauthentik/common/events";
import { actionToLabel } from "@goauthentik/common/labels";
@ -71,7 +72,8 @@ export class RecentEventsCard extends Table<Event> {
html`<div><a href="${`#/events/log/${item.pk}`}">${actionToLabel(item.action)}</a></div>
<small>${item.app}</small>`,
EventUser(item),
html`<span>${item.created?.toLocaleString()}</span>`,
html`<div>${getRelativeTime(item.created)}</div>
<small>${item.created.toLocaleString()}</small>`,
html` <div>${item.clientIp || msg("-")}</div>
<small>${EventGeo(item)}</small>`,
html`<span>${item.brand?.name || msg("-")}</span>`,

View File

@ -1,8 +1,8 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart, RGBAColor } from "@goauthentik/elements/charts/Chart";
import { ChartData, Tick } from "chart.js";
import { ChartData } from "chart.js";
import { msg, str } from "@lit/localize";
import { msg } from "@lit/localize";
import { customElement } from "lit/decorators.js";
import { AdminApi, LoginMetrics } from "@goauthentik/api";
@ -13,13 +13,6 @@ export class AdminLoginAuthorizeChart extends AKChart<LoginMetrics> {
return new AdminApi(DEFAULT_CONFIG).adminMetricsRetrieve();
}
timeTickCallback(tickValue: string | number, index: number, ticks: Tick[]): string {
const valueStamp = ticks[index];
const delta = Date.now() - valueStamp.value;
const ago = Math.round(delta / 1000 / 3600 / 24);
return msg(str`${ago} day(s) ago`);
}
getChartData(data: LoginMetrics): ChartData {
return {
datasets: [

View File

@ -1,5 +1,6 @@
import "@goauthentik/admin/events/EventVolumeChart";
import { EventGeo, EventUser } from "@goauthentik/admin/events/utils";
import { getRelativeTime } from "@goauthentik/app/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EventWithContext } from "@goauthentik/common/events";
import { actionToLabel } from "@goauthentik/common/labels";
@ -82,9 +83,9 @@ export class EventListPage extends TablePage<Event> {
html`<div>${actionToLabel(item.action)}</div>
<small>${item.app}</small>`,
EventUser(item),
html`<span>${item.created?.toLocaleString()}</span>`,
html`<div>${getRelativeTime(item.created)}</div>
<small>${item.created.toLocaleString()}</small>`,
html`<div>${item.clientIp || msg("-")}</div>
<small>${EventGeo(item)}</small>`,
html`<span>${item.brand?.name || msg("-")}</span>`,
html`<a href="#/events/log/${item.pk}">

View File

@ -1,4 +1,5 @@
import { EventGeo, EventUser } from "@goauthentik/admin/events/utils";
import { getRelativeTime } from "@goauthentik/app/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EventWithContext } from "@goauthentik/common/events";
import { actionToLabel } from "@goauthentik/common/labels";
@ -99,7 +100,8 @@ export class EventViewPage extends AKElement {
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.event.created?.toLocaleString()}
<div>${getRelativeTime(this.event.created)}</div>
<small>${this.event.created.toLocaleString()}</small>
</div>
</dd>
</div>

View File

@ -230,25 +230,29 @@ export class OutpostForm extends ModelForm<Outpost, string> {
</ak-form-element-horizontal>
<ak-form-group aria-label="Advanced settings">
<span slot="header"> ${msg("Advanced settings")} </span>
<ak-form-element-horizontal label=${msg("Configuration")} name="config">
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value="${YAML.stringify(
this.instance ? this.instance.config : this.defaultConfig?.config,
)}"
></ak-codemirror>
<p class="pf-c-form__helper-text">
${msg("Set custom attributes using YAML or JSON.")}
</p>
<p class="pf-c-form__helper-text">
${msg("See more here:")}&nbsp;
<a
target="_blank"
href="${docLink("/docs/outposts?utm_source=authentik#configuration")}"
>${msg("Documentation")}</a
>
</p>
</ak-form-element-horizontal>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Configuration")} name="config">
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value="${YAML.stringify(
this.instance ? this.instance.config : this.defaultConfig?.config,
)}"
></ak-codemirror>
<p class="pf-c-form__helper-text">
${msg("Set custom attributes using YAML or JSON.")}
</p>
<p class="pf-c-form__helper-text">
${msg("See more here:")}&nbsp;
<a
target="_blank"
href="${docLink(
"/docs/outposts?utm_source=authentik#configuration",
)}"
>${msg("Documentation")}</a
>
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;
}
}

View File

@ -22,6 +22,7 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import PFDivider from "@patternfly/patternfly/components/Divider/divider.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
@ -70,6 +71,7 @@ export class OAuth2ProviderViewPage extends AKElement {
PFForm,
PFFormControl,
PFBanner,
PFDivider,
];
}
@ -258,7 +260,7 @@ export class OAuth2ProviderViewPage extends AKElement {
value="${this.providerUrls?.issuer || msg("-")}"
/>
</div>
<hr />
<hr class="pf-c-divider" />
<div class="pf-c-form__group">
<label class="pf-c-form__label">
<span class="pf-c-form__label-text"

View File

@ -11,8 +11,8 @@ import { ifDefined } from "lit/directives/if-defined.js";
import { Permission, RbacApi } from "@goauthentik/api";
@customElement("ak-role-permissions-global-table")
export class RolePermissionGlobalTable extends Table<Permission> {
@customElement("ak-role-assigned-global-permissions-table")
export class RoleAssignedGlobalPermissionsTable extends Table<Permission> {
@property()
roleUuid?: string;

View File

@ -10,8 +10,8 @@ import { customElement, property } from "lit/decorators.js";
import { ExtraRoleObjectPermission, ModelEnum, RbacApi } from "@goauthentik/api";
@customElement("ak-role-permissions-object-table")
export class RolePermissionObjectTable extends Table<ExtraRoleObjectPermission> {
@customElement("ak-role-assigned-object-permissions-table")
export class RoleAssignedObjectPermissionTable extends Table<ExtraRoleObjectPermission> {
@property()
roleUuid?: string;

View File

@ -1,21 +1,20 @@
import "@goauthentik/admin/groups/RelatedGroupList";
import "@goauthentik/app/admin/roles/RolePermissionGlobalTable";
import "@goauthentik/app/admin/roles/RolePermissionObjectTable";
import "@goauthentik/admin/roles/RoleForm";
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { renderDescriptionList } from "@goauthentik/components/DescriptionList";
import "@goauthentik/components/events/ObjectChangelog";
import "@goauthentik/components/events/UserEvents";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/PageHeader";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/forms/ModalForm";
import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
@ -43,7 +42,7 @@ export class RoleViewPage extends AKElement {
@state()
_role?: Role;
static get styles(): CSSResult[] {
static get styles() {
return [
PFBase,
PFPage,
@ -53,7 +52,6 @@ export class RoleViewPage extends AKElement {
PFContent,
PFCard,
PFDescriptionList,
PFBanner,
css`
.pf-c-description-list__description ak-action-button {
margin-right: 6px;
@ -74,7 +72,7 @@ export class RoleViewPage extends AKElement {
});
}
render(): TemplateResult {
render() {
return html`<ak-page-header
icon="fa fa-lock"
header=${msg(str`Role ${this._role?.name || ""}`)}
@ -83,73 +81,61 @@ export class RoleViewPage extends AKElement {
${this.renderBody()}`;
}
renderBody(): TemplateResult {
renderUpdateControl(role: Role) {
return html` <div class="pf-c-description-list__text">
<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update Role")} </span>
<ak-role-form slot="form" .instancePk=${role.pk}> </ak-role-form>
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Edit")}</button>
</ak-forms-modal>
</div>`;
}
renderBody() {
if (!this._role) {
return html``;
return nothing;
}
return html`<div class="pf-c-banner pf-m-info">
${msg("RBAC is in preview.")}
<a href="mailto:hello@goauthentik.io">${msg("Send us feedback!")}</a>
</div>
<ak-tabs>
<section
slot="page-overview"
data-tab-title="${msg("Overview")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-l-grid pf-m-gutter">
<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"
>
<div class="pf-c-card__title">${msg("Role 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("Name")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this._role.name}
</div>
</dd>
</div>
</dl>
</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"
>
<div class="pf-c-card__title">
${msg("Assigned global permissions")}
</div>
<div class="pf-c-card__body">
<ak-role-permissions-global-table
roleUuid=${this._role.pk}
></ak-role-permissions-global-table>
</div>
</div>
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">
${msg("Assigned object permissions")}
</div>
<div class="pf-c-card__body">
<ak-role-permissions-object-table
roleUuid=${this._role.pk}
></ak-role-permissions-object-table>
</div>
return html` <ak-tabs>
<section
slot="page-overview"
data-tab-title="${msg("Overview")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-l-grid pf-m-gutter">
<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"
>
<div class="pf-c-card__title">${msg("Role Info")}</div>
<div class="pf-c-card__body">
${renderDescriptionList([
[msg("Name"), this._role.name],
[msg("Edit"), this.renderUpdateControl(this._role)],
])}
</div>
</div>
</section>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.RbacRole}
objectPk=${this._role.pk}
.showBanner=${false}
></ak-rbac-object-permission-page>
</ak-tabs>`;
<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"
>
<div class="pf-c-card__title">${msg("Changelog")}</div>
<div class="pf-c-card__body">
<ak-object-changelog
targetModelPk=${this.roleId}
targetModelApp="authentik_rbac"
targetModelName="role"
>
</ak-object-changelog>
</div>
</div>
</div>
</section>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.RbacRole}
objectPk=${this._role.pk}
></ak-rbac-object-permission-page>
</ak-tabs>`;
}
}

View File

@ -31,6 +31,10 @@ export class SystemTaskListPage extends TablePage<SystemTask> {
expandable = true;
searchEnabled(): boolean {
return true;
}
@property()
order = "name";

View File

@ -3,13 +3,13 @@ import "@goauthentik/admin/users/UserActiveForm";
import "@goauthentik/admin/users/UserChart";
import "@goauthentik/admin/users/UserForm";
import "@goauthentik/admin/users/UserPasswordForm";
import "@goauthentik/app/admin/users/UserAssignedGlobalPermissionsTable";
import "@goauthentik/app/admin/users/UserAssignedObjectPermissionsTable";
import {
renderRecoveryEmailRequest,
requestRecoveryLink,
} from "@goauthentik/app/admin/users/UserListPage";
import { me } from "@goauthentik/app/common/users";
import "@goauthentik/app/elements/oauth/UserAccessTokenList";
import "@goauthentik/app/elements/oauth/UserRefreshTokenList";
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
@ -31,12 +31,11 @@ import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/oauth/UserRefreshList";
import "@goauthentik/elements/user/SessionList";
import "@goauthentik/elements/user/UserConsentList";
import { msg, str } from "@lit/localize";
import { css, html, nothing } from "lit";
import { TemplateResult, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
@ -227,45 +226,94 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
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>
<ak-user-password-form
slot="form"
.instancePk=${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=${() => requestRecoveryLink(user)}
>
<pf-tooltip
position="top"
content=${msg(
"Create a link for this user to reset their password",
)}
>
${msg("Create Recovery Link")}
</pf-tooltip>
</ak-action-button>
${user.email ? renderRecoveryEmailRequest(user) : nothing}
</div>
</dd>
<ak-forms-modal size=${PFSize.Medium} id="update-password-request">
<span slot="submit">${msg("Update password")}</span>
<span slot="header">${msg("Update password")}</span>
<ak-user-password-form slot="form" .instancePk=${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=${() => requestRecoveryLink(user)}
>
<pf-tooltip
position="top"
content=${msg("Create a link for this user to reset their password")}
>
${msg("Create Recovery Link")}
</pf-tooltip>
</ak-action-button>
${user.email ? renderRecoveryEmailRequest(user) : nothing}
</div> `;
}
renderTabCredentialsToken(user: User): TemplateResult {
return html`
<ak-tabs pageIdentifier="userCredentialsTokens" ?vertical=${true}>
<section
slot="page-sessions"
data-tab-title="${msg("Sessions")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-user-session-list targetUser=${user.username}>
</ak-user-session-list>
</div>
</div>
</dl>
</div>
</section>
<section
slot="page-consent"
data-tab-title="${msg("Explicit Consent")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-user-consent-list userId=${user.pk}> </ak-user-consent-list>
</div>
</div>
</section>
<section
slot="page-oauth-access"
data-tab-title="${msg("OAuth Access Tokens")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-user-oauth-access-token-list userId=${user.pk}>
</ak-user-oauth-access-token-list>
</div>
</div>
</section>
<section
slot="page-oauth-refresh"
data-tab-title="${msg("OAuth Refresh Tokens")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-user-oauth-refresh-token-list userId=${user.pk}>
</ak-user-oauth-refresh-token-list>
</div>
</div>
</section>
<section
slot="page-mfa-authenticators"
data-tab-title="${msg("MFA Authenticators")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-user-device-table userId=${user.pk}> </ak-user-device-table>
</div>
</div>
</section>
</ak-tabs>
`;
}
@ -326,18 +374,6 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
</div>
</div>
</section>
<section
slot="page-sessions"
data-tab-title="${msg("Sessions")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-user-session-list targetUser=${this.user.username}>
</ak-user-session-list>
</div>
</div>
</section>
<section
slot="page-groups"
data-tab-title="${msg("Groups")}"
@ -360,78 +396,16 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
</div>
</div>
</section>
<section
slot="page-consent"
data-tab-title="${msg("Explicit Consent")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-user-consent-list userId=${this.user.pk}> </ak-user-consent-list>
</div>
</div>
</section>
<section
slot="page-oauth-refresh"
data-tab-title="${msg("OAuth Refresh Tokens")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-user-oauth-refresh-list userId=${this.user.pk}>
</ak-user-oauth-refresh-list>
</div>
</div>
</section>
<section
slot="page-mfa-authenticators"
data-tab-title="${msg("MFA Authenticators")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card">
<div class="pf-c-card__body">
<ak-user-device-table userId=${this.user.pk}> </ak-user-device-table>
</div>
</div>
<section slot="page-credentials" data-tab-title="${msg("Credentials / Tokens")}">
${this.renderTabCredentialsToken(this.user)}
</section>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.CoreUser}
objectPk=${this.user.pk}
></ak-rbac-object-permission-page>
<div
slot="page-mfa-assigned-permissions"
data-tab-title="${msg("Assigned permissions")}"
class=""
>
<div class="pf-c-banner pf-m-info">
${msg("RBAC is in preview.")}
<a href="mailto:hello@goauthentik.io">${msg("Send us feedback!")}</a>
</div>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-l-grid pf-m-gutter">
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">
${msg("Assigned global permissions")}
</div>
<div class="pf-c-card__body">
<ak-user-assigned-global-permissions-table userId=${this.user.pk}>
</ak-user-assigned-global-permissions-table>
</div>
</div>
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">
${msg("Assigned object permissions")}
</div>
<div class="pf-c-card__body">
<ak-user-assigned-object-permissions-table userId=${this.user.pk}>
</ak-user-assigned-object-permissions-table>
</div>
</div>
</div>
</section>
</div>
</ak-rbac-object-permission-page>
</ak-tabs>`;
}
}

View File

@ -149,3 +149,25 @@ export function adaptCSS(sheet: AdaptableStylesheet[]): CSSStyleSheet[];
export function adaptCSS(sheet: AdaptableStylesheet | AdaptableStylesheet[]): AdaptedStylesheets {
return Array.isArray(sheet) ? sheet.map(_adaptCSS) : _adaptCSS(sheet);
}
const _timeUnits = new Map<Intl.RelativeTimeFormatUnit, number>([
["year", 24 * 60 * 60 * 1000 * 365],
["month", (24 * 60 * 60 * 1000 * 365) / 12],
["day", 24 * 60 * 60 * 1000],
["hour", 60 * 60 * 1000],
["minute", 60 * 1000],
["second", 1000],
]);
export function getRelativeTime(d1: Date, d2: Date = new Date()): string {
const rtf = new Intl.RelativeTimeFormat("default", { numeric: "auto" });
const elapsed = d1.getTime() - d2.getTime();
// "Math.abs" accounts for both "past" & "future" scenarios
for (const [key, value] of _timeUnits) {
if (Math.abs(elapsed) > value || key == "second") {
return rtf.format(Math.round(elapsed / value), key);
}
}
return rtf.format(Math.round(elapsed / 1000), "second");
}

View File

@ -1,5 +1,6 @@
import { EventGeo, EventUser } from "@goauthentik/app/admin/events/utils";
import { actionToLabel } from "@goauthentik/app/common/labels";
import { getRelativeTime } from "@goauthentik/app/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EventWithContext } from "@goauthentik/common/events";
import { uiConfig } from "@goauthentik/common/ui/config";
@ -46,7 +47,7 @@ export class ObjectChangelog extends Table<Event> {
let modelName = this._targetModelName;
let appName = this.targetModelApp;
if (this._targetModelName.indexOf(".") !== -1) {
const parts = this._targetModelName.split(".");
const parts = this._targetModelName.split(".", 1);
appName = parts[0];
modelName = parts[1];
}
@ -77,7 +78,8 @@ export class ObjectChangelog extends Table<Event> {
return [
html`${actionToLabel(item.action)}`,
EventUser(item),
html`<span>${item.created?.toLocaleString()}</span>`,
html`<div>${getRelativeTime(item.created)}</div>
<small>${item.created.toLocaleString()}</small>`,
html`<div>${item.clientIp || msg("-")}</div>
<small>${EventGeo(item)}</small>`,

View File

@ -1,4 +1,5 @@
import { EventUser } from "@goauthentik/app/admin/events/utils";
import { getRelativeTime } from "@goauthentik/app/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EventWithContext } from "@goauthentik/common/events";
import { actionToLabel } from "@goauthentik/common/labels";
@ -48,7 +49,8 @@ export class UserEvents extends Table<Event> {
return [
html`${actionToLabel(item.action)}`,
EventUser(item),
html`<span>${item.created?.toLocaleString()}</span>`,
html`<div>${getRelativeTime(item.created)}</div>
<small>${item.created.toLocaleString()}</small>`,
html`<span>${item.clientIp || msg("-")}</span>`,
];
}

View File

@ -84,6 +84,7 @@ export class AggregateCard extends AKElement {
${this.renderInner()}
${this.subtext ? html`<p class="subtext">${this.subtext}</p>` : html``}
</div>
<div class="pf-c-card__footer">&nbsp;</div>
</div>`;
}
}

View File

@ -1,3 +1,4 @@
import { getRelativeTime } from "@goauthentik/app/common/utils";
import { EVENT_REFRESH, EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/EmptyState";
@ -18,7 +19,7 @@ import { ArcElement, BarElement } from "chart.js";
import { LinearScale, TimeScale } from "chart.js";
import "chartjs-adapter-moment";
import { msg, str } from "@lit/localize";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { property, state } from "lit/decorators.js";
@ -161,9 +162,7 @@ export abstract class AKChart<T> extends AKElement {
timeTickCallback(tickValue: string | number, index: number, ticks: Tick[]): string {
const valueStamp = ticks[index];
const delta = Date.now() - valueStamp.value;
const ago = Math.round(delta / 1000 / 3600);
return msg(str`${ago} hour(s) ago`);
return getRelativeTime(new Date(valueStamp.value));
}
getOptions(): ChartOptions {

View File

@ -25,8 +25,11 @@ export class APIDrawer extends AKElement {
PFContent,
PFDropdown,
css`
:host {
--header-height: 114px;
}
.pf-c-notification-drawer__header {
height: 114px;
height: var(--header-height);
align-items: center;
}
.pf-c-notification-drawer__header-action,
@ -41,6 +44,9 @@ export class APIDrawer extends AKElement {
.pf-c-notification-drawer__body {
overflow-x: hidden;
}
.pf-c-notification-drawer__list {
max-height: calc(100vh - var(--header-height));
}
`,
];
}

View File

@ -0,0 +1,93 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/components/ak-status-label";
import "@goauthentik/elements/forms/DeleteBulkForm";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
import { ExpiringBaseGrantModel, Oauth2Api, TokenModel } from "@goauthentik/api";
@customElement("ak-user-oauth-access-token-list")
export class UserOAuthAccessTokenList extends Table<TokenModel> {
expandable = true;
@property({ type: Number })
userId?: number;
static get styles(): CSSResult[] {
return super.styles.concat(PFFlex);
}
async apiEndpoint(page: number): Promise<PaginatedResponse<TokenModel>> {
return new Oauth2Api(DEFAULT_CONFIG).oauth2AccessTokensList({
user: this.userId,
ordering: "expires",
page: page,
pageSize: (await uiConfig()).pagination.perPage,
});
}
checkbox = true;
order = "-expires";
columns(): TableColumn[] {
return [
new TableColumn(msg("Provider"), "provider"),
new TableColumn(msg("Revoked?"), "revoked"),
new TableColumn(msg("Expires"), "expires"),
new TableColumn(msg("Scopes"), "scope"),
];
}
renderExpanded(item: TokenModel): TemplateResult {
return html` <td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<div class="pf-l-flex">
<div class="pf-l-flex__item">
<h3>${msg("ID Token")}</h3>
<pre>${item.idToken}</pre>
</div>
</div>
</div>
</td>
<td></td>
<td></td>`;
}
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${msg("Refresh Tokens(s)")}
.objects=${this.selectedElements}
.usedBy=${(item: ExpiringBaseGrantModel) => {
return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensUsedByList({
id: item.pk,
});
}}
.delete=${(item: ExpiringBaseGrantModel) => {
return new Oauth2Api(DEFAULT_CONFIG).oauth2RefreshTokensDestroy({
id: item.pk,
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
row(item: TokenModel): TemplateResult[] {
return [
html`<a href="#/core/providers/${item.provider?.pk}"> ${item.provider?.name} </a>`,
html`<ak-status-label type="warning" ?good=${item.revoked}></ak-status-label>`,
html`${item.expires?.toLocaleString()}`,
html`${item.scope.join(", ")}`,
];
}
}

View File

@ -13,8 +13,8 @@ import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
import { ExpiringBaseGrantModel, Oauth2Api, TokenModel } from "@goauthentik/api";
@customElement("ak-user-oauth-refresh-list")
export class UserOAuthRefreshList extends Table<TokenModel> {
@customElement("ak-user-oauth-refresh-token-list")
export class UserOAuthRefreshTokenList extends Table<TokenModel> {
expandable = true;
@property({ type: Number })

View File

@ -38,6 +38,7 @@ export class ObjectPermissionsPageForm extends ModelForm<unknown, string> {
.model=${this.model}
.objectPk=${this.objectPk}
slot="form"
.embedded=${true}
>
</ak-rbac-object-permission-page>`;
}

View File

@ -1,10 +1,14 @@
import "@goauthentik/app/admin/roles/RoleAssignedGlobalPermissionsTable";
import "@goauthentik/app/admin/roles/RoleAssignedObjectPermissionTable";
import "@goauthentik/app/admin/users/UserAssignedGlobalPermissionsTable";
import "@goauthentik/app/admin/users/UserAssignedObjectPermissionsTable";
import { AKElement } from "@goauthentik/app/elements/Base";
import "@goauthentik/app/elements/rbac/RoleObjectPermissionTable";
import "@goauthentik/app/elements/rbac/UserObjectPermissionTable";
import "@goauthentik/elements/Tabs";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
import { html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
@ -24,20 +28,26 @@ export class ObjectPermissionPage extends AKElement {
objectPk?: string | number;
@property({ type: Boolean })
showBanner = true;
embedded = false;
static get styles(): CSSResult[] {
static get styles() {
return [PFBase, PFGrid, PFPage, PFCard, PFBanner];
}
render(): TemplateResult {
return html`${this.showBanner
render() {
return html`${!this.embedded
? html`<div class="pf-c-banner pf-m-info">
${msg("RBAC is in preview.")}
<a href="mailto:hello@goauthentik.io">${msg("Send us feedback!")}</a>
</div>`
: html``}
<ak-tabs pageIdentifier="permissionPage">
: nothing}
<ak-tabs pageIdentifier="permissionPage" ?vertical=${!this.embedded}>
${this.model === RbacPermissionsAssignedByUsersListModelEnum.CoreUser
? this.renderCoreUser()
: nothing}
${this.model === RbacPermissionsAssignedByUsersListModelEnum.RbacRole
? this.renderRbacRole()
: nothing}
<section
slot="page-object-user"
data-tab-title="${msg("User Object Permissions")}"
@ -45,7 +55,10 @@ export class ObjectPermissionPage extends AKElement {
>
<div class="pf-l-grid pf-m-gutter">
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">User Object Permissions</div>
<div class="pf-c-card__title">${msg("User Object Permissions")}</div>
<div class="pf-c-card__body">
${msg("Permissions set on users which affect this object.")}
</div>
<div class="pf-c-card__body">
<ak-rbac-user-object-permission-table
.model=${this.model}
@ -63,7 +76,10 @@ export class ObjectPermissionPage extends AKElement {
>
<div class="pf-l-grid pf-m-gutter">
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__title">Role Object Permissions</div>
<div class="pf-c-card__title">${msg("Role Object Permissions")}</div>
<div class="pf-c-card__body">
${msg("Permissions set on roles which affect this object.")}
</div>
<div class="pf-c-card__body">
<ak-rbac-role-object-permission-table
.model=${this.model}
@ -76,4 +92,98 @@ export class ObjectPermissionPage extends AKElement {
</section>
</ak-tabs>`;
}
renderCoreUser() {
return html`
<div
slot="page-assigned-global-permissions"
data-tab-title="${msg("Assigned global permissions")}"
>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
<div class="pf-c-card__title">${msg("Assigned global permissions")}</div>
<div class="pf-c-card__body">
${msg(
"Permissions assigned to this user which affect all object instances of a given type.",
)}
</div>
<div class="pf-c-card__body">
<ak-user-assigned-global-permissions-table
userId=${this.objectPk as number}
>
</ak-user-assigned-global-permissions-table>
</div>
</div>
</section>
</div>
<div
slot="page-assigned-object-permissions"
data-tab-title="${msg("Assigned object permissions")}"
>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
<div class="pf-c-card__title">${msg("Assigned object permissions")}</div>
<div class="pf-c-card__body">
${msg(
"Permissions assigned to this user affecting specific object instances.",
)}
</div>
<div class="pf-c-card__body">
<ak-user-assigned-object-permissions-table
userId=${this.objectPk as number}
>
</ak-user-assigned-object-permissions-table>
</div>
</div>
</section>
</div>
`;
}
renderRbacRole() {
return html`
<div
slot="page-assigned-global-permissions"
data-tab-title="${msg("Assigned global permissions")}"
>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
<div class="pf-c-card__title">${msg("Assigned global permissions")}</div>
<div class="pf-c-card__body">
${msg(
"Permissions assigned to this role which affect all object instances of a given type.",
)}
</div>
<div class="pf-c-card__body">
<ak-role-assigned-global-permissions-table
roleUuid=${this.objectPk as string}
>
</ak-role-assigned-global-permissions-table>
</div>
</div>
</section>
</div>
<div
slot="page-assigned-object-permissions"
data-tab-title="${msg("Assigned object permissions")}"
>
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
<div class="pf-c-card__title">${msg("Assigned object permissions")}</div>
<div class="pf-c-card__body">
${msg(
"Permissions assigned to this user affecting specific object instances.",
)}
</div>
<div class="pf-c-card__body">
<ak-role-assigned-object-permissions-table
roleUuid=${this.objectPk as string}
>
</ak-role-assigned-object-permissions-table>
</div>
</div>
</section>
</div>
`;
}
}

View File

@ -180,6 +180,12 @@ export abstract class Table<T> extends AKElement implements TableLike {
.pf-c-table tbody .pf-c-table__check input {
margin-top: calc(var(--pf-c-table__check--input--MarginTop) + 1px);
}
.pf-c-toolbar__content {
row-gap: var(--pf-global--spacer--sm);
}
.pf-c-toolbar__item .pf-c-input-group {
padding: 0 var(--pf-global--spacer--sm);
}
`,
];
}

View File

@ -1,8 +1,10 @@
import { getRelativeTime } from "@goauthentik/app/common/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { uiConfig } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/forms/DeleteBulkForm";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
import getUnicodeFlagIcon from "country-flag-icons/unicode";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
@ -31,6 +33,7 @@ export class AuthenticatedSessionList extends Table<AuthenticatedSession> {
columns(): TableColumn[] {
return [
new TableColumn(msg("Last IP"), "last_ip"),
new TableColumn(msg("Last used"), "last_used"),
new TableColumn(msg("Expires"), "expires"),
];
}
@ -66,10 +69,17 @@ export class AuthenticatedSessionList extends Table<AuthenticatedSession> {
row(item: AuthenticatedSession): TemplateResult[] {
return [
html`<div>
${item.current ? html`${msg("(Current session)")}&nbsp;` : html``}${item.lastIp}
${item.current ? html`${msg("(Current session)")}&nbsp;` : html``}
${item.lastIp}
${item.geoIp?.country
? html`&nbsp;${getUnicodeFlagIcon(item.geoIp.country)} `
: html``}
</div>
<small>${item.userAgent.userAgent?.family}, ${item.userAgent.os?.family}</small>`,
html`${item.expires?.toLocaleString()}`,
html`<div>${getRelativeTime(item.lastUsed)}</div>
<small>${item.lastUsed?.toLocaleString()}</small>`,
html`<div>${getRelativeTime(item.expires || new Date())}</div>
<small>${item.expires?.toLocaleString()}</small>`,
];
}
}

View File

@ -10,6 +10,7 @@ import { TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDivider from "@patternfly/patternfly/components/Divider/divider.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
@ -32,7 +33,7 @@ export class PlexLoginInit extends BaseStage<
authUrl?: string;
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle];
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, PFDivider];
}
async firstUpdated(): Promise<void> {
@ -76,7 +77,7 @@ export class PlexLoginInit extends BaseStage<
header=${msg("Waiting for authentication...")}
>
</ak-empty-state>
<hr />
<hr class="pf-c-divider" />
<p>${msg("If no Plex popup opens, click the button below.")}</p>
<button
class="pf-c-button pf-m-block pf-m-primary"

View File

@ -8,6 +8,7 @@ import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFDivider from "@patternfly/patternfly/components/Divider/divider.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
@ -26,6 +27,7 @@ export class AccessDeniedIcon extends AKElement {
return [
PFBase,
PFTitle,
PFDivider,
css`
.big-icon {
display: flex;
@ -51,7 +53,7 @@ export class AccessDeniedIcon extends AKElement {
</p>
<h3 class="pf-c-title pf-m-3xl reason">${msg("Request has been denied.")}</h3>
${this.errorMessage
? html`<hr />
? html` <hr class="pf-c-divider" />
<p>${this.errorMessage}</p>`
: html``}
</div>`;

View File

@ -121,6 +121,9 @@ export class UserInterface extends Interface {
display: flex;
flex-direction: column;
}
.pf-c-drawer__main {
max-height: calc(100vh - 76px);
}
`,
];
}