forked from ddrilling/asb_cloud_front
Compare commits
6 Commits
dev
...
feature/ad
Author | SHA1 | Date | |
---|---|---|---|
73ae239148 | |||
f896a22f1d | |||
|
17c388c0b7 | ||
|
f481a212c5 | ||
|
aeb53eecb0 | ||
|
a924cd0ff1 |
@ -63,27 +63,6 @@
|
||||
const page: ReactNode = lazy(() => import (...))
|
||||
```
|
||||
|
||||
### 1.4. Работа с репозиторием
|
||||
|
||||
#### 1.4.1. Подготовка к публикации работы по заданию
|
||||
При получений задания необходимо создать для неё ветку, наследуемую от **dev**.
|
||||
Ветка должна именоваться в **kebab-case** и иметь префикс соответствующий типу задачи:
|
||||
* "**feature/**" - для нового функционала или визуала;
|
||||
* "**fix/**" - для багов и любых исправлений.
|
||||
|
||||
Название ветки должно кратко описывать проблему или новые возможности.
|
||||
|
||||
Далее необходимо создать *pull request* на ветку dev от новосозданной и сразу отметить его как WIP.
|
||||
При завершении задания метку WIP необходимо снять.
|
||||
|
||||
#### 1.4.2 Оформление коммита
|
||||
Изменения файлов необходимо разделять на коммиты по общим изменениям и соответствующе его именовать.
|
||||
Если в коммит попадает более одного логического изменения стоит указывать их в виде маркированного списка, например:
|
||||
```
|
||||
* На странице "Мониторинг" и "Архив" сокращено колличество запросов;
|
||||
* Страница "Сообщения" удалена.
|
||||
```
|
||||
|
||||
## 2. JS
|
||||
1. Методы, константы и переменные документируются в соответствии с `JSDoc`;
|
||||
2. При документации страниц необходимо указать её название, краткое описание и описание получаемых параметров:
|
||||
|
52
README.md
52
README.md
@ -40,7 +40,7 @@ npx openapi -i http://{IP_ADDRESS}:{PORT}/swagger/v1/swagger.json -o src/service
|
||||
|:-------------------------|:--------|:------------------------------------|
|
||||
| 127.0.0.1:5000 | oul | Локальный адрес вашей машины |
|
||||
| 192.168.1.113:5000 | oud | Локальный адрес development-сервера |
|
||||
| 46.146.207.184:80 | oug_dev | Внешний адрес development-сервера |
|
||||
| 46.146.209.148:89 | oug_dev | Внешний адрес development-сервера |
|
||||
| cloud.digitaldrilling.ru | oug | Внешний адрес production-сервера |
|
||||
|
||||
## 3. Компиляция production-версии приложения
|
||||
@ -60,53 +60,3 @@ npm run build
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
## 5. Подготовка к работе с гит репозиторием
|
||||
|
||||
### 5.1. Генерация SSH-ключей
|
||||
Для генерации ключей, в **Git Bash**, либо в **bash** консоли необходимо ввести команду:
|
||||
```bash
|
||||
ssh-keygen
|
||||
```
|
||||
|
||||
Предложенный путь сохранения ключа оставить без изменений
|
||||
|
||||
Пароль для ускорения работы можно не задавать
|
||||
|
||||
После чего публичный ключ необходимо занести ключ в [Gitea](http://46.146.207.184:8080/), в настройках пользователя.
|
||||
|
||||
Чтобы получить публичный ключ необходимо ввести в консоли команду:
|
||||
```bash
|
||||
cat ~/.ssh/id_rsa.pub
|
||||
```
|
||||
|
||||
Далее ключ небходимо проверить, для этого необходимо нажать соответствующую кнопку в Gitea, скопировать и выполнить предложенную команду в консоли, после чего вывод вставить в поле на странице.
|
||||
|
||||
### 5.2. Генерация GPG-ключей
|
||||
Для генерации ключей, в **Git Bash**, либо в **bash** консоли необходимо ввести команду:
|
||||
```bash
|
||||
gpg --full-generate-key
|
||||
```
|
||||
Тип ключа выбираем *RSA and RSA* (по умолчанию 1). Длину ключа рекомендуется задавать 4096. Далее необходимо заполнить все опрошенные данные, пароль оставить пустым.
|
||||
|
||||
После чего публичный ключ необходимо занести ключ в [Gitea](http://46.146.207.184:8080/), в настройках пользователя.
|
||||
|
||||
Чтобы получить публичный ключ необходимо ввести в консоли команду:
|
||||
```bash
|
||||
gpg --export --armor <email>
|
||||
```
|
||||
|
||||
Где вместо `<email>` необходимо подставить электронную почту, указанную к ключу.
|
||||
|
||||
Далее ключ небходимо проверить, для этого необходимо нажать соответствующую кнопку в Gitea, скопировать и выполнить предложенную команду в консоли, после чего вывод вставить в поле на странице.
|
||||
|
||||
### 5.3. Настройка подписания коммитов (требуется GPG-ключ)
|
||||
Перед началом необходимо получить ID GPG-ключа, для этого выполним команду:
|
||||
```bash
|
||||
gpg --list-keys <email>
|
||||
```
|
||||
|
||||
Где вместо `<email>` необходимо подставить электронную почту, указанную к ключу. Из полученного вывода нам нужна только строка под строкой `rsa4096`. Эту строку мы передадим в следующую команду на место `<key-id>`:
|
||||
```bash
|
||||
git config --user.signingkey <key-id>
|
||||
```
|
||||
|
22
package-lock.json
generated
22
package-lock.json
generated
@ -16,7 +16,8 @@
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"rxjs": "^7.5.5"
|
||||
"rxjs": "^7.5.5",
|
||||
"usehooks-ts": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.2",
|
||||
@ -14687,6 +14688,19 @@
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/usehooks-ts": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.7.2.tgz",
|
||||
"integrity": "sha512-DeLqSnGg9VvpwPZA+6lKVURJKM9EBu7bbIXuYclQ9COO3w4lacnJa0uP0iJbC/lAmY7GlmPinjZfGNNmDTlUpg==",
|
||||
"engines": {
|
||||
"node": ">=16.15.0",
|
||||
"npm": ">=8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@ -26397,6 +26411,12 @@
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"usehooks-ts": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.7.2.tgz",
|
||||
"integrity": "sha512-DeLqSnGg9VvpwPZA+6lKVURJKM9EBu7bbIXuYclQ9COO3w4lacnJa0uP0iJbC/lAmY7GlmPinjZfGNNmDTlUpg==",
|
||||
"requires": {}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
@ -11,7 +11,8 @@
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"rxjs": "^7.5.5"
|
||||
"rxjs": "^7.5.5",
|
||||
"usehooks-ts": "^2.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
@ -24,11 +25,11 @@
|
||||
"dev": "webpack-dev-server --env=\"ENV=dev\" --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.10:5000/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.207.184/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.207.184",
|
||||
"proxy": "http://46.146.209.148:89",
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useElementSize } from 'usehooks-ts'
|
||||
import { Property } from 'csstype'
|
||||
import { Empty } from 'antd'
|
||||
import * as d3 from 'd3'
|
||||
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { isDev, useElementSize, usePartialProps } from '@utils'
|
||||
import { isDev, usePartialProps } from '@utils'
|
||||
|
||||
import D3MouseZone from './D3MouseZone'
|
||||
import { getChartClass } from './functions'
|
||||
@ -92,6 +93,11 @@ export type D3ChartProps<DataType extends BaseDataType> = React.DetailedHTMLProp
|
||||
/** Параметры блока легенды */
|
||||
legend?: BasePluginSettings & D3LegendSettings
|
||||
}
|
||||
/** Добавление зума графику */
|
||||
zoom?: {
|
||||
/** Массив коэффициентов приближения k0 - минимальный k1 - максимальный коэффициент */
|
||||
scaleExtent: [k0: number, k1: number]
|
||||
}
|
||||
}
|
||||
|
||||
const getDefaultXAxisConfig = <DataType extends BaseDataType>(): ChartAxis<DataType> => ({
|
||||
@ -109,11 +115,12 @@ const _D3Chart = <DataType extends Record<string, unknown>>({
|
||||
height: givenHeight = '100%',
|
||||
loading,
|
||||
offset: _offset,
|
||||
animDurationMs = 200,
|
||||
animDurationMs = 20,
|
||||
backgroundColor = 'transparent',
|
||||
ticks,
|
||||
plugins,
|
||||
dash,
|
||||
zoom,
|
||||
...other
|
||||
}: D3ChartProps<DataType>) => {
|
||||
const xAxisConfig = usePartialProps<ChartAxis<DataType>>(_xAxisConfig, getDefaultXAxisConfig)
|
||||
@ -123,6 +130,7 @@ const _D3Chart = <DataType extends Record<string, unknown>>({
|
||||
const [xAxisRef, setXAxisRef] = useState<SVGGElement | null>(null)
|
||||
const [yAxisRef, setYAxisRef] = useState<SVGGElement | null>(null)
|
||||
const [chartAreaRef, setChartAreaRef] = useState<SVGGElement | null>(null)
|
||||
const [currentZoomState, setCurrentZoomState] = useState<d3.ZoomTransform | null>(null)
|
||||
|
||||
const xAxisArea = useCallback(() => d3.select(xAxisRef), [xAxisRef])
|
||||
const yAxisArea = useCallback(() => d3.select(yAxisRef), [yAxisRef])
|
||||
@ -130,7 +138,7 @@ const _D3Chart = <DataType extends Record<string, unknown>>({
|
||||
|
||||
const [charts, setCharts] = useState<ChartRegistry<DataType>[]>([])
|
||||
|
||||
const [rootRef, { width, height }] = useElementSize<HTMLDivElement>()
|
||||
const [rootRef, { width, height }] = useElementSize()
|
||||
|
||||
const xAxis = useMemo(() => {
|
||||
if (!data) return
|
||||
@ -158,8 +166,13 @@ const _D3Chart = <DataType extends Record<string, unknown>>({
|
||||
xAxis.domain([domain?.x?.min ?? minX, domain?.x?.max ?? maxX])
|
||||
}
|
||||
|
||||
if (currentZoomState) {
|
||||
const newXScale = currentZoomState.rescaleX(xAxis)
|
||||
xAxis.domain(newXScale.domain())
|
||||
}
|
||||
|
||||
return xAxis
|
||||
}, [xAxisConfig, data, domain, width, offset])
|
||||
}, [xAxisConfig, data, domain, width, offset, currentZoomState])
|
||||
|
||||
const yAxis = useMemo(() => {
|
||||
if (!data) return
|
||||
@ -199,8 +212,13 @@ const _D3Chart = <DataType extends Record<string, unknown>>({
|
||||
|
||||
yAxis.range([height - offset.top - offset.bottom, 0])
|
||||
|
||||
if (currentZoomState) {
|
||||
const newYScale = currentZoomState.rescaleY(yAxis)
|
||||
yAxis.domain(newYScale.domain())
|
||||
}
|
||||
|
||||
return yAxis
|
||||
}, [charts, data, domain, height, offset])
|
||||
}, [charts, data, domain, height, offset, currentZoomState])
|
||||
|
||||
const nTicks = {
|
||||
color: 'lightgray',
|
||||
@ -347,6 +365,20 @@ const _D3Chart = <DataType extends Record<string, unknown>>({
|
||||
redrawCharts()
|
||||
}, [redrawCharts])
|
||||
|
||||
useEffect(() => {
|
||||
if (!svgRef || !zoom) return
|
||||
const zoomBehavior = d3.zoom<SVGSVGElement, unknown>()
|
||||
.scaleExtent(zoom.scaleExtent)
|
||||
.translateExtent([[0, 0], [width - offset.left - offset.right, height - offset.top - offset.bottom]])
|
||||
.extent([[0, 0], [width - offset.left - offset.right, height - offset.top - offset.bottom]])
|
||||
.on('zoom', () => {
|
||||
const zoomState = d3.zoomTransform(svgRef)
|
||||
setCurrentZoomState(zoomState)
|
||||
})
|
||||
|
||||
d3.select(svgRef).call(zoomBehavior)
|
||||
}, [svgRef, zoom, width, height, offset])
|
||||
|
||||
return (
|
||||
<LoaderPortal
|
||||
show={loading}
|
||||
@ -365,7 +397,17 @@ const _D3Chart = <DataType extends Record<string, unknown>>({
|
||||
<svg ref={setSvgRef} width={'100%'} height={'100%'}>
|
||||
<g ref={setXAxisRef} className={'axis x'} transform={`translate(${offset.left}, ${height - offset.bottom})`} />
|
||||
<g ref={setYAxisRef} className={'axis y'} transform={`translate(${offset.left}, ${offset.top})`} />
|
||||
<g ref={setChartAreaRef} className={'chart-area'} transform={`translate(${offset.left}, ${offset.top})`}>
|
||||
<defs>
|
||||
<clipPath id={'clipZoomData'}>
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={Math.max(width - offset.left - offset.right, 0)}
|
||||
height={Math.max(height - offset.top - offset.bottom, 0)}
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g clipPath={'url(#clipZoomData)'} ref={setChartAreaRef} className={'chart-area'} transform={`translate(${offset.left}, ${offset.top})`}>
|
||||
<rect
|
||||
width={Math.max(width - offset.left - offset.right, 0)}
|
||||
height={Math.max(height - offset.top - offset.bottom, 0)}
|
||||
@ -375,7 +417,7 @@ const _D3Chart = <DataType extends Record<string, unknown>>({
|
||||
<D3MouseZone width={width} height={height} offset={offset}>
|
||||
<D3Cursor {...plugins?.cursor} />
|
||||
<D3Legend<DataType> charts={charts} {...plugins?.legend} />
|
||||
<D3Tooltip<DataType> charts={charts} {...plugins?.tooltip} />
|
||||
<D3Tooltip<DataType> charts={charts} {...plugins?.tooltip} zoomState={currentZoomState}/>
|
||||
</D3MouseZone>
|
||||
</svg>
|
||||
</D3ContextMenu>
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useElementSize } from 'usehooks-ts'
|
||||
import { Property } from 'csstype'
|
||||
import * as d3 from 'd3'
|
||||
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { useElementSize, usePartialProps } from '@utils'
|
||||
import { usePartialProps } from '@utils'
|
||||
import { ChartOffset } from './types'
|
||||
|
||||
import '@styles/components/d3.less'
|
||||
@ -33,7 +34,7 @@ export const D3HorizontalPercentChart = memo<D3HorizontalChartProps>(({
|
||||
}) => {
|
||||
const offset = usePartialProps<ChartOffset>(givenOffset, defaultOffset)
|
||||
|
||||
const [divRef, { width, height }] = useElementSize<HTMLDivElement>()
|
||||
const [divRef, { width, height }] = useElementSize()
|
||||
const rootRef = useRef<SVGGElement | null>(null)
|
||||
|
||||
const root = useCallback(() => rootRef.current ? d3.select(rootRef.current) : null, [rootRef.current])
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useElementSize } from 'usehooks-ts'
|
||||
import { Property } from 'csstype'
|
||||
import { Empty } from 'antd'
|
||||
import * as d3 from 'd3'
|
||||
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { isDev, useElementSize, usePartialProps, useUserSettings } from '@utils'
|
||||
import { isDev, usePartialProps, useUserSettings } from '@utils'
|
||||
|
||||
import {
|
||||
BaseDataType,
|
||||
@ -212,7 +213,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
||||
const yTicks = usePartialProps<Required<ChartTick<DataType>>>(_yTicks, getDefaultYTicks)
|
||||
const yAxisConfig = usePartialProps<ChartAxis<DataType>>(_yAxisConfig, getDefaultYAxisConfig)
|
||||
|
||||
const [rootRef, { width, height }] = useElementSize<HTMLDivElement>()
|
||||
const [rootRef, { width, height }] = useElementSize()
|
||||
|
||||
const yAxisArea = useCallback(() => d3.select(yAxisRef), [yAxisRef])
|
||||
const axesArea = useCallback(() => d3.select(axesAreaRef), [axesAreaRef])
|
||||
|
@ -76,6 +76,7 @@ export const makeDefaultRender = <DataType extends BaseDataType>(): D3RenderFunc
|
||||
|
||||
export type D3TooltipProps<DataType extends BaseDataType> = Partial<D3TooltipSettings<DataType>> & {
|
||||
charts: ChartRegistry<DataType>[],
|
||||
zoomState?: d3.ZoomTransform | null,
|
||||
}
|
||||
|
||||
function _D3Tooltip<DataType extends BaseDataType>({
|
||||
@ -86,7 +87,8 @@ function _D3Tooltip<DataType extends BaseDataType>({
|
||||
position: _position = 'bottom',
|
||||
className = '',
|
||||
style: _style = {},
|
||||
limit = 2
|
||||
limit = 2,
|
||||
zoomState,
|
||||
}: D3TooltipProps<DataType>) {
|
||||
const { mouseState, zoneRect, subscribe } = useD3MouseZone()
|
||||
const [tooltipBody, setTooltipBody] = useState<any>()
|
||||
@ -94,6 +96,7 @@ function _D3Tooltip<DataType extends BaseDataType>({
|
||||
const [position, setPosition] = useState<D3TooltipPosition>(_position ?? 'bottom')
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [fixed, setFixed] = useState(false)
|
||||
const [currentZoom, setCurrentZoom] = useState<d3.ZoomTransform | null>(null);
|
||||
|
||||
const tooltipRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
@ -113,7 +116,7 @@ function _D3Tooltip<DataType extends BaseDataType>({
|
||||
}, [subscribe, visible])
|
||||
|
||||
useEffect(() => {
|
||||
if (!tooltipRef.current || !zoneRect || fixed) return
|
||||
if (!tooltipRef.current || !zoneRect || fixed || !visible) return
|
||||
const rect = tooltipRef.current.getBoundingClientRect()
|
||||
|
||||
if (!mouseState.visible) return
|
||||
@ -121,8 +124,9 @@ function _D3Tooltip<DataType extends BaseDataType>({
|
||||
const offsetX = -rect.width / 2 // По центру
|
||||
const offsetY = 15 // Чуть выше курсора
|
||||
|
||||
const left = Math.max(10, Math.min(zoneRect.width - rect.width - 10, mouseState.x + offsetX))
|
||||
const left = mouseState.x + offsetX
|
||||
let top = mouseState.y - offsetY - rect.height
|
||||
|
||||
setPosition(top <= 0 ? 'top' : 'bottom')
|
||||
if (top <= 0) top = mouseState.y + offsetY
|
||||
|
||||
@ -131,7 +135,34 @@ function _D3Tooltip<DataType extends BaseDataType>({
|
||||
left,
|
||||
top,
|
||||
}))
|
||||
}, [tooltipRef.current, mouseState, zoneRect, fixed])
|
||||
}, [tooltipRef.current, mouseState, zoneRect, fixed, visible])
|
||||
|
||||
useEffect(() => {
|
||||
if (!zoomState) return
|
||||
|
||||
setCurrentZoom(zoomState)
|
||||
|
||||
if (!fixed) {
|
||||
setVisible(false)
|
||||
return
|
||||
}
|
||||
|
||||
const offsetX = Number(style.left) + Number(width) + 7
|
||||
|
||||
if (zoneRect && ((offsetX <= zoneRect.left) || (offsetX > zoneRect.right)) || zoomState.k !== currentZoom?.k) {
|
||||
setVisible(false)
|
||||
setFixed(false)
|
||||
return
|
||||
}
|
||||
|
||||
const distanceMoveX = currentZoom ? currentZoom.x - zoomState.x : 1
|
||||
|
||||
setStyle((prevStyle) => ({
|
||||
...prevStyle,
|
||||
left: prevStyle?.left ? +prevStyle.left - distanceMoveX : 0,
|
||||
}))
|
||||
}, [zoomState, fixed])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (fixed) return
|
||||
|
@ -33,7 +33,7 @@ export const makeMessageColumns = (idWell) => [
|
||||
<Tooltip title={'Нажмите для перехода в архив'}>
|
||||
<Link
|
||||
style={{ color: 'inherit'}}
|
||||
to={`/well/${idWell}/telemetry/monitoring?range=1800&end=${moment(item?.date).add(27, 'minute').local().toISOString()}`}
|
||||
to={`/well/${idWell}/telemetry/monitoring?range=1800&start=${moment(item?.date).subtract(3, 'minute').local().toISOString()}`}
|
||||
>
|
||||
<LinkOutlined />
|
||||
|
||||
|
@ -100,6 +100,10 @@ export const OperationsChart = memo(({ data, yDomain, height, category, onDomain
|
||||
},
|
||||
}), [category])
|
||||
|
||||
const zoom = useMemo(() => ({
|
||||
scaleExtent: [1, 5]
|
||||
}), [])
|
||||
|
||||
return (
|
||||
<D3Chart
|
||||
xAxis={xAxis}
|
||||
@ -109,6 +113,7 @@ export const OperationsChart = memo(({ data, yDomain, height, category, onDomain
|
||||
height={height}
|
||||
plugins={plugins}
|
||||
ticks={ticks}
|
||||
zoom={zoom}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
@ -1,269 +0,0 @@
|
||||
import { Button, Input, Modal, Radio, Table } from 'antd'
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import moment from 'moment'
|
||||
import * as d3 from 'd3'
|
||||
|
||||
import { useWell } from '@asb/context'
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { DateRangeWrapper, makeColumn, makeNumericColumn, makeNumericRender, makeTextColumn } from '@components/Table'
|
||||
import { LimitingParameterService } from '@api'
|
||||
import { unique } from '@utils/filters'
|
||||
import { useElementSize } from '@utils'
|
||||
|
||||
import { makeGetColor } from '@pages/Well/WellOperations/Tvd'
|
||||
|
||||
import '@styles/limiting_parameter_statistics.less'
|
||||
|
||||
const columns = [
|
||||
makeColumn('Цвет', 'color', { width: 50, render: (d) => (
|
||||
<div style={{ backgroundColor: d, padding: '5px 0' }} />
|
||||
) }),
|
||||
makeTextColumn('Уставка', 'nameFeedRegulator'),
|
||||
makeNumericColumn('Проходка, м', 'depth'),
|
||||
makeNumericColumn('Кол-во включений', 'numberInclusions', undefined, undefined, makeNumericRender(0)),
|
||||
]
|
||||
|
||||
export const LimitingParameterStatistics = memo(() => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [data, setData] = useState([])
|
||||
const [mode, setMode] = useState('depth')
|
||||
const [depthFilter, setDepthFilter] = useState({ from: null, to: null })
|
||||
const [dateFilter, setDateFilter] = useState([moment().subtract(1, 'day'), moment()])
|
||||
|
||||
const [svgRef, setSvgRef] = useState()
|
||||
const [selectedRegulator, setSelectedRegulator] = useState(null)
|
||||
|
||||
const [ref, { width, height }] = useElementSize()
|
||||
|
||||
const [well] = useWell()
|
||||
|
||||
const onDepthChanged = useCallback((e, type) => {
|
||||
setDepthFilter((prev) => ({ ...prev, [type]: e?.target?.value }))
|
||||
}, [])
|
||||
|
||||
const onRow = useCallback((record) => {
|
||||
const out = {
|
||||
onMouseEnter: () => {
|
||||
setSelectedRegulator(record.idFeedRegulator)
|
||||
d3.selectAll('.tl-pie-part')
|
||||
.filter((d) => d.data.idFeedRegulator === record.idFeedRegulator)
|
||||
.attr('transform', 'scale(1.05)')
|
||||
},
|
||||
onMouseLeave: () => {
|
||||
setSelectedRegulator(null)
|
||||
d3.selectAll('.tl-pie-part')
|
||||
.filter((d) => d.data.idFeedRegulator === record.idFeedRegulator)
|
||||
.attr('transform', 'scale(1)')
|
||||
}
|
||||
}
|
||||
if (record.idFeedRegulator === selectedRegulator)
|
||||
out.style = { background: '#FAFAFA', fontSize: '16px', fontWeight: '600' }
|
||||
return out
|
||||
}, [selectedRegulator])
|
||||
|
||||
const onPieOver = useCallback(function (e, d) {
|
||||
setSelectedRegulator(d.data.idFeedRegulator)
|
||||
d3.select(this).attr('transform', 'scale(1.05)')
|
||||
}, [])
|
||||
|
||||
const onPieOut = useCallback(function (e, d) {
|
||||
setSelectedRegulator(null)
|
||||
d3.select(this).attr('transform', 'scale(1)')
|
||||
}, [])
|
||||
|
||||
const update = useCallback(() => {
|
||||
invokeWebApiWrapperAsync(
|
||||
async () => {
|
||||
const data = await LimitingParameterService.getStat(well.id,
|
||||
mode === 'time' ? dateFilter[0] : undefined,
|
||||
mode === 'time' ? dateFilter[1] : undefined,
|
||||
mode === 'depth' ? depthFilter.from : undefined,
|
||||
mode === 'depth' ? depthFilter.to : undefined,
|
||||
)
|
||||
setData(data)
|
||||
},
|
||||
setIsLoading,
|
||||
`Не удалось загрузить статистику использования уставок`,
|
||||
{ actionName: `Загрузка статистики использования уставок`, well }
|
||||
)
|
||||
}, [well, mode, dateFilter, depthFilter])
|
||||
|
||||
const pie = useMemo(() => d3.pie().value((d) => d.totalMinutes), [])
|
||||
|
||||
const getColor = useMemo(() => makeGetColor(data?.map((row) => row.idFeedRegulator).filter(unique)), [data])
|
||||
|
||||
const tableData = useMemo(() => {
|
||||
if (!data) return null
|
||||
const totalTime = data.reduce((out, stat) => out + stat.totalMinutes, 0)
|
||||
return data.map((stat) => ({
|
||||
...stat,
|
||||
color: getColor(stat.idFeedRegulator),
|
||||
percent: stat.totalMinutes / totalTime * 100,
|
||||
}))
|
||||
}, [data, getColor])
|
||||
|
||||
const pieData = useMemo(() => tableData ? pie(tableData) : null, [tableData])
|
||||
|
||||
const radius = useMemo(() => Math.min(width, height) / 2, [width, height])
|
||||
|
||||
useEffect(update, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!pieData) return
|
||||
const slices = d3.select(svgRef)
|
||||
.select('.slices')
|
||||
.selectAll('path')
|
||||
.data(pieData)
|
||||
|
||||
slices.exit().remove()
|
||||
const newSlices = slices.enter().append('path')
|
||||
|
||||
slices.merge(newSlices)
|
||||
.attr('class', 'tl-pie-part')
|
||||
.attr('d', d3.arc().innerRadius(radius * 0.4).outerRadius(radius * 0.8))
|
||||
.attr('fill', (d) => d.data.color)
|
||||
.attr('data-id', (d) => d.idFeedRegulator)
|
||||
.on('mouseover', onPieOver)
|
||||
.on('mouseout', onPieOut)
|
||||
}, [svgRef, pieData, radius, onPieOver, onPieOut])
|
||||
|
||||
useEffect(() => {
|
||||
if (!pieData) return
|
||||
const innerArc = d3.arc().innerRadius(radius * 0.4).outerRadius(radius * 0.8)
|
||||
const outerArc = d3.arc().innerRadius(radius * 0.9).outerRadius(radius * 0.9)
|
||||
|
||||
const lines = d3.select(svgRef)
|
||||
.select('.lines')
|
||||
.selectAll('polyline')
|
||||
.data(pieData, (d) => d.data.nameFeedRegulator)
|
||||
|
||||
lines.exit().remove()
|
||||
const newLines = lines.enter()
|
||||
.append('polyline')
|
||||
|
||||
const abovePi = (d) => (d.startAngle + d.endAngle) / 2 < Math.PI
|
||||
|
||||
lines.merge(newLines)
|
||||
.style('display', (d) => d.data.idFeedRegulator !== selectedRegulator ? 'none' : 'block')
|
||||
.attr('points', (d) => {
|
||||
const pos = outerArc.centroid(d)
|
||||
pos[0] = radius * 0.95 * (abovePi(d) ? 1 : -1)
|
||||
return [innerArc.centroid(d), outerArc.centroid(d), pos]
|
||||
})
|
||||
|
||||
const lables = d3.select(svgRef)
|
||||
.select('.labels')
|
||||
.selectAll('text')
|
||||
.data(pieData, (d) => d.data.nameFeedRegulator)
|
||||
|
||||
lables.exit().remove()
|
||||
const newLabels = lables.enter()
|
||||
.append('text')
|
||||
.attr('dy', '.35em')
|
||||
|
||||
lables.merge(newLabels)
|
||||
.attr('transform', (d) => {
|
||||
const pos = outerArc.centroid(d)
|
||||
pos[0] = radius * 0.95 * (abovePi(d) ? 1 : -1)
|
||||
return `translate(${pos})`
|
||||
})
|
||||
.style('text-anchor', (d) => abovePi(d) ? 'start' : 'end')
|
||||
.style('display', (d) => d.data.idFeedRegulator !== selectedRegulator ? 'none' : 'block')
|
||||
.attr('width', radius * 0.4)
|
||||
.text((d) => `${d.data.percent.toFixed(2)}% (${d.data.totalMinutes.toFixed(2)} мин)`)
|
||||
|
||||
}, [svgRef, pieData, radius, selectedRegulator])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setIsOpen(true)}>Статистика использования уставок</Button>
|
||||
<Modal
|
||||
centered
|
||||
width={1024}
|
||||
footer={false}
|
||||
title={'Статистика использования уставок'}
|
||||
onCancel={() => setIsOpen(false)}
|
||||
open={isOpen}
|
||||
>
|
||||
<LoaderPortal show={isLoading}>
|
||||
<div className={'filter-groups'}>
|
||||
<Input.Group compact style={{ flex: 1 }}>
|
||||
<Input
|
||||
addonBefore={(
|
||||
<Radio
|
||||
checked={mode === 'depth'}
|
||||
onChange={() => setMode('depth')}
|
||||
>
|
||||
По глубине
|
||||
</Radio>
|
||||
)}
|
||||
allowClear
|
||||
disabled={mode !== 'depth'}
|
||||
prefix={'От'}
|
||||
suffix={'м'}
|
||||
style={{ width: 'calc(50% + 113px / 2)', textAlign: 'right' }}
|
||||
onChange={(e) => onDepthChanged(e, 'from')}
|
||||
value={depthFilter.from}
|
||||
/>
|
||||
<Input
|
||||
allowClear
|
||||
disabled={mode !== 'depth'}
|
||||
prefix={'До'}
|
||||
suffix={'м'}
|
||||
style={{ width: 'calc(50% - 113px / 2)', textAlign: 'right' }}
|
||||
onChange={(e) => onDepthChanged(e, 'to')}
|
||||
value={depthFilter.to}
|
||||
/>
|
||||
</Input.Group>
|
||||
<Input.Group compact style={{ flex: 1 }}>
|
||||
<Input style={{ width: 128 }} addonBefore={(
|
||||
<Radio
|
||||
checked={mode === 'time'}
|
||||
onChange={() => setMode('time')}
|
||||
>
|
||||
По времени
|
||||
</Radio>
|
||||
)}/>
|
||||
<DateRangeWrapper
|
||||
showTime
|
||||
value={dateFilter}
|
||||
disabled={mode !== 'time'}
|
||||
onCalendarChange={setDateFilter}
|
||||
disabledDate={(date) => date.isAfter(moment())}
|
||||
/>
|
||||
</Input.Group>
|
||||
</div>
|
||||
<Button onClick={update} style={{ marginTop: 10 }}>Обновить</Button>
|
||||
<div className={'lps-pie-chart'} ref={ref}>
|
||||
{data ? (
|
||||
<svg ref={setSvgRef} width={'100%'} height={'100%'}>
|
||||
<g transform={`translate(${width / 2}, ${height / 2})`}>
|
||||
<g className={'slices'} stroke={'#0005'} />
|
||||
<g className={'labels'} fill={'black'} />
|
||||
<g className={'lines'} fill={'none'} stroke={'black'} />
|
||||
</g>
|
||||
</svg>
|
||||
) : (
|
||||
<div className={'empty-wrapper'}>
|
||||
<Empty />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={'modal-label'}>Итоговая таблица по скважине</div>
|
||||
<Table
|
||||
bordered
|
||||
size={'small'}
|
||||
pagination={false}
|
||||
dataSource={tableData}
|
||||
columns={columns}
|
||||
onRow={onRow}
|
||||
/>
|
||||
</LoaderPortal>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export default LimitingParameterStatistics
|
@ -20,7 +20,6 @@ import {
|
||||
|
||||
import { makeChartGroups, yAxis } from './dataset'
|
||||
import { ADDITIVE_PAGES, cutData, DATA_COUNT, getLoadingInterval, makeDateTimeDisabled } from './archive_methods'
|
||||
import LimitingParameterStatistics from './LimitingParameterStatistics'
|
||||
import ActiveMessagesOnline from './ActiveMessagesOnline'
|
||||
import TelemetrySummary from './TelemetrySummary'
|
||||
import WirelineRunOut from './WirelineRunOut'
|
||||
@ -266,7 +265,6 @@ const TelemetryView = memo(() => {
|
||||
</Select>
|
||||
</div>
|
||||
<Setpoints />
|
||||
<LimitingParameterStatistics />
|
||||
<WirelineRunOut />
|
||||
<div className={'icons'}>
|
||||
<img src={isTorqueStabEnabled(spinLast) ? MomentStabPicEnabled : MomentStabPicDisabled} alt={'TorqueMaster'} />
|
||||
|
@ -3,6 +3,7 @@ import {
|
||||
AlertOutlined,
|
||||
BarChartOutlined,
|
||||
BuildOutlined,
|
||||
ControlOutlined,
|
||||
DeploymentUnitOutlined,
|
||||
ExperimentOutlined,
|
||||
FilePdfOutlined,
|
||||
@ -16,7 +17,7 @@ import { makeItem, PrivateMenu } from '@components/PrivateMenu'
|
||||
|
||||
export const menuItems = [
|
||||
makeItem('Телеметрия', 'telemetry', [], <FundViewOutlined />, [
|
||||
makeItem('Мониторинг', 'monitoring', [], <FundViewOutlined />),
|
||||
makeItem('Мониторинг', 'telemetry', [], <FundViewOutlined />),
|
||||
makeItem('Сообщения', 'messages', [], <AlertOutlined />),
|
||||
makeItem('ННБ', 'dashboard_nnb', [], <FolderOutlined />),
|
||||
makeItem('Операции', 'operations', [], <FolderOutlined />),
|
||||
|
@ -6,6 +6,8 @@ import { useState, useEffect, memo, useMemo, useCallback } from 'react'
|
||||
import { useTopRightBlock, useWell } from '@asb/context'
|
||||
import {
|
||||
EditableTable,
|
||||
makeColumn,
|
||||
makeNumericColumnOptions,
|
||||
makeSelectColumn,
|
||||
makeDateColumn,
|
||||
makeNumericColumn,
|
||||
@ -70,7 +72,8 @@ export const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => {
|
||||
const skip = ((pageNumAndPageSize.current - 1) * pageNumAndPageSize.pageSize) || 0
|
||||
const take = pageNumAndPageSize.pageSize
|
||||
const paginatedOperations = await WellOperationService.getOperations(well.id,
|
||||
undefined, undefined, undefined, undefined, undefined, idType, undefined, skip, take)
|
||||
idType, undefined, undefined, undefined, undefined,
|
||||
undefined, undefined, skip, take)
|
||||
const operations = paginatedOperations?.items ?? []
|
||||
setOperations(operations)
|
||||
const total = paginatedOperations.count?? paginatedOperations.items?.length ?? 0
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { memo, useEffect, useMemo, useState } from 'react'
|
||||
import { useElementSize } from 'usehooks-ts'
|
||||
import { Empty } from 'antd'
|
||||
import moment from 'moment'
|
||||
import * as d3 from 'd3'
|
||||
@ -7,7 +8,7 @@ import LoaderPortal from '@components/LoaderPortal'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { DetectedOperationService } from '@api'
|
||||
import { unique } from '@utils/filters'
|
||||
import { formatDate, useElementSize } from '@utils'
|
||||
import { formatDate } from '@utils'
|
||||
|
||||
import { makeTooltipRender } from '../../Telemetry/Operations/OperationsChart'
|
||||
import { makeGetColor } from '.'
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useElementSize } from 'usehooks-ts'
|
||||
import { Empty } from 'antd'
|
||||
import * as d3 from 'd3'
|
||||
|
||||
@ -7,7 +8,6 @@ import LoaderPortal from '@components/LoaderPortal'
|
||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||
import { DetectedOperationService } from '@api'
|
||||
import { unique } from '@utils/filters'
|
||||
import { useElementSize } from '@utils'
|
||||
|
||||
import { makeGetColor } from '.'
|
||||
|
||||
|
@ -95,11 +95,10 @@ const Well = memo(() => {
|
||||
<Route path={'*'} element={<NoAccessComponent />} />
|
||||
|
||||
<Route path={'telemetry/*'} element={<Telemetry />}>
|
||||
<Route index element={<Navigate to={'monitoring'} replace />} />
|
||||
<Route index element={<Navigate to={'telemetry'} replace />} />
|
||||
<Route path={'*'} element={<NoAccessComponent />} />
|
||||
<Route path={'telemetry'} element={<Navigate to={'../monitoring'} replace />} /> {/* TODO: Remove in next release */}
|
||||
|
||||
<Route path={'monitoring'} element={<TelemetryView />} />
|
||||
<Route path={'telemetry'} element={<TelemetryView />} />
|
||||
<Route path={'messages'} element={<Messages />} />
|
||||
<Route path={'dashboard_nnb/*'} element={<DashboardNNB />} />
|
||||
<Route path={'operations'} element={<Operations />} />
|
||||
|
@ -1,29 +0,0 @@
|
||||
.filter-groups {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
& .filter-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
& .date-filter {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-label {
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lps-pie-chart {
|
||||
min-height: 30vh;
|
||||
max-height: 50vh;
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
export * from './cachedFetch'
|
||||
export * from './functionalValue'
|
||||
export * from './useElementSize'
|
||||
export * from './usePartialProps'
|
||||
export * from './useUserSettings'
|
||||
|
@ -1,24 +0,0 @@
|
||||
import { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
export const useElementSize = <T extends Element>(): [MutableRefObject<T | null>, DOMRectReadOnly] => {
|
||||
const ref = useRef<T>(null)
|
||||
const [rect, setRect] = useState<DOMRectReadOnly>(new DOMRect())
|
||||
|
||||
const observer = useMemo(() => new ResizeObserver((entries) => {
|
||||
if (entries.length <= 0) return
|
||||
const rect = entries[0].contentRect
|
||||
setRect(rect)
|
||||
}), [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return
|
||||
observer.observe(ref.current)
|
||||
|
||||
return () => {
|
||||
if (!ref.current) return
|
||||
observer.unobserve(ref.current)
|
||||
}
|
||||
}, [ref.current, observer])
|
||||
|
||||
return [ref, rect]
|
||||
}
|
Loading…
Reference in New Issue
Block a user