events: rework metrics endpoint (#14934)
* rework event volume Signed-off-by: Jens Langhammer <jens@goauthentik.io> * migrate more Signed-off-by: Jens Langhammer <jens@goauthentik.io> * migrate more Signed-off-by: Jens Langhammer <jens@goauthentik.io> * migrate more Signed-off-by: Jens Langhammer <jens@goauthentik.io> * the rest of the owl Signed-off-by: Jens Langhammer <jens@goauthentik.io> * client-side data padding Signed-off-by: Jens Langhammer <jens@goauthentik.io> * I love deleting code Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix clamping Signed-off-by: Jens Langhammer <jens@goauthentik.io> * chunk it Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add event-to-color map Signed-off-by: Jens Langhammer <jens@goauthentik.io> * sync colours Signed-off-by: Jens Langhammer <jens@goauthentik.io> * switch colours Signed-off-by: Jens Langhammer <jens@goauthentik.io> * heatmap? Signed-off-by: Jens Langhammer <jens@goauthentik.io> * Revert "heatmap?" This reverts commitc1f549a18b. * Revert "Revert "heatmap?"" This reverts commit6d6175b96b. * Revert "Revert "Revert "heatmap?""" This reverts commit3717903f12. * format Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
		@ -13,7 +13,7 @@ import PFList from "@patternfly/patternfly/components/List/list.css";
 | 
			
		||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
 | 
			
		||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
 | 
			
		||||
 | 
			
		||||
import { EventActions } from "@goauthentik/api";
 | 
			
		||||
import { EventActions, EventsEventsVolumeListRequest } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-admin-dashboard-users")
 | 
			
		||||
export class DashboardUserPage extends AKElement {
 | 
			
		||||
@ -46,9 +46,9 @@ export class DashboardUserPage extends AKElement {
 | 
			
		||||
                        <ak-aggregate-card header=${msg("Users created per day in the last month")}>
 | 
			
		||||
                            <ak-charts-admin-model-per-day
 | 
			
		||||
                                .query=${{
 | 
			
		||||
                                    context__model__app: "authentik_core",
 | 
			
		||||
                                    context__model__model_name: "user",
 | 
			
		||||
                                }}
 | 
			
		||||
                                    contextModelApp: "authentik_core",
 | 
			
		||||
                                    contextModelName: "user",
 | 
			
		||||
                                } as EventsEventsVolumeListRequest}
 | 
			
		||||
                                label=${msg("Users created")}
 | 
			
		||||
                            >
 | 
			
		||||
                            </ak-charts-admin-model-per-day>
 | 
			
		||||
 | 
			
		||||
@ -1,68 +1,51 @@
 | 
			
		||||
import { EventChart } from "#elements/charts/EventChart";
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { AKChart, RGBAColor } from "@goauthentik/elements/charts/Chart";
 | 
			
		||||
import { ChartData } from "chart.js";
 | 
			
		||||
import { ChartData, ChartDataset } from "chart.js";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
import { AdminApi, LoginMetrics } from "@goauthentik/api";
 | 
			
		||||
import { EventActions, EventVolume, EventsApi } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-charts-admin-login-authorization")
 | 
			
		||||
export class AdminLoginAuthorizeChart extends AKChart<LoginMetrics> {
 | 
			
		||||
    async apiRequest(): Promise<LoginMetrics> {
 | 
			
		||||
        return new AdminApi(DEFAULT_CONFIG).adminMetricsRetrieve();
 | 
			
		||||
export class AdminLoginAuthorizeChart extends EventChart {
 | 
			
		||||
    async apiRequest(): Promise<EventVolume[]> {
 | 
			
		||||
        return new EventsApi(DEFAULT_CONFIG).eventsEventsVolumeList({
 | 
			
		||||
            actions: [
 | 
			
		||||
                EventActions.AuthorizeApplication,
 | 
			
		||||
                EventActions.Login,
 | 
			
		||||
                EventActions.LoginFailed,
 | 
			
		||||
            ],
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getChartData(data: LoginMetrics): ChartData {
 | 
			
		||||
        return {
 | 
			
		||||
            datasets: [
 | 
			
		||||
                {
 | 
			
		||||
                    label: msg("Authorizations"),
 | 
			
		||||
                    backgroundColor: new RGBAColor(43, 154, 243, 0.5).toString(),
 | 
			
		||||
                    borderColor: new RGBAColor(43, 154, 243, 1).toString(),
 | 
			
		||||
                    spanGaps: true,
 | 
			
		||||
                    fill: "origin",
 | 
			
		||||
                    cubicInterpolationMode: "monotone",
 | 
			
		||||
                    tension: 0.4,
 | 
			
		||||
                    data: data.authorizations.map((cord) => {
 | 
			
		||||
                        return {
 | 
			
		||||
                            x: cord.xCord,
 | 
			
		||||
                            y: cord.yCord,
 | 
			
		||||
                        };
 | 
			
		||||
                    }),
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    label: msg("Failed Logins"),
 | 
			
		||||
                    backgroundColor: new RGBAColor(201, 24, 11, 0.5).toString(),
 | 
			
		||||
                    borderColor: new RGBAColor(201, 24, 11, 1).toString(),
 | 
			
		||||
                    spanGaps: true,
 | 
			
		||||
                    fill: "origin",
 | 
			
		||||
                    cubicInterpolationMode: "monotone",
 | 
			
		||||
                    tension: 0.4,
 | 
			
		||||
                    data: data.loginsFailed.map((cord) => {
 | 
			
		||||
                        return {
 | 
			
		||||
                            x: cord.xCord,
 | 
			
		||||
                            y: cord.yCord,
 | 
			
		||||
                        };
 | 
			
		||||
                    }),
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    label: msg("Successful Logins"),
 | 
			
		||||
                    backgroundColor: new RGBAColor(62, 134, 53, 0.5).toString(),
 | 
			
		||||
                    borderColor: new RGBAColor(62, 134, 53, 1).toString(),
 | 
			
		||||
                    spanGaps: true,
 | 
			
		||||
                    fill: "origin",
 | 
			
		||||
                    cubicInterpolationMode: "monotone",
 | 
			
		||||
                    tension: 0.4,
 | 
			
		||||
                    data: data.logins.map((cord) => {
 | 
			
		||||
                        return {
 | 
			
		||||
                            x: cord.xCord,
 | 
			
		||||
                            y: cord.yCord,
 | 
			
		||||
                        };
 | 
			
		||||
                    }),
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        };
 | 
			
		||||
    getChartData(data: EventVolume[]): ChartData {
 | 
			
		||||
        const optsMap = new Map<EventActions, Partial<ChartDataset>>();
 | 
			
		||||
        optsMap.set(EventActions.AuthorizeApplication, {
 | 
			
		||||
            label: msg("Authorizations"),
 | 
			
		||||
            spanGaps: true,
 | 
			
		||||
            fill: "origin",
 | 
			
		||||
            cubicInterpolationMode: "monotone",
 | 
			
		||||
            tension: 0.4,
 | 
			
		||||
        });
 | 
			
		||||
        optsMap.set(EventActions.Login, {
 | 
			
		||||
            label: msg("Successful Logins"),
 | 
			
		||||
            spanGaps: true,
 | 
			
		||||
            fill: "origin",
 | 
			
		||||
            cubicInterpolationMode: "monotone",
 | 
			
		||||
            tension: 0.4,
 | 
			
		||||
        });
 | 
			
		||||
        optsMap.set(EventActions.LoginFailed, {
 | 
			
		||||
            label: msg("Failed Logins"),
 | 
			
		||||
            spanGaps: true,
 | 
			
		||||
            fill: "origin",
 | 
			
		||||
            cubicInterpolationMode: "monotone",
 | 
			
		||||
            tension: 0.4,
 | 
			
		||||
        });
 | 
			
		||||
        return this.eventVolume(data, {
 | 
			
		||||
            optsMap: optsMap,
 | 
			
		||||
            padToDays: 7,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,19 @@
 | 
			
		||||
import { EventChart } from "#elements/charts/EventChart";
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { AKChart } from "@goauthentik/elements/charts/Chart";
 | 
			
		||||
import { ChartData, Tick } from "chart.js";
 | 
			
		||||
import { ChartData } from "chart.js";
 | 
			
		||||
 | 
			
		||||
import { msg, str } from "@lit/localize";
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement, property } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
import { Coordinate, EventActions, EventsApi } from "@goauthentik/api";
 | 
			
		||||
import {
 | 
			
		||||
    EventActions,
 | 
			
		||||
    EventVolume,
 | 
			
		||||
    EventsApi,
 | 
			
		||||
    EventsEventsVolumeListRequest,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-charts-admin-model-per-day")
 | 
			
		||||
export class AdminModelPerDay extends AKChart<Coordinate[]> {
 | 
			
		||||
export class AdminModelPerDay extends EventChart {
 | 
			
		||||
    @property()
 | 
			
		||||
    action: EventActions = EventActions.ModelCreated;
 | 
			
		||||
 | 
			
		||||
@ -16,39 +21,29 @@ export class AdminModelPerDay extends AKChart<Coordinate[]> {
 | 
			
		||||
    label?: string;
 | 
			
		||||
 | 
			
		||||
    @property({ attribute: false })
 | 
			
		||||
    query?: { [key: string]: unknown } | undefined;
 | 
			
		||||
    query?: EventsEventsVolumeListRequest;
 | 
			
		||||
 | 
			
		||||
    async apiRequest(): Promise<Coordinate[]> {
 | 
			
		||||
        return new EventsApi(DEFAULT_CONFIG).eventsEventsPerMonthList({
 | 
			
		||||
    async apiRequest(): Promise<EventVolume[]> {
 | 
			
		||||
        return new EventsApi(DEFAULT_CONFIG).eventsEventsVolumeList({
 | 
			
		||||
            action: this.action,
 | 
			
		||||
            query: JSON.stringify(this.query || {}),
 | 
			
		||||
            historyDays: 30,
 | 
			
		||||
            ...this.query,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    timeTickCallback(tickValue: string | number, index: number, ticks: Tick[]): string {
 | 
			
		||||
        const valueStamp = ticks[index];
 | 
			
		||||
        const delta = Date.now() - valueStamp.value;
 | 
			
		||||
        const ago = Math.round(delta / 1000 / 3600 / 24);
 | 
			
		||||
        return msg(str`${ago} days ago`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getChartData(data: Coordinate[]): ChartData {
 | 
			
		||||
        return {
 | 
			
		||||
            datasets: [
 | 
			
		||||
                {
 | 
			
		||||
                    label: this.label || msg("Objects created"),
 | 
			
		||||
                    backgroundColor: "rgba(189, 229, 184, .5)",
 | 
			
		||||
                    spanGaps: true,
 | 
			
		||||
                    data:
 | 
			
		||||
                        data.map((cord) => {
 | 
			
		||||
                            return {
 | 
			
		||||
                                x: cord.xCord || 0,
 | 
			
		||||
                                y: cord.yCord || 0,
 | 
			
		||||
                            };
 | 
			
		||||
                        }) || [],
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        };
 | 
			
		||||
    getChartData(data: EventVolume[]): ChartData {
 | 
			
		||||
        return this.eventVolume(data, {
 | 
			
		||||
            optsMap: new Map([
 | 
			
		||||
                [
 | 
			
		||||
                    this.action,
 | 
			
		||||
                    {
 | 
			
		||||
                        label: this.label || msg("Objects created"),
 | 
			
		||||
                        spanGaps: true,
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            ]),
 | 
			
		||||
            padToDays: 30,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import { actionToColor } from "#elements/charts/EventChart";
 | 
			
		||||
import { SummarizedSyncStatus } from "@goauthentik/admin/admin-overview/charts/SyncStatusChart";
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { AKChart } from "@goauthentik/elements/charts/Chart";
 | 
			
		||||
@ -7,7 +8,7 @@ import { ChartData, ChartOptions } from "chart.js";
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
import { OutpostsApi } from "@goauthentik/api";
 | 
			
		||||
import { EventActions, OutpostsApi } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-admin-status-chart-outpost")
 | 
			
		||||
export class OutpostStatusChart extends AKChart<SummarizedSyncStatus[]> {
 | 
			
		||||
@ -65,7 +66,11 @@ export class OutpostStatusChart extends AKChart<SummarizedSyncStatus[]> {
 | 
			
		||||
            labels: [msg("Healthy outposts"), msg("Outdated outposts"), msg("Unhealthy outposts")],
 | 
			
		||||
            datasets: data.map((d) => {
 | 
			
		||||
                return {
 | 
			
		||||
                    backgroundColor: ["#3e8635", "#C9190B", "#2b9af3"],
 | 
			
		||||
                    backgroundColor: [
 | 
			
		||||
                        actionToColor(EventActions.Login),
 | 
			
		||||
                        actionToColor(EventActions.SuspiciousRequest),
 | 
			
		||||
                        actionToColor(EventActions.AuthorizeApplication),
 | 
			
		||||
                    ],
 | 
			
		||||
                    spanGaps: true,
 | 
			
		||||
                    data: [d.healthy, d.failed, d.unsynced],
 | 
			
		||||
                    label: d.label,
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import { actionToColor } from "#elements/charts/EventChart";
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { AKChart } from "@goauthentik/elements/charts/Chart";
 | 
			
		||||
import "@goauthentik/elements/forms/ConfirmationForm";
 | 
			
		||||
@ -7,7 +8,13 @@ import { ChartData, ChartOptions } from "chart.js";
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
import { ProvidersApi, SourcesApi, SyncStatus, SystemTaskStatusEnum } from "@goauthentik/api";
 | 
			
		||||
import {
 | 
			
		||||
    EventActions,
 | 
			
		||||
    ProvidersApi,
 | 
			
		||||
    SourcesApi,
 | 
			
		||||
    SyncStatus,
 | 
			
		||||
    SystemTaskStatusEnum,
 | 
			
		||||
} from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
export interface SummarizedSyncStatus {
 | 
			
		||||
    healthy: number;
 | 
			
		||||
@ -136,7 +143,11 @@ export class SyncStatusChart extends AKChart<SummarizedSyncStatus[]> {
 | 
			
		||||
            labels: [msg("Healthy"), msg("Failed"), msg("Unsynced / N/A")],
 | 
			
		||||
            datasets: data.map((d) => {
 | 
			
		||||
                return {
 | 
			
		||||
                    backgroundColor: ["#3e8635", "#C9190B", "#2b9af3"],
 | 
			
		||||
                    backgroundColor: [
 | 
			
		||||
                        actionToColor(EventActions.Login),
 | 
			
		||||
                        actionToColor(EventActions.SuspiciousRequest),
 | 
			
		||||
                        actionToColor(EventActions.AuthorizeApplication),
 | 
			
		||||
                    ],
 | 
			
		||||
                    spanGaps: true,
 | 
			
		||||
                    data: [d.healthy, d.failed, d.unsynced],
 | 
			
		||||
                    label: d.label,
 | 
			
		||||
 | 
			
		||||
@ -1,47 +1,37 @@
 | 
			
		||||
import { EventChart } from "#elements/charts/EventChart";
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { AKChart } from "@goauthentik/elements/charts/Chart";
 | 
			
		||||
import { ChartData, Tick } from "chart.js";
 | 
			
		||||
import { ChartData } from "chart.js";
 | 
			
		||||
 | 
			
		||||
import { msg, str } from "@lit/localize";
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement, property } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
import { Coordinate, CoreApi } from "@goauthentik/api";
 | 
			
		||||
import { EventActions, EventVolume, EventsApi } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-charts-application-authorize")
 | 
			
		||||
export class ApplicationAuthorizeChart extends AKChart<Coordinate[]> {
 | 
			
		||||
    @property()
 | 
			
		||||
    applicationSlug!: string;
 | 
			
		||||
export class ApplicationAuthorizeChart extends EventChart {
 | 
			
		||||
    @property({ attribute: "application-id" })
 | 
			
		||||
    applicationId!: string;
 | 
			
		||||
 | 
			
		||||
    async apiRequest(): Promise<Coordinate[]> {
 | 
			
		||||
        return new CoreApi(DEFAULT_CONFIG).coreApplicationsMetricsList({
 | 
			
		||||
            slug: this.applicationSlug,
 | 
			
		||||
    async apiRequest(): Promise<EventVolume[]> {
 | 
			
		||||
        return new EventsApi(DEFAULT_CONFIG).eventsEventsVolumeList({
 | 
			
		||||
            action: EventActions.AuthorizeApplication,
 | 
			
		||||
            contextAuthorizedApp: this.applicationId.replaceAll("-", ""),
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    timeTickCallback(tickValue: string | number, index: number, ticks: Tick[]): string {
 | 
			
		||||
        const valueStamp = ticks[index];
 | 
			
		||||
        const delta = Date.now() - valueStamp.value;
 | 
			
		||||
        const ago = Math.round(delta / 1000 / 3600 / 24);
 | 
			
		||||
        return msg(str`${ago} days ago`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getChartData(data: Coordinate[]): ChartData {
 | 
			
		||||
        return {
 | 
			
		||||
            datasets: [
 | 
			
		||||
                {
 | 
			
		||||
                    label: msg("Authorizations"),
 | 
			
		||||
                    backgroundColor: "rgba(189, 229, 184, .5)",
 | 
			
		||||
                    spanGaps: true,
 | 
			
		||||
                    data:
 | 
			
		||||
                        data.map((cord) => {
 | 
			
		||||
                            return {
 | 
			
		||||
                                x: cord.xCord || 0,
 | 
			
		||||
                                y: cord.yCord || 0,
 | 
			
		||||
                            };
 | 
			
		||||
                        }) || [],
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        };
 | 
			
		||||
    getChartData(data: EventVolume[]): ChartData {
 | 
			
		||||
        return this.eventVolume(data, {
 | 
			
		||||
            optsMap: new Map([
 | 
			
		||||
                [
 | 
			
		||||
                    EventActions.AuthorizeApplication,
 | 
			
		||||
                    {
 | 
			
		||||
                        label: msg("Authorizations"),
 | 
			
		||||
                        spanGaps: true,
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            ]),
 | 
			
		||||
            padToDays: 7,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -282,7 +282,7 @@ export class ApplicationViewPage extends AKElement {
 | 
			
		||||
                        <div class="pf-c-card__body">
 | 
			
		||||
                            ${this.application &&
 | 
			
		||||
                            html` <ak-charts-application-authorize
 | 
			
		||||
                                applicationSlug=${this.application.slug}
 | 
			
		||||
                                application-id=${this.application.pk}
 | 
			
		||||
                            >
 | 
			
		||||
                            </ak-charts-application-authorize>`}
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
@ -1,21 +1,21 @@
 | 
			
		||||
import { EventChart } from "#elements/charts/EventChart";
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { AKChart } from "@goauthentik/elements/charts/Chart";
 | 
			
		||||
import { ChartData } from "chart.js";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { CSSResult, TemplateResult, css, html } from "lit";
 | 
			
		||||
import { customElement, property } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
 | 
			
		||||
 | 
			
		||||
import { Coordinate, EventsApi, EventsEventsListRequest } from "@goauthentik/api";
 | 
			
		||||
import { EventVolume, EventsApi, EventsEventsListRequest } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-events-volume-chart")
 | 
			
		||||
export class EventVolumeChart extends AKChart<Coordinate[]> {
 | 
			
		||||
export class EventVolumeChart extends EventChart {
 | 
			
		||||
    _query?: EventsEventsListRequest;
 | 
			
		||||
 | 
			
		||||
    @property({ attribute: false })
 | 
			
		||||
    set query(value: EventsEventsListRequest | undefined) {
 | 
			
		||||
        if (JSON.stringify(this._query) === JSON.stringify(value)) return;
 | 
			
		||||
        this._query = value;
 | 
			
		||||
        this.refreshHandler();
 | 
			
		||||
    }
 | 
			
		||||
@ -24,39 +24,28 @@ export class EventVolumeChart extends AKChart<Coordinate[]> {
 | 
			
		||||
        return super.styles.concat(
 | 
			
		||||
            PFCard,
 | 
			
		||||
            css`
 | 
			
		||||
                .pf-c-card__body {
 | 
			
		||||
                    height: 12rem;
 | 
			
		||||
                .pf-c-card {
 | 
			
		||||
                    height: 20rem;
 | 
			
		||||
                }
 | 
			
		||||
            `,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    apiRequest(): Promise<Coordinate[]> {
 | 
			
		||||
        return new EventsApi(DEFAULT_CONFIG).eventsEventsVolumeList(this._query);
 | 
			
		||||
    apiRequest(): Promise<EventVolume[]> {
 | 
			
		||||
        return new EventsApi(DEFAULT_CONFIG).eventsEventsVolumeList({
 | 
			
		||||
            historyDays: 7,
 | 
			
		||||
            ...this._query,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getChartData(data: Coordinate[]): ChartData {
 | 
			
		||||
        return {
 | 
			
		||||
            datasets: [
 | 
			
		||||
                {
 | 
			
		||||
                    label: msg("Events"),
 | 
			
		||||
                    backgroundColor: "rgba(189, 229, 184, .5)",
 | 
			
		||||
                    spanGaps: true,
 | 
			
		||||
                    data:
 | 
			
		||||
                        data.map((cord) => {
 | 
			
		||||
                            return {
 | 
			
		||||
                                x: cord.xCord || 0,
 | 
			
		||||
                                y: cord.yCord || 0,
 | 
			
		||||
                            };
 | 
			
		||||
                        }) || [],
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        };
 | 
			
		||||
    getChartData(data: EventVolume[]): ChartData {
 | 
			
		||||
        return this.eventVolume(data, {
 | 
			
		||||
            padToDays: 7,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render(): TemplateResult {
 | 
			
		||||
        return html`<div class="pf-c-card">
 | 
			
		||||
            <div class="pf-c-card__title">${msg("Event volume")}</div>
 | 
			
		||||
            <div class="pf-c-card__body">${super.render()}</div>
 | 
			
		||||
        </div>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,71 +1,55 @@
 | 
			
		||||
import { EventChart } from "#elements/charts/EventChart";
 | 
			
		||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
 | 
			
		||||
import { AKChart } from "@goauthentik/elements/charts/Chart";
 | 
			
		||||
import { ChartData, Tick } from "chart.js";
 | 
			
		||||
import { ChartData } from "chart.js";
 | 
			
		||||
 | 
			
		||||
import { msg, str } from "@lit/localize";
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
import { customElement, property } from "lit/decorators.js";
 | 
			
		||||
 | 
			
		||||
import { CoreApi, UserMetrics } from "@goauthentik/api";
 | 
			
		||||
import { EventActions, EventVolume, EventsApi } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
@customElement("ak-charts-user")
 | 
			
		||||
export class UserChart extends AKChart<UserMetrics> {
 | 
			
		||||
    @property({ type: Number })
 | 
			
		||||
    userId?: number;
 | 
			
		||||
export class UserChart extends EventChart {
 | 
			
		||||
    @property()
 | 
			
		||||
    username?: string;
 | 
			
		||||
 | 
			
		||||
    async apiRequest(): Promise<UserMetrics> {
 | 
			
		||||
        return new CoreApi(DEFAULT_CONFIG).coreUsersMetricsRetrieve({
 | 
			
		||||
            id: this.userId || 0,
 | 
			
		||||
    async apiRequest(): Promise<EventVolume[]> {
 | 
			
		||||
        return new EventsApi(DEFAULT_CONFIG).eventsEventsVolumeList({
 | 
			
		||||
            actions: [
 | 
			
		||||
                EventActions.Login,
 | 
			
		||||
                EventActions.LoginFailed,
 | 
			
		||||
                EventActions.AuthorizeApplication,
 | 
			
		||||
            ],
 | 
			
		||||
            username: this.username,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    timeTickCallback(tickValue: string | number, index: number, ticks: Tick[]): string {
 | 
			
		||||
        const valueStamp = ticks[index];
 | 
			
		||||
        const delta = Date.now() - valueStamp.value;
 | 
			
		||||
        const ago = Math.round(delta / 1000 / 3600 / 24);
 | 
			
		||||
        return msg(str`${ago} days ago`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getChartData(data: UserMetrics): ChartData {
 | 
			
		||||
        return {
 | 
			
		||||
            datasets: [
 | 
			
		||||
                {
 | 
			
		||||
                    label: msg("Failed Logins"),
 | 
			
		||||
                    backgroundColor: "rgba(201, 25, 11, .5)",
 | 
			
		||||
                    spanGaps: true,
 | 
			
		||||
                    data:
 | 
			
		||||
                        data.loginsFailed?.map((cord) => {
 | 
			
		||||
                            return {
 | 
			
		||||
                                x: cord.xCord || 0,
 | 
			
		||||
                                y: cord.yCord || 0,
 | 
			
		||||
                            };
 | 
			
		||||
                        }) || [],
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    label: msg("Successful Logins"),
 | 
			
		||||
                    backgroundColor: "rgba(189, 229, 184, .5)",
 | 
			
		||||
                    spanGaps: true,
 | 
			
		||||
                    data:
 | 
			
		||||
                        data.logins?.map((cord) => {
 | 
			
		||||
                            return {
 | 
			
		||||
                                x: cord.xCord || 0,
 | 
			
		||||
                                y: cord.yCord || 0,
 | 
			
		||||
                            };
 | 
			
		||||
                        }) || [],
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    label: msg("Application authorizations"),
 | 
			
		||||
                    backgroundColor: "rgba(43, 154, 243, .5)",
 | 
			
		||||
                    spanGaps: true,
 | 
			
		||||
                    data:
 | 
			
		||||
                        data.authorizations?.map((cord) => {
 | 
			
		||||
                            return {
 | 
			
		||||
                                x: cord.xCord || 0,
 | 
			
		||||
                                y: cord.yCord || 0,
 | 
			
		||||
                            };
 | 
			
		||||
                        }) || [],
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
        };
 | 
			
		||||
    getChartData(data: EventVolume[]): ChartData {
 | 
			
		||||
        return this.eventVolume(data, {
 | 
			
		||||
            optsMap: new Map([
 | 
			
		||||
                [
 | 
			
		||||
                    EventActions.LoginFailed,
 | 
			
		||||
                    {
 | 
			
		||||
                        label: msg("Failed Logins"),
 | 
			
		||||
                        spanGaps: true,
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
                [
 | 
			
		||||
                    EventActions.Login,
 | 
			
		||||
                    {
 | 
			
		||||
                        label: msg("Successful Logins"),
 | 
			
		||||
                        spanGaps: true,
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
                [
 | 
			
		||||
                    EventActions.AuthorizeApplication,
 | 
			
		||||
                    {
 | 
			
		||||
                        label: msg("Application authorizations"),
 | 
			
		||||
                        spanGaps: true,
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            ]),
 | 
			
		||||
            padToDays: 7,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -389,7 +389,7 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
 | 
			
		||||
                            ${msg("Actions over the last week (per 8 hours)")}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="pf-c-card__body">
 | 
			
		||||
                            <ak-charts-user userId=${this.user.pk || 0}> </ak-charts-user>
 | 
			
		||||
                            <ak-charts-user username=${this.user.username}> </ak-charts-user>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,7 @@ import {
 | 
			
		||||
import { Legend, Tooltip } from "chart.js";
 | 
			
		||||
import { BarController, DoughnutController, LineController } from "chart.js";
 | 
			
		||||
import { ArcElement, BarElement } from "chart.js";
 | 
			
		||||
import { LinearScale, TimeScale } from "chart.js";
 | 
			
		||||
import { LinearScale, TimeScale, TimeSeriesScale } from "chart.js";
 | 
			
		||||
import "chartjs-adapter-date-fns";
 | 
			
		||||
 | 
			
		||||
import { msg } from "@lit/localize";
 | 
			
		||||
@ -33,37 +33,11 @@ import { UiThemeEnum } from "@goauthentik/api";
 | 
			
		||||
Chart.register(Legend, Tooltip);
 | 
			
		||||
Chart.register(LineController, BarController, DoughnutController);
 | 
			
		||||
Chart.register(ArcElement, BarElement, PointElement, LineElement);
 | 
			
		||||
Chart.register(TimeScale, LinearScale, Filler);
 | 
			
		||||
Chart.register(TimeScale, TimeSeriesScale, LinearScale, Filler);
 | 
			
		||||
 | 
			
		||||
export const FONT_COLOUR_DARK_MODE = "#fafafa";
 | 
			
		||||
export const FONT_COLOUR_LIGHT_MODE = "#151515";
 | 
			
		||||
 | 
			
		||||
export class RGBAColor {
 | 
			
		||||
    constructor(
 | 
			
		||||
        public r: number,
 | 
			
		||||
        public g: number,
 | 
			
		||||
        public b: number,
 | 
			
		||||
        public a: number = 1,
 | 
			
		||||
    ) {}
 | 
			
		||||
    toString(): string {
 | 
			
		||||
        return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getColorFromString(stringInput: string): RGBAColor {
 | 
			
		||||
    let hash = 0;
 | 
			
		||||
    for (let i = 0; i < stringInput.length; i++) {
 | 
			
		||||
        hash = stringInput.charCodeAt(i) + ((hash << 5) - hash);
 | 
			
		||||
        hash = hash & hash;
 | 
			
		||||
    }
 | 
			
		||||
    const rgb = [0, 0, 0];
 | 
			
		||||
    for (let i = 0; i < 3; i++) {
 | 
			
		||||
        const value = (hash >> (i * 8)) & 255;
 | 
			
		||||
        rgb[i] = value;
 | 
			
		||||
    }
 | 
			
		||||
    return new RGBAColor(rgb[0], rgb[1], rgb[2]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export abstract class AKChart<T> extends AKElement {
 | 
			
		||||
    abstract apiRequest(): Promise<T>;
 | 
			
		||||
    abstract getChartData(data: T): ChartData;
 | 
			
		||||
@ -184,7 +158,7 @@ export abstract class AKChart<T> extends AKElement {
 | 
			
		||||
            responsive: true,
 | 
			
		||||
            scales: {
 | 
			
		||||
                x: {
 | 
			
		||||
                    type: "time",
 | 
			
		||||
                    type: "timeseries",
 | 
			
		||||
                    display: true,
 | 
			
		||||
                    ticks: {
 | 
			
		||||
                        callback: (tickValue: string | number, index: number, ticks: Tick[]) => {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										120
									
								
								web/src/elements/charts/EventChart.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								web/src/elements/charts/EventChart.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,120 @@
 | 
			
		||||
import { actionToLabel } from "#common/labels";
 | 
			
		||||
import { AKChart } from "#elements/charts/Chart";
 | 
			
		||||
import { ChartData, ChartDataset } from "chart.js";
 | 
			
		||||
 | 
			
		||||
import { EventActions, EventVolume } from "@goauthentik/api";
 | 
			
		||||
 | 
			
		||||
export function actionToColor(action: EventActions): string {
 | 
			
		||||
    switch (action) {
 | 
			
		||||
        case EventActions.AuthorizeApplication:
 | 
			
		||||
            return "#0060c0";
 | 
			
		||||
        case EventActions.ConfigurationError:
 | 
			
		||||
            return "#23511e";
 | 
			
		||||
        case EventActions.EmailSent:
 | 
			
		||||
            return "#009596";
 | 
			
		||||
        case EventActions.FlowExecution:
 | 
			
		||||
            return "#f4c145";
 | 
			
		||||
        case EventActions.ImpersonationEnded:
 | 
			
		||||
            return "#a2d9d9";
 | 
			
		||||
        case EventActions.ImpersonationStarted:
 | 
			
		||||
            return "#a2d9d9";
 | 
			
		||||
        case EventActions.InvitationUsed:
 | 
			
		||||
            return "#8bc1f7";
 | 
			
		||||
        case EventActions.Login:
 | 
			
		||||
            return "#4cb140";
 | 
			
		||||
        case EventActions.LoginFailed:
 | 
			
		||||
            return "#ec7a08";
 | 
			
		||||
        case EventActions.Logout:
 | 
			
		||||
            return "#f9e0a2";
 | 
			
		||||
        case EventActions.ModelCreated:
 | 
			
		||||
            return "#8f4700";
 | 
			
		||||
        case EventActions.ModelDeleted:
 | 
			
		||||
            return "#002f5d";
 | 
			
		||||
        case EventActions.ModelUpdated:
 | 
			
		||||
            return "#bde2b9";
 | 
			
		||||
        case EventActions.PasswordSet:
 | 
			
		||||
            return "#003737";
 | 
			
		||||
        case EventActions.PolicyException:
 | 
			
		||||
            return "#c58c00";
 | 
			
		||||
        case EventActions.PolicyExecution:
 | 
			
		||||
            return "#f4b678";
 | 
			
		||||
        case EventActions.PropertyMappingException:
 | 
			
		||||
            return "#519de9";
 | 
			
		||||
        case EventActions.SecretRotate:
 | 
			
		||||
            return "#38812f";
 | 
			
		||||
        case EventActions.SecretView:
 | 
			
		||||
            return "#73c5c5";
 | 
			
		||||
        case EventActions.SourceLinked:
 | 
			
		||||
            return "#f6d173";
 | 
			
		||||
        case EventActions.SuspiciousRequest:
 | 
			
		||||
            return "#c46100";
 | 
			
		||||
        case EventActions.SystemException:
 | 
			
		||||
            return "#004b95";
 | 
			
		||||
        case EventActions.SystemTaskException:
 | 
			
		||||
            return "#7cc674";
 | 
			
		||||
        case EventActions.SystemTaskExecution:
 | 
			
		||||
            return "#005f60";
 | 
			
		||||
        case EventActions.UpdateAvailable:
 | 
			
		||||
            return "#f0ab00";
 | 
			
		||||
        case EventActions.UserWrite:
 | 
			
		||||
            return "#ef9234";
 | 
			
		||||
    }
 | 
			
		||||
    return "";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export abstract class EventChart extends AKChart<EventVolume[]> {
 | 
			
		||||
    eventVolume(
 | 
			
		||||
        data: EventVolume[],
 | 
			
		||||
        options?: {
 | 
			
		||||
            optsMap?: Map<EventActions, Partial<ChartDataset>>;
 | 
			
		||||
            padToDays?: number;
 | 
			
		||||
        },
 | 
			
		||||
    ): ChartData {
 | 
			
		||||
        const datasets: ChartData = {
 | 
			
		||||
            datasets: [],
 | 
			
		||||
        };
 | 
			
		||||
        if (!options) {
 | 
			
		||||
            options = {};
 | 
			
		||||
        }
 | 
			
		||||
        if (!options.optsMap) {
 | 
			
		||||
            options.optsMap = new Map<EventActions, Partial<ChartDataset>>();
 | 
			
		||||
        }
 | 
			
		||||
        const actions = new Set(data.map((v) => v.action));
 | 
			
		||||
        actions.forEach((action) => {
 | 
			
		||||
            const actionData: { x: number; y: number }[] = [];
 | 
			
		||||
            data.filter((v) => v.action === action).forEach((v) => {
 | 
			
		||||
                actionData.push({
 | 
			
		||||
                    x: v.time.getTime(),
 | 
			
		||||
                    y: v.count,
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
            // Check if we need to pad the data to reach a certain time window
 | 
			
		||||
            const earliestDate = data
 | 
			
		||||
                .filter((v) => v.action === action)
 | 
			
		||||
                .map((v) => v.time)
 | 
			
		||||
                .sort((a, b) => b.getTime() - a.getTime())
 | 
			
		||||
                .reverse();
 | 
			
		||||
            if (earliestDate.length > 0 && options.padToDays) {
 | 
			
		||||
                const earliestPadded = new Date(
 | 
			
		||||
                    new Date().getTime() - options.padToDays * (1000 * 3600 * 24),
 | 
			
		||||
                );
 | 
			
		||||
                const daysDelta = Math.round(
 | 
			
		||||
                    (earliestDate[0].getTime() - earliestPadded.getTime()) / (1000 * 3600 * 24),
 | 
			
		||||
                );
 | 
			
		||||
                if (daysDelta > 0) {
 | 
			
		||||
                    actionData.push({
 | 
			
		||||
                        x: earliestPadded.getTime(),
 | 
			
		||||
                        y: 0,
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            datasets.datasets.push({
 | 
			
		||||
                data: actionData,
 | 
			
		||||
                label: actionToLabel(action),
 | 
			
		||||
                backgroundColor: actionToColor(action),
 | 
			
		||||
                ...options.optsMap?.get(action),
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
        return datasets;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user