diff --git a/src/components/Display.tsx b/src/components/Display.tsx deleted file mode 100644 index a423bab..0000000 --- a/src/components/Display.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import moment from 'moment' -import { useState, useEffect, memo, ReactNode } from 'react' -import {CaretUpOutlined, CaretDownOutlined, CaretRightOutlined} from '@ant-design/icons' - -import '@styles/display.less' - -export const formatNumber = (value?: unknown, format?: number) => - Number.isInteger(format) && Number.isFinite(value) - ? Number(value).toFixed(format) - : Number(value).toPrecision(4) - -const iconStyle = { color:'#0008' } -const displayValueStyle = { display: 'flex', flexGrow: 1 } - -export type ValueDisplayProps = { - prefix?: ReactNode - suffix?: ReactNode - format?: number | string | ((arg: string) => ReactNode) - isArrowVisible?: boolean - enumeration?: Record - value: string -} - -export type DisplayProps = ValueDisplayProps & { - className?: string - label?: ReactNode -} - -export const ValueDisplay = memo(({ prefix, value, suffix, isArrowVisible, format, enumeration }) => { - const [val, setVal] = useState('---') - const [arrowState, setArrowState] = useState({ - preVal: NaN, - preTimestamp: Date.now(), - direction: 0, - }) - - useEffect(() => { - setVal((preVal) => { - if ((value ?? '-') === '-' || value === '--') return '---' - if (typeof format === 'function') return format(enumeration?.[value] ?? value) - if (enumeration?.[value]) return enumeration[value] - - if (Number.isFinite(+value)) { - if (isArrowVisible && (arrowState.preTimestamp + 1000 < Date.now())) { - let direction = 0 - if (+value > arrowState.preVal) - direction = 1 - if (+value < arrowState.preVal) - direction = -1 - - setArrowState({ - preVal: +value, - preTimestamp: Date.now(), - direction: direction, - }) - } - - return formatNumber(value, Number(format)) - } - - if (value.length > 4) { - const valueDate = moment(value) - if (valueDate.isValid()) - return valueDate.format(String(format)) - } - - return value - }) - },[value, isArrowVisible, arrowState, format, enumeration]) - - let arrow = null - if(isArrowVisible) - switch (arrowState.direction){ - case 0: - arrow = - break - case 1: - arrow = - break - case -1: - arrow = - break - default: - break - } - - return( - - {prefix} {val} {suffix}{arrow} - - ) -}) - -export const Display = memo(({ className, label, ...other })=> ( -
-
{label}
-
- -
-
-)) diff --git a/src/components/d3/monitoring/D3MonitoringCharts.tsx b/src/components/d3/monitoring/D3MonitoringCharts.tsx index bfeb30b..0f6b320 100644 --- a/src/components/d3/monitoring/D3MonitoringCharts.tsx +++ b/src/components/d3/monitoring/D3MonitoringCharts.tsx @@ -73,8 +73,8 @@ export type ChartGroup = { } const defaultOffsets: ChartOffset = { - top: 10, - bottom: 10, + top: 0, + bottom: 0, left: 100, right: 20, } diff --git a/src/pages/Well/Telemetry/TelemetryView/CustomColumn.jsx b/src/pages/Well/Telemetry/TelemetryView/CustomColumn.jsx deleted file mode 100644 index 9bc1cc5..0000000 --- a/src/pages/Well/Telemetry/TelemetryView/CustomColumn.jsx +++ /dev/null @@ -1,58 +0,0 @@ -import moment from 'moment' -import { memo } from 'react' -import { Tooltip, Typography } from 'antd' - -import { Display } from '@components/Display' - -import RigMnemo from './RigMnemo' - -const getTimeFormat = (value) => { - const date = moment(value) - - return ( - - {date.isSame(new Date(), 'day') || ( - {date.format('DD.MM.YYYY')} - )} - {date.format('HH:mm:ss')} - - ) -} - -const params = [ - { label: 'Рот., об/мин', accessorName: 'rotorSpeed', isArrowVisible: true }, - { label: 'Долото, м', accessorName: 'bitDepth', isArrowVisible: true, format: 2 }, - { label: 'Забой, м', accessorName: 'wellDepth', isArrowVisible: true, format: 2 }, - { label: 'Расход, м³/ч', accessorName: 'flow', isArrowVisible: true }, - { label: 'Расход х.х., м³/ч', accessorName: 'flowIdle', isArrowVisible: true }, - { label: 'Время', accessorName: 'date', format: getTimeFormat }, - { label: 'MSE, %', accessorName: 'mse', format: 2 }, -] - -export const CustomColumn = memo(({ data }) => { - const dataLast = data[data.length - 1] - params.forEach(param => param.value = dataLast?.[param.accessorName] ?? '-') - - return ( - <> - {params.map(param => ( - - ))} - - - ) -}) - -export default CustomColumn diff --git a/src/pages/Well/Telemetry/TelemetryView/ModeDisplay.jsx b/src/pages/Well/Telemetry/TelemetryView/ModeDisplay.jsx deleted file mode 100644 index 50ee80e..0000000 --- a/src/pages/Well/Telemetry/TelemetryView/ModeDisplay.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import { memo } from 'react' - -import { Display } from '@components/Display' - -const modeNames = { - 0: 'Ручной', - 1: 'Бурение в роторе', - 2: 'Проработка', - 3: 'Бурение в слайде', - 4: 'Спуск СПО', - 5: 'Подъем СПО', - 6: 'Подъем с проработкой', - - 10: 'БЛОКИРОВКА', -} - -export const ModeDisplay = memo(({ data }) => ( - -)) diff --git a/src/pages/Well/Telemetry/TelemetryView/TelemetrySummary.jsx b/src/pages/Well/Telemetry/TelemetryView/TelemetrySummary.jsx new file mode 100644 index 0000000..f85fd09 --- /dev/null +++ b/src/pages/Well/Telemetry/TelemetryView/TelemetrySummary.jsx @@ -0,0 +1,107 @@ +import { isValidElement, memo, useEffect, useMemo, useState } from 'react' +import { CaretUpOutlined, CaretDownOutlined, CaretRightOutlined } from '@ant-design/icons' +import { Tooltip, Typography } from 'antd' +import moment from 'moment' + +import { formatDate, isRawDate } from '@utils' + +import '@styles/components/data_summary.less' + +export const parseValue = (value, formatter) => { + if (!value || String(value).trim().length <= 0) return '---' + if (typeof formatter === 'function') return formatter(value) + if (isRawDate(value)) return formatDate(value) + const v = +value + if (Number.isFinite(v)) + return Number.isInteger(formatter) ? v.toFixed(formatter) : v.toPrecision(4) + return value +} + +export const DashboardDisplay = memo(({ label, title, unit, iconRenderer, value, format }) => { + const [icon, setIcon] = useState(null) + + const val = useMemo(() => parseValue(value, format), [value, format]) + + useEffect(() => setIcon((prev) => iconRenderer?.(value, prev)), [value]) + + return ( +
+
+ {title ? ( + {label} + ) : ( + {label} + )} + {unit} +
+
+ {val} + {isValidElement(icon) ? icon : icon?.value} +
+
+ ) +}) + +const getTimeFormat = (value) => { + const date = moment(value) + + return ( + + {date.isSame(new Date(), 'day') || ( + {date.format('DD.MM.YYYY')} + )} + {date.format('HH:mm:ss')} + + ) +} + +const iconRenderer = (value, prev) => { + if (!Number.isFinite(+value)) return null + if (prev?.prevDate + 1000 >= Date.now()) return prev + const val = +value + let Component = CaretRightOutlined + if ((prev?.prev ?? null) && val !== prev.prev) + Component = val > prev.prev ? CaretUpOutlined : CaretDownOutlined + + return { + prev: val, + prevDate: Date.now(), + value: , + } +} + +const modeNames = { + 0: 'Ручной', + 1: 'Бурение в роторе', + 2: 'Проработка', + 3: 'Бурение в слайде', + 4: 'Спуск СПО', + 5: 'Подъем СПО', + 6: 'Подъем с проработкой', + + 10: 'БЛОКИРОВКА', +} + +const params = [ + { label: 'Режим', accessorName: 'mode', format: (value) => modeNames[value] || '---' }, + { label: 'Пользователь', accessorName: 'user', title: 'Пользователь панели оператора' }, + { label: 'Рот.', unit: 'об/мин', accessorName: 'rotorSpeed', iconRenderer }, + { label: 'Долото', unit: 'м', accessorName: 'bitDepth', iconRenderer, format: 2 }, + { label: 'Забой', unit: 'м', accessorName: 'wellDepth', iconRenderer, format: 2 }, + { label: 'Расход', unit: 'м³/ч', accessorName: 'flow', iconRenderer }, + { label: 'Расход х.х.', unit: 'м³/ч', accessorName: 'flowIdle', iconRenderer }, + { label: 'Время', accessorName: 'date', format: getTimeFormat }, + { label: 'MSE', unit: '%', accessorName: 'mse', format: 2 }, +] + +export const TelemetrySummary = memo(({ data }) => { + return ( +
+ {params.map((param, i) => ( + + ))} +
+ ) +}) + +export default TelemetrySummary diff --git a/src/pages/Well/Telemetry/TelemetryView/UserOfWells.jsx b/src/pages/Well/Telemetry/TelemetryView/UserOfWells.jsx deleted file mode 100644 index f25217c..0000000 --- a/src/pages/Well/Telemetry/TelemetryView/UserOfWells.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Tooltip } from 'antd' -import { Display } from '@components/Display' - -export const UserOfWell = ({ data }) => ( - Пользователь} - value={data[data.length - 1]?.user} - /> -) - -export default UserOfWell diff --git a/src/pages/Well/Telemetry/TelemetryView/index.jsx b/src/pages/Well/Telemetry/TelemetryView/index.jsx index f996d82..ff43210 100644 --- a/src/pages/Well/Telemetry/TelemetryView/index.jsx +++ b/src/pages/Well/Telemetry/TelemetryView/index.jsx @@ -1,15 +1,15 @@ import { useState, useEffect, useCallback, memo, useMemo } from 'react' import { BehaviorSubject, buffer, throttleTime } from 'rxjs' +import { useSearchParams } from 'react-router-dom' import { Button, Select } from 'antd' import { useWell } from '@asb/context' -import { makeDateSorter } from '@components/Table' +import { DatePickerWrapper, makeDateSorter } from '@components/Table' import { D3MonitoringCharts } from '@components/d3/monitoring' import LoaderPortal from '@components/LoaderPortal' -import { Grid, GridItem } from '@components/Grid' import { invokeWebApiWrapperAsync } from '@components/factory' import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker' -import { formatDate, hasPermission, withPermissions } from '@utils' +import { formatDate, hasPermission, isRawDate, range, withPermissions } from '@utils' import { Subscribe } from '@services/signalr' import { DrillFlowChartService, @@ -20,10 +20,8 @@ import { import { makeChartGroups, yAxis } from './dataset' import ActiveMessagesOnline from './ActiveMessagesOnline' +import TelemetrySummary from './TelemetrySummary' import WirelineRunOut from './WirelineRunOut' -import { CustomColumn } from './CustomColumn' -import { ModeDisplay } from './ModeDisplay' -import { UserOfWell } from './UserOfWells' import { Setpoints } from './Setpoints' import { cursorRender } from './cursorRender' @@ -37,6 +35,14 @@ import '@styles/message.less' const { Option } = Select +const chartProps = { + yAxis, + chartName: 'monitoring', + yTicks: { visible: true, format: (d) => formatDate(d, 'YYYY-MM-DD') }, + plugins: { menu: { enabled: false }, cursor: { render: cursorRender } }, + style: { flexGrow: 1, height: 'auto', width: 'auto' }, +} + const getLast = (data) => Array.isArray(data) ? data.at(-1) : data const isMseEnabled = (dataSaub) => (getLast(dataSaub)?.mseState && 2) > 0 @@ -54,6 +60,7 @@ export const normalizeData = (data) => data?.map(item => ({ })) ?? [] const dateSorter = makeDateSorter('date') +const defaultDate = () => Date.now() - defaultPeriod * 1000 const makeSubjectSubsription = (subject$, handler) => { const subscribtion = subject$.pipe( @@ -64,20 +71,49 @@ const makeSubjectSubsription = (subject$, handler) => { } const TelemetryView = memo(() => { + const [well, updateWell] = useWell() + const [searchParams, setSearchParams] = useSearchParams() + const [currentWellId, setCurrentWellId] = useState(null) const [dataSaub, setDataSaub] = useState([]) const [dataSpin, setDataSpin] = useState([]) - const [chartInterval, setChartInterval] = useState(defaultPeriod) const [showLoader, setShowLoader] = useState(false) const [flowChartData, setFlowChartData] = useState([]) const [rop, setRop] = useState(null) const [domain, setDomain] = useState({}) const [chartMethods, setChartMethods] = useState() - const [well, updateWell] = useWell() + const [chartInterval, setChartInterval] = useState(defaultPeriod) + const [startDate, setStartDate] = useState(defaultDate) + const [dateLimit, setDateLimit] = useState({ from: 0, to: new Date() }) - const saubSubject$ = useMemo(() => new BehaviorSubject(), []) - const spinSubject$ = useMemo(() => new BehaviorSubject(), []) + const [archiveMode, setArchiveMode] = useState(false) + + const onStatusChanged = useCallback((value) => updateWell({ idState: value }), [well]) + + const isDateDisabled = useCallback((date) => { + if (!date) return false + const dt = new Date(date).setHours(0, 0, 0, 0) + return dt < dateLimit.from || dt > +dateLimit.to - chartInterval + }, [dateLimit, chartInterval]) + + const isDateTimeDisabled = useCallback((date) => ({ + 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(60).filter(m => { + if (!date) return false + const dt = +new Date(date).setMinutes(m) + return dt < dateLimit.from || dt > +dateLimit.to - chartInterval + }), + disabledSeconds: () => range(60).filter(s => { + if (!date) return false + const dt = +new Date(date).setSeconds(s) + return dt < dateLimit.from || dt > +dateLimit.to - chartInterval + }) + }), [dateLimit, chartInterval]) const handleDataSaub = useCallback((data, replace = false) => { setDataSaub((prev) => { @@ -92,7 +128,21 @@ const TelemetryView = memo(() => { const handleDataSpin = useCallback((data) => data && setDataSpin((prev) => [...prev, ...data]), []) - const onStatusChanged = useCallback((value) => updateWell({ idState: value }), [well]) + const onWheel = useCallback((value) => { + if (!archiveMode && value.deltaY < 0) { + setArchiveMode(true) + // load data + } else { + // move + } + }, [archiveMode]) + + const spinLast = useMemo(() => dataSpin.at(-1), [dataSpin]) + const saubLast = useMemo(() => dataSaub.at(-1), [dataSaub]) + const summaryData = useMemo(() => ({ ...saubLast, ...rop }), [saubLast, rop]) + + const saubSubject$ = useMemo(() => new BehaviorSubject(), []) + const spinSubject$ = useMemo(() => new BehaviorSubject(), []) const filteredData = useMemo(() => { let i, j @@ -112,6 +162,32 @@ const TelemetryView = memo(() => { const chartGroups = useMemo(() => makeChartGroups(flowChartData), [flowChartData]) + useEffect(() => { + setArchiveMode(isRawDate(searchParams.get('start'))) + const interval = parseInt(searchParams.get('range') || defaultPeriod) + const date = new Date(searchParams.get('start') || (Date.now() - interval)) + setChartInterval(interval) + setStartDate(date) + }, [searchParams]) + + useEffect(() => { + if (archiveMode) return + const subscribtion = saubSubject$.pipe( + buffer(saubSubject$.pipe(throttleTime(700))) + ).subscribe((data) => handleDataSaub(data.flat().filter(Boolean))) + + return () => subscribtion.unsubscribe() + }, [saubSubject$, archiveMode]) + + useEffect(() => { + if (archiveMode) return + const subscribtion = spinSubject$.pipe( + buffer(spinSubject$.pipe(throttleTime(700))) + ).subscribe((data) => handleDataSpin(data.flat().filter(Boolean))) + + return () => subscribtion.unsubscribe() + }, [spinSubject$, archiveMode]) + useEffect(() => makeSubjectSubsription(saubSubject$, handleDataSaub), [saubSubject$, handleDataSaub]) useEffect(() => makeSubjectSubsription(spinSubject$, handleDataSpin), [spinSubject$, handleDataSpin]) @@ -148,6 +224,12 @@ const TelemetryView = memo(() => { async () => { const rop = await OperationStatService.getClusterRopStatByIdWell(well.id) setRop(rop) + let dates = await TelemetryDataSaubService.getDataDatesRange(well.id) + dates = { + from: new Date(dates?.from ?? 0), + to: new Date(dates?.to ?? 0) + } + setDateLimit(dates) }, setShowLoader, `Не удалось загрузить данные`, @@ -156,74 +238,66 @@ const TelemetryView = memo(() => { }, [well]) useEffect(() => { - if (dataSaub.length <= 0) return - const last = new Date(dataSaub.at(-1).date) - setDomain({ - min: new Date(+last - chartInterval * 1000), - max: last - }) - }, [dataSaub, chartInterval]) + if (!saubLast || archiveMode) return + const last = new Date(saubLast.date) + const startDate = new Date(+last - chartInterval * 1000) + setStartDate(startDate) + setDomain({ min: startDate, max: last }) + }, [archiveMode, saubLast, chartInterval]) - return ( - - - -
- -
- Интервал:  - -
- -
- Статус:  - -
- -   - -
- {'TorqueMaster'} - {'SpinMaster'} -

MSE

-
- -
-
- - - - - formatDate(d, 'YYYY-MM-DD') - }} - plugins={{ - menu: { enabled: false }, - cursor: { - render: cursorRender, - }, - }} - height={'70vh'} - /> - - - - -
-
- ) + return ( + +
+ +
+
+ Статус:  + +
+ + +
+ {'TorqueMaster'} + {'SpinMaster'} +

MSE

+
+
+
+
+ Начальная дата:  + setStartDate(new Date(startDate))} + /> +
+
+ Интервал:  + +
+ + +
+ + +
+
+ ) }) export default withPermissions(TelemetryView, [ diff --git a/src/styles/components/data_summary.less b/src/styles/components/data_summary.less new file mode 100644 index 0000000..cbb1eac --- /dev/null +++ b/src/styles/components/data_summary.less @@ -0,0 +1,56 @@ +.data-summary { + display: flex; + gap: .5em; + flex-wrap: wrap; + justify-content: flex-start; +} + +.dashboard-display { + display: flex; + flex-direction: column; + align-items: stretch; + border: .067em solid #D9D9D9; + gap: .2em; + border-radius: .14em; + padding: .3em; + min-width: 7.5em; + + & .display-label { + gap: 1.5em; + display: flex; + justify-content: space-between; + font-size: .75em; + line-height: 1em; + color: rgba(0, 0, 0, .3); + } + + & .display-value { + display: flex; + justify-content: flex-end; + gap: .1em; + font-size: 1.3em; + line-height: 1em; + font-weight: bold; + color: rgba(0, 0, 0, .85); + } +} + +@media only screen and (max-width: 1280px) { + .data-summary { + gap: .35em; + } + + .dashboard-display { + font-size: 14px; + } +} + +@media only screen and (max-width: 1150px) { + .data-summary { + gap: .2em; + } + + .dashboard-display { + font-size: 12.5px; + } +} diff --git a/src/styles/display.less b/src/styles/display.less index 85d329f..ed55118 100644 --- a/src/styles/display.less +++ b/src/styles/display.less @@ -1,7 +1,6 @@ .display_flex_container{ display: flex; - flex-wrap: wrap; flex: auto; } .display_header { @@ -38,37 +37,36 @@ } } -.display_label{ - font-size: 16px; - color: rgb(70, 70, 70); - text-align: left; - justify-content: center; - margin: 1px 0 1px 1rem; - flex: auto; - align-items: baseline; - text-overflow: ellipsis; - overflow-x: hidden; - overflow-y: hidden; - display: -webkit-box; - -webkit-box-orient: vertical; - height: 30px; +.display { + display: flex; + justify-content: space-between; + column-gap: 20px; + padding: 1px 1rem; + align-items: stretch; + + & .display_label{ + font-size: 16px; + color: rgb(70, 70, 70); + text-overflow: ellipsis; + overflow: hidden; + flex-grow: 1; + } + + & .display_value{ + display: flex; + align-items: center; + font-size: 18px; + font-weight: bold; + color: rgb(50, 50, 50); + } } -.display_value{ - font-size: 18px; - font-weight: bold; - color: rgb(50, 50, 50); - text-align: right; - justify-content: flex-end; - align-items:baseline; - margin: 1px 1rem; - flex: auto; -} +@media only screen and (max-width: 1280px) { + .display { + padding: 1px 5px; + } -.display_small_value{ - color: rgb(50, 50, 50); - text-align: right; - justify-content: center; - margin: 1px 1rem 1px 1rem; - flex: auto; + .display_flex_container { + flex-wrap: wrap; + } }