mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
web: Fix stale clipboard tokens, untranslated labels (#23063)
* web: Fix stale clipboard tokens, untranslated labels. * Fix tooltip. * Fix type error. * Update types. * Fix types. Clean up composite. * Fix label names. * Fix broken HTML. * Fix labels, formatters. * Clean up properties, lifecyle.
This commit is contained in:
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
export type Token = unknown;
|
||||
|
||||
export type LexerAction = (
|
||||
this: Lexer,
|
||||
match: string,
|
||||
...captures: string[]
|
||||
) => Token | Token[] | null | void;
|
||||
|
||||
export type DefunctHandler = (this: Lexer, chr: string) => Token | Token[] | null | void;
|
||||
|
||||
export class Lexer {
|
||||
state: number;
|
||||
index: number;
|
||||
input: string;
|
||||
reject: boolean;
|
||||
|
||||
constructor(defunct?: DefunctHandler);
|
||||
addRule(pattern: RegExp, action: LexerAction, start?: number[]): this;
|
||||
setInput(input: string): this;
|
||||
lex(): Token | null;
|
||||
}
|
||||
|
||||
export default Lexer;
|
||||
Vendored
+5
-4
@@ -174,10 +174,11 @@ export class Lexer {
|
||||
this.reject = false;
|
||||
this.#remove++;
|
||||
|
||||
let token = match.action.apply(
|
||||
this,
|
||||
/** @type {string[]} */ (/** @type {unknown} */ (result)),
|
||||
);
|
||||
// TypeScript Native's assessment is correct.
|
||||
// but TypeScript Node can't make the connection between regex and the array.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - Remove after TypeScript Native is mainline.
|
||||
let token = match.action.apply(this, result);
|
||||
|
||||
if (this.reject) {
|
||||
this.index = result.index;
|
||||
|
||||
Vendored
+2
@@ -5,8 +5,10 @@
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"types": "./index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./index.d.ts",
|
||||
"import": "./index.js"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ import { customElement } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-endpoints-connectors-list")
|
||||
export class ConnectorsListPage extends TablePage<Connector> {
|
||||
public static override verboseName = msg("Connector");
|
||||
public static override verboseNamePlural = msg("Connectors");
|
||||
|
||||
public override searchPlaceholder = msg("Search connectors by name or type...");
|
||||
public override pageIcon = "pf-icon pf-icon-data-source";
|
||||
public override pageTitle = msg("Connectors");
|
||||
|
||||
@@ -26,6 +26,9 @@ import { customElement, property } from "lit/decorators.js";
|
||||
export class EnrollmentTokenListPage extends Table<EnrollmentToken> {
|
||||
#api = aki(EndpointsApi);
|
||||
|
||||
public static override verboseName = msg("Enrollment Token");
|
||||
public static override verboseNamePlural = msg("Enrollment Tokens");
|
||||
|
||||
protected override searchEnabled = true;
|
||||
protected emptyStateMessage = msg("No enrollment tokens found for this connector.");
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ export class DeviceListPage extends TablePage<EndpointDevice> {
|
||||
${inner
|
||||
? inner
|
||||
: html`<ak-empty-state icon=${this.pageIcon}
|
||||
><span>${msg("No objects found.")}</span>
|
||||
><span>${this.formatEmptyStateMessage()}</span>
|
||||
<div slot="body">
|
||||
${this.search ? this.renderEmptyClearSearch() : nothing}
|
||||
<p>
|
||||
|
||||
@@ -244,7 +244,7 @@ export class ObjectReviewIteration extends Table<Review> {
|
||||
protected override renderEmpty(): SlottedTemplateResult {
|
||||
return super.renderEmpty(
|
||||
html` <ak-empty-state icon="pf-icon-task"
|
||||
><span>${this.emptyStateMessage}</span></ak-empty-state
|
||||
><span>${this.formatEmptyStateMessage()}</span></ak-empty-state
|
||||
>`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList
|
||||
|
||||
@customElement("ak-rac-connection-token-list")
|
||||
export class ConnectionTokenListPage extends Table<ConnectionToken> {
|
||||
public static override verboseName = msg("Connection Token");
|
||||
public static override verboseNamePlural = msg("Connection Tokens");
|
||||
|
||||
checkbox = true;
|
||||
clearOnRefresh = true;
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export class InvitationWizard extends AKElement implements TransclusionChildElem
|
||||
|
||||
protected override render(): SlottedTemplateResult {
|
||||
return html`<ak-wizard
|
||||
entity-singular=${msg("Invitation")}
|
||||
verbose-name=${msg("Invitation")}
|
||||
description=${msg("Create a new invitation with an enrollment flow.")}
|
||||
.initialSteps=${["flow-step", "details-step", "success-step"]}
|
||||
>
|
||||
|
||||
@@ -16,7 +16,7 @@ import { CoreApi, CoreUsersListRequest, IntentEnum, Token, User } from "@goauthe
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
const EXPIRATION_DURATION = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
@@ -25,6 +25,12 @@ export class TokenForm extends ModelForm<Token, string> {
|
||||
public static override verboseName = msg("Token");
|
||||
public static override verboseNamePlural = msg("Tokens");
|
||||
|
||||
/**
|
||||
* Pre-selected user for new tokens, e.g. when creating a token from a user's detail page.
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public defaultUser: User | null = null;
|
||||
|
||||
protected expirationMinimumDate = new Date();
|
||||
|
||||
@state()
|
||||
@@ -114,7 +120,7 @@ export class TokenForm extends ModelForm<Token, string> {
|
||||
}
|
||||
|
||||
const users = await aki(CoreApi).coreUsersList(args);
|
||||
const instanceUser = this.instance?.userObj;
|
||||
const instanceUser = this.instance?.userObj ?? this.defaultUser;
|
||||
|
||||
if (!instanceUser) {
|
||||
return users.results;
|
||||
@@ -136,7 +142,11 @@ export class TokenForm extends ModelForm<Token, string> {
|
||||
return user?.pk;
|
||||
}}
|
||||
.selected=${(user: User): boolean => {
|
||||
return this.instance?.user === user.pk;
|
||||
if (this.instance) {
|
||||
return this.instance.user === user.pk;
|
||||
}
|
||||
|
||||
return this.defaultUser?.pk === user.pk;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
|
||||
@@ -8,7 +8,7 @@ import "#elements/forms/ModalForm";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { aki } from "#common/api/client";
|
||||
import { intentToLabel } from "#common/labels";
|
||||
import { formatIntentLabel } from "#common/labels";
|
||||
|
||||
import { IconTokenCopyButton } from "#elements/buttons/IconTokenCopyButton";
|
||||
import { IconEditButton, ModalInvokerButton } from "#elements/dialogs";
|
||||
@@ -94,7 +94,7 @@ export class TokenListPage extends TablePage<Token> {
|
||||
html`<a href="#/identity/users/${item.userObj?.pk}">${item.userObj?.username}</a>`,
|
||||
html`<ak-status-label type="warning" ?good=${item.expiring}></ak-status-label>`,
|
||||
Timestamp(item.expires && item.expiring ? item.expires : null),
|
||||
html`${intentToLabel(item.intent ?? IntentEnum.Api)}`,
|
||||
html`${formatIntentLabel(item.intent ?? IntentEnum.Api)}`,
|
||||
html`<div class="ak-c-table__actions">
|
||||
${!item.managed
|
||||
? IconEditButton(TokenForm, item.identifier, item.identifier)
|
||||
@@ -110,7 +110,7 @@ export class TokenListPage extends TablePage<Token> {
|
||||
model: ModelEnum.AuthentikCoreToken,
|
||||
objectPk: item.pk,
|
||||
})}
|
||||
${IconTokenCopyButton(item.identifier)}
|
||||
${IconTokenCopyButton(item)}
|
||||
</div>`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-user-device-table")
|
||||
export class UserDeviceTable extends Table<Device> {
|
||||
public static override verboseName = msg("Device");
|
||||
public static override verboseNamePlural = msg("Devices");
|
||||
|
||||
@property({ type: Number })
|
||||
userId?: number;
|
||||
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import "#admin/rbac/ObjectPermissionModal";
|
||||
import "#admin/tokens/TokenForm";
|
||||
import "#components/ak-status-label";
|
||||
import "#elements/buttons/Dropdown";
|
||||
import "#elements/buttons/TokenCopyButton/index";
|
||||
import "#elements/forms/DeleteBulkForm";
|
||||
import "#elements/forms/ModalForm";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { aki } from "#common/api/client";
|
||||
import { formatIntentLabel } from "#common/labels";
|
||||
|
||||
import { IconTokenCopyButton } from "#elements/buttons/IconTokenCopyButton";
|
||||
import { IconEditButton, ModalInvokerButton } from "#elements/dialogs";
|
||||
import { IconPermissionButton } from "#elements/dialogs/components/IconPermissionButton";
|
||||
import { showAPIErrorMessage } from "#elements/messages/MessageContainer";
|
||||
import { PaginatedResponse, Table, TableColumn, Timestamp } from "#elements/table/Table";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { TokenForm } from "#admin/tokens/TokenForm";
|
||||
|
||||
import { CoreApi, IntentEnum, ModelEnum, Token, User } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { html, nothing, PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-admin-user-token-list")
|
||||
export class AdminUserTokenList extends Table<Token> {
|
||||
public static override verboseName = msg("Token");
|
||||
public static override verboseNamePlural = msg("Tokens");
|
||||
|
||||
@property({ type: Number, attribute: "user-id", useDefault: true })
|
||||
public userID: number | null = null;
|
||||
|
||||
@property({ attribute: false, useDefault: true })
|
||||
public user: User | null = null;
|
||||
|
||||
protected override searchEnabled = true;
|
||||
|
||||
protected override rowLabel(item: Token): string | null {
|
||||
return item.identifier;
|
||||
}
|
||||
|
||||
public override checkbox = true;
|
||||
public override clearOnRefresh = true;
|
||||
public override order = "expires";
|
||||
|
||||
//#region Lifecycle
|
||||
|
||||
protected override async apiEndpoint(): Promise<PaginatedResponse<Token>> {
|
||||
if (!this.user) {
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
if (!this.user) {
|
||||
throw new TypeError("User is not set, cannot fetch tokens.");
|
||||
}
|
||||
|
||||
return aki(CoreApi).coreTokensList({
|
||||
...(await this.defaultEndpointConfig()),
|
||||
userUsername: this.user.username,
|
||||
});
|
||||
}
|
||||
|
||||
public refresh = () => {
|
||||
if (!this.userID) {
|
||||
return;
|
||||
}
|
||||
|
||||
return aki(CoreApi)
|
||||
.coreUsersRetrieve({
|
||||
id: this.userID!,
|
||||
})
|
||||
.then((user) => {
|
||||
this.user = user;
|
||||
})
|
||||
.catch(showAPIErrorMessage);
|
||||
};
|
||||
|
||||
protected override updated(changed: PropertyValues<this>) {
|
||||
super.updated(changed);
|
||||
|
||||
if (changed.has("userID") && this.userID !== null) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
//#region
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Rendering
|
||||
|
||||
protected override renderObjectCreate(): SlottedTemplateResult {
|
||||
if (!this.user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ModalInvokerButton(TokenForm, { defaultUser: this.user });
|
||||
}
|
||||
|
||||
protected columns: TableColumn[] = [
|
||||
[msg("Identifier"), "identifier"],
|
||||
[msg("Expires?"), "expiring"],
|
||||
[msg("Expiry date"), "expires"],
|
||||
[msg("Intent"), "intent"],
|
||||
[msg("Actions"), null, msg("Row Actions")],
|
||||
];
|
||||
|
||||
protected override renderToolbarSelected(): SlottedTemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
object-label=${msg("Token(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: Token) => {
|
||||
return [{ key: msg("Identifier"), value: item.identifier }];
|
||||
}}
|
||||
.usedBy=${(item: Token) => {
|
||||
return aki(CoreApi).coreTokensUsedByList({
|
||||
identifier: item.identifier,
|
||||
});
|
||||
}}
|
||||
.delete=${(item: Token) => {
|
||||
return aki(CoreApi).coreTokensDestroy({
|
||||
identifier: item.identifier,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
|
||||
${msg("Delete")}
|
||||
</button>
|
||||
</ak-forms-delete-bulk>`;
|
||||
}
|
||||
|
||||
protected override row(item: Token): SlottedTemplateResult[] {
|
||||
return [
|
||||
html`<div>${item.identifier}</div>
|
||||
${item.managed
|
||||
? html`<small>${msg("Token is managed by authentik.")}</small>`
|
||||
: nothing}`,
|
||||
html`<ak-status-label type="warning" ?good=${item.expiring}></ak-status-label>`,
|
||||
Timestamp(item.expires && item.expiring ? item.expires : null),
|
||||
html`${formatIntentLabel(item.intent ?? IntentEnum.Api)}`,
|
||||
html`<div class="ak-c-table__actions">
|
||||
${!item.managed
|
||||
? IconEditButton(TokenForm, item.identifier, item.identifier)
|
||||
: html`<button class="pf-c-button pf-m-plain" disabled type="button">
|
||||
<pf-tooltip
|
||||
position="top"
|
||||
content=${msg("Editing is disabled for managed tokens")}
|
||||
>
|
||||
<i class="fas fa-edit" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>`}
|
||||
${IconPermissionButton(item.identifier, {
|
||||
model: ModelEnum.AuthentikCoreToken,
|
||||
objectPk: item.pk,
|
||||
})}
|
||||
${IconTokenCopyButton(item)}
|
||||
</div>`,
|
||||
];
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-admin-user-token-list": AdminUserTokenList;
|
||||
}
|
||||
}
|
||||
+141
-136
@@ -8,6 +8,7 @@ import "#admin/users/UserChart";
|
||||
import "#admin/users/UserForm";
|
||||
import "#admin/users/UserImpersonateForm";
|
||||
import "#admin/users/UserPasswordForm";
|
||||
import "#admin/users/UserTokenList";
|
||||
import "#admin/users/oauth/UserAccessTokenList";
|
||||
import "#admin/users/oauth/UserRefreshTokenList";
|
||||
import "#components/DescriptionList";
|
||||
@@ -245,116 +246,123 @@ export class UserViewPage extends WithLicenseSummary(
|
||||
}
|
||||
|
||||
protected renderTabCredentialsToken(user: User): TemplateResult {
|
||||
return html`
|
||||
<ak-tabs pageIdentifier="userCredentialsTokens" vertical>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-sessions"
|
||||
id="page-sessions"
|
||||
aria-label=${msg("Sessions")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-user-session-list targetUser=${user.username}>
|
||||
</ak-user-session-list>
|
||||
</div>
|
||||
return html`<ak-tabs pageIdentifier="userCredentialsTokens" vertical>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-sessions"
|
||||
id="page-sessions"
|
||||
aria-label=${msg("Sessions")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-user-session-list targetUser=${user.username}></ak-user-session-list>
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-reputation"
|
||||
id="page-reputation"
|
||||
aria-label=${msg("Reputation scores")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-user-reputation-list
|
||||
targetUsername=${user.username}
|
||||
targetEmail=${ifDefined(user.email)}
|
||||
>
|
||||
</ak-user-reputation-list>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-tokens"
|
||||
id="page-tokens"
|
||||
aria-label=${msg("Tokens")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-admin-user-token-list .user=${user}></ak-admin-user-token-list>
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-consent"
|
||||
id="page-consent"
|
||||
aria-label=${msg("Explicit Consent")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-user-consent-list userId=${user.pk}> </ak-user-consent-list>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-reputation"
|
||||
id="page-reputation"
|
||||
aria-label=${msg("Reputation scores")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-user-reputation-list
|
||||
targetUsername=${user.username}
|
||||
targetEmail=${ifDefined(user.email)}
|
||||
></ak-user-reputation-list>
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-oauth-access"
|
||||
id="page-oauth-access"
|
||||
aria-label=${msg("OAuth Access Tokens")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-user-oauth-access-token-list userId=${user.pk}>
|
||||
</ak-user-oauth-access-token-list>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-consent"
|
||||
id="page-consent"
|
||||
aria-label=${msg("Explicit Consent")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-user-consent-list userId=${user.pk}></ak-user-consent-list>
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-oauth-refresh"
|
||||
id="page-oauth-refresh"
|
||||
aria-label=${msg("OAuth Refresh Tokens")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-user-oauth-refresh-token-list userId=${user.pk}>
|
||||
</ak-user-oauth-refresh-token-list>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-oauth-access"
|
||||
id="page-oauth-access"
|
||||
aria-label=${msg("OAuth Access Tokens")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-user-oauth-access-token-list
|
||||
userId=${user.pk}
|
||||
></ak-user-oauth-access-token-list>
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-mfa-authenticators"
|
||||
id="page-mfa-authenticators"
|
||||
aria-label=${msg("MFA Authenticators")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-user-device-table userId=${user.pk}> </ak-user-device-table>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-oauth-refresh"
|
||||
id="page-oauth-refresh"
|
||||
aria-label=${msg("OAuth Refresh Tokens")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-user-oauth-refresh-token-list
|
||||
userId=${user.pk}
|
||||
></ak-user-oauth-refresh-token-list>
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-source-connections"
|
||||
id="page-source-connections"
|
||||
aria-label=${msg("Connected services")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-user-settings-source user-id=${user.pk}>
|
||||
</ak-user-settings-source>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-mfa-authenticators"
|
||||
id="page-mfa-authenticators"
|
||||
aria-label=${msg("MFA Authenticators")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-user-device-table userId=${user.pk}></ak-user-device-table>
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-rac-connection-tokens"
|
||||
id="page-rac-connection-tokens"
|
||||
aria-label=${msg("RAC Connections")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-rac-connection-token-list userId=${user.pk}>
|
||||
</ak-rac-connection-token-list>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-source-connections"
|
||||
id="page-source-connections"
|
||||
aria-label=${msg("Connected services")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-user-settings-source user-id=${user.pk}></ak-user-settings-source>
|
||||
</div>
|
||||
</ak-tabs>
|
||||
</main>
|
||||
`;
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-rac-connection-tokens"
|
||||
id="page-rac-connection-tokens"
|
||||
aria-label=${msg("RAC Connections")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-rac-connection-token-list userId=${user.pk}></ak-rac-connection-token-list>
|
||||
</div>
|
||||
</div>
|
||||
</ak-tabs>`;
|
||||
}
|
||||
|
||||
protected renderTabApplications(user: User): TemplateResult {
|
||||
@@ -364,37 +372,35 @@ export class UserViewPage extends WithLicenseSummary(
|
||||
}
|
||||
|
||||
protected renderTabRoles(user: User): TemplateResult {
|
||||
return html`
|
||||
<ak-tabs pageIdentifier="userRoles" vertical>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-assigned-roles"
|
||||
id="page-assigned-roles"
|
||||
aria-label=${msg("Assigned Roles")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-related-role-table .targetUser=${user}></ak-related-role-table>
|
||||
</div>
|
||||
return html`<ak-tabs pageIdentifier="userRoles" vertical>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-assigned-roles"
|
||||
id="page-assigned-roles"
|
||||
aria-label=${msg("Assigned Roles")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-related-role-table .targetUser=${user}></ak-related-role-table>
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-all-roles"
|
||||
id="page-all-roles"
|
||||
aria-label=${msg("All Roles")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-related-role-table
|
||||
.targetUser=${user}
|
||||
showInherited
|
||||
></ak-related-role-table>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
role="tabpanel"
|
||||
tabindex="0"
|
||||
slot="page-all-roles"
|
||||
id="page-all-roles"
|
||||
aria-label=${msg("All Roles")}
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-related-role-table
|
||||
.targetUser=${user}
|
||||
showInherited
|
||||
></ak-related-role-table>
|
||||
</div>
|
||||
</ak-tabs>
|
||||
`;
|
||||
</div>
|
||||
</ak-tabs> `;
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
@@ -425,7 +431,7 @@ export class UserViewPage extends WithLicenseSummary(
|
||||
${msg("Actions over the last week (per 8 hours)")}
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<ak-charts-user username=${this.user.username}> </ak-charts-user>
|
||||
<ak-charts-user username=${this.user.username}></ak-charts-user>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -451,8 +457,7 @@ export class UserViewPage extends WithLicenseSummary(
|
||||
<ak-object-changelog
|
||||
targetModelPk=${this.user.pk}
|
||||
targetModelName=${ModelEnum.AuthentikCoreUser}
|
||||
>
|
||||
</ak-object-changelog>
|
||||
></ak-object-changelog>
|
||||
</div>
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<ak-object-attributes-card
|
||||
@@ -470,7 +475,7 @@ export class UserViewPage extends WithLicenseSummary(
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-group-related-list .targetUser=${this.user}> </ak-group-related-list>
|
||||
<ak-group-related-list .targetUser=${this.user}></ak-group-related-list>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -491,7 +496,7 @@ export class UserViewPage extends WithLicenseSummary(
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<ak-events-user targetUser=${this.user.username}> </ak-events-user>
|
||||
<ak-events-user targetUser=${this.user.username}></ak-events-user>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -18,6 +18,9 @@ import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
|
||||
|
||||
@customElement("ak-user-oauth-access-token-list")
|
||||
export class UserOAuthAccessTokenList extends Table<TokenModel> {
|
||||
public static override verboseName = msg("Access Token");
|
||||
public static override verboseNamePlural = msg("Access Tokens");
|
||||
|
||||
expandable = true;
|
||||
|
||||
@property({ type: Number })
|
||||
|
||||
@@ -18,6 +18,9 @@ import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
|
||||
|
||||
@customElement("ak-user-oauth-refresh-token-list")
|
||||
export class UserOAuthRefreshTokenList extends Table<TokenModel> {
|
||||
public static override verboseName = msg("Refresh Token");
|
||||
public static override verboseNamePlural = msg("Refresh Tokens");
|
||||
|
||||
expandable = true;
|
||||
|
||||
@property({ type: Number })
|
||||
|
||||
@@ -2,6 +2,7 @@ import { MessageLevel } from "#common/messages";
|
||||
import { isPromiseLike } from "#common/promises";
|
||||
|
||||
import { showMessage } from "#elements/messages/MessageContainer";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
|
||||
@@ -47,7 +48,7 @@ export async function doWriteToClipboard(...data: ClipboardItemSource[]): Promis
|
||||
export function writeToClipboard(
|
||||
data?: ClipboardItemSource | ClipboardItemSource[] | null,
|
||||
entityLabel?: string,
|
||||
description?: string,
|
||||
description?: SlottedTemplateResult,
|
||||
): Promise<boolean> {
|
||||
if (!data || (Array.isArray(data) && data.length === 0)) {
|
||||
console.warn("Cannot write empty data to clipboard");
|
||||
|
||||
+80
-61
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* @file Contains various label maps for API enums and other values that we want to display in the UI.
|
||||
*/
|
||||
|
||||
import { MessageFormatter } from "#common/ui/locale/format";
|
||||
|
||||
import {
|
||||
Device,
|
||||
DeviceChallenge,
|
||||
@@ -10,66 +16,75 @@ import {
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
|
||||
/* Various tables in the API for which we need to supply labels */
|
||||
const IntentLabelRecord: Record<IntentEnum, MessageFormatter<string>> = {
|
||||
[IntentEnum.Api]: () => msg("API Access"),
|
||||
[IntentEnum.AppPassword]: () => msg("App password"),
|
||||
[IntentEnum.Recovery]: () => msg("Recovery"),
|
||||
[IntentEnum.Verification]: () => msg("Verification"),
|
||||
[IntentEnum.UnknownDefaultOpenApi]: () => msg("Unknown intent"),
|
||||
};
|
||||
|
||||
export const intentEnumToLabel = new Map<IntentEnum, string>([
|
||||
[IntentEnum.Api, msg("API Access")],
|
||||
[IntentEnum.AppPassword, msg("App password")],
|
||||
[IntentEnum.Recovery, msg("Recovery")],
|
||||
[IntentEnum.Verification, msg("Verification")],
|
||||
[IntentEnum.UnknownDefaultOpenApi, msg("Unknown intent")],
|
||||
]);
|
||||
export function formatIntentLabel(intent: IntentEnum = IntentEnum.Api): string {
|
||||
return IntentLabelRecord[intent]();
|
||||
}
|
||||
|
||||
export const intentToLabel = (intent: IntentEnum) => intentEnumToLabel.get(intent);
|
||||
|
||||
export const eventActionToLabel = new Map<EventActions | undefined, string>([
|
||||
[EventActions.Login, msg("Login")],
|
||||
[EventActions.LoginFailed, msg("Failed login")],
|
||||
[EventActions.Logout, msg("Logout")],
|
||||
[EventActions.UserWrite, msg("User was written to")],
|
||||
[EventActions.SuspiciousRequest, msg("Suspicious request")],
|
||||
[EventActions.PasswordSet, msg("Password set")],
|
||||
[EventActions.SecretView, msg("Secret was viewed")],
|
||||
[EventActions.SecretRotate, msg("Secret was rotated")],
|
||||
[EventActions.InvitationUsed, msg("Invitation used")],
|
||||
[EventActions.AuthorizeApplication, msg("Application authorized")],
|
||||
[EventActions.SourceLinked, msg("Source linked")],
|
||||
[EventActions.ImpersonationStarted, msg("Impersonation started")],
|
||||
[EventActions.ImpersonationEnded, msg("Impersonation ended")],
|
||||
[EventActions.FlowExecution, msg("Flow execution")],
|
||||
export const EventActionLabelRecord: Record<EventActions, MessageFormatter<string>> = {
|
||||
[EventActions.Login]: () => msg("Login"),
|
||||
[EventActions.LoginFailed]: () => msg("Failed login"),
|
||||
[EventActions.Logout]: () => msg("Logout"),
|
||||
[EventActions.UserWrite]: () => msg("User was written to"),
|
||||
[EventActions.SuspiciousRequest]: () => msg("Suspicious request"),
|
||||
[EventActions.PasswordSet]: () => msg("Password set"),
|
||||
[EventActions.SecretView]: () => msg("Secret was viewed"),
|
||||
[EventActions.SecretRotate]: () => msg("Secret was rotated"),
|
||||
[EventActions.InvitationUsed]: () => msg("Invitation used"),
|
||||
[EventActions.AuthorizeApplication]: () => msg("Application authorized"),
|
||||
[EventActions.SourceLinked]: () => msg("Source linked"),
|
||||
[EventActions.ImpersonationStarted]: () => msg("Impersonation started"),
|
||||
[EventActions.ImpersonationEnded]: () => msg("Impersonation ended"),
|
||||
[EventActions.FlowExecution]: () => msg("Flow execution"),
|
||||
// These are different: look closely.
|
||||
[EventActions.PolicyExecution, msg("Policy execution")],
|
||||
[EventActions.PolicyException, msg("Policy exception")],
|
||||
[EventActions.PropertyMappingException, msg("Property Mapping exception")],
|
||||
[EventActions.PolicyExecution]: () => msg("Policy execution"),
|
||||
[EventActions.PolicyException]: () => msg("Policy exception"),
|
||||
[EventActions.PropertyMappingException]: () => msg("Property Mapping exception"),
|
||||
// These are different: look closely.
|
||||
[EventActions.SystemTaskExecution, msg("System task execution")],
|
||||
[EventActions.SystemTaskException, msg("System task exception")],
|
||||
[EventActions.SystemException, msg("General system exception")],
|
||||
[EventActions.ConfigurationError, msg("Configuration error")],
|
||||
[EventActions.ConfigurationWarning, msg("Configuration warning")],
|
||||
[EventActions.ModelCreated, msg("Model created")],
|
||||
[EventActions.ModelUpdated, msg("Model updated")],
|
||||
[EventActions.ModelDeleted, msg("Model deleted")],
|
||||
[EventActions.EmailSent, msg("Email sent")],
|
||||
[EventActions.UpdateAvailable, msg("Update available")],
|
||||
[EventActions.ExportReady, msg("Data export ready")],
|
||||
[EventActions.ReviewInitiated, msg("Review initiated")],
|
||||
[EventActions.ReviewOverdue, msg("Review overdue")],
|
||||
[EventActions.ReviewAttested, msg("Review attested")],
|
||||
[EventActions.ReviewCompleted, msg("Review completed")],
|
||||
]);
|
||||
[EventActions.SystemTaskExecution]: () => msg("System task execution"),
|
||||
[EventActions.SystemTaskException]: () => msg("System task exception"),
|
||||
[EventActions.SystemException]: () => msg("General system exception"),
|
||||
[EventActions.ConfigurationError]: () => msg("Configuration error"),
|
||||
[EventActions.ConfigurationWarning]: () => msg("Configuration warning"),
|
||||
[EventActions.ModelCreated]: () => msg("Model created"),
|
||||
[EventActions.ModelUpdated]: () => msg("Model updated"),
|
||||
[EventActions.ModelDeleted]: () => msg("Model deleted"),
|
||||
[EventActions.EmailSent]: () => msg("Email sent"),
|
||||
[EventActions.UpdateAvailable]: () => msg("Update available"),
|
||||
[EventActions.ExportReady]: () => msg("Data export ready"),
|
||||
[EventActions.ReviewInitiated]: () => msg("Review initiated"),
|
||||
[EventActions.ReviewOverdue]: () => msg("Review overdue"),
|
||||
[EventActions.ReviewAttested]: () => msg("Review attested"),
|
||||
[EventActions.ReviewCompleted]: () => msg("Review completed"),
|
||||
[EventActions.UnknownDefaultOpenApi]: () => msg("Unknown action"),
|
||||
[EventActions.Custom]: () => msg("Custom action"),
|
||||
};
|
||||
|
||||
export const actionToLabel = (action?: EventActions): string =>
|
||||
eventActionToLabel.get(action) ?? action ?? "";
|
||||
export function actionToLabel(action?: EventActions): string {
|
||||
const formatter = action ? EventActionLabelRecord[action] : null;
|
||||
|
||||
export const severityEnumToLabel = new Map<SeverityEnum | null | undefined, string>([
|
||||
[SeverityEnum.Alert, msg("Alert")],
|
||||
[SeverityEnum.Notice, msg("Notice")],
|
||||
[SeverityEnum.Warning, msg("Warning")],
|
||||
]);
|
||||
return formatter?.() || "";
|
||||
}
|
||||
|
||||
export const severityToLabel = (severity: SeverityEnum | null | undefined) =>
|
||||
severityEnumToLabel.get(severity) ?? msg("Unknown severity");
|
||||
const SeverityEnumLabelRecord: Record<SeverityEnum, MessageFormatter<string>> = {
|
||||
[SeverityEnum.Alert]: () => msg("Alert"),
|
||||
[SeverityEnum.Notice]: () => msg("Notice"),
|
||||
[SeverityEnum.Warning]: () => msg("Warning"),
|
||||
[SeverityEnum.UnknownDefaultOpenApi]: () => msg("Unknown severity"),
|
||||
};
|
||||
|
||||
export function severityToLabel(severity: SeverityEnum | null | undefined): string {
|
||||
const formatter = SeverityEnumLabelRecord[severity ?? SeverityEnum.UnknownDefaultOpenApi];
|
||||
|
||||
return formatter();
|
||||
}
|
||||
|
||||
export function severityToLevel(severity?: SeverityEnum | null): string {
|
||||
switch (severity) {
|
||||
@@ -113,12 +128,16 @@ export function formatDeviceChallengeMessage(deviceChallenge?: DeviceChallenge |
|
||||
return msg("Enter the code from your authenticator device.");
|
||||
}
|
||||
|
||||
const _userTypeToLabel = new Map<UserTypeEnum | undefined, string>([
|
||||
[UserTypeEnum.Internal, msg("Internal")],
|
||||
[UserTypeEnum.External, msg("External")],
|
||||
[UserTypeEnum.ServiceAccount, msg("Service account")],
|
||||
[UserTypeEnum.InternalServiceAccount, msg("Service account (internal)")],
|
||||
]);
|
||||
const UserTypeLabelRecord: Record<UserTypeEnum, MessageFormatter<string>> = {
|
||||
[UserTypeEnum.Internal]: () => msg("Internal"),
|
||||
[UserTypeEnum.External]: () => msg("External"),
|
||||
[UserTypeEnum.ServiceAccount]: () => msg("Service account"),
|
||||
[UserTypeEnum.InternalServiceAccount]: () => msg("Service account (internal)"),
|
||||
[UserTypeEnum.UnknownDefaultOpenApi]: () => msg("Unknown user type"),
|
||||
};
|
||||
|
||||
export const userTypeToLabel = (type?: UserTypeEnum): string =>
|
||||
_userTypeToLabel.get(type) ?? type ?? "";
|
||||
export function userTypeToLabel(type?: UserTypeEnum): string {
|
||||
const formatter = type ? UserTypeLabelRecord[type] : null;
|
||||
|
||||
return formatter?.() || "";
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
export enum MessageLevel {
|
||||
error = "error",
|
||||
@@ -18,7 +18,7 @@ export enum MessageLevel {
|
||||
export interface APIMessage {
|
||||
level: MessageLevel;
|
||||
message: string;
|
||||
description?: string | TemplateResult;
|
||||
description?: SlottedTemplateResult;
|
||||
icon?: string;
|
||||
/**
|
||||
* An optional key to determine uniqueness of the message.
|
||||
|
||||
@@ -8,7 +8,15 @@ import {
|
||||
} from "#common/ui/locale/definitions";
|
||||
import { safeParseLocale } from "#common/ui/locale/utils";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { msg, str, TemplateLike } from "@lit/localize";
|
||||
|
||||
/**
|
||||
* A Lit Localize callback function which returns a translated result.
|
||||
*/
|
||||
export type MessageFormatter<
|
||||
R extends TemplateLike = TemplateLike,
|
||||
Args extends unknown[] = never[],
|
||||
> = (...args: Args) => R;
|
||||
|
||||
/**
|
||||
* Safely get a minimized locale ID, with fallback for older browsers.
|
||||
|
||||
@@ -115,7 +115,7 @@ export class SimpleTable
|
||||
*
|
||||
* Overrides the static `verboseName` property for this instance.
|
||||
*/
|
||||
@property({ type: String, attribute: "entity-singular" })
|
||||
@property({ type: String, attribute: "verbose-name" })
|
||||
public set verboseName(value: string | null) {
|
||||
this.#verboseName = value;
|
||||
|
||||
@@ -135,7 +135,7 @@ export class SimpleTable
|
||||
*
|
||||
* Overrides the static `verboseNamePlural` property for this instance.
|
||||
*/
|
||||
@property({ type: String, attribute: "entity-plural" })
|
||||
@property({ type: String, attribute: "verbose-name-plural" })
|
||||
public set verboseNamePlural(value: string | null) {
|
||||
this.#verboseNamePlural = value;
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface IconCopyButtonProps {
|
||||
buttonLabel?: string;
|
||||
tooltipLabel?: string;
|
||||
entityLabel?: string;
|
||||
description?: string;
|
||||
description?: SlottedTemplateResult;
|
||||
}
|
||||
|
||||
export function IconCopyButton({
|
||||
@@ -52,7 +52,8 @@ export function IconCopyButton({
|
||||
@click=${doCopy}
|
||||
aria-label=${buttonLabel}
|
||||
>
|
||||
<i class="fas fa-copy" aria-hidden="true"></i>
|
||||
<pf-tooltip position="top" content=${tooltipLabel}> </pf-tooltip>
|
||||
<pf-tooltip position="top" content=${tooltipLabel}>
|
||||
<i class="fas fa-copy" aria-hidden="true"></i>
|
||||
</pf-tooltip>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
import { aki } from "#common/api/client";
|
||||
import { formatIntentLabel } from "#common/labels";
|
||||
|
||||
import { IconCopyButton } from "#elements/buttons/IconCopyButton";
|
||||
import { SlottedTemplateResult } from "#elements/types";
|
||||
|
||||
import { CoreApi } from "@goauthentik/api";
|
||||
import { CoreApi, Token } from "@goauthentik/api";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { guard } from "lit-html/directives/guard.js";
|
||||
|
||||
export function IconTokenCopyButton(identifier?: string | null): SlottedTemplateResult {
|
||||
return guard([], () => {
|
||||
export function IconTokenCopyButton(tokenLike?: Token | string | null): SlottedTemplateResult {
|
||||
return guard([tokenLike], () => {
|
||||
if (!tokenLike) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { identifier, userObj, intent } =
|
||||
typeof tokenLike === "string"
|
||||
? { identifier: tokenLike, userObj: null, intent: null }
|
||||
: tokenLike;
|
||||
|
||||
const description = userObj?.username
|
||||
? msg(str`${formatIntentLabel(intent)} token for ${userObj.username}`, {
|
||||
id: "tokens.clipboard-copy.description",
|
||||
desc: "Description for a clipboard copy action for tokens, with the token intent and username as variables.",
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const fetchTokenViewKey = (): Promise<Blob> => {
|
||||
if (!identifier) {
|
||||
console.warn("No identifier provided for IconTokenCopyButton");
|
||||
@@ -23,8 +40,15 @@ export function IconTokenCopyButton(identifier?: string | null): SlottedTemplate
|
||||
|
||||
return IconCopyButton({
|
||||
source: fetchTokenViewKey,
|
||||
buttonLabel: msg("Copy token"),
|
||||
entityLabel: msg("Token"),
|
||||
buttonLabel: msg("Copy token", {
|
||||
id: "tokens.copy-button.label",
|
||||
desc: "Label for a button that copies a token to the clipboard.",
|
||||
}),
|
||||
entityLabel: msg("Token", {
|
||||
id: "tokens.copy-button.entity-label",
|
||||
desc: "Label for a token entity, used in clipboard copy success messages.",
|
||||
}),
|
||||
description,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ export class Form<T = Record<string, unknown>, D = T>
|
||||
*
|
||||
* Overrides the static `verboseName` property for this instance.
|
||||
*/
|
||||
@property({ type: String, attribute: "entity-singular" })
|
||||
@property({ type: String, attribute: "verbose-name" })
|
||||
public set verboseName(value: string | null) {
|
||||
this.#verboseName = value;
|
||||
|
||||
@@ -309,7 +309,7 @@ export class Form<T = Record<string, unknown>, D = T>
|
||||
*
|
||||
* Overrides the static `verboseNamePlural` property for this instance.
|
||||
*/
|
||||
@property({ type: String, attribute: "entity-plural" })
|
||||
@property({ type: String, attribute: "verbose-name-plural" })
|
||||
public set verboseNamePlural(value: string | null) {
|
||||
this.#verboseNamePlural = value;
|
||||
|
||||
|
||||
+105
-17
@@ -14,11 +14,17 @@ import { type PaginatedResponse } from "#common/api/responses";
|
||||
import { EVENT_REFRESH } from "#common/constants";
|
||||
import { APIError, parseAPIResponseError, pluckErrorDetail } from "#common/errors/network";
|
||||
import { AKRefreshEvent } from "#common/events";
|
||||
import { truncateWords } from "#common/strings";
|
||||
import { GroupResult } from "#common/utils";
|
||||
|
||||
import { AKElement } from "#elements/Base";
|
||||
import { intersectionObserver } from "#elements/decorators/intersection-observer";
|
||||
import { type TransclusionChildElement, TransclusionChildSymbol } from "#elements/dialogs/shared";
|
||||
import {
|
||||
EntityDescriptorElement,
|
||||
isTransclusionParentElement,
|
||||
type TransclusionChildElement,
|
||||
TransclusionChildSymbol,
|
||||
} from "#elements/dialogs/shared";
|
||||
import { WithSession } from "#elements/mixins/session";
|
||||
import { getURLParam, updateURLParams } from "#elements/router/RouteMatch";
|
||||
import Styles from "#elements/table/Table.css";
|
||||
@@ -84,6 +90,8 @@ export abstract class Table<T extends object, D = T>
|
||||
extends WithSession(AKElement)
|
||||
implements TableLike, TransclusionChildElement
|
||||
{
|
||||
declare ["constructor"]: EntityDescriptorElement;
|
||||
|
||||
static styles: CSSResult[] = [
|
||||
PFTable,
|
||||
PFBullseye,
|
||||
@@ -95,6 +103,9 @@ export abstract class Table<T extends object, D = T>
|
||||
Styles,
|
||||
];
|
||||
|
||||
public static verboseName: string = msg("Object");
|
||||
public static verboseNamePlural: string = msg("Objects");
|
||||
|
||||
public [TransclusionChildSymbol] = true;
|
||||
|
||||
//#region Abstract members
|
||||
@@ -124,10 +135,51 @@ export abstract class Table<T extends object, D = T>
|
||||
|
||||
//#region Protected Properties
|
||||
|
||||
#verboseName: string | null = null;
|
||||
|
||||
/**
|
||||
* Customize the "No objects found" message.
|
||||
* Optional singular label for the type of entity this form creates/edits.
|
||||
*
|
||||
* Overrides the static `verboseName` property for this instance.
|
||||
*/
|
||||
protected emptyStateMessage = msg("No objects found.");
|
||||
@property({ type: String, attribute: "verbose-name" })
|
||||
public set verboseName(value: string | null) {
|
||||
this.#verboseName = value;
|
||||
|
||||
if (isTransclusionParentElement(this.parentElement)) {
|
||||
this.parentElement.slottedElementUpdatedAt = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
public get verboseName(): string | null {
|
||||
return this.#verboseName || this.constructor.verboseName || null;
|
||||
}
|
||||
|
||||
#verboseNamePlural: string | null = null;
|
||||
|
||||
/**
|
||||
* Optional plural label for the type of entity this form creates/edits.
|
||||
*
|
||||
* Overrides the static `verboseNamePlural` property for this instance.
|
||||
*/
|
||||
@property({ type: String, attribute: "verbose-name-plural" })
|
||||
public set verboseNamePlural(value: string | null) {
|
||||
this.#verboseNamePlural = value;
|
||||
|
||||
if (isTransclusionParentElement(this.parentElement)) {
|
||||
this.parentElement.slottedElementUpdatedAt = new Date();
|
||||
}
|
||||
}
|
||||
|
||||
public get verboseNamePlural(): string | null {
|
||||
return this.#verboseNamePlural || this.constructor.verboseNamePlural || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* An optional message to display when the table is empty and no search is applied.
|
||||
* If not provided, a default message will be used.
|
||||
*/
|
||||
protected emptyStateMessage: string | null = null;
|
||||
|
||||
/**
|
||||
* Whether the table is currently fetching data.
|
||||
@@ -324,6 +376,44 @@ export abstract class Table<T extends object, D = T>
|
||||
return this.fetch();
|
||||
};
|
||||
|
||||
/**
|
||||
* An overridable method for formatting the empty state message when no objects are found.
|
||||
*/
|
||||
public formatEmptyStateMessage(): string {
|
||||
if (this.searchEnabled && this.search) {
|
||||
const singularNoun = this.verboseName?.toLocaleLowerCase() || msg("object");
|
||||
|
||||
return msg(str`No ${singularNoun} matches "${truncateWords(this.search, 50)}"`, {
|
||||
id: "table.emptyState.search",
|
||||
desc: "Empty state message when no objects match the search query, where the entity singular is interpolated, followed by the search query truncated to 50 characters.",
|
||||
});
|
||||
}
|
||||
|
||||
if (this.emptyStateMessage) {
|
||||
return this.emptyStateMessage;
|
||||
}
|
||||
|
||||
const pluralNoun = this.verboseNamePlural?.toLocaleLowerCase() || msg("objects");
|
||||
|
||||
return msg(str`No ${pluralNoun} found.`, {
|
||||
id: "table.emptyState.default",
|
||||
desc: "Empty state message when no objects are found, where the entity plural is interpolated.",
|
||||
});
|
||||
}
|
||||
|
||||
public formatSearchPlaceholder(): string {
|
||||
if (this.searchPlaceholder) {
|
||||
return this.searchPlaceholder;
|
||||
}
|
||||
|
||||
const pluralNoun = this.verboseNamePlural?.toLocaleLowerCase() || msg("objects");
|
||||
|
||||
return msg(str`Search for ${pluralNoun}...`, {
|
||||
id: "table.search.placeholder",
|
||||
desc: "Placeholder text for the search input, where the entity plural is interpolated.",
|
||||
});
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region Lifecycle
|
||||
@@ -512,19 +602,17 @@ export abstract class Table<T extends object, D = T>
|
||||
}
|
||||
|
||||
protected renderEmpty(inner?: SlottedTemplateResult): SlottedTemplateResult {
|
||||
return html`
|
||||
<tr role="presentation">
|
||||
<td role="presentation" colspan=${this.columnCount}>
|
||||
<div class="pf-l-bullseye">
|
||||
${inner ??
|
||||
html`<ak-empty-state
|
||||
><span>${this.emptyStateMessage}</span>
|
||||
<div slot="primary">${this.renderObjectCreate()}</div>
|
||||
</ak-empty-state>`}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return html`<tr role="presentation">
|
||||
<td role="presentation" colspan=${this.columnCount}>
|
||||
<div class="pf-l-bullseye">
|
||||
${inner ??
|
||||
html`<ak-empty-state
|
||||
><span>${this.formatEmptyStateMessage()}</span>
|
||||
<div slot="primary">${this.renderObjectCreate()}</div>
|
||||
</ak-empty-state>`}
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -902,7 +990,7 @@ export abstract class Table<T extends object, D = T>
|
||||
part="toolbar-search"
|
||||
.defaultValue=${this.search}
|
||||
label=${ifPresent(this.searchLabel)}
|
||||
placeholder=${ifPresent(this.searchPlaceholder)}
|
||||
placeholder=${ifPresent(this.formatSearchPlaceholder())}
|
||||
.onSearch=${this.#searchListener}
|
||||
.supportsQL=${this.supportsQL}
|
||||
.apiResponse=${this.data}
|
||||
|
||||
@@ -93,7 +93,7 @@ export abstract class TablePage<T extends object> extends Table<T> {
|
||||
${inner
|
||||
? inner
|
||||
: html`<ak-empty-state icon=${this.pageIcon}
|
||||
><span>${this.emptyStateMessage}</span>
|
||||
><span>${this.formatEmptyStateMessage()}</span>
|
||||
<div slot="body">
|
||||
${this.searchEnabled ? this.renderEmptyClearSearch() : nothing}
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,9 @@ import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-user-session-list")
|
||||
export class AuthenticatedSessionList extends Table<AuthenticatedSession> {
|
||||
public static override verboseName = msg("Session");
|
||||
public static override verboseNamePlural = msg("Sessions");
|
||||
|
||||
@property()
|
||||
targetUser!: string;
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@ import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-user-consent-list")
|
||||
export class UserConsentList extends Table<UserConsent> {
|
||||
public static override verboseName = msg("Consent");
|
||||
public static override verboseNamePlural = msg("Consents");
|
||||
|
||||
@property({ type: Number })
|
||||
userId?: number;
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@ import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("ak-user-reputation-list")
|
||||
export class UserReputationList extends Table<Reputation> {
|
||||
public static override verboseName = msg("Reputation score");
|
||||
public static override verboseNamePlural = msg("Reputation scores");
|
||||
|
||||
@property()
|
||||
targetUsername!: string;
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ export class CreateWizard extends AKElement implements TransclusionChildElement
|
||||
*
|
||||
* Overrides the static `verboseName` property for this instance.
|
||||
*/
|
||||
@property({ type: String, attribute: "entity-singular" })
|
||||
@property({ type: String, attribute: "verbose-name" })
|
||||
public set verboseName(value: string | null) {
|
||||
this.#verboseName = value;
|
||||
|
||||
@@ -121,7 +121,7 @@ export class CreateWizard extends AKElement implements TransclusionChildElement
|
||||
*
|
||||
* Overrides the static `verboseNamePlural` property for this instance.
|
||||
*/
|
||||
@property({ type: String, attribute: "entity-plural" })
|
||||
@property({ type: String, attribute: "verbose-name-plural" })
|
||||
public set verboseNamePlural(value: string | null) {
|
||||
this.#verboseNamePlural = value;
|
||||
|
||||
@@ -309,8 +309,8 @@ export class CreateWizard extends AKElement implements TransclusionChildElement
|
||||
|
||||
return html`<ak-wizard
|
||||
${ref(this.wizardRef)}
|
||||
entity-singular=${ifPresent(this.verboseName)}
|
||||
entity-plural=${ifPresent(this.verboseNamePlural)}
|
||||
verbose-name=${ifPresent(this.verboseName)}
|
||||
verbose-name-plural=${ifPresent(this.verboseNamePlural)}
|
||||
description=${ifPresent(this.description)}
|
||||
part="main"
|
||||
.initialSteps=${this.initialSteps}
|
||||
|
||||
@@ -77,7 +77,7 @@ export class AKWizard<S = Record<string, unknown>> extends AKElement {
|
||||
public formatARIALabel(verboseName = this.verboseName): string {
|
||||
return verboseName
|
||||
? msg(str`New ${verboseName} Wizard`, {
|
||||
id: "wizard.ariaLabel.entity-singular",
|
||||
id: "wizard.ariaLabel.verbose-name.one",
|
||||
desc: "ARIA label for the creation wizard, where the entity singular is interpolated.",
|
||||
})
|
||||
: msg("Wizard", {
|
||||
@@ -92,7 +92,7 @@ export class AKWizard<S = Record<string, unknown>> extends AKElement {
|
||||
public formatHeader(verboseName = this.verboseName): string {
|
||||
if (verboseName) {
|
||||
return msg(str`Create New ${verboseName}`, {
|
||||
id: "wizard.header.entity-singular",
|
||||
id: "wizard.header.verbose-name.one",
|
||||
desc: "Header for the creation wizard, where the entity singular is interpolated.",
|
||||
});
|
||||
}
|
||||
@@ -130,7 +130,7 @@ export class AKWizard<S = Record<string, unknown>> extends AKElement {
|
||||
*
|
||||
* Overrides the static `verboseName` property for this instance.
|
||||
*/
|
||||
@property({ type: String, attribute: "entity-singular" })
|
||||
@property({ type: String, attribute: "verbose-name" })
|
||||
public set verboseName(value: string | null) {
|
||||
this.#verboseName = value;
|
||||
|
||||
@@ -146,7 +146,7 @@ export class AKWizard<S = Record<string, unknown>> extends AKElement {
|
||||
/**
|
||||
* Optional plural label for the type of entity this wizard creates, used in messages and the like.
|
||||
*/
|
||||
@property({ type: String, attribute: "entity-plural" })
|
||||
@property({ type: String, attribute: "verbose-name-plural" })
|
||||
public verboseNamePlural: string | null = null;
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,7 @@ import "#user/user-settings/tokens/UserTokenForm";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { aki } from "#common/api/client";
|
||||
import { intentToLabel } from "#common/labels";
|
||||
import { formatIntentLabel } from "#common/labels";
|
||||
import { formatElapsedTime } from "#common/temporal";
|
||||
|
||||
import { IconTokenCopyButton } from "#elements/buttons/IconTokenCopyButton";
|
||||
@@ -132,7 +132,7 @@ export class UserTokenList extends Table<Token> {
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
${intentToLabel(item.intent ?? IntentEnum.Api)}
|
||||
${formatIntentLabel(item.intent ?? IntentEnum.Api)}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
@@ -175,7 +175,7 @@ export class UserTokenList extends Table<Token> {
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
${IconTokenCopyButton(item.identifier)}
|
||||
${IconTokenCopyButton(item)}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
+3
-5
@@ -3,10 +3,7 @@
|
||||
"extends": "@goauthentik/tsconfig",
|
||||
"compilerOptions": {
|
||||
"ignoreDeprecations": "6.0",
|
||||
// Nothing references the web package, so it does not need to act as a
|
||||
// composite project. Disabling composite lets us exclude `packages/`
|
||||
// (each subpackage owns its own tsconfig + build) without TS6307.
|
||||
"composite": false,
|
||||
"composite": true,
|
||||
"types": ["node"],
|
||||
"checkJs": true,
|
||||
"allowJs": true,
|
||||
@@ -75,7 +72,8 @@
|
||||
// Workspace subpackages each own their tsconfig and build. Including
|
||||
// them here pulls ~1k files (notably the OpenAPI client in
|
||||
// packages/client-ts) into every `tsc -p .` for no benefit.
|
||||
"packages",
|
||||
"packages/client-ts",
|
||||
"packages/sfe",
|
||||
// TODO: @lit/localize-tools v0.8.0 has a nullish coalescing typing error.
|
||||
// Remove when we upgrade past that.
|
||||
"scripts/pseudolocalize.mjs",
|
||||
|
||||
Reference in New Issue
Block a user