Redesigned header, added loaders
This commit is contained in:
parent
52469eda4d
commit
39641194aa
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -294,3 +347,8 @@ form.horizontal .input label {
|
|||
height: 1em;
|
||||
stroke: currentColor;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(-360deg); }
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { ReactComponent as SettingsIcon } from '@/assets/icons/settings.svg'
|
||||
export { ReactComponent as RefreshIcon } from '@/assets/icons/refresh.svg'
|
||||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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,6 +47,8 @@ export const BoxDialContent = ({ box, data }: Props) => {
|
|||
}, [value.data, data])
|
||||
|
||||
return (
|
||||
<>
|
||||
{value.isFetching && <BoxLoader />}
|
||||
<div className="dial">
|
||||
{value.data && (
|
||||
<>
|
||||
|
|
@ -48,5 +57,6 @@ export const BoxDialContent = ({ box, data }: Props) => {
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement>(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 <div ref={bodyRef} />
|
||||
return (
|
||||
<>
|
||||
{values.isFetching && <BoxLoader />}
|
||||
<div ref={bodyRef} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import { RefreshIcon } from '@/icons'
|
||||
|
||||
export const BoxLoader = () => {
|
||||
return (
|
||||
<div className="box-loader">
|
||||
<RefreshIcon />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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,6 +75,9 @@ export const BoxSettings = ({ value, onSave, onClose }: Props) => {
|
|||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{formState.sensor && (
|
||||
<>
|
||||
<div className="input">
|
||||
<label>Title</label>
|
||||
<input
|
||||
|
|
@ -107,8 +111,14 @@ export const BoxSettings = ({ value, onSave, onClose }: Props) => {
|
|||
onChange={setData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="actions">
|
||||
<button className="remove" type="button" onClick={onRemove}>
|
||||
Remove
|
||||
</button>
|
||||
|
||||
<button className="cancel" onClick={onClose} type="button">
|
||||
Cancel
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ type Props = {
|
|||
|
||||
export const DashboardGrid = ({ boxes, onChange }: Props) => {
|
||||
return (
|
||||
<div className="grid-sensors-container">
|
||||
<div className="grid-sensors">
|
||||
{boxes.map((b) => (
|
||||
<EditableBox
|
||||
|
|
@ -38,8 +39,14 @@ export const DashboardGrid = ({ boxes, onChange }: Props) => {
|
|||
previous.map((b) => (b.id === newB.id ? newB : b))
|
||||
)
|
||||
}}
|
||||
onRemove={() => {
|
||||
onChange((previous) =>
|
||||
normalizeBoxes(previous.filter((pb) => pb.id !== b.id))
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { RefreshIcon } from '@/icons'
|
||||
import { Filters } from './Filters'
|
||||
|
||||
type Props = {
|
||||
|
|
@ -10,8 +11,12 @@ export const DashboardHeader = ({ onNewBox, onRefresh }: Props) => {
|
|||
<div className="dashboard-head">
|
||||
<div className="inner">
|
||||
<button onClick={onNewBox}>Add box</button>
|
||||
<div className="spacer" />
|
||||
<Filters />
|
||||
<button onClick={onRefresh}>Refresh all</button>
|
||||
<div className="spacer" />
|
||||
<button onClick={onRefresh}>
|
||||
<RefreshIcon /> Refresh all
|
||||
</button>
|
||||
</div>
|
||||
<div className="shadow"></div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement>()
|
||||
const refreshRef = useRef<() => void>(null)
|
||||
|
||||
const { verticalMode } = useDashboardContext()
|
||||
|
||||
const [editing, setEditing] = useState(false)
|
||||
|
|
@ -124,7 +127,7 @@ export const EditableBox = ({
|
|||
<div className="name">{box.title ?? box.sensor ?? ''}</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<div className="action">
|
||||
<div className="action" onClick={() => refreshRef.current?.()}>
|
||||
<RefreshIcon />
|
||||
</div>
|
||||
<div className="action" onClick={() => setEditing(true)}>
|
||||
|
|
@ -134,10 +137,18 @@ export const EditableBox = ({
|
|||
</div>
|
||||
<div className="body">
|
||||
{box.sensor && box.data?.type === 'graph' && (
|
||||
<BoxGraphContent box={box} data={box.data} />
|
||||
<BoxGraphContent
|
||||
box={box}
|
||||
data={box.data}
|
||||
refreshRef={refreshRef}
|
||||
/>
|
||||
)}
|
||||
{box.sensor && box.data?.type === 'dial' && (
|
||||
<BoxDialContent box={box} data={box.data} />
|
||||
<BoxDialContent
|
||||
box={box}
|
||||
data={box.data}
|
||||
refreshRef={refreshRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -185,6 +196,7 @@ export const EditableBox = ({
|
|||
value={box}
|
||||
onSave={onEdit}
|
||||
onClose={() => setEditing(false)}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="filter-form">
|
||||
<form className="horizontal" onSubmit={handleSubmit}>
|
||||
|
|
@ -85,8 +89,6 @@ export const Filters = () => {
|
|||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button>Apply</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue