Dynamic dashboards now work
This commit is contained in:
parent
1d46147979
commit
6879b11b4a
|
|
@ -198,6 +198,7 @@ form.horizontal .input label {
|
|||
.grid-sensors {
|
||||
position: relative;
|
||||
margin: 0.25rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.grid-sensors .grid-box {
|
||||
|
|
@ -242,6 +243,19 @@ form.horizontal .input label {
|
|||
cursor: se-resize;
|
||||
}
|
||||
|
||||
.grid-sensors .grid-box .box .header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.grid-sensors .grid-box .box .header .drag-handle {
|
||||
flex: 1;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.grid-sensors .grid-box .box .header .actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.grid-sensors .box-preview {
|
||||
position: absolute;
|
||||
background-color: #3988FF;
|
||||
|
|
@ -249,4 +263,11 @@ form.horizontal .input label {
|
|||
transition: all 0.2s;
|
||||
z-index: 1;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
import { getSensors, SensorInfo } from '@/api/sensors'
|
||||
import { intervalToRange } from '@/utils/intervalToRange'
|
||||
import { useState } from 'preact/hooks'
|
||||
import { useQuery } from 'react-query'
|
||||
import { Filters, FilterValue } from './components/Filters'
|
||||
import { Sensor } from './components/Sensor'
|
||||
import { SensorSettings } from './components/SensorSettings'
|
||||
|
||||
export const DashboardPage = () => {
|
||||
const [editedSensor, setEditedSensor] = useState<SensorInfo>()
|
||||
|
||||
const [filter, setFilter] = useState<FilterValue>(() => {
|
||||
const range = intervalToRange('week', new Date(), new Date())
|
||||
|
||||
return { interval: 'week', customFrom: range[0], customTo: range[1] }
|
||||
})
|
||||
|
||||
const sensors = useQuery(['/sensors'], getSensors)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Filters preset={filter} onApply={setFilter} />
|
||||
<div className="sensors">
|
||||
{sensors.data?.map((s) => (
|
||||
<Sensor
|
||||
sensor={s}
|
||||
filter={filter}
|
||||
key={s.sensor}
|
||||
onEdit={setEditedSensor}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{editedSensor && (
|
||||
<SensorSettings
|
||||
sensor={editedSensor}
|
||||
onClose={() => setEditedSensor(undefined)}
|
||||
onUpdate={() => sensors.refetch()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,10 +1,16 @@
|
|||
import { createDashboard, getDashboards } from '@/api/dashboards'
|
||||
import {
|
||||
createDashboard,
|
||||
getDashboards,
|
||||
updateDashboard,
|
||||
} from '@/api/dashboards'
|
||||
import { createDashboardContent } from '@/utils/createDashboardContent'
|
||||
import { parseDashboard } from '@/utils/parseDashboard'
|
||||
import { useEffect, useMemo, useState } from 'preact/hooks'
|
||||
import { useQuery } from 'react-query'
|
||||
import { EditableBox } from './components/EditableBox'
|
||||
import { normalizeBoxes } from './utils/normalizeBoxes'
|
||||
import { DashboardGrid } from './components/DashboardGrid'
|
||||
import { DashboardHeader } from './components/DashboardHeader'
|
||||
import { DashboardContextProvider } from './contexts/DashboardContext'
|
||||
import { BoxDefinition } from './types'
|
||||
|
||||
export const NewDashboardPage = () => {
|
||||
const dashboards = useQuery(['/dashboards'], getDashboards)
|
||||
|
|
@ -15,6 +21,47 @@ export const NewDashboardPage = () => {
|
|||
[dashboard]
|
||||
)
|
||||
|
||||
const [boxes, setBoxes] = useState(dashboardContent?.boxes ?? [])
|
||||
|
||||
const handleChange = (cb: (boxes: BoxDefinition[]) => BoxDefinition[]) => {
|
||||
const newBoxes = cb(boxes)
|
||||
|
||||
setBoxes(newBoxes)
|
||||
|
||||
if (dashboard && dashboardContent) {
|
||||
updateDashboard({
|
||||
id: dashboard?.id,
|
||||
contents: {
|
||||
...dashboardContent,
|
||||
boxes: newBoxes,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
console.log('Nothing to refresh right now')
|
||||
}
|
||||
|
||||
const handleNewBox = () => {
|
||||
const box = {
|
||||
id: new Date().getTime().toString(),
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 12,
|
||||
h: 200,
|
||||
}
|
||||
|
||||
const otherBoxes = boxes.map((b) => {
|
||||
b.y += 200
|
||||
|
||||
return b
|
||||
})
|
||||
|
||||
setBoxes([box, ...otherBoxes])
|
||||
// TODO: Save
|
||||
}
|
||||
|
||||
// Terrible code - ensure there's default dashboard
|
||||
useEffect(() => {
|
||||
if (dashboards.data && !dashboard) {
|
||||
|
|
@ -27,33 +74,16 @@ export const NewDashboardPage = () => {
|
|||
}
|
||||
}, [dashboards.data, dashboard])
|
||||
|
||||
const [boxes, setBoxes] = useState(dashboardContent?.boxes ?? [])
|
||||
useEffect(() => {
|
||||
setBoxes(dashboardContent?.boxes ?? [])
|
||||
}, [dashboardContent])
|
||||
|
||||
return (
|
||||
<div className="grid-sensors">
|
||||
{boxes.map((b) => (
|
||||
<EditableBox
|
||||
box={b}
|
||||
key={b.id}
|
||||
boxes={boxes}
|
||||
onPosition={(p) => {
|
||||
setBoxes((previous) => {
|
||||
b.x = p.x
|
||||
b.y = p.y
|
||||
|
||||
return normalizeBoxes([...previous])
|
||||
})
|
||||
}}
|
||||
onResize={(s) => {
|
||||
setBoxes((previous) => {
|
||||
b.w = s.w
|
||||
b.h = s.h
|
||||
|
||||
return normalizeBoxes([...previous])
|
||||
})
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<DashboardContextProvider>
|
||||
<div className="dashboard">
|
||||
<DashboardHeader onRefresh={handleRefresh} onNewBox={handleNewBox} />
|
||||
<DashboardGrid boxes={boxes} onChange={handleChange} />
|
||||
</div>
|
||||
</DashboardContextProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
import { getSensorValues } from '@/api/sensorValues'
|
||||
import { useEffect, useRef } from 'preact/hooks'
|
||||
import { useQuery } from 'react-query'
|
||||
import { useDashboardContext } from '../contexts/DashboardContext'
|
||||
import { BoxDefinition } from '../types'
|
||||
|
||||
type Props = {
|
||||
box: BoxDefinition
|
||||
}
|
||||
|
||||
export const BoxGraphContent = ({ box }: Props) => {
|
||||
const { filter } = useDashboardContext()
|
||||
|
||||
const bodyRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const valuesQuery = {
|
||||
sensor: box.sensor ?? '-1',
|
||||
from: filter.customFrom,
|
||||
to: filter.customTo,
|
||||
}
|
||||
|
||||
const values = useQuery(['/sensor/values', valuesQuery], () =>
|
||||
getSensorValues(valuesQuery)
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: These should be probably returned by server, could be outdated
|
||||
const from = filter.customFrom
|
||||
const to = filter.customTo
|
||||
const minValue = parseFloat(box.min ?? '')
|
||||
const maxValue = parseFloat(box.max ?? '')
|
||||
const customRange = !isNaN(minValue) && !isNaN(maxValue)
|
||||
|
||||
if (bodyRef.current && values.data) {
|
||||
window.Plotly.newPlot(
|
||||
bodyRef.current,
|
||||
[
|
||||
{
|
||||
...(box.graphType === 'line' && {
|
||||
type: 'scatter',
|
||||
mode: 'lines',
|
||||
}),
|
||||
...(box.graphType === 'points' && {
|
||||
type: 'scatter',
|
||||
mode: 'markers',
|
||||
}),
|
||||
...(box.graphType === 'lineAndPoints' && {
|
||||
type: 'scatter',
|
||||
mode: 'lines+markers',
|
||||
}),
|
||||
...(box.graphType === 'bar' && { type: 'bar' }),
|
||||
x: values.data.map((v) => new Date(v.timestamp * 1000)),
|
||||
y: values.data.map((v) => v.value),
|
||||
line: {
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
{
|
||||
xaxis: { range: [from, to], type: 'date' },
|
||||
yaxis: {
|
||||
...(customRange && { range: [minValue, maxValue] }),
|
||||
...(box.unit && { ticksuffix: ` ${box.unit}` }),
|
||||
},
|
||||
margin: {
|
||||
l: 70,
|
||||
r: 20,
|
||||
b: 60,
|
||||
t: 20,
|
||||
pad: 5,
|
||||
},
|
||||
height: box.h - 50,
|
||||
},
|
||||
{
|
||||
responsive: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
}, [values.data, box])
|
||||
|
||||
return <div ref={bodyRef} />
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
import { getSensors } from '@/api/sensors'
|
||||
import { useState } from 'preact/hooks'
|
||||
import { useQuery } from 'react-query'
|
||||
import { BoxDefinition } from '../types'
|
||||
|
||||
type Props = {
|
||||
value: BoxDefinition
|
||||
onSave: (newValue: BoxDefinition) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export const BoxSettings = ({ value, onSave, onClose }: Props) => {
|
||||
const sensors = useQuery(['/sensors'], getSensors)
|
||||
|
||||
const [formState, setFormState] = useState(() => ({
|
||||
sensor: value.sensor,
|
||||
title: value.title,
|
||||
min: value.min,
|
||||
max: value.max,
|
||||
graphType: value.graphType,
|
||||
unit: value.unit,
|
||||
}))
|
||||
|
||||
const handleSave = async (e: Event) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
onClose()
|
||||
|
||||
onSave({
|
||||
...value,
|
||||
...formState,
|
||||
})
|
||||
}
|
||||
|
||||
const handleChange = (e: Event) => {
|
||||
const target = e.target as HTMLSelectElement | HTMLInputElement
|
||||
|
||||
setFormState({
|
||||
...formState,
|
||||
[target.name]: target.value,
|
||||
})
|
||||
}
|
||||
|
||||
const preventPropagation = (e: Event) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="settings-modal show" onMouseDown={onClose}>
|
||||
<div
|
||||
className="inner"
|
||||
onMouseDown={preventPropagation}
|
||||
onMouseUp={preventPropagation}
|
||||
onClick={preventPropagation}
|
||||
>
|
||||
<div className="body">
|
||||
<form onSubmit={handleSave}>
|
||||
<div className="input">
|
||||
<label>Sensor</label>
|
||||
<select
|
||||
name="sensor"
|
||||
value={formState.sensor || ''}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{sensors.data?.map((s) => (
|
||||
<option key={s.sensor} value={s.sensor}>
|
||||
{s.config?.name ?? s.sensor}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="input">
|
||||
<label>Title</label>
|
||||
<input
|
||||
name="title"
|
||||
value={formState.title}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="input">
|
||||
<label>Type</label>
|
||||
<select
|
||||
name="graphType"
|
||||
value={formState.graphType || 'line'}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<option value="line">Line</option>
|
||||
<option value="points">Points</option>
|
||||
<option value="lineAndPoints">Line + Points</option>
|
||||
<option value="bar">Bar</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="input">
|
||||
<label>Unit</label>
|
||||
<input
|
||||
name="unit"
|
||||
value={formState.unit}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="input">
|
||||
<label>Min value</label>
|
||||
<input name="min" value={formState.min} onChange={handleChange} />
|
||||
</div>
|
||||
<div className="input">
|
||||
<label>Max value</label>
|
||||
<input name="max" value={formState.max} onChange={handleChange} />
|
||||
</div>
|
||||
|
||||
<div className="actions">
|
||||
<button className="cancel" onClick={onClose} type="button">
|
||||
Cancel
|
||||
</button>
|
||||
<button>Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { BoxDefinition } from '../types'
|
||||
import { normalizeBoxes } from '../utils/normalizeBoxes'
|
||||
import { EditableBox } from './EditableBox'
|
||||
|
||||
type Props = {
|
||||
boxes: BoxDefinition[]
|
||||
onChange: (cb: (previous: BoxDefinition[]) => BoxDefinition[]) => void
|
||||
}
|
||||
|
||||
export const DashboardGrid = ({ boxes, onChange }: Props) => {
|
||||
return (
|
||||
<div className="grid-sensors">
|
||||
{boxes.map((b) => (
|
||||
<EditableBox
|
||||
box={b}
|
||||
key={b.id}
|
||||
boxes={boxes}
|
||||
onPosition={(p) => {
|
||||
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
|
||||
)
|
||||
)
|
||||
)
|
||||
}}
|
||||
onEdit={(newB) => {
|
||||
onChange((previous) =>
|
||||
previous.map((b) => (b.id === newB.id ? newB : b))
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { Filters } from './Filters'
|
||||
|
||||
type Props = {
|
||||
onNewBox: () => void
|
||||
onRefresh: () => void
|
||||
}
|
||||
|
||||
export const DashboardHeader = ({ onNewBox, onRefresh }: Props) => {
|
||||
return (
|
||||
<div className="filters">
|
||||
<div className="inner">
|
||||
<button onClick={onNewBox}>Add box</button>
|
||||
<Filters />
|
||||
<button onClick={onRefresh}>Refresh all</button>
|
||||
</div>
|
||||
<div className="shadow"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,21 +1,32 @@
|
|||
import { getElementPosition } from '@/utils/getElementPosition'
|
||||
import { useWindowEvent } from '@/utils/hooks/useWindowEvent'
|
||||
import { useRef } from 'preact/hooks'
|
||||
import { useRef, useState } from 'preact/hooks'
|
||||
import { GRID_WIDTH } from '../constants'
|
||||
import { ResizingMode, useResize } from '../hooks/useResize'
|
||||
import { useDragging } from '../hooks/useDragging'
|
||||
import { BoxDefinition } from '../types'
|
||||
import { BoxSettings } from './BoxSettings'
|
||||
import { BoxGraphContent } from './BoxGraphContent'
|
||||
|
||||
type Props = {
|
||||
box: BoxDefinition
|
||||
boxes: BoxDefinition[]
|
||||
onPosition: (p: { x: number; y: number }) => void
|
||||
onResize: (p: { w: number; h: number }) => void
|
||||
onEdit: (box: BoxDefinition) => void
|
||||
}
|
||||
|
||||
export const EditableBox = ({ box, boxes, onPosition, onResize }: Props) => {
|
||||
export const EditableBox = ({
|
||||
box,
|
||||
boxes,
|
||||
onPosition,
|
||||
onResize,
|
||||
onEdit,
|
||||
}: Props) => {
|
||||
const boxRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [editing, setEditing] = useState(false)
|
||||
|
||||
const outerWidth = boxRef.current?.parentElement?.offsetWidth ?? 100
|
||||
const cellWidth = outerWidth / GRID_WIDTH
|
||||
|
||||
|
|
@ -34,8 +45,8 @@ export const EditableBox = ({ box, boxes, onPosition, onResize }: Props) => {
|
|||
const handleMouseDown = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!dragging) {
|
||||
const pos = getElementPosition(e.target as HTMLDivElement)
|
||||
if (!dragging.active && boxRef.current) {
|
||||
const pos = getElementPosition(boxRef.current)
|
||||
|
||||
setDragging({
|
||||
active: true,
|
||||
|
|
@ -51,7 +62,7 @@ export const EditableBox = ({ box, boxes, onPosition, onResize }: Props) => {
|
|||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
if (!resizing) {
|
||||
if (resizing.mode === ResizingMode.NONE) {
|
||||
setResizing({
|
||||
mode: target,
|
||||
w: (box.w / GRID_WIDTH) * outerWidth,
|
||||
|
|
@ -63,7 +74,7 @@ export const EditableBox = ({ box, boxes, onPosition, onResize }: Props) => {
|
|||
}
|
||||
|
||||
useWindowEvent('mouseup', () => {
|
||||
if (dragging) {
|
||||
if (dragging.active) {
|
||||
onPosition(draggingPosition)
|
||||
setDragging({ ...dragging, active: false })
|
||||
}
|
||||
|
|
@ -79,7 +90,6 @@ export const EditableBox = ({ box, boxes, onPosition, onResize }: Props) => {
|
|||
<div
|
||||
ref={boxRef}
|
||||
className={`grid-box${dragging ? ' dragging' : ''}`}
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{
|
||||
left: (box.x / GRID_WIDTH) * 100 + '%',
|
||||
top: box.y + 'px',
|
||||
|
|
@ -93,6 +103,21 @@ export const EditableBox = ({ box, boxes, onPosition, onResize }: Props) => {
|
|||
}}
|
||||
>
|
||||
<div className="box" style={{ height: '100%' }}>
|
||||
<div className="header">
|
||||
<div className="drag-handle" onMouseDown={handleMouseDown}>
|
||||
<div className="name">{box.title ?? box.sensor ?? ''}</div>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<button className="config" onClick={() => setEditing(true)}>
|
||||
Config
|
||||
</button>
|
||||
<button className="refresh">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="body">
|
||||
{box.sensor && <BoxGraphContent box={box} />}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="resize-h"
|
||||
onMouseDown={handleResizeDown(ResizingMode.WIDTH)}
|
||||
|
|
@ -131,6 +156,14 @@ export const EditableBox = ({ box, boxes, onPosition, onResize }: Props) => {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{editing && (
|
||||
<BoxSettings
|
||||
value={box}
|
||||
onSave={onEdit}
|
||||
onClose={() => setEditing(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { DateTimeInput } from '@/components/DateTimeInput'
|
||||
import { intervalToRange } from '@/utils/intervalToRange'
|
||||
import { useState } from 'preact/hooks'
|
||||
import { useDashboardContext } from '../contexts/DashboardContext'
|
||||
|
||||
export type FilterInterval =
|
||||
| 'hour'
|
||||
|
|
@ -16,12 +17,9 @@ export type FilterValue = {
|
|||
customTo: Date
|
||||
}
|
||||
|
||||
type Props = {
|
||||
preset: FilterValue
|
||||
onApply: (v: FilterValue) => void
|
||||
}
|
||||
export const Filters = () => {
|
||||
const { filter: preset, setFilter } = useDashboardContext()
|
||||
|
||||
export const Filters = ({ preset, onApply }: Props) => {
|
||||
const [value, setValue] = useState(preset)
|
||||
|
||||
const isCustomSelected = value.interval === 'custom'
|
||||
|
|
@ -30,7 +28,7 @@ export const Filters = ({ preset, onApply }: Props) => {
|
|||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
onApply(value)
|
||||
setFilter(value)
|
||||
}
|
||||
|
||||
const handleIntervalChange = (e: Event) => {
|
||||
|
|
@ -47,58 +45,49 @@ export const Filters = ({ preset, onApply }: Props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="filters">
|
||||
<div className="inner">
|
||||
<div className="filter-form">
|
||||
<form className="horizontal" onSubmit={handleSubmit}>
|
||||
<div className="input">
|
||||
<label>Interval</label>
|
||||
<select
|
||||
name="interval"
|
||||
onChange={handleIntervalChange}
|
||||
value={value.interval}
|
||||
>
|
||||
<option value="hour">Hour</option>
|
||||
<option value="day">Day</option>
|
||||
<option value="week">Week</option>
|
||||
<option value="month">Month</option>
|
||||
<option value="year">Year</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
<div className="filter-form">
|
||||
<form className="horizontal" onSubmit={handleSubmit}>
|
||||
<div className="input">
|
||||
<label>Interval</label>
|
||||
<select
|
||||
name="interval"
|
||||
onChange={handleIntervalChange}
|
||||
value={value.interval}
|
||||
>
|
||||
<option value="hour">Hour</option>
|
||||
<option value="day">Day</option>
|
||||
<option value="week">Week</option>
|
||||
<option value="month">Month</option>
|
||||
<option value="year">Year</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{isCustomSelected && (
|
||||
<>
|
||||
<div className="input date-time">
|
||||
<label>From</label>
|
||||
<div>
|
||||
<DateTimeInput
|
||||
value={value.customFrom}
|
||||
onChange={(v) => setValue({ ...value, customFrom: v })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="input date-time">
|
||||
<label>To</label>
|
||||
<div>
|
||||
<DateTimeInput
|
||||
value={value.customTo}
|
||||
onChange={(v) => setValue({ ...value, customTo: v })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isCustomSelected && (
|
||||
<>
|
||||
<div className="input date-time">
|
||||
<label>From</label>
|
||||
<div>
|
||||
<DateTimeInput
|
||||
value={value.customFrom}
|
||||
onChange={(v) => setValue({ ...value, customFrom: v })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="input date-time">
|
||||
<label>To</label>
|
||||
<div>
|
||||
<DateTimeInput
|
||||
value={value.customTo}
|
||||
onChange={(v) => setValue({ ...value, customTo: v })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button>Apply</button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="actions">
|
||||
{/*<label class="checkbox-label"><input type="checkbox"><span>auto-refresh</span></label>*/}
|
||||
<button>Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="shadow"></div>
|
||||
<button>Apply</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import { intervalToRange } from '@/utils/intervalToRange'
|
||||
import { ComponentChild, createContext } from 'preact'
|
||||
import { StateUpdater, useContext, useMemo, useState } from 'preact/hooks'
|
||||
import { FilterValue } from '../components/Filters'
|
||||
|
||||
type DashboardContextType = {
|
||||
filter: FilterValue
|
||||
setFilter: StateUpdater<FilterValue>
|
||||
}
|
||||
|
||||
const DashboardContext = createContext<DashboardContextType | null>(null)
|
||||
|
||||
export const DashboardContextProvider = ({
|
||||
children,
|
||||
}: {
|
||||
children: ComponentChild
|
||||
}) => {
|
||||
const [filter, setFilter] = useState<FilterValue>(() => {
|
||||
const range = intervalToRange('week', new Date(), new Date())
|
||||
|
||||
return { interval: 'week', customFrom: range[0], customTo: range[1] }
|
||||
})
|
||||
|
||||
const value = useMemo(() => ({ filter, setFilter }), [filter])
|
||||
|
||||
return (
|
||||
<DashboardContext.Provider value={value}>
|
||||
{children}
|
||||
</DashboardContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useDashboardContext = () => {
|
||||
const ctx = useContext(DashboardContext)
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error('useDashboardContext used outside of DashboardContext')
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
|
@ -89,8 +89,7 @@ export const useResize = ({ cellWidth, box, boxes }: Props) => {
|
|||
state.mode === ResizingMode.ALL ||
|
||||
state.mode === ResizingMode.WIDTH
|
||||
) {
|
||||
newState.w =
|
||||
(box.w / GRID_WIDTH) * outerWidth + e.clientX - state.offsetX
|
||||
newState.w = box.w * cellWidth + e.clientX - state.offsetX
|
||||
}
|
||||
|
||||
setState(newState)
|
||||
|
|
|
|||
|
|
@ -1,30 +1,39 @@
|
|||
import { BoxDefinition } from '../types'
|
||||
|
||||
export function normalizeBoxes(boxes: BoxDefinition[]) {
|
||||
let sorted = false
|
||||
|
||||
// TODO: This is not optimized at all
|
||||
for (let i = 0; i < boxes.length; i++) {
|
||||
const box = boxes[i]
|
||||
while (!sorted) {
|
||||
// Sort boxes to have the lowest ones first
|
||||
boxes.sort((a, b) => a.y - b.y)
|
||||
|
||||
if (box.y > 0) {
|
||||
const above = boxes
|
||||
.filter(
|
||||
(b) =>
|
||||
b.id !== box.id &&
|
||||
b.x < box.x + box.w &&
|
||||
box.x < b.x + b.w &&
|
||||
b.y < box.y
|
||||
)
|
||||
.sort((a, b) => b.y - a.y)
|
||||
sorted = true
|
||||
|
||||
if (above.length === 0) {
|
||||
box.y = 0
|
||||
i = -1
|
||||
} else {
|
||||
const newY = above[0].h + above[0].y
|
||||
for (const box of boxes) {
|
||||
if (box.y > 0) {
|
||||
const above = boxes
|
||||
.filter(
|
||||
(b) =>
|
||||
b.id !== box.id &&
|
||||
b.x < box.x + box.w &&
|
||||
box.x < b.x + b.w &&
|
||||
b.y < box.y
|
||||
)
|
||||
.sort((a, b) => b.y - a.y)
|
||||
|
||||
if (box.y !== newY) {
|
||||
box.y = newY
|
||||
i = -1
|
||||
if (above.length === 0) {
|
||||
box.y = 0
|
||||
sorted = false
|
||||
break
|
||||
} else {
|
||||
const newY = above[0].h + above[0].y
|
||||
|
||||
if (box.y !== newY) {
|
||||
box.y = newY
|
||||
sorted = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,12 @@ export type DashboardContentBox = {
|
|||
y: number
|
||||
w: number
|
||||
h: number
|
||||
sensor: string
|
||||
sensor?: string
|
||||
title?: string
|
||||
min?: string
|
||||
max?: string
|
||||
unit?: string
|
||||
graphType?: string
|
||||
}
|
||||
|
||||
export const parseDashboard = (input: string) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue