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) => {
))}
-
- Title
-
-
-
- Type
-
- Graph
- Dial
-
-
- {formState.type === 'graph' && (
-
- )}
+ {formState.sensor && (
+ <>
+
+ Title
+
+
+
+ Type
+
+ Graph
+ Dial
+
+
- {formState.type === 'dial' && (
-
+ {formState.type === 'graph' && (
+
+ )}
+
+ {formState.type === 'dial' && (
+
+ )}
+ >
)}
+
+ Remove
+
+
Cancel
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) => {
Add box
+
-
Refresh all
+
+
+ Refresh all
+
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 (
>
)}
-
-
Apply
)
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