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 Messages from './Messages'
|
||||
import DashboardNNB from './DashboardNNB'
|
||||
import TelemetryView from './TelemetryView'
|
||||
|
||||
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={'messages'} path={'messages'} icon={<AlertOutlined/>} title={'Сообщения'} />
|
||||
<PrivateMenuItem.Link root={rootPath} key={'archive'} path={'archive'} icon={<DatabaseOutlined />} title={'Архив'} />
|
||||
<PrivateMenuItem.Link root={rootPath} key={'dashboard_nnb'} path={'dashboard_nnb'} title={'ННБ'} />
|
||||
</Menu>
|
||||
|
||||
<Layout>
|
||||
@ -37,10 +39,14 @@ export const Telemetry = memo(({ idWell }) => {
|
||||
<PrivateRoute path={`${rootPath}/archive`}>
|
||||
<Archive idWell={idWell} />
|
||||
</PrivateRoute>
|
||||
<PrivateRoute path={`${rootPath}/dashboard_nnb/:tab?`}>
|
||||
<DashboardNNB idWell={idWell} />
|
||||
</PrivateRoute>
|
||||
<PrivateDefaultRoute urls={[
|
||||
`${rootPath}/monitoring`,
|
||||
`${rootPath}/messages`,
|
||||
`${rootPath}/archive`,
|
||||
`${rootPath}/dashboard_nnb`,
|
||||
]}/>
|
||||
</Switch>
|
||||
</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 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 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 { wrapValues } from '.'
|
||||
import { Role, Permission } from './permissions'
|
||||
import { normalizeColumn, optimizeColumn, TableSettings, TableSettingsStore } from './table_settings'
|
||||
|
||||
@ -9,6 +10,8 @@ export enum StorageNames {
|
||||
permissions = 'permissions',
|
||||
roles = 'roles',
|
||||
tableSettings = 'tableSettings',
|
||||
dashboardNNB = 'dashboardNNB',
|
||||
witsInfo = 'witsInfo'
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 getUserPermissions = (): Permission[] => getArrayFromLocalStorage<Permission>(StorageNames.permissions) ?? []
|
||||
export const getUserId = () => Number(localStorage.getItem(StorageNames.userId)) || null
|
||||
@ -43,31 +63,19 @@ export const removeUser = () => {
|
||||
}
|
||||
|
||||
export const getTableSettings = (tableName: string): TableSettings => {
|
||||
const tablesJSON = localStorage.getItem(StorageNames.tableSettings)
|
||||
if (!tablesJSON) return {}
|
||||
try {
|
||||
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 {}
|
||||
const tables = getJSON<TableSettingsStore>(StorageNames.tableSettings) ?? {}
|
||||
if (!(tableName in tables)) return {}
|
||||
return wrapValues(tables[tableName] ?? {}, normalizeColumn)
|
||||
}
|
||||
|
||||
export const setTableSettings = (tableName: string, settings?: TableSettings | null): boolean => {
|
||||
const currentJSON = localStorage.getItem(StorageNames.tableSettings)
|
||||
try {
|
||||
const currentStore: TableSettingsStore = currentJSON ? JSON.parse(currentJSON) : {}
|
||||
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
|
||||
const currentStore = getJSON<TableSettingsStore>(StorageNames.tableSettings) ?? {}
|
||||
currentStore[tableName] = wrapValues(settings ?? {}, optimizeColumn)
|
||||
return setJSON(StorageNames.tableSettings, currentStore)
|
||||
}
|
||||
|
||||
export type DataDashboardNNB = {
|
||||
|
||||
}
|
||||
|
||||
export const getDashboardNNB = () => getJSON<DataDashboardNNB>(StorageNames.dashboardNNB)
|
||||
|
Loading…
Reference in New Issue
Block a user