From 9430a2eea2170e5c662aedd22437e8ea354f6243 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Thu, 12 Aug 2021 21:14:51 +0200 Subject: [PATCH] web/elements: add bulk delete form Signed-off-by: Jens Langhammer --- web/src/api/Users.ts | 1 + web/src/elements/forms/DeleteBulkForm.ts | 195 +++++++++++++++++++++++ web/src/elements/table/Table.ts | 87 +++++----- web/src/locales/en.po | 32 +++- web/src/locales/pseudo-LOCALE.po | 32 +++- web/src/pages/users/UserListPage.ts | 21 ++- 6 files changed, 317 insertions(+), 51 deletions(-) create mode 100644 web/src/elements/forms/DeleteBulkForm.ts diff --git a/web/src/api/Users.ts b/web/src/api/Users.ts index 5c74d035c..5117828b5 100644 --- a/web/src/api/Users.ts +++ b/web/src/api/Users.ts @@ -9,6 +9,7 @@ export function me(): Promise { user: { pk: -1, isSuperuser: false, + isActive: true, groups: [], avatar: "", uid: "", diff --git a/web/src/elements/forms/DeleteBulkForm.ts b/web/src/elements/forms/DeleteBulkForm.ts new file mode 100644 index 000000000..12b441999 --- /dev/null +++ b/web/src/elements/forms/DeleteBulkForm.ts @@ -0,0 +1,195 @@ +import { t } from "@lingui/macro"; +import { CSSResult, customElement, html, property, TemplateResult } from "lit-element"; +import { EVENT_REFRESH } from "../../constants"; +import { ModalButton } from "../buttons/ModalButton"; +import { MessageLevel } from "../messages/Message"; +import { showMessage } from "../messages/MessageContainer"; +import "../buttons/SpinnerButton"; +import { UsedBy, UsedByActionEnum } from "authentik-api"; +import PFList from "@patternfly/patternfly/components/List/list.css"; +import { until } from "lit-html/directives/until"; +import { Table, TableColumn } from "../table/Table"; +import { AKResponse } from "../../api/Client"; +import { PFSize } from "../Spinner"; + +export interface AKObject { + pk: T; + slug?: string; + name?: string; + [key: string]: unknown; +} + +@customElement("ak-delete-objects-table") +export class DeleteObjectsTable extends Table> { + expandable = true; + paginated = false; + + @property({ attribute: false }) + objects: AKObject[] = []; + + @property({ attribute: false }) + usedBy?: (item: AKObject) => Promise; + + static get styles(): CSSResult[] { + return super.styles.concat(PFList); + } + + apiEndpoint(page: number): Promise>> { + return Promise.resolve({ + pagination: { + count: this.objects.length, + current: 1, + totalPages: 1, + startIndex: 1, + endIndex: this.objects.length, + }, + results: this.objects, + }); + } + + columns(): TableColumn[] { + return [new TableColumn(t`Name`), new TableColumn(t`ID`)]; + } + + row(item: AKObject): TemplateResult[] { + return [html`${item.name}`, html`${item.pk}`]; + } + + renderToolbarContainer(): TemplateResult { + return html``; + } + + renderExpanded(item: AKObject): TemplateResult { + return html`${this.usedBy + ? until( + this.usedBy(item).then((usedBy) => { + return this.renderUsedBy(item, usedBy); + }), + html``, + ) + : html``}`; + } + + renderUsedBy(item: AKObject, usedBy: UsedBy[]): TemplateResult { + if (usedBy.length < 1) { + return html` +
+ ${t`Not used by any other object.`} +
+ `; + } + return html` +
+

${t`The following objects use ${item.name}:`}

+
    + ${usedBy.map((ub) => { + let consequence = ""; + switch (ub.action) { + case UsedByActionEnum.Cascade: + consequence = t`object will be DELETED`; + break; + case UsedByActionEnum.CascadeMany: + consequence = t`connection will be deleted`; + break; + case UsedByActionEnum.SetDefault: + consequence = t`reference will be reset to default value`; + break; + case UsedByActionEnum.SetNull: + consequence = t`reference will be set to an empty value`; + break; + } + return html`
  • ${t`${ub.name} (${consequence})`}
  • `; + })} +
+
+ `; + } +} + +@customElement("ak-forms-delete-bulk") +export class DeleteBulkForm extends ModalButton { + @property({ attribute: false }) + objects: AKObject[] = []; + + @property() + objectLabel?: string; + + @property({ attribute: false }) + usedBy?: (itemPk: AKObject) => Promise; + + @property({ attribute: false }) + delete!: (itemPk: AKObject) => Promise; + + confirm(): Promise { + return Promise.all( + this.objects.map((item) => { + return this.delete(item); + }), + ) + .then(() => { + this.onSuccess(); + this.open = false; + this.dispatchEvent( + new CustomEvent(EVENT_REFRESH, { + bubbles: true, + composed: true, + }), + ); + }) + .catch((e) => { + this.onError(e); + throw e; + }); + } + + onSuccess(): void { + showMessage({ + message: t`Successfully deleted ${this.objects.length} ${this.objectLabel}`, + level: MessageLevel.success, + }); + } + + onError(e: Error): void { + showMessage({ + message: t`Failed to delete ${this.objectLabel}: ${e.toString()}`, + level: MessageLevel.error, + }); + } + + renderModalInner(): TemplateResult { + return html`
+
+

${t`Delete ${this.objectLabel}`}

+
+
+
+
+

+ ${t`Are you sure you want to delete ${this.objects.length} ${this.objectLabel}?`} +

+
+
+
+ + +
+
+ { + return this.confirm(); + }} + class="pf-m-danger" + > + ${t`Delete`}   + { + this.open = false; + }} + class="pf-m-secondary" + > + ${t`Cancel`} + +
`; + } +} diff --git a/web/src/elements/table/Table.ts b/web/src/elements/table/Table.ts index b425a9982..3252295f3 100644 --- a/web/src/elements/table/Table.ts +++ b/web/src/elements/table/Table.ts @@ -123,6 +123,9 @@ export abstract class Table extends LitElement { @property({ attribute: false }) selectedElements: T[] = []; + @property({ type: Boolean }) + paginated = true; + @property({ type: Boolean }) expandable = false; @@ -326,6 +329,33 @@ export abstract class Table extends LitElement { return html``; } + renderToolbarContainer(): TemplateResult { + return html`
+
+
${this.renderSearch()}
+
${this.renderToolbar()}
+
${this.renderToolbarAfter()}
+
${this.renderToolbarSelected()}
+ ${this.paginated + ? html` { + this.page = page; + this.dispatchEvent( + new CustomEvent(EVENT_REFRESH, { + bubbles: true, + composed: true, + }), + ); + }} + > + ` + : html``} +
+
`; + } + firstUpdated(): void { this.fetch(); } @@ -338,28 +368,7 @@ export abstract class Table extends LitElement { })} ` : html``} -
-
-
${this.renderSearch()}
-
${this.renderToolbar()}
-
${this.renderToolbarAfter()}
-
${this.renderToolbarSelected()}
- { - this.page = page; - this.dispatchEvent( - new CustomEvent(EVENT_REFRESH, { - bubbles: true, - composed: true, - }), - ); - }} - > - -
-
+ ${this.renderToolbarContainer()} @@ -384,22 +393,24 @@ export abstract class Table extends LitElement { ${this.isLoading || !this.data ? this.renderLoading() : this.renderRows()}
-
- { - this.page = page; - this.dispatchEvent( - new CustomEvent(EVENT_REFRESH, { - bubbles: true, - composed: true, - }), - ); - }} - > - -
`; + ${this.paginated + ? html`
+ { + this.page = page; + this.dispatchEvent( + new CustomEvent(EVENT_REFRESH, { + bubbles: true, + composed: true, + }), + ); + }} + > + +
` + : html``}`; } render(): TemplateResult { diff --git a/web/src/locales/en.po b/web/src/locales/en.po index bd9e88df5..327a2bcb1 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -280,6 +280,10 @@ msgstr "" "Are you sure you want to clear the policy cache?\n" "This will cause all policies to be re-evaluated on their next usage." +#: src/elements/forms/DeleteBulkForm.ts +msgid "Are you sure you want to delete {0} {1}?" +msgstr "Are you sure you want to delete {0} {1}?" + #: src/elements/forms/DeleteForm.ts msgid "Are you sure you want to delete {0} {objName} ?" msgstr "Are you sure you want to delete {0} {objName} ?" @@ -513,6 +517,7 @@ msgid "Can be in the format of 'unix://' when connecting to a local docker daemo msgstr "Can be in the format of 'unix://' when connecting to a local docker daemon, or 'https://:2376' when connecting to a remote system." #: src/elements/forms/ConfirmationForm.ts +#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteForm.ts #: src/elements/forms/ModalForm.ts #: src/pages/groups/MemberSelectModal.ts @@ -1085,6 +1090,7 @@ msgstr "Default?" msgid "Define how notifications are sent to users, like Email or Webhook." msgstr "Define how notifications are sent to users, like Email or Webhook." +#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteForm.ts #: src/elements/oauth/UserCodeList.ts #: src/elements/oauth/UserRefreshList.ts @@ -1158,6 +1164,7 @@ msgstr "" "Delete the currently pending user. CAUTION, this stage does not ask for\n" "confirmation. Use a consent stage to ensure the user is aware of their actions." +#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteForm.ts msgid "Delete {0}" msgstr "Delete {0}" @@ -1599,6 +1606,7 @@ msgstr "Failed to delete flow cache" msgid "Failed to delete policy cache" msgstr "Failed to delete policy cache" +#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteForm.ts msgid "Failed to delete {0}: {1}" msgstr "Failed to delete {0}: {1}" @@ -1880,6 +1888,7 @@ msgstr "Hold control/command to select multiple items." msgid "How many attempts a user has before the flow is canceled. To lock the user out, use a reputation policy and a user_write stage." msgstr "How many attempts a user has before the flow is canceled. To lock the user out, use a reputation policy and a user_write stage." +#: src/elements/forms/DeleteBulkForm.ts #: src/pages/stages/invitation/InvitationListPage.ts msgid "ID" msgstr "ID" @@ -2407,6 +2416,7 @@ msgstr "Monitor" msgid "My Applications" msgstr "My Applications" +#: src/elements/forms/DeleteBulkForm.ts #: src/pages/applications/ApplicationForm.ts #: src/pages/applications/ApplicationListPage.ts #: src/pages/crypto/CertificateKeyPairForm.ts @@ -2608,6 +2618,10 @@ msgstr "Not found" msgid "Not synced yet." msgstr "Not synced yet." +#: src/elements/forms/DeleteBulkForm.ts +msgid "Not used by any other object." +msgstr "Not used by any other object." + #: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts #: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts #: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts @@ -3839,6 +3853,7 @@ msgstr "Successfully created transport." msgid "Successfully created user." msgstr "Successfully created user." +#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteForm.ts msgid "Successfully deleted {0} {1}" msgstr "Successfully deleted {0} {1}" @@ -4132,6 +4147,10 @@ msgstr "The external URL you'll access the application at. Include any non-stand msgid "The external URL you'll authenticate at. Can be the same domain as authentik." msgstr "The external URL you'll authenticate at. Can be the same domain as authentik." +#: src/elements/forms/DeleteBulkForm.ts +msgid "The following objects use {0}:" +msgstr "The following objects use {0}:" + #: src/elements/forms/DeleteForm.ts msgid "The following objects use {objName}" msgstr "The following objects use {objName}" @@ -4596,7 +4615,6 @@ msgstr "Use this tenant for each domain that doesn't have a dedicated tenant." #: src/pages/tokens/TokenListPage.ts #: src/pages/user-settings/tokens/UserTokenList.ts #: src/pages/users/UserListPage.ts -#: src/pages/users/UserListPage.ts msgid "User" msgstr "User" @@ -4664,6 +4682,10 @@ msgstr "User's avatar" msgid "User's display name." msgstr "User's display name." +#: src/pages/users/UserListPage.ts +msgid "User(s)" +msgstr "User(s)" + #: src/pages/providers/proxy/ProxyProviderForm.ts msgid "User/Group Attribute used for the password part of the HTTP-Basic Header." msgstr "User/Group Attribute used for the password part of the HTTP-Basic Header." @@ -4906,18 +4928,25 @@ msgstr "authentik LDAP Backend" msgid "connecting object will be deleted" msgstr "connecting object will be deleted" +#: src/elements/forms/DeleteBulkForm.ts +msgid "connection will be deleted" +msgstr "connection will be deleted" + #: src/elements/Tabs.ts msgid "no tabs defined" msgstr "no tabs defined" +#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteForm.ts msgid "object will be DELETED" msgstr "object will be DELETED" +#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteForm.ts msgid "reference will be reset to default value" msgstr "reference will be reset to default value" +#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteForm.ts msgid "reference will be set to an empty value" msgstr "reference will be set to an empty value" @@ -4935,6 +4964,7 @@ msgstr "{0} (\"{1}\", of type {2})" msgid "{0} ({1})" msgstr "{0} ({1})" +#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteForm.ts msgid "{0} ({consequence})" msgstr "{0} ({consequence})" diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po index be7f53902..c49543e06 100644 --- a/web/src/locales/pseudo-LOCALE.po +++ b/web/src/locales/pseudo-LOCALE.po @@ -276,6 +276,10 @@ msgid "" "This will cause all policies to be re-evaluated on their next usage." msgstr "" +#: src/elements/forms/DeleteBulkForm.ts +msgid "Are you sure you want to delete {0} {1}?" +msgstr "" + #: src/elements/forms/DeleteForm.ts msgid "Are you sure you want to delete {0} {objName} ?" msgstr "" @@ -509,6 +513,7 @@ msgid "Can be in the format of 'unix://' when connecting to a local docker daemo msgstr "" #: src/elements/forms/ConfirmationForm.ts +#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteForm.ts #: src/elements/forms/ModalForm.ts #: src/pages/groups/MemberSelectModal.ts @@ -1079,6 +1084,7 @@ msgstr "" msgid "Define how notifications are sent to users, like Email or Webhook." msgstr "" +#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteForm.ts #: src/elements/oauth/UserCodeList.ts #: src/elements/oauth/UserRefreshList.ts @@ -1150,6 +1156,7 @@ msgid "" "confirmation. Use a consent stage to ensure the user is aware of their actions." msgstr "" +#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteForm.ts msgid "Delete {0}" msgstr "" @@ -1591,6 +1598,7 @@ msgstr "" msgid "Failed to delete policy cache" msgstr "" +#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteForm.ts msgid "Failed to delete {0}: {1}" msgstr "" @@ -1872,6 +1880,7 @@ msgstr "" msgid "How many attempts a user has before the flow is canceled. To lock the user out, use a reputation policy and a user_write stage." msgstr "" +#: src/elements/forms/DeleteBulkForm.ts #: src/pages/stages/invitation/InvitationListPage.ts msgid "ID" msgstr "" @@ -2399,6 +2408,7 @@ msgstr "" msgid "My Applications" msgstr "" +#: src/elements/forms/DeleteBulkForm.ts #: src/pages/applications/ApplicationForm.ts #: src/pages/applications/ApplicationListPage.ts #: src/pages/crypto/CertificateKeyPairForm.ts @@ -2600,6 +2610,10 @@ msgstr "" msgid "Not synced yet." msgstr "" +#: src/elements/forms/DeleteBulkForm.ts +msgid "Not used by any other object." +msgstr "" + #: src/flows/stages/authenticator_duo/AuthenticatorDuoStage.ts #: src/flows/stages/authenticator_static/AuthenticatorStaticStage.ts #: src/flows/stages/authenticator_totp/AuthenticatorTOTPStage.ts @@ -3831,6 +3845,7 @@ msgstr "" msgid "Successfully created user." msgstr "" +#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteForm.ts msgid "Successfully deleted {0} {1}" msgstr "" @@ -4124,6 +4139,10 @@ msgstr "" msgid "The external URL you'll authenticate at. Can be the same domain as authentik." msgstr "" +#: src/elements/forms/DeleteBulkForm.ts +msgid "The following objects use {0}:" +msgstr "" + #: src/elements/forms/DeleteForm.ts msgid "The following objects use {objName}" msgstr "" @@ -4581,7 +4600,6 @@ msgstr "" #: src/pages/tokens/TokenListPage.ts #: src/pages/user-settings/tokens/UserTokenList.ts #: src/pages/users/UserListPage.ts -#: src/pages/users/UserListPage.ts msgid "User" msgstr "" @@ -4649,6 +4667,10 @@ msgstr "" msgid "User's display name." msgstr "" +#: src/pages/users/UserListPage.ts +msgid "User(s)" +msgstr "" + #: src/pages/providers/proxy/ProxyProviderForm.ts msgid "User/Group Attribute used for the password part of the HTTP-Basic Header." msgstr "" @@ -4889,18 +4911,25 @@ msgstr "" msgid "connecting object will be deleted" msgstr "" +#: src/elements/forms/DeleteBulkForm.ts +msgid "connection will be deleted" +msgstr "" + #: src/elements/Tabs.ts msgid "no tabs defined" msgstr "" +#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteForm.ts msgid "object will be DELETED" msgstr "" +#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteForm.ts msgid "reference will be reset to default value" msgstr "" +#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteForm.ts msgid "reference will be set to an empty value" msgstr "" @@ -4918,6 +4947,7 @@ msgstr "" msgid "{0} ({1})" msgstr "" +#: src/elements/forms/DeleteBulkForm.ts #: src/elements/forms/DeleteForm.ts msgid "{0} ({consequence})" msgstr "" diff --git a/web/src/pages/users/UserListPage.ts b/web/src/pages/users/UserListPage.ts index b0da5c975..5258d9eab 100644 --- a/web/src/pages/users/UserListPage.ts +++ b/web/src/pages/users/UserListPage.ts @@ -10,7 +10,7 @@ import { TableColumn } from "../../elements/table/Table"; import { PAGE_SIZE } from "../../constants"; import { CoreApi, User } from "authentik-api"; import { DEFAULT_CONFIG, tenant } from "../../api/Config"; -import "../../elements/forms/DeleteForm"; +import "../../elements/forms/DeleteBulkForm"; import "./UserActiveForm"; import "./UserForm"; import "./UserResetEmailForm"; @@ -71,26 +71,25 @@ export class UserListPage extends TablePage { } renderToolbarSelected(): TemplateResult { - const disabled = this.selectedElements.length !== 1; - const item = this.selectedElements[0]; - return html` { + const disabled = this.selectedElements.length < 1; + return html` { return new CoreApi(DEFAULT_CONFIG).coreUsersUsedByList({ - id: item.pk, + id: itemPk, }); }} - .delete=${() => { + .delete=${(itemPk: number) => { return new CoreApi(DEFAULT_CONFIG).coreUsersDestroy({ - id: item.pk, + id: itemPk, }); }} > - `; + `; } row(item: User): TemplateResult[] {