diff --git a/web/src/admin/admin-overview/charts/AdminLoginAuthorizeChart.ts b/web/src/admin/admin-overview/charts/AdminLoginAuthorizeChart.ts index a371c4ed56..2b246da055 100644 --- a/web/src/admin/admin-overview/charts/AdminLoginAuthorizeChart.ts +++ b/web/src/admin/admin-overview/charts/AdminLoginAuthorizeChart.ts @@ -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 { +export class AdminLoginAuthorizeChart extends EventChart { async apiRequest(): Promise { return new EventsApi(DEFAULT_CONFIG).eventsEventsVolumeList({ actions: [ @@ -23,8 +23,6 @@ export class AdminLoginAuthorizeChart extends AKChart { const optsMap = new Map>(); 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 { }); 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 { }); 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", diff --git a/web/src/admin/admin-overview/charts/AdminModelPerDay.ts b/web/src/admin/admin-overview/charts/AdminModelPerDay.ts index be3df33fe5..6892f66444 100644 --- a/web/src/admin/admin-overview/charts/AdminModelPerDay.ts +++ b/web/src/admin/admin-overview/charts/AdminModelPerDay.ts @@ -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 { +export class AdminModelPerDay extends EventChart { @property() action: EventActions = EventActions.ModelCreated; @@ -38,7 +38,6 @@ export class AdminModelPerDay extends AKChart { this.action, { label: this.label || msg("Objects created"), - backgroundColor: "rgba(189, 229, 184, .5)", spanGaps: true, }, ], diff --git a/web/src/admin/applications/ApplicationAuthorizeChart.ts b/web/src/admin/applications/ApplicationAuthorizeChart.ts index f5c0c78c75..14788e48c9 100644 --- a/web/src/admin/applications/ApplicationAuthorizeChart.ts +++ b/web/src/admin/applications/ApplicationAuthorizeChart.ts @@ -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 { +export class ApplicationAuthorizeChart extends EventChart { @property({ attribute: "application-id" }) applicationId!: string; @@ -26,7 +26,6 @@ export class ApplicationAuthorizeChart extends AKChart { EventActions.AuthorizeApplication, { label: msg("Authorizations"), - backgroundColor: "rgba(189, 229, 184, .5)", spanGaps: true, }, ], diff --git a/web/src/admin/events/EventVolumeChart.ts b/web/src/admin/events/EventVolumeChart.ts index 608eead71a..9a188e8b96 100644 --- a/web/src/admin/events/EventVolumeChart.ts +++ b/web/src/admin/events/EventVolumeChart.ts @@ -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 { +export class EventVolumeChart extends EventChart { _query?: EventsEventsListRequest; @property({ attribute: false }) diff --git a/web/src/admin/users/UserChart.ts b/web/src/admin/users/UserChart.ts index f735ac85d6..fc837efc1d 100644 --- a/web/src/admin/users/UserChart.ts +++ b/web/src/admin/users/UserChart.ts @@ -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 { +export class UserChart extends EventChart { @property() username?: string; @@ -30,7 +30,6 @@ export class UserChart extends AKChart { EventActions.LoginFailed, { label: msg("Failed Logins"), - backgroundColor: "rgba(201, 25, 11, .5)", spanGaps: true, }, ], @@ -38,7 +37,6 @@ export class UserChart extends AKChart { EventActions.Login, { label: msg("Successful Logins"), - backgroundColor: "rgba(189, 229, 184, .5)", spanGaps: true, }, ], @@ -46,7 +44,6 @@ export class UserChart extends AKChart { EventActions.AuthorizeApplication, { label: msg("Application authorizations"), - backgroundColor: "rgba(43, 154, 243, .5)", spanGaps: true, }, ], diff --git a/web/src/elements/charts/Chart.ts b/web/src/elements/charts/Chart.ts index 358c7825a8..f6836e61f3 100644 --- a/web/src/elements/charts/Chart.ts +++ b/web/src/elements/charts/Chart.ts @@ -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 extends AKElement { abstract apiRequest(): Promise; abstract getChartData(data: T): ChartData; @@ -238,59 +211,4 @@ export abstract class AKChart extends AKElement { `; } - - eventVolume( - data: EventVolume[], - options?: { - optsMap?: Map>; - padToDays?: number; - }, - ): ChartData { - const datasets: ChartData = { - datasets: [], - }; - if (!options) { - options = {}; - } - if (!options.optsMap) { - options.optsMap = new Map>(); - } - 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; - } } diff --git a/web/src/elements/charts/EventChart.ts b/web/src/elements/charts/EventChart.ts new file mode 100644 index 0000000000..8c42a3f062 --- /dev/null +++ b/web/src/elements/charts/EventChart.ts @@ -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( + data: EventVolume[], + options?: { + optsMap?: Map>; + padToDays?: number; + }, + ): ChartData { + const datasets: ChartData = { + datasets: [], + }; + if (!options) { + options = {}; + } + if (!options.optsMap) { + options.optsMap = new Map>(); + } + 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; + } +}