398 lines
11 KiB
JavaScript
398 lines
11 KiB
JavaScript
const { html, render } = lighterhtml;
|
|
|
|
const $container = document.getElementById("sensors-container");
|
|
|
|
const $filters = document.createElement("div");
|
|
$filters.className = "filters";
|
|
$container.appendChild($filters);
|
|
|
|
const $sensorsContainer = document.createElement("div");
|
|
$sensorsContainer.className = "sensors";
|
|
$container.appendChild($sensorsContainer);
|
|
|
|
const $config = document.createElement("div");
|
|
$config.className = "config";
|
|
$container.appendChild($config);
|
|
|
|
let sensorComponents = [];
|
|
|
|
function load() {
|
|
fetch("/api/sensors")
|
|
.then((r) => r.json())
|
|
.then((sensors) => {
|
|
$sensorsContainer.innerHTML = "";
|
|
|
|
sensorComponents = sensors.map((sensor) => createSensor(sensor));
|
|
sensorComponents.forEach((component) =>
|
|
$sensorsContainer.appendChild(component.container)
|
|
);
|
|
});
|
|
}
|
|
|
|
function preventPropagation(e) {
|
|
e.stopPropagation();
|
|
}
|
|
|
|
function hideConfig() {
|
|
renderConfig({ sensor: { config: {} }, shown: false });
|
|
}
|
|
|
|
function showConfigOf(sensor, onSave) {
|
|
renderConfig({ sensor, onSave, shown: true });
|
|
}
|
|
|
|
function renderConfig({ sensor, onSave, shown }) {
|
|
const handleSave = async (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const data = Object.fromEntries(new FormData(e.target));
|
|
|
|
Promise.all(
|
|
Object.entries(data).map(([name, value]) =>
|
|
fetch(`/api/sensors/${sensor.sensor}/config/${name}`, {
|
|
method: "PUT",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ value }),
|
|
})
|
|
)
|
|
).then(() => onSave?.());
|
|
|
|
hideConfig();
|
|
};
|
|
|
|
const config = sensor.config;
|
|
|
|
render(
|
|
$config,
|
|
html`
|
|
<div class="settings-modal${shown ? " show" : ""}" onclick=${hideConfig}>
|
|
<div class="inner" onclick=${preventPropagation}>
|
|
<div class="body">
|
|
<form onsubmit="${handleSave}">
|
|
<div class="input">
|
|
<label>Sensor</label>
|
|
<input value="${sensor.sensor}" disabled />
|
|
</div>
|
|
<div class="input">
|
|
<label>Name</label>
|
|
<input name="name" value="${config.name}" />
|
|
</div>
|
|
<div class="input">
|
|
<label>Type</label>
|
|
<select name="graphType" value="${config.graphType || 'line'}">
|
|
<option value="line">Line</option>
|
|
<option value="points">Points</option>
|
|
<option value="lineAndPoints">Line + Points</option>
|
|
<option value="bar">Bar</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="input">
|
|
<label>Unit</label>
|
|
<input name="unit" value="${config.unit}" />
|
|
</div>
|
|
<div class="input">
|
|
<label>Min value</label>
|
|
<input name="min" value="${config.min}" />
|
|
</div>
|
|
<div class="input">
|
|
<label>Max value</label>
|
|
<input name="max" value="${config.max}" />
|
|
</div>
|
|
|
|
<div class="actions">
|
|
<button class="cancel" onclick=${hideConfig} type="button">Cancel</button>
|
|
<button>Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`
|
|
);
|
|
}
|
|
|
|
const CURRENT_FILTERS = loadFilters();
|
|
|
|
function loadFilters() {
|
|
const params = new URLSearchParams(location.search.replace(/^\?/, ""));
|
|
|
|
return {
|
|
interval: params.get("interval") || "week",
|
|
refresh: "1s",
|
|
customFrom: params.get("from") || new Date().toISOString(),
|
|
customTo: params.get("to") || new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
function saveFilters() {
|
|
const values = {
|
|
interval: CURRENT_FILTERS.interval,
|
|
};
|
|
|
|
if (CURRENT_FILTERS.interval === "custom") {
|
|
values.from = CURRENT_FILTERS.customFrom.toISOString();
|
|
values.to = CURRENT_FILTERS.customTo.toISOString();
|
|
}
|
|
|
|
const url = new URL(location.href);
|
|
const params = new URLSearchParams(values);
|
|
|
|
url.search = params.toString();
|
|
|
|
history.replaceState(null, null, url.toString());
|
|
}
|
|
|
|
function refreshAll() {
|
|
sensorComponents.forEach((c) => c.refreshValues());
|
|
}
|
|
|
|
let lastIntervalSelected = CURRENT_FILTERS.interval;
|
|
|
|
function splitDateTime(v) {
|
|
const d = new Date(v);
|
|
|
|
const date =
|
|
d.getFullYear() +
|
|
"-" +
|
|
(d.getMonth() + 1).toString().padStart(2, "0") +
|
|
"-" +
|
|
d.getDate().toString().padStart(2, "0");
|
|
const time =
|
|
d.getHours().toString().padStart(2, "0") +
|
|
":" +
|
|
d.getMinutes().toString().padStart(2, "0");
|
|
|
|
return [date, time];
|
|
}
|
|
|
|
function renderFilters() {
|
|
const handleApply = async (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const data = Object.fromEntries(new FormData(e.target));
|
|
|
|
CURRENT_FILTERS.interval = data.interval;
|
|
|
|
if (data.interval !== "custom") {
|
|
const [from, to] = getDataInterval(CURRENT_FILTERS.interval);
|
|
CURRENT_FILTERS.customFrom = from.toISOString();
|
|
CURRENT_FILTERS.customTo = to.toISOString();
|
|
} else {
|
|
CURRENT_FILTERS.customFrom = new Date(
|
|
`${data.fromDate} ${data.fromTime}`
|
|
);
|
|
CURRENT_FILTERS.customTo = new Date(`${data.toDate} ${data.toTime}`);
|
|
}
|
|
|
|
refreshAll();
|
|
renderFilters();
|
|
saveFilters();
|
|
};
|
|
|
|
const handleIntervalChange = (e) => {
|
|
lastIntervalSelected = e.target.value;
|
|
|
|
renderFilters();
|
|
};
|
|
|
|
const customFrom = splitDateTime(CURRENT_FILTERS.customFrom);
|
|
const customTo = splitDateTime(CURRENT_FILTERS.customTo);
|
|
const isCustomSelected = lastIntervalSelected === "custom";
|
|
|
|
render(
|
|
$filters,
|
|
html`
|
|
<div class="inner">
|
|
<div class="filter-form">
|
|
<form class="horizontal" onsubmit=${handleApply}>
|
|
<div class="input">
|
|
<label>Interval</label>
|
|
<select
|
|
name="interval"
|
|
value="${lastIntervalSelected}"
|
|
onchange=${handleIntervalChange}
|
|
>
|
|
<option value="hour">Hour</option>
|
|
<option value="day">Day</option>
|
|
<option value="week">Week</option>
|
|
<option value="month">Month</option>
|
|
<option value="custom">Custom</option>
|
|
</select>
|
|
</div>
|
|
|
|
${isCustomSelected
|
|
? html`
|
|
<div class="input date-time">
|
|
<label>From</label>
|
|
<div>
|
|
<input
|
|
type="date"
|
|
value="${customFrom[0]}"
|
|
name="fromDate"
|
|
/>
|
|
<input
|
|
type="time"
|
|
value="${customFrom[1]}"
|
|
name="fromTime"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="input date-time">
|
|
<label>To</label>
|
|
<div>
|
|
<input type="date" value="${customTo[0]}" name="toDate" />
|
|
<input type="time" value="${customTo[1]}" name="toTime" />
|
|
</div>
|
|
</div>
|
|
`
|
|
: undefined}
|
|
|
|
<button>Apply</button>
|
|
</form>
|
|
</div>
|
|
<div class="actions">
|
|
<!--TODO:-->
|
|
<!--<label class="checkbox-label"><input type="checkbox"><span>auto-refresh</span></label>-->
|
|
<button onclick=${refreshAll}>Refresh</button>
|
|
</div>
|
|
</div>
|
|
<div class="shadow"></div>
|
|
`
|
|
);
|
|
}
|
|
|
|
function getDataInterval() {
|
|
switch (CURRENT_FILTERS.interval) {
|
|
case "hour": {
|
|
return [new Date(Date.now() - 3600 * 1000), new Date()];
|
|
}
|
|
case "day": {
|
|
return [new Date(Date.now() - 24 * 3600 * 1000), new Date()];
|
|
}
|
|
case "week": {
|
|
return [new Date(Date.now() - 5 * 24 * 3600 * 1000), new Date()];
|
|
}
|
|
case "month": {
|
|
return [new Date(Date.now() - 30 * 24 * 3600 * 1000), new Date()];
|
|
}
|
|
case "year": {
|
|
return [new Date(Date.now() - 356 * 24 * 3600 * 1000), new Date()];
|
|
}
|
|
case "custom": {
|
|
return [
|
|
new Date(CURRENT_FILTERS.customFrom),
|
|
new Date(CURRENT_FILTERS.customTo),
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
function createSensor(sensor) {
|
|
const container = document.createElement("div");
|
|
container.className = "sensor";
|
|
|
|
const header = document.createElement("div");
|
|
header.className = "header";
|
|
|
|
const body = document.createElement("div");
|
|
body.className = "body";
|
|
|
|
const showSettings = () => {
|
|
showConfigOf(sensor, () => refreshSensorConfig());
|
|
};
|
|
|
|
const renderHeader = () => {
|
|
render(
|
|
header,
|
|
html`
|
|
<div class="name">${sensor.config?.name ?? sensor.sensor}</div>
|
|
<div class="actions">
|
|
<button class="config" onClick=${showSettings}>Config</button>
|
|
<button class="refresh" onClick=${refreshValues}>Refresh</button>
|
|
</div>
|
|
`
|
|
);
|
|
};
|
|
|
|
const renderBody = (range, values) => {
|
|
const { from, to } = range;
|
|
const minValue = parseFloat(sensor.config.min);
|
|
const maxValue = parseFloat(sensor.config.max);
|
|
const customRange = !isNaN(minValue) && !isNaN(maxValue);
|
|
|
|
Plotly.newPlot(
|
|
body,
|
|
[
|
|
{
|
|
...(sensor.config.graphType === 'line' && { type: 'scatter', mode: 'lines' }),
|
|
...(sensor.config.graphType === 'points' && { type: 'scatter', mode: 'markers' }),
|
|
...(sensor.config.graphType === 'lineAndPoints' && { type: 'scatter', mode: 'lines+markers' }),
|
|
...(sensor.config.graphType === 'bar' && { type: 'bar' }),
|
|
x: values.map((v) => new Date(v.timestamp * 1000)),
|
|
y: values.map((v) => v.value),
|
|
line: {
|
|
width: 1
|
|
}
|
|
},
|
|
],
|
|
{
|
|
xaxis: { range: [from, to], type: "date" },
|
|
yaxis: {
|
|
...(customRange && { range: [minValue, maxValue] }),
|
|
...(sensor.config.unit && { ticksuffix: ` ${sensor.config.unit}` }),
|
|
},
|
|
margin: {
|
|
l: 70,
|
|
r: 20,
|
|
b: 60,
|
|
t: 20,
|
|
pad: 5,
|
|
},
|
|
height: 300
|
|
},
|
|
{
|
|
responsive: true,
|
|
}
|
|
);
|
|
};
|
|
|
|
const refreshValues = () => {
|
|
const [from, to] = getDataInterval(CURRENT_FILTERS.interval);
|
|
|
|
fetch(
|
|
`/api/sensors/${sensor.sensor}/values?from=${Math.round(
|
|
from.getTime() / 1000
|
|
)}&to=${Math.round(to.getTime() / 1000)}`
|
|
)
|
|
.then((r) => r.json())
|
|
.then((values) => renderBody({ from, to }, values ?? []));
|
|
};
|
|
|
|
const refreshSensorConfig = () => {
|
|
fetch(`/api/sensors/${sensor.sensor}/config`)
|
|
.then((r) => r.json())
|
|
.then((c) => (sensor.config = c))
|
|
.then(() => {
|
|
renderHeader();
|
|
refreshValues();
|
|
});
|
|
};
|
|
|
|
renderHeader();
|
|
refreshValues();
|
|
|
|
container.appendChild(header);
|
|
container.appendChild(body);
|
|
|
|
return {
|
|
container,
|
|
refreshValues,
|
|
};
|
|
}
|
|
|
|
load();
|
|
renderFilters();
|