forked from ddrilling/asb_cloud_front
Добавлен дашборд с выводом параметров ННБ
This commit is contained in:
parent
0e519ea03f
commit
7c3d46893a
67
src/components/widgets/BaseWidget.tsx
Normal file
67
src/components/widgets/BaseWidget.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { Button } from 'antd'
|
||||||
|
import { memo, ReactNode, useMemo } from 'react'
|
||||||
|
import { CloseOutlined, SettingOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
|
import '@styles/widgets/base.less'
|
||||||
|
|
||||||
|
export type WidgetSettings<T = any> = {
|
||||||
|
id?: number,
|
||||||
|
unit?: string,
|
||||||
|
label?: string,
|
||||||
|
formatter?: ((v: T) => ReactNode) | null,
|
||||||
|
defaultValue?: ReactNode,
|
||||||
|
|
||||||
|
labelColor?: string,
|
||||||
|
valueColor?: string,
|
||||||
|
backgroundColor?: string,
|
||||||
|
unitColor?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultSettings: WidgetSettings = {
|
||||||
|
unit: '----',
|
||||||
|
label: 'Виджет',
|
||||||
|
formatter: v => isNaN(v) ? v : parseFloat(v).toFixed(2),
|
||||||
|
|
||||||
|
labelColor: '#000000',
|
||||||
|
valueColor: '#000000',
|
||||||
|
backgroundColor: '#f6f6f6',
|
||||||
|
unitColor: '#a0a0a0',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BaseWidgetProps<T = any> = WidgetSettings<T> & {
|
||||||
|
value: T,
|
||||||
|
onRemove: (settings: WidgetSettings<T>) => void,
|
||||||
|
onEdit: (settings: WidgetSettings<T>) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BaseWidget = memo<BaseWidgetProps>(({ value, onRemove, onEdit, ...settings }) => {
|
||||||
|
const sets = useMemo<WidgetSettings>(() => ({ ...defaultSettings, ...settings }), [settings])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'number_widget'} style={{ background: sets.backgroundColor }}>
|
||||||
|
<div className={'widget_head'}>
|
||||||
|
<Button
|
||||||
|
type={'text'}
|
||||||
|
onClick={() => onEdit(sets)}
|
||||||
|
icon={<SettingOutlined />}
|
||||||
|
style={{ visibility: !!onEdit ? 'visible' : 'hidden' }}
|
||||||
|
/>
|
||||||
|
<div className={'widget_label'} style={{ color: sets.labelColor }}>{sets.label}</div>
|
||||||
|
<div className={'widget_close'}>
|
||||||
|
<Button
|
||||||
|
type={'link'}
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
onClick={() => onRemove(settings)}
|
||||||
|
style={{ visibility: !!onRemove ? 'visible' : 'hidden' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={'widget_value'} style={{ color: sets.valueColor }}>
|
||||||
|
{(sets.formatter === null ? value : sets.formatter?.(value)) ?? sets.defaultValue ?? '----'}
|
||||||
|
</div>
|
||||||
|
<div className={'widget_units'} style={{ color: sets.unitColor }}>{sets.unit}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default BaseWidget
|
65
src/components/widgets/WidgetSettingsWindow.tsx
Normal file
65
src/components/widgets/WidgetSettingsWindow.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { memo, useEffect } from 'react'
|
||||||
|
import { Form, Input, Modal, ModalProps } from 'antd'
|
||||||
|
|
||||||
|
import { WidgetSettings } from './BaseWidget'
|
||||||
|
|
||||||
|
export type WidgetSettingsWindowProps<T = any> = ModalProps & {
|
||||||
|
settings: WidgetSettings<T>
|
||||||
|
onEdit: (settings: WidgetSettings<T>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Item } = Form
|
||||||
|
|
||||||
|
export const WidgetSettingsWindow = memo<WidgetSettingsWindowProps>(({ settings, onEdit, ...other }) => {
|
||||||
|
const [form] = Form.useForm()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (settings) form.setFieldsValue(settings)
|
||||||
|
}, [form, settings])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
{...other}
|
||||||
|
visible={!!settings}
|
||||||
|
title={(
|
||||||
|
<>
|
||||||
|
Настройка виджета {settings?.label ? `"${settings?.label}"` : ''}
|
||||||
|
<span style={{ color: '#a0a0a0'}}> (id: {settings?.id})</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
onOk={form.submit}
|
||||||
|
getContainer={false}
|
||||||
|
>
|
||||||
|
<Form form={form} onFinish={onEdit}>
|
||||||
|
<Item name={'id'} hidden><Input type={'hidden'} /></Item>
|
||||||
|
<Item
|
||||||
|
label={'Заголовок поля'}
|
||||||
|
name={'label'}
|
||||||
|
rules={[{ required: true, message: 'Пожалуйста, введите заголовок!' }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Item>
|
||||||
|
<Item
|
||||||
|
label={'Единицы измерения'}
|
||||||
|
name={'unit'}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Item>
|
||||||
|
<Item label={'Цвет заголовка'} name={'labelColor'}>
|
||||||
|
<Input type={'color'} />
|
||||||
|
</Item>
|
||||||
|
<Item label={'Цвет значения'} name={'valueColor'}>
|
||||||
|
<Input type={'color'} />
|
||||||
|
</Item>
|
||||||
|
<Item label={'Цвет фона'} name={'backgroundColor'}>
|
||||||
|
<Input type={'color'} />
|
||||||
|
</Item>
|
||||||
|
<Item label={'Цвет единиц измерения'} name={'unitColor'}>
|
||||||
|
<Input type={'color'} />
|
||||||
|
</Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default WidgetSettingsWindow
|
5
src/components/widgets/index.ts
Normal file
5
src/components/widgets/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export { WidgetSettingsWindow } from './WidgetSettingsWindow'
|
||||||
|
export { BaseWidget } from './BaseWidget'
|
||||||
|
|
||||||
|
export type { WidgetSettingsWindowProps } from './WidgetSettingsWindow'
|
||||||
|
export type { WidgetSettings, BaseWidgetProps } from './BaseWidget'
|
18
src/pages/Telemetry/DashboardNNB/AddGroupWindow.jsx
Normal file
18
src/pages/Telemetry/DashboardNNB/AddGroupWindow.jsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { PlusOutlined } from '@ant-design/icons'
|
||||||
|
import { Form, Input } from 'antd'
|
||||||
|
import { memo } from 'react'
|
||||||
|
|
||||||
|
import Poprompt from '@components/selectors/Poprompt'
|
||||||
|
|
||||||
|
const addGroupRules = [{ required: true, message: 'Пожалуйста, введите название группы' }]
|
||||||
|
const addGroupButtonProps = { type: 'link', className: 'add_group', icon: <PlusOutlined /> }
|
||||||
|
|
||||||
|
export const AddGroupWindow = memo(({ addGroup, initialValue = 'Новая группа' }) => (
|
||||||
|
<Poprompt placement={'right'} text={'Добавить группу'} buttonProps={addGroupButtonProps} onDone={addGroup}>
|
||||||
|
<Form.Item initialValue={initialValue} rules={addGroupRules} name={'groupName'} label={'Название группы'}>
|
||||||
|
<Input type={'text'} />
|
||||||
|
</Form.Item>
|
||||||
|
</Poprompt>
|
||||||
|
))
|
||||||
|
|
||||||
|
export default AddGroupWindow
|
52
src/pages/Telemetry/DashboardNNB/AddWidgetWindow.jsx
Normal file
52
src/pages/Telemetry/DashboardNNB/AddWidgetWindow.jsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { PlusOutlined } from '@ant-design/icons'
|
||||||
|
import { Form, Select } from 'antd'
|
||||||
|
import { memo, useCallback, useMemo } from 'react'
|
||||||
|
|
||||||
|
import Poprompt from '@components/selectors/Poprompt'
|
||||||
|
|
||||||
|
export const createWidgetId = (witsRecord, dt = false) => `${witsRecord.recordId}_${witsRecord.itemId}` + (dt ? `_${Date.now()}` : '')
|
||||||
|
|
||||||
|
export const makeWidgetFromWits = (witsRecord) => ({
|
||||||
|
id: createWidgetId(witsRecord, true),
|
||||||
|
witsId: witsRecord.longMnemonic.toLowerCase(),
|
||||||
|
recordId: witsRecord.recordId,
|
||||||
|
unit: witsRecord.metricUnits,
|
||||||
|
label: witsRecord.longMnemonic,
|
||||||
|
})
|
||||||
|
|
||||||
|
const addWidgetRules = [{ required: true, message: 'Пожалуйста, выберите виджет' }]
|
||||||
|
const addWidgetButtonProps = { type: 'link', className: 'add_group', icon: <PlusOutlined /> }
|
||||||
|
|
||||||
|
export const AddWidgetWindow = memo(({ witsInfo, onAdded }) => {
|
||||||
|
const options = useMemo(() => witsInfo?.map((witsRecord) => ({
|
||||||
|
label: `Record #${witsRecord.recordId}: ${witsRecord.longMnemonic}`,
|
||||||
|
value: createWidgetId(witsRecord),
|
||||||
|
})) ?? [], [witsInfo])
|
||||||
|
|
||||||
|
const onFormFinish = useCallback((value) => {
|
||||||
|
if (!value?.widget) return
|
||||||
|
const record = witsInfo.find((witsRecord) => createWidgetId(witsRecord) === value.widget)
|
||||||
|
if (record)
|
||||||
|
onAdded?.(makeWidgetFromWits(record))
|
||||||
|
}, [onAdded, witsInfo])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Poprompt
|
||||||
|
placement={'right'}
|
||||||
|
onDone={onFormFinish}
|
||||||
|
text={'Добавить виджет'}
|
||||||
|
buttonProps={addWidgetButtonProps}
|
||||||
|
overlayInnerStyle={{ width: '300px' }}
|
||||||
|
>
|
||||||
|
<Form.Item rules={addWidgetRules} name={'widget'} label={'Виджет'}>
|
||||||
|
<Select
|
||||||
|
showSearch
|
||||||
|
options={options}
|
||||||
|
filterOption={(input, option) => option.label.toLowerCase().includes(input.toLowerCase())}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Poprompt>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default AddWidgetWindow
|
255
src/pages/Telemetry/DashboardNNB/index.jsx
Normal file
255
src/pages/Telemetry/DashboardNNB/index.jsx
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
import { memo, useCallback, useEffect, useMemo, useReducer, useState } from 'react'
|
||||||
|
import { useHistory, useParams } from 'react-router-dom'
|
||||||
|
import { CloseOutlined } from '@ant-design/icons'
|
||||||
|
import { Button, Menu, Popconfirm } from 'antd'
|
||||||
|
|
||||||
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
import { BaseWidget, WidgetSettingsWindow } from '@components/widgets'
|
||||||
|
import { getJSON, setJSON } from '@utils/storage'
|
||||||
|
import { arrayOrDefault } from '@utils'
|
||||||
|
import Subscribe from '@services/signalr'
|
||||||
|
import {
|
||||||
|
WitsInfoService,
|
||||||
|
WitsRecord1Service,
|
||||||
|
WitsRecord7Service,
|
||||||
|
WitsRecord8Service,
|
||||||
|
WitsRecord50Service,
|
||||||
|
WitsRecord60Service,
|
||||||
|
WitsRecord61Service,
|
||||||
|
} from '@api'
|
||||||
|
|
||||||
|
import AddWidgetWindow, { makeWidgetFromWits } from './AddWidgetWindow'
|
||||||
|
|
||||||
|
import '@styles/dashboard_nnb.less'
|
||||||
|
import AddGroupWindow from './AddGroupWindow'
|
||||||
|
|
||||||
|
const getWitsInfo = async () => {
|
||||||
|
// TODO: Добавить expire с принудительным обновлением
|
||||||
|
if ('witsInfo' in localStorage)
|
||||||
|
return getJSON('witsInfo')
|
||||||
|
const info = arrayOrDefault(await WitsInfoService.getItems())
|
||||||
|
setJSON('witsInfo', info)
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_NAME = 'nnbWidgets'
|
||||||
|
|
||||||
|
const defaultGroups = [
|
||||||
|
{ id: '1', name: 'General Time-Based', editable: false },
|
||||||
|
{ id: '7', name: 'Survey/Directional', editable: false },
|
||||||
|
{ id: '8', name: 'MWD Formation Evaluation', editable: false },
|
||||||
|
{ id: '50', name: 'Резистивиметр MCR', editable: false },
|
||||||
|
{ id: '60', name: 'Передача полных', editable: false },
|
||||||
|
{ id: '61', name: 'Резистивиметр Corvet', editable: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
const makeGroup = (settings) => ({
|
||||||
|
widgets: [],
|
||||||
|
editable: true,
|
||||||
|
id: '' + Date.now(),
|
||||||
|
name: 'Новая группа',
|
||||||
|
...settings,
|
||||||
|
})
|
||||||
|
|
||||||
|
const groupsReducer = (groups, action) => {
|
||||||
|
let newGroups = [ ...groups ]
|
||||||
|
const { groupId, widgetId, value, type, witsInfo } = action
|
||||||
|
|
||||||
|
const groupIdx = newGroups.findIndex(({ id }) => `${id}` === groupId)
|
||||||
|
const widgetIdx = groupIdx < 0 ? -1 : newGroups[groupIdx].widgets.findIndex(({ id }) => id === widgetId)
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'set': return value
|
||||||
|
case 'clear':
|
||||||
|
localStorage.removeItem(STORAGE_NAME)
|
||||||
|
if (!witsInfo) return []
|
||||||
|
// break намеренно пропущен, далее должен срабатывать init
|
||||||
|
case 'init': // eslint-disable-line no-fallthrough
|
||||||
|
if (STORAGE_NAME in localStorage)
|
||||||
|
return getJSON(STORAGE_NAME)
|
||||||
|
newGroups = defaultGroups.map((group) => ({
|
||||||
|
...group,
|
||||||
|
widgets: witsInfo.filter(({ recordId }) => `${recordId}` === group.id).map(makeWidgetFromWits)
|
||||||
|
}))
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'add_group':
|
||||||
|
newGroups.push(makeGroup(value))
|
||||||
|
break
|
||||||
|
case 'edit_group':
|
||||||
|
if (groupIdx >= 0)
|
||||||
|
newGroups[groupIdx] = { ...newGroups[groupIdx], ...value }
|
||||||
|
break
|
||||||
|
case 'remove_group':
|
||||||
|
if (groupIdx >= 0)
|
||||||
|
newGroups.splice(groupIdx, 1)
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'add_widget':
|
||||||
|
if (groupIdx >= 0)
|
||||||
|
newGroups[groupIdx].widgets.push(value)
|
||||||
|
break
|
||||||
|
case 'edit_widget':
|
||||||
|
if (widgetIdx >= 0)
|
||||||
|
newGroups[groupIdx].widgets[widgetIdx] = { ...newGroups[groupIdx].widgets[widgetIdx], ...value }
|
||||||
|
break
|
||||||
|
case 'remove_widget':
|
||||||
|
if (widgetIdx >= 0)
|
||||||
|
newGroups[groupIdx].widgets.splice(widgetIdx, 1)
|
||||||
|
break
|
||||||
|
|
||||||
|
default: return newGroups
|
||||||
|
}
|
||||||
|
setJSON('nnbWidgets', newGroups)
|
||||||
|
return newGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DashboardNNB = memo(({ idWell }) => {
|
||||||
|
const [groups, dispatchGroups] = useReducer(groupsReducer, [])
|
||||||
|
const [witsInfo, setWitsInfo] = useState([])
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [selectedSettings, setSelectedSettings] = useState(null)
|
||||||
|
const [values, setValues] = useState({})
|
||||||
|
|
||||||
|
const root = useMemo(() => `/well/${idWell}/telemetry/dashboard_nnb`, [idWell])
|
||||||
|
const history = useHistory()
|
||||||
|
const { tab: selectedGroup } = useParams()
|
||||||
|
|
||||||
|
if (!selectedGroup && groups?.length > 0)
|
||||||
|
history.push(`${root}/${groups[0].id}`)
|
||||||
|
|
||||||
|
|
||||||
|
const group = useMemo(() => ({
|
||||||
|
editable: true,
|
||||||
|
...groups.find(({ id }) => `${id}` === selectedGroup),
|
||||||
|
}), [groups, selectedGroup])
|
||||||
|
|
||||||
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
const info = await getWitsInfo()
|
||||||
|
setWitsInfo(info)
|
||||||
|
dispatchGroups({ type: 'init', witsInfo: info })
|
||||||
|
},
|
||||||
|
setIsLoading,
|
||||||
|
'Не удалось загрузить информацию о параметрах ННБ',
|
||||||
|
'Получение информации о параметрах ННБ'
|
||||||
|
), [])
|
||||||
|
|
||||||
|
const handleData = useCallback((data, recordId) => {
|
||||||
|
const mergedData = data.reduce((out, record) => ({ ...out, ...record }), {})
|
||||||
|
setValues((pre) => ({
|
||||||
|
...pre,
|
||||||
|
[recordId]: { ...pre[recordId], ...mergedData },
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
handleData(await WitsRecord1Service.getLastData(idWell), '1')
|
||||||
|
handleData(await WitsRecord7Service.getLastData(idWell), '7')
|
||||||
|
handleData(await WitsRecord8Service.getLastData(idWell), '8')
|
||||||
|
handleData(await WitsRecord50Service.getLastData(idWell), '50')
|
||||||
|
handleData(await WitsRecord60Service.getLastData(idWell), '60')
|
||||||
|
handleData(await WitsRecord61Service.getLastData(idWell), '61')
|
||||||
|
},
|
||||||
|
setIsLoading,
|
||||||
|
'Не удалось загрузить последние данные',
|
||||||
|
'Получение данных WITS',
|
||||||
|
)
|
||||||
|
return Subscribe('hubs/telemetry', `well_${idWell}_wits`,
|
||||||
|
{ methodName: 'ReceiveWitsRecord1', handler: (data) => handleData(data, '1') },
|
||||||
|
{ methodName: 'ReceiveWitsRecord7', handler: (data) => handleData(data, '7') },
|
||||||
|
{ methodName: 'ReceiveWitsRecord8', handler: (data) => handleData(data, '8') },
|
||||||
|
{ methodName: 'ReceiveWitsRecord50', handler: (data) => handleData(data, '50') },
|
||||||
|
{ methodName: 'ReceiveWitsRecord60', handler: (data) => handleData(data, '60') },
|
||||||
|
{ methodName: 'ReceiveWitsRecord61', handler: (data) => handleData(data, '61') },
|
||||||
|
)
|
||||||
|
}, [idWell, handleData])
|
||||||
|
|
||||||
|
const addGroup = useCallback((values) => dispatchGroups({ type: 'add_group', value: { name: values.groupName } }), [])
|
||||||
|
|
||||||
|
const removeGroup = useCallback((id) => {
|
||||||
|
dispatchGroups({ type: 'remove_group', groupId: `${id}` })
|
||||||
|
if (id === selectedGroup) history.push(`${root}`)
|
||||||
|
}, [root, history, selectedGroup])
|
||||||
|
|
||||||
|
const addWidget = useCallback((settings) => dispatchGroups({
|
||||||
|
type: 'add_widget',
|
||||||
|
groupId: selectedGroup,
|
||||||
|
value: settings,
|
||||||
|
}), [selectedGroup])
|
||||||
|
|
||||||
|
const onEdit = useCallback((settings) => {
|
||||||
|
dispatchGroups({
|
||||||
|
type: 'edit_widget',
|
||||||
|
groupId: selectedGroup,
|
||||||
|
widgetId: settings.id,
|
||||||
|
value: settings,
|
||||||
|
})
|
||||||
|
setSelectedSettings(null)
|
||||||
|
}, [selectedGroup])
|
||||||
|
|
||||||
|
const removeWidget = useCallback((settings) => dispatchGroups({
|
||||||
|
type: 'remove_widget',
|
||||||
|
groupId: selectedGroup,
|
||||||
|
widgetId: settings.id,
|
||||||
|
}), [selectedGroup])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoaderPortal show={isLoading}>
|
||||||
|
<div className={'dashboard_nnb'}>
|
||||||
|
<Menu
|
||||||
|
className={'dashboard_menu'}
|
||||||
|
mode={'vertical'}
|
||||||
|
selectable={true}
|
||||||
|
selectedKeys={[selectedGroup]}
|
||||||
|
>
|
||||||
|
<Menu.Item key={'add_group'}>
|
||||||
|
<AddGroupWindow addGroup={addGroup} />
|
||||||
|
</Menu.Item>
|
||||||
|
{group?.editable && (
|
||||||
|
<Menu.Item key={'add_widget'}>
|
||||||
|
<AddWidgetWindow witsInfo={witsInfo} onAdded={addWidget} />
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
{groups.map(({ id, name, editable }) => (
|
||||||
|
<Menu.Item key={id}>
|
||||||
|
{editable && (
|
||||||
|
<Popconfirm
|
||||||
|
title={'Вы уверены, что хотите удалить группу, это действие невозможно отменить?'}
|
||||||
|
onConfirm={() => removeGroup(id)}
|
||||||
|
okText={'Удалить'}
|
||||||
|
cancelText={'Отмена'}
|
||||||
|
>
|
||||||
|
<Button type={'link'} icon={<CloseOutlined />} />
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
<Button type={'text'} style={{ paddingLeft: 0 }} onClick={() => history.push(`${root}/${id}`)}>{name}</Button>
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
<div className={'widgets'}>
|
||||||
|
{group.widgets?.map((widget) => (
|
||||||
|
<BaseWidget
|
||||||
|
key={widget.id}
|
||||||
|
// onEdit={group.editable && setSelectedSettings} // TODO: Доделать редактирование
|
||||||
|
onRemove={group.editable && removeWidget}
|
||||||
|
{...widget}
|
||||||
|
value={values[widget.recordId]?.[widget.witsId]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<WidgetSettingsWindow
|
||||||
|
visible={!!selectedSettings}
|
||||||
|
onCancel={() => setSelectedSettings(null)}
|
||||||
|
settings={selectedSettings}
|
||||||
|
onEdit={onEdit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</LoaderPortal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default DashboardNNB
|
@ -7,6 +7,7 @@ import { PrivateRoute, PrivateDefaultRoute, PrivateMenuItem } from '@components/
|
|||||||
|
|
||||||
import Archive from './Archive'
|
import Archive from './Archive'
|
||||||
import Messages from './Messages'
|
import Messages from './Messages'
|
||||||
|
import DashboardNNB from './DashboardNNB'
|
||||||
import TelemetryView from './TelemetryView'
|
import TelemetryView from './TelemetryView'
|
||||||
|
|
||||||
import '@styles/index.css'
|
import '@styles/index.css'
|
||||||
@ -23,6 +24,7 @@ export const Telemetry = memo(({ idWell }) => {
|
|||||||
<PrivateMenuItem.Link root={rootPath} key={'monitoring'} path={'monitoring'} icon={<FundViewOutlined />} title={'Мониторинг'}/>
|
<PrivateMenuItem.Link root={rootPath} key={'monitoring'} path={'monitoring'} icon={<FundViewOutlined />} title={'Мониторинг'}/>
|
||||||
<PrivateMenuItem.Link root={rootPath} key={'messages'} path={'messages'} icon={<AlertOutlined/>} title={'Сообщения'} />
|
<PrivateMenuItem.Link root={rootPath} key={'messages'} path={'messages'} icon={<AlertOutlined/>} title={'Сообщения'} />
|
||||||
<PrivateMenuItem.Link root={rootPath} key={'archive'} path={'archive'} icon={<DatabaseOutlined />} title={'Архив'} />
|
<PrivateMenuItem.Link root={rootPath} key={'archive'} path={'archive'} icon={<DatabaseOutlined />} title={'Архив'} />
|
||||||
|
<PrivateMenuItem.Link root={rootPath} key={'dashboard_nnb'} path={'dashboard_nnb'} title={'ННБ'} />
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
@ -37,10 +39,14 @@ export const Telemetry = memo(({ idWell }) => {
|
|||||||
<PrivateRoute path={`${rootPath}/archive`}>
|
<PrivateRoute path={`${rootPath}/archive`}>
|
||||||
<Archive idWell={idWell} />
|
<Archive idWell={idWell} />
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
|
<PrivateRoute path={`${rootPath}/dashboard_nnb/:tab?`}>
|
||||||
|
<DashboardNNB idWell={idWell} />
|
||||||
|
</PrivateRoute>
|
||||||
<PrivateDefaultRoute urls={[
|
<PrivateDefaultRoute urls={[
|
||||||
`${rootPath}/monitoring`,
|
`${rootPath}/monitoring`,
|
||||||
`${rootPath}/messages`,
|
`${rootPath}/messages`,
|
||||||
`${rootPath}/archive`,
|
`${rootPath}/archive`,
|
||||||
|
`${rootPath}/dashboard_nnb`,
|
||||||
]}/>
|
]}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Content>
|
</Content>
|
||||||
|
29
src/styles/dashboard_nnb.less
Normal file
29
src/styles/dashboard_nnb.less
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
.dashboard_nnb {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
max-height: 80vh;
|
||||||
|
|
||||||
|
& > .dashboard_menu {
|
||||||
|
flex: 1;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
& .add_group {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .widgets {
|
||||||
|
flex: 5;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
& .add_widget {
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 10px;
|
||||||
|
width: 13vh;
|
||||||
|
height: 13vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
src/styles/widgets/base.less
Normal file
56
src/styles/widgets/base.less
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
|
||||||
|
@size: 9.9vh;
|
||||||
|
.number_widget {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: stretch;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #00000009;
|
||||||
|
height: @size;
|
||||||
|
min-width: @size * 1.75;
|
||||||
|
|
||||||
|
& > .widget_head {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: 10px;
|
||||||
|
|
||||||
|
& > .widget_settings {
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .widget_label {
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 2;
|
||||||
|
color: black;
|
||||||
|
font-size: @size * 0.175;
|
||||||
|
//line-height: @size * 0.175;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .widget_close {
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 3;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .widget_value {
|
||||||
|
flex: 2;
|
||||||
|
color: black;
|
||||||
|
font-size: @size * 0.45;
|
||||||
|
line-height: @size * 0.45;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .widget_units {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
font-size: @size * 0.15;
|
||||||
|
line-height: @size * 0.15;
|
||||||
|
color: #A0A0A0;
|
||||||
|
}
|
||||||
|
}
|
@ -10,9 +10,14 @@ export const mainFrameSize = () => ({
|
|||||||
|
|
||||||
export const arrayOrDefault = <T extends unknown>(arr?: unknown, def: T[] = []): T[] => Array.isArray(arr) ? arr : def
|
export const arrayOrDefault = <T extends unknown>(arr?: unknown, def: T[] = []): T[] => Array.isArray(arr) ? arr : def
|
||||||
|
|
||||||
|
export const deepCopy = <T extends any>(data: T): T => JSON.parse(JSON.stringify(data ?? null))
|
||||||
|
|
||||||
|
export const wrapValues = <T, R>(data: Record<string, T>, handler: (data: T, key: string, object: Record<string, T>) => R): Record<string, R> =>
|
||||||
|
Object.fromEntries(Object.entries(data).map(([key, value]) => [key, handler(value, key, data)]))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Объединить типы, исключив совпадающие поля справа
|
* Объединить типы, исключив совпадающие поля справа
|
||||||
* @param T Тип, передаваемый полностью
|
* @param T Тип, передаваемый полностью
|
||||||
* @param R Аддитивный тип
|
* @param R Аддитивный тип
|
||||||
*/
|
*/
|
||||||
export type OmitExtends<T, R> = T & Omit<R, keyof T>
|
export type OmitExtends<T, R> = T & Omit<R, keyof T>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { OpenAPI, UserTokenDto } from '@api'
|
import { OpenAPI, UserTokenDto } from '@api'
|
||||||
|
import { wrapValues } from '.'
|
||||||
import { Role, Permission } from './permissions'
|
import { Role, Permission } from './permissions'
|
||||||
import { normalizeColumn, optimizeColumn, TableSettings, TableSettingsStore } from './table_settings'
|
import { normalizeColumn, optimizeColumn, TableSettings, TableSettingsStore } from './table_settings'
|
||||||
|
|
||||||
@ -9,6 +10,8 @@ export enum StorageNames {
|
|||||||
permissions = 'permissions',
|
permissions = 'permissions',
|
||||||
roles = 'roles',
|
roles = 'roles',
|
||||||
tableSettings = 'tableSettings',
|
tableSettings = 'tableSettings',
|
||||||
|
dashboardNNB = 'dashboardNNB',
|
||||||
|
witsInfo = 'witsInfo'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getArrayFromLocalStorage = <T extends string = string>(name: string, sep: string | RegExp = ','): T[] | null => {
|
export const getArrayFromLocalStorage = <T extends string = string>(name: string, sep: string | RegExp = ','): T[] | null => {
|
||||||
@ -17,6 +20,23 @@ export const getArrayFromLocalStorage = <T extends string = string>(name: string
|
|||||||
return raw.split(sep).map<T>(elm => elm as T)
|
return raw.split(sep).map<T>(elm => elm as T)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getJSON = <T extends any>(name: StorageNames): T | null => {
|
||||||
|
const raw = localStorage.getItem(name)
|
||||||
|
if (!raw) return null
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw)
|
||||||
|
} catch {}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setJSON = <T extends any>(name: StorageNames, data: T | null): boolean => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(name, JSON.stringify(data))
|
||||||
|
return true
|
||||||
|
} catch {}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
export const getUserRoles = (): Role[] => getArrayFromLocalStorage<Role>(StorageNames.roles) ?? []
|
export const getUserRoles = (): Role[] => getArrayFromLocalStorage<Role>(StorageNames.roles) ?? []
|
||||||
export const getUserPermissions = (): Permission[] => getArrayFromLocalStorage<Permission>(StorageNames.permissions) ?? []
|
export const getUserPermissions = (): Permission[] => getArrayFromLocalStorage<Permission>(StorageNames.permissions) ?? []
|
||||||
export const getUserId = () => Number(localStorage.getItem(StorageNames.userId)) || null
|
export const getUserId = () => Number(localStorage.getItem(StorageNames.userId)) || null
|
||||||
@ -43,31 +63,19 @@ export const removeUser = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getTableSettings = (tableName: string): TableSettings => {
|
export const getTableSettings = (tableName: string): TableSettings => {
|
||||||
const tablesJSON = localStorage.getItem(StorageNames.tableSettings)
|
const tables = getJSON<TableSettingsStore>(StorageNames.tableSettings) ?? {}
|
||||||
if (!tablesJSON) return {}
|
if (!(tableName in tables)) return {}
|
||||||
try {
|
return wrapValues(tables[tableName] ?? {}, normalizeColumn)
|
||||||
const tables: TableSettingsStore = JSON.parse(tablesJSON)
|
|
||||||
if (tableName in tables) {
|
|
||||||
const columns = tables[tableName] ?? {}
|
|
||||||
for (const [name, column] of Object.entries(columns))
|
|
||||||
columns[name] = normalizeColumn(column, name)
|
|
||||||
return columns
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
return {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setTableSettings = (tableName: string, settings?: TableSettings | null): boolean => {
|
export const setTableSettings = (tableName: string, settings?: TableSettings | null): boolean => {
|
||||||
const currentJSON = localStorage.getItem(StorageNames.tableSettings)
|
const currentStore = getJSON<TableSettingsStore>(StorageNames.tableSettings) ?? {}
|
||||||
try {
|
currentStore[tableName] = wrapValues(settings ?? {}, optimizeColumn)
|
||||||
const currentStore: TableSettingsStore = currentJSON ? JSON.parse(currentJSON) : {}
|
return setJSON(StorageNames.tableSettings, currentStore)
|
||||||
const newSettings = settings ?? null
|
|
||||||
if (newSettings)
|
|
||||||
for (const [name, column] of Object.entries(newSettings))
|
|
||||||
newSettings[name] = optimizeColumn(column)
|
|
||||||
currentStore[tableName] = newSettings
|
|
||||||
localStorage.setItem(StorageNames.tableSettings, JSON.stringify(currentStore))
|
|
||||||
return true
|
|
||||||
} catch {}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DataDashboardNNB = {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDashboardNNB = () => getJSON<DataDashboardNNB>(StorageNames.dashboardNNB)
|
||||||
|
Loading…
Reference in New Issue
Block a user