forked from ddrilling/asb_cloud_front
Merge branch 'dev' into feature/add-page-operation-time
This commit is contained in:
commit
321900da89
@ -38,10 +38,10 @@ npx openapi -i http://{IP_ADDRESS}:{PORT}/swagger/v1/swagger.json -o src/service
|
||||
|
||||
| IP-адрес | Описание |
|
||||
|:-|:-|
|
||||
| 127.0.0.1:5000 | Локальный адрес вашей машины (привязан к `update_openapi`) |
|
||||
| 192.168.1.70:5000 | Локальный адрес development-сервера (привязан к `update_openapi_server`) |
|
||||
| 46.146.209.148:89 | Внешний адрес development-сервера |
|
||||
| 46.146.209.148 | Внешний адрес production-сервера |
|
||||
| 127.0.0.1:5000 | Локальный адрес вашей машины (привязан к `update_openapi`) |
|
||||
| 192.168.1.113:5000 | Локальный адрес development-сервера (привязан к `update_openapi_server`) |
|
||||
| 46.146.209.148:89 | Внешний адрес development-сервера |
|
||||
| cloud.digitaldrilling.ru | Внешний адрес production-сервера |
|
||||
|
||||
## 3. Компиляция production-версии приложения
|
||||
После выполнения вышеописанных пунктов приложение готово к компиляции.
|
||||
|
12463
package-lock.json
generated
12463
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -19,9 +19,10 @@
|
||||
"start": "webpack-dev-server --mode=development --open --hot",
|
||||
"build": "webpack --mode=production",
|
||||
"test": "jest",
|
||||
"dev": "webpack-dev-server --mode=development --open --hot",
|
||||
"oul": "npx openapi -i http://127.0.0.1:5000/swagger/v1/swagger.json -o src/services/api",
|
||||
"oud": "npx openapi -i http://192.168.1.70:5000/swagger/v1/swagger.json -o src/services/api",
|
||||
"oug": "npx openapi -i http://46.146.209.148/swagger/v1/swagger.json -o src/services/api",
|
||||
"oud": "npx openapi -i http://192.168.1.113:5000/swagger/v1/swagger.json -o src/services/api",
|
||||
"oug": "npx openapi -i https://cloud.digitaldrilling.ru/swagger/v1/swagger.json -o src/services/api",
|
||||
"oug_dev": "npx openapi -i http://46.146.209.148:89/swagger/v1/swagger.json -o src/services/api"
|
||||
},
|
||||
"proxy": "http://46.146.209.148:89",
|
||||
|
@ -8,7 +8,7 @@
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" />
|
||||
<meta name="description" content="Онлайн мониторинг процесса бурения в реальном времени в офисе заказчика" />
|
||||
<title>АСБ Vision</title>
|
||||
<title>DDrilling</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
@ -1,20 +1,20 @@
|
||||
import { memo, ReactNode } from 'react'
|
||||
import { Key, memo, ReactNode } from 'react'
|
||||
import { Layout, LayoutProps } from 'antd'
|
||||
|
||||
import PageHeader from '@components/PageHeader'
|
||||
import WellTreeSelector from '@components/selectors/WellTreeSelector'
|
||||
import { WellTreeSelector, WellTreeSelectorProps } from '@components/selectors/WellTreeSelector'
|
||||
import { wrapPrivateComponent } from '@utils'
|
||||
|
||||
export type LayoutPortalProps = LayoutProps & {
|
||||
title?: ReactNode
|
||||
noSheet?: boolean
|
||||
showSelector?: boolean
|
||||
selector?: WellTreeSelectorProps
|
||||
}
|
||||
|
||||
const _LayoutPortal = memo<LayoutPortalProps>(({ title, noSheet, showSelector, ...props }) => (
|
||||
const _LayoutPortal = memo<LayoutPortalProps>(({ title, noSheet, selector, ...props }) => (
|
||||
<Layout.Content>
|
||||
<PageHeader title={title}>
|
||||
<WellTreeSelector show={showSelector} />
|
||||
<WellTreeSelector {...selector} />
|
||||
</PageHeader>
|
||||
<Layout>
|
||||
{noSheet ? props.children : (
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { memo } from 'react'
|
||||
import { Layout } from 'antd'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { BasicProps } from 'antd/lib/layout/layout'
|
||||
|
||||
import { headerHeight } from '@utils'
|
||||
@ -14,21 +14,17 @@ export type PageHeaderProps = BasicProps & {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Мониторинг', isAdmin, children, ...other }) => {
|
||||
const location = useLocation()
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Layout.Header className={'header'} {...other}>
|
||||
<Link to={'/'} style={{ height: headerHeight }}>
|
||||
<Logo />
|
||||
</Link>
|
||||
<h1 className={'title'}>{title}</h1>
|
||||
{children}
|
||||
<UserMenu isAdmin={isAdmin} />
|
||||
</Layout.Header>
|
||||
</Layout>
|
||||
)
|
||||
})
|
||||
export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Мониторинг', isAdmin, children, ...other }) => (
|
||||
<Layout>
|
||||
<Layout.Header className={'header'} {...other}>
|
||||
<Link to={'/'} style={{ height: headerHeight }}>
|
||||
<Logo />
|
||||
</Link>
|
||||
<h1 className={'title'}>{title}</h1>
|
||||
{children}
|
||||
<UserMenu isAdmin={isAdmin} />
|
||||
</Layout.Header>
|
||||
</Layout>
|
||||
))
|
||||
|
||||
export default PageHeader
|
||||
|
@ -12,7 +12,6 @@ export {
|
||||
makeNumericColumnPlanFact,
|
||||
makeNumericStartEnd,
|
||||
makeNumericMinMax,
|
||||
makeNumericAvgRange
|
||||
} from './numeric'
|
||||
export { makeColumnsPlanFact } from './plan_fact'
|
||||
export { makeSelectColumn } from './select'
|
||||
|
@ -94,18 +94,4 @@ export const makeNumericMinMax = (
|
||||
makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max')),
|
||||
])
|
||||
|
||||
export const makeNumericAvgRange = (
|
||||
title: ReactNode,
|
||||
dataIndex: string,
|
||||
fixed: number,
|
||||
filters: object[],
|
||||
filterDelegate: (key: string | number) => any,
|
||||
renderDelegate: (_: any, row: object) => any,
|
||||
width: string
|
||||
) => makeGroupColumn(title, [
|
||||
makeNumericColumn('мин', dataIndex + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Min')),
|
||||
makeNumericColumn('сред', dataIndex + 'Avg', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Avg')),
|
||||
makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max'))
|
||||
])
|
||||
|
||||
export default makeNumericColumn
|
||||
|
@ -20,7 +20,6 @@ export {
|
||||
makeNumericColumnPlanFact,
|
||||
makeNumericStartEnd,
|
||||
makeNumericMinMax,
|
||||
makeNumericAvgRange,
|
||||
makeSelectColumn,
|
||||
makeTagColumn,
|
||||
makeTagInput,
|
||||
|
@ -7,6 +7,9 @@ import { DataType } from './Columns'
|
||||
export const makeNumericSorter = <T extends unknown>(key: keyof DataType<T>) =>
|
||||
(a: DataType<T>, b: DataType<T>) => Number(a[key]) - Number(b[key])
|
||||
|
||||
export const makeNumericObjSorter = (key: [string, string]) =>
|
||||
(a: DataType, b: DataType) => Number(a[key[0]][key[1]]) - Number(b[key[0]][key[1]])
|
||||
|
||||
export const makeStringSorter = <T extends unknown>(key: keyof DataType<T>) => (a?: DataType<T> | null, b?: DataType<T> | null) => {
|
||||
if (!a && !b) return 0
|
||||
if (!a) return 1
|
||||
|
@ -10,6 +10,7 @@ import { notify, upload } from './factory'
|
||||
import { ErrorFetch } from './ErrorFetch'
|
||||
|
||||
export type UploadFormProps = {
|
||||
multiple?: boolean
|
||||
url: string
|
||||
disabled?: boolean
|
||||
accept?: string
|
||||
@ -22,7 +23,7 @@ export type UploadFormProps = {
|
||||
onUploadError?: (error: unknown) => void
|
||||
}
|
||||
|
||||
export const UploadForm = memo<UploadFormProps>(({ url, disabled, style, formData, mimeTypes, onUploadStart, onUploadSuccess, onUploadComplete, onUploadError }) => {
|
||||
export const UploadForm = memo<UploadFormProps>(({ url, multiple, disabled, style, formData, mimeTypes, onUploadStart, onUploadSuccess, onUploadComplete, onUploadError }) => {
|
||||
const [fileList, setfileList] = useState<UploadFile<any>[]>([])
|
||||
|
||||
const checkMimeTypes = useCallback((file: RcFile) => {
|
||||
@ -38,7 +39,7 @@ export const UploadForm = memo<UploadFormProps>(({ url, disabled, style, formDat
|
||||
onUploadStart?.()
|
||||
try {
|
||||
const formDataLocal = new FormData()
|
||||
fileList.forEach((val) => formDataLocal.append('files', val.originFileObj as Blob))
|
||||
fileList.forEach((val) => formDataLocal.append(multiple ? 'files' : 'file', val.originFileObj as Blob))
|
||||
|
||||
if(formData)
|
||||
for(const propName in formData)
|
||||
@ -60,7 +61,7 @@ export const UploadForm = memo<UploadFormProps>(({ url, disabled, style, formDat
|
||||
setfileList([])
|
||||
onUploadComplete?.()
|
||||
}
|
||||
}, [fileList, formData, onUploadComplete, onUploadError, onUploadStart, onUploadSuccess, url])
|
||||
}, [fileList, formData, onUploadComplete, onUploadError, onUploadStart, onUploadSuccess, url, multiple])
|
||||
|
||||
const isSendButtonEnabled = fileList.length > 0
|
||||
return(
|
||||
@ -72,6 +73,7 @@ export const UploadForm = memo<UploadFormProps>(({ url, disabled, style, formDat
|
||||
fileList={fileList}
|
||||
onChange={(props) => setfileList(props.fileList)}
|
||||
beforeUpload={checkMimeTypes}
|
||||
maxCount={multiple ? undefined : 1}
|
||||
>
|
||||
<Button disabled={disabled} icon={<UploadOutlined/>}>Загрузить файл</Button>
|
||||
</Upload>
|
||||
|
@ -27,6 +27,7 @@ import {
|
||||
D3TooltipSettings,
|
||||
} from './plugins'
|
||||
import type {
|
||||
BaseDataType,
|
||||
ChartAxis,
|
||||
ChartDataset,
|
||||
ChartDomain,
|
||||
@ -50,13 +51,13 @@ export const getByAccessor = <DataType extends Record<any, any>, R>(accessor: ke
|
||||
return (d) => d[accessor]
|
||||
}
|
||||
|
||||
const createAxis = <DataType,>(config: ChartAxis<DataType>) => {
|
||||
const createAxis = <DataType extends BaseDataType>(config: ChartAxis<DataType>) => {
|
||||
if (config.type === 'time')
|
||||
return d3.scaleTime()
|
||||
return d3.scaleLinear()
|
||||
}
|
||||
|
||||
export type D3ChartProps<DataType> = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||
export type D3ChartProps<DataType extends BaseDataType> = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||
/** Параметры общей горизонтальной оси */
|
||||
xAxis: ChartAxis<DataType>
|
||||
/** Параметры графиков */
|
||||
@ -94,7 +95,7 @@ export type D3ChartProps<DataType> = React.DetailedHTMLProps<React.HTMLAttribute
|
||||
}
|
||||
}
|
||||
|
||||
const getDefaultXAxisConfig = <DataType,>(): ChartAxis<DataType> => ({
|
||||
const getDefaultXAxisConfig = <DataType extends BaseDataType>(): ChartAxis<DataType> => ({
|
||||
type: 'time',
|
||||
accessor: (d: any) => new Date(d.date)
|
||||
})
|
||||
|
@ -6,13 +6,14 @@ import { useD3MouseZone } from '@components/d3/D3MouseZone'
|
||||
import { D3TooltipPosition } from '@components/d3/plugins/D3Tooltip'
|
||||
import { getChartIcon, isDev, usePartialProps } from '@utils'
|
||||
|
||||
import { BaseDataType } from '../types'
|
||||
import { ChartGroup, ChartSizes } from './D3MonitoringCharts'
|
||||
|
||||
import '@styles/d3.less'
|
||||
|
||||
type D3GroupRenderFunction<DataType> = (group: ChartGroup<DataType>, data: DataType[]) => ReactNode
|
||||
type D3GroupRenderFunction<DataType extends BaseDataType> = (group: ChartGroup<DataType>, data: DataType[]) => ReactNode
|
||||
|
||||
export type D3HorizontalCursorSettings<DataType> = {
|
||||
export type D3HorizontalCursorSettings<DataType extends BaseDataType> = {
|
||||
width?: number
|
||||
height?: number
|
||||
render?: D3GroupRenderFunction<DataType>
|
||||
@ -23,7 +24,7 @@ export type D3HorizontalCursorSettings<DataType> = {
|
||||
lineStyle?: SVGProps<SVGLineElement>
|
||||
}
|
||||
|
||||
export type D3HorizontalCursorProps<DataType> = D3HorizontalCursorSettings<DataType> & {
|
||||
export type D3HorizontalCursorProps<DataType extends BaseDataType> = D3HorizontalCursorSettings<DataType> & {
|
||||
groups: ChartGroup<DataType>[]
|
||||
data: DataType[]
|
||||
sizes: ChartSizes
|
||||
@ -37,7 +38,7 @@ const defaultLineStyle: SVGProps<SVGLineElement> = {
|
||||
|
||||
const offsetY = 5
|
||||
|
||||
const makeDefaultRender = <DataType,>(): D3GroupRenderFunction<DataType> => (group, data) => (
|
||||
const makeDefaultRender = <DataType extends BaseDataType>(): D3GroupRenderFunction<DataType> => (group, data) => (
|
||||
<>
|
||||
{data.length > 0 ? group.charts.map((chart) => {
|
||||
const xFormat = (d: number | Date) => chart.xAxis.format?.(d) ?? `${(+d).toFixed(2)} ${chart.xAxis.unit ?? ''}`
|
||||
@ -62,7 +63,7 @@ const makeDefaultRender = <DataType,>(): D3GroupRenderFunction<DataType> => (gro
|
||||
</>
|
||||
)
|
||||
|
||||
const _D3HorizontalCursor = <DataType,>({
|
||||
const _D3HorizontalCursor = <DataType extends BaseDataType>({
|
||||
spaceBetweenGroups = 30,
|
||||
height = 200,
|
||||
render = makeDefaultRender<DataType>(),
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Button, Checkbox, Form, FormItemProps, Input, InputNumber, Select, Tooltip } from 'antd'
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react'
|
||||
|
||||
import { MinMax } from '@components/d3/types'
|
||||
import { BaseDataType, MinMax } from '@components/d3/types'
|
||||
import { ColorPicker, Color } from '@components/ColorPicker'
|
||||
|
||||
import { ExtendedChartDataset } from './D3MonitoringCharts'
|
||||
@ -18,13 +18,13 @@ const lineTypes = [
|
||||
{ value: 'needle', label: 'Иглы' },
|
||||
]
|
||||
|
||||
export type D3MonitoringChartEditorProps<DataType> = {
|
||||
export type D3MonitoringChartEditorProps<DataType extends BaseDataType> = {
|
||||
group: ExtendedChartDataset<DataType>[]
|
||||
chart: ExtendedChartDataset<DataType>
|
||||
onChange: (value: ExtendedChartDataset<DataType>) => boolean
|
||||
}
|
||||
|
||||
const _D3MonitoringChartEditor = <DataType,>({
|
||||
const _D3MonitoringChartEditor = <DataType extends BaseDataType>({
|
||||
group,
|
||||
chart: value,
|
||||
onChange,
|
||||
@ -93,8 +93,8 @@ const _D3MonitoringChartEditor = <DataType,>({
|
||||
</Item>
|
||||
<Item label={'Диапазон'}>
|
||||
<Input.Group compact>
|
||||
<InputNumber disabled={!!value.linkedTo} value={value.xDomain?.min} onChange={(min) => onDomainChange({ min })} placeholder={'Мин'} />
|
||||
<InputNumber disabled={!!value.linkedTo} value={value.xDomain?.max} onChange={(max) => onDomainChange({ max })} placeholder={'Макс'} />
|
||||
<InputNumber disabled={!!value.linkedTo} value={value.xDomain?.min} onChange={(min) => onDomainChange({ min: min ?? undefined })} placeholder={'Мин'} />
|
||||
<InputNumber disabled={!!value.linkedTo} value={value.xDomain?.max} onChange={(max) => onDomainChange({ max: max ?? undefined })} placeholder={'Макс'} />
|
||||
<Button
|
||||
disabled={!!value.linkedTo || (!Number.isFinite(value.xDomain?.min) && !Number.isFinite(value.xDomain?.max))}
|
||||
onClick={() => onDomainChange({ min: undefined, max: undefined })}
|
||||
|
@ -8,6 +8,7 @@ import LoaderPortal from '@components/LoaderPortal'
|
||||
import { isDev, usePartialProps, useUserSettings } from '@utils'
|
||||
|
||||
import {
|
||||
BaseDataType,
|
||||
ChartAxis,
|
||||
ChartDataset,
|
||||
ChartOffset,
|
||||
@ -51,7 +52,7 @@ const calculateDomain = (mm: MinMax): Required<MinMax> => {
|
||||
return { min, max }
|
||||
}
|
||||
|
||||
export type ExtendedChartDataset<DataType> = ChartDataset<DataType> & {
|
||||
export type ExtendedChartDataset<DataType extends BaseDataType> = ChartDataset<DataType> & {
|
||||
/** Диапазон отображаемых значений по горизонтальной оси */
|
||||
xDomain: MinMax
|
||||
/** Скрыть отображение шкалы графика */
|
||||
@ -60,9 +61,9 @@ export type ExtendedChartDataset<DataType> = ChartDataset<DataType> & {
|
||||
showCurrentValue?: boolean
|
||||
}
|
||||
|
||||
export type ExtendedChartRegistry<DataType> = ChartRegistry<DataType> & ExtendedChartDataset<DataType>
|
||||
export type ExtendedChartRegistry<DataType extends BaseDataType> = ChartRegistry<DataType> & ExtendedChartDataset<DataType>
|
||||
|
||||
export type ChartGroup<DataType> = {
|
||||
export type ChartGroup<DataType extends BaseDataType> = {
|
||||
/** Получить D3 выборку, содержащую корневой G-элемент группы */
|
||||
(): d3.Selection<SVGGElement, any, any, any>
|
||||
/** Уникальный ключ группы (индекс) */
|
||||
@ -86,12 +87,12 @@ const defaultRegulators: TelemetryRegulators = {
|
||||
5: { color: '#007070', label: 'Расход' },
|
||||
}
|
||||
|
||||
const getDefaultYAxisConfig = <DataType,>(): ChartAxis<DataType> => ({
|
||||
const getDefaultYAxisConfig = <DataType extends BaseDataType>(): ChartAxis<DataType> => ({
|
||||
type: 'time',
|
||||
accessor: (d: any) => new Date(d.date)
|
||||
})
|
||||
|
||||
const getDefaultYTicks = <DataType,>(): Required<ChartTick<DataType>> => ({
|
||||
const getDefaultYTicks = <DataType extends BaseDataType>(): Required<ChartTick<DataType>> => ({
|
||||
visible: false,
|
||||
format: (d: d3.NumberValue, idx: number, data?: DataType) => String(d),
|
||||
color: 'lightgray',
|
||||
@ -101,7 +102,7 @@ const getDefaultYTicks = <DataType,>(): Required<ChartTick<DataType>> => ({
|
||||
/**
|
||||
* @template DataType тип данных отображаемых записей
|
||||
*/
|
||||
export type D3MonitoringChartsProps<DataType extends Record<string, any>> = Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, 'ref'> & {
|
||||
export type D3MonitoringChartsProps<DataType extends BaseDataType> = Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, 'ref'> & {
|
||||
/** Двумерный массив датасетов (группа-график) */
|
||||
datasetGroups: ExtendedChartDataset<DataType>[][]
|
||||
/** Ширина графика числом пикселей или CSS-значением (px/%/em/rem) */
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { memo } from 'react'
|
||||
|
||||
import { BaseDataType } from '@components/d3/types'
|
||||
import { ChartGroup, ChartSizes } from '@components/d3/monitoring/D3MonitoringCharts'
|
||||
import { makeDisplayValue } from '@utils'
|
||||
|
||||
export type D3MonitoringCurrentValuesProps<DataType> = {
|
||||
export type D3MonitoringCurrentValuesProps<DataType extends BaseDataType> = {
|
||||
groups: ChartGroup<DataType>[]
|
||||
data: DataType[]
|
||||
left: number
|
||||
@ -12,7 +13,7 @@ export type D3MonitoringCurrentValuesProps<DataType> = {
|
||||
|
||||
const display = makeDisplayValue({ def: '---', fixed: 2 })
|
||||
|
||||
const _D3MonitoringCurrentValues = <DataType,>({ groups, data, left, sizes }: D3MonitoringCurrentValuesProps<DataType>) => (
|
||||
const _D3MonitoringCurrentValues = <DataType extends BaseDataType>({ groups, data, left, sizes }: D3MonitoringCurrentValuesProps<DataType>) => (
|
||||
<g transform={`translate(${left}, ${sizes.chartsTop})`} pointerEvents={'none'}>
|
||||
{groups.map((group) => (
|
||||
<g key={group.key} transform={`translate(${sizes.groupLeft(group.key)}, 0)`}>
|
||||
|
@ -1,17 +1,18 @@
|
||||
import { CSSProperties, Key, memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Button, Divider, Empty, Modal, Popconfirm, Tooltip, Tree } from 'antd'
|
||||
import { Button, Divider, Empty, Modal, Popconfirm, Tooltip, Tree, TreeDataNode } from 'antd'
|
||||
import { UndoOutlined } from '@ant-design/icons'
|
||||
import { EventDataNode } from 'antd/lib/tree'
|
||||
|
||||
import { notify } from '@components/factory'
|
||||
import { getChartIcon } from '@utils'
|
||||
|
||||
import { BaseDataType } from '../types'
|
||||
import { ExtendedChartDataset } from './D3MonitoringCharts'
|
||||
import { TelemetryRegulators } from './D3MonitoringLimitChart'
|
||||
import D3MonitoringChartEditor from './D3MonitoringChartEditor'
|
||||
import D3MonitoringLimitEditor from './D3MonitoringLimitEditor'
|
||||
|
||||
export type D3MonitoringGroupsEditorProps<DataType> = {
|
||||
export type D3MonitoringGroupsEditorProps<DataType extends BaseDataType> = {
|
||||
visible?: boolean
|
||||
groups: ExtendedChartDataset<DataType>[][]
|
||||
regulators: TelemetryRegulators
|
||||
@ -20,7 +21,7 @@ export type D3MonitoringGroupsEditorProps<DataType> = {
|
||||
onReset: () => void
|
||||
}
|
||||
|
||||
const getChartLabel = <DataType,>(chart: ExtendedChartDataset<DataType>) => (
|
||||
const getChartLabel = <DataType extends BaseDataType>(chart: ExtendedChartDataset<DataType>) => (
|
||||
<Tooltip title={chart.label}>
|
||||
{getChartIcon(chart)} {chart.label}
|
||||
</Tooltip>
|
||||
@ -34,14 +35,14 @@ const divStyle: CSSProperties = {
|
||||
flexGrow: 1,
|
||||
}
|
||||
|
||||
const getNodePos = (node: EventDataNode): { group: number, chart?: number } => {
|
||||
const getNodePos = (node: EventDataNode<TreeDataNode>): { group: number, chart?: number } => {
|
||||
const out = node.pos.split('-').map(Number)
|
||||
return { group: out[1], chart: out[2] }
|
||||
}
|
||||
|
||||
type EditingMode = null | 'limit' | 'chart'
|
||||
|
||||
const _D3MonitoringEditor = <DataType,>({
|
||||
const _D3MonitoringEditor = <DataType extends BaseDataType>({
|
||||
visible,
|
||||
groups: oldGroups,
|
||||
regulators: oldRegulators,
|
||||
@ -61,8 +62,8 @@ const _D3MonitoringEditor = <DataType,>({
|
||||
const onModalOk = useCallback(() => onChange(groups, regulators), [groups, regulators])
|
||||
|
||||
const onDrop = useCallback((info: {
|
||||
node: EventDataNode
|
||||
dragNode: EventDataNode
|
||||
node: EventDataNode<TreeDataNode>
|
||||
dragNode: EventDataNode<TreeDataNode>
|
||||
dropPosition: number
|
||||
}) => {
|
||||
const { dragNode, dropPosition, node } = info
|
||||
@ -152,12 +153,12 @@ const _D3MonitoringEditor = <DataType,>({
|
||||
<Tree
|
||||
draggable
|
||||
selectable
|
||||
onExpand={(keys) => setExpand(keys)}
|
||||
onExpand={(keys: Key[]) => setExpand(keys)}
|
||||
expandedKeys={expand}
|
||||
selectedKeys={selected}
|
||||
treeData={treeItems}
|
||||
onDrop={onDrop}
|
||||
onSelect={(value) => {
|
||||
onSelect={(value: Key[]) => {
|
||||
setSelected(value)
|
||||
setMode('chart')
|
||||
}}
|
||||
|
@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef } from 'react'
|
||||
import { Property } from 'csstype'
|
||||
import * as d3 from 'd3'
|
||||
|
||||
import { ChartRegistry } from '@components/d3/types'
|
||||
import { BaseDataType, ChartRegistry } from '@components/d3/types'
|
||||
import { useD3MouseZone } from '@components/d3/D3MouseZone'
|
||||
import { usePartialProps } from '@utils'
|
||||
|
||||
@ -32,12 +32,12 @@ export type D3LegendSettings = {
|
||||
|
||||
const defaultOffset = { x: 10, y: 10 }
|
||||
|
||||
export type D3LegendProps<DataType> = D3LegendSettings & {
|
||||
export type D3LegendProps<DataType extends BaseDataType> = D3LegendSettings & {
|
||||
/** Массив графиков */
|
||||
charts: ChartRegistry<DataType>[]
|
||||
}
|
||||
|
||||
const _D3Legend = <DataType,>({
|
||||
const _D3Legend = <DataType extends BaseDataType>({
|
||||
charts,
|
||||
width,
|
||||
height,
|
||||
|
@ -4,7 +4,7 @@ import * as d3 from 'd3'
|
||||
|
||||
import { isDev } from '@utils'
|
||||
|
||||
import { ChartRegistry } from '@components/d3/types'
|
||||
import { BaseDataType, ChartRegistry } from '@components/d3/types'
|
||||
import { D3MouseState, useD3MouseZone } from '@components/d3/D3MouseZone'
|
||||
import { getTouchedElements, wrapPlugin } from './base'
|
||||
|
||||
@ -12,7 +12,7 @@ import '@styles/d3.less'
|
||||
|
||||
export type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none'
|
||||
|
||||
export type D3RenderData<DataType> = {
|
||||
export type D3RenderData<DataType extends BaseDataType> = {
|
||||
/** Параметры графика */
|
||||
chart: ChartRegistry<DataType>
|
||||
/** Данные графика */
|
||||
@ -21,9 +21,9 @@ export type D3RenderData<DataType> = {
|
||||
selection?: d3.Selection<any, DataType, any, any>
|
||||
}
|
||||
|
||||
export type D3RenderFunction<DataType> = (data: D3RenderData<DataType>[], mouseState: D3MouseState) => ReactNode
|
||||
export type D3RenderFunction<DataType extends BaseDataType> = (data: D3RenderData<DataType>[], mouseState: D3MouseState) => ReactNode
|
||||
|
||||
export type D3TooltipSettings<DataType> = {
|
||||
export type D3TooltipSettings<DataType extends BaseDataType> = {
|
||||
/** Функция отрисоки тултипа */
|
||||
render?: D3RenderFunction<DataType>
|
||||
/** Ширина тултипа */
|
||||
@ -39,7 +39,7 @@ export type D3TooltipSettings<DataType> = {
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export const makeDefaultRender = <DataType,>(): D3RenderFunction<DataType> => (data, mouseState) => (
|
||||
export const makeDefaultRender = <DataType extends BaseDataType>(): D3RenderFunction<DataType> => (data, mouseState) => (
|
||||
<>
|
||||
{data.length > 0 ? data.map(({ chart, data }) => {
|
||||
let Icon
|
||||
@ -74,11 +74,11 @@ export const makeDefaultRender = <DataType,>(): D3RenderFunction<DataType> => (d
|
||||
</>
|
||||
)
|
||||
|
||||
export type D3TooltipProps<DataType> = Partial<D3TooltipSettings<DataType>> & {
|
||||
export type D3TooltipProps<DataType extends BaseDataType> = Partial<D3TooltipSettings<DataType>> & {
|
||||
charts: ChartRegistry<DataType>[],
|
||||
}
|
||||
|
||||
function _D3Tooltip<DataType extends Record<string, unknown>>({
|
||||
function _D3Tooltip<DataType extends BaseDataType>({
|
||||
width = 200,
|
||||
height = 120,
|
||||
render = makeDefaultRender<DataType>(),
|
||||
|
@ -3,7 +3,7 @@ import * as d3 from 'd3'
|
||||
|
||||
import { getDistance, TouchType } from '@utils'
|
||||
|
||||
import { ChartRegistry } from '../types'
|
||||
import { BaseDataType, ChartRegistry } from '../types'
|
||||
|
||||
export type BasePluginSettings = {
|
||||
enabled?: boolean
|
||||
@ -16,7 +16,7 @@ export const wrapPlugin = <TProps,>(
|
||||
const wrappedComponent = ({ enabled, ...props }: TProps & BasePluginSettings) => {
|
||||
if (!(enabled ?? defaultEnabled)) return <></>
|
||||
|
||||
return <Component {...(props as TProps)} />
|
||||
return <Component {...(props as (TProps & JSX.IntrinsicAttributes))} /> // IntrinsicAttributes добавлено как необходимое ограничение
|
||||
}
|
||||
|
||||
return wrappedComponent
|
||||
@ -89,7 +89,7 @@ const makeIsRectTouched = (x: number, y: number, limit: number, type: TouchType
|
||||
}
|
||||
}
|
||||
|
||||
export const getTouchedElements = <DataType,>(
|
||||
export const getTouchedElements = <DataType extends BaseDataType>(
|
||||
chart: ChartRegistry<DataType>,
|
||||
x: number,
|
||||
y: number,
|
||||
|
@ -1,8 +1,8 @@
|
||||
import * as d3 from 'd3'
|
||||
|
||||
import { ChartRegistry } from '../types'
|
||||
import { BaseDataType, ChartRegistry } from '../types'
|
||||
|
||||
export const appendTransition = <DataType, BaseType extends d3.BaseType, Datum, PElement extends d3.BaseType, PDatum>(
|
||||
export const appendTransition = <DataType extends BaseDataType, BaseType extends d3.BaseType, Datum, PElement extends d3.BaseType, PDatum>(
|
||||
elms: d3.Selection<BaseType, Datum, PElement, PDatum>,
|
||||
chart: ChartRegistry<DataType>
|
||||
): d3.Selection<BaseType, Datum, PElement, PDatum> => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ChartRegistry, PointChartDataset } from '@components/d3/types'
|
||||
import { BaseDataType, ChartRegistry, PointChartDataset } from '@components/d3/types'
|
||||
|
||||
import { appendTransition } from './base'
|
||||
|
||||
@ -12,7 +12,7 @@ const defaultConfig: Required<Omit<PointChartDataset, 'type'>> = {
|
||||
fillOpacity: 1,
|
||||
}
|
||||
|
||||
const getPointsRoot = <DataType,>(chart: ChartRegistry<DataType>, embeded?: boolean): d3.Selection<SVGGElement, any, any, any> => {
|
||||
const getPointsRoot = <DataType extends BaseDataType>(chart: ChartRegistry<DataType>, embeded?: boolean): d3.Selection<SVGGElement, any, any, any> => {
|
||||
const root = chart()
|
||||
if (!embeded) return root
|
||||
if (root.select('.points').empty())
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { getByAccessor } from '@components/d3/functions'
|
||||
import { ChartRegistry } from '@components/d3/types'
|
||||
import { BaseDataType, ChartRegistry } from '@components/d3/types'
|
||||
|
||||
import { appendTransition } from './base'
|
||||
|
||||
export const renderRectArea = <DataType extends Record<string, any>>(
|
||||
export const renderRectArea = <DataType extends BaseDataType>(
|
||||
xAxis: (value: d3.NumberValue) => number,
|
||||
yAxis: (value: d3.NumberValue) => number,
|
||||
chart: ChartRegistry<DataType>
|
||||
|
@ -3,9 +3,11 @@ import { Property } from 'csstype'
|
||||
|
||||
import { D3TooltipSettings } from './plugins'
|
||||
|
||||
export type AxisAccessor<DataType extends Record<string, any>> = keyof DataType | ((d: DataType) => any)
|
||||
export type BaseDataType = Record<string, any>
|
||||
|
||||
export type ChartAxis<DataType> = {
|
||||
export type AxisAccessor<DataType extends BaseDataType> = keyof DataType | ((d: DataType) => any)
|
||||
|
||||
export type ChartAxis<DataType extends BaseDataType> = {
|
||||
/** Тип шкалы */
|
||||
type: 'linear' | 'time',
|
||||
/** Ключ записи или метод по которому будет извлекаться значение оси из массива данных */
|
||||
@ -34,7 +36,7 @@ export type PointChartDataset = {
|
||||
fillOpacity?: number
|
||||
}
|
||||
|
||||
export type BaseChartDataset<DataType> = {
|
||||
export type BaseChartDataset<DataType extends BaseDataType> = {
|
||||
/** Уникальный ключ графика */
|
||||
key: string | number
|
||||
/** Параметры вертикальной оси */
|
||||
@ -101,7 +103,7 @@ export type NeedleChartDataset = {
|
||||
type: 'needle'
|
||||
}
|
||||
|
||||
export type ChartDataset<DataType> = BaseChartDataset<DataType> & (
|
||||
export type ChartDataset<DataType extends BaseDataType> = BaseChartDataset<DataType> & (
|
||||
AreaChartDataset |
|
||||
LineChartDataset |
|
||||
NeedleChartDataset |
|
||||
@ -154,7 +156,7 @@ export type ChartTicks<DataType> = {
|
||||
y?: ChartTick<DataType>
|
||||
}
|
||||
|
||||
export type ChartRegistry<DataType> = ChartDataset<DataType> & {
|
||||
export type ChartRegistry<DataType extends BaseDataType> = ChartDataset<DataType> & {
|
||||
/** Получить D3 выборку, содержащую корневой G-элемент графика */
|
||||
(): d3.Selection<SVGGElement, DataType, any, any>
|
||||
/** Получить значение по вертикальной оси из предоставленой записи */
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Button, Drawer, Skeleton, Tree, TreeProps, Typography } from 'antd'
|
||||
import { DefaultValueType } from 'rc-tree-select/lib/interface'
|
||||
import { useState, useEffect, ReactNode, useCallback, memo, Key } from 'react'
|
||||
import { Button, Drawer, Skeleton, Tree, TreeDataNode, TreeProps, Typography } from 'antd'
|
||||
import { useState, useEffect, useCallback, memo, Key } from 'react'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
|
||||
import { WellIcon, WellIconState } from '@components/icons'
|
||||
@ -17,26 +16,18 @@ export const getWellState = (idState?: number): WellIconState => idState === 1 ?
|
||||
export const checkIsWellOnline = (lastTelemetryDate: unknown): boolean =>
|
||||
isRawDate(lastTelemetryDate) && (Date.now() - +new Date(lastTelemetryDate) < 600_000)
|
||||
|
||||
export type TreeNodeData = {
|
||||
title?: string | null
|
||||
key?: string
|
||||
value?: DefaultValueType
|
||||
icon?: ReactNode
|
||||
children?: TreeNodeData[]
|
||||
}
|
||||
|
||||
const getKeyByUrl = (url?: string): [Key | null, string | null] => {
|
||||
const result = url?.match(/^\/([^\/]+)\/([^\/?]+)/) // pattern "/:type/:id"
|
||||
if (!result) return [null, null]
|
||||
return [result[0], result[1]]
|
||||
}
|
||||
|
||||
const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined => {
|
||||
const getLabel = (wellsTree: TreeDataNode[], value?: string): string | undefined => {
|
||||
const [url, type] = getKeyByUrl(value)
|
||||
if (!url) return
|
||||
let deposit: TreeNodeData | undefined
|
||||
let cluster: TreeNodeData | undefined
|
||||
let well: TreeNodeData | undefined
|
||||
let deposit: TreeDataNode | undefined
|
||||
let cluster: TreeDataNode | undefined
|
||||
let well: TreeDataNode | undefined
|
||||
switch (type) {
|
||||
case 'deposit':
|
||||
deposit = wellsTree.find((deposit) => deposit.key === url)
|
||||
@ -46,7 +37,7 @@ const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined
|
||||
|
||||
case 'cluster':
|
||||
deposit = wellsTree.find((deposit) => (
|
||||
cluster = deposit.children?.find((cluster: TreeNodeData) => cluster.key === url)
|
||||
cluster = deposit.children?.find((cluster: TreeDataNode) => cluster.key === url)
|
||||
))
|
||||
if (deposit && cluster)
|
||||
return `${deposit.title} / ${cluster.title}`
|
||||
@ -54,8 +45,8 @@ const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined
|
||||
|
||||
case 'well':
|
||||
deposit = wellsTree.find((deposit) => (
|
||||
cluster = deposit.children?.find((cluster: TreeNodeData) => (
|
||||
well = cluster.children?.find((well: TreeNodeData) => well.key === url)
|
||||
cluster = deposit.children?.find((cluster: TreeDataNode) => (
|
||||
well = cluster.children?.find((well: TreeDataNode) => well.key === url)
|
||||
))
|
||||
))
|
||||
if (deposit && cluster && well)
|
||||
@ -79,8 +70,26 @@ const sortWellsByActive = (a: WellDto, b: WellDto): number => {
|
||||
return (a.caption || '')?.localeCompare(b.caption || '')
|
||||
}
|
||||
|
||||
export const WellTreeSelector = memo(({ show, ...other }: TreeProps<TreeNodeData> & { show?: boolean }) => {
|
||||
const [wellsTree, setWellsTree] = useState<TreeNodeData[]>([])
|
||||
export type WellTreeSelectorProps = TreeProps<TreeDataNode> & {
|
||||
show?: boolean
|
||||
expand?: boolean | Key[]
|
||||
current?: Key
|
||||
}
|
||||
|
||||
const getExpandKeys = (treeData: TreeDataNode[], depositKeys?: Key[] | boolean): Key[] => {
|
||||
const out: Key[] = []
|
||||
treeData.forEach((deposit) => {
|
||||
if (Array.isArray(depositKeys) && !depositKeys.includes(deposit.key)) return
|
||||
if (deposit.key) out.push(deposit.key)
|
||||
deposit.children?.forEach((cluster) => {
|
||||
if (cluster.key) out.push(cluster.key)
|
||||
})
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
export const WellTreeSelector = memo<WellTreeSelectorProps>(({ show, expand, current, ...other }) => {
|
||||
const [wellsTree, setWellsTree] = useState<TreeDataNode[]>([])
|
||||
const [showLoader, setShowLoader] = useState<boolean>(false)
|
||||
const [visible, setVisible] = useState<boolean>(false)
|
||||
const [expanded, setExpanded] = useState<Key[]>([])
|
||||
@ -91,26 +100,20 @@ export const WellTreeSelector = memo(({ show, ...other }: TreeProps<TreeNodeData
|
||||
const location = useLocation()
|
||||
|
||||
useEffect(() => {
|
||||
setVisible((prev) => show ?? prev)
|
||||
setExpanded((prev) => {
|
||||
if (typeof show === 'undefined') return prev
|
||||
if (!show) return []
|
||||
const out: Key[] = []
|
||||
wellsTree.forEach((deposit) => {
|
||||
if (deposit.key) out.push(deposit.key)
|
||||
deposit.children?.forEach((cluster) => {
|
||||
if (cluster.key) out.push(cluster.key)
|
||||
})
|
||||
})
|
||||
return out
|
||||
})
|
||||
}, [wellsTree, show])
|
||||
if (current) setSelected([current])
|
||||
}, [current])
|
||||
|
||||
useEffect(() => setVisible((prev) => show ?? prev), [show])
|
||||
|
||||
useEffect(() => {
|
||||
setExpanded((prev) => expand ? getExpandKeys(wellsTree, expand) : prev)
|
||||
}, [wellsTree, expand])
|
||||
|
||||
useEffect(() => {
|
||||
invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
const deposits: Array<DepositDto> = await DepositService.getDeposits()
|
||||
const wellsTree: TreeNodeData[] = deposits.map(deposit =>({
|
||||
const wellsTree: TreeDataNode[] = deposits.map(deposit =>({
|
||||
title: deposit.caption,
|
||||
key: `/deposit/${deposit.id}`,
|
||||
value: `/deposit/${deposit.id}`,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { memo } from 'react'
|
||||
import { HTMLProps, memo } from 'react'
|
||||
import { Tooltip } from 'antd'
|
||||
import { UserOutlined } from '@ant-design/icons'
|
||||
|
||||
@ -6,33 +6,58 @@ import { UserDto } from '@api'
|
||||
import { Grid, GridItem } from '@components/Grid'
|
||||
import { CompanyView } from './CompanyView'
|
||||
|
||||
export type UserViewProps = {
|
||||
user?: UserDto
|
||||
export type UserViewProps = HTMLProps<HTMLSpanElement> & {
|
||||
user?: UserDto
|
||||
}
|
||||
|
||||
export const UserView = memo<UserViewProps>(({ user }) => user ? (
|
||||
<Tooltip title={(
|
||||
<Grid style={{ columnGap: '8px' }}>
|
||||
<GridItem row={1} col={1}>Фамилия:</GridItem>
|
||||
<GridItem row={1} col={2}>{user?.surname}</GridItem>
|
||||
export const UserView = memo<UserViewProps>(({ user, ...other }) =>
|
||||
user ? (
|
||||
<Tooltip
|
||||
title={
|
||||
<Grid style={{ columnGap: '8px' }}>
|
||||
<GridItem row={1} col={1}>
|
||||
Фамилия:
|
||||
</GridItem>
|
||||
<GridItem row={1} col={2}>
|
||||
{user?.surname}
|
||||
</GridItem>
|
||||
|
||||
<GridItem row={2} col={1}>Имя:</GridItem>
|
||||
<GridItem row={2} col={2}>{user?.name}</GridItem>
|
||||
<GridItem row={2} col={1}>
|
||||
Имя:
|
||||
</GridItem>
|
||||
<GridItem row={2} col={2}>
|
||||
{user?.name}
|
||||
</GridItem>
|
||||
|
||||
<GridItem row={3} col={1}>Отчество:</GridItem>
|
||||
<GridItem row={3} col={2}>{user?.patronymic}</GridItem>
|
||||
<GridItem row={3} col={1}>
|
||||
Отчество:
|
||||
</GridItem>
|
||||
<GridItem row={3} col={2}>
|
||||
{user?.patronymic}
|
||||
</GridItem>
|
||||
|
||||
<GridItem row={4} col={1}>Компания:</GridItem>
|
||||
<GridItem row={4} col={2}>
|
||||
<CompanyView company={user?.company}/>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
)}>
|
||||
<UserOutlined style={{ marginRight: 8 }}/>
|
||||
{user?.login}
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title='нет пользователя'>-</Tooltip>
|
||||
))
|
||||
<GridItem row={4} col={1}>
|
||||
Компания:
|
||||
</GridItem>
|
||||
<GridItem row={4} col={2}>
|
||||
<CompanyView company={user?.company} />
|
||||
</GridItem>
|
||||
</Grid>
|
||||
}
|
||||
>
|
||||
<span {...other}>
|
||||
<UserOutlined style={{ marginRight: 8 }} />
|
||||
{user?.login}
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title={'нет пользователя'}>
|
||||
<span {...other}>
|
||||
<UserOutlined style={{ marginRight: 8 }} />
|
||||
---
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
)
|
||||
|
||||
export default UserView
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { memo } from 'react'
|
||||
|
||||
import logo from '@images/logo_32.png'
|
||||
import { ReactComponent as AsbLogo } from '@images/dd_logo_white_opt.svg'
|
||||
|
||||
export const Logo = memo<React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>>((props) => (
|
||||
<img src={logo} alt={'АСБ'} className={'logo'} {...props} />
|
||||
export type LogoProps = React.SVGProps<SVGSVGElement> & { size?: number }
|
||||
|
||||
export const Logo = memo<LogoProps>(({ size = 200, ...props }) => (
|
||||
<AsbLogo className={'logo'} height={'100%'} {...props} />
|
||||
))
|
||||
|
||||
export default Logo
|
||||
|
1
src/images/dd_logo_white_opt.svg
Normal file
1
src/images/dd_logo_white_opt.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.4 KiB |
@ -1,5 +1,5 @@
|
||||
import { Map, Overlay } from 'pigeon-maps'
|
||||
import { useState, useEffect, memo } from 'react'
|
||||
import { useState, useEffect, memo, useMemo } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { Popover, Badge } from 'antd'
|
||||
|
||||
@ -49,6 +49,16 @@ const Deposit = memo(() => {
|
||||
|
||||
const location = useLocation()
|
||||
|
||||
const selectorProps = useMemo(() => {
|
||||
const hasId = location.pathname.length > '/deposit/'.length
|
||||
|
||||
return {
|
||||
show: true,
|
||||
expand: hasId ? [location.pathname] : true,
|
||||
current: hasId ? location.pathname : undefined,
|
||||
}
|
||||
}, [location.pathname])
|
||||
|
||||
useEffect(() => {
|
||||
invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
@ -63,7 +73,7 @@ const Deposit = memo(() => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<LayoutPortal noSheet showSelector title={'Месторождение'}>
|
||||
<LayoutPortal noSheet selector={selectorProps} title={'Месторождение'}>
|
||||
<LoaderPortal show={showLoader}>
|
||||
<div className={'h-100vh'}>
|
||||
<Map {...viewParams}>
|
||||
|
@ -137,6 +137,7 @@ export const DocumentsTemplate = ({ idCategory, well: givenWell, mimeTypes, head
|
||||
<div>
|
||||
<span>Загрузка</span>
|
||||
<UploadForm
|
||||
multiple
|
||||
url={uploadUrl}
|
||||
mimeTypes={mimeTypes}
|
||||
onUploadStart={() => setShowLoader(true)}
|
||||
|
@ -123,6 +123,7 @@ export const CategoryRender = memo(({ partData, onUpdate, onEdit, onHistory, set
|
||||
<div className={'file_actions'}>
|
||||
{permissionToUpload && (
|
||||
<UploadForm
|
||||
multiple
|
||||
url={uploadUrl}
|
||||
mimeTypes={MimeTypes.XLSX}
|
||||
style={{ margin: '5px 0 10px 0' }}
|
||||
|
@ -198,7 +198,7 @@ const table11Columns = [
|
||||
const table11Data = [
|
||||
{
|
||||
key: '1',
|
||||
l_label: 'Время начала:',
|
||||
l_label: 'Время начала',
|
||||
r_label: 'Время начала',
|
||||
l_value: 'climbBegin',
|
||||
r_value: 'climbFinish',
|
||||
@ -223,17 +223,17 @@ const table12Columns = [
|
||||
const table12Data = [
|
||||
{
|
||||
key: '1',
|
||||
l_label: 'По стволу:',
|
||||
l_label: 'По стволу',
|
||||
r_label: 'По вертикали',
|
||||
l_value: 'climbBegin',
|
||||
r_value: 'climbFinish',
|
||||
l_value: 'bottomholeDepth',
|
||||
r_value: 'verticalDepth',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
l_label: 'Зенитный',
|
||||
r_label: 'Азимутальный',
|
||||
l_value: 'descentBegin',
|
||||
r_value: 'descentFinish',
|
||||
l_value: 'zenithAngle',
|
||||
r_value: 'azimuthAngle',
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -6,7 +6,7 @@ import moment from 'moment'
|
||||
import { useWell } from '@asb/context'
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { DateRangeWrapper, Table, makeDateColumn, makeColumn } from '@components/Table'
|
||||
import { download, invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { download, invokeWebApiWrapperAsync, notify } from '@components/factory'
|
||||
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
|
||||
import { DailyReportService } from '@api'
|
||||
|
||||
@ -37,6 +37,16 @@ const DailyReport = memo(() => {
|
||||
|
||||
const checkIsDateBusy = useCallback((current) => current.isAfter(moment(), 'day') || data.some((row) => moment(row.reportDate).isSame(current, 'day')), [data])
|
||||
|
||||
const makeOnDownloadClick = useCallback((report) => async () => {
|
||||
const date = moment(report.reportDate)
|
||||
|
||||
try {
|
||||
await download(`/api/well/${well.id}/DailyReport/${date.format('YYYY-MM-DD')}/excel`)
|
||||
} catch {
|
||||
notify(`Не удалось скачать суточный рапорт от ${date.format('DD.MM.YYYY')}`, 'error', well)
|
||||
}
|
||||
}, [well])
|
||||
|
||||
const columns = useMemo(() => [
|
||||
makeDateColumn('Дата', 'reportDate', undefined, 'DD.MM.YYYY', { width: 300 }),
|
||||
makeColumn('', '', { width: 200, render: (_, report) => (
|
||||
@ -44,7 +54,7 @@ const DailyReport = memo(() => {
|
||||
<Button
|
||||
type={'link'}
|
||||
icon={<FileExcelOutlined />}
|
||||
onClick={async () => await download(`/api/well/${well.id}/DailyReport/${report.reportDate}/excel`)}
|
||||
onClick={makeOnDownloadClick(report)}
|
||||
children={'Скачать XLSX'}
|
||||
/>
|
||||
<Button
|
||||
@ -58,7 +68,7 @@ const DailyReport = memo(() => {
|
||||
/>
|
||||
</>
|
||||
)}),
|
||||
], [well])
|
||||
], [makeOnDownloadClick])
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (!searchDate) return data
|
||||
|
@ -74,7 +74,6 @@ export const DrillerList = memo(({ loading, drillers, onChange }) => {
|
||||
<Button
|
||||
onClick={onModalOpen}
|
||||
loading={isLoading}
|
||||
style={{ marginRight: '10px' }}
|
||||
>Список бурильщиков</Button>
|
||||
</>
|
||||
)
|
||||
|
@ -121,7 +121,6 @@ export const DrillerSchedule = memo(({ drillers, loading, onChange }) => {
|
||||
<Button
|
||||
onClick={onModalOpen}
|
||||
loading={isLoading}
|
||||
style={{ marginRight: '10px' }}
|
||||
>Расписание бурильщиков</Button>
|
||||
</>
|
||||
)
|
||||
|
@ -117,7 +117,6 @@ const Operations = memo(() => {
|
||||
<Select
|
||||
allowClear={false}
|
||||
options={categories}
|
||||
style={{ marginRight: '10px' }}
|
||||
onChange={(v) => setSelectedCategory(v)}
|
||||
value={selectedCategory}
|
||||
/>
|
||||
@ -127,7 +126,6 @@ const Operations = memo(() => {
|
||||
disabled={isLoading}
|
||||
disabledDate={disabledDates}
|
||||
disabledTime={disabledTimes}
|
||||
style={{ marginRight: '10px' }}
|
||||
showTime={{ hideDisabledOptions: true }}
|
||||
/>
|
||||
<InputNumber
|
||||
@ -137,7 +135,6 @@ const Operations = memo(() => {
|
||||
onChange={setYDomain}
|
||||
addonAfter={'мин'}
|
||||
addonBefore={'Верхняя граница'}
|
||||
style={{ marginRight: '10px' }}
|
||||
/>
|
||||
{permissions.driller.get && (
|
||||
<>
|
||||
|
@ -6,7 +6,7 @@ import { useWell } from '@asb/context'
|
||||
import { makeDateSorter } from '@components/Table'
|
||||
import { D3MonitoringCharts } from '@components/d3/monitoring'
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { Grid, GridItem, Flex } from '@components/Grid'
|
||||
import { Grid, GridItem } from '@components/Grid'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
|
||||
import { formatDate, hasPermission, wrapPrivateComponent } from '@utils'
|
||||
@ -31,6 +31,7 @@ import MomentStabPicDisabled from '@images/DempherOff.png'
|
||||
import SpinPicEnabled from '@images/SpinEnabled.png'
|
||||
import SpinPicDisabled from '@images/SpinDisabled.png'
|
||||
|
||||
import '@styles/monitoring.less'
|
||||
import '@styles/message.css'
|
||||
|
||||
const { Option } = Select
|
||||
@ -87,25 +88,25 @@ export const makeChartGroups = (flowChart) => {
|
||||
makeDataset('Давление', 'Давл','#FF0000', 'pressure', 'атм'),
|
||||
makeDataset('Давление заданное', 'Давл зад-е','#CC0000', 'pressureSp', 'атм'),
|
||||
makeDataset('Давление ХХ', 'Давл ХХ','#CC4429', 'pressureIdle', 'атм', { dash }),
|
||||
makeDataset('Перепад давления максимальный', 'ΔР макс','#B34A36', 'pressureDeltaLimitMax', 'атм', { dash }),
|
||||
makeDataset('Перепад давления МАКС', 'ΔР макс','#B34A36', 'pressureDeltaLimitMax', 'атм', { dash }),
|
||||
makeDataset('Давление', 'Давл','#FF0000', 'pressureMM', 'атм', makeAreaOptions('pressure')),
|
||||
], [
|
||||
makeDataset('Осевая нагрузка', 'Нагр','#0000CC', 'axialLoad', 'т'),
|
||||
makeDataset('Осевая нагрузка заданная', 'Нагр зад-я','#3D6DCC', 'axialLoadSp', 'т', { dash }),
|
||||
makeDataset('Осевая нагрузка максимальная', 'Нагр макс','#3D3DCC', 'axialLoadLimitMax', 'т', { dash }),
|
||||
makeDataset('Осевая нагрузка МАКС', 'Нагр макс','#3D3DCC', 'axialLoadLimitMax', 'т', { dash }),
|
||||
makeDataset('Осевая нагрузка', 'Нагр','#0000CC', 'axialLoadMM', 'т', makeAreaOptions('axialLoad')),
|
||||
], [
|
||||
makeDataset('Вес на крюке', 'Вес на крюке','#00B3B3', 'hookWeight', 'т'),
|
||||
makeDataset('Вес инструмента ХХ', 'Вес инст ХХ','#29CCB1', 'hookWeightIdle', 'т', { dash }),
|
||||
makeDataset('Вес инструмента минимальный', 'Вес инст мин','#47A1B3', 'hookWeightLimitMin', 'т', { dash }),
|
||||
makeDataset('Вес инструмента максимальный', 'Вес инст мах','#2D7280', 'hookWeightLimitMax', 'т', { dash }),
|
||||
makeDataset('Вес инструмента МИН', 'Вес инст мин','#47A1B3', 'hookWeightLimitMin', 'т', { dash }),
|
||||
makeDataset('Вес инструмента МАКС', 'Вес инст мах','#2D7280', 'hookWeightLimitMax', 'т', { dash }),
|
||||
makeDataset('Обороты ротора', 'Об ротора','#11B32F', 'rotorSpeed', 'об/мин'),
|
||||
makeDataset('Обороты ротора', 'Об ротора','#11B32F', 'rotorSpeedMM', 'об/мин', makeAreaOptions('rotorSpeed')),
|
||||
], [
|
||||
makeDataset('Момент на роторе', 'Момент','#990099', 'rotorTorque', 'кН·м'),
|
||||
makeDataset('План. Момент на роторе', 'Момент зад-й','#9629CC', 'rotorTorqueSp', 'кН·м', { dash }),
|
||||
makeDataset('Момент на роторе х.х.', 'Момень ХХ','#CC2996', 'rotorTorqueIdle', 'кН·м', { dash }),
|
||||
makeDataset('Момент максимальный', 'Момент макс','#FF00FF', 'rotorTorqueLimitMax', 'кН·м', { dash }),
|
||||
makeDataset('Момент на роторе ХХ', 'Момент ХХ','#CC2996', 'rotorTorqueIdle', 'кН·м', { dash }),
|
||||
makeDataset('Момент МАКС.', 'Момент макс','#FF00FF', 'rotorTorqueLimitMax', 'кН·м', { dash }),
|
||||
makeDataset('Момент на роторе', 'Момент','#990099', 'rotorTorqueMM', 'кН·м', makeAreaOptions('rotorTorque')),
|
||||
]
|
||||
]
|
||||
@ -253,14 +254,14 @@ const TelemetryView = memo(() => {
|
||||
<LoaderPortal show={showLoader}>
|
||||
<Grid style={{ gridTemplateColumns: 'auto repeat(6, 1fr)' }}>
|
||||
<GridItem col={'1'} row={'1'} colSpan={'8'} style={{ marginBottom: '0.5rem' }}>
|
||||
<Flex>
|
||||
<div className={'page-top'}>
|
||||
<ModeDisplay data={dataSaub} />
|
||||
<div style={{ marginLeft: '1rem' }}>
|
||||
<div>
|
||||
Интервал:
|
||||
<PeriodPicker onChange={setChartInterval} />
|
||||
</div>
|
||||
<Button onClick={() => chartMethods?.setSettingsVisible(true)}>Настроить графики</Button>
|
||||
<div style={{ marginLeft: '1rem' }}>
|
||||
<div>
|
||||
Статус:
|
||||
<Select value={well.idState ?? 0} onChange={onStatusChanged} disabled={!hasPermission('Well.edit')}>
|
||||
<Option value={0} disabled hidden>Неизвестно</Option>
|
||||
@ -268,14 +269,16 @@ const TelemetryView = memo(() => {
|
||||
<Option value={2}>Завершено</Option>
|
||||
</Select>
|
||||
</div>
|
||||
<Setpoints style={{ marginLeft: '1rem' }} />
|
||||
<Setpoints />
|
||||
<span style={{ flexGrow: 20 }}> </span>
|
||||
<WirelineRunOut />
|
||||
<img src={isTorqueStabEnabled(dataSpin) ? MomentStabPicEnabled : MomentStabPicDisabled} style={{ marginRight: '15px' }} alt={'TorqueMaster'} />
|
||||
<img src={isSpinEnabled(dataSpin) ? SpinPicEnabled : SpinPicDisabled} style={{ marginRight: '15px' }} alt={'SpinMaster'} />
|
||||
<h2 style={{ marginBottom: 0, marginRight: '15px', fontWeight: 'bold', color: isMseEnabled(dataSaub) ? 'green' : 'lightgrey' }}>MSE</h2>
|
||||
<div className={'icons'}>
|
||||
<img src={isTorqueStabEnabled(dataSpin) ? MomentStabPicEnabled : MomentStabPicDisabled} alt={'TorqueMaster'} />
|
||||
<img src={isSpinEnabled(dataSpin) ? SpinPicEnabled : SpinPicDisabled} alt={'SpinMaster'} />
|
||||
<h2 style={{ marginBottom: 0, fontWeight: 'bold', color: isMseEnabled(dataSaub) ? 'green' : 'lightgrey' }}>MSE</h2>
|
||||
</div>
|
||||
<UserOfWell data={dataSaub} />
|
||||
</Flex>
|
||||
</div>
|
||||
</GridItem>
|
||||
<GridItem col={'1'} row={'2'} rowSpan={'3'} style={{ minWidth: '260px', width: '0.142fr' }}>
|
||||
<CustomColumn data={dataSaub} />
|
||||
|
@ -17,6 +17,7 @@ import { WellService } from '@api'
|
||||
|
||||
import Measure from './Measure'
|
||||
import Reports from './Reports'
|
||||
import WellCase from './WellCase'
|
||||
import Analytics from './Analytics'
|
||||
import Documents from './Documents'
|
||||
import Telemetry from './Telemetry'
|
||||
@ -68,6 +69,7 @@ const Well = memo(() => {
|
||||
<PrivateMenu.Link content={Documents} icon={<FolderOutlined />} />
|
||||
<PrivateMenu.Link content={Measure} icon={<ExperimentOutlined />} />
|
||||
<PrivateMenu.Link content={DrillingProgram} icon={<FolderOutlined />} />
|
||||
<PrivateMenu.Link content={WellCase} icon={<FolderOutlined />} />
|
||||
</PrivateMenu>
|
||||
|
||||
<WellContext.Provider value={[well, updateWell]}>
|
||||
@ -84,6 +86,7 @@ const Well = memo(() => {
|
||||
<Route path={Documents.route} element={<Documents />} />
|
||||
<Route path={Measure.route} element={<Measure />} />
|
||||
<Route path={DrillingProgram.route} element={<DrillingProgram />} />
|
||||
<Route path={WellCase.route} element={<WellCase />} />
|
||||
</Routes>
|
||||
</Content>
|
||||
</Layout>
|
||||
|
60
src/pages/WellCase/HistoryTable.jsx
Normal file
60
src/pages/WellCase/HistoryTable.jsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
import { Empty, Timeline } from 'antd'
|
||||
import moment from 'moment'
|
||||
|
||||
import { useWell } from '@asb/context'
|
||||
import { UserView } from '@components/views'
|
||||
import DownloadLink from '@components/DownloadLink'
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { WellFinalDocumentsService } from '@api'
|
||||
import { formatDate } from '@utils'
|
||||
|
||||
import '@styles/well_case.less'
|
||||
|
||||
export const HistoryTable = memo(({ category }) => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [files, setFiles] = useState([])
|
||||
|
||||
const [well] = useWell()
|
||||
|
||||
useEffect(() => {
|
||||
invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
const result = await WellFinalDocumentsService.getFilesHistoryByIdCategory(well.id, category.idCategory)
|
||||
if (!result) return
|
||||
const files = result.file
|
||||
files.sort((a, b) => moment(a.uploadDate) - moment(b.uploadDate))
|
||||
const fileSource = files.map((file) => ({
|
||||
file,
|
||||
date: file.uploadDate,
|
||||
user: file.author,
|
||||
}))
|
||||
setFiles(fileSource)
|
||||
},
|
||||
setIsLoading,
|
||||
`Не удалось загрузить историю файлов категории "${category.nameCategory}"`,
|
||||
{ actionName: `Загрузка истории файлов категории "${category.nameCategory}"`, well }
|
||||
)
|
||||
}, [well, category])
|
||||
|
||||
return (
|
||||
<LoaderPortal show={isLoading}>
|
||||
{files.length > 0 ? (
|
||||
<Timeline className={'history-timeline'} reverse>
|
||||
{files.map(({ date, user, file }) => (
|
||||
<Timeline.Item key={date}>
|
||||
{formatDate(date)}
|
||||
<UserView user={user} style={{ marginLeft: 10 }} />
|
||||
<DownloadLink file={file} />
|
||||
</Timeline.Item>
|
||||
))}
|
||||
</Timeline>
|
||||
) : (
|
||||
<Empty description={'Нет данных о версиях файлов'} />
|
||||
)}
|
||||
</LoaderPortal>
|
||||
)
|
||||
})
|
||||
|
||||
export default HistoryTable
|
164
src/pages/WellCase/WellCaseEditor.jsx
Normal file
164
src/pages/WellCase/WellCaseEditor.jsx
Normal file
@ -0,0 +1,164 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { AutoComplete, List, Modal, Tooltip, Transfer } from 'antd'
|
||||
|
||||
import { useWell } from '@asb/context'
|
||||
import { UserView } from '@components/views'
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { WellFinalDocumentsService } from '@api'
|
||||
import { arrayOrDefault } from '@utils'
|
||||
|
||||
import '@styles/well_case.less'
|
||||
|
||||
const filterCategoriesByText = (text) => (cat) =>
|
||||
!text || !!cat?.nameCategory?.toLowerCase().includes(text.toLowerCase())
|
||||
|
||||
const filterUserByText = (text, user) =>
|
||||
!text || (user && `${user.login} ${user.email} ${user.surname} ${user.name} ${user.patronymic}`.toLowerCase().includes(text.toLowerCase()))
|
||||
|
||||
export const WellCaseEditor = memo(({ categories: currentCategories, show, onClose }) => {
|
||||
const [users, setUsersDataSource] = useState([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [categories, setCategories] = useState([])
|
||||
const [availableCategories, setAvailableCategories] = useState([])
|
||||
const [catSearchText, setCatSearchText] = useState(null)
|
||||
const [selectedCatId, setSelectedCatId] = useState(null)
|
||||
|
||||
const [well] = useWell()
|
||||
|
||||
const unusedCategories = useMemo(() => availableCategories.filter(({ id }) => !categories.find((cat) => cat.idCategory === id)), [categories, availableCategories])
|
||||
|
||||
const catOptions = useMemo(() => {
|
||||
return unusedCategories
|
||||
.filter(filterCategoriesByText(catSearchText))
|
||||
.map((cat) => ({ label: cat.name, value: cat.name, id: cat.id }))
|
||||
}, [catSearchText, unusedCategories])
|
||||
|
||||
const onModalOk = useCallback(() => {
|
||||
invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
await WellFinalDocumentsService.updateRange(well.id, categories)
|
||||
onClose?.(true)
|
||||
},
|
||||
setIsLoading,
|
||||
'Не удалось изменить ответственных',
|
||||
{ actionName: 'Изменение ответственных', well }
|
||||
)
|
||||
}, [onClose, well, categories])
|
||||
|
||||
const onModalCancel = useCallback(() => onClose?.(false), [onClose])
|
||||
|
||||
const onAddCategory = useCallback((name, cat) => {
|
||||
setCategories((prev) => [...prev, { idCategory: cat.id, nameCategory: name, publishers: [] }])
|
||||
}, [])
|
||||
|
||||
const categoryRender = useCallback((item) => {
|
||||
const hasPublishers = item.idsPublishers?.length > 0
|
||||
const selected = selectedCatId === item.idCategory
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
className={`category-list-item ${!hasPublishers ? 'empty' : ''} ${selected ? 'active' : ''}`}
|
||||
onClick={() => setSelectedCatId(item.idCategory)}
|
||||
>
|
||||
{hasPublishers ? (item.nameCategory) : (
|
||||
<Tooltip title={'Категория будет скрыта, так как не назначены ответственные'}>
|
||||
{item.nameCategory}
|
||||
</Tooltip>
|
||||
)}
|
||||
</List.Item>
|
||||
)
|
||||
}, [selectedCatId])
|
||||
|
||||
const onPublishersChange = useCallback((selectedPublishers) => {
|
||||
setCategories((prev) => {
|
||||
const cats = [...prev]
|
||||
const idx = cats.findIndex((cat) => cat.idCategory === selectedCatId)
|
||||
cats[idx].idsPublishers = selectedPublishers
|
||||
return cats
|
||||
})
|
||||
}, [selectedCatId])
|
||||
|
||||
useEffect(() => {
|
||||
const cats = currentCategories?.map((cat) => ({
|
||||
idCategory: cat.idCategory,
|
||||
nameCategory: cat.nameCategory,
|
||||
idsPublishers: cat.publishers.map((user) => user.id),
|
||||
}))
|
||||
setCategories(cats ?? [])
|
||||
}, [currentCategories])
|
||||
|
||||
useEffect(() => {
|
||||
invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
const users = arrayOrDefault(await WellFinalDocumentsService.getAvailableUsers(well.id))
|
||||
const usersDataSource = users.map((user) => ({ key: user.id, ...user }))
|
||||
setUsersDataSource(usersDataSource)
|
||||
},
|
||||
setIsLoading,
|
||||
'Не удалось загрузить список доступных публикаторов',
|
||||
{ actionName: 'Загрузка списка доступных публикаторов', well }
|
||||
)
|
||||
}, [well])
|
||||
|
||||
useEffect(() => {
|
||||
invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
const categories = arrayOrDefault(await WellFinalDocumentsService.getWellCaseCategories())
|
||||
setAvailableCategories(categories)
|
||||
},
|
||||
setIsLoading,
|
||||
'Не удалось загрузить список доступных категорий',
|
||||
{ actionName: 'Загрузка списка доступных категорий' }
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
centered
|
||||
width={1000}
|
||||
visible={show}
|
||||
onOk={onModalOk}
|
||||
onCancel={onModalCancel}
|
||||
okText={'Сохранить'}
|
||||
title={'Редактирование ответственных за категории'}
|
||||
>
|
||||
<LoaderPortal show={isLoading} style={{ width: '100%', height: '100%' }}>
|
||||
<div className={'well-case-editor'}>
|
||||
<div className={'category-list'}>
|
||||
<AutoComplete
|
||||
allowClear
|
||||
options={catOptions}
|
||||
onSearch={(searchText) => setCatSearchText(searchText)}
|
||||
onSelect={onAddCategory}
|
||||
placeholder={'Впишите категорию для добавления'}
|
||||
/>
|
||||
<List
|
||||
bordered
|
||||
size={'small'}
|
||||
style={{ flexGrow: 1 }}
|
||||
itemLayout={'horizontal'}
|
||||
dataSource={categories}
|
||||
renderItem={categoryRender}
|
||||
/>
|
||||
</div>
|
||||
<Transfer
|
||||
showSearch
|
||||
dataSource={users}
|
||||
disabled={!selectedCatId}
|
||||
listStyle={{ width: 300, height: '100%' }}
|
||||
titles={['Пользователи', 'Публикаторы']}
|
||||
onChange={onPublishersChange}
|
||||
filterOption={filterUserByText}
|
||||
targetKeys={categories.find((cat) => cat.idCategory === selectedCatId)?.idsPublishers ?? []}
|
||||
render={(item) => (
|
||||
<UserView user={item} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</LoaderPortal>
|
||||
</Modal>
|
||||
)
|
||||
})
|
||||
|
||||
export default WellCaseEditor
|
107
src/pages/WellCase/index.jsx
Normal file
107
src/pages/WellCase/index.jsx
Normal file
@ -0,0 +1,107 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Alert, Button, Typography } from 'antd'
|
||||
|
||||
import { useWell } from '@asb/context'
|
||||
import { UserView } from '@components/views'
|
||||
import UploadForm from '@components/UploadForm'
|
||||
import DownloadLink from '@components/DownloadLink'
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { makeColumn, makeDateColumn, makeTextColumn, Table } from '@components/Table'
|
||||
import { WellFinalDocumentsService } from '@api'
|
||||
import { wrapPrivateComponent } from '@utils'
|
||||
|
||||
import WellCaseEditor from './WellCaseEditor'
|
||||
import { HistoryTable } from './HistoryTable'
|
||||
|
||||
import '@styles/well_case.less'
|
||||
|
||||
const expandable = {
|
||||
expandedRowRender: (category) => <HistoryTable category={category} />,
|
||||
}
|
||||
|
||||
const WellCase = memo(() => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [categories, setCategories] = useState([])
|
||||
const [canEdit, setCanEdit] = useState(false)
|
||||
const [showEdit, setShowEdit] = useState(false)
|
||||
|
||||
const [well] = useWell()
|
||||
|
||||
const updateTable = useCallback(() => {
|
||||
invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
const { permissionToSetPubliher, wellFinalDocuments } = await WellFinalDocumentsService.get(well.id)
|
||||
|
||||
setCategories(wellFinalDocuments.map((cat) => ({ ...cat, uploadDate: cat.file?.uploadDate })))
|
||||
setCanEdit(permissionToSetPubliher)
|
||||
},
|
||||
setIsLoading,
|
||||
'Не удалось загрузить список категорий',
|
||||
{ actionName: 'Загрузка списка категорий', well }
|
||||
)
|
||||
}, [well])
|
||||
|
||||
const columns = useMemo(() => [
|
||||
makeTextColumn('Категория', 'nameCategory'),
|
||||
makeColumn('Файл', 'file', {
|
||||
render: (file, category) => (
|
||||
<div className={'file-cell'}>
|
||||
{file ? <DownloadLink file={file} /> : <span style={{ marginLeft: 15 }}>Файл не загружен</span>}
|
||||
|
||||
{category.permissionToUpload && (
|
||||
<UploadForm
|
||||
url={`/api/WellFinalDocuments/${well.id}?idCategory=${category.idCategory}`}
|
||||
onUploadStart={() => setIsLoading(true)}
|
||||
onUploadComplete={updateTable}
|
||||
onUploadError={() => setIsLoading(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}),
|
||||
makeDateColumn('Дата загрузки', 'uploadDate'),
|
||||
makeColumn('Ответственные', 'publishers', {
|
||||
render: (publishers) => publishers?.map((user) => <UserView user={user} style={{ marginLeft: 10 }} />),
|
||||
}),
|
||||
], [well, updateTable])
|
||||
|
||||
const onEditClose = useCallback((changed = false) => {
|
||||
setShowEdit(false)
|
||||
if (changed) updateTable()
|
||||
}, [updateTable])
|
||||
|
||||
useEffect(updateTable, [updateTable])
|
||||
|
||||
return (
|
||||
<div className={'well-case-page'}>
|
||||
<LoaderPortal show={isLoading}>
|
||||
{canEdit && (
|
||||
<Alert
|
||||
type={'info'}
|
||||
className={'customer-block'}
|
||||
showIcon
|
||||
message={'Вам доступно редактирование ответственных.'}
|
||||
action={<Button onClick={() => setShowEdit(true)}>Редактировать</Button>}
|
||||
/>
|
||||
)}
|
||||
<Table
|
||||
bordered
|
||||
size={'small'}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
dataSource={categories}
|
||||
expandable={expandable}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
<WellCaseEditor categories={categories} show={showEdit} onClose={onEditClose} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default wrapPrivateComponent(WellCase, {
|
||||
title: 'Дело скважины',
|
||||
route: 'well_case',
|
||||
requirements: [],
|
||||
// requirements: ['WellFinalDocuments.get'],
|
||||
})
|
@ -6,7 +6,7 @@ import { ErrorFetch } from '@components/ErrorFetch'
|
||||
import { UploadForm } from '@components/UploadForm'
|
||||
|
||||
const errorTextStyle = { color: 'red', fontWeight: 'bold' }
|
||||
const uploadFormStyle = { marginTop: '24px' }
|
||||
const uploadFormStyle = { marginTop: 24 }
|
||||
|
||||
export const ImportOperations = memo(({ well: givenWell, onDone }) => {
|
||||
const [deleteBeforeImport, setDeleteBeforeImport] = useState(false)
|
||||
@ -15,7 +15,7 @@ export const ImportOperations = memo(({ well: givenWell, onDone }) => {
|
||||
const [wellContext] = useWell()
|
||||
const well = useMemo(() => givenWell ?? wellContext, [givenWell, wellContext])
|
||||
|
||||
const url = useMemo(() => `/api/well/${well.id}/wellOperations/import${deleteBeforeImport ? '/1' : '/0'}`, [well])
|
||||
const url = useMemo(() => `/api/well/${well.id}/wellOperations/import/${deleteBeforeImport ? 1 : 0}`, [well, deleteBeforeImport])
|
||||
|
||||
const onUploadSuccess = useCallback(() => {
|
||||
setErrorText('')
|
||||
@ -34,6 +34,7 @@ export const ImportOperations = memo(({ well: givenWell, onDone }) => {
|
||||
<span>Очистить список операций перед импортом </span>
|
||||
<Switch onChange={setDeleteBeforeImport} checked={deleteBeforeImport} />
|
||||
<UploadForm
|
||||
multiple
|
||||
url={url}
|
||||
style={uploadFormStyle}
|
||||
onUploadSuccess={onUploadSuccess}
|
||||
|
@ -1,17 +1,87 @@
|
||||
import { useState, useEffect, useCallback, memo, useMemo } from 'react'
|
||||
import { InputNumber } from 'antd'
|
||||
|
||||
import { useWell } from '@asb/context'
|
||||
import {
|
||||
EditableTable,
|
||||
makeSelectColumn,
|
||||
makeNumericAvgRange,
|
||||
makeGroupColumn,
|
||||
makeNumericRender,
|
||||
makeNumericSorter,
|
||||
RegExpIsFloat,
|
||||
} from '@components/Table'
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { DrillParamsService, WellOperationService } from '@api'
|
||||
import { arrayOrDefault } from '@utils'
|
||||
|
||||
import { makeNumericObjSorter } from '@components/Table/sorters'
|
||||
|
||||
const makeNumericObjRender = (fixed, columnKey) => (value, obj) => {
|
||||
let val = '-'
|
||||
const isSelected = obj && columnKey && obj[columnKey[0]] ? obj[columnKey[0]][columnKey[1]] : false
|
||||
|
||||
if ((value ?? null) !== null && Number.isFinite(+value)) {
|
||||
val = (fixed ?? null) !== null
|
||||
? (+value).toFixed(fixed)
|
||||
: (+value).toPrecision(5)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`text-align-r-container ${isSelected ? 'color-pale-green' : ''}`}>
|
||||
<span>{val}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const makeNumericColumnOptionsWithColor = (fixed, sorterKey, columnKey) => ({
|
||||
editable: true,
|
||||
initialValue: 0,
|
||||
width: 100,
|
||||
sorter: sorterKey ? makeNumericObjSorter(sorterKey) : undefined,
|
||||
formItemRules: [{
|
||||
required: true,
|
||||
message: 'Введите число',
|
||||
pattern: RegExpIsFloat,
|
||||
}],
|
||||
render: makeNumericObjRender(fixed, columnKey),
|
||||
})
|
||||
|
||||
const makeNumericObjColumn = (
|
||||
title,
|
||||
dataIndex,
|
||||
filters,
|
||||
filterDelegate,
|
||||
renderDelegate,
|
||||
width,
|
||||
other
|
||||
) => ({
|
||||
title: title,
|
||||
dataIndex: dataIndex,
|
||||
key: dataIndex,
|
||||
filters: filters,
|
||||
onFilter: filterDelegate ? filterDelegate(dataIndex) : null,
|
||||
sorter: makeNumericObjSorter(dataIndex),
|
||||
width: width,
|
||||
input: <InputNumber style={{ width: '100%' }}/>,
|
||||
render: renderDelegate ?? makeNumericRender(),
|
||||
align: 'right',
|
||||
...other
|
||||
})
|
||||
|
||||
const makeNumericAvgRange = (
|
||||
title,
|
||||
dataIndex,
|
||||
fixed,
|
||||
filters,
|
||||
filterDelegate,
|
||||
renderDelegate,
|
||||
width
|
||||
) => makeGroupColumn(title, [
|
||||
makeNumericObjColumn('мин', [dataIndex, 'min'], filters, filterDelegate, renderDelegate, width, makeNumericColumnOptionsWithColor(fixed, [dataIndex, 'min'], [dataIndex, 'isMin'])),
|
||||
makeNumericObjColumn('сред', [dataIndex, 'avg'], filters, filterDelegate, renderDelegate, width, makeNumericColumnOptionsWithColor(fixed, [dataIndex, 'avg'])),
|
||||
makeNumericObjColumn('макс', [dataIndex, 'max'], filters, filterDelegate, renderDelegate, width, makeNumericColumnOptionsWithColor(fixed, [dataIndex, 'max'], [dataIndex, 'isMax']))
|
||||
])
|
||||
|
||||
export const getColumns = async (idWell) => {
|
||||
let sectionTypes = await WellOperationService.getSectionTypes(idWell)
|
||||
|
@ -74,7 +74,7 @@ html {
|
||||
.header .title{
|
||||
flex-grow: 1;
|
||||
color: #fff;
|
||||
padding-left: 450px;
|
||||
padding-left: calc(100vw / 2 - 400px);
|
||||
}
|
||||
|
||||
.header button{
|
||||
|
@ -1,7 +1,11 @@
|
||||
.detected-operations-page {
|
||||
& .page-top {
|
||||
width: 100%;
|
||||
margin: 10px;
|
||||
margin: 5px;
|
||||
|
||||
& > * {
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -142,3 +142,15 @@ code {
|
||||
height: 32px;
|
||||
padding: 4px 15px;
|
||||
}
|
||||
|
||||
.ant-table-cell:has(.color-pale-green) {
|
||||
background-color: #98fb98;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td.ant-table-cell-row-hover:has( > div.color-pale-green) {
|
||||
background: #98fb98;
|
||||
}
|
||||
|
||||
.color-pale-green {
|
||||
background-color: #98fb98;
|
||||
}
|
17
src/styles/monitoring.less
Normal file
17
src/styles/monitoring.less
Normal file
@ -0,0 +1,17 @@
|
||||
.page-top {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: -5px;
|
||||
|
||||
& > * {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
& > .icons {
|
||||
display: flex;
|
||||
|
||||
& > * {
|
||||
margin-left: 15px;
|
||||
}
|
||||
}
|
||||
}
|
54
src/styles/well_case.less
Normal file
54
src/styles/well_case.less
Normal file
@ -0,0 +1,54 @@
|
||||
.well-case-page {
|
||||
padding-top: 10px;
|
||||
|
||||
& .file-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
& .history-timeline {
|
||||
margin-left: 50px;
|
||||
margin-top: 20px;
|
||||
|
||||
& .ant-timeline-item-last {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& .customer-block {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
& .well-case-editor {
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
max-height: 70vh;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
|
||||
& .category-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
width: 300px;
|
||||
|
||||
& .category-list-item {
|
||||
cursor: pointer;
|
||||
|
||||
&.empty {
|
||||
background-color: #ffe58f;
|
||||
border-color: #ffc53d;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #fafafa;
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import { BarChartOutlined, LineChartOutlined, DotChartOutlined, AreaChartOutlined, BorderOuterOutlined, } from '@ant-design/icons'
|
||||
import { AntdIconProps } from '@ant-design/icons/lib/components/AntdIcon'
|
||||
|
||||
import { ChartDataset } from '@components/d3'
|
||||
import { BaseDataType, ChartDataset } from '@components/d3'
|
||||
|
||||
export const makePointsOptimizator = <DataType extends Record<string, unknown>>(isEquals: (a: DataType, b: DataType) => boolean) => (points: DataType[]) => {
|
||||
export const makePointsOptimizator = <DataType extends BaseDataType>(isEquals: (a: DataType, b: DataType) => boolean) => (points: DataType[]) => {
|
||||
if (!Array.isArray(points) || points.length < 3) return points
|
||||
|
||||
const out: DataType[] = []
|
||||
@ -23,7 +23,7 @@ export const getDistance = (x1: number, y1: number, x2: number, y2: number, type
|
||||
return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2))
|
||||
}
|
||||
|
||||
export const getChartIcon = <DataType,>(chart: ChartDataset<DataType>, options?: Omit<AntdIconProps, 'ref'>) => {
|
||||
export const getChartIcon = <DataType extends BaseDataType>(chart: ChartDataset<DataType>, options?: Omit<AntdIconProps, 'ref'>) => {
|
||||
let Icon
|
||||
switch (chart.type) {
|
||||
case 'needle': Icon = BarChartOutlined; break
|
||||
|
@ -11,3 +11,9 @@ export const mainFrameSize = () => ({
|
||||
})
|
||||
|
||||
export const isDev = () => process.env.NODE_ENV === 'development'
|
||||
|
||||
/**
|
||||
* Используется в асинхронных функциях для создания задержки
|
||||
* @param ms время задержки в мс
|
||||
*/
|
||||
export const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
|
||||
|
Loading…
Reference in New Issue
Block a user