![gcp-cherry-pick-bot[bot]](/assets/img/avatar_default.png)
Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com> fix bug that was causing charts to be too tall (#14253)
242 lines
7.2 KiB
TypeScript
242 lines
7.2 KiB
TypeScript
import { EVENT_REFRESH, EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
|
|
import {
|
|
APIError,
|
|
parseAPIResponseError,
|
|
pluckErrorDetail,
|
|
} from "@goauthentik/common/errors/network";
|
|
import { formatElapsedTime } from "@goauthentik/common/temporal";
|
|
import { AKElement } from "@goauthentik/elements/Base";
|
|
import "@goauthentik/elements/EmptyState";
|
|
import {
|
|
Chart,
|
|
ChartConfiguration,
|
|
ChartData,
|
|
ChartOptions,
|
|
Filler,
|
|
LineElement,
|
|
Plugin,
|
|
PointElement,
|
|
Tick,
|
|
} from "chart.js";
|
|
import { Legend, Tooltip } from "chart.js";
|
|
import { BarController, DoughnutController, LineController } from "chart.js";
|
|
import { ArcElement, BarElement } from "chart.js";
|
|
import { LinearScale, TimeScale } from "chart.js";
|
|
import "chartjs-adapter-date-fns";
|
|
|
|
import { msg } from "@lit/localize";
|
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
|
import { property, state } from "lit/decorators.js";
|
|
|
|
import { UiThemeEnum } from "@goauthentik/api";
|
|
|
|
Chart.register(Legend, Tooltip);
|
|
Chart.register(LineController, BarController, DoughnutController);
|
|
Chart.register(ArcElement, BarElement, PointElement, LineElement);
|
|
Chart.register(TimeScale, 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;
|
|
|
|
@state()
|
|
chart?: Chart;
|
|
|
|
@state()
|
|
error?: APIError;
|
|
|
|
@property()
|
|
centerText?: string;
|
|
|
|
fontColour = FONT_COLOUR_LIGHT_MODE;
|
|
|
|
static get styles(): CSSResult[] {
|
|
return [
|
|
css`
|
|
.container {
|
|
height: 100%;
|
|
width: 100%;
|
|
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
position: relative;
|
|
}
|
|
.container > span {
|
|
position: absolute;
|
|
font-size: 2.5rem;
|
|
}
|
|
canvas {
|
|
width: 100px;
|
|
height: 100px;
|
|
z-index: 1;
|
|
cursor: crosshair;
|
|
}
|
|
`,
|
|
];
|
|
}
|
|
|
|
connectedCallback(): void {
|
|
super.connectedCallback();
|
|
window.addEventListener("resize", this.resizeHandler);
|
|
this.addEventListener(EVENT_REFRESH, this.refreshHandler);
|
|
this.addEventListener(EVENT_THEME_CHANGE, ((ev: CustomEvent<UiThemeEnum>) => {
|
|
if (ev.detail === UiThemeEnum.Light) {
|
|
this.fontColour = FONT_COLOUR_LIGHT_MODE;
|
|
} else {
|
|
this.fontColour = FONT_COLOUR_DARK_MODE;
|
|
}
|
|
this.chart?.update();
|
|
}) as EventListener);
|
|
}
|
|
|
|
disconnectedCallback(): void {
|
|
super.disconnectedCallback();
|
|
window.removeEventListener("resize", this.resizeHandler);
|
|
this.removeEventListener(EVENT_REFRESH, this.refreshHandler);
|
|
}
|
|
|
|
refreshHandler(): void {
|
|
this.apiRequest().then((r: T) => {
|
|
if (!this.chart) return;
|
|
this.chart.data = this.getChartData(r);
|
|
this.chart.update();
|
|
});
|
|
}
|
|
|
|
resizeHandler(): void {
|
|
if (!this.chart) {
|
|
return;
|
|
}
|
|
this.chart.resize();
|
|
}
|
|
|
|
firstUpdated(): void {
|
|
this.apiRequest()
|
|
.then((r) => {
|
|
const canvas = this.shadowRoot?.querySelector<HTMLCanvasElement>("canvas");
|
|
|
|
if (!canvas) {
|
|
console.warn("Failed to get canvas element");
|
|
return;
|
|
}
|
|
|
|
const ctx = canvas.getContext("2d");
|
|
|
|
if (!ctx) {
|
|
console.warn("failed to get 2d context");
|
|
return;
|
|
}
|
|
|
|
this.chart = this.configureChart(r, ctx);
|
|
})
|
|
.catch(async (error: unknown) => {
|
|
const parsedError = await parseAPIResponseError(error);
|
|
this.error = parsedError;
|
|
});
|
|
}
|
|
|
|
getChartType(): string {
|
|
return "bar";
|
|
}
|
|
|
|
getPlugins(): Plugin[] {
|
|
return [];
|
|
}
|
|
|
|
timeTickCallback(tickValue: string | number, index: number, ticks: Tick[]): string {
|
|
const valueStamp = ticks[index];
|
|
return formatElapsedTime(new Date(valueStamp.value));
|
|
}
|
|
|
|
getOptions(): ChartOptions {
|
|
return {
|
|
maintainAspectRatio: false,
|
|
responsive: true,
|
|
scales: {
|
|
x: {
|
|
type: "time",
|
|
display: true,
|
|
ticks: {
|
|
callback: (tickValue: string | number, index: number, ticks: Tick[]) => {
|
|
return this.timeTickCallback(tickValue, index, ticks);
|
|
},
|
|
autoSkip: true,
|
|
maxTicksLimit: 8,
|
|
},
|
|
stacked: true,
|
|
grid: {
|
|
color: "rgba(0, 0, 0, 0)",
|
|
},
|
|
offset: true,
|
|
},
|
|
y: {
|
|
type: "linear",
|
|
display: true,
|
|
stacked: true,
|
|
grid: {
|
|
color: "rgba(0, 0, 0, 0)",
|
|
},
|
|
},
|
|
},
|
|
} as ChartOptions;
|
|
}
|
|
|
|
configureChart(data: T, ctx: CanvasRenderingContext2D): Chart {
|
|
const config = {
|
|
type: this.getChartType(),
|
|
data: this.getChartData(data),
|
|
options: this.getOptions(),
|
|
plugins: this.getPlugins(),
|
|
};
|
|
return new Chart(ctx, config as ChartConfiguration);
|
|
}
|
|
|
|
render(): TemplateResult {
|
|
return html`
|
|
<div class="container">
|
|
${this.error
|
|
? html`
|
|
<ak-empty-state header="${msg("Failed to fetch data.")}" icon="fa-times">
|
|
<p slot="body">${pluckErrorDetail(this.error)}</p>
|
|
</ak-empty-state>
|
|
`
|
|
: html`${this.chart
|
|
? html``
|
|
: html`<ak-empty-state ?loading="${true}"></ak-empty-state>`}`}
|
|
${this.centerText ? html` <span>${this.centerText}</span> ` : html``}
|
|
<canvas style="${this.chart === undefined ? "display: none;" : ""}"></canvas>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|