Merge branch 'dev' into feature/add-page-operation-time

This commit is contained in:
ts_salikhov 2022-10-05 14:28:54 +04:00
commit 321900da89
50 changed files with 8085 additions and 5326 deletions

View File

@ -39,9 +39,9 @@ npx openapi -i http://{IP_ADDRESS}:{PORT}/swagger/v1/swagger.json -o src/service
| IP-адрес | Описание |
|:-|:-|
| 127.0.0.1:5000 | Локальный адрес вашей машины (привязан к `update_openapi`) |
| 192.168.1.70:5000 | Локальный адрес development-сервера (привязан к `update_openapi_server`) |
| 192.168.1.113:5000 | Локальный адрес development-сервера (привязан к `update_openapi_server`) |
| 46.146.209.148:89 | Внешний адрес development-сервера |
| 46.146.209.148 | Внешний адрес production-сервера |
| cloud.digitaldrilling.ru | Внешний адрес production-сервера |
## 3. Компиляция production-версии приложения
После выполнения вышеописанных пунктов приложение готово к компиляции.

12563
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,9 +19,10 @@
"start": "webpack-dev-server --mode=development --open --hot",
"build": "webpack --mode=production",
"test": "jest",
"dev": "webpack-dev-server --mode=development --open --hot",
"oul": "npx openapi -i http://127.0.0.1:5000/swagger/v1/swagger.json -o src/services/api",
"oud": "npx openapi -i http://192.168.1.70:5000/swagger/v1/swagger.json -o src/services/api",
"oug": "npx openapi -i http://46.146.209.148/swagger/v1/swagger.json -o src/services/api",
"oud": "npx openapi -i http://192.168.1.113:5000/swagger/v1/swagger.json -o src/services/api",
"oug": "npx openapi -i https://cloud.digitaldrilling.ru/swagger/v1/swagger.json -o src/services/api",
"oug_dev": "npx openapi -i http://46.146.209.148:89/swagger/v1/swagger.json -o src/services/api"
},
"proxy": "http://46.146.209.148:89",

View File

@ -8,7 +8,7 @@
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" />
<meta name="description" content="Онлайн мониторинг процесса бурения в реальном времени в офисе заказчика" />
<title>АСБ Vision</title>
<title>DDrilling</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -1,20 +1,20 @@
import { memo, ReactNode } from 'react'
import { Key, memo, ReactNode } from 'react'
import { Layout, LayoutProps } from 'antd'
import PageHeader from '@components/PageHeader'
import WellTreeSelector from '@components/selectors/WellTreeSelector'
import { WellTreeSelector, WellTreeSelectorProps } from '@components/selectors/WellTreeSelector'
import { wrapPrivateComponent } from '@utils'
export type LayoutPortalProps = LayoutProps & {
title?: ReactNode
noSheet?: boolean
showSelector?: boolean
selector?: WellTreeSelectorProps
}
const _LayoutPortal = memo<LayoutPortalProps>(({ title, noSheet, showSelector, ...props }) => (
const _LayoutPortal = memo<LayoutPortalProps>(({ title, noSheet, selector, ...props }) => (
<Layout.Content>
<PageHeader title={title}>
<WellTreeSelector show={showSelector} />
<WellTreeSelector {...selector} />
</PageHeader>
<Layout>
{noSheet ? props.children : (

View File

@ -1,6 +1,6 @@
import { memo } from 'react'
import { Layout } from 'antd'
import { Link, useLocation } from 'react-router-dom'
import { Link } from 'react-router-dom'
import { BasicProps } from 'antd/lib/layout/layout'
import { headerHeight } from '@utils'
@ -14,10 +14,7 @@ export type PageHeaderProps = BasicProps & {
children?: React.ReactNode
}
export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Мониторинг', isAdmin, children, ...other }) => {
const location = useLocation()
return (
export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Мониторинг', isAdmin, children, ...other }) => (
<Layout>
<Layout.Header className={'header'} {...other}>
<Link to={'/'} style={{ height: headerHeight }}>
@ -28,7 +25,6 @@ export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Монит
<UserMenu isAdmin={isAdmin} />
</Layout.Header>
</Layout>
)
})
))
export default PageHeader

View File

@ -12,7 +12,6 @@ export {
makeNumericColumnPlanFact,
makeNumericStartEnd,
makeNumericMinMax,
makeNumericAvgRange
} from './numeric'
export { makeColumnsPlanFact } from './plan_fact'
export { makeSelectColumn } from './select'

View File

@ -94,18 +94,4 @@ export const makeNumericMinMax = (
makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max')),
])
export const makeNumericAvgRange = (
title: ReactNode,
dataIndex: string,
fixed: number,
filters: object[],
filterDelegate: (key: string | number) => any,
renderDelegate: (_: any, row: object) => any,
width: string
) => makeGroupColumn(title, [
makeNumericColumn('мин', dataIndex + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Min')),
makeNumericColumn('сред', dataIndex + 'Avg', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Avg')),
makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max'))
])
export default makeNumericColumn

View File

@ -20,7 +20,6 @@ export {
makeNumericColumnPlanFact,
makeNumericStartEnd,
makeNumericMinMax,
makeNumericAvgRange,
makeSelectColumn,
makeTagColumn,
makeTagInput,

View File

@ -7,6 +7,9 @@ import { DataType } from './Columns'
export const makeNumericSorter = <T extends unknown>(key: keyof DataType<T>) =>
(a: DataType<T>, b: DataType<T>) => Number(a[key]) - Number(b[key])
export const makeNumericObjSorter = (key: [string, string]) =>
(a: DataType, b: DataType) => Number(a[key[0]][key[1]]) - Number(b[key[0]][key[1]])
export const makeStringSorter = <T extends unknown>(key: keyof DataType<T>) => (a?: DataType<T> | null, b?: DataType<T> | null) => {
if (!a && !b) return 0
if (!a) return 1

View File

@ -10,6 +10,7 @@ import { notify, upload } from './factory'
import { ErrorFetch } from './ErrorFetch'
export type UploadFormProps = {
multiple?: boolean
url: string
disabled?: boolean
accept?: string
@ -22,7 +23,7 @@ export type UploadFormProps = {
onUploadError?: (error: unknown) => void
}
export const UploadForm = memo<UploadFormProps>(({ url, disabled, style, formData, mimeTypes, onUploadStart, onUploadSuccess, onUploadComplete, onUploadError }) => {
export const UploadForm = memo<UploadFormProps>(({ url, multiple, disabled, style, formData, mimeTypes, onUploadStart, onUploadSuccess, onUploadComplete, onUploadError }) => {
const [fileList, setfileList] = useState<UploadFile<any>[]>([])
const checkMimeTypes = useCallback((file: RcFile) => {
@ -38,7 +39,7 @@ export const UploadForm = memo<UploadFormProps>(({ url, disabled, style, formDat
onUploadStart?.()
try {
const formDataLocal = new FormData()
fileList.forEach((val) => formDataLocal.append('files', val.originFileObj as Blob))
fileList.forEach((val) => formDataLocal.append(multiple ? 'files' : 'file', val.originFileObj as Blob))
if(formData)
for(const propName in formData)
@ -60,7 +61,7 @@ export const UploadForm = memo<UploadFormProps>(({ url, disabled, style, formDat
setfileList([])
onUploadComplete?.()
}
}, [fileList, formData, onUploadComplete, onUploadError, onUploadStart, onUploadSuccess, url])
}, [fileList, formData, onUploadComplete, onUploadError, onUploadStart, onUploadSuccess, url, multiple])
const isSendButtonEnabled = fileList.length > 0
return(
@ -72,6 +73,7 @@ export const UploadForm = memo<UploadFormProps>(({ url, disabled, style, formDat
fileList={fileList}
onChange={(props) => setfileList(props.fileList)}
beforeUpload={checkMimeTypes}
maxCount={multiple ? undefined : 1}
>
<Button disabled={disabled} icon={<UploadOutlined/>}>Загрузить файл</Button>
</Upload>

View File

@ -27,6 +27,7 @@ import {
D3TooltipSettings,
} from './plugins'
import type {
BaseDataType,
ChartAxis,
ChartDataset,
ChartDomain,
@ -50,13 +51,13 @@ export const getByAccessor = <DataType extends Record<any, any>, R>(accessor: ke
return (d) => d[accessor]
}
const createAxis = <DataType,>(config: ChartAxis<DataType>) => {
const createAxis = <DataType extends BaseDataType>(config: ChartAxis<DataType>) => {
if (config.type === 'time')
return d3.scaleTime()
return d3.scaleLinear()
}
export type D3ChartProps<DataType> = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
export type D3ChartProps<DataType extends BaseDataType> = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
/** Параметры общей горизонтальной оси */
xAxis: ChartAxis<DataType>
/** Параметры графиков */
@ -94,7 +95,7 @@ export type D3ChartProps<DataType> = React.DetailedHTMLProps<React.HTMLAttribute
}
}
const getDefaultXAxisConfig = <DataType,>(): ChartAxis<DataType> => ({
const getDefaultXAxisConfig = <DataType extends BaseDataType>(): ChartAxis<DataType> => ({
type: 'time',
accessor: (d: any) => new Date(d.date)
})

View File

@ -6,13 +6,14 @@ import { useD3MouseZone } from '@components/d3/D3MouseZone'
import { D3TooltipPosition } from '@components/d3/plugins/D3Tooltip'
import { getChartIcon, isDev, usePartialProps } from '@utils'
import { BaseDataType } from '../types'
import { ChartGroup, ChartSizes } from './D3MonitoringCharts'
import '@styles/d3.less'
type D3GroupRenderFunction<DataType> = (group: ChartGroup<DataType>, data: DataType[]) => ReactNode
type D3GroupRenderFunction<DataType extends BaseDataType> = (group: ChartGroup<DataType>, data: DataType[]) => ReactNode
export type D3HorizontalCursorSettings<DataType> = {
export type D3HorizontalCursorSettings<DataType extends BaseDataType> = {
width?: number
height?: number
render?: D3GroupRenderFunction<DataType>
@ -23,7 +24,7 @@ export type D3HorizontalCursorSettings<DataType> = {
lineStyle?: SVGProps<SVGLineElement>
}
export type D3HorizontalCursorProps<DataType> = D3HorizontalCursorSettings<DataType> & {
export type D3HorizontalCursorProps<DataType extends BaseDataType> = D3HorizontalCursorSettings<DataType> & {
groups: ChartGroup<DataType>[]
data: DataType[]
sizes: ChartSizes
@ -37,7 +38,7 @@ const defaultLineStyle: SVGProps<SVGLineElement> = {
const offsetY = 5
const makeDefaultRender = <DataType,>(): D3GroupRenderFunction<DataType> => (group, data) => (
const makeDefaultRender = <DataType extends BaseDataType>(): D3GroupRenderFunction<DataType> => (group, data) => (
<>
{data.length > 0 ? group.charts.map((chart) => {
const xFormat = (d: number | Date) => chart.xAxis.format?.(d) ?? `${(+d).toFixed(2)} ${chart.xAxis.unit ?? ''}`
@ -62,7 +63,7 @@ const makeDefaultRender = <DataType,>(): D3GroupRenderFunction<DataType> => (gro
</>
)
const _D3HorizontalCursor = <DataType,>({
const _D3HorizontalCursor = <DataType extends BaseDataType>({
spaceBetweenGroups = 30,
height = 200,
render = makeDefaultRender<DataType>(),

View File

@ -1,7 +1,7 @@
import { Button, Checkbox, Form, FormItemProps, Input, InputNumber, Select, Tooltip } from 'antd'
import { memo, useCallback, useEffect, useMemo } from 'react'
import { MinMax } from '@components/d3/types'
import { BaseDataType, MinMax } from '@components/d3/types'
import { ColorPicker, Color } from '@components/ColorPicker'
import { ExtendedChartDataset } from './D3MonitoringCharts'
@ -18,13 +18,13 @@ const lineTypes = [
{ value: 'needle', label: 'Иглы' },
]
export type D3MonitoringChartEditorProps<DataType> = {
export type D3MonitoringChartEditorProps<DataType extends BaseDataType> = {
group: ExtendedChartDataset<DataType>[]
chart: ExtendedChartDataset<DataType>
onChange: (value: ExtendedChartDataset<DataType>) => boolean
}
const _D3MonitoringChartEditor = <DataType,>({
const _D3MonitoringChartEditor = <DataType extends BaseDataType>({
group,
chart: value,
onChange,
@ -93,8 +93,8 @@ const _D3MonitoringChartEditor = <DataType,>({
</Item>
<Item label={'Диапазон'}>
<Input.Group compact>
<InputNumber disabled={!!value.linkedTo} value={value.xDomain?.min} onChange={(min) => onDomainChange({ min })} placeholder={'Мин'} />
<InputNumber disabled={!!value.linkedTo} value={value.xDomain?.max} onChange={(max) => onDomainChange({ max })} placeholder={'Макс'} />
<InputNumber disabled={!!value.linkedTo} value={value.xDomain?.min} onChange={(min) => onDomainChange({ min: min ?? undefined })} placeholder={'Мин'} />
<InputNumber disabled={!!value.linkedTo} value={value.xDomain?.max} onChange={(max) => onDomainChange({ max: max ?? undefined })} placeholder={'Макс'} />
<Button
disabled={!!value.linkedTo || (!Number.isFinite(value.xDomain?.min) && !Number.isFinite(value.xDomain?.max))}
onClick={() => onDomainChange({ min: undefined, max: undefined })}

View File

@ -8,6 +8,7 @@ import LoaderPortal from '@components/LoaderPortal'
import { isDev, usePartialProps, useUserSettings } from '@utils'
import {
BaseDataType,
ChartAxis,
ChartDataset,
ChartOffset,
@ -51,7 +52,7 @@ const calculateDomain = (mm: MinMax): Required<MinMax> => {
return { min, max }
}
export type ExtendedChartDataset<DataType> = ChartDataset<DataType> & {
export type ExtendedChartDataset<DataType extends BaseDataType> = ChartDataset<DataType> & {
/** Диапазон отображаемых значений по горизонтальной оси */
xDomain: MinMax
/** Скрыть отображение шкалы графика */
@ -60,9 +61,9 @@ export type ExtendedChartDataset<DataType> = ChartDataset<DataType> & {
showCurrentValue?: boolean
}
export type ExtendedChartRegistry<DataType> = ChartRegistry<DataType> & ExtendedChartDataset<DataType>
export type ExtendedChartRegistry<DataType extends BaseDataType> = ChartRegistry<DataType> & ExtendedChartDataset<DataType>
export type ChartGroup<DataType> = {
export type ChartGroup<DataType extends BaseDataType> = {
/** Получить D3 выборку, содержащую корневой G-элемент группы */
(): d3.Selection<SVGGElement, any, any, any>
/** Уникальный ключ группы (индекс) */
@ -86,12 +87,12 @@ const defaultRegulators: TelemetryRegulators = {
5: { color: '#007070', label: 'Расход' },
}
const getDefaultYAxisConfig = <DataType,>(): ChartAxis<DataType> => ({
const getDefaultYAxisConfig = <DataType extends BaseDataType>(): ChartAxis<DataType> => ({
type: 'time',
accessor: (d: any) => new Date(d.date)
})
const getDefaultYTicks = <DataType,>(): Required<ChartTick<DataType>> => ({
const getDefaultYTicks = <DataType extends BaseDataType>(): Required<ChartTick<DataType>> => ({
visible: false,
format: (d: d3.NumberValue, idx: number, data?: DataType) => String(d),
color: 'lightgray',
@ -101,7 +102,7 @@ const getDefaultYTicks = <DataType,>(): Required<ChartTick<DataType>> => ({
/**
* @template DataType тип данных отображаемых записей
*/
export type D3MonitoringChartsProps<DataType extends Record<string, any>> = Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, 'ref'> & {
export type D3MonitoringChartsProps<DataType extends BaseDataType> = Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, 'ref'> & {
/** Двумерный массив датасетов (группа-график) */
datasetGroups: ExtendedChartDataset<DataType>[][]
/** Ширина графика числом пикселей или CSS-значением (px/%/em/rem) */

View File

@ -1,9 +1,10 @@
import { memo } from 'react'
import { BaseDataType } from '@components/d3/types'
import { ChartGroup, ChartSizes } from '@components/d3/monitoring/D3MonitoringCharts'
import { makeDisplayValue } from '@utils'
export type D3MonitoringCurrentValuesProps<DataType> = {
export type D3MonitoringCurrentValuesProps<DataType extends BaseDataType> = {
groups: ChartGroup<DataType>[]
data: DataType[]
left: number
@ -12,7 +13,7 @@ export type D3MonitoringCurrentValuesProps<DataType> = {
const display = makeDisplayValue({ def: '---', fixed: 2 })
const _D3MonitoringCurrentValues = <DataType,>({ groups, data, left, sizes }: D3MonitoringCurrentValuesProps<DataType>) => (
const _D3MonitoringCurrentValues = <DataType extends BaseDataType>({ groups, data, left, sizes }: D3MonitoringCurrentValuesProps<DataType>) => (
<g transform={`translate(${left}, ${sizes.chartsTop})`} pointerEvents={'none'}>
{groups.map((group) => (
<g key={group.key} transform={`translate(${sizes.groupLeft(group.key)}, 0)`}>

View File

@ -1,17 +1,18 @@
import { CSSProperties, Key, memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Button, Divider, Empty, Modal, Popconfirm, Tooltip, Tree } from 'antd'
import { Button, Divider, Empty, Modal, Popconfirm, Tooltip, Tree, TreeDataNode } from 'antd'
import { UndoOutlined } from '@ant-design/icons'
import { EventDataNode } from 'antd/lib/tree'
import { notify } from '@components/factory'
import { getChartIcon } from '@utils'
import { BaseDataType } from '../types'
import { ExtendedChartDataset } from './D3MonitoringCharts'
import { TelemetryRegulators } from './D3MonitoringLimitChart'
import D3MonitoringChartEditor from './D3MonitoringChartEditor'
import D3MonitoringLimitEditor from './D3MonitoringLimitEditor'
export type D3MonitoringGroupsEditorProps<DataType> = {
export type D3MonitoringGroupsEditorProps<DataType extends BaseDataType> = {
visible?: boolean
groups: ExtendedChartDataset<DataType>[][]
regulators: TelemetryRegulators
@ -20,7 +21,7 @@ export type D3MonitoringGroupsEditorProps<DataType> = {
onReset: () => void
}
const getChartLabel = <DataType,>(chart: ExtendedChartDataset<DataType>) => (
const getChartLabel = <DataType extends BaseDataType>(chart: ExtendedChartDataset<DataType>) => (
<Tooltip title={chart.label}>
{getChartIcon(chart)} {chart.label}
</Tooltip>
@ -34,14 +35,14 @@ const divStyle: CSSProperties = {
flexGrow: 1,
}
const getNodePos = (node: EventDataNode): { group: number, chart?: number } => {
const getNodePos = (node: EventDataNode<TreeDataNode>): { group: number, chart?: number } => {
const out = node.pos.split('-').map(Number)
return { group: out[1], chart: out[2] }
}
type EditingMode = null | 'limit' | 'chart'
const _D3MonitoringEditor = <DataType,>({
const _D3MonitoringEditor = <DataType extends BaseDataType>({
visible,
groups: oldGroups,
regulators: oldRegulators,
@ -61,8 +62,8 @@ const _D3MonitoringEditor = <DataType,>({
const onModalOk = useCallback(() => onChange(groups, regulators), [groups, regulators])
const onDrop = useCallback((info: {
node: EventDataNode
dragNode: EventDataNode
node: EventDataNode<TreeDataNode>
dragNode: EventDataNode<TreeDataNode>
dropPosition: number
}) => {
const { dragNode, dropPosition, node } = info
@ -152,12 +153,12 @@ const _D3MonitoringEditor = <DataType,>({
<Tree
draggable
selectable
onExpand={(keys) => setExpand(keys)}
onExpand={(keys: Key[]) => setExpand(keys)}
expandedKeys={expand}
selectedKeys={selected}
treeData={treeItems}
onDrop={onDrop}
onSelect={(value) => {
onSelect={(value: Key[]) => {
setSelected(value)
setMode('chart')
}}

View File

@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef } from 'react'
import { Property } from 'csstype'
import * as d3 from 'd3'
import { ChartRegistry } from '@components/d3/types'
import { BaseDataType, ChartRegistry } from '@components/d3/types'
import { useD3MouseZone } from '@components/d3/D3MouseZone'
import { usePartialProps } from '@utils'
@ -32,12 +32,12 @@ export type D3LegendSettings = {
const defaultOffset = { x: 10, y: 10 }
export type D3LegendProps<DataType> = D3LegendSettings & {
export type D3LegendProps<DataType extends BaseDataType> = D3LegendSettings & {
/** Массив графиков */
charts: ChartRegistry<DataType>[]
}
const _D3Legend = <DataType,>({
const _D3Legend = <DataType extends BaseDataType>({
charts,
width,
height,

View File

@ -4,7 +4,7 @@ import * as d3 from 'd3'
import { isDev } from '@utils'
import { ChartRegistry } from '@components/d3/types'
import { BaseDataType, ChartRegistry } from '@components/d3/types'
import { D3MouseState, useD3MouseZone } from '@components/d3/D3MouseZone'
import { getTouchedElements, wrapPlugin } from './base'
@ -12,7 +12,7 @@ import '@styles/d3.less'
export type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none'
export type D3RenderData<DataType> = {
export type D3RenderData<DataType extends BaseDataType> = {
/** Параметры графика */
chart: ChartRegistry<DataType>
/** Данные графика */
@ -21,9 +21,9 @@ export type D3RenderData<DataType> = {
selection?: d3.Selection<any, DataType, any, any>
}
export type D3RenderFunction<DataType> = (data: D3RenderData<DataType>[], mouseState: D3MouseState) => ReactNode
export type D3RenderFunction<DataType extends BaseDataType> = (data: D3RenderData<DataType>[], mouseState: D3MouseState) => ReactNode
export type D3TooltipSettings<DataType> = {
export type D3TooltipSettings<DataType extends BaseDataType> = {
/** Функция отрисоки тултипа */
render?: D3RenderFunction<DataType>
/** Ширина тултипа */
@ -39,7 +39,7 @@ export type D3TooltipSettings<DataType> = {
limit?: number
}
export const makeDefaultRender = <DataType,>(): D3RenderFunction<DataType> => (data, mouseState) => (
export const makeDefaultRender = <DataType extends BaseDataType>(): D3RenderFunction<DataType> => (data, mouseState) => (
<>
{data.length > 0 ? data.map(({ chart, data }) => {
let Icon
@ -74,11 +74,11 @@ export const makeDefaultRender = <DataType,>(): D3RenderFunction<DataType> => (d
</>
)
export type D3TooltipProps<DataType> = Partial<D3TooltipSettings<DataType>> & {
export type D3TooltipProps<DataType extends BaseDataType> = Partial<D3TooltipSettings<DataType>> & {
charts: ChartRegistry<DataType>[],
}
function _D3Tooltip<DataType extends Record<string, unknown>>({
function _D3Tooltip<DataType extends BaseDataType>({
width = 200,
height = 120,
render = makeDefaultRender<DataType>(),

View File

@ -3,7 +3,7 @@ import * as d3 from 'd3'
import { getDistance, TouchType } from '@utils'
import { ChartRegistry } from '../types'
import { BaseDataType, ChartRegistry } from '../types'
export type BasePluginSettings = {
enabled?: boolean
@ -16,7 +16,7 @@ export const wrapPlugin = <TProps,>(
const wrappedComponent = ({ enabled, ...props }: TProps & BasePluginSettings) => {
if (!(enabled ?? defaultEnabled)) return <></>
return <Component {...(props as TProps)} />
return <Component {...(props as (TProps & JSX.IntrinsicAttributes))} /> // IntrinsicAttributes добавлено как необходимое ограничение
}
return wrappedComponent
@ -89,7 +89,7 @@ const makeIsRectTouched = (x: number, y: number, limit: number, type: TouchType
}
}
export const getTouchedElements = <DataType,>(
export const getTouchedElements = <DataType extends BaseDataType>(
chart: ChartRegistry<DataType>,
x: number,
y: number,

View File

@ -1,8 +1,8 @@
import * as d3 from 'd3'
import { ChartRegistry } from '../types'
import { BaseDataType, ChartRegistry } from '../types'
export const appendTransition = <DataType, BaseType extends d3.BaseType, Datum, PElement extends d3.BaseType, PDatum>(
export const appendTransition = <DataType extends BaseDataType, BaseType extends d3.BaseType, Datum, PElement extends d3.BaseType, PDatum>(
elms: d3.Selection<BaseType, Datum, PElement, PDatum>,
chart: ChartRegistry<DataType>
): d3.Selection<BaseType, Datum, PElement, PDatum> => {

View File

@ -1,4 +1,4 @@
import { ChartRegistry, PointChartDataset } from '@components/d3/types'
import { BaseDataType, ChartRegistry, PointChartDataset } from '@components/d3/types'
import { appendTransition } from './base'
@ -12,7 +12,7 @@ const defaultConfig: Required<Omit<PointChartDataset, 'type'>> = {
fillOpacity: 1,
}
const getPointsRoot = <DataType,>(chart: ChartRegistry<DataType>, embeded?: boolean): d3.Selection<SVGGElement, any, any, any> => {
const getPointsRoot = <DataType extends BaseDataType>(chart: ChartRegistry<DataType>, embeded?: boolean): d3.Selection<SVGGElement, any, any, any> => {
const root = chart()
if (!embeded) return root
if (root.select('.points').empty())

View File

@ -1,9 +1,9 @@
import { getByAccessor } from '@components/d3/functions'
import { ChartRegistry } from '@components/d3/types'
import { BaseDataType, ChartRegistry } from '@components/d3/types'
import { appendTransition } from './base'
export const renderRectArea = <DataType extends Record<string, any>>(
export const renderRectArea = <DataType extends BaseDataType>(
xAxis: (value: d3.NumberValue) => number,
yAxis: (value: d3.NumberValue) => number,
chart: ChartRegistry<DataType>

View File

@ -3,9 +3,11 @@ import { Property } from 'csstype'
import { D3TooltipSettings } from './plugins'
export type AxisAccessor<DataType extends Record<string, any>> = keyof DataType | ((d: DataType) => any)
export type BaseDataType = Record<string, any>
export type ChartAxis<DataType> = {
export type AxisAccessor<DataType extends BaseDataType> = keyof DataType | ((d: DataType) => any)
export type ChartAxis<DataType extends BaseDataType> = {
/** Тип шкалы */
type: 'linear' | 'time',
/** Ключ записи или метод по которому будет извлекаться значение оси из массива данных */
@ -34,7 +36,7 @@ export type PointChartDataset = {
fillOpacity?: number
}
export type BaseChartDataset<DataType> = {
export type BaseChartDataset<DataType extends BaseDataType> = {
/** Уникальный ключ графика */
key: string | number
/** Параметры вертикальной оси */
@ -101,7 +103,7 @@ export type NeedleChartDataset = {
type: 'needle'
}
export type ChartDataset<DataType> = BaseChartDataset<DataType> & (
export type ChartDataset<DataType extends BaseDataType> = BaseChartDataset<DataType> & (
AreaChartDataset |
LineChartDataset |
NeedleChartDataset |
@ -154,7 +156,7 @@ export type ChartTicks<DataType> = {
y?: ChartTick<DataType>
}
export type ChartRegistry<DataType> = ChartDataset<DataType> & {
export type ChartRegistry<DataType extends BaseDataType> = ChartDataset<DataType> & {
/** Получить D3 выборку, содержащую корневой G-элемент графика */
(): d3.Selection<SVGGElement, DataType, any, any>
/** Получить значение по вертикальной оси из предоставленой записи */

View File

@ -1,6 +1,5 @@
import { Button, Drawer, Skeleton, Tree, TreeProps, Typography } from 'antd'
import { DefaultValueType } from 'rc-tree-select/lib/interface'
import { useState, useEffect, ReactNode, useCallback, memo, Key } from 'react'
import { Button, Drawer, Skeleton, Tree, TreeDataNode, TreeProps, Typography } from 'antd'
import { useState, useEffect, useCallback, memo, Key } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { WellIcon, WellIconState } from '@components/icons'
@ -17,26 +16,18 @@ export const getWellState = (idState?: number): WellIconState => idState === 1 ?
export const checkIsWellOnline = (lastTelemetryDate: unknown): boolean =>
isRawDate(lastTelemetryDate) && (Date.now() - +new Date(lastTelemetryDate) < 600_000)
export type TreeNodeData = {
title?: string | null
key?: string
value?: DefaultValueType
icon?: ReactNode
children?: TreeNodeData[]
}
const getKeyByUrl = (url?: string): [Key | null, string | null] => {
const result = url?.match(/^\/([^\/]+)\/([^\/?]+)/) // pattern "/:type/:id"
if (!result) return [null, null]
return [result[0], result[1]]
}
const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined => {
const getLabel = (wellsTree: TreeDataNode[], value?: string): string | undefined => {
const [url, type] = getKeyByUrl(value)
if (!url) return
let deposit: TreeNodeData | undefined
let cluster: TreeNodeData | undefined
let well: TreeNodeData | undefined
let deposit: TreeDataNode | undefined
let cluster: TreeDataNode | undefined
let well: TreeDataNode | undefined
switch (type) {
case 'deposit':
deposit = wellsTree.find((deposit) => deposit.key === url)
@ -46,7 +37,7 @@ const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined
case 'cluster':
deposit = wellsTree.find((deposit) => (
cluster = deposit.children?.find((cluster: TreeNodeData) => cluster.key === url)
cluster = deposit.children?.find((cluster: TreeDataNode) => cluster.key === url)
))
if (deposit && cluster)
return `${deposit.title} / ${cluster.title}`
@ -54,8 +45,8 @@ const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined
case 'well':
deposit = wellsTree.find((deposit) => (
cluster = deposit.children?.find((cluster: TreeNodeData) => (
well = cluster.children?.find((well: TreeNodeData) => well.key === url)
cluster = deposit.children?.find((cluster: TreeDataNode) => (
well = cluster.children?.find((well: TreeDataNode) => well.key === url)
))
))
if (deposit && cluster && well)
@ -79,8 +70,26 @@ const sortWellsByActive = (a: WellDto, b: WellDto): number => {
return (a.caption || '')?.localeCompare(b.caption || '')
}
export const WellTreeSelector = memo(({ show, ...other }: TreeProps<TreeNodeData> & { show?: boolean }) => {
const [wellsTree, setWellsTree] = useState<TreeNodeData[]>([])
export type WellTreeSelectorProps = TreeProps<TreeDataNode> & {
show?: boolean
expand?: boolean | Key[]
current?: Key
}
const getExpandKeys = (treeData: TreeDataNode[], depositKeys?: Key[] | boolean): Key[] => {
const out: Key[] = []
treeData.forEach((deposit) => {
if (Array.isArray(depositKeys) && !depositKeys.includes(deposit.key)) return
if (deposit.key) out.push(deposit.key)
deposit.children?.forEach((cluster) => {
if (cluster.key) out.push(cluster.key)
})
})
return out
}
export const WellTreeSelector = memo<WellTreeSelectorProps>(({ show, expand, current, ...other }) => {
const [wellsTree, setWellsTree] = useState<TreeDataNode[]>([])
const [showLoader, setShowLoader] = useState<boolean>(false)
const [visible, setVisible] = useState<boolean>(false)
const [expanded, setExpanded] = useState<Key[]>([])
@ -91,26 +100,20 @@ export const WellTreeSelector = memo(({ show, ...other }: TreeProps<TreeNodeData
const location = useLocation()
useEffect(() => {
setVisible((prev) => show ?? prev)
setExpanded((prev) => {
if (typeof show === 'undefined') return prev
if (!show) return []
const out: Key[] = []
wellsTree.forEach((deposit) => {
if (deposit.key) out.push(deposit.key)
deposit.children?.forEach((cluster) => {
if (cluster.key) out.push(cluster.key)
})
})
return out
})
}, [wellsTree, show])
if (current) setSelected([current])
}, [current])
useEffect(() => setVisible((prev) => show ?? prev), [show])
useEffect(() => {
setExpanded((prev) => expand ? getExpandKeys(wellsTree, expand) : prev)
}, [wellsTree, expand])
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const deposits: Array<DepositDto> = await DepositService.getDeposits()
const wellsTree: TreeNodeData[] = deposits.map(deposit =>({
const wellsTree: TreeDataNode[] = deposits.map(deposit =>({
title: deposit.caption,
key: `/deposit/${deposit.id}`,
value: `/deposit/${deposit.id}`,

View File

@ -1,4 +1,4 @@
import { memo } from 'react'
import { HTMLProps, memo } from 'react'
import { Tooltip } from 'antd'
import { UserOutlined } from '@ant-design/icons'
@ -6,33 +6,58 @@ import { UserDto } from '@api'
import { Grid, GridItem } from '@components/Grid'
import { CompanyView } from './CompanyView'
export type UserViewProps = {
export type UserViewProps = HTMLProps<HTMLSpanElement> & {
user?: UserDto
}
export const UserView = memo<UserViewProps>(({ user }) => user ? (
<Tooltip title={(
export const UserView = memo<UserViewProps>(({ user, ...other }) =>
user ? (
<Tooltip
title={
<Grid style={{ columnGap: '8px' }}>
<GridItem row={1} col={1}>Фамилия:</GridItem>
<GridItem row={1} col={2}>{user?.surname}</GridItem>
<GridItem row={1} col={1}>
Фамилия:
</GridItem>
<GridItem row={1} col={2}>
{user?.surname}
</GridItem>
<GridItem row={2} col={1}>Имя:</GridItem>
<GridItem row={2} col={2}>{user?.name}</GridItem>
<GridItem row={2} col={1}>
Имя:
</GridItem>
<GridItem row={2} col={2}>
{user?.name}
</GridItem>
<GridItem row={3} col={1}>Отчество:</GridItem>
<GridItem row={3} col={2}>{user?.patronymic}</GridItem>
<GridItem row={3} col={1}>
Отчество:
</GridItem>
<GridItem row={3} col={2}>
{user?.patronymic}
</GridItem>
<GridItem row={4} col={1}>Компания:</GridItem>
<GridItem row={4} col={1}>
Компания:
</GridItem>
<GridItem row={4} col={2}>
<CompanyView company={user?.company}/>
<CompanyView company={user?.company} />
</GridItem>
</Grid>
)}>
<UserOutlined style={{ marginRight: 8 }}/>
}
>
<span {...other}>
<UserOutlined style={{ marginRight: 8 }} />
{user?.login}
</span>
</Tooltip>
) : (
<Tooltip title='нет пользователя'>-</Tooltip>
))
) : (
<Tooltip title={'нет пользователя'}>
<span {...other}>
<UserOutlined style={{ marginRight: 8 }} />
---
</span>
</Tooltip>
)
)
export default UserView

View File

@ -1,9 +1,11 @@
import { memo } from 'react'
import logo from '@images/logo_32.png'
import { ReactComponent as AsbLogo } from '@images/dd_logo_white_opt.svg'
export const Logo = memo<React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>>((props) => (
<img src={logo} alt={'АСБ'} className={'logo'} {...props} />
export type LogoProps = React.SVGProps<SVGSVGElement> & { size?: number }
export const Logo = memo<LogoProps>(({ size = 200, ...props }) => (
<AsbLogo className={'logo'} height={'100%'} {...props} />
))
export default Logo

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -1,5 +1,5 @@
import { Map, Overlay } from 'pigeon-maps'
import { useState, useEffect, memo } from 'react'
import { useState, useEffect, memo, useMemo } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { Popover, Badge } from 'antd'
@ -49,6 +49,16 @@ const Deposit = memo(() => {
const location = useLocation()
const selectorProps = useMemo(() => {
const hasId = location.pathname.length > '/deposit/'.length
return {
show: true,
expand: hasId ? [location.pathname] : true,
current: hasId ? location.pathname : undefined,
}
}, [location.pathname])
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
@ -63,7 +73,7 @@ const Deposit = memo(() => {
}, [])
return (
<LayoutPortal noSheet showSelector title={'Месторождение'}>
<LayoutPortal noSheet selector={selectorProps} title={'Месторождение'}>
<LoaderPortal show={showLoader}>
<div className={'h-100vh'}>
<Map {...viewParams}>

View File

@ -137,6 +137,7 @@ export const DocumentsTemplate = ({ idCategory, well: givenWell, mimeTypes, head
<div>
<span>Загрузка</span>
<UploadForm
multiple
url={uploadUrl}
mimeTypes={mimeTypes}
onUploadStart={() => setShowLoader(true)}

View File

@ -123,6 +123,7 @@ export const CategoryRender = memo(({ partData, onUpdate, onEdit, onHistory, set
<div className={'file_actions'}>
{permissionToUpload && (
<UploadForm
multiple
url={uploadUrl}
mimeTypes={MimeTypes.XLSX}
style={{ margin: '5px 0 10px 0' }}

View File

@ -198,7 +198,7 @@ const table11Columns = [
const table11Data = [
{
key: '1',
l_label: 'Время начала:',
l_label: 'Время начала',
r_label: 'Время начала',
l_value: 'climbBegin',
r_value: 'climbFinish',
@ -223,17 +223,17 @@ const table12Columns = [
const table12Data = [
{
key: '1',
l_label: 'По стволу:',
l_label: 'По стволу',
r_label: 'По вертикали',
l_value: 'climbBegin',
r_value: 'climbFinish',
l_value: 'bottomholeDepth',
r_value: 'verticalDepth',
},
{
key: '2',
l_label: 'Зенитный',
r_label: 'Азимутальный',
l_value: 'descentBegin',
r_value: 'descentFinish',
l_value: 'zenithAngle',
r_value: 'azimuthAngle',
},
]

View File

@ -6,7 +6,7 @@ import moment from 'moment'
import { useWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal'
import { DateRangeWrapper, Table, makeDateColumn, makeColumn } from '@components/Table'
import { download, invokeWebApiWrapperAsync } from '@components/factory'
import { download, invokeWebApiWrapperAsync, notify } from '@components/factory'
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import { DailyReportService } from '@api'
@ -37,6 +37,16 @@ const DailyReport = memo(() => {
const checkIsDateBusy = useCallback((current) => current.isAfter(moment(), 'day') || data.some((row) => moment(row.reportDate).isSame(current, 'day')), [data])
const makeOnDownloadClick = useCallback((report) => async () => {
const date = moment(report.reportDate)
try {
await download(`/api/well/${well.id}/DailyReport/${date.format('YYYY-MM-DD')}/excel`)
} catch {
notify(`Не удалось скачать суточный рапорт от ${date.format('DD.MM.YYYY')}`, 'error', well)
}
}, [well])
const columns = useMemo(() => [
makeDateColumn('Дата', 'reportDate', undefined, 'DD.MM.YYYY', { width: 300 }),
makeColumn('', '', { width: 200, render: (_, report) => (
@ -44,7 +54,7 @@ const DailyReport = memo(() => {
<Button
type={'link'}
icon={<FileExcelOutlined />}
onClick={async () => await download(`/api/well/${well.id}/DailyReport/${report.reportDate}/excel`)}
onClick={makeOnDownloadClick(report)}
children={'Скачать XLSX'}
/>
<Button
@ -58,7 +68,7 @@ const DailyReport = memo(() => {
/>
</>
)}),
], [well])
], [makeOnDownloadClick])
const filteredData = useMemo(() => {
if (!searchDate) return data

View File

@ -74,7 +74,6 @@ export const DrillerList = memo(({ loading, drillers, onChange }) => {
<Button
onClick={onModalOpen}
loading={isLoading}
style={{ marginRight: '10px' }}
>Список бурильщиков</Button>
</>
)

View File

@ -121,7 +121,6 @@ export const DrillerSchedule = memo(({ drillers, loading, onChange }) => {
<Button
onClick={onModalOpen}
loading={isLoading}
style={{ marginRight: '10px' }}
>Расписание бурильщиков</Button>
</>
)

View File

@ -117,7 +117,6 @@ const Operations = memo(() => {
<Select
allowClear={false}
options={categories}
style={{ marginRight: '10px' }}
onChange={(v) => setSelectedCategory(v)}
value={selectedCategory}
/>
@ -127,7 +126,6 @@ const Operations = memo(() => {
disabled={isLoading}
disabledDate={disabledDates}
disabledTime={disabledTimes}
style={{ marginRight: '10px' }}
showTime={{ hideDisabledOptions: true }}
/>
<InputNumber
@ -137,7 +135,6 @@ const Operations = memo(() => {
onChange={setYDomain}
addonAfter={'мин'}
addonBefore={'Верхняя граница'}
style={{ marginRight: '10px' }}
/>
{permissions.driller.get && (
<>

View File

@ -6,7 +6,7 @@ import { useWell } from '@asb/context'
import { makeDateSorter } from '@components/Table'
import { D3MonitoringCharts } from '@components/d3/monitoring'
import LoaderPortal from '@components/LoaderPortal'
import { Grid, GridItem, Flex } from '@components/Grid'
import { Grid, GridItem } from '@components/Grid'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
import { formatDate, hasPermission, wrapPrivateComponent } from '@utils'
@ -31,6 +31,7 @@ import MomentStabPicDisabled from '@images/DempherOff.png'
import SpinPicEnabled from '@images/SpinEnabled.png'
import SpinPicDisabled from '@images/SpinDisabled.png'
import '@styles/monitoring.less'
import '@styles/message.css'
const { Option } = Select
@ -87,25 +88,25 @@ export const makeChartGroups = (flowChart) => {
makeDataset('Давление', 'Давл','#FF0000', 'pressure', 'атм'),
makeDataset('Давление заданное', 'Давл зад-е','#CC0000', 'pressureSp', 'атм'),
makeDataset('Давление ХХ', 'Давл ХХ','#CC4429', 'pressureIdle', 'атм', { dash }),
makeDataset('Перепад давления максимальный', 'ΔР макс','#B34A36', 'pressureDeltaLimitMax', 'атм', { dash }),
makeDataset('Перепад давления МАКС', 'ΔР макс','#B34A36', 'pressureDeltaLimitMax', 'атм', { dash }),
makeDataset('Давление', 'Давл','#FF0000', 'pressureMM', 'атм', makeAreaOptions('pressure')),
], [
makeDataset('Осевая нагрузка', 'Нагр','#0000CC', 'axialLoad', 'т'),
makeDataset('Осевая нагрузка заданная', 'Нагр зад-я','#3D6DCC', 'axialLoadSp', 'т', { dash }),
makeDataset('Осевая нагрузка максимальная', 'Нагр макс','#3D3DCC', 'axialLoadLimitMax', 'т', { dash }),
makeDataset('Осевая нагрузка МАКС', 'Нагр макс','#3D3DCC', 'axialLoadLimitMax', 'т', { dash }),
makeDataset('Осевая нагрузка', 'Нагр','#0000CC', 'axialLoadMM', 'т', makeAreaOptions('axialLoad')),
], [
makeDataset('Вес на крюке', 'Вес на крюке','#00B3B3', 'hookWeight', 'т'),
makeDataset('Вес инструмента ХХ', 'Вес инст ХХ','#29CCB1', 'hookWeightIdle', 'т', { dash }),
makeDataset('Вес инструмента минимальный', 'Вес инст мин','#47A1B3', 'hookWeightLimitMin', 'т', { dash }),
makeDataset('Вес инструмента максимальный', 'Вес инст мах','#2D7280', 'hookWeightLimitMax', 'т', { dash }),
makeDataset('Вес инструмента МИН', 'Вес инст мин','#47A1B3', 'hookWeightLimitMin', 'т', { dash }),
makeDataset('Вес инструмента МАКС', 'Вес инст мах','#2D7280', 'hookWeightLimitMax', 'т', { dash }),
makeDataset('Обороты ротора', 'Об ротора','#11B32F', 'rotorSpeed', 'об/мин'),
makeDataset('Обороты ротора', 'Об ротора','#11B32F', 'rotorSpeedMM', 'об/мин', makeAreaOptions('rotorSpeed')),
], [
makeDataset('Момент на роторе', 'Момент','#990099', 'rotorTorque', 'кН·м'),
makeDataset('План. Момент на роторе', 'Момент зад-й','#9629CC', 'rotorTorqueSp', 'кН·м', { dash }),
makeDataset('Момент на роторе х.х.', 'Момень ХХ','#CC2996', 'rotorTorqueIdle', 'кН·м', { dash }),
makeDataset('Момент максимальный', 'Момент макс','#FF00FF', 'rotorTorqueLimitMax', 'кН·м', { dash }),
makeDataset('Момент на роторе ХХ', 'Момент ХХ','#CC2996', 'rotorTorqueIdle', 'кН·м', { dash }),
makeDataset('Момент МАКС.', 'Момент макс','#FF00FF', 'rotorTorqueLimitMax', 'кН·м', { dash }),
makeDataset('Момент на роторе', 'Момент','#990099', 'rotorTorqueMM', 'кН·м', makeAreaOptions('rotorTorque')),
]
]
@ -253,14 +254,14 @@ const TelemetryView = memo(() => {
<LoaderPortal show={showLoader}>
<Grid style={{ gridTemplateColumns: 'auto repeat(6, 1fr)' }}>
<GridItem col={'1'} row={'1'} colSpan={'8'} style={{ marginBottom: '0.5rem' }}>
<Flex>
<div className={'page-top'}>
<ModeDisplay data={dataSaub} />
<div style={{ marginLeft: '1rem' }}>
<div>
Интервал:&nbsp;
<PeriodPicker onChange={setChartInterval} />
</div>
<Button onClick={() => chartMethods?.setSettingsVisible(true)}>Настроить графики</Button>
<div style={{ marginLeft: '1rem' }}>
<div>
Статус:&nbsp;
<Select value={well.idState ?? 0} onChange={onStatusChanged} disabled={!hasPermission('Well.edit')}>
<Option value={0} disabled hidden>Неизвестно</Option>
@ -268,14 +269,16 @@ const TelemetryView = memo(() => {
<Option value={2}>Завершено</Option>
</Select>
</div>
<Setpoints style={{ marginLeft: '1rem' }} />
<Setpoints />
<span style={{ flexGrow: 20 }}>&nbsp;</span>
<WirelineRunOut />
<img src={isTorqueStabEnabled(dataSpin) ? MomentStabPicEnabled : MomentStabPicDisabled} style={{ marginRight: '15px' }} alt={'TorqueMaster'} />
<img src={isSpinEnabled(dataSpin) ? SpinPicEnabled : SpinPicDisabled} style={{ marginRight: '15px' }} alt={'SpinMaster'} />
<h2 style={{ marginBottom: 0, marginRight: '15px', fontWeight: 'bold', color: isMseEnabled(dataSaub) ? 'green' : 'lightgrey' }}>MSE</h2>
<div className={'icons'}>
<img src={isTorqueStabEnabled(dataSpin) ? MomentStabPicEnabled : MomentStabPicDisabled} alt={'TorqueMaster'} />
<img src={isSpinEnabled(dataSpin) ? SpinPicEnabled : SpinPicDisabled} alt={'SpinMaster'} />
<h2 style={{ marginBottom: 0, fontWeight: 'bold', color: isMseEnabled(dataSaub) ? 'green' : 'lightgrey' }}>MSE</h2>
</div>
<UserOfWell data={dataSaub} />
</Flex>
</div>
</GridItem>
<GridItem col={'1'} row={'2'} rowSpan={'3'} style={{ minWidth: '260px', width: '0.142fr' }}>
<CustomColumn data={dataSaub} />

View File

@ -17,6 +17,7 @@ import { WellService } from '@api'
import Measure from './Measure'
import Reports from './Reports'
import WellCase from './WellCase'
import Analytics from './Analytics'
import Documents from './Documents'
import Telemetry from './Telemetry'
@ -68,6 +69,7 @@ const Well = memo(() => {
<PrivateMenu.Link content={Documents} icon={<FolderOutlined />} />
<PrivateMenu.Link content={Measure} icon={<ExperimentOutlined />} />
<PrivateMenu.Link content={DrillingProgram} icon={<FolderOutlined />} />
<PrivateMenu.Link content={WellCase} icon={<FolderOutlined />} />
</PrivateMenu>
<WellContext.Provider value={[well, updateWell]}>
@ -84,6 +86,7 @@ const Well = memo(() => {
<Route path={Documents.route} element={<Documents />} />
<Route path={Measure.route} element={<Measure />} />
<Route path={DrillingProgram.route} element={<DrillingProgram />} />
<Route path={WellCase.route} element={<WellCase />} />
</Routes>
</Content>
</Layout>

View 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

View 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

View 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'],
})

View File

@ -6,7 +6,7 @@ import { ErrorFetch } from '@components/ErrorFetch'
import { UploadForm } from '@components/UploadForm'
const errorTextStyle = { color: 'red', fontWeight: 'bold' }
const uploadFormStyle = { marginTop: '24px' }
const uploadFormStyle = { marginTop: 24 }
export const ImportOperations = memo(({ well: givenWell, onDone }) => {
const [deleteBeforeImport, setDeleteBeforeImport] = useState(false)
@ -15,7 +15,7 @@ export const ImportOperations = memo(({ well: givenWell, onDone }) => {
const [wellContext] = useWell()
const well = useMemo(() => givenWell ?? wellContext, [givenWell, wellContext])
const url = useMemo(() => `/api/well/${well.id}/wellOperations/import${deleteBeforeImport ? '/1' : '/0'}`, [well])
const url = useMemo(() => `/api/well/${well.id}/wellOperations/import/${deleteBeforeImport ? 1 : 0}`, [well, deleteBeforeImport])
const onUploadSuccess = useCallback(() => {
setErrorText('')
@ -34,6 +34,7 @@ export const ImportOperations = memo(({ well: givenWell, onDone }) => {
<span>Очистить список операций перед импортом&nbsp;</span>
<Switch onChange={setDeleteBeforeImport} checked={deleteBeforeImport} />
<UploadForm
multiple
url={url}
style={uploadFormStyle}
onUploadSuccess={onUploadSuccess}

View File

@ -1,17 +1,87 @@
import { useState, useEffect, useCallback, memo, useMemo } from 'react'
import { InputNumber } from 'antd'
import { useWell } from '@asb/context'
import {
EditableTable,
makeSelectColumn,
makeNumericAvgRange,
makeGroupColumn,
makeNumericRender,
makeNumericSorter,
RegExpIsFloat,
} from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { DrillParamsService, WellOperationService } from '@api'
import { arrayOrDefault } from '@utils'
import { makeNumericObjSorter } from '@components/Table/sorters'
const makeNumericObjRender = (fixed, columnKey) => (value, obj) => {
let val = '-'
const isSelected = obj && columnKey && obj[columnKey[0]] ? obj[columnKey[0]][columnKey[1]] : false
if ((value ?? null) !== null && Number.isFinite(+value)) {
val = (fixed ?? null) !== null
? (+value).toFixed(fixed)
: (+value).toPrecision(5)
}
return (
<div className={`text-align-r-container ${isSelected ? 'color-pale-green' : ''}`}>
<span>{val}</span>
</div>
)
}
const makeNumericColumnOptionsWithColor = (fixed, sorterKey, columnKey) => ({
editable: true,
initialValue: 0,
width: 100,
sorter: sorterKey ? makeNumericObjSorter(sorterKey) : undefined,
formItemRules: [{
required: true,
message: 'Введите число',
pattern: RegExpIsFloat,
}],
render: makeNumericObjRender(fixed, columnKey),
})
const makeNumericObjColumn = (
title,
dataIndex,
filters,
filterDelegate,
renderDelegate,
width,
other
) => ({
title: title,
dataIndex: dataIndex,
key: dataIndex,
filters: filters,
onFilter: filterDelegate ? filterDelegate(dataIndex) : null,
sorter: makeNumericObjSorter(dataIndex),
width: width,
input: <InputNumber style={{ width: '100%' }}/>,
render: renderDelegate ?? makeNumericRender(),
align: 'right',
...other
})
const makeNumericAvgRange = (
title,
dataIndex,
fixed,
filters,
filterDelegate,
renderDelegate,
width
) => makeGroupColumn(title, [
makeNumericObjColumn('мин', [dataIndex, 'min'], filters, filterDelegate, renderDelegate, width, makeNumericColumnOptionsWithColor(fixed, [dataIndex, 'min'], [dataIndex, 'isMin'])),
makeNumericObjColumn('сред', [dataIndex, 'avg'], filters, filterDelegate, renderDelegate, width, makeNumericColumnOptionsWithColor(fixed, [dataIndex, 'avg'])),
makeNumericObjColumn('макс', [dataIndex, 'max'], filters, filterDelegate, renderDelegate, width, makeNumericColumnOptionsWithColor(fixed, [dataIndex, 'max'], [dataIndex, 'isMax']))
])
export const getColumns = async (idWell) => {
let sectionTypes = await WellOperationService.getSectionTypes(idWell)

View File

@ -74,7 +74,7 @@ html {
.header .title{
flex-grow: 1;
color: #fff;
padding-left: 450px;
padding-left: calc(100vw / 2 - 400px);
}
.header button{

View File

@ -1,7 +1,11 @@
.detected-operations-page {
& .page-top {
width: 100%;
margin: 10px;
margin: 5px;
& > * {
margin: 5px;
}
}
}

View File

@ -142,3 +142,15 @@ code {
height: 32px;
padding: 4px 15px;
}
.ant-table-cell:has(.color-pale-green) {
background-color: #98fb98;
}
.ant-table-tbody > tr > td.ant-table-cell-row-hover:has( > div.color-pale-green) {
background: #98fb98;
}
.color-pale-green {
background-color: #98fb98;
}

View 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
View 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;
}
}
}
}

View File

@ -1,9 +1,9 @@
import { BarChartOutlined, LineChartOutlined, DotChartOutlined, AreaChartOutlined, BorderOuterOutlined, } from '@ant-design/icons'
import { AntdIconProps } from '@ant-design/icons/lib/components/AntdIcon'
import { ChartDataset } from '@components/d3'
import { BaseDataType, ChartDataset } from '@components/d3'
export const makePointsOptimizator = <DataType extends Record<string, unknown>>(isEquals: (a: DataType, b: DataType) => boolean) => (points: DataType[]) => {
export const makePointsOptimizator = <DataType extends BaseDataType>(isEquals: (a: DataType, b: DataType) => boolean) => (points: DataType[]) => {
if (!Array.isArray(points) || points.length < 3) return points
const out: DataType[] = []
@ -23,7 +23,7 @@ export const getDistance = (x1: number, y1: number, x2: number, y2: number, type
return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2))
}
export const getChartIcon = <DataType,>(chart: ChartDataset<DataType>, options?: Omit<AntdIconProps, 'ref'>) => {
export const getChartIcon = <DataType extends BaseDataType>(chart: ChartDataset<DataType>, options?: Omit<AntdIconProps, 'ref'>) => {
let Icon
switch (chart.type) {
case 'needle': Icon = BarChartOutlined; break

View File

@ -11,3 +11,9 @@ export const mainFrameSize = () => ({
})
export const isDev = () => process.env.NODE_ENV === 'development'
/**
* Используется в асинхронных функциях для создания задержки
* @param ms время задержки в мс
*/
export const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))