diff --git a/authentik/enterprise/reports/api/reports.py b/authentik/enterprise/reports/api/reports.py index c16be1baab..9251f79138 100644 --- a/authentik/enterprise/reports/api/reports.py +++ b/authentik/enterprise/reports/api/reports.py @@ -4,37 +4,35 @@ from django.urls import reverse from drf_spectacular.utils import extend_schema from rest_framework import mixins from rest_framework.decorators import action -from rest_framework.fields import CharField +from rest_framework.fields import CharField, SerializerMethodField from rest_framework.permissions import BasePermission from rest_framework.request import Request from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet +from authentik.core.api.groups import PartialUserSerializer from authentik.core.api.utils import ModelSerializer -from authentik.core.models import User from authentik.enterprise.api import EnterpriseRequiredMixin from authentik.enterprise.reports.models import DataExport from authentik.enterprise.reports.tasks import generate_export from authentik.rbac.permissions import HasPermission -class RequestedBySerializer(ModelSerializer): - class Meta: - model = User - fields = ("pk", "username") - - class ContentTypeSerializer(ModelSerializer): app_label = CharField(read_only=True) model = CharField(read_only=True) + verbose_name_plural = SerializerMethodField() + + def get_verbose_name_plural(self, ct: ContentType) -> str: + return ct.model_class()._meta.verbose_name_plural class Meta: model = ContentType - fields = ("id", "app_label", "model") + fields = ("id", "app_label", "model", "verbose_name_plural") class DataExportSerializer(EnterpriseRequiredMixin, ModelSerializer): - requested_by = RequestedBySerializer(read_only=True) + requested_by = PartialUserSerializer(read_only=True) content_type = ContentTypeSerializer(read_only=True) class Meta: diff --git a/schema.yml b/schema.yml index e2bbe86542..56fe250fbd 100644 --- a/schema.yml +++ b/schema.yml @@ -35387,10 +35387,14 @@ components: model: type: string readOnly: true + verbose_name_plural: + type: string + readOnly: true required: - app_label - id - model + - verbose_name_plural ContextualFlowInfo: type: object description: Contextual flow information for a challenge @@ -35738,7 +35742,7 @@ components: readOnly: true requested_by: allOf: - - $ref: '#/components/schemas/RequestedBy' + - $ref: '#/components/schemas/PartialUser' readOnly: true requested_on: type: string @@ -51252,22 +51256,6 @@ components: minimum: -2147483648 required: - name - RequestedBy: - type: object - properties: - pk: - type: integer - readOnly: true - title: ID - username: - type: string - description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_ - only. - pattern: ^[\w.@+-]+$ - maxLength: 150 - required: - - pk - - username ResidentKeyRequirementEnum: enum: - discouraged diff --git a/web/src/admin/events/DataExportListPage.ts b/web/src/admin/events/DataExportListPage.ts index b017e6f268..1a12ebdc5c 100644 --- a/web/src/admin/events/DataExportListPage.ts +++ b/web/src/admin/events/DataExportListPage.ts @@ -4,20 +4,26 @@ import "#elements/buttons/SpinnerButton/index"; import "#elements/forms/DeleteBulkForm"; import "#elements/forms/ModalForm"; import "#elements/tasks/TaskList"; +import "#components/ak-status-label"; import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; import { DEFAULT_CONFIG } from "#common/api/config"; +import { PFColor } from "#elements/Label"; import { PaginatedResponse, TableColumn, Timestamp } from "#elements/table/Table"; import { TablePage } from "#elements/table/TablePage"; import { SlottedTemplateResult } from "#elements/types"; +import renderDescriptionList, { DescriptionPair } from "#components/DescriptionList"; + import { DataExport, ReportsApi } from "@goauthentik/api"; import { msg } from "@lit/localize"; import { html, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; +import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; + @customElement("ak-data-export-list") export class DataExportListPage extends TablePage { protected override searchEnabled = true; @@ -32,6 +38,8 @@ export class DataExportListPage extends TablePage { @property({ type: String }) public order = "-requested_on"; + static styles = [...TablePage.styles, PFDescriptionList]; + async apiEndpoint(): Promise> { return new ReportsApi(DEFAULT_CONFIG).reportsExportsList( await this.defaultEndpointConfig(), @@ -65,12 +73,14 @@ export class DataExportListPage extends TablePage { row(item: DataExport): SlottedTemplateResult[] { return [ - html`${item.contentType.model}`, + html`${item.contentType.verboseNamePlural}`, html`${item.requestedBy.username}`, Timestamp(item.requestedOn), - html`${item.completed ? msg("Yes") : msg("No")}`, + html`${item.completed + ? html`${msg("Finished")}` + : html`${msg("Queued")}`}`, item.completed && item.fileUrl ? html`
@@ -87,7 +97,18 @@ export class DataExportListPage extends TablePage { return html`
${msg("Query parameters")}
- ${JSON.stringify(item.queryParams, null, 4)} + ${renderDescriptionList( + Object.keys(item.queryParams) + .filter((key) => { + if (key === "page" || key === "pageSize") return false; + + return !!item.queryParams[key]; + }) + .map((key): DescriptionPair => { + return [key, html`
${item.queryParams[key]}
`]; + }), + { horizontal: true, compact: true }, + )}
`; } diff --git a/web/src/admin/events/EventListPage.ts b/web/src/admin/events/EventListPage.ts index 4e34222e9e..08304f7841 100644 --- a/web/src/admin/events/EventListPage.ts +++ b/web/src/admin/events/EventListPage.ts @@ -16,7 +16,7 @@ import { SlottedTemplateResult } from "#elements/types"; import { EventGeo, renderEventUser } from "#admin/events/utils"; -import { Event, EventsApi } from "@goauthentik/api"; +import { Event, EventsApi, EventsEventsExportCreateRequest } from "@goauthentik/api"; import { msg } from "@lit/localize"; import { css, CSSResult, html, TemplateResult } from "lit"; @@ -123,15 +123,12 @@ export class EventListPage extends WithLicenseSummary(TablePage) { protected renderToolbar(): TemplateResult { return html`${super.renderToolbar()} { + return new EventsApi(DEFAULT_CONFIG).eventsEventsExportCreate(params); + }} + .exportParams=${() => this.defaultEndpointConfig()} >`; } - - #createExport = async () => { - await new EventsApi(DEFAULT_CONFIG).eventsEventsExportCreate({ - ...(await this.defaultEndpointConfig()), - }); - }; } declare global { diff --git a/web/src/admin/reports/ExportButton.ts b/web/src/admin/reports/ExportButton.ts index 417d7e61bf..0b525edea2 100644 --- a/web/src/admin/reports/ExportButton.ts +++ b/web/src/admin/reports/ExportButton.ts @@ -1,25 +1,37 @@ -import { parseAPIResponseError } from "../../common/errors/network"; -import { MessageLevel } from "../../common/messages"; -import { AKElement } from "../../elements/Base"; -import { showAPIErrorMessage, showMessage } from "../../elements/messages/MessageContainer"; -import { SlottedTemplateResult } from "../../elements/types"; +import "#elements/forms/ConfirmationForm"; +import { parseAPIResponseError } from "#common/errors/network"; + +import { AKElement } from "#elements/Base"; +import { showAPIErrorMessage } from "#elements/messages/MessageContainer"; +import { WithBrandConfig } from "#elements/mixins/branding"; import { WithLicenseSummary } from "#elements/mixins/license"; +import { SlottedTemplateResult } from "#elements/types"; -import { msg } from "@lit/localize"; +import renderDescriptionList, { DescriptionPair } from "#components/DescriptionList"; + +import { msg, str } from "@lit/localize"; import { CSSResult, html, nothing, PropertyValues } from "lit"; -import { customElement, property } from "lit/decorators.js"; +import { customElement, property, state } from "lit/decorators.js"; import PFButton from "@patternfly/patternfly/components/Button/button.css"; -import PFBase from "@patternfly/patternfly/patternfly-base.css"; +import PFContent from "@patternfly/patternfly/components/Content/content.css"; +import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; @customElement("ak-reports-export-button") -export class ExportButton extends WithLicenseSummary(AKElement) { - static styles: CSSResult[] = [PFBase, PFButton]; +export class ExportButton extends WithBrandConfig(WithLicenseSummary(AKElement)) { + static styles: CSSResult[] = [PFButton, PFContent, PFDescriptionList]; @property({ attribute: false }) - // public createExport: (() => Promise) | null = null; - public createExport: (() => Promise) | null = null; + public createExport: ((params: Record) => Promise) | null = + null; + + @property({ attribute: false }) + public exportParams: () => Promise> = () => + Promise.resolve({}); + + @state() + protected params: Record = {}; // safest display setting for a button cachedDisplay = "inline-block"; @@ -43,28 +55,54 @@ export class ExportButton extends WithLicenseSummary(AKElement) { if (typeof this.createExport !== "function") { throw new TypeError("`createExport` property must be a function"); } - - return this.createExport() - .then(() => { - showMessage({ - level: MessageLevel.success, - message: msg("Data export requested successfully"), - description: msg("You will receive a notification once the data is ready"), - }); - }) - .catch(async (error) => { - const apiError = await parseAPIResponseError(error); - showAPIErrorMessage(apiError); - }); + return this.createExport(this.params).catch(async (error) => { + const apiError = await parseAPIResponseError(error); + showAPIErrorMessage(apiError); + }); }; render(): SlottedTemplateResult { if (!this.hasEnterpriseLicense) { return nothing; } - return html``; + return html` { + this.exportParams().then((params) => { + this.params = params; + }); + }} + action=${msg("Start export")} + actionLevel="pf-m-primary" + > + ${msg("Export data")} +
+

+ ${msg( + str`${this.brand.brandingTitle} will collect all objects with the specified parameters:`, + )} +

+
+ ${renderDescriptionList( + Object.keys(this.params) + .filter((key) => { + if (key === "page" || key === "pageSize") return false; + + return !!this.params[key]; + }) + .map((key): DescriptionPair => { + return [key, html`
${this.params[key]}
`]; + }), + { horizontal: true, compact: true }, + )} +
+ +
+
`; } } diff --git a/web/src/admin/users/UserListPage.ts b/web/src/admin/users/UserListPage.ts index 77c9d48348..be72ddf240 100644 --- a/web/src/admin/users/UserListPage.ts +++ b/web/src/admin/users/UserListPage.ts @@ -29,7 +29,7 @@ import { TablePage } from "#elements/table/TablePage"; import { SlottedTemplateResult } from "#elements/types"; import { writeToClipboard } from "#elements/utils/writeToClipboard"; -import { CoreApi, User, UserPath } from "@goauthentik/api"; +import { CoreApi, CoreUsersExportCreateRequest, User, UserPath } from "@goauthentik/api"; import { msg, str } from "@lit/localize"; import { css, CSSResult, html, nothing, TemplateResult } from "lit"; @@ -156,14 +156,6 @@ export class UserListPage extends WithBrandConfig( [msg("Actions"), null, msg("Row Actions")], ]; - #createExport = async () => { - await new CoreApi(DEFAULT_CONFIG).coreUsersExportCreate({ - ...(await this.defaultEndpointConfig()), - pathStartswith: this.activePath, - isActive: this.hideDeactivated ? true : undefined, - }); - }; - renderToolbarSelected(): TemplateResult { const disabled = this.selectedElements.length < 1; const { currentUser, originalUser } = this; @@ -403,7 +395,16 @@ export class UserListPage extends WithBrandConfig( { + return new CoreApi(DEFAULT_CONFIG).coreUsersExportCreate(params); + }} + .exportParams=${async () => { + return { + ...(await this.defaultEndpointConfig()), + pathStartswith: this.activePath, + isActive: this.hideDeactivated ? true : undefined, + }; + }} > `; } diff --git a/web/src/elements/forms/ConfirmationForm.ts b/web/src/elements/forms/ConfirmationForm.ts index 96aa905641..ad08de0bcd 100644 --- a/web/src/elements/forms/ConfirmationForm.ts +++ b/web/src/elements/forms/ConfirmationForm.ts @@ -21,6 +21,9 @@ export class ConfirmationForm extends ModalButton { @property() action!: string; + @property() + actionLevel = "pf-m-danger"; + @property({ attribute: false }) onConfirm!: () => Promise; @@ -76,7 +79,7 @@ export class ConfirmationForm extends ModalButton { .callAction=${() => { return this.confirm(); }} - class="pf-m-danger" + class=${this.actionLevel} > ${this.action}   diff --git a/website/docs/sys-mgmt/events/logging-events.md b/website/docs/sys-mgmt/events/logging-events.md index 37ea0a754e..81602a6310 100644 --- a/website/docs/sys-mgmt/events/logging-events.md +++ b/website/docs/sys-mgmt/events/logging-events.md @@ -67,14 +67,8 @@ You can export your authentik instance's events to a CSV file. To generate a dat 1. Log in to authentik as an administrator and navigate to **Events > Logs**. 2. Set a [search query](#tell-me-more) as well as the ordering, as needed. The data export will honor these settings. 3. Click **Export** above the event list. -4. Note that the export is processed in the background. Once the export is ready, you will receive a notification in the notification drawer. -5. In the notification, click **Download**. - -6. Log in to authentik as an administrator and open the authentik Admin interface. -7. Navigate to **Events** > **Logs**. -8. Set a [search query](#tell-me-more) as well as the ordering for the data export. -9. Click **Export** above the event list. -10. The export is processed in the background and after it's ready, you will receive a notification in the notification drawer. -11. In the notification, click **Download**. +4. Confirm the export parameters in the confirmation dialog. +5. Note that the export is processed in the background. Once the export is ready, you will receive a notification in the notification drawer. +6. In the notification, click **Download**. To review, download, or delete past data exports, navigate to **Events** > **Data Exports** in the Admin interface. diff --git a/website/docs/users-sources/user/user_basic_operations.md b/website/docs/users-sources/user/user_basic_operations.md index ff5483e6fc..3cb8ea7603 100644 --- a/website/docs/users-sources/user/user_basic_operations.md +++ b/website/docs/users-sources/user/user_basic_operations.md @@ -196,7 +196,8 @@ You can export your authentik instance's user data to a CSV file. To generate a 2. Navigate to **Directory** > **Users** and click **Export**. 3. Set a [search query](#tell-me-more) as well as the ordering for the data export. 4. Click **Export** above the event list. -5. The export is processed in the background and after it's ready, you will receive a notification in the Admin interface's notification area. -6. In the notification, click **Download**. +5. Confirm the export parameters in the confirmation dialog. +6. The export is processed in the background. When it's ready, you will receive a notification in the Admin interface's notification area. +7. In the notification, click **Download**. To review, download, or delete past data exports, navigate to **Events** > **Data Exports** in the Admin interface.