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-zoom": "^1.1.1",
|
||||
"craco-less": "^1.17.1",
|
||||
"d3": "^7.4.4",
|
||||
"moment": "^2.29.1",
|
||||
"pigeon-maps": "^0.19.7",
|
||||
"react": "^17.0.2",
|
||||
@ -56,6 +57,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/d3": "^7.1.0",
|
||||
"@types/react": "^17.0.3",
|
||||
"@types/react-router-dom": "^5.3.2",
|
||||
"craco-alias": "^3.0.1",
|
||||
|
@ -8,6 +8,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { DatePickerWrapper, makeDateSorter } from '@components/Table'
|
||||
import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
|
||||
import { TelemetryDataSaubService } from '@api'
|
||||
import { range } from '@utils'
|
||||
|
||||
import { normalizeData } from '../TelemetryView'
|
||||
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(() => {
|
||||
const [dataSaub, setDataSaub] = useState([])
|
||||
const [dateLimit, setDateLimit] = useState({ from: 0, to: new Date() })
|
||||
@ -91,17 +85,17 @@ export const Archive = memo(() => {
|
||||
}, [dateLimit])
|
||||
|
||||
const isDateTimeDisabled = useCallback((date) => ({
|
||||
disabledHours: () => range(0, 24).filter(h => {
|
||||
disabledHours: () => range(24).filter(h => {
|
||||
if (!date) return false
|
||||
const dt = +new Date(date).setHours(h)
|
||||
return dt < dateLimit.from || dt > +dateLimit.to - chartInterval
|
||||
}),
|
||||
disabledMinutes: () => range(0, 60).filter(m => {
|
||||
disabledMinutes: () => range(60).filter(m => {
|
||||
if (!date) return false
|
||||
const dt = +new Date(date).setMinutes(m)
|
||||
return dt < dateLimit.from || dt > +dateLimit.to - chartInterval
|
||||
}),
|
||||
disabledSeconds: () => range(0, 60).filter(s => {
|
||||
disabledSeconds: () => range(60).filter(s => {
|
||||
if (!date) return false
|
||||
const dt = +new Date(date).setSeconds(s)
|
||||
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 LoaderPortal from '@components/LoaderPortal'
|
||||
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(() => {
|
||||
const idWell = useContext(IdWellContext)
|
||||
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(
|
||||
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,
|
||||
'Не удалось загрузить список определённых операций',
|
||||
'Получение списка определённых операций',
|
||||
))
|
||||
), [idWell, dates])
|
||||
|
||||
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}>
|
||||
|
||||
<div className={'page-main'}>
|
||||
<OperationsChart data={data} yDomain={yDomain} height={'50vh'} />
|
||||
<OperationsTable data={data} height={'20vh'} />
|
||||
</div>
|
||||
</LoaderPortal>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -12,6 +12,7 @@ import DashboardNNB from './DashboardNNB'
|
||||
import TelemetryView from './TelemetryView'
|
||||
|
||||
import '@styles/index.css'
|
||||
import Operations from './Operations'
|
||||
|
||||
const { Content } = Layout
|
||||
|
||||
@ -28,6 +29,7 @@ export const Telemetry = memo(() => {
|
||||
<PrivateMenu.Link key={'messages'} icon={<AlertOutlined/>} title={'Сообщения'} />
|
||||
<PrivateMenu.Link key={'archive'} icon={<DatabaseOutlined />} title={'Архив'} />
|
||||
<PrivateMenu.Link key={'dashboard_nnb'} title={'ННБ'} />
|
||||
<PrivateMenu.Link key={'operations'} title={'Отмеченные операции'} />
|
||||
</PrivateMenu>
|
||||
|
||||
<Layout>
|
||||
@ -37,6 +39,7 @@ export const Telemetry = memo(() => {
|
||||
<Messages key={'messages'} />
|
||||
<Archive key={'archive'} />
|
||||
<DashboardNNB key={'dashboard_nnb/:tab?'} />
|
||||
<Operations key={'operations'}/>
|
||||
</PrivateSwitch>
|
||||
</Content>
|
||||
</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> =>
|
||||
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 Тип, передаваемый полностью
|
||||
|
@ -97,6 +97,7 @@ export const requirements: PermissionRecord = {
|
||||
monitoring: ['Deposit.get', 'DrillFlowChart.get', 'TelemetryDataSaub.get', 'TelemetryDataSpin.get'],
|
||||
messages: ['Deposit.get', 'TelemetryDataSaub.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'],
|
||||
analytics: {
|
||||
|
Loading…
Reference in New Issue
Block a user