add event-to-color map

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens Langhammer
2025-06-07 01:29:01 +02:00
parent 19adbe2c06
commit 1306d7548e
7 changed files with 133 additions and 104 deletions

View File

@ -1,14 +1,14 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart } from "@goauthentik/elements/charts/Chart";
import { ChartData, ChartDataset } from "chart.js";
import { msg } from "@lit/localize";
import { customElement } from "lit/decorators.js";
import { EventActions, EventVolume, EventsApi } from "@goauthentik/api";
import { EventChart } from "#elements/charts/EventChart";
@customElement("ak-charts-admin-login-authorization")
export class AdminLoginAuthorizeChart extends AKChart<EventVolume[]> {
export class AdminLoginAuthorizeChart extends EventChart {
async apiRequest(): Promise<EventVolume[]> {
return new EventsApi(DEFAULT_CONFIG).eventsEventsVolumeList({
actions: [
@ -23,8 +23,6 @@ export class AdminLoginAuthorizeChart extends AKChart<EventVolume[]> {
const optsMap = new Map<EventActions, Partial<ChartDataset>>();
optsMap.set(EventActions.AuthorizeApplication, {
label: msg("Authorizations"),
backgroundColor: "rgba(43, 154, 243, 0.5)",
borderColor: "rgba(43, 154, 243, 1)",
spanGaps: true,
fill: "origin",
cubicInterpolationMode: "monotone",
@ -32,8 +30,6 @@ export class AdminLoginAuthorizeChart extends AKChart<EventVolume[]> {
});
optsMap.set(EventActions.Login, {
label: msg("Successful Logins"),
backgroundColor: "rgba(62, 134, 53, 0.5)",
borderColor: "rgba(62, 134, 53, 1)",
spanGaps: true,
fill: "origin",
cubicInterpolationMode: "monotone",
@ -41,8 +37,6 @@ export class AdminLoginAuthorizeChart extends AKChart<EventVolume[]> {
});
optsMap.set(EventActions.LoginFailed, {
label: msg("Failed Logins"),
backgroundColor: "rgba(201, 24, 11, 0.5)",
borderColor: "rgba(201, 24, 11, 1)",
spanGaps: true,
fill: "origin",
cubicInterpolationMode: "monotone",

View File

@ -1,5 +1,4 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart } from "@goauthentik/elements/charts/Chart";
import { ChartData } from "chart.js";
import { msg } from "@lit/localize";
@ -11,9 +10,10 @@ import {
EventsApi,
EventsEventsVolumeListRequest,
} from "@goauthentik/api";
import { EventChart } from "#elements/charts/EventChart";
@customElement("ak-charts-admin-model-per-day")
export class AdminModelPerDay extends AKChart<EventVolume[]> {
export class AdminModelPerDay extends EventChart {
@property()
action: EventActions = EventActions.ModelCreated;
@ -38,7 +38,6 @@ export class AdminModelPerDay extends AKChart<EventVolume[]> {
this.action,
{
label: this.label || msg("Objects created"),
backgroundColor: "rgba(189, 229, 184, .5)",
spanGaps: true,
},
],

View File

@ -1,14 +1,14 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart } from "@goauthentik/elements/charts/Chart";
import { ChartData } from "chart.js";
import { msg } from "@lit/localize";
import { customElement, property } from "lit/decorators.js";
import { EventActions, EventVolume, EventsApi } from "@goauthentik/api";
import { EventChart } from "#elements/charts/EventChart";
@customElement("ak-charts-application-authorize")
export class ApplicationAuthorizeChart extends AKChart<EventVolume[]> {
export class ApplicationAuthorizeChart extends EventChart {
@property({ attribute: "application-id" })
applicationId!: string;
@ -26,7 +26,6 @@ export class ApplicationAuthorizeChart extends AKChart<EventVolume[]> {
EventActions.AuthorizeApplication,
{
label: msg("Authorizations"),
backgroundColor: "rgba(189, 229, 184, .5)",
spanGaps: true,
},
],

View File

@ -1,5 +1,4 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart } from "@goauthentik/elements/charts/Chart";
import { ChartData } from "chart.js";
import { CSSResult, TemplateResult, css, html } from "lit";
@ -8,9 +7,10 @@ import { customElement, property } from "lit/decorators.js";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import { EventVolume, EventsApi, EventsEventsListRequest } from "@goauthentik/api";
import { EventChart } from "#elements/charts/EventChart";
@customElement("ak-events-volume-chart")
export class EventVolumeChart extends AKChart<EventVolume[]> {
export class EventVolumeChart extends EventChart {
_query?: EventsEventsListRequest;
@property({ attribute: false })

View File

@ -1,14 +1,14 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKChart } from "@goauthentik/elements/charts/Chart";
import { ChartData } from "chart.js";
import { msg } from "@lit/localize";
import { customElement, property } from "lit/decorators.js";
import { EventActions, EventVolume, EventsApi } from "@goauthentik/api";
import { EventChart } from "#elements/charts/EventChart";
@customElement("ak-charts-user")
export class UserChart extends AKChart<EventVolume[]> {
export class UserChart extends EventChart {
@property()
username?: string;
@ -30,7 +30,6 @@ export class UserChart extends AKChart<EventVolume[]> {
EventActions.LoginFailed,
{
label: msg("Failed Logins"),
backgroundColor: "rgba(201, 25, 11, .5)",
spanGaps: true,
},
],
@ -38,7 +37,6 @@ export class UserChart extends AKChart<EventVolume[]> {
EventActions.Login,
{
label: msg("Successful Logins"),
backgroundColor: "rgba(189, 229, 184, .5)",
spanGaps: true,
},
],
@ -46,7 +44,6 @@ export class UserChart extends AKChart<EventVolume[]> {
EventActions.AuthorizeApplication,
{
label: msg("Application authorizations"),
backgroundColor: "rgba(43, 154, 243, .5)",
spanGaps: true,
},
],

View File

@ -1,4 +1,3 @@
import { actionToLabel } from "#common/labels";
import { EVENT_REFRESH, EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
import {
APIError,
@ -30,7 +29,7 @@ import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { property, state } from "lit/decorators.js";
import { EventActions, EventVolume, UiThemeEnum } from "@goauthentik/api";
import { UiThemeEnum } from "@goauthentik/api";
Chart.register(Legend, Tooltip);
Chart.register(LineController, BarController, DoughnutController);
@ -40,32 +39,6 @@ Chart.register(TimeScale, TimeSeriesScale, LinearScale, Filler);
export const FONT_COLOUR_DARK_MODE = "#fafafa";
export const FONT_COLOUR_LIGHT_MODE = "#151515";
export class RGBAColor {
constructor(
public r: number,
public g: number,
public b: number,
public a: number = 1,
) {}
toString(): string {
return `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`;
}
}
export function getColorFromString(stringInput: string): RGBAColor {
let hash = 0;
for (let i = 0; i < stringInput.length; i++) {
hash = stringInput.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash;
}
const rgb = [0, 0, 0];
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 255;
rgb[i] = value;
}
return new RGBAColor(rgb[0], rgb[1], rgb[2]);
}
export abstract class AKChart<T> extends AKElement {
abstract apiRequest(): Promise<T>;
abstract getChartData(data: T): ChartData;
@ -238,59 +211,4 @@ export abstract class AKChart<T> extends AKElement {
</div>
`;
}
eventVolume(
data: EventVolume[],
options?: {
optsMap?: Map<EventActions, Partial<ChartDataset>>;
padToDays?: number;
},
): ChartData {
const datasets: ChartData = {
datasets: [],
};
if (!options) {
options = {};
}
if (!options.optsMap) {
options.optsMap = new Map<EventActions, Partial<ChartDataset>>();
}
const actions = new Set(data.map((v) => v.action));
actions.forEach((action) => {
const actionData: { x: number; y: number }[] = [];
data.filter((v) => v.action === action).forEach((v) => {
actionData.push({
x: v.time.getTime(),
y: v.count,
});
});
// Check if we need to pad the data to reach a certain time window
const earliestDate = data
.filter((v) => v.action === action)
.map((v) => v.time)
.sort((a, b) => b.getTime() - a.getTime())
.reverse();
if (earliestDate.length > 0 && options.padToDays) {
const earliestPadded = new Date(
new Date().getTime() - options.padToDays * (1000 * 3600 * 24),
);
const daysDelta = Math.round(
(earliestDate[0].getTime() - earliestPadded.getTime()) / (1000 * 3600 * 24),
);
if (daysDelta > 0) {
actionData.push({
x: earliestPadded.getTime(),
y: 0,
});
}
}
datasets.datasets.push({
data: actionData,
label: actionToLabel(action),
backgroundColor: getColorFromString(action).toString(),
...options.optsMap?.get(action),
});
});
return datasets;
}
}

View File

@ -0,0 +1,122 @@
import { actionToLabel } from "#common/labels";
import { AKChart } from "#elements/charts/Chart";
import { ChartData, ChartDataset } from "chart.js";
import { EventActions, EventVolume } from "@goauthentik/api";
export function actionToColor(action: EventActions): string {
switch (action) {
case EventActions.AuthorizeApplication:
return "#0060c0";
case EventActions.ConfigurationError:
return "#4cb140";
case EventActions.EmailSent:
return "#009596";
case EventActions.FlowExecution:
return "#f4c145";
case EventActions.ImpersonationEnded:
return "#a2d9d9";
case EventActions.ImpersonationStarted:
return "#a2d9d9";
case EventActions.InvitationUsed:
return "#8bc1f7";
case EventActions.Login:
return "#23511e";
case EventActions.LoginFailed:
return "#ec7a08";
case EventActions.Logout:
return "#f9e0a2";
case EventActions.ModelCreated:
return "#8f4700";
case EventActions.ModelDeleted:
return "#002f5d";
case EventActions.ModelUpdated:
return "#bde2b9";
case EventActions.PasswordSet:
return "#003737";
case EventActions.PolicyException:
return "#c58c00";
case EventActions.PolicyExecution:
return "#f4b678";
case EventActions.PropertyMappingException:
return "#519de9";
case EventActions.SecretRotate:
return "#38812f";
case EventActions.SecretView:
return "#73c5c5";
case EventActions.SourceLinked:
return "#f6d173";
case EventActions.SuspiciousRequest:
return "#c46100";
case EventActions.SystemException:
return "#004b95";
case EventActions.SystemTaskException:
return "#7cc674";
case EventActions.SystemTaskExecution:
return "#005f60";
case EventActions.UpdateAvailable:
return "#f0ab00";
case EventActions.UserWrite:
return "#ef9234";
}
return "";
}
export abstract class EventChart extends AKChart<EventVolume[]> {
eventVolume(
data: EventVolume[],
options?: {
optsMap?: Map<EventActions, Partial<ChartDataset>>;
padToDays?: number;
},
): ChartData {
const datasets: ChartData = {
datasets: [],
};
if (!options) {
options = {};
}
if (!options.optsMap) {
options.optsMap = new Map<EventActions, Partial<ChartDataset>>();
}
const actions = new Set(data.map((v) => v.action));
actions.forEach((action) => {
const actionData: { x: number; y: number }[] = [];
data.filter((v) => v.action === action).forEach((v) => {
actionData.push({
x: v.time.getTime(),
y: v.count,
});
});
// Check if we need to pad the data to reach a certain time window
const earliestDate = data
.filter((v) => v.action === action)
.map((v) => v.time)
.sort((a, b) => b.getTime() - a.getTime())
.reverse();
if (earliestDate.length > 0 && options.padToDays) {
const earliestPadded = new Date(
new Date().getTime() - options.padToDays * (1000 * 3600 * 24),
);
const daysDelta = Math.round(
(earliestDate[0].getTime() - earliestPadded.getTime()) / (1000 * 3600 * 24),
);
if (daysDelta > 0) {
actionData.push({
x: earliestPadded.getTime(),
y: 0,
});
}
}
datasets.datasets.push({
data: actionData,
label: actionToLabel(action),
backgroundColor: actionToColor(action),
...options.optsMap?.get(action),
});
});
return datasets;
}
}