From 39641194aa3ac55c5a7c82dcb00ca4365027b388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Z=C3=ADpek?= Date: Wed, 24 Aug 2022 22:36:37 +0200 Subject: [PATCH] Redesigned header, added loaders --- client/src/Root.tsx | 8 ++- client/src/assets/style.css | 66 +++++++++++++++-- client/src/icons.ts | 2 + .../src/pages/dashboard/NewDashboardPage.tsx | 6 +- .../dashboard/components/BoxDialContent.tsx | 28 +++++--- .../dashboard/components/BoxGraphContent.tsx | 16 ++++- .../pages/dashboard/components/BoxLoader.tsx | 9 +++ .../components/BoxSettings/BoxSettings.tsx | 72 +++++++++++-------- .../dashboard/components/DashboardGrid.tsx | 61 +++++++++------- .../dashboard/components/DashboardHeader.tsx | 7 +- .../dashboard/components/EditableBox.tsx | 24 +++++-- .../pages/dashboard/components/Filters.tsx | 8 ++- .../pages/dashboard/utils/normalizeBoxes.tsx | 2 +- 13 files changed, 222 insertions(+), 87 deletions(-) create mode 100644 client/src/icons.ts create mode 100644 client/src/pages/dashboard/components/BoxLoader.tsx diff --git a/client/src/Root.tsx b/client/src/Root.tsx index 114e97c..02f20d6 100644 --- a/client/src/Root.tsx +++ b/client/src/Root.tsx @@ -2,7 +2,13 @@ import { QueryClient, QueryClientProvider } from 'react-query' import { AppContextProvider } from './contexts/AppContext' import { Router } from './pages/Router' -const queryClient = new QueryClient() +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + }, + }, +}) export const Root = () => { return ( diff --git a/client/src/assets/style.css b/client/src/assets/style.css index fbc47b5..cdd43aa 100644 --- a/client/src/assets/style.css +++ b/client/src/assets/style.css @@ -101,6 +101,17 @@ input, select { padding: 0.75rem 1rem; } +.settings-modal .actions { + display: flex; + align-items: center; +} + +.settings-modal .actions .remove { + margin-right: auto; + background-color: transparent; + color: #ff0000; +} + form { display: flex; flex-direction: column; @@ -152,12 +163,19 @@ form.horizontal .input label { .dashboard-head .shadow { position: absolute; - background: linear-gradient(0deg, rgba(255,255,255,0) 5%, rgba(190,190,190,1) 100%); + background: linear-gradient(0deg, rgba(255,255,255,0) 5%, rgba(190,190,190,0.6) 100%); width: 100%; - height: 12px; + height: 8px; z-index: 1; } +.dashboard-head .spacer { + margin: 0 1rem; + width: 1px; + background: #ddd; + height: 20px; +} + .checkbox-label { display: inline-flex; align-items: center; @@ -167,10 +185,18 @@ form.horizontal .input label { margin-top: 6px; } -.grid-sensors { +.grid-sensors-container { position: relative; - margin: 0.25rem; + padding: 0.25rem; flex: 1; + overflow: auto; + min-height: 0; +} + +.grid-sensors { + width: 100%; + height: 100%; + position: relative; } .grid-sensors .grid-box { @@ -188,6 +214,33 @@ form.horizontal .input label { position: relative; display: flex; flex-direction: column; + overflow: hidden; +} + +.grid-sensors .grid-box .box .body { + position: relative; +} + +.grid-sensors .grid-box .box .box-loader { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + left: 0; + right: 0; + top: 0; + bottom: 0; + background-color: rgba(128, 128, 128, 0.3); + color: #fff; + font-size: 300%; + z-index: 2; +} + +.grid-sensors .grid-box .box .box-loader .svg-icon { + animation-name: rotate; + animation-iteration-count: infinite; + animation-duration: 0.75s; + animation-timing-function: linear; } .grid-sensors .grid-box .box .resize-h { @@ -293,4 +346,9 @@ form.horizontal .input label { .svg-icon { height: 1em; stroke: currentColor; +} + +@keyframes rotate { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(-360deg); } } \ No newline at end of file diff --git a/client/src/icons.ts b/client/src/icons.ts new file mode 100644 index 0000000..1db3e9a --- /dev/null +++ b/client/src/icons.ts @@ -0,0 +1,2 @@ +export { ReactComponent as SettingsIcon } from '@/assets/icons/settings.svg' +export { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg' diff --git a/client/src/pages/dashboard/NewDashboardPage.tsx b/client/src/pages/dashboard/NewDashboardPage.tsx index 2f7f707..6f7aeaa 100644 --- a/client/src/pages/dashboard/NewDashboardPage.tsx +++ b/client/src/pages/dashboard/NewDashboardPage.tsx @@ -6,7 +6,7 @@ import { import { createDashboardContent } from '@/utils/createDashboardContent' import { parseDashboard } from '@/utils/parseDashboard' import { useEffect, useMemo, useState } from 'preact/hooks' -import { useQuery } from 'react-query' +import { useQuery, useQueryClient } from 'react-query' import { DashboardGrid } from './components/DashboardGrid' import { DashboardHeader } from './components/DashboardHeader' import { GRID_H_SNAP, GRID_WIDTH } from './constants' @@ -14,6 +14,7 @@ import { DashboardContextProvider } from './contexts/DashboardContext' import { BoxDefinition } from './types' export const NewDashboardPage = () => { + const queryClient = useQueryClient() const dashboards = useQuery(['/dashboards'], getDashboards) const dashboard = dashboards.data?.find((i) => i.id === 'default') @@ -41,7 +42,8 @@ export const NewDashboardPage = () => { } const handleRefresh = () => { - console.log('Nothing to refresh right now') + queryClient.invalidateQueries(['/sensor/values']) + queryClient.invalidateQueries(['/sensor/values/latest']) } const handleNewBox = () => { diff --git a/client/src/pages/dashboard/components/BoxDialContent.tsx b/client/src/pages/dashboard/components/BoxDialContent.tsx index 46cbcfe..4393782 100644 --- a/client/src/pages/dashboard/components/BoxDialContent.tsx +++ b/client/src/pages/dashboard/components/BoxDialContent.tsx @@ -1,16 +1,19 @@ import { getLatestSensorValue } from '@/api/sensorValues' import { DashboardDialData } from '@/utils/parseDashboard' +import { RefObject } from 'preact' import { useMemo } from 'preact/hooks' import { useQuery } from 'react-query' import { useDashboardContext } from '../contexts/DashboardContext' import { BoxDefinition } from '../types' +import { BoxLoader } from './BoxLoader' type Props = { box: BoxDefinition data: DashboardDialData + refreshRef: RefObject<() => void> } -export const BoxDialContent = ({ box, data }: Props) => { +export const BoxDialContent = ({ box, data, refreshRef }: Props) => { const { filter } = useDashboardContext() const valuesQuery = { @@ -22,6 +25,10 @@ export const BoxDialContent = ({ box, data }: Props) => { getLatestSensorValue(valuesQuery) ) + refreshRef.current = () => { + value.refetch() + } + const displayValue = useMemo(() => { if (!value.data) { return '' @@ -40,13 +47,16 @@ export const BoxDialContent = ({ box, data }: Props) => { }, [value.data, data]) return ( -
- {value.data && ( - <> - {displayValue} - {` ${data.unit}`} - - )} -
+ <> + {value.isFetching && } +
+ {value.data && ( + <> + {displayValue} + {` ${data.unit}`} + + )} +
+ ) } diff --git a/client/src/pages/dashboard/components/BoxGraphContent.tsx b/client/src/pages/dashboard/components/BoxGraphContent.tsx index ac8eb04..b856143 100644 --- a/client/src/pages/dashboard/components/BoxGraphContent.tsx +++ b/client/src/pages/dashboard/components/BoxGraphContent.tsx @@ -1,16 +1,19 @@ import { getSensorValues } from '@/api/sensorValues' import { DashboardGraphData } from '@/utils/parseDashboard' +import { RefObject } from 'preact' import { useEffect, useRef } from 'preact/hooks' import { useQuery } from 'react-query' import { useDashboardContext } from '../contexts/DashboardContext' import { BoxDefinition } from '../types' +import { BoxLoader } from './BoxLoader' type Props = { box: BoxDefinition data: DashboardGraphData + refreshRef: RefObject<() => void> } -export const BoxGraphContent = ({ box, data }: Props) => { +export const BoxGraphContent = ({ box, data, refreshRef }: Props) => { const { filter } = useDashboardContext() const bodyRef = useRef(null) @@ -25,6 +28,10 @@ export const BoxGraphContent = ({ box, data }: Props) => { getSensorValues(valuesQuery) ) + refreshRef.current = () => { + values.refetch() + } + useEffect(() => { // TODO: These should be probably returned by server, could be outdated const from = filter.customFrom @@ -80,5 +87,10 @@ export const BoxGraphContent = ({ box, data }: Props) => { } }, [values.data, box, data]) - return
+ return ( + <> + {values.isFetching && } +
+ + ) } diff --git a/client/src/pages/dashboard/components/BoxLoader.tsx b/client/src/pages/dashboard/components/BoxLoader.tsx new file mode 100644 index 0000000..5e9213d --- /dev/null +++ b/client/src/pages/dashboard/components/BoxLoader.tsx @@ -0,0 +1,9 @@ +import { RefreshIcon } from '@/icons' + +export const BoxLoader = () => { + return ( +
+ +
+ ) +} diff --git a/client/src/pages/dashboard/components/BoxSettings/BoxSettings.tsx b/client/src/pages/dashboard/components/BoxSettings/BoxSettings.tsx index e3b2a4c..117b463 100644 --- a/client/src/pages/dashboard/components/BoxSettings/BoxSettings.tsx +++ b/client/src/pages/dashboard/components/BoxSettings/BoxSettings.tsx @@ -8,11 +8,12 @@ import { GraphSettings } from './components/GraphSettings' type Props = { value: BoxDefinition + onRemove: () => void onSave: (newValue: BoxDefinition) => void onClose: () => void } -export const BoxSettings = ({ value, onSave, onClose }: Props) => { +export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => { const sensors = useQuery(['/sensors'], getSensors) const [formState, setFormState] = useState(() => ({ @@ -74,41 +75,50 @@ export const BoxSettings = ({ value, onSave, onClose }: Props) => { ))}
-
- - -
-
- - -
- {formState.type === 'graph' && ( - - )} + {formState.sensor && ( + <> +
+ + +
+
+ + +
- {formState.type === 'dial' && ( - + {formState.type === 'graph' && ( + + )} + + {formState.type === 'dial' && ( + + )} + )}
+ + diff --git a/client/src/pages/dashboard/components/DashboardGrid.tsx b/client/src/pages/dashboard/components/DashboardGrid.tsx index 671c6d8..b1dd6d0 100644 --- a/client/src/pages/dashboard/components/DashboardGrid.tsx +++ b/client/src/pages/dashboard/components/DashboardGrid.tsx @@ -9,37 +9,44 @@ type Props = { export const DashboardGrid = ({ boxes, onChange }: Props) => { return ( -
- {boxes.map((b) => ( - { - onChange((previous) => - normalizeBoxes( - previous.map((pb) => - pb.id === b.id ? { ...pb, x: p.x, y: p.y } : pb +
+
+ {boxes.map((b) => ( + { + onChange((previous) => + normalizeBoxes( + previous.map((pb) => + pb.id === b.id ? { ...pb, x: p.x, y: p.y } : pb + ) ) ) - ) - }} - onResize={(s) => { - onChange((previous) => - normalizeBoxes( - previous.map((pb) => - pb.id === b.id ? { ...pb, w: s.w, h: s.h } : pb + }} + onResize={(s) => { + onChange((previous) => + normalizeBoxes( + previous.map((pb) => + pb.id === b.id ? { ...pb, w: s.w, h: s.h } : pb + ) ) ) - ) - }} - onEdit={(newB) => { - onChange((previous) => - previous.map((b) => (b.id === newB.id ? newB : b)) - ) - }} - /> - ))} + }} + onEdit={(newB) => { + onChange((previous) => + previous.map((b) => (b.id === newB.id ? newB : b)) + ) + }} + onRemove={() => { + onChange((previous) => + normalizeBoxes(previous.filter((pb) => pb.id !== b.id)) + ) + }} + /> + ))} +
) } diff --git a/client/src/pages/dashboard/components/DashboardHeader.tsx b/client/src/pages/dashboard/components/DashboardHeader.tsx index 703c215..57b07f6 100644 --- a/client/src/pages/dashboard/components/DashboardHeader.tsx +++ b/client/src/pages/dashboard/components/DashboardHeader.tsx @@ -1,3 +1,4 @@ +import { RefreshIcon } from '@/icons' import { Filters } from './Filters' type Props = { @@ -10,8 +11,12 @@ export const DashboardHeader = ({ onNewBox, onRefresh }: Props) => {
+
- +
+
diff --git a/client/src/pages/dashboard/components/EditableBox.tsx b/client/src/pages/dashboard/components/EditableBox.tsx index 1c8a830..97745df 100644 --- a/client/src/pages/dashboard/components/EditableBox.tsx +++ b/client/src/pages/dashboard/components/EditableBox.tsx @@ -1,5 +1,5 @@ import { useWindowEvent } from '@/utils/hooks/useWindowEvent' -import { useState } from 'preact/hooks' +import { useRef, useState } from 'preact/hooks' import { GRID_WIDTH } from '../constants' import { useDashboardContext } from '../contexts/DashboardContext' import { useDragging } from '../hooks/useDragging' @@ -8,9 +8,8 @@ import { BoxDefinition } from '../types' import { BoxDialContent } from './BoxDialContent' import { BoxGraphContent } from './BoxGraphContent' import { BoxSettings } from './BoxSettings/BoxSettings' -import { ReactComponent as SettingsIcon } from '@/assets/icons/settings.svg' -import { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg' import { useElementOffsets } from '@/utils/hooks/useElementOffsets' +import { RefreshIcon, SettingsIcon } from '@/icons' type Props = { box: BoxDefinition @@ -18,6 +17,7 @@ type Props = { onPosition: (p: { x: number; y: number }) => void onResize: (p: { w: number; h: number }) => void onEdit: (box: BoxDefinition) => void + onRemove: () => void } export const EditableBox = ({ @@ -26,8 +26,11 @@ export const EditableBox = ({ onPosition, onResize, onEdit, + onRemove, }: Props) => { const [boxRef, setBoxRef] = useState() + const refreshRef = useRef<() => void>(null) + const { verticalMode } = useDashboardContext() const [editing, setEditing] = useState(false) @@ -124,7 +127,7 @@ export const EditableBox = ({
{box.title ?? box.sensor ?? ''}
-
+
refreshRef.current?.()}>
setEditing(true)}> @@ -134,10 +137,18 @@ export const EditableBox = ({
{box.sensor && box.data?.type === 'graph' && ( - + )} {box.sensor && box.data?.type === 'dial' && ( - + )}
@@ -185,6 +196,7 @@ export const EditableBox = ({ value={box} onSave={onEdit} onClose={() => setEditing(false)} + onRemove={onRemove} /> )} diff --git a/client/src/pages/dashboard/components/Filters.tsx b/client/src/pages/dashboard/components/Filters.tsx index 3773969..166f4fb 100644 --- a/client/src/pages/dashboard/components/Filters.tsx +++ b/client/src/pages/dashboard/components/Filters.tsx @@ -1,6 +1,6 @@ import { DateTimeInput } from '@/components/DateTimeInput' import { intervalToRange } from '@/utils/intervalToRange' -import { useState } from 'preact/hooks' +import { useEffect, useState } from 'preact/hooks' import { useDashboardContext } from '../contexts/DashboardContext' export type FilterInterval = @@ -44,6 +44,10 @@ export const Filters = () => { }) } + useEffect(() => { + setFilter(value) + }, [value]) + return (
@@ -85,8 +89,6 @@ export const Filters = () => {
)} - -
) diff --git a/client/src/pages/dashboard/utils/normalizeBoxes.tsx b/client/src/pages/dashboard/utils/normalizeBoxes.tsx index 8b9a3fc..bd91a9a 100644 --- a/client/src/pages/dashboard/utils/normalizeBoxes.tsx +++ b/client/src/pages/dashboard/utils/normalizeBoxes.tsx @@ -10,7 +10,7 @@ export function normalizeBoxes(boxes: BoxDefinition[]) { // TODO: This is not optimized at all while (!sorted) { // Sort boxes to have the lowest ones first - boxes.sort((a, b) => a.y - b.y) + boxes.sort((a, b) => (a.y === b.y ? a.x - b.x : a.y - b.y)) sorted = true