Added proper global filters and sensor settings
This commit is contained in:
parent
1ca9b04351
commit
94d2549fca
|
|
@ -1,12 +1,28 @@
|
||||||
body {
|
body, html {
|
||||||
background: #eee;
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sensors-container {
|
body {
|
||||||
|
background: #eee;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, input {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sensors {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 1200px) {
|
||||||
|
.sensors {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sensor {
|
.sensor {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
|
@ -23,4 +39,89 @@ body {
|
||||||
|
|
||||||
.sensor .header .actions {
|
.sensor .header .actions {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
overflow: auto;
|
||||||
|
display: none;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
background-color: rgba(0,0,0,0.2);
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-modal.show {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-modal .inner {
|
||||||
|
background: #fff;
|
||||||
|
margin-top: 5%;
|
||||||
|
box-shadow: 0px 10px 15px -3px rgba(0,0,0,0.1);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-modal .inner .body {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .input {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
form .actions {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
form.horizontal {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.horizontal .input {
|
||||||
|
flex-direction: row;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
form.horizontal .input label {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0px 10px 15px -3px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters .actions {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label input[type=checkbox] {
|
||||||
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
@ -1,18 +1,222 @@
|
||||||
const { html, render } = lighterhtml;
|
const { html, render } = lighterhtml;
|
||||||
|
|
||||||
const $container = document.getElementById("sensors-container");
|
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() {
|
function load() {
|
||||||
fetch("/api/sensors")
|
fetch("/api/sensors")
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((sensors) => {
|
.then((sensors) => {
|
||||||
const components = sensors.map((sensor) => createSensor(sensor));
|
$sensorsContainer.innerHTML = ''
|
||||||
|
|
||||||
components.forEach((component) =>
|
sensorComponents = sensors.map((sensor) => createSensor(sensor));
|
||||||
$container.appendChild(component.container)
|
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) {
|
function createSensor(sensor) {
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
container.className = "sensor";
|
container.className = "sensor";
|
||||||
|
|
@ -23,13 +227,17 @@ function createSensor(sensor) {
|
||||||
const body = document.createElement("div");
|
const body = document.createElement("div");
|
||||||
body.className = "body";
|
body.className = "body";
|
||||||
|
|
||||||
|
const showSettings = () => {
|
||||||
|
showConfigOf(sensor, () => refreshSensorConfig());
|
||||||
|
};
|
||||||
|
|
||||||
const renderHeader = () => {
|
const renderHeader = () => {
|
||||||
render(
|
render(
|
||||||
header,
|
header,
|
||||||
html`
|
html`
|
||||||
<div class="name">${sensor.config?.name ?? sensor.sensor}</div>
|
<div class="name">${sensor.config?.name ?? sensor.sensor}</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="config">Config</button>
|
<button class="config" onClick=${showSettings}>Config</button>
|
||||||
<button class="refresh" onClick=${refreshValues}>Refresh</button>
|
<button class="refresh" onClick=${refreshValues}>Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
@ -65,8 +273,7 @@ function createSensor(sensor) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshValues = () => {
|
const refreshValues = () => {
|
||||||
const from = new Date(Date.now() - 5 * 24 * 3600 * 1000);
|
const [from, to] = getDataInterval(CURRENT_FILTERS.interval);
|
||||||
const to = new Date();
|
|
||||||
|
|
||||||
fetch(
|
fetch(
|
||||||
`/api/sensors/${sensor.sensor}/values?from=${Math.round(
|
`/api/sensors/${sensor.sensor}/values?from=${Math.round(
|
||||||
|
|
@ -74,7 +281,17 @@ function createSensor(sensor) {
|
||||||
)}&to=${Math.round(to.getTime() / 1000)}`
|
)}&to=${Math.round(to.getTime() / 1000)}`
|
||||||
)
|
)
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((values) => renderBody({ from, to }, values));
|
.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();
|
renderHeader();
|
||||||
|
|
@ -85,7 +302,9 @@ function createSensor(sensor) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
container,
|
container,
|
||||||
|
refreshValues
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
load();
|
load();
|
||||||
|
renderFilters();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue