rework event volume

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens Langhammer
2025-06-06 02:54:02 +02:00
parent ef5d3580e8
commit 533b51cb8c
4 changed files with 73 additions and 38 deletions

View File

@ -4,15 +4,16 @@ from datetime import timedelta
from json import loads from json import loads
import django_filters import django_filters
from django.db.models.aggregates import Count from django.db.models import Count
from django.db.models.fields.json import KeyTextTransform, KeyTransform from django.db.models.fields.json import KeyTextTransform, KeyTransform
from django.db.models.functions import ExtractDay, ExtractHour from django.db.models.functions import ExtractDay, TruncDate
from django.db.models.query_utils import Q from django.db.models.query_utils import Q
from django.utils.timezone import now
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema from drf_spectacular.utils import OpenApiParameter, extend_schema, inline_serializer
from guardian.shortcuts import get_objects_for_user from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import DictField, IntegerField from rest_framework.fields import CharField, DateField, DictField, IntegerField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
@ -156,13 +157,29 @@ class EventViewSet(ModelViewSet):
return Response(EventTopPerUserSerializer(instance=events, many=True).data) return Response(EventTopPerUserSerializer(instance=events, many=True).data)
@extend_schema( @extend_schema(
responses={200: CoordinateSerializer(many=True)}, responses={
200: inline_serializer(
"EventVolume",
fields={
"action": CharField(),
"day": DateField(),
"count": IntegerField(),
},
many=True,
)
},
) )
@action(detail=False, methods=["GET"], pagination_class=None) @action(detail=False, methods=["GET"], pagination_class=None)
def volume(self, request: Request) -> Response: def volume(self, request: Request) -> Response:
"""Get event volume for specified filters and timeframe""" """Get event volume for specified filters and timeframe"""
queryset = self.filter_queryset(self.get_queryset()) queryset = self.filter_queryset(self.get_queryset())
return Response(queryset.get_events_per(timedelta(days=7), ExtractHour, 7 * 3)) return Response(
queryset.filter(created__gte=now() - timedelta(days=14))
.annotate(day=TruncDate("created"))
.values("day", "action")
.annotate(count=Count("pk"))
.order_by("-day", "action")
)
@extend_schema( @extend_schema(
responses={200: CoordinateSerializer(many=True)}, responses={200: CoordinateSerializer(many=True)},

View File

@ -7540,7 +7540,7 @@ paths:
schema: schema:
type: array type: array
items: items:
$ref: '#/components/schemas/Coordinate' $ref: '#/components/schemas/EventVolume'
description: '' description: ''
'400': '400':
content: content:
@ -44986,6 +44986,20 @@ components:
- application - application
- counted_events - counted_events
- unique_users - unique_users
EventVolume:
type: object
properties:
action:
type: string
day:
type: string
format: date
count:
type: integer
required:
- action
- count
- day
EventsRequestedEnum: EventsRequestedEnum:
enum: enum:
- https://schemas.openid.net/secevent/caep/event-type/session-revoked - https://schemas.openid.net/secevent/caep/event-type/session-revoked

View File

@ -1,5 +1,5 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart, RGBAColor } from "@goauthentik/elements/charts/Chart"; import { AKChart } from "@goauthentik/elements/charts/Chart";
import { ChartData } from "chart.js"; import { ChartData } from "chart.js";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
@ -18,8 +18,8 @@ export class AdminLoginAuthorizeChart extends AKChart<LoginMetrics> {
datasets: [ datasets: [
{ {
label: msg("Authorizations"), label: msg("Authorizations"),
backgroundColor: new RGBAColor(43, 154, 243, 0.5).toString(), backgroundColor: "rgba(43, 154, 243, 0.5)",
borderColor: new RGBAColor(43, 154, 243, 1).toString(), borderColor: "rgba(43, 154, 243, 1)",
spanGaps: true, spanGaps: true,
fill: "origin", fill: "origin",
cubicInterpolationMode: "monotone", cubicInterpolationMode: "monotone",
@ -33,8 +33,8 @@ export class AdminLoginAuthorizeChart extends AKChart<LoginMetrics> {
}, },
{ {
label: msg("Failed Logins"), label: msg("Failed Logins"),
backgroundColor: new RGBAColor(201, 24, 11, 0.5).toString(), backgroundColor: "rgba(201, 24, 11, 0.5)",
borderColor: new RGBAColor(201, 24, 11, 1).toString(), borderColor: "rgba(201, 24, 11, 1)",
spanGaps: true, spanGaps: true,
fill: "origin", fill: "origin",
cubicInterpolationMode: "monotone", cubicInterpolationMode: "monotone",
@ -48,8 +48,8 @@ export class AdminLoginAuthorizeChart extends AKChart<LoginMetrics> {
}, },
{ {
label: msg("Successful Logins"), label: msg("Successful Logins"),
backgroundColor: new RGBAColor(62, 134, 53, 0.5).toString(), backgroundColor: "rgba(62, 134, 53, 0.5)",
borderColor: new RGBAColor(62, 134, 53, 1).toString(), borderColor: "rgba(62, 134, 53, 1)",
spanGaps: true, spanGaps: true,
fill: "origin", fill: "origin",
cubicInterpolationMode: "monotone", cubicInterpolationMode: "monotone",

View File

@ -1,17 +1,17 @@
import { actionToLabel } from "#common/labels";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart } from "@goauthentik/elements/charts/Chart"; import { AKChart, getColorFromString } from "@goauthentik/elements/charts/Chart";
import { ChartData } from "chart.js"; import { ChartData } from "chart.js";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import PFCard from "@patternfly/patternfly/components/Card/card.css"; import PFCard from "@patternfly/patternfly/components/Card/card.css";
import { Coordinate, EventsApi, EventsEventsListRequest } from "@goauthentik/api"; import { EventActions, EventVolume, EventsApi, EventsEventsListRequest } from "@goauthentik/api";
@customElement("ak-events-volume-chart") @customElement("ak-events-volume-chart")
export class EventVolumeChart extends AKChart<Coordinate[]> { export class EventVolumeChart extends AKChart<EventVolume[]> {
_query?: EventsEventsListRequest; _query?: EventsEventsListRequest;
@property({ attribute: false }) @property({ attribute: false })
@ -24,39 +24,43 @@ export class EventVolumeChart extends AKChart<Coordinate[]> {
return super.styles.concat( return super.styles.concat(
PFCard, PFCard,
css` css`
.pf-c-card__body { .pf-c-card {
height: 12rem; height: 20rem;
} }
`, `,
); );
} }
apiRequest(): Promise<Coordinate[]> { apiRequest(): Promise<EventVolume[]> {
return new EventsApi(DEFAULT_CONFIG).eventsEventsVolumeList(this._query); return new EventsApi(DEFAULT_CONFIG).eventsEventsVolumeList(this._query);
} }
getChartData(data: Coordinate[]): ChartData { getChartData(data: EventVolume[]): ChartData {
return { const datasets: ChartData = {
datasets: [ datasets: [],
{ };
label: msg("Events"), // Get a list of all actions
backgroundColor: "rgba(189, 229, 184, .5)", 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.day.getTime(),
y: v.count,
});
});
datasets.datasets.push({
label: actionToLabel(action as EventActions),
backgroundColor: getColorFromString(action),
spanGaps: true, spanGaps: true,
data: data: actionData,
data.map((cord) => { });
return { });
x: cord.xCord || 0, return datasets;
y: cord.yCord || 0,
};
}) || [],
},
],
};
} }
render(): TemplateResult { render(): TemplateResult {
return html`<div class="pf-c-card"> 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 class="pf-c-card__body">${super.render()}</div>
</div>`; </div>`;
} }