Reformat everything
This commit is contained in:
parent
93663fff8c
commit
36ce75b43d
12 changed files with 560 additions and 526 deletions
|
|
@ -116,28 +116,28 @@ ordering for both display modes, so this is also relevant to the normal mode.
|
|||
// https://sashamaps.net/docs/resources/20-colors/
|
||||
// Related: https://en.wikipedia.org/wiki/Help:Distinguishable_colors
|
||||
const COLORS = [
|
||||
"#e6194B", // Red
|
||||
"#3cb44b", // Green
|
||||
"#ffe119", // Yellow
|
||||
"#4363d8", // Blue
|
||||
"#f58231", // Orange
|
||||
// "#911eb4", // Purple
|
||||
"#42d4f4", // Cyan
|
||||
"#f032e6", // Magenta
|
||||
// "#bfef45", // Lime
|
||||
// "#fabed4", // Pink
|
||||
"#469990", // Teal
|
||||
// "#dcbeff", // Lavender
|
||||
"#9A6324", // Brown
|
||||
// "#fffac8", // Beige
|
||||
"#800000", // Maroon
|
||||
// "#aaffc3", // Mint
|
||||
// "#808000", // Olive
|
||||
// "#ffd8b1", // Apricot
|
||||
"#000075", // Navy
|
||||
"#a9a9a9", // Grey
|
||||
// "#ffffff", // White
|
||||
"#000000", // Black
|
||||
"#e6194B", // Red
|
||||
"#3cb44b", // Green
|
||||
"#ffe119", // Yellow
|
||||
"#4363d8", // Blue
|
||||
"#f58231", // Orange
|
||||
// "#911eb4", // Purple
|
||||
"#42d4f4", // Cyan
|
||||
"#f032e6", // Magenta
|
||||
// "#bfef45", // Lime
|
||||
// "#fabed4", // Pink
|
||||
"#469990", // Teal
|
||||
// "#dcbeff", // Lavender
|
||||
"#9A6324", // Brown
|
||||
// "#fffac8", // Beige
|
||||
"#800000", // Maroon
|
||||
// "#aaffc3", // Mint
|
||||
// "#808000", // Olive
|
||||
// "#ffd8b1", // Apricot
|
||||
"#000075", // Navy
|
||||
"#a9a9a9", // Grey
|
||||
// "#ffffff", // White
|
||||
"#000000", // Black
|
||||
];
|
||||
|
||||
// Initialization
|
||||
|
|
|
|||
|
|
@ -2,156 +2,161 @@ import { CommitsResponse } from "./requests.js";
|
|||
import { SECONDS_PER_DAY } from "./util.js";
|
||||
|
||||
type Commit = {
|
||||
indexByHash: number;
|
||||
indexByGraph: number;
|
||||
hash: string;
|
||||
parents: Commit[];
|
||||
children: Commit[];
|
||||
author: string;
|
||||
committerDate: number;
|
||||
summary: string;
|
||||
indexByHash: number;
|
||||
indexByGraph: number;
|
||||
hash: string;
|
||||
parents: Commit[];
|
||||
children: Commit[];
|
||||
author: string;
|
||||
committerDate: number;
|
||||
summary: string;
|
||||
};
|
||||
|
||||
export class Commits {
|
||||
#graphId: number | null = null;
|
||||
#commitsByGraph: Commit[] = [];
|
||||
#committerDatesNormal: Date[] = [];
|
||||
#committerDatesDayEquidistant: Date[] = [];
|
||||
#graphId: number | null = null;
|
||||
#commitsByGraph: Commit[] = [];
|
||||
#committerDatesNormal: Date[] = [];
|
||||
#committerDatesDayEquidistant: Date[] = [];
|
||||
|
||||
requiresUpdate(graphId: number): boolean {
|
||||
return this.#graphId === null || this.#graphId < graphId;
|
||||
requiresUpdate(graphId: number): boolean {
|
||||
return this.#graphId === null || this.#graphId < graphId;
|
||||
}
|
||||
|
||||
update(response: CommitsResponse) {
|
||||
console.assert(response.hashByHash.length == response.authorByHash.length);
|
||||
console.assert(
|
||||
response.hashByHash.length == response.committerDateByHash.length,
|
||||
);
|
||||
console.assert(response.hashByHash.length == response.summaryByHash.length);
|
||||
|
||||
let commits = this.#loadCommits(response);
|
||||
commits = this.#sortCommitsTopologically(commits);
|
||||
this.#sortCommitsByCommitterDate(commits);
|
||||
|
||||
// Fill in indexes - "later" is now
|
||||
for (const [idx, commit] of commits.entries()) {
|
||||
commit.indexByGraph = idx;
|
||||
}
|
||||
|
||||
update(response: CommitsResponse) {
|
||||
console.assert(response.hashByHash.length == response.authorByHash.length);
|
||||
console.assert(response.hashByHash.length == response.committerDateByHash.length);
|
||||
console.assert(response.hashByHash.length == response.summaryByHash.length);
|
||||
const committerDatesNormal = commits.map((c) => c.committerDate);
|
||||
const committerDatesDayEquidistant =
|
||||
this.#makeDayEquidistant(committerDatesNormal);
|
||||
|
||||
let commits = this.#loadCommits(response);
|
||||
commits = this.#sortCommitsTopologically(commits);
|
||||
this.#sortCommitsByCommitterDate(commits);
|
||||
// To prevent exceptions and other weirdness from messing up our state,
|
||||
// we update everything in one go.
|
||||
this.#graphId = response.graphId;
|
||||
this.#commitsByGraph = commits;
|
||||
this.#committerDatesNormal = this.#epochTimesToDates(committerDatesNormal);
|
||||
this.#committerDatesDayEquidistant = this.#epochTimesToDates(
|
||||
committerDatesDayEquidistant,
|
||||
);
|
||||
}
|
||||
|
||||
// Fill in indexes - "later" is now
|
||||
for (const [idx, commit] of commits.entries()) {
|
||||
commit.indexByGraph = idx;
|
||||
#loadCommits(response: CommitsResponse): Commit[] {
|
||||
const commits = new Map<string, Commit>();
|
||||
const commitsByHash = [];
|
||||
|
||||
for (const [idx, hash] of response.hashByHash.entries()) {
|
||||
const commit = {
|
||||
indexByHash: idx,
|
||||
indexByGraph: NaN, // Filled in later
|
||||
hash,
|
||||
parents: [],
|
||||
children: [],
|
||||
author: response.authorByHash[idx]!,
|
||||
committerDate: response.committerDateByHash[idx]!,
|
||||
summary: response.summaryByHash[idx]!,
|
||||
};
|
||||
commits.set(hash, commit);
|
||||
commitsByHash.push(commit);
|
||||
}
|
||||
|
||||
// Fill in parents and children
|
||||
for (const [childIdx, parentIdx] of response.childParentIndexPairs) {
|
||||
const childHash = response.hashByHash[childIdx]!;
|
||||
const parentHash = response.hashByHash[parentIdx]!;
|
||||
|
||||
const child = commits.get(childHash)!;
|
||||
const parent = commits.get(parentHash)!;
|
||||
|
||||
child.parents.push(parent);
|
||||
parent.children.push(child);
|
||||
}
|
||||
|
||||
return commitsByHash;
|
||||
}
|
||||
|
||||
#sortCommitsByCommitterDate(commits: Commit[]) {
|
||||
commits.sort((a, b) => a.committerDate - b.committerDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort commits topologically such that parents come before their children.
|
||||
*
|
||||
* Assumes that there are no duplicated commits anywhere.
|
||||
*
|
||||
* A reverse post-order DFS is a topological sort, so that is what this
|
||||
* function implements.
|
||||
*/
|
||||
#sortCommitsTopologically(commits: Commit[]): Commit[] {
|
||||
const visited: Set<string> = new Set();
|
||||
const visiting: Commit[] = commits.filter((c) => c.parents.length == 0);
|
||||
|
||||
const sorted: Commit[] = [];
|
||||
|
||||
while (visiting.length > 0) {
|
||||
const commit = visiting.at(-1)!;
|
||||
if (visited.has(commit.hash)) {
|
||||
visiting.pop();
|
||||
sorted.push(commit);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const child of commit.children) {
|
||||
if (!visited.has(child.hash)) {
|
||||
visiting.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
const committerDatesNormal = commits.map(c => c.committerDate);
|
||||
const committerDatesDayEquidistant = this.#makeDayEquidistant(committerDatesNormal);
|
||||
|
||||
// To prevent exceptions and other weirdness from messing up our state,
|
||||
// we update everything in one go.
|
||||
this.#graphId = response.graphId;
|
||||
this.#commitsByGraph = commits;
|
||||
this.#committerDatesNormal = this.#epochTimesToDates(committerDatesNormal);
|
||||
this.#committerDatesDayEquidistant = this.#epochTimesToDates(committerDatesDayEquidistant);
|
||||
visited.add(commit.hash);
|
||||
}
|
||||
|
||||
#loadCommits(response: CommitsResponse): Commit[] {
|
||||
const commits = new Map<string, Commit>();
|
||||
const commitsByHash = [];
|
||||
sorted.reverse();
|
||||
|
||||
for (const [idx, hash] of response.hashByHash.entries()) {
|
||||
const commit = {
|
||||
indexByHash: idx,
|
||||
indexByGraph: NaN, // Filled in later
|
||||
hash,
|
||||
parents: [],
|
||||
children: [],
|
||||
author: response.authorByHash[idx]!,
|
||||
committerDate: response.committerDateByHash[idx]!,
|
||||
summary: response.summaryByHash[idx]!,
|
||||
};
|
||||
commits.set(hash, commit);
|
||||
commitsByHash.push(commit);
|
||||
}
|
||||
console.assert(visited.size === commits.length);
|
||||
console.assert(visiting.length === 0);
|
||||
console.assert(sorted.length === commits.length);
|
||||
return sorted;
|
||||
}
|
||||
|
||||
// Fill in parents and children
|
||||
for (const [childIdx, parentIdx] of response.childParentIndexPairs) {
|
||||
const childHash = response.hashByHash[childIdx]!;
|
||||
const parentHash = response.hashByHash[parentIdx]!;
|
||||
|
||||
const child = commits.get(childHash)!;
|
||||
const parent = commits.get(parentHash)!;
|
||||
|
||||
child.parents.push(parent);
|
||||
parent.children.push(child);
|
||||
}
|
||||
|
||||
return commitsByHash;
|
||||
/**
|
||||
* Assumes the times are sorted.
|
||||
*/
|
||||
#makeDayEquidistant(times: number[]): number[] {
|
||||
const days: { day: number; amount: number }[] = [];
|
||||
for (const time of times) {
|
||||
const day = time % SECONDS_PER_DAY;
|
||||
const prev = days.at(-1);
|
||||
if (prev === undefined || prev.day !== day) {
|
||||
days.push({ day, amount: 1 });
|
||||
} else {
|
||||
prev.amount++;
|
||||
}
|
||||
}
|
||||
|
||||
#sortCommitsByCommitterDate(commits: Commit[]) {
|
||||
commits.sort((a, b) => a.committerDate - b.committerDate);
|
||||
const result: number[] = [];
|
||||
for (const day of days) {
|
||||
const secondsPerCommit = SECONDS_PER_DAY / day.amount;
|
||||
for (let i = 0; i < day.amount; i++) {
|
||||
const time = day.day * SECONDS_PER_DAY + secondsPerCommit * (i + 0.5);
|
||||
result.push(time);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort commits topologically such that parents come before their children.
|
||||
*
|
||||
* Assumes that there are no duplicated commits anywhere.
|
||||
*
|
||||
* A reverse post-order DFS is a topological sort, so that is what this
|
||||
* function implements.
|
||||
*/
|
||||
#sortCommitsTopologically(commits: Commit[]): Commit[] {
|
||||
const visited: Set<string> = new Set();
|
||||
const visiting: Commit[] = commits.filter(c => c.parents.length == 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
const sorted: Commit[] = [];
|
||||
|
||||
while (visiting.length > 0) {
|
||||
const commit = visiting.at(-1)!;
|
||||
if (visited.has(commit.hash)) {
|
||||
visiting.pop();
|
||||
sorted.push(commit);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const child of commit.children) {
|
||||
if (!visited.has(child.hash)) {
|
||||
visiting.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
visited.add(commit.hash);
|
||||
}
|
||||
|
||||
sorted.reverse();
|
||||
|
||||
console.assert(visited.size === commits.length);
|
||||
console.assert(visiting.length === 0);
|
||||
console.assert(sorted.length === commits.length);
|
||||
return sorted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assumes the times are sorted.
|
||||
*/
|
||||
#makeDayEquidistant(times: number[]): number[] {
|
||||
const days: { day: number, amount: number; }[] = [];
|
||||
for (const time of times) {
|
||||
const day = time % SECONDS_PER_DAY;
|
||||
const prev = days.at(-1);
|
||||
if (prev === undefined || prev.day !== day) {
|
||||
days.push({ day, amount: 1 });
|
||||
} else {
|
||||
prev.amount++;
|
||||
}
|
||||
}
|
||||
|
||||
const result: number[] = [];
|
||||
for (const day of days) {
|
||||
const secondsPerCommit = SECONDS_PER_DAY / day.amount;
|
||||
for (let i = 0; i < day.amount; i++) {
|
||||
const time = day.day * SECONDS_PER_DAY + secondsPerCommit * (i + 0.5);
|
||||
result.push(time);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#epochTimesToDates(times: number[]): Date[] {
|
||||
return times.map(t => new Date(1000 * t));
|
||||
}
|
||||
#epochTimesToDates(times: number[]): Date[] {
|
||||
return times.map((t) => new Date(1000 * t));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,110 +2,126 @@ import { MetricsResponse } from "./requests.js";
|
|||
import { el } from "./util.js";
|
||||
|
||||
class Folder {
|
||||
metric: string | null = null;
|
||||
children: Map<string, Folder> = new Map();
|
||||
metric: string | null = null;
|
||||
children: Map<string, Folder> = new Map();
|
||||
|
||||
getOrCreateChild(name: string): Folder {
|
||||
let child = this.children.get(name);
|
||||
if (child === undefined) {
|
||||
child = new Folder();
|
||||
this.children.set(name, child);
|
||||
}
|
||||
return child;
|
||||
getOrCreateChild(name: string): Folder {
|
||||
let child = this.children.get(name);
|
||||
if (child === undefined) {
|
||||
child = new Folder();
|
||||
this.children.set(name, child);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
add(metric: string) {
|
||||
let current: Folder = this;
|
||||
for (let segment of metric.split("/")) {
|
||||
current = current.getOrCreateChild(segment);
|
||||
}
|
||||
current.metric = metric;
|
||||
add(metric: string) {
|
||||
let current: Folder = this;
|
||||
for (let segment of metric.split("/")) {
|
||||
current = current.getOrCreateChild(segment);
|
||||
}
|
||||
current.metric = metric;
|
||||
}
|
||||
|
||||
toHtmlElement(name: string): HTMLElement {
|
||||
if (this.children.size > 0) { // Folder
|
||||
name = `${name}/`;
|
||||
if (this.metric === null) { // Folder without metric
|
||||
return el("details", { "class": "no-metric" },
|
||||
el("summary", {}, name),
|
||||
this.childrenToHtmlElements(),
|
||||
);
|
||||
} else { // Folder with metric
|
||||
return el("details", {},
|
||||
el("summary", {},
|
||||
el("input", { "type": "checkbox", "name": this.metric }),
|
||||
" ", name,
|
||||
),
|
||||
this.childrenToHtmlElements(),
|
||||
);
|
||||
}
|
||||
} else if (this.metric !== null) { // Normal metric
|
||||
return el("label", {},
|
||||
el("input", { "type": "checkbox", "name": this.metric }),
|
||||
" ", name,
|
||||
);
|
||||
} else { // Metric without metric, should never happen
|
||||
return el("label", {}, name);
|
||||
}
|
||||
toHtmlElement(name: string): HTMLElement {
|
||||
if (this.children.size > 0) {
|
||||
// Folder
|
||||
name = `${name}/`;
|
||||
if (this.metric === null) {
|
||||
// Folder without metric
|
||||
return el(
|
||||
"details",
|
||||
{ class: "no-metric" },
|
||||
el("summary", {}, name),
|
||||
this.childrenToHtmlElements(),
|
||||
);
|
||||
} else {
|
||||
// Folder with metric
|
||||
return el(
|
||||
"details",
|
||||
{},
|
||||
el(
|
||||
"summary",
|
||||
{},
|
||||
el("input", { type: "checkbox", name: this.metric }),
|
||||
" ",
|
||||
name,
|
||||
),
|
||||
this.childrenToHtmlElements(),
|
||||
);
|
||||
}
|
||||
} else if (this.metric !== null) {
|
||||
// Normal metric
|
||||
return el(
|
||||
"label",
|
||||
{},
|
||||
el("input", { type: "checkbox", name: this.metric }),
|
||||
" ",
|
||||
name,
|
||||
);
|
||||
} else {
|
||||
// Metric without metric, should never happen
|
||||
return el("label", {}, name);
|
||||
}
|
||||
}
|
||||
|
||||
childrenToHtmlElements(): HTMLElement {
|
||||
let result: HTMLElement = el("ul", {});
|
||||
for (let [name, folder] of this.children.entries()) {
|
||||
result.append(el("li", {}, folder.toHtmlElement(name)));
|
||||
}
|
||||
return result;
|
||||
childrenToHtmlElements(): HTMLElement {
|
||||
let result: HTMLElement = el("ul", {});
|
||||
for (let [name, folder] of this.children.entries()) {
|
||||
result.append(el("li", {}, folder.toHtmlElement(name)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export class Metrics {
|
||||
#div: HTMLElement;
|
||||
#dataId: number | null = null;
|
||||
#div: HTMLElement;
|
||||
#dataId: number | null = null;
|
||||
|
||||
constructor(div: HTMLElement) {
|
||||
this.#div = div;
|
||||
constructor(div: HTMLElement) {
|
||||
this.#div = div;
|
||||
}
|
||||
|
||||
getSelected(): Set<string> {
|
||||
const selected = new Set<string>();
|
||||
|
||||
const checkedInputs =
|
||||
this.#div.querySelectorAll<HTMLInputElement>("input:checked");
|
||||
for (const input of checkedInputs) {
|
||||
selected.add(input.name);
|
||||
}
|
||||
|
||||
getSelected(): Set<string> {
|
||||
const selected = new Set<string>();
|
||||
return selected;
|
||||
}
|
||||
|
||||
const checkedInputs = this.#div.querySelectorAll<HTMLInputElement>("input:checked");
|
||||
for (const input of checkedInputs) {
|
||||
selected.add(input.name);
|
||||
}
|
||||
requiresUpdate(dataId: number): boolean {
|
||||
// At the moment, updating the metrics results in all <details> tags
|
||||
// closing again. To prevent this (as it can be frustrating if you've
|
||||
// navigated deep into the metrics hierarchy), we never require updates
|
||||
// after the initial update.
|
||||
return this.#dataId === null;
|
||||
// return this.#dataId === null || this.#dataId < dataId;
|
||||
}
|
||||
|
||||
return selected;
|
||||
update(response: MetricsResponse) {
|
||||
const selected = this.getSelected();
|
||||
|
||||
const folder = new Folder();
|
||||
for (const metric of response.metrics) {
|
||||
folder.add(metric);
|
||||
}
|
||||
|
||||
requiresUpdate(dataId: number): boolean {
|
||||
// At the moment, updating the metrics results in all <details> tags
|
||||
// closing again. To prevent this (as it can be frustrating if you've
|
||||
// navigated deep into the metrics hierarchy), we never require updates
|
||||
// after the initial update.
|
||||
return this.#dataId === null;
|
||||
// return this.#dataId === null || this.#dataId < dataId;
|
||||
this.#div.textContent = ""; // Remove children
|
||||
if (folder.children.size == 0) {
|
||||
this.#div.append("There aren't yet any metrics");
|
||||
} else {
|
||||
this.#div.append(folder.childrenToHtmlElements());
|
||||
}
|
||||
|
||||
update(response: MetricsResponse) {
|
||||
const selected = this.getSelected();
|
||||
|
||||
const folder = new Folder();
|
||||
for (const metric of response.metrics) {
|
||||
folder.add(metric);
|
||||
}
|
||||
|
||||
this.#div.textContent = ""; // Remove children
|
||||
if (folder.children.size == 0) {
|
||||
this.#div.append("There aren't yet any metrics");
|
||||
} else {
|
||||
this.#div.append(folder.childrenToHtmlElements());
|
||||
}
|
||||
|
||||
const inputs = this.#div.querySelectorAll<HTMLInputElement>("input");
|
||||
for (const input of inputs) {
|
||||
input.checked = selected.has(input.name);
|
||||
}
|
||||
|
||||
this.#dataId = response.dataId;
|
||||
const inputs = this.#div.querySelectorAll<HTMLInputElement>("input");
|
||||
for (const input of inputs) {
|
||||
input.checked = selected.has(input.name);
|
||||
}
|
||||
|
||||
this.#dataId = response.dataId;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,46 +2,48 @@
|
|||
* `/graph/metrics` response data.
|
||||
*/
|
||||
export type MetricsResponse = {
|
||||
dataId: number;
|
||||
metrics: string[];
|
||||
dataId: number;
|
||||
metrics: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* `/graph/commits` response data.
|
||||
*/
|
||||
export type CommitsResponse = {
|
||||
graphId: number;
|
||||
hashByHash: string[];
|
||||
authorByHash: string[];
|
||||
committerDateByHash: number[];
|
||||
summaryByHash: string[];
|
||||
childParentIndexPairs: [number, number][];
|
||||
graphId: number;
|
||||
hashByHash: string[];
|
||||
authorByHash: string[];
|
||||
committerDateByHash: number[];
|
||||
summaryByHash: string[];
|
||||
childParentIndexPairs: [number, number][];
|
||||
};
|
||||
|
||||
/**
|
||||
* `/graph/measurements` response data.
|
||||
*/
|
||||
export type MeasurementsResponse = {
|
||||
graphId: number;
|
||||
dataId: number;
|
||||
measurements: { [key: string]: (number | null)[]; };
|
||||
graphId: number;
|
||||
dataId: number;
|
||||
measurements: { [key: string]: (number | null)[] };
|
||||
};
|
||||
|
||||
async function getData<R>(url: string): Promise<R> {
|
||||
const response = await fetch(url);
|
||||
const data: R = await response.json();
|
||||
return data;
|
||||
const response = await fetch(url);
|
||||
const data: R = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getMetrics(): Promise<MetricsResponse> {
|
||||
return getData("metrics");
|
||||
return getData("metrics");
|
||||
}
|
||||
|
||||
export async function getCommits(): Promise<CommitsResponse> {
|
||||
return getData("commits");
|
||||
return getData("commits");
|
||||
}
|
||||
|
||||
export async function getMeasurements(metrics: string[]): Promise<MeasurementsResponse> {
|
||||
const params = new URLSearchParams(metrics.map(m => ["metric", m]));
|
||||
return getData(`measurements?${params}`);
|
||||
export async function getMeasurements(
|
||||
metrics: string[],
|
||||
): Promise<MeasurementsResponse> {
|
||||
const params = new URLSearchParams(metrics.map((m) => ["metric", m]));
|
||||
return getData(`measurements?${params}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,85 +3,85 @@ import { Metrics } from "./metrics.js";
|
|||
import { getCommits, getMetrics } from "./requests.js";
|
||||
|
||||
export class State {
|
||||
#latestGraphId: number = -Infinity;
|
||||
#latestDataId: number = -Infinity;
|
||||
#latestGraphId: number = -Infinity;
|
||||
#latestDataId: number = -Infinity;
|
||||
|
||||
#metrics: Metrics;
|
||||
#commits: Commits = new Commits();
|
||||
#metrics: Metrics;
|
||||
#commits: Commits = new Commits();
|
||||
|
||||
#requestingMetrics: boolean = false;
|
||||
#requestingCommits: boolean = false;
|
||||
#requestingMetrics: boolean = false;
|
||||
#requestingCommits: boolean = false;
|
||||
|
||||
// raw measurements (with graph id and data id)
|
||||
// processed measurements (with graph id and data id)
|
||||
// raw measurements (with graph id and data id)
|
||||
// processed measurements (with graph id and data id)
|
||||
|
||||
constructor(metrics: Metrics) {
|
||||
this.#metrics = metrics;
|
||||
constructor(metrics: Metrics) {
|
||||
this.#metrics = metrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state and plot and request new data if necessary. Tries to match
|
||||
* the user's wishes as closely as possible.
|
||||
*
|
||||
* This function is idempotent.
|
||||
*/
|
||||
update() {
|
||||
// TODO Invalidate and update data
|
||||
// TODO Update graph
|
||||
this.#requestDataWhereNecessary();
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// Requesting and updating data //
|
||||
//////////////////////////////////
|
||||
|
||||
#updateDataId(dataId: number) {
|
||||
if (dataId > this.#latestDataId) {
|
||||
this.#latestDataId = dataId;
|
||||
}
|
||||
}
|
||||
|
||||
#updateGraphId(graphId: number) {
|
||||
if (graphId > this.#latestGraphId) {
|
||||
this.#latestGraphId = graphId;
|
||||
}
|
||||
}
|
||||
|
||||
#requestDataWhereNecessary() {
|
||||
if (this.#metrics.requiresUpdate(this.#latestDataId)) {
|
||||
this.#requestMetrics();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update state and plot and request new data if necessary. Tries to match
|
||||
* the user's wishes as closely as possible.
|
||||
*
|
||||
* This function is idempotent.
|
||||
*/
|
||||
update() {
|
||||
// TODO Invalidate and update data
|
||||
// TODO Update graph
|
||||
this.#requestDataWhereNecessary();
|
||||
if (this.#commits.requiresUpdate(this.#latestGraphId)) {
|
||||
this.#requestCommits();
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////
|
||||
// Requesting and updating data //
|
||||
//////////////////////////////////
|
||||
|
||||
#updateDataId(dataId: number) {
|
||||
if (dataId > this.#latestDataId) {
|
||||
this.#latestDataId = dataId;
|
||||
}
|
||||
async #requestMetrics() {
|
||||
if (this.#requestingMetrics) return;
|
||||
console.log("Requesting metrics");
|
||||
try {
|
||||
this.#requestingMetrics = true;
|
||||
const response = await getMetrics();
|
||||
this.#updateDataId(response.dataId);
|
||||
this.#metrics.update(response);
|
||||
this.update();
|
||||
} finally {
|
||||
this.#requestingMetrics = false;
|
||||
}
|
||||
}
|
||||
|
||||
#updateGraphId(graphId: number) {
|
||||
if (graphId > this.#latestGraphId) {
|
||||
this.#latestGraphId = graphId;
|
||||
}
|
||||
}
|
||||
|
||||
#requestDataWhereNecessary() {
|
||||
if (this.#metrics.requiresUpdate(this.#latestDataId)) {
|
||||
this.#requestMetrics();
|
||||
}
|
||||
|
||||
if (this.#commits.requiresUpdate(this.#latestGraphId)) {
|
||||
this.#requestCommits();
|
||||
}
|
||||
}
|
||||
|
||||
async #requestMetrics() {
|
||||
if (this.#requestingMetrics) return;
|
||||
console.log("Requesting metrics");
|
||||
try {
|
||||
this.#requestingMetrics = true;
|
||||
const response = await getMetrics();
|
||||
this.#updateDataId(response.dataId);
|
||||
this.#metrics.update(response);
|
||||
this.update();
|
||||
} finally {
|
||||
this.#requestingMetrics = false;
|
||||
}
|
||||
}
|
||||
|
||||
async #requestCommits() {
|
||||
if (this.#requestingCommits) return;
|
||||
console.log("Requesting commits");
|
||||
try {
|
||||
this.#requestingCommits = true;
|
||||
const response = await getCommits();
|
||||
this.#updateGraphId(response.graphId);
|
||||
this.#commits.update(response);
|
||||
this.update();
|
||||
} finally {
|
||||
this.#requestingCommits = false;
|
||||
}
|
||||
async #requestCommits() {
|
||||
if (this.#requestingCommits) return;
|
||||
console.log("Requesting commits");
|
||||
try {
|
||||
this.#requestingCommits = true;
|
||||
const response = await getCommits();
|
||||
this.#updateGraphId(response.graphId);
|
||||
this.#commits.update(response);
|
||||
this.update();
|
||||
} finally {
|
||||
this.#requestingCommits = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
/**
|
||||
* Create an {@link HTMLElement}.
|
||||
*/
|
||||
export function el(name: string, attributes: { [key: string]: string; }, ...children: (string | Node)[]) {
|
||||
let element = document.createElement(name);
|
||||
for (let [name, value] of Object.entries(attributes)) {
|
||||
element.setAttribute(name, value);
|
||||
}
|
||||
element.append(...children);
|
||||
return element;
|
||||
export function el(
|
||||
name: string,
|
||||
attributes: { [key: string]: string },
|
||||
...children: (string | Node)[]
|
||||
) {
|
||||
let element = document.createElement(name);
|
||||
for (let [name, value] of Object.entries(attributes)) {
|
||||
element.setAttribute(name, value);
|
||||
}
|
||||
element.append(...children);
|
||||
return element;
|
||||
}
|
||||
|
||||
export const SECONDS_PER_DAY = 24 * 60 * 60;
|
||||
|
|
|
|||
|
|
@ -2,13 +2,16 @@ const INNER = document.getElementById("inner")!;
|
|||
const REFRESH_SECONDS = 10;
|
||||
|
||||
function update() {
|
||||
fetch("inner")
|
||||
.then(response => response.text())
|
||||
.then(text => {
|
||||
INNER.innerHTML = text;
|
||||
let count = document.getElementById("queue")?.dataset["count"]!;
|
||||
document.title = document.title.replace(/^queue \(\S+\)/, `queue (${count})`);
|
||||
});
|
||||
fetch("inner")
|
||||
.then((response) => response.text())
|
||||
.then((text) => {
|
||||
INNER.innerHTML = text;
|
||||
let count = document.getElementById("queue")?.dataset["count"]!;
|
||||
document.title = document.title.replace(
|
||||
/^queue \(\S+\)/,
|
||||
`queue (${count})`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
setInterval(update, REFRESH_SECONDS * 1000);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue