web: remove more until (#5057)

* more cleanup

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

* don't dynamically import duo form

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

* migrate more

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

* fix import

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

* properly send evens when tab isn't switched

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

* fix loop on tabs

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

* migrate more

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

* don't bubble tab events

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

* remove most other uses of until()

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

* cleanup user settings

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

* only use stale for issues

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L
2023-03-23 23:16:26 +01:00
committed by GitHub
parent af7189953c
commit b3dd87bbab
26 changed files with 699 additions and 744 deletions

View File

@ -1,3 +1,4 @@
import { AdminInterface } from "@goauthentik/admin/AdminInterface";
import "@goauthentik/admin/admin-overview/TopApplicationsTable";
import "@goauthentik/admin/admin-overview/cards/AdminStatusCard";
import "@goauthentik/admin/admin-overview/cards/RecentEventsCard";
@ -8,8 +9,7 @@ import "@goauthentik/admin/admin-overview/charts/AdminLoginAuthorizeChart";
import "@goauthentik/admin/admin-overview/charts/OutpostStatusChart";
import "@goauthentik/admin/admin-overview/charts/SyncStatusChart";
import { VERSION } from "@goauthentik/common/constants";
import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base";
import { AKElement, rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/PageHeader";
import "@goauthentik/elements/cards/AggregatePromiseCard";
import { paramURL } from "@goauthentik/elements/router/RouterOutlet";
@ -17,15 +17,13 @@ import { paramURL } from "@goauthentik/elements/router/RouterOutlet";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { customElement } from "lit/decorators.js";
import PFContent from "@patternfly/patternfly/components/Content/content.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";
import { SessionUser } from "@goauthentik/api";
export function versionFamily(): string {
const parts = VERSION.split(".");
parts.pop();
@ -58,17 +56,11 @@ export class AdminOverviewPage extends AKElement {
];
}
@state()
user?: SessionUser;
async firstUpdated(): Promise<void> {
this.user = await me();
}
render(): TemplateResult {
let name = this.user?.user.username;
if (this.user?.user.name) {
name = this.user.user.name;
const user = rootInterface<AdminInterface>()?.user;
let name = user?.user.username;
if (user?.user.name) {
name = user.user.name;
}
return html`<ak-page-header icon="" header="" description=${t`General system status`}>
<span slot="header"> ${t`Welcome, ${name}.`} </span>

View File

@ -18,13 +18,12 @@ import { t } from "@lingui/macro";
import { CSSResult } from "lit";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import { Outpost, OutpostTypeEnum, OutpostsApi } from "@goauthentik/api";
import { Outpost, OutpostHealth, OutpostTypeEnum, OutpostsApi } from "@goauthentik/api";
export function TypeToLabel(type?: OutpostTypeEnum): string {
if (!type) return "";
@ -56,14 +55,31 @@ export class OutpostListPage extends TablePage<Outpost> {
searchEnabled(): boolean {
return true;
}
async apiEndpoint(page: number): Promise<PaginatedResponse<Outpost>> {
return new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesList({
const outposts = await new OutpostsApi(DEFAULT_CONFIG).outpostsInstancesList({
ordering: this.order,
page: page,
pageSize: (await uiConfig()).pagination.perPage,
search: this.search || "",
});
Promise.all(
outposts.results.map((outpost) => {
return new OutpostsApi(DEFAULT_CONFIG)
.outpostsInstancesHealthList({
uuid: outpost.pk,
})
.then((health) => {
this.health[outpost.pk] = health;
});
}),
);
return outposts;
}
@state()
health: { [key: string]: OutpostHealth[] } = {};
columns(): TableColumn[] {
return [
new TableColumn(t`Name`, "name"),
@ -136,25 +152,15 @@ export class OutpostListPage extends TablePage<Outpost> {
${t`Detailed health (one instance per column, data is cached so may be out of date)`}
</h3>
<dl class="pf-c-description-list pf-m-3-col-on-lg">
${until(
new OutpostsApi(DEFAULT_CONFIG)
.outpostsInstancesHealthList({
uuid: item.pk,
})
.then((health) => {
return health.map((h) => {
return html` <div class="pf-c-description-list__group">
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ak-outpost-health
.outpostHealth=${h}
></ak-outpost-health>
</div>
</dd>
</div>`;
});
}),
)}
${this.health[item.pk].map((h) => {
return html`<div class="pf-c-description-list__group">
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ak-outpost-health .outpostHealth=${h}></ak-outpost-health>
</div>
</dd>
</div>`;
})}
</dl>
</div>
</td>`;

View File

@ -16,11 +16,10 @@ import { TablePage } from "@goauthentik/elements/table/TablePage";
import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { OutpostsApi, ServiceConnection } from "@goauthentik/api";
import { OutpostsApi, ServiceConnection, ServiceConnectionState } from "@goauthentik/api";
@customElement("ak-outpost-service-connection-list")
export class OutpostServiceConnectionListPage extends TablePage<ServiceConnection> {
@ -40,14 +39,31 @@ export class OutpostServiceConnectionListPage extends TablePage<ServiceConnectio
checkbox = true;
async apiEndpoint(page: number): Promise<PaginatedResponse<ServiceConnection>> {
return new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsAllList({
ordering: this.order,
page: page,
pageSize: (await uiConfig()).pagination.perPage,
search: this.search || "",
});
const connections = await new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsAllList(
{
ordering: this.order,
page: page,
pageSize: (await uiConfig()).pagination.perPage,
search: this.search || "",
},
);
Promise.all(
connections.results.map((connection) => {
return new OutpostsApi(DEFAULT_CONFIG)
.outpostsServiceConnectionsAllStateRetrieve({
uuid: connection.pk,
})
.then((state) => {
this.state[connection.pk] = state;
});
}),
);
return connections;
}
@state()
state: { [key: string]: ServiceConnectionState } = {};
columns(): TableColumn[] {
return [
new TableColumn(t`Name`, "name"),
@ -62,27 +78,16 @@ export class OutpostServiceConnectionListPage extends TablePage<ServiceConnectio
order = "name";
row(item: ServiceConnection): TemplateResult[] {
const itemState = this.state[item.pk];
return [
html`${item.name}`,
html`${item.verboseName}`,
html`<ak-label color=${item.local ? PFColor.Grey : PFColor.Green}>
${item.local ? t`Yes` : t`No`}
</ak-label>`,
html`${until(
new OutpostsApi(DEFAULT_CONFIG)
.outpostsServiceConnectionsAllStateRetrieve({
uuid: item.pk || "",
})
.then((state) => {
if (state.healthy) {
return html`<ak-label color=${PFColor.Green}
>${ifDefined(state.version)}</ak-label
>`;
}
return html`<ak-label color=${PFColor.Red}>${t`Unhealthy`}</ak-label>`;
}),
html`<ak-spinner></ak-spinner>`,
)}`,
html`${itemState.healthy
? html`<ak-label color=${PFColor.Green}>${ifDefined(itemState.version)}</ak-label>`
: html`<ak-label color=${PFColor.Red}>${t`Unhealthy`}</ak-label>`}`,
html` <ak-forms-modal>
<span slot="submit"> ${t`Update`} </span>
<span slot="header"> ${t`Update ${item.verboseName}`} </span>

View File

@ -6,6 +6,7 @@ import { convertToTitle } from "@goauthentik/common/utils";
import MDProviderOAuth2 from "@goauthentik/docs/providers/oauth2/index.md";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/Markdown";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/ModalButton";
@ -15,8 +16,7 @@ import "@goauthentik/elements/events/ObjectChangelog";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { until } from "lit/directives/until.js";
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";
@ -29,31 +29,35 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { OAuth2Provider, OAuth2ProviderSetupURLs, ProvidersApi } from "@goauthentik/api";
import {
OAuth2Provider,
OAuth2ProviderSetupURLs,
PropertyMappingPreview,
ProvidersApi,
} from "@goauthentik/api";
@customElement("ak-provider-oauth2-view")
export class OAuth2ProviderViewPage extends AKElement {
@property({ type: Number })
set providerID(value: number) {
const api = new ProvidersApi(DEFAULT_CONFIG);
api.providersOauth2Retrieve({
id: value,
}).then((prov) => {
this.provider = prov;
});
api.providersOauth2SetupUrlsRetrieve({
id: value,
}).then((prov) => {
this.providerUrls = prov;
});
new ProvidersApi(DEFAULT_CONFIG)
.providersOauth2Retrieve({
id: value,
})
.then((prov) => {
this.provider = prov;
});
}
@property({ attribute: false })
provider?: OAuth2Provider;
@property({ attribute: false })
@state()
providerUrls?: OAuth2ProviderSetupURLs;
@state()
preview?: PropertyMappingPreview;
static get styles(): CSSResult[] {
return [
PFBase,
@ -82,10 +86,32 @@ export class OAuth2ProviderViewPage extends AKElement {
return html``;
}
return html` <ak-tabs>
<section slot="page-overview" data-tab-title="${t`Overview`}">
<section
slot="page-overview"
data-tab-title="${t`Overview`}"
@activate=${() => {
new ProvidersApi(DEFAULT_CONFIG)
.providersOauth2SetupUrlsRetrieve({
id: this.provider?.pk || 0,
})
.then((prov) => {
this.providerUrls = prov;
});
}}
>
${this.renderTabOverview()}
</section>
<section slot="page-preview" data-tab-title="${t`Preview`}">
<section
slot="page-preview"
data-tab-title="${t`Preview`}"
@activate=${() => {
new ProvidersApi(DEFAULT_CONFIG)
.providersOauth2PreviewUserRetrieve({
id: this.provider?.pk || 0,
})
.then((preview) => (this.preview = preview));
}}
>
${this.renderTabPreview()}
</section>
<section
@ -318,15 +344,9 @@ export class OAuth2ProviderViewPage extends AKElement {
${t`Example JWT payload (for currently authenticated user)`}
</div>
<div class="pf-c-card__body">
${until(
new ProvidersApi(DEFAULT_CONFIG)
.providersOauth2PreviewUserRetrieve({
id: this.provider?.pk,
})
.then((data) => {
return html`<pre>${JSON.stringify(data.preview, null, 4)}</pre>`;
}),
)}
${this.preview
? html`<pre>${JSON.stringify(this.preview?.preview, null, 4)}</pre>`
: html` <ak-empty-state ?loading=${true}></ak-empty-state> `}
</div>
</div>
</div>`;

View File

@ -5,6 +5,7 @@ import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { MessageLevel } from "@goauthentik/common/messages";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/ModalButton";
@ -15,9 +16,8 @@ import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -31,7 +31,13 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { CryptoApi, ProvidersApi, SAMLProvider } from "@goauthentik/api";
import {
CertificateKeyPair,
CryptoApi,
ProvidersApi,
SAMLMetadata,
SAMLProvider,
} from "@goauthentik/api";
interface SAMLPreviewAttribute {
attributes: {
@ -54,12 +60,40 @@ export class SAMLProviderViewPage extends AKElement {
.providersSamlRetrieve({
id: value,
})
.then((prov) => (this.provider = prov));
.then((prov) => {
this.provider = prov;
if (prov.signingKp) {
new CryptoApi(DEFAULT_CONFIG)
.cryptoCertificatekeypairsRetrieve({
kpUuid: prov.signingKp,
})
.then((kp) => (this.signer = kp));
}
if (prov.verificationKp) {
new CryptoApi(DEFAULT_CONFIG)
.cryptoCertificatekeypairsRetrieve({
kpUuid: prov.verificationKp,
})
.then((kp) => (this.verifier = kp));
}
});
}
@property({ attribute: false })
provider?: SAMLProvider;
@state()
preview?: SAMLPreviewAttribute;
@state()
metadata?: SAMLMetadata;
@state()
signer?: CertificateKeyPair;
@state()
verifier?: CertificateKeyPair;
static get styles(): CSSResult[] {
return [
PFBase,
@ -84,7 +118,7 @@ export class SAMLProviderViewPage extends AKElement {
});
}
async renderRelatedObjects(): Promise<TemplateResult> {
renderRelatedObjects(): TemplateResult {
const relatedObjects = [];
if (this.provider?.assignedApplicationName) {
relatedObjects.push(html`<div class="pf-c-description-list__group">
@ -122,10 +156,7 @@ export class SAMLProviderViewPage extends AKElement {
</dd>
</div>`);
}
if (this.provider?.signingKp) {
const kp = await new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsRetrieve({
kpUuid: this.provider.signingKp,
});
if (this.signer) {
relatedObjects.push(html`<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
@ -134,7 +165,9 @@ export class SAMLProviderViewPage extends AKElement {
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<a class="pf-c-button pf-m-primary" href=${kp.certificateDownloadUrl}
<a
class="pf-c-button pf-m-primary"
href=${this.signer.certificateDownloadUrl}
>${t`Download`}</a
>
</div>
@ -160,7 +193,19 @@ export class SAMLProviderViewPage extends AKElement {
${this.renderTabOverview()}
</section>
${this.renderTabMetadata()}
<section slot="page-preview" data-tab-title="${t`Preview`}">
<section
slot="page-preview"
data-tab-title="${t`Preview`}"
@activate=${() => {
new ProvidersApi(DEFAULT_CONFIG)
.providersSamlPreviewUserRetrieve({
id: this.provider?.pk || 0,
})
.then((preview) => {
this.preview = preview.preview as SAMLPreviewAttribute;
});
}}
>
${this.renderTabPreview()}
</section>
<section
@ -264,7 +309,7 @@ export class SAMLProviderViewPage extends AKElement {
</ak-forms-modal>
</div>
</div>
${until(this.renderRelatedObjects())}
${this.renderRelatedObjects()}
${
this.provider.assignedApplicationName
? html` <div class="pf-c-card pf-l-grid__item pf-m-12-col">
@ -364,7 +409,17 @@ export class SAMLProviderViewPage extends AKElement {
}
return html`
${this.provider.assignedApplicationName
? html` <section slot="page-metadata" data-tab-title="${t`Metadata`}">
? html` <section
slot="page-metadata"
data-tab-title="${t`Metadata`}"
@activate=${() => {
new ProvidersApi(DEFAULT_CONFIG)
.providersSamlMetadataRetrieve({
id: this.provider?.pk || 0,
})
.then((metadata) => (this.metadata = metadata));
}}
>
<div
class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter"
>
@ -399,19 +454,11 @@ export class SAMLProviderViewPage extends AKElement {
</ak-action-button>
</div>
<div class="pf-c-card__footer">
${until(
new ProvidersApi(DEFAULT_CONFIG)
.providersSamlMetadataRetrieve({
id: this.provider.pk || 0,
})
.then((m) => {
return html`<ak-codemirror
mode="xml"
?readOnly=${true}
value="${ifDefined(m.metadata)}"
></ak-codemirror>`;
}),
)}
<ak-codemirror
mode="xml"
?readOnly=${true}
value="${ifDefined(this.metadata?.metadata)}"
></ak-codemirror>
</div>
</div>
</div>
@ -421,65 +468,50 @@ export class SAMLProviderViewPage extends AKElement {
}
renderTabPreview(): TemplateResult {
if (!this.provider) {
return html``;
if (!this.preview) {
return html`<ak-empty-state ?loading=${true}></ak-empty-state>`;
}
return html` <div
class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter"
>
<div class="pf-c-card">
<div class="pf-c-card__title">${t`Example SAML attributes`}</div>
${until(
new ProvidersApi(DEFAULT_CONFIG)
.providersSamlPreviewUserRetrieve({
id: this.provider?.pk,
})
.then((data) => {
const d = data.preview as SAMLPreviewAttribute;
return html`
<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-2-col-on-lg">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${t`NameID attribute`}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${d.nameID}
</div>
</dd>
</div>
</dl>
<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-2-col-on-lg">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${t`NameID attribute`}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.preview?.nameID}
</div>
<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-2-col-on-lg">
${d.attributes.map((attr) => {
return html` <div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${attr.Name}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ul class="pf-c-list">
${attr.Value.map((value) => {
return html`
<li><pre>${value}</pre></li>
`;
})}
</ul>
</div>
</dd>
</div>`;
})}
</dl>
</div>
`;
}),
)}
</dd>
</div>
</dl>
</div>
<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-2-col-on-lg">
${this.preview?.attributes.map((attr) => {
return html` <div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${attr.Name}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<ul class="pf-c-list">
${attr.Value.map((value) => {
return html` <li><pre>${value}</pre></li> `;
})}
</ul>
</div>
</dd>
</div>`;
})}
</dl>
</div>
</div>
</div>`;
}

View File

@ -1,7 +1,6 @@
import "@goauthentik/admin/providers/scim/SCIMProviderForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { me } from "@goauthentik/common/users";
import MDSCIMProvider from "@goauthentik/docs/providers/scim/index.md";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/Markdown";
@ -14,7 +13,6 @@ import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { until } from "lit/directives/until.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -29,7 +27,7 @@ import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFStack from "@patternfly/patternfly/layouts/Stack/stack.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { ProvidersApi, SCIMProvider, SessionUser } from "@goauthentik/api";
import { ProvidersApi, SCIMProvider, Task } from "@goauthentik/api";
@customElement("ak-provider-scim-view")
export class SCIMProviderViewPage extends AKElement {
@ -51,7 +49,7 @@ export class SCIMProviderViewPage extends AKElement {
provider?: SCIMProvider;
@state()
me?: SessionUser;
syncState?: Task;
static get styles(): CSSResult[] {
return [
@ -76,9 +74,6 @@ export class SCIMProviderViewPage extends AKElement {
if (!this.provider?.pk) return;
this.providerID = this.provider?.pk;
});
me().then((user) => {
this.me = user;
});
}
render(): TemplateResult {
@ -86,7 +81,22 @@ export class SCIMProviderViewPage extends AKElement {
return html``;
}
return html` <ak-tabs>
<section slot="page-overview" data-tab-title="${t`Overview`}">
<section
slot="page-overview"
data-tab-title="${t`Overview`}"
@activate=${() => {
new ProvidersApi(DEFAULT_CONFIG)
.providersScimSyncStatusRetrieve({
id: this.provider?.pk || 0,
})
.then((state) => {
this.syncState = state;
})
.catch(() => {
this.syncState = undefined;
});
}}
>
${this.renderTabOverview()}
</section>
<section
@ -158,23 +168,13 @@ export class SCIMProviderViewPage extends AKElement {
<p>${t`Sync status`}</p>
</div>
<div class="pf-c-card__body">
${until(
new ProvidersApi(DEFAULT_CONFIG)
.providersScimSyncStatusRetrieve({
id: this.provider.pk,
})
.then((task) => {
return html` <ul class="pf-c-list">
${task.messages.map((m) => {
return html`<li>${m}</li>`;
})}
</ul>`;
})
.catch(() => {
return html`${t`Sync not run yet.`}`;
}),
"loading",
)}
${this.syncState
? html` <ul class="pf-c-list">
${this.syncState.messages.map((m) => {
return html`<li>${m}</li>`;
})}
</ul>`
: html` ${t`Sync not run yet.`} `}
</div>
<div class="pf-c-card__footer">

View File

@ -12,8 +12,7 @@ import "@goauthentik/elements/forms/ModalForm";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { until } from "lit/directives/until.js";
import { customElement, property, state } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
@ -24,7 +23,7 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { LDAPSource, SourcesApi, TaskStatusEnum } from "@goauthentik/api";
import { LDAPSource, SourcesApi, Task, TaskStatusEnum } from "@goauthentik/api";
@customElement("ak-source-ldap-view")
export class LDAPSourceViewPage extends AKElement {
@ -42,6 +41,9 @@ export class LDAPSourceViewPage extends AKElement {
@property({ attribute: false })
source!: LDAPSource;
@state()
syncState: Task[] = [];
static get styles(): CSSResult[] {
return [PFBase, PFPage, PFButton, PFGrid, PFContent, PFCard, PFDescriptionList, PFList];
}
@ -63,6 +65,15 @@ export class LDAPSourceViewPage extends AKElement {
slot="page-overview"
data-tab-title="${t`Overview`}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
@activate=${() => {
new SourcesApi(DEFAULT_CONFIG)
.sourcesLdapSyncStatusList({
slug: this.source.slug,
})
.then((state) => {
this.syncState = state;
});
}}
>
<div class="pf-l-grid pf-m-gutter">
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
@ -123,39 +134,31 @@ export class LDAPSourceViewPage extends AKElement {
<p>${t`Sync status`}</p>
</div>
<div class="pf-c-card__body">
${until(
new SourcesApi(DEFAULT_CONFIG)
.sourcesLdapSyncStatusList({
slug: this.source.slug,
})
.then((tasks) => {
if (tasks.length < 1) {
return html`<p>${t`Not synced yet.`}</p>`;
}
return html`<ul class="pf-c-list">
${tasks.map((task) => {
let header = "";
if (task.status === TaskStatusEnum.Warning) {
header = t`Task finished with warnings`;
} else if (task.status === TaskStatusEnum.Error) {
header = t`Task finished with errors`;
} else {
header = t`Last sync: ${task.taskFinishTimestamp.toLocaleString()}`;
}
return html`<li>
<p>${task.taskName}</p>
<ul class="pf-c-list">
<li>${header}</li>
${task.messages.map((m) => {
return html`<li>${m}</li>`;
})}
</ul>
</li> `;
})}
</ul>`;
}),
"loading",
)}
${this.syncState.length < 1
? html`<p>${t`Not synced yet.`}</p>`
: html`
<ul class="pf-c-list">
${this.syncState.map((task) => {
let header = "";
if (task.status === TaskStatusEnum.Warning) {
header = t`Task finished with warnings`;
} else if (task.status === TaskStatusEnum.Error) {
header = t`Task finished with errors`;
} else {
header = t`Last sync: ${task.taskFinishTimestamp.toLocaleString()}`;
}
return html`<li>
<p>${task.taskName}</p>
<ul class="pf-c-list">
<li>${header}</li>
${task.messages.map((m) => {
return html`<li>${m}</li>`;
})}
</ul>
</li> `;
})}
</ul>
`}
</div>
<div class="pf-c-card__footer">
<ak-action-button

View File

@ -12,9 +12,8 @@ import "@goauthentik/elements/forms/ModalForm";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
@ -24,7 +23,7 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { SAMLSource, SourcesApi } from "@goauthentik/api";
import { SAMLMetadata, SAMLSource, SourcesApi } from "@goauthentik/api";
@customElement("ak-source-saml-view")
export class SAMLSourceViewPage extends AKElement {
@ -42,6 +41,9 @@ export class SAMLSourceViewPage extends AKElement {
@property({ attribute: false })
source?: SAMLSource;
@state()
metadata?: SAMLMetadata;
static get styles(): CSSResult[] {
return [PFBase, PFPage, PFGrid, PFButton, PFContent, PFCard, PFDescriptionList];
}
@ -152,35 +154,34 @@ export class SAMLSourceViewPage extends AKElement {
slot="page-metadata"
data-tab-title="${t`Metadata`}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
@activate=${() => {
new SourcesApi(DEFAULT_CONFIG)
.sourcesSamlMetadataRetrieve({
slug: this.source?.slug || "",
})
.then((metadata) => {
this.metadata = metadata;
});
}}
>
<div class="pf-l-grid pf-m-gutter">
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
${until(
new SourcesApi(DEFAULT_CONFIG)
.sourcesSamlMetadataRetrieve({
slug: this.source.slug,
})
.then((m) => {
return html`
<div class="pf-c-card__body">
<ak-codemirror
mode="xml"
?readOnly=${true}
value="${ifDefined(m.metadata)}"
></ak-codemirror>
</div>
<div class="pf-c-card__footer">
<a
class="pf-c-button pf-m-primary"
target="_blank"
href=${ifDefined(m.downloadUrl)}
>
${t`Download`}
</a>
</div>
`;
}),
)}
<div class="pf-c-card__body">
<ak-codemirror
mode="xml"
?readOnly=${true}
value="${ifDefined(this.metadata?.metadata)}"
></ak-codemirror>
</div>
<div class="pf-c-card__footer">
<a
class="pf-c-button pf-m-primary"
target="_blank"
href=${ifDefined(this.metadata?.downloadUrl)}
>
${t`Download`}
</a>
</div>
</div>
</div>
</section>

View File

@ -1,5 +1,6 @@
import "@goauthentik/admin/stages/StageWizard";
import "@goauthentik/admin/stages/authenticator_duo/AuthenticatorDuoStageForm";
import "@goauthentik/admin/stages/authenticator_duo/DuoDeviceImportForm";
import "@goauthentik/admin/stages/authenticator_sms/AuthenticatorSMSStageForm";
import "@goauthentik/admin/stages/authenticator_static/AuthenticatorStaticStageForm";
import "@goauthentik/admin/stages/authenticator_totp/AuthenticatorTOTPStageForm";
@ -33,7 +34,6 @@ import { t } from "@lingui/macro";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { Stage, StagesApi } from "@goauthentik/api";
@ -100,20 +100,24 @@ export class StageListPage extends TablePage<Stage> {
</ak-forms-delete-bulk>`;
}
async renderStageActions(stage: Stage): Promise<TemplateResult> {
if (stage.component === "ak-stage-authenticator-duo-form") {
await import("@goauthentik/admin/stages/authenticator_duo/DuoDeviceImportForm");
return html`<ak-forms-modal>
<span slot="submit">${t`Import`}</span>
<span slot="header">${t`Import Duo device`}</span>
<ak-stage-authenticator-duo-device-import-form slot="form" .instancePk=${stage.pk}>
</ak-stage-authenticator-duo-device-import-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<i class="fas fa-file-import"></i>
</button>
</ak-forms-modal>`;
renderStageActions(stage: Stage): TemplateResult {
switch (stage.component) {
case "ak-stage-authenticator-duo-form":
return html`<ak-forms-modal>
<span slot="submit">${t`Import`}</span>
<span slot="header">${t`Import Duo device`}</span>
<ak-stage-authenticator-duo-device-import-form
slot="form"
.instancePk=${stage.pk}
>
</ak-stage-authenticator-duo-device-import-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<i class="fas fa-file-import"></i>
</button>
</ak-forms-modal>`;
default:
return html``;
}
return html``;
}
row(item: Stage): TemplateResult[] {
@ -144,7 +148,7 @@ export class StageListPage extends TablePage<Stage> {
<i class="fas fa-edit"></i>
</button>
</ak-forms-modal>
${until(this.renderStageActions(item))}`,
${this.renderStageActions(item)}`,
];
}

View File

@ -3,10 +3,11 @@ import "@goauthentik/admin/users/UserActiveForm";
import "@goauthentik/admin/users/UserForm";
import "@goauthentik/admin/users/UserPasswordForm";
import "@goauthentik/admin/users/UserResetEmailForm";
import { DEFAULT_CONFIG, config, tenant } from "@goauthentik/common/api/config";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { MessageLevel } from "@goauthentik/common/messages";
import { uiConfig } from "@goauthentik/common/ui/config";
import { first } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base";
import { PFColor } from "@goauthentik/elements/Label";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/Dropdown";
@ -25,7 +26,6 @@ import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
@ -189,19 +189,16 @@ export class RelatedUserList extends Table<User> {
<i class="fas fa-edit"></i>
</button>
</ak-forms-modal>
${until(
config().then((config) => {
if (config.capabilities.includes(CapabilitiesEnum.Impersonate)) {
return html`<a
class="pf-c-button pf-m-tertiary"
href="${`/-/impersonation/${item.pk}/`}"
>
${t`Impersonate`}
</a>`;
}
return html``;
}),
)}`,
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.Impersonate)
? html`
<a
class="pf-c-button pf-m-tertiary"
href="${`/-/impersonation/${item.pk}/`}"
>
${t`Impersonate`}
</a>
`
: html``}`,
];
}
@ -266,70 +263,61 @@ export class RelatedUserList extends Table<User> {
${t`Set password`}
</button>
</ak-forms-modal>
${until(
tenant().then((tenant) => {
if (!tenant.flowRecovery) {
return html`
<p>
${t`To let a user directly reset a their password, configure a recovery flow on the currently active tenant.`}
</p>
`;
}
return html`
<ak-action-button
class="pf-m-secondary"
.apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersRecoveryRetrieve({
id: item.pk || 0,
})
.then((rec) => {
showMessage({
level: MessageLevel.success,
message: t`Successfully generated recovery link`,
description: rec.link,
});
})
.catch((ex: ResponseError) => {
ex.response.json().then(() => {
showMessage({
level: MessageLevel.error,
message: t`No recovery flow is configured.`,
});
});
});
}}
>
${t`Copy recovery link`}
</ak-action-button>
${item.email
? html`<ak-forms-modal
.closeAfterSuccessfulSubmit=${false}
>
<span slot="submit">
${t`Send link`}
</span>
<span slot="header">
${t`Send recovery link to user`}
</span>
<ak-user-reset-email-form
slot="form"
.user=${item}
>
</ak-user-reset-email-form>
<button
slot="trigger"
class="pf-c-button pf-m-secondary"
>
${t`Email recovery link`}
</button>
</ak-forms-modal>`
: html`<span
>${t`Recovery link cannot be emailed, user has no email address saved.`}</span
>`}
`;
}),
)}
${rootInterface()?.tenant?.flowRecovery
? html`
<ak-action-button
class="pf-m-secondary"
.apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersRecoveryRetrieve({
id: item.pk,
})
.then((rec) => {
showMessage({
level: MessageLevel.success,
message: t`Successfully generated recovery link`,
description: rec.link,
});
})
.catch((ex: ResponseError) => {
ex.response.json().then(() => {
showMessage({
level: MessageLevel.error,
message: t`No recovery flow is configured.`,
});
});
});
}}
>
${t`Copy recovery link`}
</ak-action-button>
${item.email
? html`<ak-forms-modal
.closeAfterSuccessfulSubmit=${false}
>
<span slot="submit"> ${t`Send link`} </span>
<span slot="header">
${t`Send recovery link to user`}
</span>
<ak-user-reset-email-form
slot="form"
.user=${item}
>
</ak-user-reset-email-form>
<button
slot="trigger"
class="pf-c-button pf-m-secondary"
>
${t`Email recovery link`}
</button>
</ak-forms-modal>`
: html`<span
>${t`Recovery link cannot be emailed, user has no email address saved.`}</span
>`}
`
: html` <p>
${t`To let a user directly reset a their password, configure a recovery flow on the currently active tenant.`}
</p>`}
</div>
</dd>
</div>

View File

@ -1,13 +1,14 @@
import { AdminInterface } from "@goauthentik/admin/AdminInterface";
import "@goauthentik/admin/users/ServiceAccountForm";
import "@goauthentik/admin/users/UserActiveForm";
import "@goauthentik/admin/users/UserForm";
import "@goauthentik/admin/users/UserPasswordForm";
import "@goauthentik/admin/users/UserResetEmailForm";
import { DEFAULT_CONFIG, config, tenant } from "@goauthentik/common/api/config";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { MessageLevel } from "@goauthentik/common/messages";
import { uiConfig } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users";
import { first } from "@goauthentik/common/utils";
import { rootInterface } from "@goauthentik/elements/Base";
import { PFColor } from "@goauthentik/elements/Label";
import { PFSize } from "@goauthentik/elements/Spinner";
import "@goauthentik/elements/TreeView";
@ -23,14 +24,13 @@ import { TablePage } from "@goauthentik/elements/table/TablePage";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { until } from "lit/directives/until.js";
import { customElement, property, state } from "lit/decorators.js";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import { CapabilitiesEnum, CoreApi, ResponseError, User } from "@goauthentik/api";
import { CapabilitiesEnum, CoreApi, ResponseError, User, UserPath } from "@goauthentik/api";
@customElement("ak-user-list")
export class UserListPage extends TablePage<User> {
@ -56,18 +56,25 @@ export class UserListPage extends TablePage<User> {
@property()
activePath = getURLParam<string>("path", "/");
@state()
userPaths?: UserPath;
static get styles(): CSSResult[] {
return super.styles.concat(PFDescriptionList, PFCard, PFAlert);
}
async apiEndpoint(page: number): Promise<PaginatedResponse<User>> {
return new CoreApi(DEFAULT_CONFIG).coreUsersList({
const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList({
ordering: this.order,
page: page,
pageSize: (await uiConfig()).pagination.perPage,
search: this.search || "",
pathStartswith: getURLParam("path", ""),
});
this.userPaths = await new CoreApi(DEFAULT_CONFIG).coreUsersPathsRetrieve({
search: this.search,
});
return users;
}
columns(): TableColumn[] {
@ -81,6 +88,10 @@ export class UserListPage extends TablePage<User> {
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
const currentUser = rootInterface<AdminInterface>()?.user;
const shouldShowWarning = this.selectedElements.find((el) => {
return el.pk === currentUser?.user.pk || el.pk == currentUser?.original?.pk;
});
return html`<ak-forms-delete-bulk
objectLabel=${t`User(s)`}
.objects=${this.selectedElements}
@ -102,28 +113,18 @@ export class UserListPage extends TablePage<User> {
});
}}
>
${until(
me().then((user) => {
const shouldShowWarning = this.selectedElements.find((el) => {
return el.pk === user.user.pk || el.pk == user.original?.pk;
});
if (shouldShowWarning) {
return html`
<div slot="notice" class="pf-c-form__alert">
<div class="pf-c-alert pf-m-inline pf-m-warning">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-circle"></i>
</div>
<h4 class="pf-c-alert__title">
${t`Warning: You're about to delete the user you're logged in as (${shouldShowWarning.username}). Proceed at your own risk.`}
</h4>
</div>
</div>
`;
}
return html``;
}),
)}
${shouldShowWarning
? html`<div slot="notice" class="pf-c-form__alert">
<div class="pf-c-alert pf-m-inline pf-m-warning">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-circle"></i>
</div>
<h4 class="pf-c-alert__title">
${t`Warning: You're about to delete the user you're logged in as (${shouldShowWarning.username}). Proceed at your own risk.`}
</h4>
</div>
</div>`
: html``}
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${t`Delete`}
</button>
@ -148,19 +149,16 @@ export class UserListPage extends TablePage<User> {
<i class="fas fa-edit"></i>
</button>
</ak-forms-modal>
${until(
config().then((config) => {
if (config.capabilities.includes(CapabilitiesEnum.Impersonate)) {
return html`<a
class="pf-c-button pf-m-tertiary"
href="${`/-/impersonation/${item.pk}/`}"
>
${t`Impersonate`}
</a>`;
}
return html``;
}),
)}`,
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.Impersonate)
? html`
<a
class="pf-c-button pf-m-tertiary"
href="${`/-/impersonation/${item.pk}/`}"
>
${t`Impersonate`}
</a>
`
: html``}`,
];
}
@ -194,7 +192,7 @@ export class UserListPage extends TablePage<User> {
return new CoreApi(
DEFAULT_CONFIG,
).coreUsersPartialUpdate({
id: item.pk || 0,
id: item.pk,
patchedUserRequest: {
isActive: !item.isActive,
},
@ -225,70 +223,61 @@ export class UserListPage extends TablePage<User> {
${t`Set password`}
</button>
</ak-forms-modal>
${until(
tenant().then((tenant) => {
if (!tenant.flowRecovery) {
return html`
<p>
${t`To let a user directly reset a their password, configure a recovery flow on the currently active tenant.`}
</p>
`;
}
return html`
<ak-action-button
class="pf-m-secondary"
.apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersRecoveryRetrieve({
id: item.pk || 0,
})
.then((rec) => {
showMessage({
level: MessageLevel.success,
message: t`Successfully generated recovery link`,
description: rec.link,
});
})
.catch((ex: ResponseError) => {
ex.response.json().then(() => {
showMessage({
level: MessageLevel.error,
message: t`No recovery flow is configured.`,
});
});
});
}}
>
${t`Copy recovery link`}
</ak-action-button>
${item.email
? html`<ak-forms-modal
.closeAfterSuccessfulSubmit=${false}
>
<span slot="submit">
${t`Send link`}
</span>
<span slot="header">
${t`Send recovery link to user`}
</span>
<ak-user-reset-email-form
slot="form"
.user=${item}
>
</ak-user-reset-email-form>
<button
slot="trigger"
class="pf-c-button pf-m-secondary"
>
${t`Email recovery link`}
</button>
</ak-forms-modal>`
: html`<span
>${t`Recovery link cannot be emailed, user has no email address saved.`}</span
>`}
`;
}),
)}
${rootInterface()?.tenant?.flowRecovery
? html`
<ak-action-button
class="pf-m-secondary"
.apiRequest=${() => {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersRecoveryRetrieve({
id: item.pk,
})
.then((rec) => {
showMessage({
level: MessageLevel.success,
message: t`Successfully generated recovery link`,
description: rec.link,
});
})
.catch((ex: ResponseError) => {
ex.response.json().then(() => {
showMessage({
level: MessageLevel.error,
message: t`No recovery flow is configured.`,
});
});
});
}}
>
${t`Copy recovery link`}
</ak-action-button>
${item.email
? html`<ak-forms-modal
.closeAfterSuccessfulSubmit=${false}
>
<span slot="submit"> ${t`Send link`} </span>
<span slot="header">
${t`Send recovery link to user`}
</span>
<ak-user-reset-email-form
slot="form"
.user=${item}
>
</ak-user-reset-email-form>
<button
slot="trigger"
class="pf-c-button pf-m-secondary"
>
${t`Email recovery link`}
</button>
</ak-forms-modal>`
: html`<span
>${t`Recovery link cannot be emailed, user has no email address saved.`}</span
>`}
`
: html` <p>
${t`To let a user directly reset a their password, configure a recovery flow on the currently active tenant.`}
</p>`}
</div>
</dd>
</div>
@ -323,18 +312,10 @@ export class UserListPage extends TablePage<User> {
<div class="pf-c-card">
<div class="pf-c-card__title">${t`User folders`}</div>
<div class="pf-c-card__body">
${until(
new CoreApi(DEFAULT_CONFIG)
.coreUsersPathsRetrieve({
search: this.search,
})
.then((paths) => {
return html`<ak-treeview
.items=${paths.paths}
activePath=${this.activePath}
></ak-treeview>`;
}),
)}
<ak-treeview
.items=${this.userPaths?.paths || []}
activePath=${this.activePath}
></ak-treeview>
</div>
</div>
</div>`;

View File

@ -3,10 +3,10 @@ import "@goauthentik/admin/users/UserActiveForm";
import "@goauthentik/admin/users/UserChart";
import "@goauthentik/admin/users/UserForm";
import "@goauthentik/admin/users/UserPasswordForm";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { MessageLevel } from "@goauthentik/common/messages";
import { AKElement } from "@goauthentik/elements/Base";
import { AKElement, rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import { PFColor } from "@goauthentik/elements/Label";
import "@goauthentik/elements/PageHeader";
@ -27,7 +27,6 @@ import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { until } from "lit/directives/until.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
@ -197,21 +196,18 @@ export class UserViewPage extends AKElement {
</button>
</ak-forms-modal>
</div>
${until(
config().then((config) => {
if (config.capabilities.includes(CapabilitiesEnum.Impersonate)) {
return html` <div class="pf-c-card__footer">
<a
class="pf-c-button pf-m-tertiary"
href="${`/-/impersonation/${this.user?.pk}/`}"
>
${t`Impersonate`}
</a>
</div>`;
}
return html``;
}),
)}
${rootInterface()?.config?.capabilities.includes(
CapabilitiesEnum.Impersonate,
)
? html`
<a
class="pf-c-button pf-m-tertiary"
href="${`/-/impersonation/${this.user?.pk}/`}"
>
${t`Impersonate`}
</a>
`
: html``}
<div class="pf-c-card__footer">
<ak-user-active-form
.obj=${this.user}