events: rework metrics (#4407)
* rework metrics Signed-off-by: Jens Langhammer <jens@goauthentik.io> * change graphs to be over last week Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix Apps with most usage card Signed-off-by: Jens Langhammer <jens@goauthentik.io> Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
a35b8f5862
commit
67a6fa6399
|
@ -1,4 +1,7 @@
|
||||||
"""authentik administration metrics"""
|
"""authentik administration metrics"""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.db.models.functions import ExtractDay
|
||||||
from drf_spectacular.utils import extend_schema, extend_schema_field
|
from drf_spectacular.utils import extend_schema, extend_schema_field
|
||||||
from guardian.shortcuts import get_objects_for_user
|
from guardian.shortcuts import get_objects_for_user
|
||||||
from rest_framework.fields import IntegerField, SerializerMethodField
|
from rest_framework.fields import IntegerField, SerializerMethodField
|
||||||
|
@ -21,38 +24,44 @@ class CoordinateSerializer(PassiveSerializer):
|
||||||
class LoginMetricsSerializer(PassiveSerializer):
|
class LoginMetricsSerializer(PassiveSerializer):
|
||||||
"""Login Metrics per 1h"""
|
"""Login Metrics per 1h"""
|
||||||
|
|
||||||
logins_per_1h = SerializerMethodField()
|
logins = SerializerMethodField()
|
||||||
logins_failed_per_1h = SerializerMethodField()
|
logins_failed = SerializerMethodField()
|
||||||
authorizations_per_1h = SerializerMethodField()
|
authorizations = SerializerMethodField()
|
||||||
|
|
||||||
@extend_schema_field(CoordinateSerializer(many=True))
|
@extend_schema_field(CoordinateSerializer(many=True))
|
||||||
def get_logins_per_1h(self, _):
|
def get_logins(self, _):
|
||||||
"""Get successful logins per hour for the last 24 hours"""
|
"""Get successful logins per 8 hours for the last 7 days"""
|
||||||
user = self.context["user"]
|
user = self.context["user"]
|
||||||
return (
|
return (
|
||||||
get_objects_for_user(user, "authentik_events.view_event")
|
get_objects_for_user(user, "authentik_events.view_event").filter(
|
||||||
.filter(action=EventAction.LOGIN)
|
action=EventAction.LOGIN
|
||||||
.get_events_per_hour()
|
)
|
||||||
|
# 3 data points per day, so 8 hour spans
|
||||||
|
.get_events_per(timedelta(days=7), ExtractDay, 7 * 3)
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema_field(CoordinateSerializer(many=True))
|
@extend_schema_field(CoordinateSerializer(many=True))
|
||||||
def get_logins_failed_per_1h(self, _):
|
def get_logins_failed(self, _):
|
||||||
"""Get failed logins per hour for the last 24 hours"""
|
"""Get failed logins per 8 hours for the last 7 days"""
|
||||||
user = self.context["user"]
|
user = self.context["user"]
|
||||||
return (
|
return (
|
||||||
get_objects_for_user(user, "authentik_events.view_event")
|
get_objects_for_user(user, "authentik_events.view_event").filter(
|
||||||
.filter(action=EventAction.LOGIN_FAILED)
|
action=EventAction.LOGIN_FAILED
|
||||||
.get_events_per_hour()
|
)
|
||||||
|
# 3 data points per day, so 8 hour spans
|
||||||
|
.get_events_per(timedelta(days=7), ExtractDay, 7 * 3)
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema_field(CoordinateSerializer(many=True))
|
@extend_schema_field(CoordinateSerializer(many=True))
|
||||||
def get_authorizations_per_1h(self, _):
|
def get_authorizations(self, _):
|
||||||
"""Get successful authorizations per hour for the last 24 hours"""
|
"""Get successful authorizations per 8 hours for the last 7 days"""
|
||||||
user = self.context["user"]
|
user = self.context["user"]
|
||||||
return (
|
return (
|
||||||
get_objects_for_user(user, "authentik_events.view_event")
|
get_objects_for_user(user, "authentik_events.view_event").filter(
|
||||||
.filter(action=EventAction.AUTHORIZE_APPLICATION)
|
action=EventAction.AUTHORIZE_APPLICATION
|
||||||
.get_events_per_hour()
|
)
|
||||||
|
# 3 data points per day, so 8 hour spans
|
||||||
|
.get_events_per(timedelta(days=7), ExtractDay, 7 * 3)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
"""Application API Views"""
|
"""Application API Views"""
|
||||||
|
from datetime import timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
|
from django.db.models.functions import ExtractDay
|
||||||
from django.http.response import HttpResponseBadRequest
|
from django.http.response import HttpResponseBadRequest
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
@ -259,10 +261,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""Metrics for application logins"""
|
"""Metrics for application logins"""
|
||||||
app = self.get_object()
|
app = self.get_object()
|
||||||
return Response(
|
return Response(
|
||||||
get_objects_for_user(request.user, "authentik_events.view_event")
|
get_objects_for_user(request.user, "authentik_events.view_event").filter(
|
||||||
.filter(
|
|
||||||
action=EventAction.AUTHORIZE_APPLICATION,
|
action=EventAction.AUTHORIZE_APPLICATION,
|
||||||
context__authorized_application__pk=app.pk.hex,
|
context__authorized_application__pk=app.pk.hex,
|
||||||
)
|
)
|
||||||
.get_events_per_hour()
|
# 3 data points per day, so 8 hour spans
|
||||||
|
.get_events_per(timedelta(days=7), ExtractDay, 7 * 3)
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,7 @@ from json import loads
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from django.contrib.auth import update_session_auth_hash
|
from django.contrib.auth import update_session_auth_hash
|
||||||
|
from django.db.models.functions import ExtractDay
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.db.transaction import atomic
|
from django.db.transaction import atomic
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
|
@ -199,38 +200,44 @@ class SessionUserSerializer(PassiveSerializer):
|
||||||
class UserMetricsSerializer(PassiveSerializer):
|
class UserMetricsSerializer(PassiveSerializer):
|
||||||
"""User Metrics"""
|
"""User Metrics"""
|
||||||
|
|
||||||
logins_per_1h = SerializerMethodField()
|
logins = SerializerMethodField()
|
||||||
logins_failed_per_1h = SerializerMethodField()
|
logins_failed = SerializerMethodField()
|
||||||
authorizations_per_1h = SerializerMethodField()
|
authorizations = SerializerMethodField()
|
||||||
|
|
||||||
@extend_schema_field(CoordinateSerializer(many=True))
|
@extend_schema_field(CoordinateSerializer(many=True))
|
||||||
def get_logins_per_1h(self, _):
|
def get_logins(self, _):
|
||||||
"""Get successful logins per hour for the last 24 hours"""
|
"""Get successful logins per 8 hours for the last 7 days"""
|
||||||
user = self.context["user"]
|
user = self.context["user"]
|
||||||
return (
|
return (
|
||||||
get_objects_for_user(user, "authentik_events.view_event")
|
get_objects_for_user(user, "authentik_events.view_event").filter(
|
||||||
.filter(action=EventAction.LOGIN, user__pk=user.pk)
|
action=EventAction.LOGIN, user__pk=user.pk
|
||||||
.get_events_per_hour()
|
)
|
||||||
|
# 3 data points per day, so 8 hour spans
|
||||||
|
.get_events_per(timedelta(days=7), ExtractDay, 7 * 3)
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema_field(CoordinateSerializer(many=True))
|
@extend_schema_field(CoordinateSerializer(many=True))
|
||||||
def get_logins_failed_per_1h(self, _):
|
def get_logins_failed(self, _):
|
||||||
"""Get failed logins per hour for the last 24 hours"""
|
"""Get failed logins per 8 hours for the last 7 days"""
|
||||||
user = self.context["user"]
|
user = self.context["user"]
|
||||||
return (
|
return (
|
||||||
get_objects_for_user(user, "authentik_events.view_event")
|
get_objects_for_user(user, "authentik_events.view_event").filter(
|
||||||
.filter(action=EventAction.LOGIN_FAILED, context__username=user.username)
|
action=EventAction.LOGIN_FAILED, context__username=user.username
|
||||||
.get_events_per_hour()
|
)
|
||||||
|
# 3 data points per day, so 8 hour spans
|
||||||
|
.get_events_per(timedelta(days=7), ExtractDay, 7 * 3)
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema_field(CoordinateSerializer(many=True))
|
@extend_schema_field(CoordinateSerializer(many=True))
|
||||||
def get_authorizations_per_1h(self, _):
|
def get_authorizations(self, _):
|
||||||
"""Get failed logins per hour for the last 24 hours"""
|
"""Get failed logins per 8 hours for the last 7 days"""
|
||||||
user = self.context["user"]
|
user = self.context["user"]
|
||||||
return (
|
return (
|
||||||
get_objects_for_user(user, "authentik_events.view_event")
|
get_objects_for_user(user, "authentik_events.view_event").filter(
|
||||||
.filter(action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk)
|
action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk
|
||||||
.get_events_per_hour()
|
)
|
||||||
|
# 3 data points per day, so 8 hour spans
|
||||||
|
.get_events_per(timedelta(days=7), ExtractDay, 7 * 3)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
"""Events API Views"""
|
"""Events API Views"""
|
||||||
|
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.aggregates import Count
|
||||||
from django.db.models.fields.json import KeyTextTransform
|
from django.db.models.fields.json import KeyTextTransform
|
||||||
|
from django.db.models.functions import ExtractDay
|
||||||
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
|
||||||
from guardian.shortcuts import get_objects_for_user
|
from guardian.shortcuts import get_objects_for_user
|
||||||
|
@ -178,7 +180,7 @@ class EventViewSet(ModelViewSet):
|
||||||
get_objects_for_user(request.user, "authentik_events.view_event")
|
get_objects_for_user(request.user, "authentik_events.view_event")
|
||||||
.filter(action=filtered_action)
|
.filter(action=filtered_action)
|
||||||
.filter(**query)
|
.filter(**query)
|
||||||
.get_events_per_day()
|
.get_events_per(timedelta(weeks=4), ExtractDay, 30)
|
||||||
)
|
)
|
||||||
|
|
||||||
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
|
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
|
||||||
|
|
|
@ -11,8 +11,7 @@ from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Count, ExpressionWrapper, F
|
from django.db.models import Count, ExpressionWrapper, F
|
||||||
from django.db.models.fields import DurationField
|
from django.db.models.fields import DurationField
|
||||||
from django.db.models.functions import ExtractHour
|
from django.db.models.functions import Extract
|
||||||
from django.db.models.functions.datetime import ExtractDay
|
|
||||||
from django.db.models.manager import Manager
|
from django.db.models.manager import Manager
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
@ -111,48 +110,36 @@ class EventAction(models.TextChoices):
|
||||||
class EventQuerySet(QuerySet):
|
class EventQuerySet(QuerySet):
|
||||||
"""Custom events query set with helper functions"""
|
"""Custom events query set with helper functions"""
|
||||||
|
|
||||||
def get_events_per_hour(self) -> list[dict[str, int]]:
|
def get_events_per(
|
||||||
|
self,
|
||||||
|
time_since: timedelta,
|
||||||
|
extract: Extract,
|
||||||
|
data_points: int,
|
||||||
|
) -> list[dict[str, int]]:
|
||||||
"""Get event count by hour in the last day, fill with zeros"""
|
"""Get event count by hour in the last day, fill with zeros"""
|
||||||
date_from = now() - timedelta(days=1)
|
_now = now()
|
||||||
|
max_since = timedelta(days=60)
|
||||||
|
# Allow maximum of 60 days to limit load
|
||||||
|
if time_since.total_seconds() > max_since.total_seconds():
|
||||||
|
time_since = max_since
|
||||||
|
date_from = _now - time_since
|
||||||
result = (
|
result = (
|
||||||
self.filter(created__gte=date_from)
|
self.filter(created__gte=date_from)
|
||||||
.annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
|
.annotate(age=ExpressionWrapper(_now - F("created"), output_field=DurationField()))
|
||||||
.annotate(age_hours=ExtractHour("age"))
|
.annotate(age_interval=extract("age"))
|
||||||
.values("age_hours")
|
.values("age_interval")
|
||||||
.annotate(count=Count("pk"))
|
.annotate(count=Count("pk"))
|
||||||
.order_by("age_hours")
|
.order_by("age_interval")
|
||||||
)
|
)
|
||||||
data = Counter({int(d["age_hours"]): d["count"] for d in result})
|
data = Counter({int(d["age_interval"]): d["count"] for d in result})
|
||||||
results = []
|
results = []
|
||||||
_now = now()
|
interval_timdelta = time_since / data_points
|
||||||
for hour in range(0, -24, -1):
|
for interval in range(1, -data_points, -1):
|
||||||
results.append(
|
results.append(
|
||||||
{
|
{
|
||||||
"x_cord": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000,
|
"x_cord": time.mktime((_now + (interval_timdelta * interval)).timetuple())
|
||||||
"y_cord": data[hour * -1],
|
* 1000,
|
||||||
}
|
"y_cord": data[interval * -1],
|
||||||
)
|
|
||||||
return results
|
|
||||||
|
|
||||||
def get_events_per_day(self) -> list[dict[str, int]]:
|
|
||||||
"""Get event count by hour in the last day, fill with zeros"""
|
|
||||||
date_from = now() - timedelta(weeks=4)
|
|
||||||
result = (
|
|
||||||
self.filter(created__gte=date_from)
|
|
||||||
.annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
|
|
||||||
.annotate(age_days=ExtractDay("age"))
|
|
||||||
.values("age_days")
|
|
||||||
.annotate(count=Count("pk"))
|
|
||||||
.order_by("age_days")
|
|
||||||
)
|
|
||||||
data = Counter({int(d["age_days"]): d["count"] for d in result})
|
|
||||||
results = []
|
|
||||||
_now = now()
|
|
||||||
for day in range(0, -30, -1):
|
|
||||||
results.append(
|
|
||||||
{
|
|
||||||
"x_cord": time.mktime((_now + timedelta(days=day)).timetuple()) * 1000,
|
|
||||||
"y_cord": data[day * -1],
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return results
|
return results
|
||||||
|
@ -165,13 +152,14 @@ class EventManager(Manager):
|
||||||
"""use custom queryset"""
|
"""use custom queryset"""
|
||||||
return EventQuerySet(self.model, using=self._db)
|
return EventQuerySet(self.model, using=self._db)
|
||||||
|
|
||||||
def get_events_per_hour(self) -> list[dict[str, int]]:
|
def get_events_per(
|
||||||
|
self,
|
||||||
|
time_since: timedelta,
|
||||||
|
extract: Extract,
|
||||||
|
data_points: int,
|
||||||
|
) -> list[dict[str, int]]:
|
||||||
"""Wrap method from queryset"""
|
"""Wrap method from queryset"""
|
||||||
return self.get_queryset().get_events_per_hour()
|
return self.get_queryset().get_events_per(time_since, extract, data_points)
|
||||||
|
|
||||||
def get_events_per_day(self) -> list[dict[str, int]]:
|
|
||||||
"""Wrap method from queryset"""
|
|
||||||
return self.get_queryset().get_events_per_day()
|
|
||||||
|
|
||||||
|
|
||||||
class Event(SerializerModel, ExpiringModel):
|
class Event(SerializerModel, ExpiringModel):
|
||||||
|
|
24
schema.yml
24
schema.yml
|
@ -28957,25 +28957,25 @@ components:
|
||||||
type: object
|
type: object
|
||||||
description: Login Metrics per 1h
|
description: Login Metrics per 1h
|
||||||
properties:
|
properties:
|
||||||
logins_per_1h:
|
logins:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/Coordinate'
|
$ref: '#/components/schemas/Coordinate'
|
||||||
readOnly: true
|
readOnly: true
|
||||||
logins_failed_per_1h:
|
logins_failed:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/Coordinate'
|
$ref: '#/components/schemas/Coordinate'
|
||||||
readOnly: true
|
readOnly: true
|
||||||
authorizations_per_1h:
|
authorizations:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/Coordinate'
|
$ref: '#/components/schemas/Coordinate'
|
||||||
readOnly: true
|
readOnly: true
|
||||||
required:
|
required:
|
||||||
- authorizations_per_1h
|
- authorizations
|
||||||
- logins_failed_per_1h
|
- logins
|
||||||
- logins_per_1h
|
- logins_failed
|
||||||
LoginSource:
|
LoginSource:
|
||||||
type: object
|
type: object
|
||||||
description: Serializer for Login buttons of sources
|
description: Serializer for Login buttons of sources
|
||||||
|
@ -37741,25 +37741,25 @@ components:
|
||||||
type: object
|
type: object
|
||||||
description: User Metrics
|
description: User Metrics
|
||||||
properties:
|
properties:
|
||||||
logins_per_1h:
|
logins:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/Coordinate'
|
$ref: '#/components/schemas/Coordinate'
|
||||||
readOnly: true
|
readOnly: true
|
||||||
logins_failed_per_1h:
|
logins_failed:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/Coordinate'
|
$ref: '#/components/schemas/Coordinate'
|
||||||
readOnly: true
|
readOnly: true
|
||||||
authorizations_per_1h:
|
authorizations:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/Coordinate'
|
$ref: '#/components/schemas/Coordinate'
|
||||||
readOnly: true
|
readOnly: true
|
||||||
required:
|
required:
|
||||||
- authorizations_per_1h
|
- authorizations
|
||||||
- logins_failed_per_1h
|
- logins
|
||||||
- logins_per_1h
|
- logins_failed
|
||||||
UserOAuthSourceConnection:
|
UserOAuthSourceConnection:
|
||||||
type: object
|
type: object
|
||||||
description: OAuth Source Serializer
|
description: OAuth Source Serializer
|
||||||
|
|
|
@ -180,7 +180,7 @@ export class AdminOverviewPage extends AKElement {
|
||||||
>
|
>
|
||||||
<ak-aggregate-card
|
<ak-aggregate-card
|
||||||
icon="pf-icon pf-icon-server"
|
icon="pf-icon pf-icon-server"
|
||||||
header=${t`Logins and authorizations over the last 24 hours`}
|
header=${t`Logins and authorizations over the last week (per 8 hours)`}
|
||||||
>
|
>
|
||||||
<ak-charts-admin-login-authorization></ak-charts-admin-login-authorization>
|
<ak-charts-admin-login-authorization></ak-charts-admin-login-authorization>
|
||||||
</ak-aggregate-card>
|
</ak-aggregate-card>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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, RGBAColor } from "@goauthentik/elements/charts/Chart";
|
||||||
import { ChartData } from "chart.js";
|
import { ChartData, Tick } from "chart.js";
|
||||||
|
|
||||||
import { t } from "@lingui/macro";
|
import { t } from "@lingui/macro";
|
||||||
|
|
||||||
|
@ -14,6 +14,13 @@ export class AdminLoginAuthorizeChart extends AKChart<LoginMetrics> {
|
||||||
return new AdminApi(DEFAULT_CONFIG).adminMetricsRetrieve();
|
return new AdminApi(DEFAULT_CONFIG).adminMetricsRetrieve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 t`${ago} day(s) ago`;
|
||||||
|
}
|
||||||
|
|
||||||
getChartData(data: LoginMetrics): ChartData {
|
getChartData(data: LoginMetrics): ChartData {
|
||||||
return {
|
return {
|
||||||
datasets: [
|
datasets: [
|
||||||
|
@ -25,7 +32,7 @@ export class AdminLoginAuthorizeChart extends AKChart<LoginMetrics> {
|
||||||
fill: "origin",
|
fill: "origin",
|
||||||
cubicInterpolationMode: "monotone",
|
cubicInterpolationMode: "monotone",
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
data: data.authorizationsPer1h.map((cord) => {
|
data: data.authorizations.map((cord) => {
|
||||||
return {
|
return {
|
||||||
x: cord.xCord,
|
x: cord.xCord,
|
||||||
y: cord.yCord,
|
y: cord.yCord,
|
||||||
|
@ -40,7 +47,7 @@ export class AdminLoginAuthorizeChart extends AKChart<LoginMetrics> {
|
||||||
fill: "origin",
|
fill: "origin",
|
||||||
cubicInterpolationMode: "monotone",
|
cubicInterpolationMode: "monotone",
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
data: data.loginsFailedPer1h.map((cord) => {
|
data: data.loginsFailed.map((cord) => {
|
||||||
return {
|
return {
|
||||||
x: cord.xCord,
|
x: cord.xCord,
|
||||||
y: cord.yCord,
|
y: cord.yCord,
|
||||||
|
@ -55,7 +62,7 @@ export class AdminLoginAuthorizeChart extends AKChart<LoginMetrics> {
|
||||||
fill: "origin",
|
fill: "origin",
|
||||||
cubicInterpolationMode: "monotone",
|
cubicInterpolationMode: "monotone",
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
data: data.loginsPer1h.map((cord) => {
|
data: data.logins.map((cord) => {
|
||||||
return {
|
return {
|
||||||
x: cord.xCord,
|
x: cord.xCord,
|
||||||
y: cord.yCord,
|
y: cord.yCord,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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 } from "@goauthentik/elements/charts/Chart";
|
||||||
import { ChartData } from "chart.js";
|
import { ChartData, Tick } from "chart.js";
|
||||||
|
|
||||||
import { t } from "@lingui/macro";
|
import { t } from "@lingui/macro";
|
||||||
|
|
||||||
|
@ -19,6 +19,13 @@ export class ApplicationAuthorizeChart extends AKChart<Coordinate[]> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 t`${ago} days ago`;
|
||||||
|
}
|
||||||
|
|
||||||
getChartData(data: Coordinate[]): ChartData {
|
getChartData(data: Coordinate[]): ChartData {
|
||||||
return {
|
return {
|
||||||
datasets: [
|
datasets: [
|
||||||
|
|
|
@ -225,7 +225,9 @@ export class ApplicationViewPage extends AKElement {
|
||||||
<div
|
<div
|
||||||
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-10-col-on-xl pf-m-10-col-on-2xl"
|
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-10-col-on-xl pf-m-10-col-on-2xl"
|
||||||
>
|
>
|
||||||
<div class="pf-c-card__title">${t`Logins over the last 24 hours`}</div>
|
<div class="pf-c-card__title">
|
||||||
|
${t`Logins over the last week (per 8 hours)`}
|
||||||
|
</div>
|
||||||
<div class="pf-c-card__body">
|
<div class="pf-c-card__body">
|
||||||
${this.application &&
|
${this.application &&
|
||||||
html` <ak-charts-application-authorize
|
html` <ak-charts-application-authorize
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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 } from "@goauthentik/elements/charts/Chart";
|
||||||
import { ChartData } from "chart.js";
|
import { ChartData, Tick } from "chart.js";
|
||||||
|
|
||||||
import { t } from "@lingui/macro";
|
import { t } from "@lingui/macro";
|
||||||
|
|
||||||
|
@ -19,6 +19,13 @@ export class UserChart extends AKChart<UserMetrics> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 t`${ago} days ago`;
|
||||||
|
}
|
||||||
|
|
||||||
getChartData(data: UserMetrics): ChartData {
|
getChartData(data: UserMetrics): ChartData {
|
||||||
return {
|
return {
|
||||||
datasets: [
|
datasets: [
|
||||||
|
@ -27,7 +34,7 @@ export class UserChart extends AKChart<UserMetrics> {
|
||||||
backgroundColor: "rgba(201, 25, 11, .5)",
|
backgroundColor: "rgba(201, 25, 11, .5)",
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
data:
|
data:
|
||||||
data.loginsFailedPer1h?.map((cord) => {
|
data.loginsFailed?.map((cord) => {
|
||||||
return {
|
return {
|
||||||
x: cord.xCord || 0,
|
x: cord.xCord || 0,
|
||||||
y: cord.yCord || 0,
|
y: cord.yCord || 0,
|
||||||
|
@ -39,7 +46,7 @@ export class UserChart extends AKChart<UserMetrics> {
|
||||||
backgroundColor: "rgba(189, 229, 184, .5)",
|
backgroundColor: "rgba(189, 229, 184, .5)",
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
data:
|
data:
|
||||||
data.loginsPer1h?.map((cord) => {
|
data.logins?.map((cord) => {
|
||||||
return {
|
return {
|
||||||
x: cord.xCord || 0,
|
x: cord.xCord || 0,
|
||||||
y: cord.yCord || 0,
|
y: cord.yCord || 0,
|
||||||
|
@ -51,7 +58,7 @@ export class UserChart extends AKChart<UserMetrics> {
|
||||||
backgroundColor: "rgba(43, 154, 243, .5)",
|
backgroundColor: "rgba(43, 154, 243, .5)",
|
||||||
spanGaps: true,
|
spanGaps: true,
|
||||||
data:
|
data:
|
||||||
data.authorizationsPer1h?.map((cord) => {
|
data.authorizations?.map((cord) => {
|
||||||
return {
|
return {
|
||||||
x: cord.xCord || 0,
|
x: cord.xCord || 0,
|
||||||
y: cord.yCord || 0,
|
y: cord.yCord || 0,
|
||||||
|
|
|
@ -276,7 +276,9 @@ export class UserViewPage extends AKElement {
|
||||||
<div
|
<div
|
||||||
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-9-col-on-xl pf-m-9-col-on-2xl"
|
class="pf-c-card pf-l-grid__item pf-m-12-col pf-m-9-col-on-xl pf-m-9-col-on-2xl"
|
||||||
>
|
>
|
||||||
<div class="pf-c-card__title">${t`Actions over the last 24 hours`}</div>
|
<div class="pf-c-card__title">
|
||||||
|
${t`Actions over the last week (per 8 hours)`}
|
||||||
|
</div>
|
||||||
<div class="pf-c-card__body">
|
<div class="pf-c-card__body">
|
||||||
<ak-charts-user userId=${this.user.pk || 0}> </ak-charts-user>
|
<ak-charts-user userId=${this.user.pk || 0}> </ak-charts-user>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -40,6 +40,9 @@ export class AggregateCard extends AKElement {
|
||||||
.subtext {
|
.subtext {
|
||||||
font-size: var(--pf-global--FontSize--sm);
|
font-size: var(--pf-global--FontSize--sm);
|
||||||
}
|
}
|
||||||
|
.pf-c-card__body {
|
||||||
|
overflow-x: scroll;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,7 +168,7 @@ export abstract class AKChart<T> extends AKElement {
|
||||||
const valueStamp = ticks[index];
|
const valueStamp = ticks[index];
|
||||||
const delta = Date.now() - valueStamp.value;
|
const delta = Date.now() - valueStamp.value;
|
||||||
const ago = Math.round(delta / 1000 / 3600);
|
const ago = Math.round(delta / 1000 / 3600);
|
||||||
return t`${ago} hours ago`;
|
return t`${ago} hour(s) ago`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getOptions(): ChartOptions {
|
getOptions(): ChartOptions {
|
||||||
|
|
Reference in New Issue