From d1c997b2fea7d80f7d945b5847e2f31aaa2a150b Mon Sep 17 00:00:00 2001 From: "Jens L." Date: Sun, 29 Mar 2026 20:58:12 +0100 Subject: [PATCH] core: Application stats, device events & cleanup (#21225) * core: app stats Signed-off-by: Jens Langhammer * refctor Signed-off-by: Jens Langhammer * rework to generic API Signed-off-by: Jens Langhammer * oops Signed-off-by: Jens Langhammer * handling Signed-off-by: Jens Langhammer * fix docs Signed-off-by: Jens Langhammer * more docs Signed-off-by: Jens Langhammer * unrelated fix Signed-off-by: Jens Langhammer * allow filtering events by device Signed-off-by: Jens Langhammer * show device events on device page Signed-off-by: Jens Langhammer * simply event tables Signed-off-by: Jens Langhammer * tests Signed-off-by: Jens Langhammer --------- Signed-off-by: Jens Langhammer --- authentik/events/api/events.py | 92 +++++- authentik/events/tests/test_api.py | 25 ++ packages/client-go/api_events.go | 303 +++++++++++++++++- packages/client-go/model_event_stats.go | 196 +++++++++++ packages/client-go/model_event_volume.go | 2 +- packages/client-rust/src/apis/events_api.rs | 176 +++++++++- .../client-rust/src/models/event_stats.rs | 33 ++ .../client-rust/src/models/event_volume.rs | 4 +- packages/client-rust/src/models/mod.rs | 2 + packages/client-ts/src/apis/EventsApi.ts | 138 ++++++++ packages/client-ts/src/models/EventStats.ts | 75 +++++ packages/client-ts/src/models/EventVolume.ts | 2 +- packages/client-ts/src/models/index.ts | 1 + schema.yml | 114 ++++++- .../admin-overview/cards/RecentEventsCard.ts | 56 +--- .../admin/applications/ApplicationEvents.ts | 60 +--- .../admin/applications/ApplicationViewPage.ts | 254 ++++++++------- .../admin/endpoints/devices/DeviceEvents.ts | 25 ++ .../admin/endpoints/devices/DeviceViewPage.ts | 103 +++--- web/src/admin/events/SimpleEventTable.ts | 67 ++++ web/src/admin/events/UserEvents.ts | 60 +--- web/src/admin/outposts/OutpostProviderList.ts | 9 +- 22 files changed, 1446 insertions(+), 351 deletions(-) create mode 100644 packages/client-go/model_event_stats.go create mode 100644 packages/client-rust/src/models/event_stats.rs create mode 100644 packages/client-ts/src/models/EventStats.ts create mode 100644 web/src/admin/endpoints/devices/DeviceEvents.ts create mode 100644 web/src/admin/events/SimpleEventTable.ts diff --git a/authentik/events/api/events.py b/authentik/events/api/events.py index 104d3a1192..aa9d625769 100644 --- a/authentik/events/api/events.py +++ b/authentik/events/api/events.py @@ -9,30 +9,49 @@ from django.db.models import DateTimeField as DjangoDateTimeField from django.db.models.fields.json import KeyTextTransform, KeyTransform from django.db.models.functions import TruncHour from django.db.models.query_utils import Q +from django.utils.text import slugify from django.utils.timezone import now from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter, extend_schema from guardian.shortcuts import get_objects_for_user from rest_framework.decorators import action -from rest_framework.fields import ChoiceField, DateTimeField, DictField, IntegerField +from rest_framework.fields import ( + CharField, + ChoiceField, + DateTimeField, + DictField, + IntegerField, + ListField, +) from rest_framework.request import Request from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet +from authentik.api.validation import validate from authentik.core.api.object_types import TypeCreateSerializer from authentik.core.api.utils import ModelSerializer, PassiveSerializer from authentik.events.models import Event, EventAction from authentik.lib.utils.reflection import ConditionalInheritance +from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator + +AGGR_MAX_AGE = timedelta(days=90) class EventVolumeSerializer(PassiveSerializer): - """Count of events of action created on day""" + """Count of events of action created on day for a single event action""" action = ChoiceField(choices=EventAction.choices) time = DateTimeField() count = IntegerField() +class EventStatsSerializer(PassiveSerializer): + """Count of unique users in events and aggregated counts per specified deltas""" + + unique_users = IntegerField() + count_step = DictField() + + class EventSerializer(ModelSerializer): """Event Serializer""" @@ -84,6 +103,11 @@ class EventsFilter(django_filters.FilterSet): lookup_expr="authorized_application__pk", label="Context Authorized application", ) + context_device = django_filters.CharFilter( + field_name="context", + lookup_expr="device__pk", + label="Context Device Primary Key", + ) action = django_filters.CharFilter( field_name="action", lookup_expr="icontains", @@ -123,6 +147,16 @@ class EventViewSet( ): """Event Read-Only Viewset""" + class EventVolumeParameters(PassiveSerializer): + history_days = IntegerField(default=7, required=False) + + class EventStatsParameters(PassiveSerializer): + count_steps = ListField( + child=CharField(validators=[timedelta_string_validator]), + required=True, + help_text="Timedelta, format of 'weeks=3;days=2;hours=3,seconds=2'", + ) + queryset = Event.objects.all() serializer_class = EventSerializer ordering = ["-created"] @@ -225,24 +259,16 @@ class EventViewSet( @extend_schema( responses={200: EventVolumeSerializer(many=True)}, - parameters=[ - OpenApiParameter( - "history_days", - type=OpenApiTypes.NUMBER, - location=OpenApiParameter.QUERY, - required=False, - default=7, - ), - ], + parameters=[EventVolumeParameters], ) @action(detail=False, methods=["GET"], pagination_class=None) - def volume(self, request: Request) -> Response: + @validate(EventVolumeParameters, "query") + def volume(self, request: Request, query: EventVolumeParameters) -> Response: """Get event volume for specified filters and timeframe""" queryset: QuerySet[Event] = self.filter_queryset(self.get_queryset()) - delta = timedelta(days=7) - time_delta = request.query_params.get("history_days", 7) - if time_delta: - delta = timedelta(days=min(int(time_delta), 60)) + delta = timedelta(days=query.validated_data.get("history_days", 7)) + if delta.total_seconds() > AGGR_MAX_AGE: + delta = AGGR_MAX_AGE return Response( queryset.filter(created__gte=now() - delta) .annotate(hour=TruncHour("created")) @@ -257,6 +283,40 @@ class EventViewSet( .order_by("time", "action") ) + @extend_schema( + responses={200: EventStatsSerializer()}, + parameters=[EventStatsParameters], + filters=True, + ) + @action(detail=False, methods=["GET"], pagination_class=None) + @validate(EventStatsParameters, "query") + def stats(self, request: Request, query: EventStatsParameters) -> Response: + """Get event stats for specified filters and count steps""" + _now = now() + aggrs = { + "unique_users": Count("user__pk", distinct=True), + } + largest_delta = 0 + for step in query.validated_data.get("count_steps"): + delta = timedelta_from_string(step) + if delta.total_seconds() > AGGR_MAX_AGE.total_seconds(): + delta = AGGR_MAX_AGE + largest_delta = max(largest_delta, delta.total_seconds()) + aggrs[slugify(step).replace("-", "_")] = Count( + "event_uuid", filter=Q(created__gte=_now - delta) + ) + data = ( + self.filter_queryset(self.get_queryset()) + .filter(created__gte=now() - timedelta(days=60)) + .aggregate(**aggrs) + ) + return Response( + { + "unique_users": data.pop("unique_users"), + "count_step": data, + } + ) + @extend_schema(responses={200: TypeCreateSerializer(many=True)}) @action(detail=False, pagination_class=None, filter_backends=[]) def actions(self, request: Request) -> Response: diff --git a/authentik/events/tests/test_api.py b/authentik/events/tests/test_api.py index 51028c8a8f..624c07eb86 100644 --- a/authentik/events/tests/test_api.py +++ b/authentik/events/tests/test_api.py @@ -1,8 +1,10 @@ """Event API tests""" +from datetime import timedelta from json import loads from django.urls import reverse +from django.utils.timezone import now from rest_framework.test import APITestCase from authentik.core.tests.utils import create_test_admin_user @@ -91,3 +93,26 @@ class TestEventsAPI(APITestCase): }, ) self.assertEqual(response.status_code, 400) + + def test_stats(self): + Event.objects.all().delete() + Event.new(EventAction.LOGIN).set_user(self.user).save() + evt = Event.new(EventAction.LOGIN).set_user(self.user) + evt.created = now() - timedelta(days=6) + evt.save() + res = self.client.get( + reverse("authentik_api:event-stats") + + "?count_steps=hours=24&count_steps=days=7&count_steps=days=240" + ) + self.assertEqual(res.status_code, 200) + self.assertJSONEqual( + res.content, {"unique_users": 1, "count_step": {"hours24": 2, "days7": 2, "days240": 2}} + ) + + def test_stats_invalid(self): + res = self.client.get(reverse("authentik_api:event-stats") + "?count_steps=24") + self.assertEqual(res.status_code, 400) + self.assertJSONEqual( + res.content, + {"count_steps": {"0": ["24 is not in the correct format of 'hours=3;minutes=1'."]}}, + ) diff --git a/packages/client-go/api_events.go b/packages/client-go/api_events.go index 963d47c6ba..e02af61c03 100644 --- a/packages/client-go/api_events.go +++ b/packages/client-go/api_events.go @@ -399,6 +399,7 @@ type ApiEventsEventsExportCreateRequest struct { brandName *string clientIp *string contextAuthorizedApp *string + contextDevice *string contextModelApp *string contextModelName *string contextModelPk *string @@ -434,6 +435,12 @@ func (r ApiEventsEventsExportCreateRequest) ContextAuthorizedApp(contextAuthoriz return r } +// Context Device Primary Key +func (r ApiEventsEventsExportCreateRequest) ContextDevice(contextDevice string) ApiEventsEventsExportCreateRequest { + r.contextDevice = &contextDevice + return r +} + // Context Model App func (r ApiEventsEventsExportCreateRequest) ContextModelApp(contextModelApp string) ApiEventsEventsExportCreateRequest { r.contextModelApp = &contextModelApp @@ -538,6 +545,9 @@ func (a *EventsAPIService) EventsEventsExportCreateExecute(r ApiEventsEventsExpo if r.contextAuthorizedApp != nil { parameterAddToHeaderOrQuery(localVarQueryParams, "context_authorized_app", r.contextAuthorizedApp, "form", "") } + if r.contextDevice != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "context_device", r.contextDevice, "form", "") + } if r.contextModelApp != nil { parameterAddToHeaderOrQuery(localVarQueryParams, "context_model_app", r.contextModelApp, "form", "") } @@ -639,6 +649,7 @@ type ApiEventsEventsListRequest struct { brandName *string clientIp *string contextAuthorizedApp *string + contextDevice *string contextModelApp *string contextModelName *string contextModelPk *string @@ -676,6 +687,12 @@ func (r ApiEventsEventsListRequest) ContextAuthorizedApp(contextAuthorizedApp st return r } +// Context Device Primary Key +func (r ApiEventsEventsListRequest) ContextDevice(contextDevice string) ApiEventsEventsListRequest { + r.contextDevice = &contextDevice + return r +} + // Context Model App func (r ApiEventsEventsListRequest) ContextModelApp(contextModelApp string) ApiEventsEventsListRequest { r.contextModelApp = &contextModelApp @@ -788,6 +805,9 @@ func (a *EventsAPIService) EventsEventsListExecute(r ApiEventsEventsListRequest) if r.contextAuthorizedApp != nil { parameterAddToHeaderOrQuery(localVarQueryParams, "context_authorized_app", r.contextAuthorizedApp, "form", "") } + if r.contextDevice != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "context_device", r.contextDevice, "form", "") + } if r.contextModelApp != nil { parameterAddToHeaderOrQuery(localVarQueryParams, "context_model_app", r.contextModelApp, "form", "") } @@ -1145,6 +1165,273 @@ func (a *EventsAPIService) EventsEventsRetrieveExecute(r ApiEventsEventsRetrieve return localVarReturnValue, localVarHTTPResponse, nil } +type ApiEventsEventsStatsRetrieveRequest struct { + ctx context.Context + ApiService *EventsAPIService + countSteps *[]string + action *string + actions *[]EventActions + brandName *string + clientIp *string + contextAuthorizedApp *string + contextDevice *string + contextModelApp *string + contextModelName *string + contextModelPk *string + ordering *string + search *string + username *string +} + +// Timedelta, format of 'weeks=3;days=2;hours=3,seconds=2' +func (r ApiEventsEventsStatsRetrieveRequest) CountSteps(countSteps []string) ApiEventsEventsStatsRetrieveRequest { + r.countSteps = &countSteps + return r +} + +func (r ApiEventsEventsStatsRetrieveRequest) Action(action string) ApiEventsEventsStatsRetrieveRequest { + r.action = &action + return r +} + +func (r ApiEventsEventsStatsRetrieveRequest) Actions(actions []EventActions) ApiEventsEventsStatsRetrieveRequest { + r.actions = &actions + return r +} + +// Brand name +func (r ApiEventsEventsStatsRetrieveRequest) BrandName(brandName string) ApiEventsEventsStatsRetrieveRequest { + r.brandName = &brandName + return r +} + +func (r ApiEventsEventsStatsRetrieveRequest) ClientIp(clientIp string) ApiEventsEventsStatsRetrieveRequest { + r.clientIp = &clientIp + return r +} + +// Context Authorized application +func (r ApiEventsEventsStatsRetrieveRequest) ContextAuthorizedApp(contextAuthorizedApp string) ApiEventsEventsStatsRetrieveRequest { + r.contextAuthorizedApp = &contextAuthorizedApp + return r +} + +// Context Device Primary Key +func (r ApiEventsEventsStatsRetrieveRequest) ContextDevice(contextDevice string) ApiEventsEventsStatsRetrieveRequest { + r.contextDevice = &contextDevice + return r +} + +// Context Model App +func (r ApiEventsEventsStatsRetrieveRequest) ContextModelApp(contextModelApp string) ApiEventsEventsStatsRetrieveRequest { + r.contextModelApp = &contextModelApp + return r +} + +// Context Model Name +func (r ApiEventsEventsStatsRetrieveRequest) ContextModelName(contextModelName string) ApiEventsEventsStatsRetrieveRequest { + r.contextModelName = &contextModelName + return r +} + +// Context Model Primary Key +func (r ApiEventsEventsStatsRetrieveRequest) ContextModelPk(contextModelPk string) ApiEventsEventsStatsRetrieveRequest { + r.contextModelPk = &contextModelPk + return r +} + +// Which field to use when ordering the results. +func (r ApiEventsEventsStatsRetrieveRequest) Ordering(ordering string) ApiEventsEventsStatsRetrieveRequest { + r.ordering = &ordering + return r +} + +// A search term. +func (r ApiEventsEventsStatsRetrieveRequest) Search(search string) ApiEventsEventsStatsRetrieveRequest { + r.search = &search + return r +} + +// Username +func (r ApiEventsEventsStatsRetrieveRequest) Username(username string) ApiEventsEventsStatsRetrieveRequest { + r.username = &username + return r +} + +func (r ApiEventsEventsStatsRetrieveRequest) Execute() (*EventStats, *http.Response, error) { + return r.ApiService.EventsEventsStatsRetrieveExecute(r) +} + +/* +EventsEventsStatsRetrieve Method for EventsEventsStatsRetrieve + +Get event stats for specified filters and count steps + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiEventsEventsStatsRetrieveRequest +*/ +func (a *EventsAPIService) EventsEventsStatsRetrieve(ctx context.Context) ApiEventsEventsStatsRetrieveRequest { + return ApiEventsEventsStatsRetrieveRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return EventStats +func (a *EventsAPIService) EventsEventsStatsRetrieveExecute(r ApiEventsEventsStatsRetrieveRequest) (*EventStats, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *EventStats + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "EventsAPIService.EventsEventsStatsRetrieve") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/events/events/stats/" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + if r.countSteps == nil { + return localVarReturnValue, nil, reportError("countSteps is required and must be specified") + } + + if r.action != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "action", r.action, "form", "") + } + if r.actions != nil { + t := *r.actions + if reflect.TypeOf(t).Kind() == reflect.Slice { + s := reflect.ValueOf(t) + for i := 0; i < s.Len(); i++ { + parameterAddToHeaderOrQuery(localVarQueryParams, "actions", s.Index(i).Interface(), "form", "multi") + } + } else { + parameterAddToHeaderOrQuery(localVarQueryParams, "actions", t, "form", "multi") + } + } + if r.brandName != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "brand_name", r.brandName, "form", "") + } + if r.clientIp != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "client_ip", r.clientIp, "form", "") + } + if r.contextAuthorizedApp != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "context_authorized_app", r.contextAuthorizedApp, "form", "") + } + if r.contextDevice != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "context_device", r.contextDevice, "form", "") + } + if r.contextModelApp != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "context_model_app", r.contextModelApp, "form", "") + } + if r.contextModelName != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "context_model_name", r.contextModelName, "form", "") + } + if r.contextModelPk != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "context_model_pk", r.contextModelPk, "form", "") + } + { + t := *r.countSteps + if reflect.TypeOf(t).Kind() == reflect.Slice { + s := reflect.ValueOf(t) + for i := 0; i < s.Len(); i++ { + parameterAddToHeaderOrQuery(localVarQueryParams, "count_steps", s.Index(i).Interface(), "form", "multi") + } + } else { + parameterAddToHeaderOrQuery(localVarQueryParams, "count_steps", t, "form", "multi") + } + } + if r.ordering != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "ordering", r.ordering, "form", "") + } + if r.search != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "search", r.search, "form", "") + } + if r.username != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "username", r.username, "form", "") + } + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v ValidationError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 403 { + var v GenericError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type ApiEventsEventsTopPerUserListRequest struct { ctx context.Context ApiService *EventsAPIService @@ -1428,10 +1715,11 @@ type ApiEventsEventsVolumeListRequest struct { brandName *string clientIp *string contextAuthorizedApp *string + contextDevice *string contextModelApp *string contextModelName *string contextModelPk *string - historyDays *float32 + historyDays *int32 ordering *string search *string username *string @@ -1464,6 +1752,12 @@ func (r ApiEventsEventsVolumeListRequest) ContextAuthorizedApp(contextAuthorized return r } +// Context Device Primary Key +func (r ApiEventsEventsVolumeListRequest) ContextDevice(contextDevice string) ApiEventsEventsVolumeListRequest { + r.contextDevice = &contextDevice + return r +} + // Context Model App func (r ApiEventsEventsVolumeListRequest) ContextModelApp(contextModelApp string) ApiEventsEventsVolumeListRequest { r.contextModelApp = &contextModelApp @@ -1482,7 +1776,7 @@ func (r ApiEventsEventsVolumeListRequest) ContextModelPk(contextModelPk string) return r } -func (r ApiEventsEventsVolumeListRequest) HistoryDays(historyDays float32) ApiEventsEventsVolumeListRequest { +func (r ApiEventsEventsVolumeListRequest) HistoryDays(historyDays int32) ApiEventsEventsVolumeListRequest { r.historyDays = &historyDays return r } @@ -1569,6 +1863,9 @@ func (a *EventsAPIService) EventsEventsVolumeListExecute(r ApiEventsEventsVolume if r.contextAuthorizedApp != nil { parameterAddToHeaderOrQuery(localVarQueryParams, "context_authorized_app", r.contextAuthorizedApp, "form", "") } + if r.contextDevice != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "context_device", r.contextDevice, "form", "") + } if r.contextModelApp != nil { parameterAddToHeaderOrQuery(localVarQueryParams, "context_model_app", r.contextModelApp, "form", "") } @@ -1581,7 +1878,7 @@ func (a *EventsAPIService) EventsEventsVolumeListExecute(r ApiEventsEventsVolume if r.historyDays != nil { parameterAddToHeaderOrQuery(localVarQueryParams, "history_days", r.historyDays, "form", "") } else { - var defaultValue float32 = 7 + var defaultValue int32 = 7 parameterAddToHeaderOrQuery(localVarQueryParams, "history_days", defaultValue, "form", "") r.historyDays = &defaultValue } diff --git a/packages/client-go/model_event_stats.go b/packages/client-go/model_event_stats.go new file mode 100644 index 0000000000..3108748d05 --- /dev/null +++ b/packages/client-go/model_event_stats.go @@ -0,0 +1,196 @@ +/* +authentik + +Making authentication simple. + +API version: 2026.5.0-rc1 +Contact: hello@goauthentik.io +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package api + +import ( + "encoding/json" + "fmt" +) + +// checks if the EventStats type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &EventStats{} + +// EventStats Count of unique users in events and aggregated counts per specified deltas +type EventStats struct { + UniqueUsers int32 `json:"unique_users"` + CountStep map[string]interface{} `json:"count_step"` + AdditionalProperties map[string]interface{} +} + +type _EventStats EventStats + +// NewEventStats instantiates a new EventStats object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewEventStats(uniqueUsers int32, countStep map[string]interface{}) *EventStats { + this := EventStats{} + this.UniqueUsers = uniqueUsers + this.CountStep = countStep + return &this +} + +// NewEventStatsWithDefaults instantiates a new EventStats object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewEventStatsWithDefaults() *EventStats { + this := EventStats{} + return &this +} + +// GetUniqueUsers returns the UniqueUsers field value +func (o *EventStats) GetUniqueUsers() int32 { + if o == nil { + var ret int32 + return ret + } + + return o.UniqueUsers +} + +// GetUniqueUsersOk returns a tuple with the UniqueUsers field value +// and a boolean to check if the value has been set. +func (o *EventStats) GetUniqueUsersOk() (*int32, bool) { + if o == nil { + return nil, false + } + return &o.UniqueUsers, true +} + +// SetUniqueUsers sets field value +func (o *EventStats) SetUniqueUsers(v int32) { + o.UniqueUsers = v +} + +// GetCountStep returns the CountStep field value +func (o *EventStats) GetCountStep() map[string]interface{} { + if o == nil { + var ret map[string]interface{} + return ret + } + + return o.CountStep +} + +// GetCountStepOk returns a tuple with the CountStep field value +// and a boolean to check if the value has been set. +func (o *EventStats) GetCountStepOk() (map[string]interface{}, bool) { + if o == nil { + return map[string]interface{}{}, false + } + return o.CountStep, true +} + +// SetCountStep sets field value +func (o *EventStats) SetCountStep(v map[string]interface{}) { + o.CountStep = v +} + +func (o EventStats) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o EventStats) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["unique_users"] = o.UniqueUsers + toSerialize["count_step"] = o.CountStep + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + return toSerialize, nil +} + +func (o *EventStats) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "unique_users", + "count_step", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varEventStats := _EventStats{} + + err = json.Unmarshal(data, &varEventStats) + + if err != nil { + return err + } + + *o = EventStats(varEventStats) + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(data, &additionalProperties); err == nil { + delete(additionalProperties, "unique_users") + delete(additionalProperties, "count_step") + o.AdditionalProperties = additionalProperties + } + + return err +} + +type NullableEventStats struct { + value *EventStats + isSet bool +} + +func (v NullableEventStats) Get() *EventStats { + return v.value +} + +func (v *NullableEventStats) Set(val *EventStats) { + v.value = val + v.isSet = true +} + +func (v NullableEventStats) IsSet() bool { + return v.isSet +} + +func (v *NullableEventStats) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableEventStats(val *EventStats) *NullableEventStats { + return &NullableEventStats{value: val, isSet: true} +} + +func (v NullableEventStats) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableEventStats) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/packages/client-go/model_event_volume.go b/packages/client-go/model_event_volume.go index 38169ea879..7a96cf8e24 100644 --- a/packages/client-go/model_event_volume.go +++ b/packages/client-go/model_event_volume.go @@ -20,7 +20,7 @@ import ( // checks if the EventVolume type satisfies the MappedNullable interface at compile time var _ MappedNullable = &EventVolume{} -// EventVolume Count of events of action created on day +// EventVolume Count of events of action created on day for a single event action type EventVolume struct { Action EventActions `json:"action"` Time time.Time `json:"time"` diff --git a/packages/client-rust/src/apis/events_api.rs b/packages/client-rust/src/apis/events_api.rs index 09f2324d51..81ed79afe5 100644 --- a/packages/client-rust/src/apis/events_api.rs +++ b/packages/client-rust/src/apis/events_api.rs @@ -75,6 +75,15 @@ pub enum EventsEventsRetrieveError { UnknownValue(serde_json::Value), } +/// struct for typed errors of method [`events_events_stats_retrieve`] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum EventsEventsStatsRetrieveError { + Status400(models::ValidationError), + Status403(models::GenericError), + UnknownValue(serde_json::Value), +} + /// struct for typed errors of method [`events_events_top_per_user_list`] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] @@ -467,6 +476,7 @@ pub async fn events_events_export_create( brand_name: Option<&str>, client_ip: Option<&str>, context_authorized_app: Option<&str>, + context_device: Option<&str>, context_model_app: Option<&str>, context_model_name: Option<&str>, context_model_pk: Option<&str>, @@ -480,6 +490,7 @@ pub async fn events_events_export_create( let p_query_brand_name = brand_name; let p_query_client_ip = client_ip; let p_query_context_authorized_app = context_authorized_app; + let p_query_context_device = context_device; let p_query_context_model_app = context_model_app; let p_query_context_model_name = context_model_name; let p_query_context_model_pk = context_model_pk; @@ -523,6 +534,9 @@ pub async fn events_events_export_create( if let Some(ref param_value) = p_query_context_authorized_app { req_builder = req_builder.query(&[("context_authorized_app", ¶m_value.to_string())]); } + if let Some(ref param_value) = p_query_context_device { + req_builder = req_builder.query(&[("context_device", ¶m_value.to_string())]); + } if let Some(ref param_value) = p_query_context_model_app { req_builder = req_builder.query(&[("context_model_app", ¶m_value.to_string())]); } @@ -595,6 +609,7 @@ pub async fn events_events_list( brand_name: Option<&str>, client_ip: Option<&str>, context_authorized_app: Option<&str>, + context_device: Option<&str>, context_model_app: Option<&str>, context_model_name: Option<&str>, context_model_pk: Option<&str>, @@ -610,6 +625,7 @@ pub async fn events_events_list( let p_query_brand_name = brand_name; let p_query_client_ip = client_ip; let p_query_context_authorized_app = context_authorized_app; + let p_query_context_device = context_device; let p_query_context_model_app = context_model_app; let p_query_context_model_name = context_model_name; let p_query_context_model_pk = context_model_pk; @@ -653,6 +669,9 @@ pub async fn events_events_list( if let Some(ref param_value) = p_query_context_authorized_app { req_builder = req_builder.query(&[("context_authorized_app", ¶m_value.to_string())]); } + if let Some(ref param_value) = p_query_context_device { + req_builder = req_builder.query(&[("context_device", ¶m_value.to_string())]); + } if let Some(ref param_value) = p_query_context_model_app { req_builder = req_builder.query(&[("context_model_app", ¶m_value.to_string())]); } @@ -850,6 +869,156 @@ pub async fn events_events_retrieve( } } +/// Get event stats for specified filters and count steps +pub async fn events_events_stats_retrieve( + configuration: &configuration::Configuration, + count_steps: Vec, + action: Option<&str>, + actions: Option>, + brand_name: Option<&str>, + client_ip: Option<&str>, + context_authorized_app: Option<&str>, + context_device: Option<&str>, + context_model_app: Option<&str>, + context_model_name: Option<&str>, + context_model_pk: Option<&str>, + ordering: Option<&str>, + search: Option<&str>, + username: Option<&str>, +) -> Result> { + // add a prefix to parameters to efficiently prevent name collisions + let p_query_count_steps = count_steps; + let p_query_action = action; + let p_query_actions = actions; + let p_query_brand_name = brand_name; + let p_query_client_ip = client_ip; + let p_query_context_authorized_app = context_authorized_app; + let p_query_context_device = context_device; + let p_query_context_model_app = context_model_app; + let p_query_context_model_name = context_model_name; + let p_query_context_model_pk = context_model_pk; + let p_query_ordering = ordering; + let p_query_search = search; + let p_query_username = username; + + let uri_str = format!("{}/events/events/stats/", configuration.base_path); + let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str); + + if let Some(ref param_value) = p_query_action { + req_builder = req_builder.query(&[("action", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_query_actions { + req_builder = match "multi" { + "multi" => req_builder.query( + ¶m_value + .into_iter() + .map(|p| ("actions".to_owned(), p.to_string())) + .collect::>(), + ), + _ => req_builder.query(&[( + "actions", + ¶m_value + .into_iter() + .map(|p| p.to_string()) + .collect::>() + .join(",") + .to_string(), + )]), + }; + } + if let Some(ref param_value) = p_query_brand_name { + req_builder = req_builder.query(&[("brand_name", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_query_client_ip { + req_builder = req_builder.query(&[("client_ip", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_query_context_authorized_app { + req_builder = req_builder.query(&[("context_authorized_app", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_query_context_device { + req_builder = req_builder.query(&[("context_device", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_query_context_model_app { + req_builder = req_builder.query(&[("context_model_app", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_query_context_model_name { + req_builder = req_builder.query(&[("context_model_name", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_query_context_model_pk { + req_builder = req_builder.query(&[("context_model_pk", ¶m_value.to_string())]); + } + req_builder = match "multi" { + "multi" => req_builder.query( + &p_query_count_steps + .into_iter() + .map(|p| ("count_steps".to_owned(), p.to_string())) + .collect::>(), + ), + _ => req_builder.query(&[( + "count_steps", + &p_query_count_steps + .into_iter() + .map(|p| p.to_string()) + .collect::>() + .join(",") + .to_string(), + )]), + }; + if let Some(ref param_value) = p_query_ordering { + req_builder = req_builder.query(&[("ordering", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_query_search { + req_builder = req_builder.query(&[("search", ¶m_value.to_string())]); + } + if let Some(ref param_value) = p_query_username { + req_builder = req_builder.query(&[("username", ¶m_value.to_string())]); + } + if let Some(ref user_agent) = configuration.user_agent { + req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone()); + } + if let Some(ref token) = configuration.bearer_access_token { + req_builder = req_builder.bearer_auth(token.to_owned()); + }; + + let req = req_builder.build()?; + let resp = configuration.client.execute(req).await?; + + let status = resp.status(); + let content_type = resp + .headers() + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or("application/octet-stream"); + let content_type = super::ContentType::from(content_type); + + if !status.is_client_error() && !status.is_server_error() { + let content = resp.text().await?; + match content_type { + ContentType::Json => serde_json::from_str(&content).map_err(Error::from), + ContentType::Text => { + return Err(Error::from(serde_json::Error::custom( + "Received `text/plain` content type response that cannot be converted to \ + `models::EventStats`", + ))); + } + ContentType::Unsupported(unknown_type) => { + return Err(Error::from(serde_json::Error::custom(format!( + "Received `{unknown_type}` content type response that cannot be converted to \ + `models::EventStats`" + )))); + } + } + } else { + let content = resp.text().await?; + let entity: Option = serde_json::from_str(&content).ok(); + Err(Error::ResponseError(ResponseContent { + status, + content, + entity, + })) + } +} + /// Get the top_n events grouped by user count pub async fn events_events_top_per_user_list( configuration: &configuration::Configuration, @@ -987,10 +1156,11 @@ pub async fn events_events_volume_list( brand_name: Option<&str>, client_ip: Option<&str>, context_authorized_app: Option<&str>, + context_device: Option<&str>, context_model_app: Option<&str>, context_model_name: Option<&str>, context_model_pk: Option<&str>, - history_days: Option, + history_days: Option, ordering: Option<&str>, search: Option<&str>, username: Option<&str>, @@ -1001,6 +1171,7 @@ pub async fn events_events_volume_list( let p_query_brand_name = brand_name; let p_query_client_ip = client_ip; let p_query_context_authorized_app = context_authorized_app; + let p_query_context_device = context_device; let p_query_context_model_app = context_model_app; let p_query_context_model_name = context_model_name; let p_query_context_model_pk = context_model_pk; @@ -1043,6 +1214,9 @@ pub async fn events_events_volume_list( if let Some(ref param_value) = p_query_context_authorized_app { req_builder = req_builder.query(&[("context_authorized_app", ¶m_value.to_string())]); } + if let Some(ref param_value) = p_query_context_device { + req_builder = req_builder.query(&[("context_device", ¶m_value.to_string())]); + } if let Some(ref param_value) = p_query_context_model_app { req_builder = req_builder.query(&[("context_model_app", ¶m_value.to_string())]); } diff --git a/packages/client-rust/src/models/event_stats.rs b/packages/client-rust/src/models/event_stats.rs new file mode 100644 index 0000000000..1ff8844a1a --- /dev/null +++ b/packages/client-rust/src/models/event_stats.rs @@ -0,0 +1,33 @@ +// authentik +// +// Making authentication simple. +// +// The version of the OpenAPI document: 2026.5.0-rc1 +// Contact: hello@goauthentik.io +// Generated by: https://openapi-generator.tech + +use serde::{Deserialize, Serialize}; + +use crate::models; + +/// EventStats : Count of unique users in events and aggregated counts per specified deltas +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct EventStats { + #[serde(rename = "unique_users")] + pub unique_users: i32, + #[serde(rename = "count_step")] + pub count_step: std::collections::HashMap, +} + +impl EventStats { + /// Count of unique users in events and aggregated counts per specified deltas + pub fn new( + unique_users: i32, + count_step: std::collections::HashMap, + ) -> EventStats { + EventStats { + unique_users, + count_step, + } + } +} diff --git a/packages/client-rust/src/models/event_volume.rs b/packages/client-rust/src/models/event_volume.rs index 6b2f77f015..99a8aa72f3 100644 --- a/packages/client-rust/src/models/event_volume.rs +++ b/packages/client-rust/src/models/event_volume.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use crate::models; -/// EventVolume : Count of events of action created on day +/// EventVolume : Count of events of action created on day for a single event action #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct EventVolume { #[serde(rename = "action")] @@ -22,7 +22,7 @@ pub struct EventVolume { } impl EventVolume { - /// Count of events of action created on day + /// Count of events of action created on day for a single event action pub fn new(action: models::EventActions, time: String, count: i32) -> EventVolume { EventVolume { action, diff --git a/packages/client-rust/src/models/mod.rs b/packages/client-rust/src/models/mod.rs index 49f20aa447..b0524ff9ee 100644 --- a/packages/client-rust/src/models/mod.rs +++ b/packages/client-rust/src/models/mod.rs @@ -342,6 +342,8 @@ pub mod event_matcher_policy_request; pub use self::event_matcher_policy_request::EventMatcherPolicyRequest; pub mod event_request; pub use self::event_request::EventRequest; +pub mod event_stats; +pub use self::event_stats::EventStats; pub mod event_top_per_user; pub use self::event_top_per_user::EventTopPerUser; pub mod event_volume; diff --git a/packages/client-ts/src/apis/EventsApi.ts b/packages/client-ts/src/apis/EventsApi.ts index bbe3fc89dd..d9b2fc4770 100644 --- a/packages/client-ts/src/apis/EventsApi.ts +++ b/packages/client-ts/src/apis/EventsApi.ts @@ -19,6 +19,7 @@ import type { Event, EventActions, EventRequest, + EventStats, EventTopPerUser, EventVolume, GenericError, @@ -52,6 +53,8 @@ import { EventActionsToJSON, EventRequestFromJSON, EventRequestToJSON, + EventStatsFromJSON, + EventStatsToJSON, EventTopPerUserFromJSON, EventTopPerUserToJSON, EventVolumeFromJSON, @@ -114,6 +117,7 @@ export interface EventsEventsExportCreateRequest { brandName?: string; clientIp?: string; contextAuthorizedApp?: string; + contextDevice?: string; contextModelApp?: string; contextModelName?: string; contextModelPk?: string; @@ -128,6 +132,7 @@ export interface EventsEventsListRequest { brandName?: string; clientIp?: string; contextAuthorizedApp?: string; + contextDevice?: string; contextModelApp?: string; contextModelName?: string; contextModelPk?: string; @@ -147,6 +152,22 @@ export interface EventsEventsRetrieveRequest { eventUuid: string; } +export interface EventsEventsStatsRetrieveRequest { + countSteps: Array; + action?: string; + actions?: Array; + brandName?: string; + clientIp?: string; + contextAuthorizedApp?: string; + contextDevice?: string; + contextModelApp?: string; + contextModelName?: string; + contextModelPk?: string; + ordering?: string; + search?: string; + username?: string; +} + export interface EventsEventsTopPerUserListRequest { action?: string; topN?: number; @@ -163,6 +184,7 @@ export interface EventsEventsVolumeListRequest { brandName?: string; clientIp?: string; contextAuthorizedApp?: string; + contextDevice?: string; contextModelApp?: string; contextModelName?: string; contextModelPk?: string; @@ -467,6 +489,10 @@ export class EventsApi extends runtime.BaseAPI { queryParameters['context_authorized_app'] = requestParameters['contextAuthorizedApp']; } + if (requestParameters['contextDevice'] != null) { + queryParameters['context_device'] = requestParameters['contextDevice']; + } + if (requestParameters['contextModelApp'] != null) { queryParameters['context_model_app'] = requestParameters['contextModelApp']; } @@ -556,6 +582,10 @@ export class EventsApi extends runtime.BaseAPI { queryParameters['context_authorized_app'] = requestParameters['contextAuthorizedApp']; } + if (requestParameters['contextDevice'] != null) { + queryParameters['context_device'] = requestParameters['contextDevice']; + } + if (requestParameters['contextModelApp'] != null) { queryParameters['context_model_app'] = requestParameters['contextModelApp']; } @@ -736,6 +766,110 @@ export class EventsApi extends runtime.BaseAPI { return await response.value(); } + /** + * Creates request options for eventsEventsStatsRetrieve without sending the request + */ + async eventsEventsStatsRetrieveRequestOpts(requestParameters: EventsEventsStatsRetrieveRequest): Promise { + if (requestParameters['countSteps'] == null) { + throw new runtime.RequiredError( + 'countSteps', + 'Required parameter "countSteps" was null or undefined when calling eventsEventsStatsRetrieve().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['action'] != null) { + queryParameters['action'] = requestParameters['action']; + } + + if (requestParameters['actions'] != null) { + queryParameters['actions'] = requestParameters['actions']; + } + + if (requestParameters['brandName'] != null) { + queryParameters['brand_name'] = requestParameters['brandName']; + } + + if (requestParameters['clientIp'] != null) { + queryParameters['client_ip'] = requestParameters['clientIp']; + } + + if (requestParameters['contextAuthorizedApp'] != null) { + queryParameters['context_authorized_app'] = requestParameters['contextAuthorizedApp']; + } + + if (requestParameters['contextDevice'] != null) { + queryParameters['context_device'] = requestParameters['contextDevice']; + } + + if (requestParameters['contextModelApp'] != null) { + queryParameters['context_model_app'] = requestParameters['contextModelApp']; + } + + if (requestParameters['contextModelName'] != null) { + queryParameters['context_model_name'] = requestParameters['contextModelName']; + } + + if (requestParameters['contextModelPk'] != null) { + queryParameters['context_model_pk'] = requestParameters['contextModelPk']; + } + + if (requestParameters['countSteps'] != null) { + queryParameters['count_steps'] = requestParameters['countSteps']; + } + + if (requestParameters['ordering'] != null) { + queryParameters['ordering'] = requestParameters['ordering']; + } + + if (requestParameters['search'] != null) { + queryParameters['search'] = requestParameters['search']; + } + + if (requestParameters['username'] != null) { + queryParameters['username'] = requestParameters['username']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.accessToken) { + const token = this.configuration.accessToken; + const tokenString = await token("authentik", []); + + if (tokenString) { + headerParameters["Authorization"] = `Bearer ${tokenString}`; + } + } + + let urlPath = `/events/events/stats/`; + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Get event stats for specified filters and count steps + */ + async eventsEventsStatsRetrieveRaw(requestParameters: EventsEventsStatsRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.eventsEventsStatsRetrieveRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => EventStatsFromJSON(jsonValue)); + } + + /** + * Get event stats for specified filters and count steps + */ + async eventsEventsStatsRetrieve(requestParameters: EventsEventsStatsRetrieveRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.eventsEventsStatsRetrieveRaw(requestParameters, initOverrides); + return await response.value(); + } + /** * Creates request options for eventsEventsTopPerUserList without sending the request */ @@ -878,6 +1012,10 @@ export class EventsApi extends runtime.BaseAPI { queryParameters['context_authorized_app'] = requestParameters['contextAuthorizedApp']; } + if (requestParameters['contextDevice'] != null) { + queryParameters['context_device'] = requestParameters['contextDevice']; + } + if (requestParameters['contextModelApp'] != null) { queryParameters['context_model_app'] = requestParameters['contextModelApp']; } diff --git a/packages/client-ts/src/models/EventStats.ts b/packages/client-ts/src/models/EventStats.ts new file mode 100644 index 0000000000..6589398b8d --- /dev/null +++ b/packages/client-ts/src/models/EventStats.ts @@ -0,0 +1,75 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * authentik + * Making authentication simple. + * + * The version of the OpenAPI document: 2026.5.0-rc1 + * Contact: hello@goauthentik.io + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * Count of unique users in events and aggregated counts per specified deltas + * @export + * @interface EventStats + */ +export interface EventStats { + /** + * + * @type {number} + * @memberof EventStats + */ + uniqueUsers: number; + /** + * + * @type {{ [key: string]: any; }} + * @memberof EventStats + */ + countStep: { [key: string]: any; }; +} + +/** + * Check if a given object implements the EventStats interface. + */ +export function instanceOfEventStats(value: object): value is EventStats { + if (!('uniqueUsers' in value) || value['uniqueUsers'] === undefined) return false; + if (!('countStep' in value) || value['countStep'] === undefined) return false; + return true; +} + +export function EventStatsFromJSON(json: any): EventStats { + return EventStatsFromJSONTyped(json, false); +} + +export function EventStatsFromJSONTyped(json: any, ignoreDiscriminator: boolean): EventStats { + if (json == null) { + return json; + } + return { + + 'uniqueUsers': json['unique_users'], + 'countStep': json['count_step'], + }; +} + +export function EventStatsToJSON(json: any): EventStats { + return EventStatsToJSONTyped(json, false); +} + +export function EventStatsToJSONTyped(value?: EventStats | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'unique_users': value['uniqueUsers'], + 'count_step': value['countStep'], + }; +} + diff --git a/packages/client-ts/src/models/EventVolume.ts b/packages/client-ts/src/models/EventVolume.ts index 3dc1257721..204850d867 100644 --- a/packages/client-ts/src/models/EventVolume.ts +++ b/packages/client-ts/src/models/EventVolume.ts @@ -22,7 +22,7 @@ import { } from './EventActions'; /** - * Count of events of action created on day + * Count of events of action created on day for a single event action * @export * @interface EventVolume */ diff --git a/packages/client-ts/src/models/index.ts b/packages/client-ts/src/models/index.ts index e52dd6bb9b..2bec6481a5 100644 --- a/packages/client-ts/src/models/index.ts +++ b/packages/client-ts/src/models/index.ts @@ -172,6 +172,7 @@ export * from './EventActions'; export * from './EventMatcherPolicy'; export * from './EventMatcherPolicyRequest'; export * from './EventRequest'; +export * from './EventStats'; export * from './EventTopPerUser'; export * from './EventVolume'; export * from './EventsRequestedEnum'; diff --git a/schema.yml b/schema.yml index 2b80827cae..964715fb7d 100644 --- a/schema.yml +++ b/schema.yml @@ -7094,6 +7094,11 @@ paths: schema: type: string description: Context Authorized application + - in: query + name: context_device + schema: + type: string + description: Context Device Primary Key - in: query name: context_model_app schema: @@ -7326,6 +7331,11 @@ paths: schema: type: string description: Context Authorized application + - in: query + name: context_device + schema: + type: string + description: Context Device Primary Key - in: query name: context_model_app schema: @@ -7363,6 +7373,88 @@ paths: $ref: '#/components/responses/ValidationErrorResponse' '403': $ref: '#/components/responses/GenericErrorResponse' + /events/events/stats/: + get: + operationId: events_events_stats_retrieve + description: Get event stats for specified filters and count steps + parameters: + - in: query + name: action + schema: + type: string + - in: query + name: actions + schema: + type: array + items: + $ref: '#/components/schemas/EventActions' + explode: true + style: form + - in: query + name: brand_name + schema: + type: string + description: Brand name + - in: query + name: client_ip + schema: + type: string + - in: query + name: context_authorized_app + schema: + type: string + description: Context Authorized application + - in: query + name: context_device + schema: + type: string + description: Context Device Primary Key + - in: query + name: context_model_app + schema: + type: string + description: Context Model App + - in: query + name: context_model_name + schema: + type: string + description: Context Model Name + - in: query + name: context_model_pk + schema: + type: string + description: Context Model Primary Key + - in: query + name: count_steps + schema: + type: array + items: + type: string + minLength: 1 + description: Timedelta, format of 'weeks=3;days=2;hours=3,seconds=2' + required: true + - $ref: '#/components/parameters/QueryPaginationOrdering' + - $ref: '#/components/parameters/QuerySearch' + - in: query + name: username + schema: + type: string + description: Username + tags: + - events + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/EventStats' + description: '' + '400': + $ref: '#/components/responses/ValidationErrorResponse' + '403': + $ref: '#/components/responses/GenericErrorResponse' /events/events/top_per_user/: get: operationId: events_events_top_per_user_list @@ -7424,6 +7516,11 @@ paths: schema: type: string description: Context Authorized application + - in: query + name: context_device + schema: + type: string + description: Context Device Primary Key - in: query name: context_model_app schema: @@ -7442,7 +7539,7 @@ paths: - in: query name: history_days schema: - type: number + type: integer default: 7 - $ref: '#/components/parameters/QueryPaginationOrdering' - $ref: '#/components/parameters/QuerySearch' @@ -38168,6 +38265,19 @@ components: required: - action - app + EventStats: + type: object + description: Count of unique users in events and aggregated counts per specified + deltas + properties: + unique_users: + type: integer + count_step: + type: object + additionalProperties: {} + required: + - count_step + - unique_users EventTopPerUser: type: object description: Response object of Event's top_per_user @@ -38185,7 +38295,7 @@ components: - unique_users EventVolume: type: object - description: Count of events of action created on day + description: Count of events of action created on day for a single event action properties: action: $ref: '#/components/schemas/EventActions' diff --git a/web/src/admin/admin-overview/cards/RecentEventsCard.ts b/web/src/admin/admin-overview/cards/RecentEventsCard.ts index 79bd1569f2..c98eade8c0 100644 --- a/web/src/admin/admin-overview/cards/RecentEventsCard.ts +++ b/web/src/admin/admin-overview/cards/RecentEventsCard.ts @@ -5,17 +5,10 @@ import "#elements/buttons/Dropdown"; import "#elements/buttons/ModalButton"; import "#elements/buttons/SpinnerButton/index"; -import { DEFAULT_CONFIG } from "#common/api/config"; -import { EventWithContext } from "#common/events"; -import { actionToLabel } from "#common/labels"; - -import { PaginatedResponse, Table, TableColumn } from "#elements/table/Table"; -import { SlottedTemplateResult } from "#elements/types"; - import Styles from "#admin/admin-overview/cards/RecentEventsCard.css"; -import { EventGeo, renderEventUser } from "#admin/events/utils"; +import { SimpleEventTable } from "#admin/events/SimpleEventTable"; -import { Event, EventsApi } from "@goauthentik/api"; +import { EventsEventsListRequest } from "@goauthentik/api"; import { msg } from "@lit/localize"; import { CSSResult, html, TemplateResult } from "lit"; @@ -24,10 +17,11 @@ import { customElement, property } from "lit/decorators.js"; import PFCard from "@patternfly/patternfly/components/Card/card.css"; @customElement("ak-recent-events") -export class RecentEventsCard extends Table { +export class RecentEventsCard extends SimpleEventTable { public override role = "region"; public override ariaLabel = msg("Recent events"); public override label = msg("Events"); + public override expandable = false; @property() order = "-created"; @@ -35,11 +29,10 @@ export class RecentEventsCard extends Table { @property({ type: Number }) pageSize = 10; - async apiEndpoint(): Promise> { - return new EventsApi(DEFAULT_CONFIG).eventsEventsList({ - ...(await this.defaultEndpointConfig()), + async apiParameters(): Promise> { + return { pageSize: this.pageSize, - }); + }; } static styles: CSSResult[] = [ @@ -49,47 +42,12 @@ export class RecentEventsCard extends Table { Styles, ]; - protected override rowLabel(item: Event): string { - return actionToLabel(item.action); - } - - protected columns: TableColumn[] = [ - [msg("Action"), "action"], - [msg("User"), "user"], - [msg("Creation Date"), "created"], - [msg("Client IP"), "client_ip"], - ]; - renderToolbar(): TemplateResult { return html`

${msg("Recent events")}

`; } - - row(item: EventWithContext): SlottedTemplateResult[] { - return [ - html` - ${item.app}`, - renderEventUser(item), - html``, - html`
${item.clientIp || msg("-")}
- ${EventGeo(item)}`, - ]; - } - - renderEmpty(inner?: SlottedTemplateResult): TemplateResult { - if (this.error) { - return super.renderEmpty(inner); - } - - return super.renderEmpty( - html`${msg("No Events found.")} -
${msg("No matching events could be found.")}
-
`, - ); - } } declare global { diff --git a/web/src/admin/applications/ApplicationEvents.ts b/web/src/admin/applications/ApplicationEvents.ts index f24202d7b8..a6880a93eb 100644 --- a/web/src/admin/applications/ApplicationEvents.ts +++ b/web/src/admin/applications/ApplicationEvents.ts @@ -1,68 +1,20 @@ import "#components/ak-event-info"; -import { DEFAULT_CONFIG } from "#common/api/config"; -import { EventWithContext } from "#common/events"; -import { actionToLabel } from "#common/labels"; +import { SimpleEventTable } from "#admin/events/SimpleEventTable"; -import { PaginatedResponse, Table, TableColumn, Timestamp } from "#elements/table/Table"; -import { SlottedTemplateResult } from "#elements/types"; +import { EventsEventsListRequest } from "@goauthentik/api"; -import { renderEventUser } from "#admin/events/utils"; - -import { Event, EventsApi } from "@goauthentik/api"; - -import { msg } from "@lit/localize"; -import { html, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; @customElement("ak-events-application") -export class ApplicationEvents extends Table { - expandable = true; - - @property() - order = "-created"; - +export class ApplicationEvents extends SimpleEventTable { @property({ attribute: "application-id" }) applicationId!: string; - async apiEndpoint(): Promise> { - return new EventsApi(DEFAULT_CONFIG).eventsEventsList({ - ...(await this.defaultEndpointConfig()), + async apiParameters(): Promise> { + return { contextAuthorizedApp: this.applicationId.replaceAll("-", ""), - }); - } - - protected override rowLabel(item: Event): string { - return actionToLabel(item.action); - } - - protected columns: TableColumn[] = [ - [msg("Action"), "action"], - [msg("User"), "enabled"], - [msg("Creation Date"), "created"], - [msg("Client IP"), "client_ip"], - ]; - - row(item: EventWithContext): SlottedTemplateResult[] { - return [ - html`${actionToLabel(item.action)}`, - renderEventUser(item), - Timestamp(item.created), - html`${item.clientIp || msg("-")}`, - ]; - } - - renderExpanded(item: Event): TemplateResult { - return html``; - } - - renderEmpty(): TemplateResult { - return super.renderEmpty( - html`${msg("No Events found.")} -
${msg("No matching events could be found.")}
-
`, - ); + }; } } diff --git a/web/src/admin/applications/ApplicationViewPage.ts b/web/src/admin/applications/ApplicationViewPage.ts index 16f6316fc7..4cacd07aa9 100644 --- a/web/src/admin/applications/ApplicationViewPage.ts +++ b/web/src/admin/applications/ApplicationViewPage.ts @@ -19,11 +19,21 @@ import { AKElement } from "#elements/Base"; import { WithLicenseSummary } from "#elements/mixins/license"; import { setPageDetails } from "#components/ak-page-navbar"; +import renderDescriptionList from "#components/DescriptionList"; -import { Application, ContentTypeEnum, CoreApi, ModelEnum, OutpostsApi } from "@goauthentik/api"; +import { + Application, + ContentTypeEnum, + CoreApi, + EventActions, + EventsApi, + EventStats, + ModelEnum, + OutpostsApi, +} from "@goauthentik/api"; import { msg, str } from "@lit/localize"; -import { CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit"; +import { css, CSSResult, html, nothing, PropertyValues, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; @@ -48,6 +58,11 @@ export class ApplicationViewPage extends WithLicenseSummary(AKElement) { PFGrid, PFFlex, PFCard, + css` + .big-number { + font-size: 250%; + } + `, ]; //#region Properties @@ -61,6 +76,9 @@ export class ApplicationViewPage extends WithLicenseSummary(AKElement) { @state() protected application?: Application; + @state() + protected stats?: EventStats; + @state() protected error?: APIError; @@ -98,6 +116,15 @@ export class ApplicationViewPage extends WithLicenseSummary(AKElement) { ) { this.fetchIsMissingOutpost([app.provider || 0]); } + return new EventsApi(DEFAULT_CONFIG) + .eventsEventsStatsRetrieve({ + action: EventActions.AuthorizeApplication, + contextAuthorizedApp: app.pk.replaceAll("-", ""), + countSteps: ["hours=24", "days=7", "days=30"], + }) + .then((stats) => { + this.stats = stats; + }); }) .catch(async (error) => { this.error = await parseAPIResponseError(error); @@ -120,125 +147,116 @@ export class ApplicationViewPage extends WithLicenseSummary(AKElement) {
${msg("Related")}
-
- ${this.application.providerObj - ? html`
-
- ${msg("Provider")} + ${this.application.providerObj?.name} + (${this.application.providerObj?.verboseName}) + ` + : html`-`, + ], + [ + msg("Backchannel Providers"), + (this.application.backchannelProvidersObj || []).length > 0 + ? html`` + : html`-`, + ], + [ + msg("Policy engine mode"), + html`${this.application.policyEngineMode?.toUpperCase()}`, + ], + [ + msg("Related actions"), + html` + ${msg("Save Changes")} + ${msg("Update Application")} + + + + + + ${msg("Check")} + ${msg("Check Application access")} + + + + + ${this.application.launchUrl + ? html` -
-
- -
-
` - : nothing} - ${(this.application.backchannelProvidersObj || []).length > 0 - ? html`
-
- ${msg("Backchannel Providers")} -
-
-
- -
-
-
` - : nothing} -
-
- ${msg("Policy engine mode")} -
-
-
- ${this.application.policyEngineMode?.toUpperCase()} -
-
-
-
-
- ${msg("Related actions")} -
-
-
- - ${msg("Save Changes")} - ${msg("Update Application")} - - - - - - ${msg("Check")} - - ${msg("Check Application access")} - - - - - - ${this.application.launchUrl - ? html` - ${msg("Launch")} - ` - : nothing} -
-
-
-
+ ${msg("Launch")} + ` + : nothing}`, + ], + ])}
-
+
+
${msg("Statistics")}
+
+ ${renderDescriptionList([ + [ + msg("Users"), + html`

+ ${this.stats ? this.stats?.uniqueUsers : "-"} +

`, + ], + [ + msg("Authorizations (24h)"), + html`

+ ${this.stats ? this.stats?.countStep.hours24 : "-"} +

`, + ], + [ + msg("Authorizations (7d)"), + html`

+ ${this.stats ? this.stats?.countStep.days7 : "-"} +

`, + ], + [ + msg("Authorizations (1m)"), + html`

+ ${this.stats ? this.stats?.countStep.days30 : "-"} +

`, + ], + ])} +
+
+
${msg("Logins over the last week (per 8 hours)")}
diff --git a/web/src/admin/endpoints/devices/DeviceEvents.ts b/web/src/admin/endpoints/devices/DeviceEvents.ts new file mode 100644 index 0000000000..634c451fc0 --- /dev/null +++ b/web/src/admin/endpoints/devices/DeviceEvents.ts @@ -0,0 +1,25 @@ +import "#components/ak-event-info"; + +import { SimpleEventTable } from "#admin/events/SimpleEventTable"; + +import { EventsEventsListRequest } from "@goauthentik/api"; + +import { customElement, property } from "lit/decorators.js"; + +@customElement("ak-events-device") +export class DeviceEvents extends SimpleEventTable { + @property({ attribute: "device-id" }) + deviceId!: string; + + async apiParameters(): Promise> { + return { + contextDevice: this.deviceId.replaceAll("-", ""), + }; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ak-events-device": DeviceEvents; + } +} diff --git a/web/src/admin/endpoints/devices/DeviceViewPage.ts b/web/src/admin/endpoints/devices/DeviceViewPage.ts index 38fe4606b5..6f08c2e0ae 100644 --- a/web/src/admin/endpoints/devices/DeviceViewPage.ts +++ b/web/src/admin/endpoints/devices/DeviceViewPage.ts @@ -6,6 +6,7 @@ import "#admin/endpoints/devices/facts/DeviceUserTable"; import "#admin/endpoints/devices/facts/DeviceSoftwareTable"; import "#admin/endpoints/devices/facts/DeviceGroupTable"; import "#admin/endpoints/devices/DeviceForm"; +import "#admin/endpoints/devices/DeviceEvents"; import "#elements/forms/ModalForm"; import "#elements/Tabs"; @@ -31,6 +32,7 @@ import PFCard from "@patternfly/patternfly/components/Card/card.css"; import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; import PFPage from "@patternfly/patternfly/components/Page/page.css"; import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css"; +import PFStack from "@patternfly/patternfly/layouts/Stack/stack.css"; @customElement("ak-endpoints-device-view") export class DeviceViewPage extends AKElement { @@ -43,7 +45,7 @@ export class DeviceViewPage extends AKElement { @state() protected error?: APIError; - static styles: CSSResult[] = [PFCard, PFPage, PFGrid, PFButton, PFDescriptionList]; + static styles: CSSResult[] = [PFCard, PFPage, PFGrid, PFStack, PFButton, PFDescriptionList]; protected fetchDevice(id: string) { new EndpointsApi(DEFAULT_CONFIG) @@ -74,49 +76,52 @@ export class DeviceViewPage extends AKElement { osFamilyToLabel(this.device.facts.data.os.family), this.device.facts.data.os?.version, ].join(" ") - : undefined, + : "-", icon: "fa fa-laptop", }); } - renderOverview() { - if (!this.device) { - return nothing; - } + renderDetails() { const _rootDisk = - this.device.facts.data.disks?.filter( + this.device?.facts.data.disks?.filter( (d) => d.mountpoint === "/" || d.mountpoint === "C:", ) || []; let rootDisk: Disk | undefined = undefined; if (_rootDisk?.length > 0) { rootDisk = _rootDisk[0]; } - return html`
-
+ return html`
+
${msg("Device details")}
${renderDescriptionList( [ - [msg("Name"), this.device.name], - [msg("Hostname"), this.device.facts.data.network?.hostname ?? "-"], - [msg("Serial number"), this.device.facts.data.hardware?.serial ?? "-"], + [msg("Name"), this.device?.name], + [msg("Hostname"), this.device?.facts.data.network?.hostname ?? "-"], + [msg("Serial number"), this.device?.facts.data.hardware?.serial ?? "-"], [ msg("Operating system"), - this.device.facts.data.os + this.device?.facts.data.os ? [ - this.device.facts.data.os?.name || - osFamilyToLabel(this.device.facts.data.os.family), - this.device.facts.data.os?.version, + this.device?.facts.data.os?.name || + osFamilyToLabel(this.device?.facts.data.os.family), + this.device?.facts.data.os?.version, ].join(" ") : "-", ], [ msg("Firewall enabled"), html``, ], - [msg("Device access group"), this.device.accessGroupObj?.name ?? "-"], + [ + msg("Disk encryption"), + html``, + ], + [msg("Device access group"), this.device?.accessGroupObj?.name ?? "-"], [ msg("Actions"), html` @@ -124,7 +129,7 @@ export class DeviceViewPage extends AKElement { ${msg("Update Device")}
-
+
${msg("Hardware")}
${renderDescriptionList( [ [ msg("Manufacturer"), - this.device.facts.data.hardware?.manufacturer ?? "-", + this.device?.facts.data.hardware?.manufacturer ?? "-", ], - [msg("Model"), this.device.facts.data.hardware?.model ?? "-"], + [msg("Model"), this.device?.facts.data.hardware?.model ?? "-"], [ msg("CPU"), - this.device.facts.data.hardware?.cpuCount && - this.device.facts.data.hardware?.cpuName + this.device?.facts.data.hardware?.cpuCount && + this.device?.facts.data.hardware?.cpuName ? msg( - str`${this.device.facts.data.hardware?.cpuCount} x ${this.device.facts.data.hardware?.cpuName}`, + str`${this.device?.facts.data.hardware?.cpuCount} x ${this.device?.facts.data.hardware?.cpuName}`, ) : "-", ], [ msg("Memory"), - this.device.facts.data.hardware?.memoryBytes - ? getSize(this.device.facts.data.hardware?.memoryBytes) + this.device?.facts.data.hardware?.memoryBytes + ? getSize(this.device?.facts.data.hardware?.memoryBytes) : "-", ], - [ - msg("Disk encryption"), - html``, - ], [ msg("Primary disk size"), rootDisk?.capacityTotalBytes @@ -192,11 +191,11 @@ export class DeviceViewPage extends AKElement { )}
-
+
${msg("Connections")}
${renderDescriptionList( - this.device.connectionsObj.map((conn) => { + this.device?.connectionsObj.map((conn) => { return [ html`${conn.connectorObj.name}`, html`
@@ -215,18 +214,6 @@ export class DeviceViewPage extends AKElement { )}
-
-
${msg("Users / Groups")}
- -
-
- -
`; } @@ -288,7 +275,27 @@ export class DeviceViewPage extends AKElement { aria-label="${msg("Overview")}" class="pf-c-page__main-section" > - ${this.renderOverview()} +
+
${this.renderDetails()}
+
+
+
${msg("Events")}
+ +
+
+
${msg("Users / Groups")}
+ +
+
+ +
+
+
{ + abstract apiParameters(): Promise>; + + expandable = true; + + @property() + order = "-created"; + + async apiEndpoint(): Promise> { + return new EventsApi(DEFAULT_CONFIG).eventsEventsList({ + ...(await this.defaultEndpointConfig()), + ...(await this.apiParameters()), + }); + } + + protected override rowLabel(item: Event): string { + return actionToLabel(item.action); + } + + protected columns: TableColumn[] = [ + [msg("Action"), "action"], + [msg("User"), "enabled"], + [msg("Creation Date"), "created"], + [msg("Client IP"), "client_ip"], + ]; + + row(item: EventWithContext): SlottedTemplateResult[] { + return [ + html` + ${item.app}`, + renderEventUser(item), + Timestamp(item.created), + html`
${item.clientIp || msg("-")}
+ ${EventGeo(item)}`, + ]; + } + + renderExpanded(item: Event): TemplateResult { + return html``; + } + + renderEmpty(): TemplateResult { + return super.renderEmpty( + html`${msg("No Events found.")} +
${msg("No matching events could be found.")}
+
`, + ); + } +} diff --git a/web/src/admin/events/UserEvents.ts b/web/src/admin/events/UserEvents.ts index 1fda73c931..c444d181ff 100644 --- a/web/src/admin/events/UserEvents.ts +++ b/web/src/admin/events/UserEvents.ts @@ -1,68 +1,20 @@ import "#components/ak-event-info"; -import { DEFAULT_CONFIG } from "#common/api/config"; -import { EventWithContext } from "#common/events"; -import { actionToLabel } from "#common/labels"; +import { SimpleEventTable } from "#admin/events/SimpleEventTable"; -import { PaginatedResponse, Table, TableColumn, Timestamp } from "#elements/table/Table"; -import { SlottedTemplateResult } from "#elements/types"; +import { EventsEventsListRequest } from "@goauthentik/api"; -import { renderEventUser } from "#admin/events/utils"; - -import { Event, EventsApi } from "@goauthentik/api"; - -import { msg } from "@lit/localize"; -import { html, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; @customElement("ak-events-user") -export class UserEvents extends Table { - expandable = true; - - @property() - order = "-created"; - +export class UserEvents extends SimpleEventTable { @property() targetUser!: string; - async apiEndpoint(): Promise> { - return new EventsApi(DEFAULT_CONFIG).eventsEventsList({ - ...(await this.defaultEndpointConfig()), + async apiParameters(): Promise> { + return { username: this.targetUser, - }); - } - - protected override rowLabel(item: Event): string { - return actionToLabel(item.action); - } - - protected columns: TableColumn[] = [ - [msg("Action"), "action"], - [msg("User"), "enabled"], - [msg("Creation Date"), "created"], - [msg("Client IP"), "client_ip"], - ]; - - row(item: EventWithContext): SlottedTemplateResult[] { - return [ - html`${actionToLabel(item.action)}`, - renderEventUser(item), - Timestamp(item.created), - html`${item.clientIp || msg("-")}`, - ]; - } - - renderExpanded(item: Event): TemplateResult { - return html``; - } - - renderEmpty(): TemplateResult { - return super.renderEmpty( - html`${msg("No Events found.")} -
${msg("No matching events could be found.")}
-
`, - ); + }; } } diff --git a/web/src/admin/outposts/OutpostProviderList.ts b/web/src/admin/outposts/OutpostProviderList.ts index 6f58c6b7b3..583be38b43 100644 --- a/web/src/admin/outposts/OutpostProviderList.ts +++ b/web/src/admin/outposts/OutpostProviderList.ts @@ -10,7 +10,7 @@ import { SlottedTemplateResult } from "#elements/types"; import { Provider } from "@goauthentik/api"; import { msg } from "@lit/localize"; -import { html } from "lit"; +import { html, nothing } from "lit"; import { customElement } from "lit/decorators.js"; @customElement("ak-outposts-provider-list") @@ -19,7 +19,7 @@ export class OutpostsProviderList extends StaticTable { protected columns: TableColumn[] = [ // --- [msg("Name")], - [msg("ID")], + [msg("Application")], ]; row(item: Provider): SlottedTemplateResult[] { @@ -27,6 +27,11 @@ export class OutpostsProviderList extends StaticTable { html`
${item.name}
`, + item.assignedApplicationName + ? html` +
${item.assignedApplicationName}
+
` + : nothing, ]; } }