mirror of
https://github.com/hrfee/jfa-go.git
synced 2026-01-18 16:47:42 +01:00
not gonna go any further. This is an unnecessary feature which could just be a wiki page (and it will).
362 lines
11 KiB
TypeScript
362 lines
11 KiB
TypeScript
import { ActivityQueries } from "./activity";
|
|
import { HTTPMethod, _http } from "./common";
|
|
import { PageCountDTO } from "./list";
|
|
import { Search, ServerFilterReqDTO } from "./search";
|
|
|
|
import * as echarts from "echarts/core";
|
|
import { BarChart } from "echarts/charts";
|
|
import {
|
|
TitleComponent,
|
|
TooltipComponent,
|
|
GridComponent,
|
|
DatasetComponent,
|
|
TransformComponent
|
|
} from 'echarts/components';
|
|
import { CanvasRenderer } from 'echarts/renderers';
|
|
import { AccountsQueries } from "./accounts";
|
|
import { SimpleRadioTabs } from "./ui";
|
|
|
|
echarts.use([
|
|
BarChart,
|
|
TitleComponent,
|
|
TooltipComponent,
|
|
GridComponent,
|
|
DatasetComponent,
|
|
TransformComponent,
|
|
CanvasRenderer
|
|
]);
|
|
|
|
// FIXME: These should be language keys, not literals!
|
|
export type Unit = string | [string, string]; // string or [singular, plural]
|
|
export const UnitUsers: Unit = ["User", "Users"];
|
|
export const UnitRecords: Unit = ["Record", "Records"];
|
|
export const UnitOccurences: Unit = ["Occurrence", "Occurrences"];
|
|
export const UnitInvites: Unit = ["Invite", "Invites"];
|
|
|
|
export const EventNamespace = "stats-";
|
|
export const GlobalReload = EventNamespace + "reload-all";
|
|
|
|
export interface StatCard {
|
|
name: string;
|
|
value: any;
|
|
reload: () => void;
|
|
asElement: () => HTMLElement;
|
|
// Event the card will listen to for reloads
|
|
eventName: string;
|
|
};
|
|
|
|
export abstract class NumberCard implements StatCard {
|
|
protected _name: string;
|
|
protected _nameEl: HTMLElement;
|
|
get name(): string { return this._name; }
|
|
set name(v: string) {
|
|
this._name = v;
|
|
this._nameEl.textContent = v;
|
|
}
|
|
|
|
protected _unit: Unit | null = null;
|
|
protected _unitEl: HTMLElement;
|
|
get unit(): Unit { return this._unit; }
|
|
set unit(v: Unit) {
|
|
this._unit = v;
|
|
// re-load value to set units correctly
|
|
this.value = this.value;
|
|
}
|
|
|
|
protected _value: number;
|
|
protected _valueEl: HTMLElement;
|
|
get value(): number { return this._value; }
|
|
set value(v: number) {
|
|
this._value = v;
|
|
if (v == -1) {
|
|
this._valueEl.textContent = "";
|
|
} else {
|
|
this._valueEl.textContent = ""+this._value;
|
|
}
|
|
|
|
if (this._unit === null) return;
|
|
|
|
if (typeof this._unit === "string") this._unitEl.textContent = this._unit;
|
|
else this._unitEl.textContent = (v == 1) ? this._unit[0] : this._unit[1];
|
|
}
|
|
|
|
protected _url: string;
|
|
protected _method: HTTPMethod;
|
|
protected _container: HTMLElement;
|
|
|
|
// generates data to be passed in the HTTP request.
|
|
abstract params: () => any;
|
|
// returns value from HTTP response.
|
|
abstract handler: (req: XMLHttpRequest) => number;
|
|
|
|
// Name of a custom event that will trigger this specific card to reload.
|
|
readonly eventName: string;
|
|
|
|
public reload = () => {
|
|
let params = this.params();
|
|
_http(this._method, this._url, params, (req: XMLHttpRequest) => {
|
|
if (req.readyState != 4) return;
|
|
if (req.status != 200) {
|
|
this.value = -1;
|
|
} else {
|
|
this.value = this.handler(req);
|
|
}
|
|
});
|
|
}
|
|
|
|
protected onReloadRequest = () => this.reload();
|
|
|
|
asElement = () => { return this._container; }
|
|
|
|
constructor(name: string, url: string, method: HTTPMethod, unit?: Unit) {
|
|
this._url = url;
|
|
this._method = method;
|
|
|
|
this._container = document.createElement("div");
|
|
this._container.classList.add("card", "@low", "dark:~d_neutral", "flex", "flex-col", "gap-4");
|
|
this._container.innerHTML = `
|
|
<p class="text-xl italic number-card-name"></p>
|
|
<p class="text-2xl font-bold"><span class="number-card-value"></span> <span class="number-card-unit"></span></p>
|
|
`;
|
|
this._nameEl = this._container.querySelector(".number-card-name");
|
|
this._valueEl = this._container.querySelector(".number-card-value");
|
|
this._unitEl = this._container.querySelector(".number-card-unit");
|
|
|
|
this.name = name;
|
|
if (unit) this.unit = unit;
|
|
this.value = -1;
|
|
|
|
this.eventName = this.name;
|
|
|
|
document.addEventListener(EventNamespace+this.eventName, this.onReloadRequest);
|
|
document.addEventListener(GlobalReload, this.onReloadRequest);
|
|
}
|
|
}
|
|
|
|
export class CountCard extends NumberCard {
|
|
params = () => {};
|
|
handler = (req: XMLHttpRequest): number => {
|
|
return (req.response as PageCountDTO).count;
|
|
};
|
|
|
|
constructor(name: string, url: string, unit?: Unit) {
|
|
super(name, url, "GET", unit);
|
|
}
|
|
}
|
|
|
|
export class FilteredCountCard extends CountCard {
|
|
private _params: ServerFilterReqDTO;
|
|
params = (): ServerFilterReqDTO => { return this._params };
|
|
|
|
constructor(name: string, url: string, params: ServerFilterReqDTO, unit?: Unit) {
|
|
super(name, url, unit);
|
|
this._method = "POST";
|
|
this._params = params;
|
|
}
|
|
}
|
|
|
|
export class GraphCard implements StatCard {
|
|
protected _name: string;
|
|
protected _nameEl: HTMLElement;
|
|
get name(): string { return this._name; }
|
|
set name(v: string) {
|
|
this._name = v;
|
|
this._nameEl.textContent = v;
|
|
this._chartOpts.title = { text: v };
|
|
}
|
|
|
|
protected _unit: Unit | null = null;
|
|
get unit(): Unit { return this._unit; }
|
|
set unit(v: Unit) {
|
|
this._unit = v;
|
|
this._chartOpts.series[0].name = v;
|
|
}
|
|
|
|
protected _range: string[];
|
|
get range(): string[] { return this._range; }
|
|
set range(v: string[]) {
|
|
this._range = v;
|
|
this._chartOpts.xAxis = { data: v };
|
|
}
|
|
|
|
protected _value: number[];
|
|
protected _graphEl: HTMLElement;
|
|
protected _chart: echarts.ECharts;
|
|
protected _chartOpts: echarts.EChartsCoreOption;
|
|
|
|
get value(): number[] { return this._value; }
|
|
set value(v: number[]) {
|
|
this._value = v;
|
|
|
|
this._chartOpts.series[0].data = v;
|
|
}
|
|
|
|
protected redraw() {
|
|
this._chart.setOption(this._chartOpts);
|
|
}
|
|
|
|
protected _url: string;
|
|
protected _method: HTTPMethod;
|
|
protected _container: HTMLElement;
|
|
|
|
// generates params for each req.
|
|
params: () => ServerFilterReqDTO[];
|
|
// returns value from HTTP response.
|
|
handler: (req: XMLHttpRequest) => number[];
|
|
|
|
// Name of a custom event that will trigger this specific card to reload.
|
|
readonly eventName: string;
|
|
|
|
public reload = () => {
|
|
let params = this.params();
|
|
_http(this._method, this._url, params, (req: XMLHttpRequest) => {
|
|
if (req.readyState != 4) return;
|
|
if (req.status != 200) {
|
|
this.value = [];
|
|
} else {
|
|
this.value = this.handler(req);
|
|
}
|
|
});
|
|
}
|
|
|
|
protected onReloadRequest = () => this.reload();
|
|
|
|
asElement = () => { return this._container; }
|
|
|
|
constructor(name: string, url: string, method: HTTPMethod, unit?: Unit) {
|
|
this._url = url;
|
|
this._method = method;
|
|
|
|
this._container = document.createElement("div");
|
|
this._container.classList.add("card", "@low", "dark:~d_neutral", "flex", "flex-col", "gap-4");
|
|
this._container.innerHTML = `
|
|
<p class="text-xl italic graph-card-name"></p>
|
|
<div class="text-2xl font-bold graph-card-graph"></div>
|
|
`;
|
|
this._nameEl = this._container.querySelector(".graph-card-name");
|
|
this._graphEl = this._container.querySelector(".graph-card-graph");
|
|
|
|
this._chart = echarts.init(this._graphEl);
|
|
this._chartOpts = {
|
|
title: {
|
|
text: "?"
|
|
},
|
|
tooltip: {},
|
|
xAxis: {
|
|
data: []
|
|
},
|
|
yAxis: {},
|
|
series: [
|
|
{
|
|
name: "",
|
|
type: "bar",
|
|
data: []
|
|
}
|
|
]
|
|
};
|
|
|
|
this.name = name;
|
|
if (unit) this.unit = unit;
|
|
this.range = [];
|
|
this.value = [];
|
|
|
|
this.eventName = this.name;
|
|
|
|
document.addEventListener(EventNamespace+this.eventName, this.onReloadRequest);
|
|
document.addEventListener(GlobalReload, this.onReloadRequest);
|
|
}
|
|
}
|
|
|
|
// FIXME: Make a page and load some of these!
|
|
|
|
export class StatsPanel {
|
|
private _container: HTMLElement;
|
|
private _cards: Map<string, StatCard>;
|
|
private _order: string[];
|
|
|
|
private _loaded = false;
|
|
|
|
public static DefaultLayout(): StatCard[] {
|
|
return [
|
|
new CountCard("Number of users", "/users/count", UnitUsers),
|
|
new CountCard("Number of invites", "/invites/count", UnitInvites),
|
|
new FilteredCountCard(
|
|
"Users created through jfa-go",
|
|
"/activity/count",
|
|
Search.DTOFromString("account-creation:true", ActivityQueries()),
|
|
UnitUsers
|
|
)
|
|
];
|
|
}
|
|
|
|
addCards = (...cards: StatCard[]) => {
|
|
for (const card of cards) {
|
|
this._cards.set(card.name, card);
|
|
this._order.push(card.name);
|
|
}
|
|
}
|
|
|
|
deleteCards = (...cards: StatCard[]) => {
|
|
for (const card of cards) {
|
|
this._cards.delete(card.name);
|
|
let idx = this._order.indexOf(card.name);
|
|
if (idx != -1) this._order.splice(idx, 1);
|
|
}
|
|
};
|
|
|
|
reflow = () => {
|
|
const temp = document.createDocumentFragment();
|
|
for (const name of this._order) {
|
|
temp.appendChild(this._cards.get(name).asElement());
|
|
}
|
|
this._container.replaceChildren(temp);
|
|
}
|
|
|
|
reload = () => {
|
|
const hasLoaded = this._loaded;
|
|
this._loaded = true;
|
|
document.dispatchEvent(new CustomEvent(GlobalReload));
|
|
if (!hasLoaded) {
|
|
this.reflow();
|
|
}
|
|
}
|
|
|
|
bindPageEvents = () => {};
|
|
unbindPageEvents = () => {};
|
|
|
|
constructor(container: HTMLElement) {
|
|
this._container = container;
|
|
this._container.classList.add("flex", "flex-row", "gap-2", "flex-wrap");
|
|
|
|
this._cards = new Map<string, StatCard>();
|
|
this._order = [];
|
|
|
|
const queryTypeTabs = new SimpleRadioTabs(
|
|
[document.getElementById("statistics-query-tab-accounts"), document.getElementById("statistics-query-tab-activity")],
|
|
"statistics-query-type"
|
|
);
|
|
|
|
queryTypeTabs.select(0);
|
|
|
|
const areas = [document.getElementById("statistics-query-tab-accounts"), document.getElementById("statistics-query-tab-activity")];
|
|
const querySets = [AccountsQueries(), ActivityQueries()];
|
|
|
|
for (let i = 0; i < 2; i++) {
|
|
let search = new Search({
|
|
queries: querySets[i],
|
|
filterArea: areas[i].getElementsByClassName("statistics-filter-area")[0] as HTMLElement,
|
|
filterList: areas[i].getElementsByClassName("statistics-filter-list")[0] as HTMLElement,
|
|
search: areas[i].getElementsByClassName("search")[0] as HTMLInputElement,
|
|
|
|
setVisibility: () => {},
|
|
onSearchCallback: () => {},
|
|
searchServer: () => {},
|
|
clearServerSearch: () => {},
|
|
});
|
|
search.generateFilterList();
|
|
}
|
|
}
|
|
}
|
|
|
|
|