Added dashboard migration, reworked types

This commit is contained in:
Jan Zípek 2022-08-28 22:52:57 +02:00
parent 7846a09668
commit e74c9d0793
Signed by: kamen
GPG Key ID: A17882625B33AC31
16 changed files with 158 additions and 100 deletions

View File

@ -1,4 +1,4 @@
import { DashboardContent } from '@/utils/parseDashboard' import { DashboardContent } from '@/utils/dashboard/parseDashboard'
import { request } from './request' import { request } from './request'
export type DashboardInfo = { export type DashboardInfo = {

View File

@ -5,7 +5,7 @@ import {
} from '@/api/dashboards' } from '@/api/dashboards'
import { UserLayout } from '@/layouts/UserLayout/UserLayout' import { UserLayout } from '@/layouts/UserLayout/UserLayout'
import { createDashboardContent } from '@/utils/createDashboardContent' import { createDashboardContent } from '@/utils/createDashboardContent'
import { parseDashboard } from '@/utils/parseDashboard' import { parseDashboard } from '@/utils/dashboard/parseDashboard'
import { useEffect, useMemo, useState } from 'preact/hooks' import { useEffect, useMemo, useState } from 'preact/hooks'
import { useQuery, useQueryClient } from 'react-query' import { useQuery, useQueryClient } from 'react-query'
import { DashboardGrid } from './components/DashboardGrid/DashboardGrid' import { DashboardGrid } from './components/DashboardGrid/DashboardGrid'

View File

@ -1,7 +1,11 @@
import { getSensors } from '@/api/sensors' import { getSensors } from '@/api/sensors'
import { Modal } from '@/components/Modal' import { Modal } from '@/components/Modal'
import { useConfirmModal } from '@/contexts/ConfirmModalsContext' import { useConfirmModal } from '@/contexts/ConfirmModalsContext'
import { DashboardDialData, DashboardGraphData } from '@/utils/parseDashboard' import {
DashboardDialData,
DashboardGraphData,
} from '@/utils/dashboard/parseDashboard'
import { useForm } from '@/utils/hooks/useForm'
import { useState } from 'preact/hooks' import { useState } from 'preact/hooks'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
import { BoxDefinition } from '../../types' import { BoxDefinition } from '../../types'
@ -18,14 +22,28 @@ type Props = {
export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => { export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
const sensors = useQuery(['/sensors'], getSensors) const sensors = useQuery(['/sensors'], getSensors)
const [formState, setFormState] = useState(() => ({
sensor: value.sensor,
title: value.title,
type: value.data?.type ?? 'graph',
}))
const [data, setData] = useState(() => value.data) const [data, setData] = useState(() => value.data)
const {
value: formState,
handleChange,
handleSubmit,
} = useForm({
defaultValue: () => ({
title: value.title,
type: value.data?.type ?? '',
}),
onSubmit: async (v) => {
onClose()
onSave({
...value,
title: v.title,
data: data,
})
},
})
const deleteConfirm = useConfirmModal({ const deleteConfirm = useConfirmModal({
content: 'Are you sure you want to delete the box?', content: 'Are you sure you want to delete the box?',
onConfirm: () => { onConfirm: () => {
@ -33,48 +51,10 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
}, },
}) })
const handleSave = async (e: Event) => {
e.preventDefault()
e.stopPropagation()
onClose()
onSave({
...value,
sensor: formState.sensor,
title: formState.title,
data: data,
})
}
const handleChange = (e: Event) => {
const target = e.target as HTMLSelectElement | HTMLInputElement
setFormState({
...formState,
[target.name]: target.value,
})
}
return ( return (
<Modal onClose={onClose} open> <Modal onClose={onClose} open>
<form onSubmit={handleSave}> <form onSubmit={handleSubmit}>
<div className="input"> {
<label>Sensor</label>
<select
name="sensor"
value={formState.sensor || ''}
onChange={handleChange}
>
{sensors.data?.map((s) => (
<option key={s.id} value={s.id}>
{s.name}
</option>
))}
</select>
</div>
{formState.sensor && (
<> <>
<div className="input"> <div className="input">
<label>Title</label> <label>Title</label>
@ -90,6 +70,7 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
name="type" name="type"
value={formState.type} value={formState.type}
onChange={handleChange} onChange={handleChange}
required
> >
<option value="graph">Graph</option> <option value="graph">Graph</option>
<option value="dial">Dial</option> <option value="dial">Dial</option>
@ -100,6 +81,7 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
<GraphSettings <GraphSettings
value={data as DashboardGraphData} value={data as DashboardGraphData}
onChange={setData} onChange={setData}
sensors={sensors.data ?? []}
/> />
)} )}
@ -107,10 +89,11 @@ export const BoxSettings = ({ value, onSave, onClose, onRemove }: Props) => {
<DialSettings <DialSettings
value={data as DashboardDialData} value={data as DashboardDialData}
onChange={setData} onChange={setData}
sensors={sensors.data ?? []}
/> />
)} )}
</> </>
)} }
<div className="actions"> <div className="actions">
<button className="remove" type="button" onClick={deleteConfirm.show}> <button className="remove" type="button" onClick={deleteConfirm.show}>

View File

@ -1,26 +1,21 @@
import { DashboardDialData } from '@/utils/parseDashboard' import { SensorInfo } from '@/api/sensors'
import { useEffect, useState } from 'preact/hooks' import { DashboardDialData } from '@/utils/dashboard/parseDashboard'
import { useForm } from '@/utils/hooks/useForm'
import { omit } from '@/utils/omit'
import { useEffect } from 'preact/hooks'
type Props = { type Props = {
sensors: SensorInfo[]
value?: DashboardDialData value?: DashboardDialData
onChange: (data: DashboardDialData) => void onChange: (data: DashboardDialData) => void
} }
export const DialSettings = ({ value, onChange }: Props) => { export const DialSettings = ({ sensors, value, onChange }: Props) => {
const [formState, setFormState] = useState(() => ({ const { value: formState, handleChange } = useForm({
unit: value?.unit, defaultValue: () => ({
decimals: value?.decimals, ...(value && omit(value, ['type'])),
multiplier: value?.multiplier, }),
})) })
const handleChange = (e: Event) => {
const target = e.target as HTMLSelectElement | HTMLInputElement
setFormState({
...formState,
[target.name]: target.value,
})
}
useEffect(() => { useEffect(() => {
onChange({ ...formState, type: 'dial' }) onChange({ ...formState, type: 'dial' })
@ -28,6 +23,21 @@ export const DialSettings = ({ value, onChange }: Props) => {
return ( return (
<> <>
<div className="input">
<label>Sensor</label>
<select
name="sensor"
value={formState.sensor || ''}
onChange={handleChange}
required
>
{sensors.map((s) => (
<option key={s.id} value={s.id}>
{s.name}
</option>
))}
</select>
</div>
<div className="input"> <div className="input">
<label>Unit</label> <label>Unit</label>
<input name="unit" value={formState.unit} onChange={handleChange} /> <input name="unit" value={formState.unit} onChange={handleChange} />

View File

@ -1,25 +1,21 @@
import { SensorInfo } from '@/api/sensors'
import { DashboardGraphData } from '@/utils/dashboard/parseDashboard'
import { useForm } from '@/utils/hooks/useForm'
import { omit } from '@/utils/omit' import { omit } from '@/utils/omit'
import { DashboardGraphData } from '@/utils/parseDashboard' import { useEffect } from 'preact/hooks'
import { useEffect, useState } from 'preact/hooks'
type Props = { type Props = {
sensors: SensorInfo[]
value?: DashboardGraphData value?: DashboardGraphData
onChange: (data: DashboardGraphData) => void onChange: (data: DashboardGraphData) => void
} }
export const GraphSettings = ({ value, onChange }: Props) => { export const GraphSettings = ({ value, onChange, sensors }: Props) => {
const [formState, setFormState] = useState(() => ({ const { value: formState, handleChange } = useForm({
...(value && omit(value, ['type'])), defaultValue: () => ({
})) ...(value && omit(value, ['type'])),
}),
const handleChange = (e: Event) => { })
const target = e.target as HTMLSelectElement | HTMLInputElement
setFormState({
...formState,
[target.name]: target.value,
})
}
useEffect(() => { useEffect(() => {
onChange({ ...formState, type: 'graph' }) onChange({ ...formState, type: 'graph' })
@ -27,6 +23,22 @@ export const GraphSettings = ({ value, onChange }: Props) => {
return ( return (
<> <>
<div className="input">
<label>Sensor</label>
<select
name="sensor"
value={formState.sensor || ''}
onChange={handleChange}
required
>
{sensors.map((s) => (
<option key={s.id} value={s.id}>
{s.name}
</option>
))}
</select>
</div>
<div className="input"> <div className="input">
<label>Graph Type</label> <label>Graph Type</label>
<select <select

View File

@ -1,5 +1,5 @@
import { getLatestSensorValue } from '@/api/sensorValues' import { getLatestSensorValue } from '@/api/sensorValues'
import { DashboardDialData } from '@/utils/parseDashboard' import { DashboardDialData } from '@/utils/dashboard/parseDashboard'
import { RefObject } from 'preact' import { RefObject } from 'preact'
import { useMemo } from 'preact/hooks' import { useMemo } from 'preact/hooks'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
@ -13,16 +13,18 @@ type Props = {
refreshRef: RefObject<() => void> refreshRef: RefObject<() => void>
} }
export const BoxDialContent = ({ box, data, refreshRef }: Props) => { export const BoxDialContent = ({ data, refreshRef }: Props) => {
const { filter } = useDashboardContext() const { filter } = useDashboardContext()
const valuesQuery = { const valuesQuery = {
sensor: box.sensor ?? '-1', sensor: data.sensor ?? '-1',
to: filter.customTo, to: filter.customTo,
} }
const value = useQuery(['/sensor/values/latest', valuesQuery], () => const value = useQuery(
getLatestSensorValue(valuesQuery) ['/sensor/values/latest', valuesQuery],
() => getLatestSensorValue(valuesQuery),
{ enabled: !!data.sensor }
) )
refreshRef.current = () => { refreshRef.current = () => {

View File

@ -3,7 +3,7 @@ import { useDashboardContext } from '@/pages/dashboard/contexts/DashboardContext
import { BoxDefinition } from '@/pages/dashboard/types' import { BoxDefinition } from '@/pages/dashboard/types'
import { max } from '@/utils/max' import { max } from '@/utils/max'
import { min } from '@/utils/min' import { min } from '@/utils/min'
import { DashboardGraphData } from '@/utils/parseDashboard' import { DashboardGraphData } from '@/utils/dashboard/parseDashboard'
import { RefObject } from 'preact' import { RefObject } from 'preact'
import { useEffect, useRef } from 'preact/hooks' import { useEffect, useRef } from 'preact/hooks'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
@ -21,13 +21,15 @@ export const BoxGraphContent = ({ box, data, refreshRef }: Props) => {
const bodyRef = useRef<HTMLDivElement>(null) const bodyRef = useRef<HTMLDivElement>(null)
const valuesQuery = { const valuesQuery = {
sensor: box.sensor ?? '-1', sensor: data.sensor ?? '-1',
from: filter.customFrom, from: filter.customFrom,
to: filter.customTo, to: filter.customTo,
} }
const values = useQuery(['/sensor/values', valuesQuery], () => const values = useQuery(
getSensorValues(valuesQuery) ['/sensor/values', valuesQuery],
() => getSensorValues(valuesQuery),
{ enabled: !!data.sensor }
) )
refreshRef.current = () => { refreshRef.current = () => {

View File

@ -132,7 +132,7 @@ export const EditableBox = ({
<div className="box" style={{ height: '100%' }}> <div className="box" style={{ height: '100%' }}>
<div className="header"> <div className="header">
<div className="drag-handle" onMouseDown={handleMouseDown}> <div className="drag-handle" onMouseDown={handleMouseDown}>
<div className="name">{box.title || box.sensor || ''}</div> <div className="name">{box.title || ''}</div>
</div> </div>
<div className="actions"> <div className="actions">
<div className="action" onClick={() => refreshRef.current?.()}> <div className="action" onClick={() => refreshRef.current?.()}>
@ -144,14 +144,14 @@ export const EditableBox = ({
</div> </div>
</div> </div>
<div className="body"> <div className="body">
{box.sensor && box.data?.type === 'graph' && ( {box.data?.type === 'graph' && (
<BoxGraphContent <BoxGraphContent
box={box} box={box}
data={box.data} data={box.data}
refreshRef={refreshRef} refreshRef={refreshRef}
/> />
)} )}
{box.sensor && box.data?.type === 'dial' && ( {box.data?.type === 'dial' && (
<BoxDialContent <BoxDialContent
box={box} box={box}
data={box.data} data={box.data}

View File

@ -1,3 +1,3 @@
import { DashboardContentBox } from '@/utils/parseDashboard' import { DashboardContentBox } from '@/utils/dashboard/parseDashboard'
export type BoxDefinition = DashboardContentBox export type BoxDefinition = DashboardContentBox

View File

@ -1,4 +1,4 @@
import { DashboardContent } from './parseDashboard' import { DashboardContent } from './dashboard/parseDashboard'
export const createDashboardContent = (): DashboardContent => ({ export const createDashboardContent = (): DashboardContent => ({
version: '1.0', version: '1.0',

View File

@ -0,0 +1,9 @@
export type DashboardMigration = {
from: string
to: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
migrate: (v: any) => any
}
export const createDashboardMigration = (migration: DashboardMigration) =>
migration

View File

@ -0,0 +1,11 @@
import * as migrations from './migrations'
export const getDashboardMigrations = (version: string) => {
const sortedMigrations = Object.values(migrations).sort((a, b) =>
a.from.localeCompare(b.to)
)
const migration = sortedMigrations.findIndex((m) => m.from === version)
return migration < 0 ? [] : sortedMigrations.slice(migration)
}

View File

@ -0,0 +1 @@
export * from './migrationOf10to11'

View File

@ -0,0 +1,17 @@
import { createDashboardMigration } from '../createDashboardMigration'
export const migrationOf100To101 = createDashboardMigration({
from: '1.0',
to: '1.1',
migrate(v) {
for (const box of v.boxes) {
if (box.data) {
box.data.sensor = box.sensor
}
delete box.sensor
}
return v
},
})

View File

@ -1,3 +1,5 @@
import { getDashboardMigrations } from './getDashboardMigrations'
export type DashboardContent = { export type DashboardContent = {
version: string version: string
boxes: DashboardContentBox[] boxes: DashboardContentBox[]
@ -9,13 +11,13 @@ export type DashboardContentBox = {
y: number y: number
w: number w: number
h: number h: number
sensor?: string
title?: string title?: string
data?: DashboardGraphData | DashboardDialData data?: DashboardGraphData | DashboardDialData
} }
export type DashboardGraphData = { export type DashboardGraphData = {
type: 'graph' type: 'graph'
sensor?: string
min?: string min?: string
max?: string max?: string
unit?: string unit?: string
@ -34,11 +36,20 @@ export type DashboardGraphData = {
export type DashboardDialData = { export type DashboardDialData = {
type: 'dial' type: 'dial'
sensor?: string
unit?: string unit?: string
decimals?: string decimals?: string
multiplier?: string multiplier?: string
} }
export const parseDashboard = (input: string) => { export const parseDashboard = (input: string) => {
return JSON.parse(input) as DashboardContent const data = JSON.parse(input) as DashboardContent
const migrations = getDashboardMigrations(data.version)
return migrations.reduce((acc, migration) => {
const res = migration.migrate(acc)
res.version = migration.to
return res
}, data)
} }

View File

@ -1,7 +1,7 @@
import { useCallback, useRef, useState } from 'preact/hooks' import { useCallback, useRef, useState } from 'preact/hooks'
type Props<TValue> = { type Props<TValue> = {
onSubmit: (value: TValue) => void onSubmit?: (value: TValue) => void
defaultValue: TValue | (() => TValue) defaultValue: TValue | (() => TValue)
} }
@ -33,7 +33,7 @@ export const useForm = <TValue = unknown>({
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
submitRef.current(stateRef.current) submitRef.current?.(stateRef.current)
}, []) }, [])
return { return {