stats: add unfinished query builder

not gonna go any further. This is an unnecessary feature which could
just be a wiki page (and it will).
This commit is contained in:
Harvey Tindall
2025-11-23 16:41:10 +00:00
parent faf782458f
commit 788952d0a6
6 changed files with 155 additions and 47 deletions

View File

@@ -566,11 +566,11 @@
<div class="card ~neutral @low flex flex-col gap-2 flex-1">
<div class="flex flex-row gap-2">
<label class="w-1/2">
<input type="radio" name="duration" class="unfocused" id="radio-inv-duration" checked>
<input type="radio" name="radio-duration" class="unfocused" checked>
<span class="button ~neutral @high supra full-width center">{{ .strings.inviteDuration }}</span>
</label>
<label class="w-1/2">
<input type="radio" name="duration" class="unfocused" id="radio-user-expiry">
<input type="radio" name="radio-duration" class="unfocused">
<span class="button ~neutral @low supra full-width center">{{ .strings.userExpiry }}</span>
</label>
</div>
@@ -900,7 +900,52 @@
</div>
</div>
<div id="tab-statistics" class="flex flex-col gap-4 unfocused">
<div class="card @low dark:~d_neutral mb-4" id="statistics-container">
<div class="card @low dark:~d_neutral">
<div class="card @low dark:~d_neutral flex flex-col gap-2">
<div class="flex flex-row gap-2">
<label class="w-full">
<input type="radio" name="statistics-query-type" class="hidden" id="radio-statistics-accounts" checked>
<span class="button ~neutral w-full center @high">{{ .strings.accounts }}</span>
</label>
<label class="w-full">
<input type="radio" name="statistics-query-type" class="hidden" id="radio-statistics-activity">
<span class="button ~neutral w-full center @low">{{ .strings.activity }}</span>
</label>
</div>
<div id="statistics-query-tab-accounts">
<div class="flex flex-col align-middle gap-2">
<div class="flex flex-row align-middle w-full gap-2">
<input type="search" class="field ~neutral @low input search mr-2" placeholder="{{ .strings.query }}">
<span class="button ~neutral @low center inside-input rounded-s-none statistics-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></span>
<button class="button ~info @low statistics-query-execute" aria-label="{{ .strings.run }}"><i class="ri-refresh-line"></i></button>
</div>
<div class="flex flex-row gap-2 flex-wrap">
<div class="statistics-sort-by-field"></div>
<span class="flex flex-row gap-2 flex-wrap statistics-filter-area"></span>
</div>
<div class="card ~neutral @low statistics-filter-list">
<p class="supra pb-2">{{ .strings.filters }}</p>
</div>
</div>
</div>
<div id="statistics-query-tab-activity">
<div class="flex flex-col align-middle gap-2">
<div class="flex flex-row align-middle w-full gap-2">
<input type="search" class="field ~neutral @low input search mr-2" placeholder="{{ .strings.query }}">
<span class="button ~neutral @low center inside-input rounded-s-none statistics-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></span>
<button class="button ~info @low statistics-query-execute" aria-label="{{ .strings.run }}"><i class="ri-refresh-line"></i></button>
</div>
<div class="flex flex-row gap-2 flex-wrap">
<div class="statistics-sort-by-field"></div>
<span class="flex flex-row gap-2 flex-wrap statistics-filter-area"></span>
</div>
<div class="card ~neutral @low statistics-filter-list">
<p class="supra pb-2">{{ .strings.filters }}</p>
</div>
</div>
</div>
</div>
<div id="statistics-container"></div>
</div>
</div>
<div id="tab-settings" class="flex flex-col gap-4 unfocused">

View File

@@ -57,6 +57,8 @@
"unlink": "Unlink Account",
"deleted": "Deleted",
"disabled": "Disabled",
"query": "Query",
"run": "Run",
"sendPWR": "Send Password Reset",
"noResultsFound": "No Results Found",
"noResultsFoundLocally": "Only loaded records were searched. You can load more, or perform the search over all records on the server.",

View File

@@ -1,6 +1,7 @@
import { _get, _post, _delete, toClipboard, toggleLoader, toDateString } from "../modules/common.js";
import { DiscordUser, newDiscordSearch } from "../modules/discord.js";
import { reloadProfileNames } from "../modules/profiles.js";
import { SimpleRadioTabs } from "./ui.js";
declare var window: GlobalWindow;
@@ -604,10 +605,10 @@ export class createInvite {
private _userHours = document.getElementById("user-hours") as HTMLSelectElement;
private _userMinutes = document.getElementById("user-minutes") as HTMLSelectElement;
private _invDurationButton = document.getElementById('radio-inv-duration') as HTMLInputElement;
private _userExpiryButton = document.getElementById('radio-user-expiry') as HTMLInputElement;
private _invDuration = document.getElementById('inv-duration');
private _userExpiry = document.getElementById('user-expiry');
private _durationTabs = new SimpleRadioTabs(
[document.getElementById("inv-duration"), document.getElementById("user-expiry")],
"radio-duration"
);
private _sendToDiscord: (passData: string) => void;
@@ -849,30 +850,8 @@ export class createInvite {
this.uses = 1;
this.label = "";
const checkDuration = () => {
const invSpan = this._invDurationButton.nextElementSibling as HTMLSpanElement;
const userSpan = this._userExpiryButton.nextElementSibling as HTMLSpanElement;
if (this._invDurationButton.checked) {
this._invDuration.classList.remove("unfocused");
this._userExpiry.classList.add("unfocused");
invSpan.classList.add("@high");
invSpan.classList.remove("@low");
userSpan.classList.add("@low");
userSpan.classList.remove("@high");
} else if (this._userExpiryButton.checked) {
this._userExpiry.classList.remove("unfocused");
this._invDuration.classList.add("unfocused");
invSpan.classList.add("@low");
invSpan.classList.remove("@high");
userSpan.classList.add("@high");
userSpan.classList.remove("@low");
}
};
this._userExpiryButton.checked = false;
this._invDurationButton.checked = true;
this._userExpiryButton.onchange = checkDuration;
this._invDurationButton.onchange = checkDuration;
// Select the first tab by default (inv duration)
this._durationTabs.select(0);
this._days.onchange = this._checkDurationValidity;
this._months.onchange = this._checkDurationValidity;

View File

@@ -33,17 +33,21 @@ export interface QueryType {
}
export interface SearchConfiguration {
filterArea: HTMLElement;
sortingByButton?: HTMLButtonElement;
searchOptionsHeader: HTMLElement;
notFoundPanel: HTMLElement;
notFoundLocallyText: HTMLElement;
notFoundCallback?: (notFound: boolean) => void;
filterList: HTMLElement;
clearSearchButtonSelector: string;
serverSearchButtonSelector: string;
search: HTMLInputElement;
queries: { [field: string]: QueryType };
filterArea: HTMLElement;
filterList: HTMLElement;
search: HTMLInputElement;
clearSearchButtonSelector?: string;
serverSearchButtonSelector?: string;
// Optionally supply a button for a single type of sorting.
sortingByButton?: HTMLButtonElement;
searchOptionsHeader?: HTMLElement;
notFoundPanel?: HTMLElement;
notFoundLocallyText?: HTMLElement;
notFoundCallback?: (notFound: boolean) => void;
// Function for showing/hiding search items.
setVisibility: (items: string[], visible: boolean, appendedItems: boolean) => void;
onSearchCallback: (newItems: boolean, loadAll: boolean, callback?: (resp: paginatedDTO) => void) => void;
searchServer: (params: PaginatedReqDTO, newSearch: boolean) => void;
@@ -521,9 +525,9 @@ export class Search {
if (this._c.sortingByButton) sortingBy = !(this._c.sortingByButton.classList.contains("hidden"));
const hasFilters = this._c.filterArea.textContent != "";
if (sortingBy || hasFilters) {
this._c.searchOptionsHeader.classList.remove("hidden");
this._c.searchOptionsHeader?.classList.remove("hidden");
} else {
this._c.searchOptionsHeader.classList.add("hidden");
this._c.searchOptionsHeader?.classList.add("hidden");
}
}
@@ -576,14 +580,14 @@ export class Search {
setNotFoundPanelVisibility = (visible: boolean) => {
if (this._inServerSearch || !this.inSearch) {
this._c.notFoundLocallyText.classList.add("unfocused");
this._c.notFoundLocallyText?.classList.add("unfocused");
} else if (this.inSearch) {
this._c.notFoundLocallyText.classList.remove("unfocused");
this._c.notFoundLocallyText?.classList.remove("unfocused");
}
if (visible) {
this._c.notFoundPanel.classList.remove("unfocused");
this._c.notFoundPanel?.classList.remove("unfocused");
} else {
this._c.notFoundPanel.classList.add("unfocused");
this._c.notFoundPanel?.classList.add("unfocused");
}
}

View File

@@ -13,6 +13,8 @@ import {
TransformComponent
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import { AccountsQueries } from "./accounts";
import { SimpleRadioTabs } from "./ui";
echarts.use([
BarChart,
@@ -328,5 +330,32 @@ export class StatsPanel {
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();
}
}
}

View File

@@ -100,3 +100,52 @@ myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};*/
// Simple radio tabs, when each "button" is a label containing an input[type="radio"] and some element[class="button"].
export class SimpleRadioTabs {
tabs: Array<HTMLElement>;
radios: Array<HTMLInputElement>;
// Pass nothing, or a list of the tab container elements and either the "name" field used on the radios, of a list of them.
constructor(tabs?: Array<HTMLElement>, radios?: Array<HTMLInputElement>|string) {
this.tabs = tabs || new Array<HTMLElement>();
if (radios) {
if (typeof radios === "string") {
this.radios = Array.from(document.querySelectorAll(`input[name=${radios}]`)) as Array<HTMLInputElement>;
} else {
this.radios = radios as Array<HTMLInputElement>;
}
this.radios.forEach((radio) => { radio.onchange = this.onChange });
}
}
onChange = () => {
for (let i = 0; i < this.radios.length; i++) {
const buttonEl = this.radios[i].nextElementSibling;
if (this.radios[i].checked) {
buttonEl.classList.add("@high");
buttonEl.classList.remove("@low");
this.tabs[i].classList.remove("unfocused");
} else {
buttonEl.classList.add("@low");
buttonEl.classList.remove("@high");
this.tabs[i].classList.add("unfocused");
}
}
};
select(i: number) {
for (let j = 0; j < this.radios.length; j++) {
this.radios[j].checked = i == j;
}
this.onChange();
}
push(tab: HTMLElement, radio: HTMLInputElement) {
this.tabs.push(tab);
radio.onchange = this.onChange;
this.radios.push(radio);
}
}