mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> `;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
>
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user