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
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.functions import ExtractDay, ExtractHour
from django.db.models.functions import ExtractDay, TruncDate
from django.db.models.query_utils import Q
from django.utils.timezone import now
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 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.response import Response
from rest_framework.viewsets import ModelViewSet
@ -156,13 +157,29 @@ class EventViewSet(ModelViewSet):
return Response(EventTopPerUserSerializer(instance=events, many=True).data)
@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)
def volume(self, request: Request) -> Response:
"""Get event volume for specified filters and timeframe"""
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(
responses={200: CoordinateSerializer(many=True)},

View File

@ -7540,7 +7540,7 @@ paths:
schema:
type: array
items:
$ref: '#/components/schemas/Coordinate'
$ref: '#/components/schemas/EventVolume'
description: ''
'400':
content:
@ -44986,6 +44986,20 @@ components:
- application
- counted_events
- unique_users
EventVolume:
type: object
properties:
action:
type: string
day:
type: string
format: date
count:
type: integer
required:
- action
- count
- day
EventsRequestedEnum:
enum:
- 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 { AKChart, RGBAColor } from "@goauthentik/elements/charts/Chart";
import { AKChart } from "@goauthentik/elements/charts/Chart";
import { ChartData } from "chart.js";
import { msg } from "@lit/localize";
@ -18,8 +18,8 @@ export class AdminLoginAuthorizeChart extends AKChart<LoginMetrics> {
datasets: [
{
label: msg("Authorizations"),
backgroundColor: new RGBAColor(43, 154, 243, 0.5).toString(),
borderColor: new RGBAColor(43, 154, 243, 1).toString(),
backgroundColor: "rgba(43, 154, 243, 0.5)",
borderColor: "rgba(43, 154, 243, 1)",
spanGaps: true,
fill: "origin",
cubicInterpolationMode: "monotone",
@ -33,8 +33,8 @@ export class AdminLoginAuthorizeChart extends AKChart<LoginMetrics> {
},
{
label: msg("Failed Logins"),
backgroundColor: new RGBAColor(201, 24, 11, 0.5).toString(),
borderColor: new RGBAColor(201, 24, 11, 1).toString(),
backgroundColor: "rgba(201, 24, 11, 0.5)",
borderColor: "rgba(201, 24, 11, 1)",
spanGaps: true,
fill: "origin",
cubicInterpolationMode: "monotone",
@ -48,8 +48,8 @@ export class AdminLoginAuthorizeChart extends AKChart<LoginMetrics> {
},
{
label: msg("Successful Logins"),
backgroundColor: new RGBAColor(62, 134, 53, 0.5).toString(),
borderColor: new RGBAColor(62, 134, 53, 1).toString(),
backgroundColor: "rgba(62, 134, 53, 0.5)",
borderColor: "rgba(62, 134, 53, 1)",
spanGaps: true,
fill: "origin",
cubicInterpolationMode: "monotone",

View File

@ -1,17 +1,17 @@
import { actionToLabel } from "#common/labels";
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 { 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 { EventActions, EventVolume, EventsApi, EventsEventsListRequest } from "@goauthentik/api";
@customElement("ak-events-volume-chart")
export class EventVolumeChart extends AKChart<Coordinate[]> {
export class EventVolumeChart extends AKChart<EventVolume[]> {
_query?: EventsEventsListRequest;
@property({ attribute: false })
@ -24,39 +24,43 @@ 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[]> {
apiRequest(): Promise<EventVolume[]> {
return new EventsApi(DEFAULT_CONFIG).eventsEventsVolumeList(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 {
const datasets: ChartData = {
datasets: [],
};
// Get a list of all actions
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,
data: actionData,
});
});
return datasets;
}
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>`;
}