Скорректирования работа редактирования и сохранения настроек графиков

This commit is contained in:
goodmice 2022-08-10 15:22:57 +05:00
parent 15333510fd
commit a8613e6b75
6 changed files with 128 additions and 126 deletions

View File

@ -1,4 +1,4 @@
import { Button, Form, FormItemProps, Input, InputNumber, Select, Space, Tooltip, Typography } from 'antd'
import { Button, Form, FormItemProps, Input, InputNumber, Select, Tooltip } from 'antd'
import { CSSProperties, memo, useCallback, useEffect, useMemo, useState } from 'react'
import { ColorPicker, Color } from '../ColorPicker'
@ -17,87 +17,68 @@ const lineTypes = [
{ value: 'needle', label: 'Иглы' },
]
export type D3MonitoringChartEditorProps<DataType> = Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, 'onChange'> & {
chart?: ExtendedChartDataset<DataType> | null
export type D3MonitoringChartEditorProps<DataType> = {
chart: ExtendedChartDataset<DataType>
onChange: (value: ExtendedChartDataset<DataType>) => boolean
}
const _D3MonitoringChartEditor = <DataType,>({
chart: value,
onChange,
style,
...other
}: D3MonitoringChartEditorProps<DataType>) => {
const [domain, setDomain] = useState<MinMax>({})
const [color, setColor] = useState<Color>()
const [form] = Form.useForm()
const onDomainChange = useCallback((mm: MinMax) => {
setDomain((prev) => ({
min: ('min' in mm) ? mm.min : prev?.min,
max: ('max' in mm) ? mm.max : prev?.max,
}))
}, [])
const onColorChange = useCallback((color: Color) => setColor((prev) => color ?? prev), [])
useEffect(() => {
if (value?.type) {
form.setFieldsValue(value)
} else {
form.resetFields()
}
setColor(value?.color ? new Color(String(value.color)) : undefined)
setDomain(value?.xDomain ?? {})
}, [value, form])
const onSave = useCallback(() => {
if (!value) return
const onSave = useCallback((props: Partial<ExtendedChartDataset<DataType>>) => {
const values = form.getFieldsValue()
const newValue = {
...value,
color,
xDomain: domain,
...values,
...props
}
onChange(newValue)
}, [form, domain, color, value])
}, [value])
const divStyle: CSSProperties = useMemo(() => ({
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'center',
...style,
}), [style])
const onDomainChange = useCallback((mm: MinMax) => {
onSave({ xDomain: {
min: ('min' in mm) ? mm.min : value.xDomain?.min,
max: ('max' in mm) ? mm.max : value.xDomain?.max,
}})
}, [value])
const onColorChange = useCallback((color: Color) => {
onSave({ color: color.toHexString() })
}, [value])
useEffect(() => {
if (value.type)
form.setFieldsValue(value)
else
form.resetFields()
}, [value, form])
return (
<div style={divStyle} {...other}>
<Form form={form} onChange={onSave}>
<Tooltip title={'Возможность изменения типов линий будет добавлена в будущих обновлениях'}>
<Item label={'Тип'}><Select disabled value={value?.type ?? 'Неизвестный'} options={lineTypes} /></Item>
</Tooltip>
<Item label={'Название'}>
<Input.Group compact>
<Item name={'label'} rules={[{ required: true }]}><Input placeholder={'Полное'} /></Item>
<Item name={'shortLabel'}><Input placeholder={'Краткое'}/></Item>
</Input.Group>
</Item>
<Item label={'Диапазон'}>
<Input.Group compact>
<Item><InputNumber value={domain?.min} onChange={(min) => onDomainChange({ min })} placeholder={'Мин'} /></Item>
<Item><InputNumber value={domain?.max} onChange={(max) => onDomainChange({ max })} placeholder={'Макс'} /></Item>
<Button
disabled={!domain?.min && !domain?.max}
onClick={() => onDomainChange({ min: undefined, max: undefined })}
>Авто</Button>
</Input.Group>
</Item>
<Item label={'Цвет линий'}><ColorPicker onChange={onColorChange} value={color} /></Item>
</Form>
</div>
<Form form={form}>
<Tooltip title={'Возможность изменения типов линий будет добавлена в будущих обновлениях'}>
<Item label={'Тип'}><Select disabled value={value.type ?? 'Неизвестный'} options={lineTypes} /></Item>
</Tooltip>
<Item label={'Название'}>
<Input.Group compact>
<Item name={'label'} rules={[{ required: true }]}><Input placeholder={'Полное'} onChange={(e) => onSave({ label: e.target.value })} /></Item>
<Item name={'shortLabel'}><Input placeholder={'Краткое'} onChange={(e) => onSave({ shortLabel: e.target.value })} /></Item>
</Input.Group>
</Item>
<Item label={'Диапазон'}>
<Input.Group compact>
<InputNumber value={value.xDomain?.min} onChange={(min) => onDomainChange({ min })} placeholder={'Мин'} />
<InputNumber value={value.xDomain?.max} onChange={(max) => onDomainChange({ max })} placeholder={'Макс'} />
<Button
disabled={!Number.isFinite(value.xDomain?.min) && !Number.isFinite(value.xDomain?.max)}
onClick={() => onDomainChange({ min: undefined, max: undefined })}
>Авто</Button>
</Input.Group>
</Item>
<Item label={'Цвет линий'}><ColorPicker onChange={onColorChange} value={value.color} /></Item>
</Form>
)
}

View File

@ -184,31 +184,27 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
const [settingsVisible, setSettingsVisible] = useState<boolean>(true)
useEffect(() => {
if (!chartName) {
setDatasets(datasetGroups)
return
}
let datasets: ExtendedChartDataset<DataType>[][] = []
let needInsert = false
invokeWebApiWrapperAsync(
async () => {
const sets = await UserSettingsService.get(chartName)
needInsert = !sets
datasets = sets ?? datasetGroups
let sets = chartName ? await UserSettingsService.get(chartName) : null
if (typeof sets === 'string')
sets = JSON.parse(sets)
if (Array.isArray(sets)) {
setDatasets(sets)
} else if (Array.isArray(datasetGroups)) {
setDatasets(datasetGroups)
if (chartName) {
invokeWebApiWrapperAsync(
async () => await UserSettingsService.insert(chartName, datasetGroups),
undefined,
'Не удалось сохранить настройки графиков'
)
}
}
},
undefined,
'Не удалось загрузить настройки графиков'
)
setDatasets(datasets)
if (needInsert) {
invokeWebApiWrapperAsync(
async () => {
await UserSettingsService.insert(chartName, datasets)
},
undefined,
'Не удалось сохранить настройки графиков'
)
}
}, [datasetGroups, chartName])
const offset = usePartialProps(_offset, defaultOffsets)
@ -242,7 +238,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
groupLeft: (i: number) => (groupWidth + spaceBetweenGroups) * i,
axisTop: (i: number, count: number) => axisHeight * (maxChartCount - count + i + 1)
})
}, [groups, height, offset])
}, [groups, width, height, offset, axisHeight, spaceBetweenGroups])
const yAxis = useMemo(() => {
if (!data) return
@ -258,7 +254,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
const out: [string | number, ChartDomain][] = group.charts.map((chart) => {
const mm = { ...chart.xDomain }
let domain: Required<MinMax> = { min: 0, max: 100 }
if (mm.min && mm.max) {
if (!Number.isNaN((mm.min ?? NaN) + (mm.max ?? NaN))) {
domain = mm as Required<MinMax>
} else if (data) {
const [min, max] = d3.extent(data, chart.x)
@ -291,9 +287,18 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
), [chartArea, axesArea])
const onGroupsChange = useCallback((sets: ExtendedChartDataset<DataType>[][]) => {
if (chartName) {
invokeWebApiWrapperAsync(
async () => {
await UserSettingsService.update(chartName, sets)
},
undefined,
'Не удалось сохранить параметры графиков'
)
}
setDatasets(sets)
setSettingsVisible(false)
}, [])
}, [chartName])
useEffect(() => {
if (isDev()) {
@ -345,7 +350,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
animDurationMs,
...dataset,
yAxis: dataset.yAxis ?? yAxisConfig,
y: getByAccessor(dataset.yAxis.accessor ?? yAxisConfig.accessor),
y: getByAccessor(dataset.yAxis?.accessor ?? yAxisConfig.accessor),
x: getByAccessor(dataset.xAxis?.accessor),
}
)

View File

@ -1,10 +1,11 @@
import { Key, memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Divider, Modal, Tooltip, Tree } from 'antd'
import { CSSProperties, Key, memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Divider, Empty, Modal, Tooltip, Tree } from 'antd'
import { getChartIcon } from '@utils'
import { ChartGroup, ExtendedChartDataset } from './D3MonitoringCharts'
import { ExtendedChartDataset } from './D3MonitoringCharts'
import D3MonitoringChartEditor from './D3MonitoringChartEditor'
import { notify } from '../factory'
export type D3MonitoringGroupsEditorProps<DataType> = {
visible?: boolean
@ -14,30 +15,30 @@ export type D3MonitoringGroupsEditorProps<DataType> = {
name?: string
}
const moveToPos = <T,>(arr: T[], pos: number, newPos: number): T[] => {
if (pos === newPos) return arr
if (newPos === -1) return [arr[pos], ...arr.slice(0, pos), ...arr.slice(pos + 1)]
if (newPos === arr.length) return [...arr.slice(0, pos), ...arr.slice(pos + 1), arr[pos]]
const newArray = []
for (let i = 0; i < arr.length; i++) {
if (i == newPos) newArray.push(arr[pos])
if (i !== pos) newArray.push(arr[i])
}
return newArray
}
const getChartLabel = <DataType,>(chart: ExtendedChartDataset<DataType>) => (
<Tooltip title={chart.label}>
{getChartIcon(chart)} {chart.label}
</Tooltip>
)
)
const divStyle: CSSProperties = {
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'center',
flexGrow: 1,
}
const getNodePos = (node: any): { group: number, chart?: number } => {
const out = node.pos.split('-').map(Number)
return { group: out[1], chart: out[2] }
}
const _D3MonitoringGroupsEditor = <DataType,>({
visible,
groups: oldGroups,
onChange,
onCancel,
name,
}: D3MonitoringGroupsEditorProps<DataType>) => {
const [groups, setGroups] = useState<ExtendedChartDataset<DataType>[][]>([])
const [expand, setExpand] = useState<Key[]>([])
@ -48,22 +49,30 @@ const _D3MonitoringGroupsEditor = <DataType,>({
const onModalOk = useCallback(() => onChange(groups), [groups])
const onDrop = useCallback((info: any) => {
const { dragNode, dropPosition, dropToGap } = info
const { dragNode, dropPosition, node } = info
const nodes = dragNode.pos.split('-')
const groupPos = Number(nodes[1])
const targetNodes = getNodePos(node)
const dragNodes = getNodePos(dragNode)
const groupPos = dragNodes.group
if (!Number.isFinite(groupPos)) return
setGroups((prev) => {
if (dropToGap) {
if (nodes.length < 3)
return moveToPos(prev, groupPos, dropPosition)
} else {
if (nodes.length < 3) return prev
const chartPos = Number(nodes[2])
if (Number.isFinite(chartPos)) {
prev[groupPos] = moveToPos(prev[groupPos], chartPos, dropPosition)
return [ ...prev ]
if (typeof dragNodes.chart === 'undefined') {
const groups = [ ...prev ]
const movedGroups = groups.splice(dragNodes.group, 1)
groups.splice(Math.max(dropPosition - 1, 0), 0, ...movedGroups)
return groups
} else if (Number.isFinite(dragNodes.chart)) {
if (groupPos !== targetNodes.group) {
const dragKey = prev[dragNodes.group][dragNodes.chart].key
if (prev[targetNodes.group].find((chart) => chart.key === dragKey)) {
notify('График с данным ключом уже существует в этой группе. Перемещение невозможно', 'warning')
return prev
}
}
const groups = [ ...prev ]
const charts = groups[groupPos].splice(dragNodes.chart, 1)
groups[targetNodes.group].splice(Math.max(dropPosition - 1, 0), 0, ...charts)
return groups
}
return prev
})
@ -108,7 +117,7 @@ const _D3MonitoringGroupsEditor = <DataType,>({
return (
<Modal
centered
width={700}
width={800}
visible={visible}
onOk={onModalOk}
onCancel={onCancel}
@ -116,9 +125,9 @@ const _D3MonitoringGroupsEditor = <DataType,>({
title={'Настройка групп графиков'}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'stretch', height: 250 }}>
<div style={{ width: '25%' }}>
<div style={{ width: '35%' }}>
<Tree
// draggable
draggable
selectable
onExpand={(keys) => setExpand(keys)}
expandedKeys={expand}
@ -130,7 +139,13 @@ const _D3MonitoringGroupsEditor = <DataType,>({
/>
</div>
<Divider type={'vertical'} style={{ height: '100%', padding: '0 5px' }} />
<D3MonitoringChartEditor<DataType> chart={selectedChart} style={{ flexGrow: 1 }} onChange={onChartChange} />
<div style={divStyle}>
{selectedChart ? (
<D3MonitoringChartEditor<DataType> chart={selectedChart} onChange={onChartChange} />
) : (
<Empty description={'Выберите график для редактирования'} />
)}
</div>
</div>
</Modal>
)

View File

@ -1,4 +1,4 @@
import { CSSProperties, ReactNode, SVGProps, useEffect, useMemo, useRef, useState } from 'react'
import { CSSProperties, ReactNode, SVGProps, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import * as d3 from 'd3'
import { useD3MouseZone } from '@components/d3/D3MouseZone'
@ -104,7 +104,7 @@ const _D3HorizontalCursor = <DataType,>({
if (!unsubscribe() && isDev())
console.warn('Не удалось отвязать эвент')
}
}, [subscribe])
}, [subscribe, fixed, mouseState.visible])
useEffect(() => {
if (!zone || !getXLine) return

View File

@ -110,7 +110,7 @@ function _D3Tooltip<DataType extends Record<string, unknown>>({
if (!unsubscribe() && isDev())
console.warn('Не удалось отвязать эвент')
}
}, [visible])
}, [subscribe, visible])
useEffect(() => {
if (!tooltipRef.current || !zoneRect || fixed) return

View File

@ -49,7 +49,6 @@ const makeDataset = (label, shortLabel, color, key, unit, other) => ({
label,
shortLabel,
color,
yAxis,
xAxis: {
type: 'linear',
accessor: key,
@ -171,7 +170,7 @@ const TelemetryView = memo(() => {
useEffect(() => {
const subscribtion = saubSubject$.pipe(
buffer(saubSubject$.pipe(throttleTime(700)))
).subscribe((data) => handleDataSaub(data.flat()))
).subscribe((data) => handleDataSaub(data.flat().filter(Boolean)))
return () => subscribtion.unsubscribe()
}, [saubSubject$])
@ -179,7 +178,7 @@ const TelemetryView = memo(() => {
useEffect(() => {
const subscribtion = spinSubject$.pipe(
buffer(spinSubject$.pipe(throttleTime(700)))
).subscribe((data) => handleDataSpin(data.flat()))
).subscribe((data) => handleDataSpin(data.flat().filter(Boolean)))
return () => subscribtion.unsubscribe()
}, [spinSubject$])
@ -294,9 +293,11 @@ const TelemetryView = memo(() => {
</GridItem>
<GridItem col={2} row={2} colSpan={8} rowSpan={2}>
<D3MonitoringCharts
chartName={'monitoring'}
datasetGroups={chartGroups}
data={filteredData}
yDomain={domain}
yAxis={yAxis}
yTicks={{
visible: true,
format: (d) => formatDate(d, 'YYYY-MM-DD')