Закончена страница Определённых операций

This commit is contained in:
goodmice 2022-05-05 17:26:52 +05:00
parent fb82815cbc
commit 71f801751c
10 changed files with 1395 additions and 20 deletions

1227
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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

View 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

View 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

View File

@ -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>
) )
}) })

View File

@ -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>

View File

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

View File

@ -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 Тип, передаваемый полностью

View File

@ -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: {