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;
}
.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 {
visibility: visible;
}
@ -622,6 +637,15 @@ video {
margin-bottom: 1rem;
}
.my-3 {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-3 {
margin-bottom: 0.75rem;
}
@ -682,16 +706,8 @@ video {
margin-top: 1.5rem;
}
.mb-1 {
margin-bottom: 0.25rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-10 {
margin-bottom: 2.5rem;
.mt-auto {
margin-top: auto;
}
.block {
@ -706,6 +722,10 @@ video {
display: flex;
}
.grid {
display: grid;
}
.hidden {
display: none;
}
@ -746,10 +766,31 @@ video {
width: 100%;
}
.w-16 {
width: 4rem;
}
.w-24 {
width: 6rem;
}
.w-52 {
width: 13rem;
}
.min-w-\[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-width: 42rem;
}
@ -770,6 +811,10 @@ video {
flex: 1 1 auto;
}
.basis-52 {
flex-basis: 13rem;
}
.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));
}
@ -798,6 +843,42 @@ video {
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-direction: row;
}
@ -806,6 +887,10 @@ video {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.items-center {
align-items: center;
}
@ -814,10 +899,22 @@ video {
justify-content: flex-end;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.gap-3 {
gap: 0.75rem;
}
.gap-2 {
gap: 0.5rem;
}
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
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));
}
.overflow-auto {
overflow: auto;
}
.overflow-y-auto {
overflow-y: auto;
}
@ -850,6 +951,10 @@ video {
border-radius: 0.125rem;
}
.rounded-md {
border-radius: 0.375rem;
}
.border {
border-width: 1px;
}
@ -858,6 +963,10 @@ video {
border-width: 2px;
}
.border-4 {
border-width: 4px;
}
.border-b {
border-bottom-width: 1px;
}
@ -962,6 +1071,10 @@ video {
padding-top: 1.25rem;
}
.pl-0 {
padding-left: 0px;
}
.text-left {
text-align: left;
}
@ -1000,6 +1113,11 @@ video {
line-height: 1.75rem;
}
.text-5xl {
font-size: 3rem;
line-height: 1;
}
.font-bold {
font-weight: 700;
}
@ -1134,3 +1252,33 @@ button:disabled {
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 {
@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] {

View File

@ -16,7 +16,7 @@
</div> -->
<!-- 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 -->
<template x-if="$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>
</div> -->
<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">
<!-- List of devices -->
<span class="italic text-sm">Devices:</span>
@ -33,7 +33,7 @@
No devices added.
</span>
<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
@click="selectDevice(device)"
:class="{'bg-gray-100': selectedDevice?.id === device.id}"
@ -43,8 +43,50 @@
</button>
</template>
</div>
<!-- Add new device -->
<button class="w-full min-h-[50px] text-center border-2 border-dashed rounded-sm hover:bg-gray-50 cursor-pointer">
<!-- MODAL: Create new device -->
<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>
</button>
</div>
@ -64,30 +106,73 @@
</div>
<div class="flex-auto pl-2">
<div x-show="selectedDevice != null">
<div class="pt-5 border-b flex justify-between items-center pr-6">
<h2 x-text="selectedDevice?.name" class="font-bold text-3xl my-2"></h2>
<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 mr-2"></h2>
<!-- Device create, delete -->
<div>
<a href="#">Edit</a>
<a @click="$dispatch('dialog', {
<a
@click="$dispatch('dialog', {
title: 'Remove device',
body: `Remove ${selectedDevice.name}? All logs will be cleared.`,
isOpen: true,
onConfirm: async () => await removeDevice(selectedDevice.id)
});"
class="text-red-500"
href="#">
href="#"
>
Delete
</a>
</div>
</div>
<!-- Display Graph -->
<div class="mt-4">
<div id="div_g" x-ref="graph" style="width: 720px; height: 300px"></div>
<div x-show="selectedDevice?.g_display_reg_addr != null" class="mt-4">
<div id="div_g" style="width: 720px; height: 300px"></div>
<hr class="my-3" />
</div>
<hr class="mt-10" />
<!-- 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 class="select-none" x-show="selectedDevice === null">
<span class="block text-center italic text-gray-500 mt-10">No device selected.</span>
@ -307,12 +392,9 @@
<!-- 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
role="dialog"
class="px-4 py-3.5 ml-auto mr-auto mt-20 bg-white max-w-md border rounded-sm"
>
<div role="dialog" class="modal">
<div class="flex justify-between items-center">
<h2 x-text="labels.title" class="font-bold text-lg"></h2>
<button @click="close()" aria-label="Close">
@ -327,7 +409,14 @@
<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
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>

View File

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