forked from ddrilling/asb_cloud_front
Merge branch 'dev' into feature/add-page-operation-time
This commit is contained in:
commit
321900da89
@ -39,9 +39,9 @@ npx openapi -i http://{IP_ADDRESS}:{PORT}/swagger/v1/swagger.json -o src/service
|
|||||||
| IP-адрес | Описание |
|
| IP-адрес | Описание |
|
||||||
|:-|:-|
|
|:-|:-|
|
||||||
| 127.0.0.1:5000 | Локальный адрес вашей машины (привязан к `update_openapi`) |
|
| 127.0.0.1:5000 | Локальный адрес вашей машины (привязан к `update_openapi`) |
|
||||||
| 192.168.1.70:5000 | Локальный адрес development-сервера (привязан к `update_openapi_server`) |
|
| 192.168.1.113:5000 | Локальный адрес development-сервера (привязан к `update_openapi_server`) |
|
||||||
| 46.146.209.148:89 | Внешний адрес development-сервера |
|
| 46.146.209.148:89 | Внешний адрес development-сервера |
|
||||||
| 46.146.209.148 | Внешний адрес production-сервера |
|
| cloud.digitaldrilling.ru | Внешний адрес production-сервера |
|
||||||
|
|
||||||
## 3. Компиляция production-версии приложения
|
## 3. Компиляция production-версии приложения
|
||||||
После выполнения вышеописанных пунктов приложение готово к компиляции.
|
После выполнения вышеописанных пунктов приложение готово к компиляции.
|
||||||
|
12563
package-lock.json
generated
12563
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",
|
"start": "webpack-dev-server --mode=development --open --hot",
|
||||||
"build": "webpack --mode=production",
|
"build": "webpack --mode=production",
|
||||||
"test": "jest",
|
"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",
|
"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",
|
"oud": "npx openapi -i http://192.168.1.113: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",
|
"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"
|
"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",
|
"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: light)" content="white" />
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" />
|
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" />
|
||||||
<meta name="description" content="Онлайн мониторинг процесса бурения в реальном времени в офисе заказчика" />
|
<meta name="description" content="Онлайн мониторинг процесса бурения в реальном времени в офисе заказчика" />
|
||||||
<title>АСБ Vision</title>
|
<title>DDrilling</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<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 { Layout, LayoutProps } from 'antd'
|
||||||
|
|
||||||
import PageHeader from '@components/PageHeader'
|
import PageHeader from '@components/PageHeader'
|
||||||
import WellTreeSelector from '@components/selectors/WellTreeSelector'
|
import { WellTreeSelector, WellTreeSelectorProps } from '@components/selectors/WellTreeSelector'
|
||||||
import { wrapPrivateComponent } from '@utils'
|
import { wrapPrivateComponent } from '@utils'
|
||||||
|
|
||||||
export type LayoutPortalProps = LayoutProps & {
|
export type LayoutPortalProps = LayoutProps & {
|
||||||
title?: ReactNode
|
title?: ReactNode
|
||||||
noSheet?: boolean
|
noSheet?: boolean
|
||||||
showSelector?: boolean
|
selector?: WellTreeSelectorProps
|
||||||
}
|
}
|
||||||
|
|
||||||
const _LayoutPortal = memo<LayoutPortalProps>(({ title, noSheet, showSelector, ...props }) => (
|
const _LayoutPortal = memo<LayoutPortalProps>(({ title, noSheet, selector, ...props }) => (
|
||||||
<Layout.Content>
|
<Layout.Content>
|
||||||
<PageHeader title={title}>
|
<PageHeader title={title}>
|
||||||
<WellTreeSelector show={showSelector} />
|
<WellTreeSelector {...selector} />
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Layout>
|
<Layout>
|
||||||
{noSheet ? props.children : (
|
{noSheet ? props.children : (
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { Layout } from 'antd'
|
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 { BasicProps } from 'antd/lib/layout/layout'
|
||||||
|
|
||||||
import { headerHeight } from '@utils'
|
import { headerHeight } from '@utils'
|
||||||
@ -14,10 +14,7 @@ export type PageHeaderProps = BasicProps & {
|
|||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Мониторинг', isAdmin, children, ...other }) => {
|
export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Мониторинг', isAdmin, children, ...other }) => (
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<Layout.Header className={'header'} {...other}>
|
<Layout.Header className={'header'} {...other}>
|
||||||
<Link to={'/'} style={{ height: headerHeight }}>
|
<Link to={'/'} style={{ height: headerHeight }}>
|
||||||
@ -28,7 +25,6 @@ export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Монит
|
|||||||
<UserMenu isAdmin={isAdmin} />
|
<UserMenu isAdmin={isAdmin} />
|
||||||
</Layout.Header>
|
</Layout.Header>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
))
|
||||||
})
|
|
||||||
|
|
||||||
export default PageHeader
|
export default PageHeader
|
||||||
|
@ -12,7 +12,6 @@ export {
|
|||||||
makeNumericColumnPlanFact,
|
makeNumericColumnPlanFact,
|
||||||
makeNumericStartEnd,
|
makeNumericStartEnd,
|
||||||
makeNumericMinMax,
|
makeNumericMinMax,
|
||||||
makeNumericAvgRange
|
|
||||||
} from './numeric'
|
} from './numeric'
|
||||||
export { makeColumnsPlanFact } from './plan_fact'
|
export { makeColumnsPlanFact } from './plan_fact'
|
||||||
export { makeSelectColumn } from './select'
|
export { makeSelectColumn } from './select'
|
||||||
|
@ -94,18 +94,4 @@ export const makeNumericMinMax = (
|
|||||||
makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max')),
|
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
|
export default makeNumericColumn
|
||||||
|
@ -20,7 +20,6 @@ export {
|
|||||||
makeNumericColumnPlanFact,
|
makeNumericColumnPlanFact,
|
||||||
makeNumericStartEnd,
|
makeNumericStartEnd,
|
||||||
makeNumericMinMax,
|
makeNumericMinMax,
|
||||||
makeNumericAvgRange,
|
|
||||||
makeSelectColumn,
|
makeSelectColumn,
|
||||||
makeTagColumn,
|
makeTagColumn,
|
||||||
makeTagInput,
|
makeTagInput,
|
||||||
|
@ -7,6 +7,9 @@ import { DataType } from './Columns'
|
|||||||
export const makeNumericSorter = <T extends unknown>(key: keyof DataType<T>) =>
|
export const makeNumericSorter = <T extends unknown>(key: keyof DataType<T>) =>
|
||||||
(a: DataType<T>, b: DataType<T>) => Number(a[key]) - Number(b[key])
|
(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) => {
|
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 && !b) return 0
|
||||||
if (!a) return 1
|
if (!a) return 1
|
||||||
|
@ -10,6 +10,7 @@ import { notify, upload } from './factory'
|
|||||||
import { ErrorFetch } from './ErrorFetch'
|
import { ErrorFetch } from './ErrorFetch'
|
||||||
|
|
||||||
export type UploadFormProps = {
|
export type UploadFormProps = {
|
||||||
|
multiple?: boolean
|
||||||
url: string
|
url: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
accept?: string
|
accept?: string
|
||||||
@ -22,7 +23,7 @@ export type UploadFormProps = {
|
|||||||
onUploadError?: (error: unknown) => void
|
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 [fileList, setfileList] = useState<UploadFile<any>[]>([])
|
||||||
|
|
||||||
const checkMimeTypes = useCallback((file: RcFile) => {
|
const checkMimeTypes = useCallback((file: RcFile) => {
|
||||||
@ -38,7 +39,7 @@ export const UploadForm = memo<UploadFormProps>(({ url, disabled, style, formDat
|
|||||||
onUploadStart?.()
|
onUploadStart?.()
|
||||||
try {
|
try {
|
||||||
const formDataLocal = new FormData()
|
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)
|
if(formData)
|
||||||
for(const propName in formData)
|
for(const propName in formData)
|
||||||
@ -60,7 +61,7 @@ export const UploadForm = memo<UploadFormProps>(({ url, disabled, style, formDat
|
|||||||
setfileList([])
|
setfileList([])
|
||||||
onUploadComplete?.()
|
onUploadComplete?.()
|
||||||
}
|
}
|
||||||
}, [fileList, formData, onUploadComplete, onUploadError, onUploadStart, onUploadSuccess, url])
|
}, [fileList, formData, onUploadComplete, onUploadError, onUploadStart, onUploadSuccess, url, multiple])
|
||||||
|
|
||||||
const isSendButtonEnabled = fileList.length > 0
|
const isSendButtonEnabled = fileList.length > 0
|
||||||
return(
|
return(
|
||||||
@ -72,6 +73,7 @@ export const UploadForm = memo<UploadFormProps>(({ url, disabled, style, formDat
|
|||||||
fileList={fileList}
|
fileList={fileList}
|
||||||
onChange={(props) => setfileList(props.fileList)}
|
onChange={(props) => setfileList(props.fileList)}
|
||||||
beforeUpload={checkMimeTypes}
|
beforeUpload={checkMimeTypes}
|
||||||
|
maxCount={multiple ? undefined : 1}
|
||||||
>
|
>
|
||||||
<Button disabled={disabled} icon={<UploadOutlined/>}>Загрузить файл</Button>
|
<Button disabled={disabled} icon={<UploadOutlined/>}>Загрузить файл</Button>
|
||||||
</Upload>
|
</Upload>
|
||||||
|
@ -27,6 +27,7 @@ import {
|
|||||||
D3TooltipSettings,
|
D3TooltipSettings,
|
||||||
} from './plugins'
|
} from './plugins'
|
||||||
import type {
|
import type {
|
||||||
|
BaseDataType,
|
||||||
ChartAxis,
|
ChartAxis,
|
||||||
ChartDataset,
|
ChartDataset,
|
||||||
ChartDomain,
|
ChartDomain,
|
||||||
@ -50,13 +51,13 @@ export const getByAccessor = <DataType extends Record<any, any>, R>(accessor: ke
|
|||||||
return (d) => d[accessor]
|
return (d) => d[accessor]
|
||||||
}
|
}
|
||||||
|
|
||||||
const createAxis = <DataType,>(config: ChartAxis<DataType>) => {
|
const createAxis = <DataType extends BaseDataType>(config: ChartAxis<DataType>) => {
|
||||||
if (config.type === 'time')
|
if (config.type === 'time')
|
||||||
return d3.scaleTime()
|
return d3.scaleTime()
|
||||||
return d3.scaleLinear()
|
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>
|
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',
|
type: 'time',
|
||||||
accessor: (d: any) => new Date(d.date)
|
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 { D3TooltipPosition } from '@components/d3/plugins/D3Tooltip'
|
||||||
import { getChartIcon, isDev, usePartialProps } from '@utils'
|
import { getChartIcon, isDev, usePartialProps } from '@utils'
|
||||||
|
|
||||||
|
import { BaseDataType } from '../types'
|
||||||
import { ChartGroup, ChartSizes } from './D3MonitoringCharts'
|
import { ChartGroup, ChartSizes } from './D3MonitoringCharts'
|
||||||
|
|
||||||
import '@styles/d3.less'
|
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
|
width?: number
|
||||||
height?: number
|
height?: number
|
||||||
render?: D3GroupRenderFunction<DataType>
|
render?: D3GroupRenderFunction<DataType>
|
||||||
@ -23,7 +24,7 @@ export type D3HorizontalCursorSettings<DataType> = {
|
|||||||
lineStyle?: SVGProps<SVGLineElement>
|
lineStyle?: SVGProps<SVGLineElement>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type D3HorizontalCursorProps<DataType> = D3HorizontalCursorSettings<DataType> & {
|
export type D3HorizontalCursorProps<DataType extends BaseDataType> = D3HorizontalCursorSettings<DataType> & {
|
||||||
groups: ChartGroup<DataType>[]
|
groups: ChartGroup<DataType>[]
|
||||||
data: DataType[]
|
data: DataType[]
|
||||||
sizes: ChartSizes
|
sizes: ChartSizes
|
||||||
@ -37,7 +38,7 @@ const defaultLineStyle: SVGProps<SVGLineElement> = {
|
|||||||
|
|
||||||
const offsetY = 5
|
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) => {
|
{data.length > 0 ? group.charts.map((chart) => {
|
||||||
const xFormat = (d: number | Date) => chart.xAxis.format?.(d) ?? `${(+d).toFixed(2)} ${chart.xAxis.unit ?? ''}`
|
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,
|
spaceBetweenGroups = 30,
|
||||||
height = 200,
|
height = 200,
|
||||||
render = makeDefaultRender<DataType>(),
|
render = makeDefaultRender<DataType>(),
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Button, Checkbox, Form, FormItemProps, Input, InputNumber, Select, Tooltip } from 'antd'
|
import { Button, Checkbox, Form, FormItemProps, Input, InputNumber, Select, Tooltip } from 'antd'
|
||||||
import { memo, useCallback, useEffect, useMemo } from 'react'
|
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 { ColorPicker, Color } from '@components/ColorPicker'
|
||||||
|
|
||||||
import { ExtendedChartDataset } from './D3MonitoringCharts'
|
import { ExtendedChartDataset } from './D3MonitoringCharts'
|
||||||
@ -18,13 +18,13 @@ const lineTypes = [
|
|||||||
{ value: 'needle', label: 'Иглы' },
|
{ value: 'needle', label: 'Иглы' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export type D3MonitoringChartEditorProps<DataType> = {
|
export type D3MonitoringChartEditorProps<DataType extends BaseDataType> = {
|
||||||
group: ExtendedChartDataset<DataType>[]
|
group: ExtendedChartDataset<DataType>[]
|
||||||
chart: ExtendedChartDataset<DataType>
|
chart: ExtendedChartDataset<DataType>
|
||||||
onChange: (value: ExtendedChartDataset<DataType>) => boolean
|
onChange: (value: ExtendedChartDataset<DataType>) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const _D3MonitoringChartEditor = <DataType,>({
|
const _D3MonitoringChartEditor = <DataType extends BaseDataType>({
|
||||||
group,
|
group,
|
||||||
chart: value,
|
chart: value,
|
||||||
onChange,
|
onChange,
|
||||||
@ -93,8 +93,8 @@ const _D3MonitoringChartEditor = <DataType,>({
|
|||||||
</Item>
|
</Item>
|
||||||
<Item label={'Диапазон'}>
|
<Item label={'Диапазон'}>
|
||||||
<Input.Group compact>
|
<Input.Group compact>
|
||||||
<InputNumber disabled={!!value.linkedTo} value={value.xDomain?.min} onChange={(min) => onDomainChange({ min })} 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 })} placeholder={'Макс'} />
|
<InputNumber disabled={!!value.linkedTo} value={value.xDomain?.max} onChange={(max) => onDomainChange({ max: max ?? undefined })} placeholder={'Макс'} />
|
||||||
<Button
|
<Button
|
||||||
disabled={!!value.linkedTo || (!Number.isFinite(value.xDomain?.min) && !Number.isFinite(value.xDomain?.max))}
|
disabled={!!value.linkedTo || (!Number.isFinite(value.xDomain?.min) && !Number.isFinite(value.xDomain?.max))}
|
||||||
onClick={() => onDomainChange({ min: undefined, max: undefined })}
|
onClick={() => onDomainChange({ min: undefined, max: undefined })}
|
||||||
|
@ -8,6 +8,7 @@ import LoaderPortal from '@components/LoaderPortal'
|
|||||||
import { isDev, usePartialProps, useUserSettings } from '@utils'
|
import { isDev, usePartialProps, useUserSettings } from '@utils'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
BaseDataType,
|
||||||
ChartAxis,
|
ChartAxis,
|
||||||
ChartDataset,
|
ChartDataset,
|
||||||
ChartOffset,
|
ChartOffset,
|
||||||
@ -51,7 +52,7 @@ const calculateDomain = (mm: MinMax): Required<MinMax> => {
|
|||||||
return { min, max }
|
return { min, max }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExtendedChartDataset<DataType> = ChartDataset<DataType> & {
|
export type ExtendedChartDataset<DataType extends BaseDataType> = ChartDataset<DataType> & {
|
||||||
/** Диапазон отображаемых значений по горизонтальной оси */
|
/** Диапазон отображаемых значений по горизонтальной оси */
|
||||||
xDomain: MinMax
|
xDomain: MinMax
|
||||||
/** Скрыть отображение шкалы графика */
|
/** Скрыть отображение шкалы графика */
|
||||||
@ -60,9 +61,9 @@ export type ExtendedChartDataset<DataType> = ChartDataset<DataType> & {
|
|||||||
showCurrentValue?: boolean
|
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 выборку, содержащую корневой G-элемент группы */
|
||||||
(): d3.Selection<SVGGElement, any, any, any>
|
(): d3.Selection<SVGGElement, any, any, any>
|
||||||
/** Уникальный ключ группы (индекс) */
|
/** Уникальный ключ группы (индекс) */
|
||||||
@ -86,12 +87,12 @@ const defaultRegulators: TelemetryRegulators = {
|
|||||||
5: { color: '#007070', label: 'Расход' },
|
5: { color: '#007070', label: 'Расход' },
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDefaultYAxisConfig = <DataType,>(): ChartAxis<DataType> => ({
|
const getDefaultYAxisConfig = <DataType extends BaseDataType>(): ChartAxis<DataType> => ({
|
||||||
type: 'time',
|
type: 'time',
|
||||||
accessor: (d: any) => new Date(d.date)
|
accessor: (d: any) => new Date(d.date)
|
||||||
})
|
})
|
||||||
|
|
||||||
const getDefaultYTicks = <DataType,>(): Required<ChartTick<DataType>> => ({
|
const getDefaultYTicks = <DataType extends BaseDataType>(): Required<ChartTick<DataType>> => ({
|
||||||
visible: false,
|
visible: false,
|
||||||
format: (d: d3.NumberValue, idx: number, data?: DataType) => String(d),
|
format: (d: d3.NumberValue, idx: number, data?: DataType) => String(d),
|
||||||
color: 'lightgray',
|
color: 'lightgray',
|
||||||
@ -101,7 +102,7 @@ const getDefaultYTicks = <DataType,>(): Required<ChartTick<DataType>> => ({
|
|||||||
/**
|
/**
|
||||||
* @template 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>[][]
|
datasetGroups: ExtendedChartDataset<DataType>[][]
|
||||||
/** Ширина графика числом пикселей или CSS-значением (px/%/em/rem) */
|
/** Ширина графика числом пикселей или CSS-значением (px/%/em/rem) */
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
|
|
||||||
|
import { BaseDataType } from '@components/d3/types'
|
||||||
import { ChartGroup, ChartSizes } from '@components/d3/monitoring/D3MonitoringCharts'
|
import { ChartGroup, ChartSizes } from '@components/d3/monitoring/D3MonitoringCharts'
|
||||||
import { makeDisplayValue } from '@utils'
|
import { makeDisplayValue } from '@utils'
|
||||||
|
|
||||||
export type D3MonitoringCurrentValuesProps<DataType> = {
|
export type D3MonitoringCurrentValuesProps<DataType extends BaseDataType> = {
|
||||||
groups: ChartGroup<DataType>[]
|
groups: ChartGroup<DataType>[]
|
||||||
data: DataType[]
|
data: DataType[]
|
||||||
left: number
|
left: number
|
||||||
@ -12,7 +13,7 @@ export type D3MonitoringCurrentValuesProps<DataType> = {
|
|||||||
|
|
||||||
const display = makeDisplayValue({ def: '---', fixed: 2 })
|
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'}>
|
<g transform={`translate(${left}, ${sizes.chartsTop})`} pointerEvents={'none'}>
|
||||||
{groups.map((group) => (
|
{groups.map((group) => (
|
||||||
<g key={group.key} transform={`translate(${sizes.groupLeft(group.key)}, 0)`}>
|
<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 { 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 { UndoOutlined } from '@ant-design/icons'
|
||||||
import { EventDataNode } from 'antd/lib/tree'
|
import { EventDataNode } from 'antd/lib/tree'
|
||||||
|
|
||||||
import { notify } from '@components/factory'
|
import { notify } from '@components/factory'
|
||||||
import { getChartIcon } from '@utils'
|
import { getChartIcon } from '@utils'
|
||||||
|
|
||||||
|
import { BaseDataType } from '../types'
|
||||||
import { ExtendedChartDataset } from './D3MonitoringCharts'
|
import { ExtendedChartDataset } from './D3MonitoringCharts'
|
||||||
import { TelemetryRegulators } from './D3MonitoringLimitChart'
|
import { TelemetryRegulators } from './D3MonitoringLimitChart'
|
||||||
import D3MonitoringChartEditor from './D3MonitoringChartEditor'
|
import D3MonitoringChartEditor from './D3MonitoringChartEditor'
|
||||||
import D3MonitoringLimitEditor from './D3MonitoringLimitEditor'
|
import D3MonitoringLimitEditor from './D3MonitoringLimitEditor'
|
||||||
|
|
||||||
export type D3MonitoringGroupsEditorProps<DataType> = {
|
export type D3MonitoringGroupsEditorProps<DataType extends BaseDataType> = {
|
||||||
visible?: boolean
|
visible?: boolean
|
||||||
groups: ExtendedChartDataset<DataType>[][]
|
groups: ExtendedChartDataset<DataType>[][]
|
||||||
regulators: TelemetryRegulators
|
regulators: TelemetryRegulators
|
||||||
@ -20,7 +21,7 @@ export type D3MonitoringGroupsEditorProps<DataType> = {
|
|||||||
onReset: () => void
|
onReset: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const getChartLabel = <DataType,>(chart: ExtendedChartDataset<DataType>) => (
|
const getChartLabel = <DataType extends BaseDataType>(chart: ExtendedChartDataset<DataType>) => (
|
||||||
<Tooltip title={chart.label}>
|
<Tooltip title={chart.label}>
|
||||||
{getChartIcon(chart)} {chart.label}
|
{getChartIcon(chart)} {chart.label}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -34,14 +35,14 @@ const divStyle: CSSProperties = {
|
|||||||
flexGrow: 1,
|
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)
|
const out = node.pos.split('-').map(Number)
|
||||||
return { group: out[1], chart: out[2] }
|
return { group: out[1], chart: out[2] }
|
||||||
}
|
}
|
||||||
|
|
||||||
type EditingMode = null | 'limit' | 'chart'
|
type EditingMode = null | 'limit' | 'chart'
|
||||||
|
|
||||||
const _D3MonitoringEditor = <DataType,>({
|
const _D3MonitoringEditor = <DataType extends BaseDataType>({
|
||||||
visible,
|
visible,
|
||||||
groups: oldGroups,
|
groups: oldGroups,
|
||||||
regulators: oldRegulators,
|
regulators: oldRegulators,
|
||||||
@ -61,8 +62,8 @@ const _D3MonitoringEditor = <DataType,>({
|
|||||||
const onModalOk = useCallback(() => onChange(groups, regulators), [groups, regulators])
|
const onModalOk = useCallback(() => onChange(groups, regulators), [groups, regulators])
|
||||||
|
|
||||||
const onDrop = useCallback((info: {
|
const onDrop = useCallback((info: {
|
||||||
node: EventDataNode
|
node: EventDataNode<TreeDataNode>
|
||||||
dragNode: EventDataNode
|
dragNode: EventDataNode<TreeDataNode>
|
||||||
dropPosition: number
|
dropPosition: number
|
||||||
}) => {
|
}) => {
|
||||||
const { dragNode, dropPosition, node } = info
|
const { dragNode, dropPosition, node } = info
|
||||||
@ -152,12 +153,12 @@ const _D3MonitoringEditor = <DataType,>({
|
|||||||
<Tree
|
<Tree
|
||||||
draggable
|
draggable
|
||||||
selectable
|
selectable
|
||||||
onExpand={(keys) => setExpand(keys)}
|
onExpand={(keys: Key[]) => setExpand(keys)}
|
||||||
expandedKeys={expand}
|
expandedKeys={expand}
|
||||||
selectedKeys={selected}
|
selectedKeys={selected}
|
||||||
treeData={treeItems}
|
treeData={treeItems}
|
||||||
onDrop={onDrop}
|
onDrop={onDrop}
|
||||||
onSelect={(value) => {
|
onSelect={(value: Key[]) => {
|
||||||
setSelected(value)
|
setSelected(value)
|
||||||
setMode('chart')
|
setMode('chart')
|
||||||
}}
|
}}
|
||||||
|
@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef } from 'react'
|
|||||||
import { Property } from 'csstype'
|
import { Property } from 'csstype'
|
||||||
import * as d3 from 'd3'
|
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 { useD3MouseZone } from '@components/d3/D3MouseZone'
|
||||||
import { usePartialProps } from '@utils'
|
import { usePartialProps } from '@utils'
|
||||||
|
|
||||||
@ -32,12 +32,12 @@ export type D3LegendSettings = {
|
|||||||
|
|
||||||
const defaultOffset = { x: 10, y: 10 }
|
const defaultOffset = { x: 10, y: 10 }
|
||||||
|
|
||||||
export type D3LegendProps<DataType> = D3LegendSettings & {
|
export type D3LegendProps<DataType extends BaseDataType> = D3LegendSettings & {
|
||||||
/** Массив графиков */
|
/** Массив графиков */
|
||||||
charts: ChartRegistry<DataType>[]
|
charts: ChartRegistry<DataType>[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _D3Legend = <DataType,>({
|
const _D3Legend = <DataType extends BaseDataType>({
|
||||||
charts,
|
charts,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
@ -4,7 +4,7 @@ import * as d3 from 'd3'
|
|||||||
|
|
||||||
import { isDev } from '@utils'
|
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 { D3MouseState, useD3MouseZone } from '@components/d3/D3MouseZone'
|
||||||
import { getTouchedElements, wrapPlugin } from './base'
|
import { getTouchedElements, wrapPlugin } from './base'
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ import '@styles/d3.less'
|
|||||||
|
|
||||||
export type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none'
|
export type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none'
|
||||||
|
|
||||||
export type D3RenderData<DataType> = {
|
export type D3RenderData<DataType extends BaseDataType> = {
|
||||||
/** Параметры графика */
|
/** Параметры графика */
|
||||||
chart: ChartRegistry<DataType>
|
chart: ChartRegistry<DataType>
|
||||||
/** Данные графика */
|
/** Данные графика */
|
||||||
@ -21,9 +21,9 @@ export type D3RenderData<DataType> = {
|
|||||||
selection?: d3.Selection<any, DataType, any, any>
|
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>
|
render?: D3RenderFunction<DataType>
|
||||||
/** Ширина тултипа */
|
/** Ширина тултипа */
|
||||||
@ -39,7 +39,7 @@ export type D3TooltipSettings<DataType> = {
|
|||||||
limit?: number
|
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 }) => {
|
{data.length > 0 ? data.map(({ chart, data }) => {
|
||||||
let Icon
|
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>[],
|
charts: ChartRegistry<DataType>[],
|
||||||
}
|
}
|
||||||
|
|
||||||
function _D3Tooltip<DataType extends Record<string, unknown>>({
|
function _D3Tooltip<DataType extends BaseDataType>({
|
||||||
width = 200,
|
width = 200,
|
||||||
height = 120,
|
height = 120,
|
||||||
render = makeDefaultRender<DataType>(),
|
render = makeDefaultRender<DataType>(),
|
||||||
|
@ -3,7 +3,7 @@ import * as d3 from 'd3'
|
|||||||
|
|
||||||
import { getDistance, TouchType } from '@utils'
|
import { getDistance, TouchType } from '@utils'
|
||||||
|
|
||||||
import { ChartRegistry } from '../types'
|
import { BaseDataType, ChartRegistry } from '../types'
|
||||||
|
|
||||||
export type BasePluginSettings = {
|
export type BasePluginSettings = {
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
@ -16,7 +16,7 @@ export const wrapPlugin = <TProps,>(
|
|||||||
const wrappedComponent = ({ enabled, ...props }: TProps & BasePluginSettings) => {
|
const wrappedComponent = ({ enabled, ...props }: TProps & BasePluginSettings) => {
|
||||||
if (!(enabled ?? defaultEnabled)) return <></>
|
if (!(enabled ?? defaultEnabled)) return <></>
|
||||||
|
|
||||||
return <Component {...(props as TProps)} />
|
return <Component {...(props as (TProps & JSX.IntrinsicAttributes))} /> // IntrinsicAttributes добавлено как необходимое ограничение
|
||||||
}
|
}
|
||||||
|
|
||||||
return wrappedComponent
|
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>,
|
chart: ChartRegistry<DataType>,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import * as d3 from 'd3'
|
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>,
|
elms: d3.Selection<BaseType, Datum, PElement, PDatum>,
|
||||||
chart: ChartRegistry<DataType>
|
chart: ChartRegistry<DataType>
|
||||||
): d3.Selection<BaseType, Datum, PElement, PDatum> => {
|
): 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'
|
import { appendTransition } from './base'
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ const defaultConfig: Required<Omit<PointChartDataset, 'type'>> = {
|
|||||||
fillOpacity: 1,
|
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()
|
const root = chart()
|
||||||
if (!embeded) return root
|
if (!embeded) return root
|
||||||
if (root.select('.points').empty())
|
if (root.select('.points').empty())
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { getByAccessor } from '@components/d3/functions'
|
import { getByAccessor } from '@components/d3/functions'
|
||||||
import { ChartRegistry } from '@components/d3/types'
|
import { BaseDataType, ChartRegistry } from '@components/d3/types'
|
||||||
|
|
||||||
import { appendTransition } from './base'
|
import { appendTransition } from './base'
|
||||||
|
|
||||||
export const renderRectArea = <DataType extends Record<string, any>>(
|
export const renderRectArea = <DataType extends BaseDataType>(
|
||||||
xAxis: (value: d3.NumberValue) => number,
|
xAxis: (value: d3.NumberValue) => number,
|
||||||
yAxis: (value: d3.NumberValue) => number,
|
yAxis: (value: d3.NumberValue) => number,
|
||||||
chart: ChartRegistry<DataType>
|
chart: ChartRegistry<DataType>
|
||||||
|
@ -3,9 +3,11 @@ import { Property } from 'csstype'
|
|||||||
|
|
||||||
import { D3TooltipSettings } from './plugins'
|
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',
|
type: 'linear' | 'time',
|
||||||
/** Ключ записи или метод по которому будет извлекаться значение оси из массива данных */
|
/** Ключ записи или метод по которому будет извлекаться значение оси из массива данных */
|
||||||
@ -34,7 +36,7 @@ export type PointChartDataset = {
|
|||||||
fillOpacity?: number
|
fillOpacity?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BaseChartDataset<DataType> = {
|
export type BaseChartDataset<DataType extends BaseDataType> = {
|
||||||
/** Уникальный ключ графика */
|
/** Уникальный ключ графика */
|
||||||
key: string | number
|
key: string | number
|
||||||
/** Параметры вертикальной оси */
|
/** Параметры вертикальной оси */
|
||||||
@ -101,7 +103,7 @@ export type NeedleChartDataset = {
|
|||||||
type: 'needle'
|
type: 'needle'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChartDataset<DataType> = BaseChartDataset<DataType> & (
|
export type ChartDataset<DataType extends BaseDataType> = BaseChartDataset<DataType> & (
|
||||||
AreaChartDataset |
|
AreaChartDataset |
|
||||||
LineChartDataset |
|
LineChartDataset |
|
||||||
NeedleChartDataset |
|
NeedleChartDataset |
|
||||||
@ -154,7 +156,7 @@ export type ChartTicks<DataType> = {
|
|||||||
y?: ChartTick<DataType>
|
y?: ChartTick<DataType>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChartRegistry<DataType> = ChartDataset<DataType> & {
|
export type ChartRegistry<DataType extends BaseDataType> = ChartDataset<DataType> & {
|
||||||
/** Получить D3 выборку, содержащую корневой G-элемент графика */
|
/** Получить D3 выборку, содержащую корневой G-элемент графика */
|
||||||
(): d3.Selection<SVGGElement, DataType, any, any>
|
(): d3.Selection<SVGGElement, DataType, any, any>
|
||||||
/** Получить значение по вертикальной оси из предоставленой записи */
|
/** Получить значение по вертикальной оси из предоставленой записи */
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Button, Drawer, Skeleton, Tree, TreeProps, Typography } from 'antd'
|
import { Button, Drawer, Skeleton, Tree, TreeDataNode, TreeProps, Typography } from 'antd'
|
||||||
import { DefaultValueType } from 'rc-tree-select/lib/interface'
|
import { useState, useEffect, useCallback, memo, Key } from 'react'
|
||||||
import { useState, useEffect, ReactNode, useCallback, memo, Key } from 'react'
|
|
||||||
import { useNavigate, useLocation } from 'react-router-dom'
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import { WellIcon, WellIconState } from '@components/icons'
|
import { WellIcon, WellIconState } from '@components/icons'
|
||||||
@ -17,26 +16,18 @@ export const getWellState = (idState?: number): WellIconState => idState === 1 ?
|
|||||||
export const checkIsWellOnline = (lastTelemetryDate: unknown): boolean =>
|
export const checkIsWellOnline = (lastTelemetryDate: unknown): boolean =>
|
||||||
isRawDate(lastTelemetryDate) && (Date.now() - +new Date(lastTelemetryDate) < 600_000)
|
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 getKeyByUrl = (url?: string): [Key | null, string | null] => {
|
||||||
const result = url?.match(/^\/([^\/]+)\/([^\/?]+)/) // pattern "/:type/:id"
|
const result = url?.match(/^\/([^\/]+)\/([^\/?]+)/) // pattern "/:type/:id"
|
||||||
if (!result) return [null, null]
|
if (!result) return [null, null]
|
||||||
return [result[0], result[1]]
|
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)
|
const [url, type] = getKeyByUrl(value)
|
||||||
if (!url) return
|
if (!url) return
|
||||||
let deposit: TreeNodeData | undefined
|
let deposit: TreeDataNode | undefined
|
||||||
let cluster: TreeNodeData | undefined
|
let cluster: TreeDataNode | undefined
|
||||||
let well: TreeNodeData | undefined
|
let well: TreeDataNode | undefined
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'deposit':
|
case 'deposit':
|
||||||
deposit = wellsTree.find((deposit) => deposit.key === url)
|
deposit = wellsTree.find((deposit) => deposit.key === url)
|
||||||
@ -46,7 +37,7 @@ const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined
|
|||||||
|
|
||||||
case 'cluster':
|
case 'cluster':
|
||||||
deposit = wellsTree.find((deposit) => (
|
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)
|
if (deposit && cluster)
|
||||||
return `${deposit.title} / ${cluster.title}`
|
return `${deposit.title} / ${cluster.title}`
|
||||||
@ -54,8 +45,8 @@ const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined
|
|||||||
|
|
||||||
case 'well':
|
case 'well':
|
||||||
deposit = wellsTree.find((deposit) => (
|
deposit = wellsTree.find((deposit) => (
|
||||||
cluster = deposit.children?.find((cluster: TreeNodeData) => (
|
cluster = deposit.children?.find((cluster: TreeDataNode) => (
|
||||||
well = cluster.children?.find((well: TreeNodeData) => well.key === url)
|
well = cluster.children?.find((well: TreeDataNode) => well.key === url)
|
||||||
))
|
))
|
||||||
))
|
))
|
||||||
if (deposit && cluster && well)
|
if (deposit && cluster && well)
|
||||||
@ -79,8 +70,26 @@ const sortWellsByActive = (a: WellDto, b: WellDto): number => {
|
|||||||
return (a.caption || '')?.localeCompare(b.caption || '')
|
return (a.caption || '')?.localeCompare(b.caption || '')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WellTreeSelector = memo(({ show, ...other }: TreeProps<TreeNodeData> & { show?: boolean }) => {
|
export type WellTreeSelectorProps = TreeProps<TreeDataNode> & {
|
||||||
const [wellsTree, setWellsTree] = useState<TreeNodeData[]>([])
|
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 [showLoader, setShowLoader] = useState<boolean>(false)
|
||||||
const [visible, setVisible] = useState<boolean>(false)
|
const [visible, setVisible] = useState<boolean>(false)
|
||||||
const [expanded, setExpanded] = useState<Key[]>([])
|
const [expanded, setExpanded] = useState<Key[]>([])
|
||||||
@ -91,26 +100,20 @@ export const WellTreeSelector = memo(({ show, ...other }: TreeProps<TreeNodeData
|
|||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setVisible((prev) => show ?? prev)
|
if (current) setSelected([current])
|
||||||
setExpanded((prev) => {
|
}, [current])
|
||||||
if (typeof show === 'undefined') return prev
|
|
||||||
if (!show) return []
|
useEffect(() => setVisible((prev) => show ?? prev), [show])
|
||||||
const out: Key[] = []
|
|
||||||
wellsTree.forEach((deposit) => {
|
useEffect(() => {
|
||||||
if (deposit.key) out.push(deposit.key)
|
setExpanded((prev) => expand ? getExpandKeys(wellsTree, expand) : prev)
|
||||||
deposit.children?.forEach((cluster) => {
|
}, [wellsTree, expand])
|
||||||
if (cluster.key) out.push(cluster.key)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return out
|
|
||||||
})
|
|
||||||
}, [wellsTree, show])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
invokeWebApiWrapperAsync(
|
invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
const deposits: Array<DepositDto> = await DepositService.getDeposits()
|
const deposits: Array<DepositDto> = await DepositService.getDeposits()
|
||||||
const wellsTree: TreeNodeData[] = deposits.map(deposit =>({
|
const wellsTree: TreeDataNode[] = deposits.map(deposit =>({
|
||||||
title: deposit.caption,
|
title: deposit.caption,
|
||||||
key: `/deposit/${deposit.id}`,
|
key: `/deposit/${deposit.id}`,
|
||||||
value: `/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 { Tooltip } from 'antd'
|
||||||
import { UserOutlined } from '@ant-design/icons'
|
import { UserOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
@ -6,33 +6,58 @@ import { UserDto } from '@api'
|
|||||||
import { Grid, GridItem } from '@components/Grid'
|
import { Grid, GridItem } from '@components/Grid'
|
||||||
import { CompanyView } from './CompanyView'
|
import { CompanyView } from './CompanyView'
|
||||||
|
|
||||||
export type UserViewProps = {
|
export type UserViewProps = HTMLProps<HTMLSpanElement> & {
|
||||||
user?: UserDto
|
user?: UserDto
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserView = memo<UserViewProps>(({ user }) => user ? (
|
export const UserView = memo<UserViewProps>(({ user, ...other }) =>
|
||||||
<Tooltip title={(
|
user ? (
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
<Grid style={{ columnGap: '8px' }}>
|
<Grid style={{ columnGap: '8px' }}>
|
||||||
<GridItem row={1} col={1}>Фамилия:</GridItem>
|
<GridItem row={1} col={1}>
|
||||||
<GridItem row={1} col={2}>{user?.surname}</GridItem>
|
Фамилия:
|
||||||
|
</GridItem>
|
||||||
|
<GridItem row={1} col={2}>
|
||||||
|
{user?.surname}
|
||||||
|
</GridItem>
|
||||||
|
|
||||||
<GridItem row={2} col={1}>Имя:</GridItem>
|
<GridItem row={2} col={1}>
|
||||||
<GridItem row={2} col={2}>{user?.name}</GridItem>
|
Имя:
|
||||||
|
</GridItem>
|
||||||
|
<GridItem row={2} col={2}>
|
||||||
|
{user?.name}
|
||||||
|
</GridItem>
|
||||||
|
|
||||||
<GridItem row={3} col={1}>Отчество:</GridItem>
|
<GridItem row={3} col={1}>
|
||||||
<GridItem row={3} col={2}>{user?.patronymic}</GridItem>
|
Отчество:
|
||||||
|
</GridItem>
|
||||||
|
<GridItem row={3} col={2}>
|
||||||
|
{user?.patronymic}
|
||||||
|
</GridItem>
|
||||||
|
|
||||||
<GridItem row={4} col={1}>Компания:</GridItem>
|
<GridItem row={4} col={1}>
|
||||||
|
Компания:
|
||||||
|
</GridItem>
|
||||||
<GridItem row={4} col={2}>
|
<GridItem row={4} col={2}>
|
||||||
<CompanyView company={user?.company}/>
|
<CompanyView company={user?.company} />
|
||||||
</GridItem>
|
</GridItem>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}>
|
}
|
||||||
<UserOutlined style={{ marginRight: 8 }}/>
|
>
|
||||||
|
<span {...other}>
|
||||||
|
<UserOutlined style={{ marginRight: 8 }} />
|
||||||
{user?.login}
|
{user?.login}
|
||||||
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip title='нет пользователя'>-</Tooltip>
|
<Tooltip title={'нет пользователя'}>
|
||||||
))
|
<span {...other}>
|
||||||
|
<UserOutlined style={{ marginRight: 8 }} />
|
||||||
|
---
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
export default UserView
|
export default UserView
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { memo } from 'react'
|
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) => (
|
export type LogoProps = React.SVGProps<SVGSVGElement> & { size?: number }
|
||||||
<img src={logo} alt={'АСБ'} className={'logo'} {...props} />
|
|
||||||
|
export const Logo = memo<LogoProps>(({ size = 200, ...props }) => (
|
||||||
|
<AsbLogo className={'logo'} height={'100%'} {...props} />
|
||||||
))
|
))
|
||||||
|
|
||||||
export default Logo
|
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 { 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 { Link, useLocation } from 'react-router-dom'
|
||||||
import { Popover, Badge } from 'antd'
|
import { Popover, Badge } from 'antd'
|
||||||
|
|
||||||
@ -49,6 +49,16 @@ const Deposit = memo(() => {
|
|||||||
|
|
||||||
const location = useLocation()
|
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(() => {
|
useEffect(() => {
|
||||||
invokeWebApiWrapperAsync(
|
invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
@ -63,7 +73,7 @@ const Deposit = memo(() => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutPortal noSheet showSelector title={'Месторождение'}>
|
<LayoutPortal noSheet selector={selectorProps} title={'Месторождение'}>
|
||||||
<LoaderPortal show={showLoader}>
|
<LoaderPortal show={showLoader}>
|
||||||
<div className={'h-100vh'}>
|
<div className={'h-100vh'}>
|
||||||
<Map {...viewParams}>
|
<Map {...viewParams}>
|
||||||
|
@ -137,6 +137,7 @@ export const DocumentsTemplate = ({ idCategory, well: givenWell, mimeTypes, head
|
|||||||
<div>
|
<div>
|
||||||
<span>Загрузка</span>
|
<span>Загрузка</span>
|
||||||
<UploadForm
|
<UploadForm
|
||||||
|
multiple
|
||||||
url={uploadUrl}
|
url={uploadUrl}
|
||||||
mimeTypes={mimeTypes}
|
mimeTypes={mimeTypes}
|
||||||
onUploadStart={() => setShowLoader(true)}
|
onUploadStart={() => setShowLoader(true)}
|
||||||
|
@ -123,6 +123,7 @@ export const CategoryRender = memo(({ partData, onUpdate, onEdit, onHistory, set
|
|||||||
<div className={'file_actions'}>
|
<div className={'file_actions'}>
|
||||||
{permissionToUpload && (
|
{permissionToUpload && (
|
||||||
<UploadForm
|
<UploadForm
|
||||||
|
multiple
|
||||||
url={uploadUrl}
|
url={uploadUrl}
|
||||||
mimeTypes={MimeTypes.XLSX}
|
mimeTypes={MimeTypes.XLSX}
|
||||||
style={{ margin: '5px 0 10px 0' }}
|
style={{ margin: '5px 0 10px 0' }}
|
||||||
|
@ -198,7 +198,7 @@ const table11Columns = [
|
|||||||
const table11Data = [
|
const table11Data = [
|
||||||
{
|
{
|
||||||
key: '1',
|
key: '1',
|
||||||
l_label: 'Время начала:',
|
l_label: 'Время начала',
|
||||||
r_label: 'Время начала',
|
r_label: 'Время начала',
|
||||||
l_value: 'climbBegin',
|
l_value: 'climbBegin',
|
||||||
r_value: 'climbFinish',
|
r_value: 'climbFinish',
|
||||||
@ -223,17 +223,17 @@ const table12Columns = [
|
|||||||
const table12Data = [
|
const table12Data = [
|
||||||
{
|
{
|
||||||
key: '1',
|
key: '1',
|
||||||
l_label: 'По стволу:',
|
l_label: 'По стволу',
|
||||||
r_label: 'По вертикали',
|
r_label: 'По вертикали',
|
||||||
l_value: 'climbBegin',
|
l_value: 'bottomholeDepth',
|
||||||
r_value: 'climbFinish',
|
r_value: 'verticalDepth',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: '2',
|
key: '2',
|
||||||
l_label: 'Зенитный',
|
l_label: 'Зенитный',
|
||||||
r_label: 'Азимутальный',
|
r_label: 'Азимутальный',
|
||||||
l_value: 'descentBegin',
|
l_value: 'zenithAngle',
|
||||||
r_value: 'descentFinish',
|
r_value: 'azimuthAngle',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import moment from 'moment'
|
|||||||
import { useWell } from '@asb/context'
|
import { useWell } from '@asb/context'
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
import { DateRangeWrapper, Table, makeDateColumn, makeColumn } from '@components/Table'
|
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 { arrayOrDefault, wrapPrivateComponent } from '@utils'
|
||||||
import { DailyReportService } from '@api'
|
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 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(() => [
|
const columns = useMemo(() => [
|
||||||
makeDateColumn('Дата', 'reportDate', undefined, 'DD.MM.YYYY', { width: 300 }),
|
makeDateColumn('Дата', 'reportDate', undefined, 'DD.MM.YYYY', { width: 300 }),
|
||||||
makeColumn('', '', { width: 200, render: (_, report) => (
|
makeColumn('', '', { width: 200, render: (_, report) => (
|
||||||
@ -44,7 +54,7 @@ const DailyReport = memo(() => {
|
|||||||
<Button
|
<Button
|
||||||
type={'link'}
|
type={'link'}
|
||||||
icon={<FileExcelOutlined />}
|
icon={<FileExcelOutlined />}
|
||||||
onClick={async () => await download(`/api/well/${well.id}/DailyReport/${report.reportDate}/excel`)}
|
onClick={makeOnDownloadClick(report)}
|
||||||
children={'Скачать XLSX'}
|
children={'Скачать XLSX'}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@ -58,7 +68,7 @@ const DailyReport = memo(() => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}),
|
)}),
|
||||||
], [well])
|
], [makeOnDownloadClick])
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = useMemo(() => {
|
||||||
if (!searchDate) return data
|
if (!searchDate) return data
|
||||||
|
@ -74,7 +74,6 @@ export const DrillerList = memo(({ loading, drillers, onChange }) => {
|
|||||||
<Button
|
<Button
|
||||||
onClick={onModalOpen}
|
onClick={onModalOpen}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
style={{ marginRight: '10px' }}
|
|
||||||
>Список бурильщиков</Button>
|
>Список бурильщиков</Button>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -121,7 +121,6 @@ export const DrillerSchedule = memo(({ drillers, loading, onChange }) => {
|
|||||||
<Button
|
<Button
|
||||||
onClick={onModalOpen}
|
onClick={onModalOpen}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
style={{ marginRight: '10px' }}
|
|
||||||
>Расписание бурильщиков</Button>
|
>Расписание бурильщиков</Button>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -117,7 +117,6 @@ const Operations = memo(() => {
|
|||||||
<Select
|
<Select
|
||||||
allowClear={false}
|
allowClear={false}
|
||||||
options={categories}
|
options={categories}
|
||||||
style={{ marginRight: '10px' }}
|
|
||||||
onChange={(v) => setSelectedCategory(v)}
|
onChange={(v) => setSelectedCategory(v)}
|
||||||
value={selectedCategory}
|
value={selectedCategory}
|
||||||
/>
|
/>
|
||||||
@ -127,7 +126,6 @@ const Operations = memo(() => {
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
disabledDate={disabledDates}
|
disabledDate={disabledDates}
|
||||||
disabledTime={disabledTimes}
|
disabledTime={disabledTimes}
|
||||||
style={{ marginRight: '10px' }}
|
|
||||||
showTime={{ hideDisabledOptions: true }}
|
showTime={{ hideDisabledOptions: true }}
|
||||||
/>
|
/>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
@ -137,7 +135,6 @@ const Operations = memo(() => {
|
|||||||
onChange={setYDomain}
|
onChange={setYDomain}
|
||||||
addonAfter={'мин'}
|
addonAfter={'мин'}
|
||||||
addonBefore={'Верхняя граница'}
|
addonBefore={'Верхняя граница'}
|
||||||
style={{ marginRight: '10px' }}
|
|
||||||
/>
|
/>
|
||||||
{permissions.driller.get && (
|
{permissions.driller.get && (
|
||||||
<>
|
<>
|
||||||
|
@ -6,7 +6,7 @@ import { useWell } from '@asb/context'
|
|||||||
import { makeDateSorter } from '@components/Table'
|
import { makeDateSorter } from '@components/Table'
|
||||||
import { D3MonitoringCharts } from '@components/d3/monitoring'
|
import { D3MonitoringCharts } from '@components/d3/monitoring'
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
import { Grid, GridItem, Flex } from '@components/Grid'
|
import { Grid, GridItem } from '@components/Grid'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
|
import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
|
||||||
import { formatDate, hasPermission, wrapPrivateComponent } from '@utils'
|
import { formatDate, hasPermission, wrapPrivateComponent } from '@utils'
|
||||||
@ -31,6 +31,7 @@ import MomentStabPicDisabled from '@images/DempherOff.png'
|
|||||||
import SpinPicEnabled from '@images/SpinEnabled.png'
|
import SpinPicEnabled from '@images/SpinEnabled.png'
|
||||||
import SpinPicDisabled from '@images/SpinDisabled.png'
|
import SpinPicDisabled from '@images/SpinDisabled.png'
|
||||||
|
|
||||||
|
import '@styles/monitoring.less'
|
||||||
import '@styles/message.css'
|
import '@styles/message.css'
|
||||||
|
|
||||||
const { Option } = Select
|
const { Option } = Select
|
||||||
@ -87,25 +88,25 @@ export const makeChartGroups = (flowChart) => {
|
|||||||
makeDataset('Давление', 'Давл','#FF0000', 'pressure', 'атм'),
|
makeDataset('Давление', 'Давл','#FF0000', 'pressure', 'атм'),
|
||||||
makeDataset('Давление заданное', 'Давл зад-е','#CC0000', 'pressureSp', 'атм'),
|
makeDataset('Давление заданное', 'Давл зад-е','#CC0000', 'pressureSp', 'атм'),
|
||||||
makeDataset('Давление ХХ', 'Давл ХХ','#CC4429', 'pressureIdle', 'атм', { dash }),
|
makeDataset('Давление ХХ', 'Давл ХХ','#CC4429', 'pressureIdle', 'атм', { dash }),
|
||||||
makeDataset('Перепад давления максимальный', 'ΔР макс','#B34A36', 'pressureDeltaLimitMax', 'атм', { dash }),
|
makeDataset('Перепад давления МАКС', 'ΔР макс','#B34A36', 'pressureDeltaLimitMax', 'атм', { dash }),
|
||||||
makeDataset('Давление', 'Давл','#FF0000', 'pressureMM', 'атм', makeAreaOptions('pressure')),
|
makeDataset('Давление', 'Давл','#FF0000', 'pressureMM', 'атм', makeAreaOptions('pressure')),
|
||||||
], [
|
], [
|
||||||
makeDataset('Осевая нагрузка', 'Нагр','#0000CC', 'axialLoad', 'т'),
|
makeDataset('Осевая нагрузка', 'Нагр','#0000CC', 'axialLoad', 'т'),
|
||||||
makeDataset('Осевая нагрузка заданная', 'Нагр зад-я','#3D6DCC', 'axialLoadSp', 'т', { dash }),
|
makeDataset('Осевая нагрузка заданная', 'Нагр зад-я','#3D6DCC', 'axialLoadSp', 'т', { dash }),
|
||||||
makeDataset('Осевая нагрузка максимальная', 'Нагр макс','#3D3DCC', 'axialLoadLimitMax', 'т', { dash }),
|
makeDataset('Осевая нагрузка МАКС', 'Нагр макс','#3D3DCC', 'axialLoadLimitMax', 'т', { dash }),
|
||||||
makeDataset('Осевая нагрузка', 'Нагр','#0000CC', 'axialLoadMM', 'т', makeAreaOptions('axialLoad')),
|
makeDataset('Осевая нагрузка', 'Нагр','#0000CC', 'axialLoadMM', 'т', makeAreaOptions('axialLoad')),
|
||||||
], [
|
], [
|
||||||
makeDataset('Вес на крюке', 'Вес на крюке','#00B3B3', 'hookWeight', 'т'),
|
makeDataset('Вес на крюке', 'Вес на крюке','#00B3B3', 'hookWeight', 'т'),
|
||||||
makeDataset('Вес инструмента ХХ', 'Вес инст ХХ','#29CCB1', 'hookWeightIdle', 'т', { dash }),
|
makeDataset('Вес инструмента ХХ', 'Вес инст ХХ','#29CCB1', 'hookWeightIdle', 'т', { dash }),
|
||||||
makeDataset('Вес инструмента минимальный', 'Вес инст мин','#47A1B3', 'hookWeightLimitMin', 'т', { dash }),
|
makeDataset('Вес инструмента МИН', 'Вес инст мин','#47A1B3', 'hookWeightLimitMin', 'т', { dash }),
|
||||||
makeDataset('Вес инструмента максимальный', 'Вес инст мах','#2D7280', 'hookWeightLimitMax', 'т', { dash }),
|
makeDataset('Вес инструмента МАКС', 'Вес инст мах','#2D7280', 'hookWeightLimitMax', 'т', { dash }),
|
||||||
makeDataset('Обороты ротора', 'Об ротора','#11B32F', 'rotorSpeed', 'об/мин'),
|
makeDataset('Обороты ротора', 'Об ротора','#11B32F', 'rotorSpeed', 'об/мин'),
|
||||||
makeDataset('Обороты ротора', 'Об ротора','#11B32F', 'rotorSpeedMM', 'об/мин', makeAreaOptions('rotorSpeed')),
|
makeDataset('Обороты ротора', 'Об ротора','#11B32F', 'rotorSpeedMM', 'об/мин', makeAreaOptions('rotorSpeed')),
|
||||||
], [
|
], [
|
||||||
makeDataset('Момент на роторе', 'Момент','#990099', 'rotorTorque', 'кН·м'),
|
makeDataset('Момент на роторе', 'Момент','#990099', 'rotorTorque', 'кН·м'),
|
||||||
makeDataset('План. Момент на роторе', 'Момент зад-й','#9629CC', 'rotorTorqueSp', 'кН·м', { dash }),
|
makeDataset('План. Момент на роторе', 'Момент зад-й','#9629CC', 'rotorTorqueSp', 'кН·м', { dash }),
|
||||||
makeDataset('Момент на роторе х.х.', 'Момень ХХ','#CC2996', 'rotorTorqueIdle', 'кН·м', { dash }),
|
makeDataset('Момент на роторе ХХ', 'Момент ХХ','#CC2996', 'rotorTorqueIdle', 'кН·м', { dash }),
|
||||||
makeDataset('Момент максимальный', 'Момент макс','#FF00FF', 'rotorTorqueLimitMax', 'кН·м', { dash }),
|
makeDataset('Момент МАКС.', 'Момент макс','#FF00FF', 'rotorTorqueLimitMax', 'кН·м', { dash }),
|
||||||
makeDataset('Момент на роторе', 'Момент','#990099', 'rotorTorqueMM', 'кН·м', makeAreaOptions('rotorTorque')),
|
makeDataset('Момент на роторе', 'Момент','#990099', 'rotorTorqueMM', 'кН·м', makeAreaOptions('rotorTorque')),
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@ -253,14 +254,14 @@ const TelemetryView = memo(() => {
|
|||||||
<LoaderPortal show={showLoader}>
|
<LoaderPortal show={showLoader}>
|
||||||
<Grid style={{ gridTemplateColumns: 'auto repeat(6, 1fr)' }}>
|
<Grid style={{ gridTemplateColumns: 'auto repeat(6, 1fr)' }}>
|
||||||
<GridItem col={'1'} row={'1'} colSpan={'8'} style={{ marginBottom: '0.5rem' }}>
|
<GridItem col={'1'} row={'1'} colSpan={'8'} style={{ marginBottom: '0.5rem' }}>
|
||||||
<Flex>
|
<div className={'page-top'}>
|
||||||
<ModeDisplay data={dataSaub} />
|
<ModeDisplay data={dataSaub} />
|
||||||
<div style={{ marginLeft: '1rem' }}>
|
<div>
|
||||||
Интервал:
|
Интервал:
|
||||||
<PeriodPicker onChange={setChartInterval} />
|
<PeriodPicker onChange={setChartInterval} />
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => chartMethods?.setSettingsVisible(true)}>Настроить графики</Button>
|
<Button onClick={() => chartMethods?.setSettingsVisible(true)}>Настроить графики</Button>
|
||||||
<div style={{ marginLeft: '1rem' }}>
|
<div>
|
||||||
Статус:
|
Статус:
|
||||||
<Select value={well.idState ?? 0} onChange={onStatusChanged} disabled={!hasPermission('Well.edit')}>
|
<Select value={well.idState ?? 0} onChange={onStatusChanged} disabled={!hasPermission('Well.edit')}>
|
||||||
<Option value={0} disabled hidden>Неизвестно</Option>
|
<Option value={0} disabled hidden>Неизвестно</Option>
|
||||||
@ -268,14 +269,16 @@ const TelemetryView = memo(() => {
|
|||||||
<Option value={2}>Завершено</Option>
|
<Option value={2}>Завершено</Option>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<Setpoints style={{ marginLeft: '1rem' }} />
|
<Setpoints />
|
||||||
<span style={{ flexGrow: 20 }}> </span>
|
<span style={{ flexGrow: 20 }}> </span>
|
||||||
<WirelineRunOut />
|
<WirelineRunOut />
|
||||||
<img src={isTorqueStabEnabled(dataSpin) ? MomentStabPicEnabled : MomentStabPicDisabled} style={{ marginRight: '15px' }} alt={'TorqueMaster'} />
|
<div className={'icons'}>
|
||||||
<img src={isSpinEnabled(dataSpin) ? SpinPicEnabled : SpinPicDisabled} style={{ marginRight: '15px' }} alt={'SpinMaster'} />
|
<img src={isTorqueStabEnabled(dataSpin) ? MomentStabPicEnabled : MomentStabPicDisabled} alt={'TorqueMaster'} />
|
||||||
<h2 style={{ marginBottom: 0, marginRight: '15px', fontWeight: 'bold', color: isMseEnabled(dataSaub) ? 'green' : 'lightgrey' }}>MSE</h2>
|
<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} />
|
<UserOfWell data={dataSaub} />
|
||||||
</Flex>
|
</div>
|
||||||
</GridItem>
|
</GridItem>
|
||||||
<GridItem col={'1'} row={'2'} rowSpan={'3'} style={{ minWidth: '260px', width: '0.142fr' }}>
|
<GridItem col={'1'} row={'2'} rowSpan={'3'} style={{ minWidth: '260px', width: '0.142fr' }}>
|
||||||
<CustomColumn data={dataSaub} />
|
<CustomColumn data={dataSaub} />
|
||||||
|
@ -17,6 +17,7 @@ import { WellService } from '@api'
|
|||||||
|
|
||||||
import Measure from './Measure'
|
import Measure from './Measure'
|
||||||
import Reports from './Reports'
|
import Reports from './Reports'
|
||||||
|
import WellCase from './WellCase'
|
||||||
import Analytics from './Analytics'
|
import Analytics from './Analytics'
|
||||||
import Documents from './Documents'
|
import Documents from './Documents'
|
||||||
import Telemetry from './Telemetry'
|
import Telemetry from './Telemetry'
|
||||||
@ -68,6 +69,7 @@ const Well = memo(() => {
|
|||||||
<PrivateMenu.Link content={Documents} icon={<FolderOutlined />} />
|
<PrivateMenu.Link content={Documents} icon={<FolderOutlined />} />
|
||||||
<PrivateMenu.Link content={Measure} icon={<ExperimentOutlined />} />
|
<PrivateMenu.Link content={Measure} icon={<ExperimentOutlined />} />
|
||||||
<PrivateMenu.Link content={DrillingProgram} icon={<FolderOutlined />} />
|
<PrivateMenu.Link content={DrillingProgram} icon={<FolderOutlined />} />
|
||||||
|
<PrivateMenu.Link content={WellCase} icon={<FolderOutlined />} />
|
||||||
</PrivateMenu>
|
</PrivateMenu>
|
||||||
|
|
||||||
<WellContext.Provider value={[well, updateWell]}>
|
<WellContext.Provider value={[well, updateWell]}>
|
||||||
@ -84,6 +86,7 @@ const Well = memo(() => {
|
|||||||
<Route path={Documents.route} element={<Documents />} />
|
<Route path={Documents.route} element={<Documents />} />
|
||||||
<Route path={Measure.route} element={<Measure />} />
|
<Route path={Measure.route} element={<Measure />} />
|
||||||
<Route path={DrillingProgram.route} element={<DrillingProgram />} />
|
<Route path={DrillingProgram.route} element={<DrillingProgram />} />
|
||||||
|
<Route path={WellCase.route} element={<WellCase />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Content>
|
</Content>
|
||||||
</Layout>
|
</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'
|
import { UploadForm } from '@components/UploadForm'
|
||||||
|
|
||||||
const errorTextStyle = { color: 'red', fontWeight: 'bold' }
|
const errorTextStyle = { color: 'red', fontWeight: 'bold' }
|
||||||
const uploadFormStyle = { marginTop: '24px' }
|
const uploadFormStyle = { marginTop: 24 }
|
||||||
|
|
||||||
export const ImportOperations = memo(({ well: givenWell, onDone }) => {
|
export const ImportOperations = memo(({ well: givenWell, onDone }) => {
|
||||||
const [deleteBeforeImport, setDeleteBeforeImport] = useState(false)
|
const [deleteBeforeImport, setDeleteBeforeImport] = useState(false)
|
||||||
@ -15,7 +15,7 @@ export const ImportOperations = memo(({ well: givenWell, onDone }) => {
|
|||||||
const [wellContext] = useWell()
|
const [wellContext] = useWell()
|
||||||
const well = useMemo(() => givenWell ?? wellContext, [givenWell, wellContext])
|
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(() => {
|
const onUploadSuccess = useCallback(() => {
|
||||||
setErrorText('')
|
setErrorText('')
|
||||||
@ -34,6 +34,7 @@ export const ImportOperations = memo(({ well: givenWell, onDone }) => {
|
|||||||
<span>Очистить список операций перед импортом </span>
|
<span>Очистить список операций перед импортом </span>
|
||||||
<Switch onChange={setDeleteBeforeImport} checked={deleteBeforeImport} />
|
<Switch onChange={setDeleteBeforeImport} checked={deleteBeforeImport} />
|
||||||
<UploadForm
|
<UploadForm
|
||||||
|
multiple
|
||||||
url={url}
|
url={url}
|
||||||
style={uploadFormStyle}
|
style={uploadFormStyle}
|
||||||
onUploadSuccess={onUploadSuccess}
|
onUploadSuccess={onUploadSuccess}
|
||||||
|
@ -1,17 +1,87 @@
|
|||||||
import { useState, useEffect, useCallback, memo, useMemo } from 'react'
|
import { useState, useEffect, useCallback, memo, useMemo } from 'react'
|
||||||
|
import { InputNumber } from 'antd'
|
||||||
|
|
||||||
import { useWell } from '@asb/context'
|
import { useWell } from '@asb/context'
|
||||||
import {
|
import {
|
||||||
EditableTable,
|
EditableTable,
|
||||||
makeSelectColumn,
|
makeSelectColumn,
|
||||||
makeNumericAvgRange,
|
makeGroupColumn,
|
||||||
|
makeNumericRender,
|
||||||
makeNumericSorter,
|
makeNumericSorter,
|
||||||
|
RegExpIsFloat,
|
||||||
} from '@components/Table'
|
} from '@components/Table'
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { DrillParamsService, WellOperationService } from '@api'
|
import { DrillParamsService, WellOperationService } from '@api'
|
||||||
import { arrayOrDefault } from '@utils'
|
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) => {
|
export const getColumns = async (idWell) => {
|
||||||
let sectionTypes = await WellOperationService.getSectionTypes(idWell)
|
let sectionTypes = await WellOperationService.getSectionTypes(idWell)
|
||||||
|
@ -74,7 +74,7 @@ html {
|
|||||||
.header .title{
|
.header .title{
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding-left: 450px;
|
padding-left: calc(100vw / 2 - 400px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header button{
|
.header button{
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
.detected-operations-page {
|
.detected-operations-page {
|
||||||
& .page-top {
|
& .page-top {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 10px;
|
margin: 5px;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -142,3 +142,15 @@ code {
|
|||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 4px 15px;
|
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 { BarChartOutlined, LineChartOutlined, DotChartOutlined, AreaChartOutlined, BorderOuterOutlined, } from '@ant-design/icons'
|
||||||
import { AntdIconProps } from '@ant-design/icons/lib/components/AntdIcon'
|
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
|
if (!Array.isArray(points) || points.length < 3) return points
|
||||||
|
|
||||||
const out: DataType[] = []
|
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))
|
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
|
let Icon
|
||||||
switch (chart.type) {
|
switch (chart.type) {
|
||||||
case 'needle': Icon = BarChartOutlined; break
|
case 'needle': Icon = BarChartOutlined; break
|
||||||
|
@ -11,3 +11,9 @@ export const mainFrameSize = () => ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const isDev = () => process.env.NODE_ENV === 'development'
|
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