This commit is contained in:
Sergey Yarkov 2023-10-29 22:32:00 +03:00
parent d934e93d93
commit 8a0cfe5063
No known key found for this signature in database
GPG Key ID: E91CE1FABC619B6F
5 changed files with 304 additions and 51 deletions

BIN
data.db

Binary file not shown.

View File

@ -587,6 +587,21 @@ video {
opacity: 0.7; opacity: 0.7;
} }
.modal {
margin-left: auto;
margin-right: auto;
margin-top: 5rem;
max-width: 28rem;
border-radius: 0.125rem;
border-width: 1px;
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
padding-left: 1rem;
padding-right: 1rem;
padding-top: 0.875rem;
padding-bottom: 0.875rem;
}
.visible { .visible {
visibility: visible; visibility: visible;
} }
@ -622,6 +637,15 @@ video {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.my-3 {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-3 { .mb-3 {
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
@ -682,16 +706,8 @@ video {
margin-top: 1.5rem; margin-top: 1.5rem;
} }
.mb-1 { .mt-auto {
margin-bottom: 0.25rem; margin-top: auto;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-10 {
margin-bottom: 2.5rem;
} }
.block { .block {
@ -706,6 +722,10 @@ video {
display: flex; display: flex;
} }
.grid {
display: grid;
}
.hidden { .hidden {
display: none; display: none;
} }
@ -746,10 +766,31 @@ video {
width: 100%; width: 100%;
} }
.w-16 {
width: 4rem;
}
.w-24 {
width: 6rem;
}
.w-52 {
width: 13rem;
}
.min-w-\[256px\] { .min-w-\[256px\] {
min-width: 256px; min-width: 256px;
} }
.min-w-fit {
min-width: -moz-fit-content;
min-width: fit-content;
}
.min-w-\[100px\] {
min-width: 100px;
}
.max-w-2xl { .max-w-2xl {
max-width: 42rem; max-width: 42rem;
} }
@ -770,6 +811,10 @@ video {
flex: 1 1 auto; flex: 1 1 auto;
} }
.basis-52 {
flex-basis: 13rem;
}
.transform { .transform {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
} }
@ -798,6 +843,42 @@ video {
resize: both; resize: both;
} }
.grid-flow-col {
grid-auto-flow: column;
}
.grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid-cols-\[200px_minmax\(900px\2c _1fr\)_100px\] {
grid-template-columns: 200px minmax(900px, 1fr) 100px;
}
.grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.grid-rows-3 {
grid-template-rows: repeat(3, minmax(0, 1fr));
}
.grid-rows-4 {
grid-template-rows: repeat(4, minmax(0, 1fr));
}
.grid-rows-1 {
grid-template-rows: repeat(1, minmax(0, 1fr));
}
.grid-rows-2 {
grid-template-rows: repeat(2, minmax(0, 1fr));
}
.flex-row { .flex-row {
flex-direction: row; flex-direction: row;
} }
@ -806,6 +887,10 @@ video {
flex-direction: column; flex-direction: column;
} }
.flex-wrap {
flex-wrap: wrap;
}
.items-center { .items-center {
align-items: center; align-items: center;
} }
@ -814,10 +899,22 @@ video {
justify-content: flex-end; justify-content: flex-end;
} }
.justify-center {
justify-content: center;
}
.justify-between { .justify-between {
justify-content: space-between; justify-content: space-between;
} }
.gap-3 {
gap: 0.75rem;
}
.gap-2 {
gap: 0.5rem;
}
.space-x-2 > :not([hidden]) ~ :not([hidden]) { .space-x-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0; --tw-space-x-reverse: 0;
margin-right: calc(0.5rem * var(--tw-space-x-reverse)); margin-right: calc(0.5rem * var(--tw-space-x-reverse));
@ -842,6 +939,10 @@ video {
margin-bottom: calc(0.75rem * var(--tw-space-y-reverse)); margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
} }
.overflow-auto {
overflow: auto;
}
.overflow-y-auto { .overflow-y-auto {
overflow-y: auto; overflow-y: auto;
} }
@ -850,6 +951,10 @@ video {
border-radius: 0.125rem; border-radius: 0.125rem;
} }
.rounded-md {
border-radius: 0.375rem;
}
.border { .border {
border-width: 1px; border-width: 1px;
} }
@ -858,6 +963,10 @@ video {
border-width: 2px; border-width: 2px;
} }
.border-4 {
border-width: 4px;
}
.border-b { .border-b {
border-bottom-width: 1px; border-bottom-width: 1px;
} }
@ -962,6 +1071,10 @@ video {
padding-top: 1.25rem; padding-top: 1.25rem;
} }
.pl-0 {
padding-left: 0px;
}
.text-left { .text-left {
text-align: left; text-align: left;
} }
@ -1000,6 +1113,11 @@ video {
line-height: 1.75rem; line-height: 1.75rem;
} }
.text-5xl {
font-size: 3rem;
line-height: 1;
}
.font-bold { .font-bold {
font-weight: 700; font-weight: 700;
} }
@ -1134,3 +1252,33 @@ button:disabled {
color: rgb(75 85 99 / var(--tw-text-opacity)); color: rgb(75 85 99 / var(--tw-text-opacity));
} }
} }
@media (min-width: 640px) {
.sm\:grid-cols-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
}
@media (min-width: 768px) {
@media (min-width: 640px) {
.md\:sm\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.md\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
@media (min-width: 640px) {
.lg\:sm\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.lg\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}

View File

@ -6,6 +6,10 @@
.btn { .btn {
@apply bg-blue-600 text-white py-2 px-3.5 font-semibold text-sm rounded-sm hover:opacity-70; @apply bg-blue-600 text-white py-2 px-3.5 font-semibold text-sm rounded-sm hover:opacity-70;
} }
.modal {
@apply px-4 py-3.5 ml-auto mr-auto mt-20 bg-white max-w-md border rounded-sm;
}
} }
[x-cloak] { [x-cloak] {

View File

@ -16,7 +16,7 @@
</div> --> </div> -->
<!-- Root element --> <!-- Root element -->
<div x-data x-cloak class="mx-auto max-w-5xl flex flex-col h-screen"> <div x-data class="mx-auto max-w-5xl flex flex-col h-screen px-2.5">
<!-- Monitoring content --> <!-- Monitoring content -->
<template x-if="$store.app.currentPage === 'monitoring'"> <template x-if="$store.app.currentPage === 'monitoring'">
<main x-data="monitoringPage" class="flex-1" x-bind:hidden="$store.app.currentPage !== 'monitoring'"> <main x-data="monitoringPage" class="flex-1" x-bind:hidden="$store.app.currentPage !== 'monitoring'">
@ -25,7 +25,7 @@
<button @click="$store.app.setPage('configuration')" class="btn">Config</button> <button @click="$store.app.setPage('configuration')" class="btn">Config</button>
</div> --> </div> -->
<div class="flex h-full flex-row space-x-2"> <div class="flex h-full flex-row space-x-2">
<div class="border-r min-w-[256px] p-2.5 pt-5"> <div class="border-r min-w-[256px] p-2.5 pl-0 pt-5">
<div class="flex flex-col space-y-1"> <div class="flex flex-col space-y-1">
<!-- List of devices --> <!-- List of devices -->
<span class="italic text-sm">Devices:</span> <span class="italic text-sm">Devices:</span>
@ -33,7 +33,7 @@
No devices added. No devices added.
</span> </span>
<div class="flex flex-col max-h-[400px] overflow-y-auto space-y-2"> <div class="flex flex-col max-h-[400px] overflow-y-auto space-y-2">
<template x-for="device in devices"> <template x-for="device in devices" :key="device.id">
<button <button
@click="selectDevice(device)" @click="selectDevice(device)"
:class="{'bg-gray-100': selectedDevice?.id === device.id}" :class="{'bg-gray-100': selectedDevice?.id === device.id}"
@ -43,8 +43,50 @@
</button> </button>
</template> </template>
</div> </div>
<!-- Add new device --> <!-- MODAL: Create new device -->
<button class="w-full min-h-[50px] text-center border-2 border-dashed rounded-sm hover:bg-gray-50 cursor-pointer"> <template x-teleport="body">
<div
@keyup.escape.window="modalCreateDevice.close()"
class="backdrop z-50"
x-show="modalCreateDevice.isOpen"
>
<div class="modal">
<div class="flex justify-between items-center">
<h2 class="font-bold text-lg">Create new device</h2>
<button @click="modalCreateDevice.close()" aria-label="Close">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M6.4 19L5 17.6l5.6-5.6L5 6.4L6.4 5l5.6 5.6L17.6 5L19 6.4L13.4 12l5.6 5.6l-1.4 1.4l-5.6-5.6L6.4 19Z"
/>
</svg>
</button>
</div>
<hr />
<form x-on:submit.prevent="createDevice" action="#">
<div class="flex flex-col space-y-2">
<div>
<label for="id">Slave Id</label>
<input placeholder="1" id="id" name="id" type="number" min="1" max="255" required />
</div>
<div>
<label for="id">Name</label>
<input placeholder="Device #1" id="id" name="id" type="text" required />
</div>
</div>
</form>
<div class="mt-4 flex justify-end space-x-2">
<button class="btn">Create</button>
<button @click="modalCreateDevice.close()" class="btn bg-red-500">Cancel</button>
</div>
</div>
</div>
</template>
<!-- Create new device -->
<button
@click="modalCreateDevice.open()"
class="w-full min-h-[50px] text-center border-2 border-dashed rounded-sm hover:bg-gray-50 cursor-pointer"
>
<span class="text-4xl text-gray-400 select-none">+</span> <span class="text-4xl text-gray-400 select-none">+</span>
</button> </button>
</div> </div>
@ -64,30 +106,73 @@
</div> </div>
<div class="flex-auto pl-2"> <div class="flex-auto pl-2">
<div x-show="selectedDevice != null"> <div x-show="selectedDevice != null">
<div class="pt-5 border-b flex justify-between items-center pr-6"> <div class="pt-5 border-b flex flex-wrap justify-between items-center pr-6">
<h2 x-text="selectedDevice?.name" class="font-bold text-3xl my-2"></h2> <h2 x-text="selectedDevice?.name" class="font-bold text-3xl my-2 mr-2"></h2>
<!-- Device create, delete --> <!-- Device create, delete -->
<div> <div>
<a href="#">Edit</a> <a href="#">Edit</a>
<a @click="$dispatch('dialog', { <a
@click="$dispatch('dialog', {
title: 'Remove device', title: 'Remove device',
body: `Remove ${selectedDevice.name}? All logs will be cleared.`, body: `Remove ${selectedDevice.name}? All logs will be cleared.`,
isOpen: true, isOpen: true,
onConfirm: async () => await removeDevice(selectedDevice.id) onConfirm: async () => await removeDevice(selectedDevice.id)
});" });"
class="text-red-500" class="text-red-500"
href="#"> href="#"
>
Delete Delete
</a> </a>
</div> </div>
</div> </div>
<!-- Display Graph --> <!-- Display Graph -->
<div class="mt-4"> <div x-show="selectedDevice?.g_display_reg_addr != null" class="mt-4">
<div id="div_g" x-ref="graph" style="width: 720px; height: 300px"></div> <div id="div_g" style="width: 720px; height: 300px"></div>
<hr class="my-3" />
</div> </div>
<hr class="mt-10" />
<!-- Display values --> <!-- Display values -->
<h2 class="text-xl font-bold mb-3">Display values</h2> <h2 class="text-xl font-bold mb-3 mt-5">Display values</h2>
<div>
<div class="grid grid-cols-1 md:sm:grid-cols-2 lg:sm:grid-cols-3 gap-2">
<template x-for="value in [1,2,3,4,5]">
<div class="border-2 p-2.5 rounded-sm">
<div class="flex justify-between">
<h3 class="text-xl font-bold">CV1</h3>
<div>
<button>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path
fill="#eab308"
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a.996.996 0 0 0 0-1.41l-2.34-2.34a.996.996 0 0 0-1.41 0l-1.83 1.83l3.75 3.75l1.83-1.83z"
/>
</svg>
</button>
<button>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path
fill="#ef4444"
d="m9.4 16.5l2.6-2.6l2.6 2.6l1.4-1.4l-2.6-2.6L16 9.9l-1.4-1.4l-2.6 2.6l-2.6-2.6L8 9.9l2.6 2.6L8 15.1l1.4 1.4ZM5 21V6H4V4h5V3h6v1h5v2h-1v15H5Zm2-2h10V6H7v13ZM7 6v13V6Z"
/>
</svg>
</button>
</div>
</div>
<hr />
<div class="flex flex-col">
<span>Value: 7892</span>
<span>Reg Addr: 6</span>
<span>Format: 32</span>
</div>
</div>
</template>
<div
class="flex justify-center items-center border-4 text-center border-dashed p-2.5 rounded-sm cursor-pointer hover:bg-gray-50"
>
<span class="text-5xl text-gray-400 font-bold select-none">+</span>
</div>
</div>
</div>
</div> </div>
<div class="select-none" x-show="selectedDevice === null"> <div class="select-none" x-show="selectedDevice === null">
<span class="block text-center italic text-gray-500 mt-10">No device selected.</span> <span class="block text-center italic text-gray-500 mt-10">No device selected.</span>
@ -307,30 +392,34 @@
<!-- Dialog --> <!-- Dialog -->
<div x-data="dialog"> <div x-data="dialog">
<template @dialog.window="open($event)" x-if="isOpen"> <template @dialog.window="open($event)" @keyup.escape.window="close()" x-if="isOpen">
<div class="backdrop z-50"> <div class="backdrop z-50">
<div <div role="dialog" class="modal">
role="dialog" <div class="flex justify-between items-center">
class="px-4 py-3.5 ml-auto mr-auto mt-20 bg-white max-w-md border rounded-sm" <h2 x-text="labels.title" class="font-bold text-lg"></h2>
> <button @click="close()" aria-label="Close">
<div class="flex justify-between items-center"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<h2 x-text="labels.title" class="font-bold text-lg"></h2> <path
<button @click="close()" aria-label="Close"> fill="currentColor"
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> d="M6.4 19L5 17.6l5.6-5.6L5 6.4L6.4 5l5.6 5.6L17.6 5L19 6.4L13.4 12l5.6 5.6l-1.4 1.4l-5.6-5.6L6.4 19Z"
<path />
fill="currentColor" </svg>
d="M6.4 19L5 17.6l5.6-5.6L5 6.4L6.4 5l5.6 5.6L17.6 5L19 6.4L13.4 12l5.6 5.6l-1.4 1.4l-5.6-5.6L6.4 19Z" </button>
/>
</svg>
</button>
</div>
<hr />
<p x-text="labels.body"></p>
<div class="mt-4 flex justify-end space-x-2">
<button x-bind:disabled="isLoading" x-bind:class="isLoading && 'opacity-70'" @click="confirm()" class="btn">Confirm</button>
<button @click="close()" class="btn bg-red-500">Cancel</button>
</div>
</div> </div>
<hr />
<p x-text="labels.body"></p>
<div class="mt-4 flex justify-end space-x-2">
<button
x-bind:disabled="isLoading"
x-bind:class="isLoading && 'opacity-70'"
@click="confirm()"
class="btn"
>
Confirm
</button>
<button @click="close()" class="btn bg-red-500">Cancel</button>
</div>
</div>
</div> </div>
</template> </template>
</div> </div>

View File

@ -60,6 +60,15 @@ document.addEventListener("alpine:init", async () => {
Alpine.data("monitoringPage", () => ({ Alpine.data("monitoringPage", () => ({
selectedDevice: null, selectedDevice: null,
devices: [], devices: [],
modalCreateDevice: {
isOpen: false,
open() {
this.isOpen = true;
},
close() {
this.isOpen = false;
},
},
async init() { async init() {
try { try {
this.$store.app.currentPage = "loading"; this.$store.app.currentPage = "loading";
@ -88,7 +97,7 @@ document.addEventListener("alpine:init", async () => {
}, },
async removeDevice(id) { async removeDevice(id) {
try { try {
await api.post('/modbus/remove_device', { id }); await api.post("/modbus/remove_device", { id });
this.devices = this.devices.filter((d) => d.id != id); this.devices = this.devices.filter((d) => d.id != id);
this.selectedDevice = null; this.selectedDevice = null;
} catch (error) { } catch (error) {
@ -135,16 +144,15 @@ document.addEventListener("alpine:init", async () => {
title: "Confirm dialog", title: "Confirm dialog",
body: "Are you sure you want to perform this action?", body: "Are you sure you want to perform this action?",
}, },
cb: null,
onConfirm: () => undefined, onConfirm: () => undefined,
onError: () => undefined, onError: () => undefined,
confirm() { confirm() {
if (this.onConfirm.constructor.name === 'AsyncFunction') { if (this.onConfirm.constructor.name === "AsyncFunction") {
this.isLoading = true; this.isLoading = true;
this.onConfirm() this.onConfirm()
.then(() => this.close()) .then(() => this.close())
.catch((e) => this.onError(e)) .catch((e) => this.onError(e))
.finally(() => this.isLoading = false) .finally(() => (this.isLoading = false));
} else { } else {
this.onConfirm(); this.onConfirm();
this.close(); this.close();
@ -152,13 +160,17 @@ document.addEventListener("alpine:init", async () => {
}, },
close() { close() {
this.isOpen = false; this.isOpen = false;
this.cb = null;
}, },
/**
* Update dialog state on event
* @param {CustomEvent} event
*/
open(event) { open(event) {
const { detail } = event; const { detail } = event;
this.isOpen = detail.isOpen; this.isOpen = detail.isOpen;
this.onConfirm = detail.onConfirm; this.onConfirm = detail.onConfirm;
this.labels = { title: detail.title, body: detail.body }; this.labels = { title: detail.title, body: detail.body };
} },
})); }));
}); });