forked from ddrilling/asb_cloud_front
Закончена страница Определённых операций
This commit is contained in:
parent
fb82815cbc
commit
71f801751c
1227
package-lock.json
generated
1227
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -15,6 +15,7 @@
|
|||||||
"chartjs-plugin-datalabels": "^2.0.0-rc.1",
|
"chartjs-plugin-datalabels": "^2.0.0-rc.1",
|
||||||
"chartjs-plugin-zoom": "^1.1.1",
|
"chartjs-plugin-zoom": "^1.1.1",
|
||||||
"craco-less": "^1.17.1",
|
"craco-less": "^1.17.1",
|
||||||
|
"d3": "^7.4.4",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"pigeon-maps": "^0.19.7",
|
"pigeon-maps": "^0.19.7",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
@ -56,6 +57,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/d3": "^7.1.0",
|
||||||
"@types/react": "^17.0.3",
|
"@types/react": "^17.0.3",
|
||||||
"@types/react-router-dom": "^5.3.2",
|
"@types/react-router-dom": "^5.3.2",
|
||||||
"craco-alias": "^3.0.1",
|
"craco-alias": "^3.0.1",
|
||||||
|
@ -8,6 +8,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory'
|
|||||||
import { DatePickerWrapper, makeDateSorter } from '@components/Table'
|
import { DatePickerWrapper, makeDateSorter } from '@components/Table'
|
||||||
import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
|
import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
|
||||||
import { TelemetryDataSaubService } from '@api'
|
import { TelemetryDataSaubService } from '@api'
|
||||||
|
import { range } from '@utils'
|
||||||
|
|
||||||
import { normalizeData } from '../TelemetryView'
|
import { normalizeData } from '../TelemetryView'
|
||||||
import { ArchiveDisplay, cutData } from './ArchiveDisplay'
|
import { ArchiveDisplay, cutData } from './ArchiveDisplay'
|
||||||
@ -55,13 +56,6 @@ const getLoadingInterval = (loaded, startDate, interval) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const range = (start, end) => {
|
|
||||||
const result = []
|
|
||||||
for (let i = start; i < end; i++)
|
|
||||||
result.push(i)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Archive = memo(() => {
|
export const Archive = memo(() => {
|
||||||
const [dataSaub, setDataSaub] = useState([])
|
const [dataSaub, setDataSaub] = useState([])
|
||||||
const [dateLimit, setDateLimit] = useState({ from: 0, to: new Date() })
|
const [dateLimit, setDateLimit] = useState({ from: 0, to: new Date() })
|
||||||
@ -91,17 +85,17 @@ export const Archive = memo(() => {
|
|||||||
}, [dateLimit])
|
}, [dateLimit])
|
||||||
|
|
||||||
const isDateTimeDisabled = useCallback((date) => ({
|
const isDateTimeDisabled = useCallback((date) => ({
|
||||||
disabledHours: () => range(0, 24).filter(h => {
|
disabledHours: () => range(24).filter(h => {
|
||||||
if (!date) return false
|
if (!date) return false
|
||||||
const dt = +new Date(date).setHours(h)
|
const dt = +new Date(date).setHours(h)
|
||||||
return dt < dateLimit.from || dt > +dateLimit.to - chartInterval
|
return dt < dateLimit.from || dt > +dateLimit.to - chartInterval
|
||||||
}),
|
}),
|
||||||
disabledMinutes: () => range(0, 60).filter(m => {
|
disabledMinutes: () => range(60).filter(m => {
|
||||||
if (!date) return false
|
if (!date) return false
|
||||||
const dt = +new Date(date).setMinutes(m)
|
const dt = +new Date(date).setMinutes(m)
|
||||||
return dt < dateLimit.from || dt > +dateLimit.to - chartInterval
|
return dt < dateLimit.from || dt > +dateLimit.to - chartInterval
|
||||||
}),
|
}),
|
||||||
disabledSeconds: () => range(0, 60).filter(s => {
|
disabledSeconds: () => range(60).filter(s => {
|
||||||
if (!date) return false
|
if (!date) return false
|
||||||
const dt = +new Date(date).setSeconds(s)
|
const dt = +new Date(date).setSeconds(s)
|
||||||
return dt < dateLimit.from || dt > +dateLimit.to - chartInterval
|
return dt < dateLimit.from || dt > +dateLimit.to - chartInterval
|
||||||
|
52
src/pages/Telemetry/Operations/OperationsChart.jsx
Normal file
52
src/pages/Telemetry/Operations/OperationsChart.jsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { memo, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import * as d3 from 'd3'
|
||||||
|
|
||||||
|
import '@styles/detected_operations.less'
|
||||||
|
|
||||||
|
export const OperationsChart = memo(({ data, yDomain, width, height, bottom = 30, left = 50, top = 10, right = 10, color = '#00F' }) => {
|
||||||
|
const [ref, setRef] = useState(null)
|
||||||
|
const axisX = useRef(null)
|
||||||
|
const axisY = useRef(null)
|
||||||
|
const w = useMemo(() => Number.isFinite(+width) ? +width : ref?.offsetWidth, [width, ref])
|
||||||
|
const h = useMemo(() => Number.isFinite(+height) ? +height : ref?.offsetHeight, [height, ref])
|
||||||
|
|
||||||
|
const d = useMemo(() => data.map((row) => ({
|
||||||
|
date: new Date(row.dateStart),
|
||||||
|
value: row.durationMinutes,
|
||||||
|
})), [data]) // Нормализуем данные для графика
|
||||||
|
|
||||||
|
const x = useMemo(() => d3
|
||||||
|
.scaleTime()
|
||||||
|
.range([0, w - left - right])
|
||||||
|
.domain([d3.min(d, d => d.date), d3.max(d, d => d.date)])
|
||||||
|
, [w, d, left, right]) // Создаём ось X
|
||||||
|
|
||||||
|
const y = useMemo(() => d3
|
||||||
|
.scaleLinear()
|
||||||
|
.range([h - bottom - top, 0])
|
||||||
|
.domain([0, yDomain ?? d3.max(d, d => d.value)])
|
||||||
|
, [h, d, top, bottom, yDomain]) // Создаём ось Y
|
||||||
|
|
||||||
|
const lines = useMemo(() => d.map(d => ({ x: x(d.date), y: y(d.value) })), [d, x, y]) // Получаем массив координат линий
|
||||||
|
|
||||||
|
useEffect(() => d3.select(axisX.current).call(d3.axisBottom(x)), [axisX, x]) // Рисуем ось X
|
||||||
|
useEffect(() => d3.select(axisY.current).call(d3.axisLeft(y)), [axisY, y]) // Рисуем ось Y
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={'page-left'} ref={setRef}>
|
||||||
|
<svg width={width ?? '100%'} height={height ?? '100%'}>
|
||||||
|
<g ref={axisX} className={'axis x'} transform={`translate(${left}, ${h - bottom})`} />
|
||||||
|
<g ref={axisY} className={'axis y'} transform={`translate(${left}, ${top})`} />
|
||||||
|
<g transform={`translate(${left}, ${top})`} stroke={color}>
|
||||||
|
{lines.map(({ x, y }, i) => (
|
||||||
|
<line key={i} x1={x} y1={h - bottom - top} x2={x} y2={y} />
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default OperationsChart
|
30
src/pages/Telemetry/Operations/OperationsTable.jsx
Normal file
30
src/pages/Telemetry/Operations/OperationsTable.jsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
|
||||||
|
import { Table, makeTextColumn, makeNumericColumn, makeDateColumn } from '@components/Table'
|
||||||
|
|
||||||
|
import '@styles/detected_operations.less'
|
||||||
|
|
||||||
|
export const columns = [
|
||||||
|
makeTextColumn('Пользователь', 'telemetryUserName', null, null, null, { width: 200 }),
|
||||||
|
makeDateColumn('Дата начала', 'dateStart', null, undefined, { width: 150 }),
|
||||||
|
makeNumericColumn('Продолжительность (мин)', 'durationMinutes', null, null, null, 150),
|
||||||
|
makeNumericColumn('Глубина (м)', 'depthStart', null, null, null, 100),
|
||||||
|
]
|
||||||
|
|
||||||
|
export const OperationsTable = memo(({ data, height, ...other }) => (
|
||||||
|
<div className={'page-right'}>
|
||||||
|
<Table
|
||||||
|
bordered
|
||||||
|
size={'small'}
|
||||||
|
pagination={false}
|
||||||
|
{...other}
|
||||||
|
sticky={true}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={data}
|
||||||
|
tableName={'well_telemetry_detected_operations'}
|
||||||
|
scroll={{ y: height ?? '70vh', scrollToFirstRowOnChange: true }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
|
||||||
|
export default OperationsTable
|
@ -1,27 +1,90 @@
|
|||||||
import { memo, useContext, useEffect, useState } from 'react'
|
import moment from 'moment'
|
||||||
|
import { InputNumber } from 'antd'
|
||||||
|
import { memo, useCallback, useContext, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { IdWellContext } from '@asb/context'
|
import { IdWellContext } from '@asb/context'
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
import DateRangeWrapper from '@components/Table/DateRangeWrapper'
|
||||||
|
import { DetectedOperationService, TelemetryDataSaubService } from '@api'
|
||||||
|
import { range } from '@utils'
|
||||||
|
|
||||||
|
import OperationsChart from './OperationsChart'
|
||||||
|
import OperationsTable from './OperationsTable'
|
||||||
|
|
||||||
|
import '@styles/detected_operations.less'
|
||||||
|
|
||||||
export const Operations = memo(() => {
|
export const Operations = memo(() => {
|
||||||
const idWell = useContext(IdWellContext)
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [dateRange, setDateRange] = useState([])
|
||||||
|
const [yDomain, setYDomain] = useState(10)
|
||||||
|
const [dates, setDates] = useState()
|
||||||
|
const [data, setData] = useState([])
|
||||||
|
|
||||||
|
const idWell = useContext(IdWellContext)
|
||||||
|
|
||||||
|
const disabledDates = useCallback((current) => current && !moment(current).isBetween(...dateRange, 'day', '[]'), [dateRange])
|
||||||
|
|
||||||
|
const disabledTimes = useCallback((date) => ({
|
||||||
|
disabledHours: () => range(24).filter(h => date && !moment(date).hours(h).isBetween(...dateRange, 'hour', '[]')),
|
||||||
|
disabledMinutes: () => range(60).filter(m => date && !moment(date).minutes(m).isBetween(...dateRange, 'minute', '[]')),
|
||||||
|
disabledSeconds: () => range(60).filter(s => date && !moment(date).seconds(s).isBetween(...dateRange, 'second', '[]'))
|
||||||
|
}), [dateRange])
|
||||||
|
|
||||||
useEffect(() => invokeWebApiWrapperAsync(
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
//
|
const dates = await TelemetryDataSaubService.getDataDatesRange(idWell)
|
||||||
|
if (dates) {
|
||||||
|
const dt = [moment(dates.from), moment(dates.to)]
|
||||||
|
setDateRange(dt)
|
||||||
|
setDates(dt)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setIsLoading,
|
||||||
|
'Не удалось загрузить диапазон доступных дат',
|
||||||
|
'Получение дапазона доступних дат',
|
||||||
|
), [idWell])
|
||||||
|
|
||||||
|
useEffect(() => invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
if (!dates) return
|
||||||
|
const data = await DetectedOperationService.get(idWell, undefined, dates[0].toISOString(), dates[1].toISOString())
|
||||||
|
setData(data)
|
||||||
},
|
},
|
||||||
setIsLoading,
|
setIsLoading,
|
||||||
'Не удалось загрузить список определённых операций',
|
'Не удалось загрузить список определённых операций',
|
||||||
'Получение списка определённых операций',
|
'Получение списка определённых операций',
|
||||||
))
|
), [idWell, dates])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className={'container detected-operations-page'}>
|
||||||
|
<div className={'page-top'}>
|
||||||
|
<DateRangeWrapper
|
||||||
|
value={dates}
|
||||||
|
onChange={setDates}
|
||||||
|
disabledDate={disabledDates}
|
||||||
|
disabledTime={disabledTimes}
|
||||||
|
style={{ marginRight: '10px' }}
|
||||||
|
showTime={{ hideDisabledOptions: true }}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
<InputNumber
|
||||||
|
min={1}
|
||||||
|
max={2*24*60}
|
||||||
|
value={yDomain}
|
||||||
|
onChange={setYDomain}
|
||||||
|
addonAfter={'мин'}
|
||||||
|
addonBefore={'Верхняя граница'}
|
||||||
|
style={{ marginRight: '10px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<LoaderPortal show={isLoading}>
|
<LoaderPortal show={isLoading}>
|
||||||
|
<div className={'page-main'}>
|
||||||
|
<OperationsChart data={data} yDomain={yDomain} height={'50vh'} />
|
||||||
|
<OperationsTable data={data} height={'20vh'} />
|
||||||
|
</div>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import DashboardNNB from './DashboardNNB'
|
|||||||
import TelemetryView from './TelemetryView'
|
import TelemetryView from './TelemetryView'
|
||||||
|
|
||||||
import '@styles/index.css'
|
import '@styles/index.css'
|
||||||
|
import Operations from './Operations'
|
||||||
|
|
||||||
const { Content } = Layout
|
const { Content } = Layout
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ export const Telemetry = memo(() => {
|
|||||||
<PrivateMenu.Link key={'messages'} icon={<AlertOutlined/>} title={'Сообщения'} />
|
<PrivateMenu.Link key={'messages'} icon={<AlertOutlined/>} title={'Сообщения'} />
|
||||||
<PrivateMenu.Link key={'archive'} icon={<DatabaseOutlined />} title={'Архив'} />
|
<PrivateMenu.Link key={'archive'} icon={<DatabaseOutlined />} title={'Архив'} />
|
||||||
<PrivateMenu.Link key={'dashboard_nnb'} title={'ННБ'} />
|
<PrivateMenu.Link key={'dashboard_nnb'} title={'ННБ'} />
|
||||||
|
<PrivateMenu.Link key={'operations'} title={'Отмеченные операции'} />
|
||||||
</PrivateMenu>
|
</PrivateMenu>
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
@ -37,6 +39,7 @@ export const Telemetry = memo(() => {
|
|||||||
<Messages key={'messages'} />
|
<Messages key={'messages'} />
|
||||||
<Archive key={'archive'} />
|
<Archive key={'archive'} />
|
||||||
<DashboardNNB key={'dashboard_nnb/:tab?'} />
|
<DashboardNNB key={'dashboard_nnb/:tab?'} />
|
||||||
|
<Operations key={'operations'}/>
|
||||||
</PrivateSwitch>
|
</PrivateSwitch>
|
||||||
</Content>
|
</Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
7
src/styles/detected_operations.less
Normal file
7
src/styles/detected_operations.less
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.detected-operations-page {
|
||||||
|
& .page-top {
|
||||||
|
width: 100%;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -15,6 +15,8 @@ export const deepCopy = <T extends any>(data: T): T => JSON.parse(JSON.stringify
|
|||||||
export const wrapValues = <T, R>(data: Record<string, T>, handler: (data: T, key: string, object: Record<string, T>) => R): Record<string, R> =>
|
export const wrapValues = <T, R>(data: Record<string, T>, handler: (data: T, key: string, object: Record<string, T>) => R): Record<string, R> =>
|
||||||
Object.fromEntries(Object.entries(data).map(([key, value]) => [key, handler(value, key, data)]))
|
Object.fromEntries(Object.entries(data).map(([key, value]) => [key, handler(value, key, data)]))
|
||||||
|
|
||||||
|
export const range = (end: number, start: number = 0) => Array.from({ length: end - start }, (_, i) => start + i)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Объединить типы, исключив совпадающие поля справа
|
* Объединить типы, исключив совпадающие поля справа
|
||||||
* @param T Тип, передаваемый полностью
|
* @param T Тип, передаваемый полностью
|
||||||
|
@ -97,6 +97,7 @@ export const requirements: PermissionRecord = {
|
|||||||
monitoring: ['Deposit.get', 'DrillFlowChart.get', 'TelemetryDataSaub.get', 'TelemetryDataSpin.get'],
|
monitoring: ['Deposit.get', 'DrillFlowChart.get', 'TelemetryDataSaub.get', 'TelemetryDataSpin.get'],
|
||||||
messages: ['Deposit.get', 'TelemetryDataSaub.get'],
|
messages: ['Deposit.get', 'TelemetryDataSaub.get'],
|
||||||
dashboard_nnb: ['Deposit.get'], //'WitsInfo.get', 'WitsRecord1.get', 'WitsRecord7.get', 'WitsRecord8.get', 'WitsRecord50.get', 'WitsRecord60.get', 'WitsRecord61.get'],
|
dashboard_nnb: ['Deposit.get'], //'WitsInfo.get', 'WitsRecord1.get', 'WitsRecord7.get', 'WitsRecord8.get', 'WitsRecord50.get', 'WitsRecord60.get', 'WitsRecord61.get'],
|
||||||
|
operations: ['Deposit.get'], //'TelemetryDataSaubService.get', 'DetectedOperationService.get'],
|
||||||
},
|
},
|
||||||
report: ['Deposit.get', 'Report.get'],
|
report: ['Deposit.get', 'Report.get'],
|
||||||
analytics: {
|
analytics: {
|
||||||
|
Loading…
Reference in New Issue
Block a user