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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 699 additions and 744 deletions

1
.github/stale.yml vendored
View file

@ -16,3 +16,4 @@ markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
only: issues

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}

View file

@ -1,4 +1,4 @@
import { DEFAULT_CONFIG, tenant } from "@goauthentik/common/api/config";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import {
EVENT_API_DRAWER_TOGGLE,
EVENT_NOTIFICATION_DRAWER_TOGGLE,
@ -8,7 +8,7 @@ import {
} from "@goauthentik/common/constants";
import { currentInterface } from "@goauthentik/common/sentry";
import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base";
import { AKElement, rootInterface } from "@goauthentik/elements/Base";
import { t } from "@lingui/macro";
@ -35,17 +35,16 @@ export class PageHeader extends AKElement {
@property()
set header(value: string) {
tenant().then((tenant) => {
const currentIf = currentInterface();
let title = tenant.brandingTitle || TITLE_DEFAULT;
if (currentIf === "admin") {
title = `${t`Admin`} - ${title}`;
}
if (value !== "") {
title = `${value} - ${title}`;
}
document.title = title;
});
const tenant = rootInterface()?.tenant;
const currentIf = currentInterface();
let title = tenant?.brandingTitle || TITLE_DEFAULT;
if (currentIf === "admin") {
title = `${t`Admin`} - ${title}`;
}
if (value !== "") {
title = `${value} - ${title}`;
}
document.title = title;
this._header = value;
}

View file

@ -73,12 +73,8 @@ export class Tabs extends AKElement {
updateURLParams(params);
const page = this.querySelector(`[slot='${this.currentPage}']`);
if (!page) return;
page.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
page.dispatchEvent(new CustomEvent(EVENT_REFRESH));
page.dispatchEvent(new CustomEvent("activate"));
}
renderTab(page: Element): TemplateResult {
@ -94,10 +90,10 @@ export class Tabs extends AKElement {
const pages = Array.from(this.querySelectorAll(":scope > [slot^='page-']"));
if (window.location.hash.includes(ROUTE_SEPARATOR)) {
const params = getURLParams();
if (this.pageIdentifier in params) {
if (this.pageIdentifier in params && !this.currentPage) {
if (this.querySelector(`[slot='${params[this.pageIdentifier]}']`) !== null) {
// To update the URL to match with the current slot
this.currentPage = params[this.pageIdentifier] as string;
this.onClick(params[this.pageIdentifier] as string);
}
}
}

View file

@ -11,13 +11,12 @@ export class AggregatePromiseCard extends AggregateCard {
@property({ attribute: false })
promise?: Promise<Record<string, unknown>>;
promiseProxy(): Promise<TemplateResult> {
async promiseProxy(): Promise<TemplateResult> {
if (!this.promise) {
return new Promise<TemplateResult>(() => html``);
return html``;
}
return this.promise.then((s) => {
return html`<i class="fa fa-check-circle"></i>&nbsp;${s.toString()}`;
});
const value = await this.promise;
return html`<i class="fa fa-check-circle"></i>&nbsp;${value.toString()}`;
}
renderInner(): TemplateResult {

View file

@ -4,7 +4,6 @@ import { AKElement } from "@goauthentik/elements/Base";
import { CSSResult, css } from "lit";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { until } from "lit/directives/until.js";
import PFNav from "@patternfly/patternfly/components/Nav/nav.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
@ -72,9 +71,6 @@ export class SidebarItem extends AKElement {
@property()
path?: string;
@property({ attribute: false })
condition: () => Promise<boolean> = async () => true;
activeMatchers: RegExp[] = [];
@property({ type: Boolean })
@ -145,16 +141,10 @@ export class SidebarItem extends AKElement {
}
render(): TemplateResult {
return html`${until(this.renderInner())}`;
return this.renderInner();
}
async renderInner(): Promise<TemplateResult> {
if (this.condition) {
const result = await this.condition();
if (!result) {
return html``;
}
}
renderInner(): TemplateResult {
if (this.childItems.length > 0) {
return html`<li
class="pf-c-nav__item ${this.expanded ? "pf-m-expandable pf-m-expanded" : ""}"

View file

@ -1,10 +1,9 @@
import { me } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base";
import { AdminInterface } from "@goauthentik/admin/AdminInterface";
import { AKElement, rootInterface } from "@goauthentik/elements/Base";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
import PFNav from "@patternfly/patternfly/components/Nav/nav.css";
@ -34,18 +33,12 @@ export class SidebarUser extends AKElement {
}
render(): TemplateResult {
const me = rootInterface<AdminInterface>()?.user;
return html`
<a href="/if/user/#/settings" class="pf-c-nav__link user-avatar" id="user-settings">
${until(
me().then((u) => {
return html`<img
class="pf-c-avatar"
src="${ifDefined(u.user.avatar)}"
alt=""
/>`;
}),
html``,
)}
${me
? html`<img class="pf-c-avatar" src="${ifDefined(me.user.avatar)}" alt="" />`
: html``}
</a>
<a href="/flows/-/default/invalidation/" class="pf-c-nav__link user-logout" id="logout">
<i class="fas fa-sign-out-alt" aria-hidden="true"></i>

View file

@ -1,4 +1,4 @@
import { DEFAULT_CONFIG, tenant } from "@goauthentik/common/api/config";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import {
EVENT_FLOW_ADVANCE,
EVENT_FLOW_INSPECTOR_TOGGLE,
@ -64,13 +64,11 @@ export class FlowExecutor extends Interface implements StageHost {
);
window.location.assign((value as RedirectChallenge).to);
}
tenant().then((tenant) => {
if (value?.flowInfo?.title) {
document.title = `${value.flowInfo?.title} - ${tenant.brandingTitle}`;
} else {
document.title = tenant.brandingTitle || TITLE_DEFAULT;
}
});
if (value?.flowInfo?.title) {
document.title = `${value.flowInfo?.title} - ${this.tenant?.brandingTitle}`;
} else {
document.title = this.tenant?.brandingTitle || TITLE_DEFAULT;
}
this.requestUpdate();
}
@ -527,15 +525,13 @@ export class FlowExecutor extends Interface implements StageHost {
<footer class="pf-c-login__footer">
<p></p>
<ul class="pf-c-list pf-m-inline">
${until(
this.tenant?.uiFooterLinks?.map((link) => {
return html`<li>
<a href="${link.href || ""}"
>${link.name}</a
>
</li>`;
}),
)}
${this.tenant?.uiFooterLinks?.map((link) => {
return html`<li>
<a href="${link.href || ""}"
>${link.name}</a
>
</li>`;
})}
<li>
<a
href="https://goauthentik.io?utm_source=authentik&amp;utm_medium=flow"

View file

@ -40,6 +40,7 @@ export class AuthenticatorValidateStage
set loading(value: boolean) {
this.host.loading = value;
}
get loading(): boolean {
return this.host.loading;
}

View file

@ -1,14 +1,12 @@
import { uiConfig } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users";
import { truncateWords } from "@goauthentik/common/utils";
import { AKElement } from "@goauthentik/elements/Base";
import { AKElement, rootInterface } from "@goauthentik/elements/Base";
import { UserInterface } from "@goauthentik/user/UserInterface";
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, css, 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 PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -80,6 +78,7 @@ export class LibraryApplication extends AKElement {
if (!this.application) {
return html`<ak-spinner></ak-spinner>`;
}
const me = rootInterface<UserInterface>()?.me;
return html` <div
class="pf-c-card pf-m-hoverable pf-m-compact ${this.selected
? "pf-m-selectable pf-m-selected"
@ -93,24 +92,16 @@ export class LibraryApplication extends AKElement {
>
${this.renderIcon()}
</a>
${until(
uiConfig().then((config) => {
if (!config.enabledFeatures.applicationEdit) {
return html``;
}
return me().then((u) => {
if (!u.user.isSuperuser) return html``;
return html`
<a
class="pf-c-button pf-m-control pf-m-small"
href="/if/admin/#/core/applications/${this.application?.slug}"
>
<i class="fas fa-pencil-alt"></i>
</a>
`;
});
}),
)}
${rootInterface()?.uiConfig?.enabledFeatures.applicationEdit && me?.user.isSuperuser
? html`
<a
class="pf-c-button pf-m-control pf-m-small"
href="/if/admin/#/core/applications/${this.application?.slug}"
>
<i class="fas fa-pencil-alt"></i>
</a>
`
: html``}
</div>
<div class="pf-c-card__title">
<p>

View file

@ -1,7 +1,7 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { LayoutType, UIConfig, uiConfig } from "@goauthentik/common/ui/config";
import { LayoutType } from "@goauthentik/common/ui/config";
import { groupBy } from "@goauthentik/common/utils";
import { AKElement } from "@goauthentik/elements/Base";
import { AKElement, rootInterface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/EmptyState";
import { getURLParam, updateURLParams } from "@goauthentik/elements/router/RouteMatch";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
@ -12,7 +12,7 @@ import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { until } from "lit/directives/until.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
@ -130,10 +130,11 @@ export class LibraryPage extends AKElement {
return groupBy(this.filterApps(), (app) => app.group || "");
}
renderApps(config: UIConfig): TemplateResult {
renderApps(): TemplateResult {
let groupClass = "";
let groupGrid = "";
switch (config.layout.type) {
const uiConfig = rootInterface()?.uiConfig;
switch (uiConfig?.layout.type) {
case LayoutType.row:
groupClass = "pf-m-12-col";
groupGrid =
@ -161,7 +162,7 @@ export class LibraryPage extends AKElement {
return html`<ak-library-app
class="pf-l-grid__item"
.application=${app}
background=${config.theme.cardBackground}
background=${ifDefined(uiConfig?.theme.cardBackground)}
?selected=${app.slug === this.selectedApp?.slug}
></ak-library-app>`;
})}
@ -172,57 +173,48 @@ export class LibraryPage extends AKElement {
}
render(): TemplateResult {
return html`${until(
uiConfig().then((config) => {
return html`<main
role="main"
class="pf-c-page__main"
tabindex="-1"
id="main-content"
>
<div class="pf-c-content header">
<h1>${t`My applications`}</h1>
${config.enabledFeatures.search
? html`<input
@input=${(ev: InputEvent) => {
this.query = (ev.target as HTMLInputElement).value;
updateURLParams({
search: this.query,
});
if (!this.fuse) return;
const apps = this.fuse.search(this.query);
if (apps.length < 1) return;
this.selectedApp = apps[0].item;
}}
@keydown=${(ev: KeyboardEvent) => {
if (ev.key === "Enter" && this.selectedApp?.launchUrl) {
window.location.assign(this.selectedApp.launchUrl);
} else if (ev.key === "Escape") {
(ev.target as HTMLInputElement).value = "";
this.query = "";
updateURLParams({
search: this.query,
});
this.selectedApp = undefined;
}
}}
type="text"
class="pf-u-display-none pf-u-display-block-on-md"
autofocus
placeholder=${t`Search...`}
/>`
: html``}
</div>
<section class="pf-c-page__main-section">
${loading(
this.apps,
html`${this.filterApps().length > 0
? this.renderApps(config)
: this.renderEmptyState()}`,
)}
</section>
</main>`;
}),
)}`;
return html`<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
<div class="pf-c-content header">
<h1>${t`My applications`}</h1>
${rootInterface()?.uiConfig?.enabledFeatures.search
? html`<input
@input=${(ev: InputEvent) => {
this.query = (ev.target as HTMLInputElement).value;
updateURLParams({
search: this.query,
});
if (!this.fuse) return;
const apps = this.fuse.search(this.query);
if (apps.length < 1) return;
this.selectedApp = apps[0].item;
}}
@keydown=${(ev: KeyboardEvent) => {
if (ev.key === "Enter" && this.selectedApp?.launchUrl) {
window.location.assign(this.selectedApp.launchUrl);
} else if (ev.key === "Escape") {
(ev.target as HTMLInputElement).value = "";
this.query = "";
updateURLParams({
search: this.query,
});
this.selectedApp = undefined;
}
}}
type="text"
class="pf-u-display-none pf-u-display-block-on-md"
autofocus
placeholder=${t`Search...`}
/>`
: html``}
</div>
<section class="pf-c-page__main-section">
${loading(
this.apps,
html`${this.filterApps().length > 0
? this.renderApps()
: this.renderEmptyState()}`,
)}
</section>
</main>`;
}
}

View file

@ -1,10 +1,10 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } 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/Tabs";
import "@goauthentik/elements/user/SessionList";
import "@goauthentik/elements/user/UserConsentList";
import { UserInterface } from "@goauthentik/user/UserInterface";
import "@goauthentik/user/user-settings/details/UserPassword";
import "@goauthentik/user/user-settings/details/UserSettingsFlowExecutor";
import "@goauthentik/user/user-settings/mfa/MFADevicesPage";
@ -16,7 +16,6 @@ import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
@ -62,7 +61,7 @@ export class UserSettingsPage extends AKElement {
}
@state()
userSettings!: Promise<UserSetting[]>;
userSettings?: UserSetting[];
constructor() {
super();
@ -71,11 +70,14 @@ export class UserSettingsPage extends AKElement {
});
}
firstUpdated(): void {
this.userSettings = new StagesApi(DEFAULT_CONFIG).stagesAllUserSettingsList();
async firstUpdated(): Promise<void> {
this.userSettings = await new StagesApi(DEFAULT_CONFIG).stagesAllUserSettingsList();
}
render(): TemplateResult {
const pwStage = this.userSettings?.filter(
(stage) => stage.component === "ak-user-settings-password",
);
return html`<div class="pf-c-page">
<main role="main" class="pf-c-page__main" tabindex="-1">
<ak-tabs ?vertical="${true}">
@ -89,20 +91,11 @@ export class UserSettingsPage extends AKElement {
<ak-user-settings-flow-executor></ak-user-settings-flow-executor>
</div>
<div class="pf-l-stack__item">
${until(
this.userSettings?.then((settings) => {
const pwStage = settings.filter(
(stage) =>
stage.component === "ak-user-settings-password",
);
if (pwStage.length > 0) {
return html`<ak-user-settings-password
configureUrl=${ifDefined(pwStage[0].configureUrl)}
></ak-user-settings-password>`;
}
return html``;
}),
)}
${pwStage
? html`<ak-user-settings-password
configureUrl=${ifDefined(pwStage[0].configureUrl)}
></ak-user-settings-password>`
: html``}
</div>
</div>
</section>
@ -111,26 +104,20 @@ export class UserSettingsPage extends AKElement {
data-tab-title="${t`Sessions`}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
${until(
me().then((u) => {
return html`<ak-user-session-list
targetUser=${u.user.username}
></ak-user-session-list>`;
}),
)}
<ak-user-session-list
targetUser=${ifDefined(
rootInterface<UserInterface>()?.me?.user.username,
)}
></ak-user-session-list>
</section>
<section
slot="page-consents"
data-tab-title="${t`Consent`}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
${until(
me().then((u) => {
return html`<ak-user-consent-list
userId=${u.user.pk}
></ak-user-consent-list>`;
}),
)}
<ak-user-consent-list
userId=${ifDefined(rootInterface<UserInterface>()?.me?.user.pk)}
></ak-user-consent-list>
</section>
<section
slot="page-mfa"

View file

@ -1,8 +1,8 @@
import { DEFAULT_CONFIG, tenant } 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 { refreshMe } from "@goauthentik/common/users";
import { AKElement } from "@goauthentik/elements/Base";
import { AKElement, rootInterface } from "@goauthentik/elements/Base";
import { showMessage } from "@goauthentik/elements/messages/MessageContainer";
import { StageHost } from "@goauthentik/flow/stages/base";
import "@goauthentik/user/user-settings/details/stages/prompt/PromptStage";
@ -22,7 +22,6 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import {
ChallengeChoices,
ChallengeTypes,
CurrentTenant,
FlowChallengeResponseRequest,
FlowErrorChallenge,
FlowsApi,
@ -51,18 +50,10 @@ export class UserSettingsFlowExecutor extends AKElement implements StageHost {
@property({ type: Boolean })
loading = false;
@property({ attribute: false })
tenant!: CurrentTenant;
static get styles(): CSSResult[] {
return [PFBase, PFCard, PFPage, PFButton, PFContent];
}
constructor() {
super();
tenant().then((tenant) => (this.tenant = tenant));
}
submit(payload?: FlowChallengeResponseRequest): Promise<boolean> {
if (!payload) return Promise.reject();
if (!this.challenge) return Promise.reject();
@ -93,13 +84,12 @@ export class UserSettingsFlowExecutor extends AKElement implements StageHost {
}
firstUpdated(): void {
tenant().then((tenant) => {
this.flowSlug = tenant.flowUserSettings;
if (!this.flowSlug) {
return;
}
this.nextChallenge();
});
const tenant = rootInterface()?.tenant;
this.flowSlug = tenant?.flowUserSettings;
if (!this.flowSlug) {
return;
}
this.nextChallenge();
}
async nextChallenge(): Promise<void> {

View file

@ -13,7 +13,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 { AuthenticatorsApi, Device, UserSetting } from "@goauthentik/api";
@ -47,7 +46,7 @@ export function deviceTypeName(device: Device): string {
@customElement("ak-user-settings-mfa")
export class MFADevicesPage extends Table<Device> {
@property({ attribute: false })
userSettings?: Promise<UserSetting[]>;
userSettings?: UserSetting[];
checkbox = true;
@ -70,41 +69,32 @@ export class MFADevicesPage extends Table<Device> {
}
renderToolbar(): TemplateResult {
const settings = (this.userSettings || []).filter((stage) => {
if (stage.component === "ak-user-settings-password") {
return false;
}
return stage.configureUrl;
});
return html`<ak-dropdown class="pf-c-dropdown">
<button class="pf-m-primary pf-c-dropdown__toggle" type="button">
<span class="pf-c-dropdown__toggle-text">${t`Enroll`}</span>
<i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
</button>
<ul class="pf-c-dropdown__menu" hidden>
${until(
this.userSettings?.then((stages) => {
return stages
.filter((stage) => {
if (stage.component === "ak-user-settings-password") {
return false;
}
return stage.configureUrl;
})
.map((stage) => {
return html`<li>
<a
href="${ifDefined(stage.configureUrl)}${AndNext(
`/if/user/#/settings;${JSON.stringify({
page: "page-mfa",
})}`,
)}"
class="pf-c-dropdown__menu-item"
>
${stageToAuthenticatorName(stage)}
</a>
</li>`;
});
}),
html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}
></ak-empty-state>`,
)}
${settings.map((stage) => {
return html`<li>
<a
href="${ifDefined(stage.configureUrl)}${AndNext(
`/if/user/#/settings;${JSON.stringify({
page: "page-mfa",
})}`,
)}"
class="pf-c-dropdown__menu-item"
>
${stageToAuthenticatorName(stage)}
</a>
</li>`;
})}
</ul>
</ak-dropdown>
${super.renderToolbar()}`;

View file

@ -12,7 +12,6 @@ import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { until } from "lit/directives/until.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFDataList from "@patternfly/patternfly/components/DataList/data-list.css";
@ -22,7 +21,7 @@ import { PaginatedUserSourceConnectionList, SourcesApi, UserSetting } from "@goa
@customElement("ak-user-settings-source")
export class UserSourceSettingsPage extends AKElement {
@property({ attribute: false })
sourceSettings?: Promise<UserSetting[]>;
sourceSettings?: UserSetting[];
@property({ attribute: false })
connections?: PaginatedUserSourceConnectionList;
@ -57,7 +56,7 @@ export class UserSourceSettingsPage extends AKElement {
async firstUpdated(): Promise<void> {
const user = await me();
this.sourceSettings = new SourcesApi(DEFAULT_CONFIG).sourcesAllUserSettingsList();
this.sourceSettings = await new SourcesApi(DEFAULT_CONFIG).sourcesAllUserSettingsList();
this.connections = await new SourcesApi(DEFAULT_CONFIG).sourcesUserConnectionsAllList({
user: user.user.pk,
});
@ -115,30 +114,33 @@ export class UserSourceSettingsPage extends AKElement {
</p>
</div>
<ul class="pf-c-data-list" role="list">
${until(
this.sourceSettings?.then((source) => {
if (source.length < 1) {
return html`<ak-empty-state
header=${t`No services available.`}
></ak-empty-state>`;
}
return source.map((source) => {
return html`<li class="pf-c-data-list__item">
<div class="pf-c-data-list__item-content">
<div class="pf-c-data-list__cell">
${renderSourceIcon(source.title, source.iconUrl)}
${source.title}
</div>
<div class="pf-c-data-list__cell">
${this.renderSourceSettings(source)}
</div>
</div>
</li>`;
});
}),
html`<ak-empty-state ?loading="${true}" header=${t`Loading`}>
</ak-empty-state>`,
)}
${this.sourceSettings
? html`
${this.sourceSettings.length < 1
? html`<ak-empty-state
header=${t`No services available.`}
></ak-empty-state>`
: html`
${this.sourceSettings.map((source) => {
return html`<li class="pf-c-data-list__item">
<div class="pf-c-data-list__item-content">
<div class="pf-c-data-list__cell">
${renderSourceIcon(
source.title,
source.iconUrl,
)}
${source.title}
</div>
<div class="pf-c-data-list__cell">
${this.renderSourceSettings(source)}
</div>
</div>
</li>`;
})}
`}
`
: html`<ak-empty-state ?loading="${true}" header=${t`Loading`}>
</ak-empty-state>`}
</ul>`;
}
}