enterprise/reports: improve export list, confirmation (#18981)

* enterprise/reports: use verbose name for model label

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

* add confirmation for export

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

* update docs

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

* remove duplicated api

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

* fix duplicate

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

* fix search query not updated

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

* exclude page & page size

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

* improve query display

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

* fix user display

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

* exclude unset params

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

* Apply suggestions from code review

Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
Signed-off-by: Jens L. <jens@beryju.org>

* more code style

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

* format

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

* fix types

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>
This commit is contained in:
Jens L.
2025-12-22 20:35:18 +01:00
committed by GitHub
parent 3cd1a31365
commit 7fa28c60c7
9 changed files with 129 additions and 88 deletions
+8 -10
View File
@@ -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:
+5 -17
View File
@@ -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
+24 -3
View File
@@ -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<DataExport> {
protected override searchEnabled = true;
@@ -32,6 +38,8 @@ export class DataExportListPage extends TablePage<DataExport> {
@property({ type: String })
public order = "-requested_on";
static styles = [...TablePage.styles, PFDescriptionList];
async apiEndpoint(): Promise<PaginatedResponse<DataExport>> {
return new ReportsApi(DEFAULT_CONFIG).reportsExportsList(
await this.defaultEndpointConfig(),
@@ -65,12 +73,14 @@ export class DataExportListPage extends TablePage<DataExport> {
row(item: DataExport): SlottedTemplateResult[] {
return [
html`${item.contentType.model}`,
html`${item.contentType.verboseNamePlural}`,
html`<a href="#/identity/users/${item.requestedBy.pk}"
>${item.requestedBy.username}</a
>`,
Timestamp(item.requestedOn),
html`${item.completed ? msg("Yes") : msg("No")}`,
html`${item.completed
? html`<ak-label color=${PFColor.Green}>${msg("Finished")}</ak-label>`
: html`<ak-label color=${PFColor.Grey}>${msg("Queued")}</ak-label>`}`,
item.completed && item.fileUrl
? html`<div>
<a href="${item.fileUrl}">
@@ -87,7 +97,18 @@ export class DataExportListPage extends TablePage<DataExport> {
return html` <dl class="pf-c-description-list pf-m-horizontal">
<div class="pf-c-card__title">${msg("Query parameters")}</div>
<div class="pf-c-card__body">
<code>${JSON.stringify(item.queryParams, null, 4)}</code>
${renderDescriptionList(
Object.keys(item.queryParams)
.filter((key) => {
if (key === "page" || key === "pageSize") return false;
return !!item.queryParams[key];
})
.map((key): DescriptionPair => {
return [key, html`<pre>${item.queryParams[key]}</pre>`];
}),
{ horizontal: true, compact: true },
)}
</div>
</dl>`;
}
+5 -8
View File
@@ -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<Event>) {
protected renderToolbar(): TemplateResult {
return html`${super.renderToolbar()}
<ak-reports-export-button
.createExport=${this.#createExport}
.createExport=${(params: EventsEventsExportCreateRequest) => {
return new EventsApi(DEFAULT_CONFIG).eventsEventsExportCreate(params);
}}
.exportParams=${() => this.defaultEndpointConfig()}
></ak-reports-export-button>`;
}
#createExport = async () => {
await new EventsApi(DEFAULT_CONFIG).eventsEventsExportCreate({
...(await this.defaultEndpointConfig()),
});
};
}
declare global {
+66 -28
View File
@@ -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<void>) | null = null;
public createExport: (() => Promise<void>) | null = null;
public createExport: ((params: Record<string, string | undefined>) => Promise<void>) | null =
null;
@property({ attribute: false })
public exportParams: () => Promise<Record<string, string | undefined>> = () =>
Promise.resolve({});
@state()
protected params: Record<string, string | undefined> = {};
// 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`<button @click=${this.#clickHandler} class="pf-c-button pf-m-secondary">
${msg("Export")}
</button>`;
return html`<ak-forms-confirm
successMessage=${msg("Successfully requested data export")}
errorMessage=${msg("Failed to export data")}
.onConfirm=${this.#clickHandler}
@ak-modal-show=${() => {
this.exportParams().then((params) => {
this.params = params;
});
}}
action=${msg("Start export")}
actionLevel="pf-m-primary"
>
<span slot="header">${msg("Export data")}</span>
<div slot="body">
<p>
${msg(
str`${this.brand.brandingTitle} will collect all objects with the specified parameters:`,
)}
</p>
<br />
${renderDescriptionList(
Object.keys(this.params)
.filter((key) => {
if (key === "page" || key === "pageSize") return false;
return !!this.params[key];
})
.map((key): DescriptionPair => {
return [key, html`<pre>${this.params[key]}</pre>`];
}),
{ horizontal: true, compact: true },
)}
</div>
<button slot="trigger" class="pf-c-button pf-m-secondary" type="button">
${msg("Export")}
</button>
<div slot="modal"></div>
</ak-forms-confirm> `;
}
}
+11 -10
View File
@@ -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(
</button>
</ak-forms-modal>
<ak-reports-export-button
.createExport=${this.#createExport}
.createExport=${(params: CoreUsersExportCreateRequest) => {
return new CoreApi(DEFAULT_CONFIG).coreUsersExportCreate(params);
}}
.exportParams=${async () => {
return {
...(await this.defaultEndpointConfig()),
pathStartswith: this.activePath,
isActive: this.hideDeactivated ? true : undefined,
};
}}
></ak-reports-export-button>
`;
}
+4 -1
View File
@@ -21,6 +21,9 @@ export class ConfirmationForm extends ModalButton {
@property()
action!: string;
@property()
actionLevel = "pf-m-danger";
@property({ attribute: false })
onConfirm!: () => Promise<unknown>;
@@ -76,7 +79,7 @@ export class ConfirmationForm extends ModalButton {
.callAction=${() => {
return this.confirm();
}}
class="pf-m-danger"
class=${this.actionLevel}
>
${this.action} </ak-spinner-button
>&nbsp;
@@ -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.
@@ -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.