graphicek/client/js/index.js

311 lines
8.0 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>Name</label>
<input name="name" value="${config.name}" />
</div>
<div class="input">
<label>Unit</label>
<input name="unit" value="${config.unit}" />
</div>
<div class="actions">
<button onclick=${hideConfig} type="button">Cancel</button>
<button>Save</button>
</div>
</form>
</div>
</div>
</div>
`
);
}
const CURRENT_FILTERS = {
interval: "week",
refresh: "1s",
customFrom: new Date().toISOString(),
customTo: new Date().toISOString(),
};
function refreshAll() {
sensorComponents.forEach(c => c.refreshValues())
}
let isCustomSelected = false
let lastIntervalSelected = 'week'
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()
};
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="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>
`
);
}
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;
Plotly.newPlot(
body,
[
{
type: "lines",
x: values.map((v) => new Date(v.timestamp * 1000)),
y: values.map((v) => v.value),
},
],
{
xaxis: { range: [from, to], type: "date" },
margin: {
l: 50,
r: 20,
b: 60,
t: 20,
pad: 5,
},
},
{
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();