big update

This commit is contained in:
Daniel Ledda
2023-07-26 22:40:44 +02:00
parent d0af60f7f4
commit 70cc228bcb
23 changed files with 4068 additions and 8812 deletions

View File

@@ -3,9 +3,11 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Ledda's Room Climate</title> <title>Ledda's Room Climate</title>
<link type="text/css" href="/styles.css" rel="stylesheet" />
<script type="application/javascript" src="/dashboard.js"></script>
<link rel="shortcut icon" type="image/jpg" href="/favicon64.png"/> <link rel="shortcut icon" type="image/jpg" href="/favicon64.png"/>
<script type="module" crossorigin src="/assets/index-58d7c2d1.js"></script>
<link rel="stylesheet" href="/assets/index-25859e9b.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -47,12 +47,29 @@ html, body {
} }
.hidden { .hidden {
opacity: 0; display: none;
z-index: -1; }
.toggle {
margin-bottom: 20px;
}
.timeseries-val {
white-space: nowrap;
display: flex;
justify-content: end;
width: 100%;
}
.timeseries-val span {
margin-left: 20px;
font-weight: bold;
text-align: right;
} }
h1 { h1 {
display: block; display: block;
text-align: center;
font-family: 'Roboto Slab', serif; font-family: 'Roboto Slab', serif;
font-weight: normal; font-weight: normal;
color: var(--accent-dark); color: var(--accent-dark);
@@ -272,4 +289,4 @@ h1 {
} }
.button:active { .button:active {
filter: brightness(0.9); filter: brightness(0.9);
} }

13
dashboard/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Ledda's Room Climate</title>
<link type="text/css" href="./assets/styles.css" rel="stylesheet" />
<script type="module" src="./src/main.ts"></script>
<link rel="shortcut icon" type="image/jpg" href="/favicon64.png"/>
</head>
<body>
<div id="root"></div>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -11,22 +11,7 @@
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": {
"@types/react": "^17.0.3",
"@webpack-cli/init": "^1.0.3",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"css-loader": "^5.0.1",
"style-loader": "^2.0.0",
"file-loader": "^6.0.0",
"terser-webpack-plugin": "^5.0.3",
"ts-loader": "^8.0.18",
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0"
},
"dependencies": { "dependencies": {
"@types/chart.js": "^2.9.27", "vite": "4.4.7"
"chart.js": "^2.9.4",
"webpack-dev-server": "^3.11.0"
} }
} }

View File

@@ -60,12 +60,12 @@ function createChildren(children: any[]): Node[] {
if (Array.isArray(child)) { if (Array.isArray(child)) {
childrenNodes.push(...createChildren(child)); childrenNodes.push(...createChildren(child));
} }
else if (typeof child === "string") {
childrenNodes.push(document.createTextNode(String(child)));
}
else if (child instanceof Node) { else if (child instanceof Node) {
childrenNodes.push(child); childrenNodes.push(child);
} }
else {
childrenNodes.push(document.createTextNode(String(child)));
}
} }
return childrenNodes; return childrenNodes;
} }

View File

@@ -68,7 +68,7 @@ function newDefaultState(): AppState {
}; };
} }
class AppStateStore { class AppStateStore {
private readonly subscriptions: IAppStateSubscriptions; private readonly subscriptions: IAppStateSubscriptions;
private readonly eventCallbacks: EventCallbackListing<keyof EventCallback>; private readonly eventCallbacks: EventCallbackListing<keyof EventCallback>;
private readonly state: AppState; private readonly state: AppState;
@@ -87,7 +87,7 @@ class AppStateStore {
} }
async init() { async init() {
await this.updateTimeseriesFromSettings(); await this.updateTimeseriesFromSettings(true);
await this.getNewTimeseriesData(); await this.getNewTimeseriesData();
this.emit("ready"); this.emit("ready");
} }
@@ -121,7 +121,7 @@ class AppStateStore {
} }
} }
private async updateTimeseriesFromSettings() { private async updateTimeseriesFromSettings(initial?: boolean) {
let start: number; let start: number;
let stop: number; let stop: number;
if (this.state.displayMode === "window") { if (this.state.displayMode === "window") {
@@ -131,6 +131,9 @@ class AppStateStore {
start = this.state.lastUpdateTime - this.state.minutesDisplayed * 60; start = this.state.lastUpdateTime - this.state.minutesDisplayed * 60;
stop = this.state.lastUpdateTime; stop = this.state.lastUpdateTime;
} }
if (initial) {
start -= stop - start;
}
const allTimeseries = this.state.leftTimeseries.concat(this.state.rightTimeseries); const allTimeseries = this.state.leftTimeseries.concat(this.state.rightTimeseries);
const allHistoriesComplete = !allTimeseries.some(timeseries => !timeseries.historyIsComplete()); const allHistoriesComplete = !allTimeseries.some(timeseries => !timeseries.historyIsComplete());
if (start < this.getExtrema().minIndex && allHistoriesComplete) { if (start < this.getExtrema().minIndex && allHistoriesComplete) {
@@ -424,4 +427,6 @@ export function getAppState() {
} else { } else {
throw new AppStateError("Store not yet initialised!"); throw new AppStateError("Store not yet initialised!");
} }
} }
export type { AppStateStore };

View File

@@ -110,6 +110,10 @@ class Timeseries {
} }
} }
current() {
return this.cache[this.currentEndPointer - 2] ?? null;
}
async updateFromWindow(start: number, stop: number) { async updateFromWindow(start: number, stop: number) {
if (!this.fetching) { if (!this.fetching) {
try { try {
@@ -245,4 +249,4 @@ class Timeseries {
} }
} }
export default Timeseries; export default Timeseries;

View File

@@ -50,4 +50,4 @@ async function loadClimateTimeseriesData(dataType: "temp" | "humidity" | "co2",
const message = "timeseries data couldn't be loaded from the server"; const message = "timeseries data couldn't be loaded from the server";
throw new ClayPIDashboardError(`${message}: ${e}`, message); throw new ClayPIDashboardError(`${message}: ${e}`, message);
} }
} }

View File

@@ -2,5 +2,5 @@
"development": false, "development": false,
"defaultMinuteSpan": 60, "defaultMinuteSpan": 60,
"reloadIntervalSec": 30, "reloadIntervalSec": 30,
"dataEndpoint": "/climate/api" "dataEndpoint": "http://home.djledda.de:4040/climate/api"
} }

View File

@@ -1,5 +1,5 @@
import config from "./config.json"; import config from "./config.json";
import {AppStore, getAppState, initStore} from "./StateStore"; import {AppStore, type AppStateStore, getAppState, initStore} from "./StateStore";
import AppUI from "./ui-components/AppUI"; import AppUI from "./ui-components/AppUI";
import { import {
newCo2Timeseries, newCo2Timeseries,
@@ -36,7 +36,6 @@ document.onreadystatechange = async () => {
await init(); await init();
AppStore().setDocumentReady(true); AppStore().setDocumentReady(true);
AppStore().on("stateChange", () => debounce(() => updateUrlState())()); AppStore().on("stateChange", () => debounce(() => updateUrlState())());
// @ts-ignore (window as unknown as { store: AppStateStore }).store = AppStore();
window.store = AppStore();
document.onreadystatechange = null; document.onreadystatechange = null;
}; };

View File

@@ -23,10 +23,12 @@ class AppUI extends UIComponent {
private grid: HTMLDivElement = document.createElement("div"); private grid: HTMLDivElement = document.createElement("div");
private messageOverlay: MessageOverlay = new MessageOverlay(); private messageOverlay: MessageOverlay = new MessageOverlay();
private helpModal: HelpModal = new HelpModal(); private helpModal: HelpModal = new HelpModal();
private nowView: HTMLElement;
constructor() { constructor() {
super(); super();
this.setupGrid({width: 5, height: 10}); this.setupGrid({width: 5, height: 10});
this.nowView = <this.Now />;
this.element.append( this.element.append(
<img <img
alt={"Help"} alt={"Help"}
@@ -34,11 +36,41 @@ class AppUI extends UIComponent {
className={"help-button button"} className={"help-button button"}
onclick={() => AppStore().showHelp()}/>, onclick={() => AppStore().showHelp()}/>,
<h1>Ledda's Room Climate</h1>, <h1>Ledda's Room Climate</h1>,
this.grid,
this.messageOverlay.current(), this.messageOverlay.current(),
<this.Toggle />,
this.nowView,
this.grid,
this.helpModal.current(), this.helpModal.current(),
); );
this.grid.classList.add("hidden");
this.element.className = "center"; this.element.className = "center";
AppStore().on("timeseriesUpdated", () => {
const old = this.nowView;
this.nowView = <this.Now/>;
old.replaceWith(this.nowView);
});
}
private Now = () => {
const state = AppStore().getState();
return <div>
{state.leftTimeseries.concat(state.rightTimeseries).map(timeseries => {
const val = timeseries.current() ?? "-";
return <div className={"timeseries-val"}>{timeseries.getName()}: <span className={"timeseries-val"}>{val}</span></div>;
})}
</div>;
}
private Toggle = () => {
return <div className={"toggle"}>
<button onclick={() => {
this.grid.classList.toggle("hidden");
this.nowView.classList.toggle("hidden");
this.chartWidget.updateDimensions();
}}>
Toggle Dashboard
</button>
</div>;
} }
private setupGrid(size: GridSize) { private setupGrid(size: GridSize) {
@@ -86,4 +118,4 @@ class AppUI extends UIComponent {
} }
} }
export default AppUI; export default AppUI;

View File

@@ -129,4 +129,4 @@ class ClimateChartWidget extends UIComponent {
} }
} }
export default ClimateChartWidget; export default ClimateChartWidget;

View File

@@ -5,19 +5,19 @@ import UIComponent from "./UIComponent";
class DisplayModeWidget extends UIComponent { class DisplayModeWidget extends UIComponent {
private skeleton: GridWidget; private skeleton: GridWidget;
private minsCounterRef: number; private minsCounterRef: HTMLElement;
private windowStartTimeRef: number; private windowStartTimeRef: HTMLElement;
private windowStartTimeInputRef: number; private windowStartTimeInputRef: HTMLInputElement;
private windowStopTimeRef: number; private windowStopTimeRef: HTMLElement;
private windowStopTimeInputRef: number; private windowStopTimeInputRef: HTMLInputElement;
private windowedDisplayRef: number; private windowedDisplayRef: HTMLElement;
private minsDisplayRef: number; private minsDisplayRef: HTMLElement;
private mainDisplay: HTMLElement; private mainDisplay: HTMLElement;
private minsInputRef: number; private minsInputRef: HTMLInputElement;
constructor(gridProps: GridProps) { constructor(gridProps: GridProps) {
super(); super();
this.mainDisplay = this.MainDisplay({ctx: this}); this.mainDisplay = <this.MainDisplay />;
this.skeleton = new GridWidget({ this.skeleton = new GridWidget({
...gridProps, ...gridProps,
title: "Displaying:", title: "Displaying:",
@@ -30,45 +30,45 @@ class DisplayModeWidget extends UIComponent {
this.updateDisplay(); this.updateDisplay();
} }
private WindowStartTime({ ctx }: {ctx: DisplayModeWidget}) { private WindowStartTime = () => {
ctx.windowStartTimeInputRef = ctx.makeRef(<input this.windowStartTimeInputRef = <input
type={"datetime-local"} type={"datetime-local"}
onblur={() => ctx.onWindowStartInputBlur()} onblur={() => this.onWindowStartInputBlur()}
/>); /> as HTMLInputElement;
ctx.windowStartTimeRef = ctx.makeRef(<div this.windowStartTimeRef = <div
className={"display-mode-widget-date"} className={"display-mode-widget-date"}
onwheel={(e: WheelEvent) => ctx.onStartTimeInputScroll(e)} onwheel={(e: WheelEvent) => this.onStartTimeInputScroll(e)}
onclick={() => ctx.onWindowStartDisplayClick()}> onclick={() => this.onWindowStartDisplayClick()}>
{new Date(getAppState().displayWindow.start + getAppState().utcOffset * 60 * 60 * 1000).toLocaleString()} {new Date(getAppState().displayWindow.start + getAppState().utcOffset * 60 * 60 * 1000).toLocaleString()}
</div>); </div>;
return ctx.fromRef(ctx.windowStartTimeRef); return this.windowStartTimeRef;
} }
private WindowStopTime({ctx}: {ctx: DisplayModeWidget}) { private WindowStopTime = () => {
ctx.windowStopTimeInputRef = ctx.makeRef(<input this.windowStopTimeInputRef = <input
value={new Date()} value={new Date()}
type={"datetime-local"} type={"datetime-local"}
onblur={() => ctx.onWindowStopInputBlur()} onblur={() => this.onWindowStopInputBlur()}
/>); /> as HTMLInputElement;
ctx.windowStopTimeRef = ctx.makeRef(<div this.windowStopTimeRef = <div
className={"display-mode-widget-date"} className={"display-mode-widget-date"}
onwheel={(e: WheelEvent) => ctx.onStopTimeInputScroll(e)} onwheel={(e: WheelEvent) => this.onStopTimeInputScroll(e)}
onclick={() => ctx.onWindowStopDisplayClick()}> onclick={() => this.onWindowStopDisplayClick()}>
{new Date(getAppState().displayWindow.stop + getAppState().utcOffset * 60 * 60 * 1000).toLocaleString()} {new Date(getAppState().displayWindow.stop + getAppState().utcOffset * 60 * 60 * 1000).toLocaleString()}
</div>); </div>;
return ctx.fromRef(ctx.windowStopTimeRef); return this.windowStopTimeRef;
} }
private MinutesCounter({ctx, onclick}: {ctx: DisplayModeWidget, onclick: () => any}) { private MinutesCounter = ({ onclick }: {onclick: () => any }) => {
ctx.minsInputRef = ctx.makeRef( this.minsInputRef =
<input <input
value={getAppState().minutesDisplayed.toString()} value={getAppState().minutesDisplayed.toString()}
onblur={(e: FocusEvent) => ctx.onMinutesCounterInputBlur(e)}/>); onblur={(e: FocusEvent) => this.onMinutesCounterInputBlur(e)}/> as HTMLInputElement;
ctx.minsCounterRef = ctx.makeRef( this.minsCounterRef =
<div className={"min-count"} onclick={onclick} onwheel={(e: WheelEvent) => ctx.onMinutesCounterInputScroll(e)}> <div className={"min-count"} onclick={onclick} onwheel={(e: WheelEvent) => this.onMinutesCounterInputScroll(e)}>
{getAppState().minutesDisplayed.toString()} {getAppState().minutesDisplayed.toString()}
</div>); </div>;
return ctx.fromRef(ctx.minsCounterRef); return this.minsCounterRef;
} }
private onMinutesCounterInputScroll(e: WheelEvent) { private onMinutesCounterInputScroll(e: WheelEvent) {
@@ -94,18 +94,18 @@ class DisplayModeWidget extends UIComponent {
} else { } else {
(e.target as HTMLInputElement).value = getAppState().minutesDisplayed.toString(); (e.target as HTMLInputElement).value = getAppState().minutesDisplayed.toString();
} }
this.fromRef(this.minsInputRef).replaceWith(this.fromRef(this.minsCounterRef)); this.minsInputRef.replaceWith(this.minsCounterRef);
} }
private MinutesDisplay({ctx}: {ctx: DisplayModeWidget}) { private MinutesDisplay = () => {
return (<div className={"display-mode-widget-mins"}> return (<div className={"display-mode-widget-mins"}>
<div>Last</div> <div>Last</div>
<ctx.MinusButton onclick={() => { <this.MinusButton onclick={() => {
const mins = AppStore().getState().minutesDisplayed; const mins = AppStore().getState().minutesDisplayed;
AppStore().setMinutesDisplayed(mins - 1); AppStore().setMinutesDisplayed(mins - 1);
}}/> }}/>
<ctx.MinutesCounter ctx={ctx} onclick={() => ctx.onMinutesCounterClick()}/> <this.MinutesCounter onclick={() => this.onMinutesCounterClick()}/>
<ctx.PlusButton onclick={() => { <this.PlusButton onclick={() => {
const mins = AppStore().getState().minutesDisplayed; const mins = AppStore().getState().minutesDisplayed;
AppStore().setMinutesDisplayed(mins + 1); AppStore().setMinutesDisplayed(mins + 1);
}}/> }}/>
@@ -114,17 +114,17 @@ class DisplayModeWidget extends UIComponent {
} }
private onMinutesCounterClick() { private onMinutesCounterClick() {
const input = this.fromRef(this.minsInputRef) as HTMLInputElement; const input = this.minsInputRef;
this.fromRef(this.minsCounterRef).replaceWith(input); this.minsCounterRef.replaceWith(input);
input.focus(); input.focus();
input.selectionStart = 0; input.selectionStart = 0;
input.selectionEnd = input.value.length; input.selectionEnd = input.value.length;
} }
private onWindowStopDisplayClick() { private onWindowStopDisplayClick() {
const stopTimeDisplay = this.fromRef(this.windowStopTimeRef); const stopTimeDisplay = this.windowStopTimeRef;
(stopTimeDisplay as HTMLInputElement).valueAsDate = new Date(getAppState().displayWindow.stop); const stopTimeInputDisplay = this.windowStopTimeInputRef;
const stopTimeInputDisplay = this.fromRef(this.windowStopTimeInputRef) as HTMLInputElement; stopTimeInputDisplay.valueAsDate = new Date(getAppState().displayWindow.stop);
stopTimeDisplay.replaceWith(stopTimeInputDisplay); stopTimeDisplay.replaceWith(stopTimeInputDisplay);
const date = new Date(getAppState().displayWindow.stop * 1000 + getAppState().utcOffset * 60 * 60 * 1000); const date = new Date(getAppState().displayWindow.stop * 1000 + getAppState().utcOffset * 60 * 60 * 1000);
stopTimeInputDisplay.value = `${date.toLocaleDateString()}, ${date.toLocaleTimeString()}`; stopTimeInputDisplay.value = `${date.toLocaleDateString()}, ${date.toLocaleTimeString()}`;
@@ -132,7 +132,7 @@ class DisplayModeWidget extends UIComponent {
} }
private onWindowStopInputBlur() { private onWindowStopInputBlur() {
const stopTimeInput = this.fromRef(this.windowStopTimeInputRef); const stopTimeInput = this.windowStopTimeInputRef;
const val = new Date((stopTimeInput as HTMLInputElement).value).getTime() / 1000; const val = new Date((stopTimeInput as HTMLInputElement).value).getTime() / 1000;
if (!isNaN(val)) { if (!isNaN(val)) {
AppStore().setDisplayWindow({ AppStore().setDisplayWindow({
@@ -140,13 +140,13 @@ class DisplayModeWidget extends UIComponent {
stop: val stop: val
}); });
} }
stopTimeInput.replaceWith(this.fromRef(this.windowStopTimeRef)); stopTimeInput.replaceWith(this.windowStopTimeRef);
} }
private onWindowStartDisplayClick() { private onWindowStartDisplayClick() {
const startTimeDisplay = this.fromRef(this.windowStartTimeRef); const startTimeDisplay = this.windowStartTimeRef;
(startTimeDisplay as HTMLInputElement).valueAsDate = new Date(getAppState().displayWindow.start); const startTimeInputDisplay = this.windowStartTimeInputRef;
const startTimeInputDisplay = this.fromRef(this.windowStartTimeInputRef) as HTMLInputElement; startTimeInputDisplay.valueAsDate = new Date(getAppState().displayWindow.start);
startTimeDisplay.replaceWith(startTimeInputDisplay); startTimeDisplay.replaceWith(startTimeInputDisplay);
const date = new Date(getAppState().displayWindow.start * 1000 + getAppState().utcOffset * 60 * 60 * 1000); const date = new Date(getAppState().displayWindow.start * 1000 + getAppState().utcOffset * 60 * 60 * 1000);
startTimeInputDisplay.value = `${date.toLocaleDateString()}, ${date.toLocaleTimeString()}`; startTimeInputDisplay.value = `${date.toLocaleDateString()}, ${date.toLocaleTimeString()}`;
@@ -155,7 +155,7 @@ class DisplayModeWidget extends UIComponent {
} }
private onWindowStartInputBlur() { private onWindowStartInputBlur() {
const startTimeInput = this.fromRef(this.windowStartTimeInputRef); const startTimeInput = this.windowStartTimeInputRef;
const val = new Date((startTimeInput as HTMLInputElement).value).getTime() / 1000; const val = new Date((startTimeInput as HTMLInputElement).value).getTime() / 1000;
if (!isNaN(val)) { if (!isNaN(val)) {
AppStore().setDisplayWindow({ AppStore().setDisplayWindow({
@@ -163,56 +163,56 @@ class DisplayModeWidget extends UIComponent {
stop: getAppState().displayWindow.stop, stop: getAppState().displayWindow.stop,
}); });
} }
startTimeInput.replaceWith(this.fromRef(this.windowStartTimeRef)); startTimeInput.replaceWith(this.windowStartTimeRef);
} }
private MinusButton(props: {onclick: () => any}) { private MinusButton(props: {onclick: () => void}) {
return <div return <div
className={"minus-button"} className={"minus-button"}
onclick={props.onclick} onclick={props.onclick}
/>; />;
} }
private PlusButton(props: {onclick: () => any}) { private PlusButton(props: {onclick: () => void}) {
return <div return <div
className={"plus-button"} className={"plus-button"}
onclick={props.onclick} onclick={props.onclick}
/>; />;
} }
private WindowedDisplay({ctx}: {ctx: DisplayModeWidget}) { private WindowedDisplay = () => {
return (<div> return (<div>
<div>From</div> <div>From</div>
<ctx.MinusButton onclick={() => { <this.MinusButton onclick={() => {
const displayWindow = AppStore().getState().displayWindow; const displayWindow = AppStore().getState().displayWindow;
AppStore().setDisplayWindow({start: displayWindow.start - 60, stop: displayWindow.stop}); AppStore().setDisplayWindow({start: displayWindow.start - 60, stop: displayWindow.stop});
}}/> }}/>
<ctx.WindowStartTime ctx={ctx}/> <this.WindowStartTime this={this}/>
<ctx.PlusButton onclick={() => { <this.PlusButton onclick={() => {
const displayWindow = AppStore().getState().displayWindow; const displayWindow = AppStore().getState().displayWindow;
AppStore().setDisplayWindow({start: displayWindow.start + 60, stop: displayWindow.stop}); AppStore().setDisplayWindow({start: displayWindow.start + 60, stop: displayWindow.stop});
}}/> }}/>
<div>to</div> <div>to</div>
<ctx.MinusButton onclick={() => { <this.MinusButton onclick={() => {
const displayWindow = AppStore().getState().displayWindow; const displayWindow = AppStore().getState().displayWindow;
AppStore().setDisplayWindow({start: displayWindow.start, stop: displayWindow.stop - 60}); AppStore().setDisplayWindow({start: displayWindow.start, stop: displayWindow.stop - 60});
}}/> }}/>
<ctx.WindowStopTime ctx={ctx}/> <this.WindowStopTime this={this}/>
<ctx.PlusButton onclick={() => { <this.PlusButton onclick={() => {
const displayWindow = AppStore().getState().displayWindow; const displayWindow = AppStore().getState().displayWindow;
AppStore().setDisplayWindow({start: displayWindow.start, stop: displayWindow.stop + 60}); AppStore().setDisplayWindow({start: displayWindow.start, stop: displayWindow.stop + 60});
}}/> }}/>
</div>); </div>);
} }
private MainDisplay({ ctx }: { ctx: DisplayModeWidget }) { private MainDisplay = () => {
const windowMode = getAppState().displayMode === "window"; const windowMode = getAppState().displayMode === "window";
ctx.windowedDisplayRef = ctx.makeRef(<ctx.WindowedDisplay ctx={ctx}/>); this.windowedDisplayRef = <this.WindowedDisplay />;
ctx.minsDisplayRef = ctx.makeRef(<ctx.MinutesDisplay ctx={ctx}/>); this.minsDisplayRef = <this.MinutesDisplay />;
return <div className={"display-mode-widget"}> return <div className={"display-mode-widget"}>
{windowMode {windowMode
? ctx.fromRef(ctx.windowedDisplayRef) ? this.windowedDisplayRef
: ctx.fromRef(ctx.minsDisplayRef)} : this.minsDisplayRef}
</div> as HTMLElement; </div> as HTMLElement;
} }
@@ -222,16 +222,16 @@ class DisplayModeWidget extends UIComponent {
private updateDisplay() { private updateDisplay() {
if (getAppState().displayMode === "window") { if (getAppState().displayMode === "window") {
this.mainDisplay.children.item(0).replaceWith(this.fromRef(this.windowedDisplayRef)); this.mainDisplay.children.item(0).replaceWith(this.windowedDisplayRef);
const offset = getAppState().utcOffset * 60 * 60; const offset = getAppState().utcOffset * 60 * 60;
const startDate = new Date((getAppState().displayWindow.start + offset) * 1000); const startDate = new Date((getAppState().displayWindow.start + offset) * 1000);
const stopDate = new Date((getAppState().displayWindow.stop + offset) * 1000); const stopDate = new Date((getAppState().displayWindow.stop + offset) * 1000);
this.fromRef(this.windowStartTimeRef).innerText = startDate.toLocaleString(); this.windowStartTimeRef.innerText = startDate.toLocaleString();
this.fromRef(this.windowStopTimeRef).innerText = stopDate.toLocaleString(); this.windowStopTimeRef.innerText = stopDate.toLocaleString();
} else { } else {
this.mainDisplay.children.item(0).replaceWith(this.fromRef(this.minsDisplayRef)); this.mainDisplay.children.item(0).replaceWith(this.minsDisplayRef);
this.fromRef(this.minsCounterRef).innerText = getAppState().minutesDisplayed.toString(); this.minsCounterRef.innerText = getAppState().minutesDisplayed.toString();
(this.fromRef(this.minsInputRef) as HTMLInputElement).value = getAppState().minutesDisplayed.toString(); this.minsInputRef.value = getAppState().minutesDisplayed.toString();
} }
} }
@@ -240,4 +240,4 @@ class DisplayModeWidget extends UIComponent {
} }
} }
export default DisplayModeWidget; export default DisplayModeWidget;

View File

@@ -7,11 +7,11 @@ import Timeseries from "../Timeseries";
class LegendWidget extends UIComponent { class LegendWidget extends UIComponent {
private skeleton: GridWidget; private skeleton: GridWidget;
private display: HTMLSpanElement = document.createElement("span"); private display: HTMLSpanElement = document.createElement("span");
private bodyRef: number; private bodyRef: HTMLElement;
constructor(gridProps: GridProps) { constructor(gridProps: GridProps) {
super(); super();
this.display = <this.MainBody ctx={this}/>; this.display = <this.MainBody />;
this.skeleton = new GridWidget({ this.skeleton = new GridWidget({
...gridProps, ...gridProps,
title: "Legend:", title: "Legend:",
@@ -23,29 +23,29 @@ class LegendWidget extends UIComponent {
} }
private updateDisplay() { private updateDisplay() {
this.fromRef(this.bodyRef).replaceWith(<this.MainBody ctx={this}/>); this.bodyRef.replaceWith(<this.MainBody />);
} }
private MainBody({ctx}: {ctx: LegendWidget}) { private MainBody = () => {
ctx.bodyRef = ctx.makeRef(<div><ctx.TimeseriesList ctx={ctx}/></div>); this.bodyRef = <div><this.TimeseriesList /></div>;
return ctx.fromRef(ctx.bodyRef); return this.bodyRef;
} }
private TimeseriesList({ctx}: { ctx: LegendWidget }) { private TimeseriesList = () => {
const highlightedTimeseries = getAppState().highlightedTimeseries; const highlightedTimeseries = getAppState().highlightedTimeseries;
return <ul> return <ul>
{ ...getAppState().rightTimeseries.map(timeseries => { ...getAppState().rightTimeseries.map(timeseries =>
<ctx.TimeseriesLegendEntry <this.TimeseriesLegendEntry
timeseries={timeseries} timeseries={timeseries}
highlighted={timeseries.getName() === highlightedTimeseries}/>) } highlighted={timeseries.getName() === highlightedTimeseries}/>) }
{ ...getAppState().leftTimeseries.map(timeseries => { ...getAppState().leftTimeseries.map(timeseries =>
<ctx.TimeseriesLegendEntry <this.TimeseriesLegendEntry
timeseries={timeseries} timeseries={timeseries}
highlighted={timeseries.getName() === highlightedTimeseries}/>) } highlighted={timeseries.getName() === highlightedTimeseries}/>) }
</ul>; </ul>;
} }
private TimeseriesLegendEntry({timeseries, highlighted}: {timeseries: Timeseries, highlighted: boolean}) { private TimeseriesLegendEntry = ({timeseries, highlighted}: {timeseries: Timeseries, highlighted: boolean}) => {
const option = new Option(); const option = new Option();
option.style.color = timeseries.getColour(); option.style.color = timeseries.getColour();
return <li return <li
@@ -62,4 +62,4 @@ class LegendWidget extends UIComponent {
} }
} }
export default LegendWidget; export default LegendWidget;

View File

@@ -17,7 +17,7 @@ class MessageOverlay extends UIComponent {
private build() { private build() {
this.element = document.createElement("div"); this.element = document.createElement("div");
this.element.classList.add("overlay", "center"); this.element.classList.add("overlay");
this.textElement = document.createElement("span"); this.textElement = document.createElement("span");
this.textElement.innerText = ""; this.textElement.innerText = "";
this.element.appendChild(this.textElement); this.element.appendChild(this.textElement);
@@ -60,4 +60,4 @@ class MessageOverlay extends UIComponent {
} }
} }
export default MessageOverlay; export default MessageOverlay;

View File

@@ -6,13 +6,14 @@ import {AppStore, DisplayMode, getAppState} from "../StateStore";
export default class SelectDisplayModeWidget extends UIComponent { export default class SelectDisplayModeWidget extends UIComponent {
private mainBody: HTMLElement; private mainBody: HTMLElement;
private gridWidgetSkeleton: GridWidget; private gridWidgetSkeleton: GridWidget;
private windowInputRef: number; private windowInputRef: HTMLInputElement;
private minSpanInputRef: number; private minSpanInputRef: HTMLInputElement;
private windowInputContainerRef: number; private windowInputContainerRef: HTMLElement;
private minSpanInputContainerRef: number; private minSpanInputContainerRef: HTMLElement;
constructor(gridProps: GridProps) { constructor(gridProps: GridProps) {
super(); super();
this.mainBody = this.MainBody({ctx: this}); this.mainBody = <this.MainBody />;
this.gridWidgetSkeleton = new GridWidget({ this.gridWidgetSkeleton = new GridWidget({
...gridProps, ...gridProps,
title: "Display Mode:", title: "Display Mode:",
@@ -27,44 +28,44 @@ export default class SelectDisplayModeWidget extends UIComponent {
private update() { private update() {
const windowedMode = getAppState().displayMode === "window"; const windowedMode = getAppState().displayMode === "window";
(this.fromRef(this.windowInputRef) as HTMLInputElement).checked = windowedMode; this.windowInputRef.checked = windowedMode;
(this.fromRef(this.minSpanInputRef) as HTMLInputElement).checked = !windowedMode; this.minSpanInputRef.checked = !windowedMode;
if (!windowedMode) { if (!windowedMode) {
this.fromRef(this.minSpanInputContainerRef).classList.add("selected"); this.minSpanInputContainerRef.classList.add("selected");
this.fromRef(this.windowInputContainerRef).classList.remove("selected"); this.windowInputContainerRef.classList.remove("selected");
} else { } else {
this.fromRef(this.minSpanInputContainerRef).classList.remove("selected"); this.minSpanInputContainerRef.classList.remove("selected");
this.fromRef(this.windowInputContainerRef).classList.add("selected"); this.windowInputContainerRef.classList.add("selected");
} }
} }
private MainBody({ ctx }: { ctx: SelectDisplayModeWidget }) { private MainBody = () => {
const isInWindowMode = getAppState().displayMode === "window"; const isInWindowMode = getAppState().displayMode === "window";
ctx.windowInputRef = this.makeRef(<input this.windowInputRef = <input
type={"radio"} type={"radio"}
id={"window"} id={"window"}
name={"display-mode"} name={"display-mode"}
checked={isInWindowMode}/>); checked={isInWindowMode}/> as HTMLInputElement;
ctx.minSpanInputRef = this.makeRef(<input this.minSpanInputRef = <input
type={"radio"} type={"radio"}
id={"min-span"} id={"min-span"}
name={"display-mode"} name={"display-mode"}
checked={!isInWindowMode}/>); checked={!isInWindowMode}/> as HTMLInputElement;
ctx.windowInputContainerRef = this.makeRef(<div this.windowInputContainerRef = <div
className={`display-mode-option${isInWindowMode ? " selected" : ""}`} className={`display-mode-option${isInWindowMode ? " selected" : ""}`}
onclick={() => ctx.selectMode("window")}> onclick={() => this.selectMode("window")}>
{this.fromRef(ctx.windowInputRef)} {this.windowInputRef}
<label htmlFor={"window"}>Time Window</label> <label htmlFor={"window"}>Time Window</label>
</div>); </div>;
ctx.minSpanInputContainerRef = this.makeRef(<div this.minSpanInputContainerRef = <div
className={`display-mode-option${!isInWindowMode ? " selected" : ""}`} className={`display-mode-option${!isInWindowMode ? " selected" : ""}`}
onclick={() => ctx.selectMode("pastMins")}> onclick={() => this.selectMode("pastMins")}>
{this.fromRef(ctx.minSpanInputRef)} {this.minSpanInputRef}
<label htmlFor={"minSpan"}>Rolling Minute Span</label> <label htmlFor={"minSpan"}>Rolling Minute Span</label>
</div>); </div>;
return (<div> return (<div>
{this.fromRef(ctx.windowInputContainerRef)} {this.windowInputContainerRef}
{this.fromRef(ctx.minSpanInputContainerRef)} {this.minSpanInputContainerRef}
</div>); </div>);
} }

View File

@@ -7,12 +7,12 @@ class TimerWidget extends UIComponent {
private readonly display: HTMLElement; private readonly display: HTMLElement;
private skeleton: GridWidget; private skeleton: GridWidget;
private nextUpdateTime: number; private nextUpdateTime: number;
private timerRef: number; private timerRef: HTMLElement;
private lastUpdateRef: number; private lastUpdateRef: HTMLElement;
constructor(gridProps: GridProps) { constructor(gridProps: GridProps) {
super(); super();
this.display = <this.MainDisplay ctx={this}/>; this.display = <this.MainDisplay />;
this.skeleton = new GridWidget({ this.skeleton = new GridWidget({
...gridProps, ...gridProps,
className: "timer-widget", className: "timer-widget",
@@ -32,20 +32,20 @@ class TimerWidget extends UIComponent {
} }
private updateUpdateText() { private updateUpdateText() {
this.fromRef(this.lastUpdateRef).innerText = new Date(getAppState().lastUpdateTime * 1000 + getAppState().utcOffset * 60 * 60 * 1000).toLocaleString(); this.lastUpdateRef.innerText = new Date(getAppState().lastUpdateTime * 1000 + getAppState().utcOffset * 60 * 60 * 1000).toLocaleString();
} }
private MainDisplay({ ctx }: { ctx: TimerWidget }) { private MainDisplay = () => {
ctx.timerRef = ctx.makeRef(<div className={"countdown"}/>); this.timerRef = <div className={"countdown"}/>;
ctx.lastUpdateRef = ctx.makeRef( this.lastUpdateRef =
<span className={"last-update"}> <span className={"last-update"}>
{new Date(getAppState().lastUpdateTime).toLocaleString()} {new Date(getAppState().lastUpdateTime).toLocaleString()}
</span>); </span>;
return (<div> return (<div>
{ctx.fromRef(ctx.timerRef)} {this.timerRef}
<div> <div>
<div className={"last-update"}>Last update was at:</div> <div className={"last-update"}>Last update was at:</div>
<div>{ctx.fromRef(ctx.lastUpdateRef)}</div> <div>{this.lastUpdateRef}</div>
</div> </div>
</div>); </div>);
} }
@@ -53,9 +53,9 @@ class TimerWidget extends UIComponent {
private refreshTimer() { private refreshTimer() {
const now = new Date().getTime() / 1000; const now = new Date().getTime() / 1000;
if (now <= this.nextUpdateTime) { if (now <= this.nextUpdateTime) {
this.fromRef(this.timerRef).innerText = `${(this.nextUpdateTime - now).toFixed(2)}s`; this.timerRef.innerText = `${(this.nextUpdateTime - now).toFixed(2)}s`;
} else { } else {
this.fromRef(this.timerRef).innerText = "0.00s"; this.timerRef.innerText = "0.00s";
} }
} }
@@ -64,4 +64,4 @@ class TimerWidget extends UIComponent {
} }
} }
export default TimerWidget; export default TimerWidget;

View File

@@ -6,12 +6,12 @@ import * as JSX from "../JSXFactory";
class TimezoneWidget extends UIComponent { class TimezoneWidget extends UIComponent {
private skeleton: GridWidget; private skeleton: GridWidget;
private display: HTMLSpanElement = document.createElement("span"); private display: HTMLSpanElement = document.createElement("span");
private timezoneInputRef: number; private timezoneInputRef: HTMLInputElement;
private timezoneDisplayRef: number; private timezoneDisplayRef: HTMLElement;
constructor(gridProps: GridProps) { constructor(gridProps: GridProps) {
super(); super();
this.display = <this.MainBody ctx={this}/>; this.display = <this.MainBody />;
this.skeleton = new GridWidget({ this.skeleton = new GridWidget({
...gridProps, ...gridProps,
title: "Displayed Timezone:", title: "Displayed Timezone:",
@@ -23,39 +23,39 @@ class TimezoneWidget extends UIComponent {
private updateDisplay() { private updateDisplay() {
const offset = AppStore().getState().utcOffset; const offset = AppStore().getState().utcOffset;
this.fromRef(this.timezoneDisplayRef).innerText = `${offset > 0 ? "+" : ""} ${Math.abs(offset)}`; this.timezoneDisplayRef.innerText = `${offset > 0 ? "+" : ""} ${Math.abs(offset)}`;
(this.fromRef(this.timezoneInputRef) as HTMLInputElement).value = `${offset > 0 ? "" : "-"}${Math.abs(offset)}`; (this.timezoneInputRef as HTMLInputElement).value = `${offset > 0 ? "" : "-"}${Math.abs(offset)}`;
} }
private MainBody({ctx}: {ctx: TimezoneWidget}) { private MainBody = () => {
return <div return <div
className={"timezone-widget"} className={"timezone-widget"}
onclick={() => ctx.onTimezoneClick()}> onclick={() => this.onTimezoneClick()}>
<span>UTC </span> <span>UTC </span>
<ctx.TimezoneDisplay ctx={ctx} /> <this.TimezoneDisplay />
<span>:00</span> <span>:00</span>
</div>; </div>;
} }
private TimezoneDisplay({ctx}: {ctx: TimezoneWidget}) { private TimezoneDisplay = () => {
ctx.timezoneDisplayRef = ctx.makeRef(<span/>); this.timezoneDisplayRef = <span/>;
ctx.timezoneInputRef = ctx.makeRef(<input this.timezoneInputRef = <input
type={"text"} type={"text"}
onblur={() => ctx.onTimezoneInputBlur()}/>); onblur={() => this.onTimezoneInputBlur()}/> as HTMLInputElement;
return ctx.fromRef(ctx.timezoneDisplayRef); return this.timezoneDisplayRef;
} }
private onTimezoneInputBlur() { private onTimezoneInputBlur() {
const input = this.fromRef(this.timezoneInputRef) as HTMLInputElement; const input = this.timezoneInputRef;
const display = this.fromRef(this.timezoneDisplayRef); const display = this.timezoneDisplayRef;
AppStore().setUtcOffset(Number(input.value)); AppStore().setUtcOffset(Number(input.value));
input.replaceWith(display); input.replaceWith(display);
this.updateDisplay(); this.updateDisplay();
} }
private onTimezoneClick() { private onTimezoneClick() {
const input = this.fromRef(this.timezoneInputRef) as HTMLInputElement; const input = this.timezoneInputRef as HTMLInputElement;
this.fromRef(this.timezoneDisplayRef).replaceWith(input); this.timezoneDisplayRef.replaceWith(input);
input.focus(); input.focus();
input.selectionStart = 0; input.selectionStart = 0;
input.selectionEnd = input.value.length; input.selectionEnd = input.value.length;
@@ -66,4 +66,4 @@ class TimezoneWidget extends UIComponent {
} }
} }
export default TimezoneWidget; export default TimezoneWidget;

View File

@@ -1,22 +1,11 @@
export default abstract class UIComponent { export default abstract class UIComponent {
public readonly id: number; public readonly id: number;
private static componentCount = 0; private static componentCount = 0;
private static reffedComponentCount = 0;
private static readonly reffedComponents: HTMLElement[] = [];
protected constructor() { protected constructor() {
this.id = UIComponent.componentCount; this.id = UIComponent.componentCount;
UIComponent.componentCount++; UIComponent.componentCount++;
} }
protected makeRef(el: HTMLElement | DocumentFragment): number {
UIComponent.reffedComponents.push(el as HTMLElement);
return UIComponent.reffedComponentCount++;
}
protected fromRef(ref: number): HTMLElement | null {
return UIComponent.reffedComponents[ref] ?? null;
}
abstract current(): HTMLElement; abstract current(): HTMLElement;
} }

7
dashboard/vite.config.ts Normal file
View File

@@ -0,0 +1,7 @@
import {defineConfig} from "vite";
export default defineConfig({
build: {
outDir: '../app-dist/static',
},
});

2871
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@
"concurrently": "^6.0.0", "concurrently": "^6.0.0",
"eslint": "^7.21.0", "eslint": "^7.21.0",
"prettier": "^2.1.2", "prettier": "^2.1.2",
"typescript": "^4.2.3" "typescript": "^5.1.0"
}, },
"author": "Daniel Ledda", "author": "Daniel Ledda",
"license": "ISC" "license": "ISC"