Compare commits

...

30 Commits

Author SHA1 Message Date
goodmice
04bd79d563
Исправлена ссылка на мониторинг в меню 2022-12-28 11:39:35 +05:00
goodmice
4b0adeeb7b
Исправлена ошибка перехода с сообщений на мониторинг 2022-12-28 11:37:04 +05:00
goodmice
3b0182216e
Белый IP DEV-сервера обновлён 2022-12-26 12:49:38 +05:00
goodmice
269b59560a
Добавлена секция работы с репозиторием 2022-12-23 17:12:02 +05:00
goodmice
c867c51bf5
Добавлена секция настройки ssh/gpg ключей 2022-12-23 17:11:46 +05:00
goodmice
5d64102a7c
Обновлено API WellOperationService 2022-12-23 14:45:13 +05:00
goodmice
d0c2774774
Удалено логгирование в useElementSize 2022-12-23 10:45:08 +05:00
goodmice
de58c81e87
* Описан хук useElementSize на основе ResizeObserver
* пакет usehooks-ts удалён
2022-12-22 12:33:01 +05:00
goodmice
860d74f2db
Merge branch 'dev' into feature/limit-parameter-staistics-window 2022-12-22 10:38:59 +05:00
ed43ebb082 Merge pull request 'Актуализировать обработку данных на странице "Операции".' (#4) from fix/changing-operation-categories-controller into dev
Reviewed-on: http://46.146.209.148:8080/DDrilling/asb_cloud_front/pulls/4
Reviewed-by: Александр Сироткин <av.sirotkin@digitaldrilling.ru>
2022-12-20 14:28:13 +05:00
44104f672b
Исправлена опечатка "загрзуить" -> "загрузить" 2022-12-20 12:53:39 +05:00
fce6c1909e
1. Изменена логика зависимостей в дочернем компоненте на странице "Операции".
2. Уменьшена длина поля "Верхняя граница", на странице "Операции".
2022-12-20 12:36:31 +05:00
c0b5f82ad2
Убран вызов метода получения списка категорий из дочернего компонента
на странице "Операции".
2022-12-20 12:04:58 +05:00
8d6f5ac1a5
На странице "Операции" изменен метод получения категорий операций. (Upd) 2022-12-20 11:27:37 +05:00
bef281feb8
На странице "Операции" изменен метод получения категорий операций. (Upd) 2022-12-20 11:23:56 +05:00
a79cac9d51
На странице "Операции" изменен метод получения категорий операций 2022-12-20 10:49:12 +05:00
878ab921c1 Merge pull request 'Добавлена кнопка скачивания "Выгрузка расширенной автоформируемой РТК"' (#1) from feature/add-download-button into dev
Reviewed-on: http://46.146.209.148:8080/DDrilling/asb_cloud_front/pulls/1
2022-12-19 18:24:56 +05:00
8154f2f0ba Merge branch 'dev' into feature/add-download-button 2022-12-19 18:24:47 +05:00
94707b3c9a Merge branch 'feature/add-download-button' of ssh://46.146.209.148:35222/DDrilling/asb_cloud_front into feature/add-download-button 2022-12-19 18:02:06 +05:00
8825c4c26c
Исправления по стилю кода 2022-12-19 17:56:14 +05:00
0e25e10785 Исправлена ошибка описания таблицы IP адресов 2022-12-19 17:43:31 +05:00
b063b5dcd9 Merge branch 'dev' into feature/add-download-button 2022-12-19 17:16:11 +05:00
969edda933 Merge pull request 'Исправлена опечатка на странице 404, “запрешён” -> “запрещён”.' (#2) from fix/spelling-errors into dev
Reviewed-on: http://46.146.209.148:8080/DDrilling/asb_cloud_front/pulls/2
2022-12-19 17:12:42 +05:00
3396050fae Merge branch 'dev' into fix/spelling-errors 2022-12-19 16:58:58 +05:00
d36cd1acbd Добавлена кнопка скачивания "Выгрузка расширенной автоформируемой РТК" 2022-12-19 15:25:38 +05:00
77d65be601 Исправлена опечатка на странице 404, “запрешён” -> “запрещён”. 2022-12-19 11:37:06 +05:00
16fec4e42f Обновлены команды в readme 2022-12-19 09:25:05 +05:00
ee289cc619 Удалена колонка общего времени работы уставки 2022-11-29 12:17:17 +05:00
e430cdd5b4 Добавлена использование новых полей 2022-11-29 12:11:33 +05:00
259e2e4be8 Добавлен черновик окна статистики использования уставок 2022-11-28 10:13:40 +05:00
23 changed files with 483 additions and 84 deletions

View File

@ -12,5 +12,6 @@
"Setpoints", "Setpoints",
"usehooks" "usehooks"
], ],
"liveServer.settings.port": 5501 "liveServer.settings.port": 5501,
"cSpell.language": "en,ru"
} }

View File

@ -63,6 +63,27 @@
const page: ReactNode = lazy(() => import (...)) 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 ## 2. JS
1. Методы, константы и переменные документируются в соответствии с `JSDoc`; 1. Методы, константы и переменные документируются в соответствии с `JSDoc`;
2. При документации страниц необходимо указать её название, краткое описание и описание получаемых параметров: 2. При документации страниц необходимо указать её название, краткое описание и описание получаемых параметров:

View File

@ -7,7 +7,7 @@
Установка выполняется одной командой: Установка выполняется одной командой:
```bash ```bash
npm i npm ci
``` ```
## 2. Автогенерация сервисов ## 2. Автогенерация сервисов
@ -17,14 +17,14 @@ npm i
Автогенерацию можно запустить с помощью уже прописанных в [package.json](package.json) скриптов, либо вручную. Автогенерацию можно запустить с помощью уже прописанных в [package.json](package.json) скриптов, либо вручную.
Если сервер запущен на текущей машине достаточно написать: Если сервер запущен на текущей машине достаточно написать:
```bash ```bash
npm run update_openapi npm run oul
``` ```
Для получения сервисов с основного сервера: Для получения сервисов с основного сервера:
```bash ```bash
npm run update_openapi_server npm run oug_dev
``` ```
или же ручной вариант: или же ручной вариант:
@ -36,12 +36,12 @@ npx openapi -i http://{IP_ADDRESS}:{PORT}/swagger/v1/swagger.json -o src/service
На данный момент имеются следующие IP-адреса: На данный момент имеются следующие IP-адреса:
| IP-адрес | Описание | | IP-адрес | Команда | Описание |
|:-|:-| |:-------------------------|:--------|:------------------------------------|
| 127.0.0.1:5000 | Локальный адрес вашей машины (привязан к `update_openapi`) | | 127.0.0.1:5000 | oul | Локальный адрес вашей машины |
| 192.168.1.113:5000 | Локальный адрес development-сервера (привязан к `update_openapi_server`) | | 192.168.1.113:5000 | oud | Локальный адрес development-сервера |
| 46.146.209.148:89 | Внешний адрес development-сервера | | 46.146.207.184:80 | oug_dev | Внешний адрес development-сервера |
| cloud.digitaldrilling.ru | Внешний адрес production-сервера | | cloud.digitaldrilling.ru | oug | Внешний адрес production-сервера |
## 3. Компиляция production-версии приложения ## 3. Компиляция production-версии приложения
После выполнения вышеописанных пунктов приложение готово к компиляции. После выполнения вышеописанных пунктов приложение готово к компиляции.
@ -60,3 +60,53 @@ npm run build
```bash ```bash
npm start 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
View File

@ -16,8 +16,7 @@
"react": "^18.1.0", "react": "^18.1.0",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",
"rxjs": "^7.5.5", "rxjs": "^7.5.5"
"usehooks-ts": "^2.6.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.18.2", "@babel/core": "^7.18.2",
@ -14688,19 +14687,6 @@
"requires-port": "^1.0.0" "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": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -26411,12 +26397,6 @@
"requires-port": "^1.0.0" "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": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@ -11,8 +11,7 @@
"react": "^18.1.0", "react": "^18.1.0",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",
"rxjs": "^7.5.5", "rxjs": "^7.5.5"
"usehooks-ts": "^2.6.0"
}, },
"scripts": { "scripts": {
"test": "jest", "test": "jest",
@ -25,11 +24,11 @@
"dev": "webpack-dev-server --env=\"ENV=dev\" --open --hot", "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", "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.113: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",
"oug": "npx openapi -i https://cloud.digitaldrilling.ru/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.209.148:89/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"
}, },
"proxy": "http://46.146.209.148:89", "proxy": "http://46.146.207.184",
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
"react-app", "react-app",

View File

@ -1,11 +1,10 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useElementSize } from 'usehooks-ts'
import { Property } from 'csstype' import { Property } from 'csstype'
import { Empty } from 'antd' import { Empty } from 'antd'
import * as d3 from 'd3' import * as d3 from 'd3'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { isDev, usePartialProps } from '@utils' import { isDev, useElementSize, usePartialProps } from '@utils'
import D3MouseZone from './D3MouseZone' import D3MouseZone from './D3MouseZone'
import { getChartClass } from './functions' import { getChartClass } from './functions'
@ -131,7 +130,7 @@ const _D3Chart = <DataType extends Record<string, unknown>>({
const [charts, setCharts] = useState<ChartRegistry<DataType>[]>([]) const [charts, setCharts] = useState<ChartRegistry<DataType>[]>([])
const [rootRef, { width, height }] = useElementSize() const [rootRef, { width, height }] = useElementSize<HTMLDivElement>()
const xAxis = useMemo(() => { const xAxis = useMemo(() => {
if (!data) return if (!data) return

View File

@ -1,10 +1,9 @@
import { memo, useCallback, useEffect, useMemo, useRef } from 'react' import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
import { useElementSize } from 'usehooks-ts'
import { Property } from 'csstype' import { Property } from 'csstype'
import * as d3 from 'd3' import * as d3 from 'd3'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { usePartialProps } from '@utils' import { useElementSize, usePartialProps } from '@utils'
import { ChartOffset } from './types' import { ChartOffset } from './types'
import '@styles/components/d3.less' import '@styles/components/d3.less'
@ -34,7 +33,7 @@ export const D3HorizontalPercentChart = memo<D3HorizontalChartProps>(({
}) => { }) => {
const offset = usePartialProps<ChartOffset>(givenOffset, defaultOffset) const offset = usePartialProps<ChartOffset>(givenOffset, defaultOffset)
const [divRef, { width, height }] = useElementSize() const [divRef, { width, height }] = useElementSize<HTMLDivElement>()
const rootRef = useRef<SVGGElement | null>(null) const rootRef = useRef<SVGGElement | null>(null)
const root = useCallback(() => rootRef.current ? d3.select(rootRef.current) : null, [rootRef.current]) const root = useCallback(() => rootRef.current ? d3.select(rootRef.current) : null, [rootRef.current])
@ -74,13 +73,13 @@ export const D3HorizontalPercentChart = memo<D3HorizontalChartProps>(({
rects.exit().remove() rects.exit().remove()
const selectedRects = r.selectChild<SVGGElement>('.data') const selectedRects = r.selectChild<SVGGElement>('.data')
.selectAll<SVGRectElement, PercentChartDataType>('rect') .selectAll<SVGRectElement, PercentChartDataType>('rect')
selectedRects.attr('fill', (d) => d.color || 'black') selectedRects.attr('fill', (d) => d.color || 'black')
.attr('y', (d) => yScale(d.name) ?? null) .attr('y', (d) => yScale(d.name) ?? null)
.attr('height', yScale.bandwidth()) .attr('height', yScale.bandwidth())
.transition(delay) .transition(delay)
.attr('width', (d) => d.percent > 0 ? xScale(d.percent) : 0) .attr('width', (d) => d.percent > 0 ? xScale(d.percent) : 0)
afterDraw?.(selectedRects) afterDraw?.(selectedRects)

View File

@ -1,11 +1,10 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useElementSize } from 'usehooks-ts'
import { Property } from 'csstype' import { Property } from 'csstype'
import { Empty } from 'antd' import { Empty } from 'antd'
import * as d3 from 'd3' import * as d3 from 'd3'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { isDev, usePartialProps, useUserSettings } from '@utils' import { isDev, useElementSize, usePartialProps, useUserSettings } from '@utils'
import { import {
BaseDataType, BaseDataType,
@ -213,7 +212,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
const yTicks = usePartialProps<Required<ChartTick<DataType>>>(_yTicks, getDefaultYTicks) const yTicks = usePartialProps<Required<ChartTick<DataType>>>(_yTicks, getDefaultYTicks)
const yAxisConfig = usePartialProps<ChartAxis<DataType>>(_yAxisConfig, getDefaultYAxisConfig) const yAxisConfig = usePartialProps<ChartAxis<DataType>>(_yAxisConfig, getDefaultYAxisConfig)
const [rootRef, { width, height }] = useElementSize() const [rootRef, { width, height }] = useElementSize<HTMLDivElement>()
const yAxisArea = useCallback(() => d3.select(yAxisRef), [yAxisRef]) const yAxisArea = useCallback(() => d3.select(yAxisRef), [yAxisRef])
const axesArea = useCallback(() => d3.select(axesAreaRef), [axesAreaRef]) const axesArea = useCallback(() => d3.select(axesAreaRef), [axesAreaRef])

View File

@ -33,7 +33,7 @@ export const makeMessageColumns = (idWell) => [
<Tooltip title={'Нажмите для перехода в архив'}> <Tooltip title={'Нажмите для перехода в архив'}>
<Link <Link
style={{ color: 'inherit'}} style={{ color: 'inherit'}}
to={`/well/${idWell}/telemetry/monitoring?range=1800&start=${moment(item?.date).subtract(3, 'minute').local().toISOString()}`} to={`/well/${idWell}/telemetry/monitoring?range=1800&end=${moment(item?.date).add(27, 'minute').local().toISOString()}`}
> >
<LinkOutlined /> <LinkOutlined />
&nbsp; &nbsp;

View File

@ -4,7 +4,7 @@ import { Button, Modal } from 'antd'
import { useWell } from '@asb/context' import { useWell } from '@asb/context'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { EditableTable, makeGroupColumn, makeNumericColumn, makeNumericRender, makeSelectColumn } from '@components/Table' import { EditableTable, makeGroupColumn, makeNumericColumn, makeNumericRender, makeSelectColumn } from '@components/Table'
import { DetectedOperationService, OperationValueService } from '@api' import { OperationValueService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault } from '@utils'
const columnOptions = { const columnOptions = {
@ -14,7 +14,7 @@ const columnOptions = {
const scroll = { y: '75vh', scrollToFirstRowOnChange: true } const scroll = { y: '75vh', scrollToFirstRowOnChange: true }
const numericRender = makeNumericRender(2) const numericRender = makeNumericRender(2)
export const TargetEditor = memo(({ loading, onChange }) => { export const TargetEditor = memo(({ loading, onChange, options }) => {
const [targets, setTargets] = useState([]) const [targets, setTargets] = useState([])
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
@ -62,9 +62,6 @@ export const TargetEditor = memo(({ loading, onChange }) => {
useEffect(() => { useEffect(() => {
invokeWebApiWrapperAsync( invokeWebApiWrapperAsync(
async () => { async () => {
const categories = arrayOrDefault(await DetectedOperationService.getCategories())
const options = categories.map(({ id, name }) => ({ value: id, label: name }))
setTargetColumns([ setTargetColumns([
makeSelectColumn('Название', 'idOperationCategory', options, undefined, { ...columnOptions, width: 200 }, { makeSelectColumn('Название', 'idOperationCategory', options, undefined, { ...columnOptions, width: 200 }, {
showSearch: true, showSearch: true,
@ -83,7 +80,7 @@ export const TargetEditor = memo(({ loading, onChange }) => {
`Не удалось получить список категорий целей`, `Не удалось получить список категорий целей`,
{ actionName: 'Получение списка категорий целей', well } { actionName: 'Получение списка категорий целей', well }
) )
}, [well]) }, [options])
useEffect(() => { useEffect(() => {
updateTable() updateTable()

View File

@ -8,7 +8,7 @@ import { DateRangeWrapper } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { unique } from '@utils/filters' import { unique } from '@utils/filters'
import { getPermissions, arrayOrDefault, range, withPermissions, prettify } from '@utils' import { getPermissions, arrayOrDefault, range, withPermissions, prettify } from '@utils'
import { DetectedOperationService, DrillerService, TelemetryDataSaubService } from '@api' import { DetectedOperationService, DrillerService, TelemetryDataSaubService, WellOperationService } from '@api'
import DrillerList from './DrillerList' import DrillerList from './DrillerList'
import TargetEditor from './TargetEditor' import TargetEditor from './TargetEditor'
@ -26,7 +26,7 @@ const Operations = memo(() => {
const [data, setData] = useState({}) const [data, setData] = useState({})
const [drillers, setDrillers] = useState([]) const [drillers, setDrillers] = useState([])
const [drillersLoader, setDrillersLoader] = useState(false) const [drillersLoader, setDrillersLoader] = useState(false)
const [selectedCategory, setSelectedCategory] = useState(14) const [selectedCategory, setSelectedCategory] = useState(5011)
const [categories, setCategories] = useState() const [categories, setCategories] = useState()
const [well] = useWell() const [well] = useWell()
@ -78,7 +78,7 @@ const Operations = memo(() => {
useEffect(() => { useEffect(() => {
invokeWebApiWrapperAsync( invokeWebApiWrapperAsync(
async () => { async () => {
const categories = arrayOrDefault(await DetectedOperationService.getCategories()) const categories = arrayOrDefault(await WellOperationService.getCategories(well.id))
setCategories(categories.map((category) => ({ setCategories(categories.map((category) => ({
...category, ...category,
value: category.id, value: category.id,
@ -86,10 +86,10 @@ const Operations = memo(() => {
}))) })))
}, },
setIsLoading, setIsLoading,
'Не удалось загрзуить категории операций', 'Не удалось загрузить категории операций',
{ actionName: 'Получение категорий операций' } { actionName: 'Получение категорий операций' }
) )
}, []) }, [well])
useEffect(() => { useEffect(() => {
invokeWebApiWrapperAsync( invokeWebApiWrapperAsync(
@ -135,6 +135,7 @@ const Operations = memo(() => {
onChange={setYDomain} onChange={setYDomain}
addonAfter={'мин'} addonAfter={'мин'}
addonBefore={'Верхняя граница'} addonBefore={'Верхняя граница'}
style={{width: '20em'}}
/> />
{permissions.driller.get && ( {permissions.driller.get && (
<> <>
@ -142,8 +143,8 @@ const Operations = memo(() => {
<DrillerList drillers={drillers} loading={drillersLoader} onChange={updateDrillers} /> <DrillerList drillers={drillers} loading={drillersLoader} onChange={updateDrillers} />
</> </>
)} )}
{permissions.detectedOperation.get && permissions.operationValue.get && ( {permissions.detectedOperation.get && permissions.operationValue.get && categories && (
<TargetEditor onChange={updateData} /> <TargetEditor onChange={updateData} options={categories} />
)} )}
</div> </div>
<LoaderPortal show={isLoading}> <LoaderPortal show={isLoading}>

View File

@ -0,0 +1,269 @@
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

View File

@ -20,6 +20,7 @@ import {
import { makeChartGroups, yAxis } from './dataset' import { makeChartGroups, yAxis } from './dataset'
import { ADDITIVE_PAGES, cutData, DATA_COUNT, getLoadingInterval, makeDateTimeDisabled } from './archive_methods' import { ADDITIVE_PAGES, cutData, DATA_COUNT, getLoadingInterval, makeDateTimeDisabled } from './archive_methods'
import LimitingParameterStatistics from './LimitingParameterStatistics'
import ActiveMessagesOnline from './ActiveMessagesOnline' import ActiveMessagesOnline from './ActiveMessagesOnline'
import TelemetrySummary from './TelemetrySummary' import TelemetrySummary from './TelemetrySummary'
import WirelineRunOut from './WirelineRunOut' import WirelineRunOut from './WirelineRunOut'
@ -265,6 +266,7 @@ const TelemetryView = memo(() => {
</Select> </Select>
</div> </div>
<Setpoints /> <Setpoints />
<LimitingParameterStatistics />
<WirelineRunOut /> <WirelineRunOut />
<div className={'icons'}> <div className={'icons'}>
<img src={isTorqueStabEnabled(spinLast) ? MomentStabPicEnabled : MomentStabPicDisabled} alt={'TorqueMaster'} /> <img src={isTorqueStabEnabled(spinLast) ? MomentStabPicEnabled : MomentStabPicDisabled} alt={'TorqueMaster'} />

View File

@ -3,7 +3,6 @@ import {
AlertOutlined, AlertOutlined,
BarChartOutlined, BarChartOutlined,
BuildOutlined, BuildOutlined,
ControlOutlined,
DeploymentUnitOutlined, DeploymentUnitOutlined,
ExperimentOutlined, ExperimentOutlined,
FilePdfOutlined, FilePdfOutlined,
@ -17,7 +16,7 @@ import { makeItem, PrivateMenu } from '@components/PrivateMenu'
export const menuItems = [ export const menuItems = [
makeItem('Телеметрия', 'telemetry', [], <FundViewOutlined />, [ makeItem('Телеметрия', 'telemetry', [], <FundViewOutlined />, [
makeItem('Мониторинг', 'telemetry', [], <FundViewOutlined />), makeItem('Мониторинг', 'monitoring', [], <FundViewOutlined />),
makeItem('Сообщения', 'messages', [], <AlertOutlined />), makeItem('Сообщения', 'messages', [], <AlertOutlined />),
makeItem('ННБ', 'dashboard_nnb', [], <FolderOutlined />), makeItem('ННБ', 'dashboard_nnb', [], <FolderOutlined />),
makeItem('Операции', 'operations', [], <FolderOutlined />), makeItem('Операции', 'operations', [], <FolderOutlined />),

View File

@ -1,6 +1,8 @@
import { useState, useEffect, memo, useMemo, useCallback } from 'react' import { useState, useEffect, memo, useMemo, useCallback, FC } from 'react'
import { Button, Tooltip } from 'antd'
import { FileOutlined } from '@ant-design/icons'
import { useWell } from '@asb/context' import { useWell, useTopRightBlock } from '@asb/context'
import { import {
EditableTable, EditableTable,
makeGroupColumn, makeGroupColumn,
@ -11,10 +13,30 @@ import {
makeSelectColumn, makeSelectColumn,
} from '@components/Table' } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync, download } from '@components/factory'
import { ProcessMapService, WellOperationService } from '@api' import { ProcessMapService, WellOperationService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault } from '@utils'
const style = { margin: 4 }
const ImportExportBar = memo(({ well: givenWell, disabled }) => {
const [wellContext] = useWell()
const well = useMemo(() => givenWell ?? wellContext, [givenWell, wellContext])
const downloadExport = useCallback(
async () => await download(`/api/ProcessMap/getReportFile/${well.id}`),
[well.id]
)
return (
<div>
<Tooltip title={'Выгрузка расширенной автоформируемой РТК'}>
<Button disabled={disabled} icon={<FileOutlined />} style={style} onClick={downloadExport} />
</Tooltip>
</div>
)
})
const numericRender = makeNumericRender(2) const numericRender = makeNumericRender(2)
export const getColumns = async (idWell) => { export const getColumns = async (idWell) => {
@ -25,7 +47,7 @@ export const getColumns = async (idWell) => {
})) }))
return [ return [
makeSelectColumn('Конструкция секции','idWellSectionType', sectionTypes, null, { makeSelectColumn('Конструкция секции', 'idWellSectionType', sectionTypes, null, {
width: 160, width: 160,
sorter: makeNumericSorter('idWellSectionType'), sorter: makeNumericSorter('idWellSectionType'),
}), }),
@ -48,6 +70,7 @@ export const DrillProcessFlow = memo(() => {
const [columns, setColumns] = useState([]) const [columns, setColumns] = useState([])
const [well] = useWell() const [well] = useWell()
const setTopRightBlock = useTopRightBlock()
const updateFlows = useCallback(() => invokeWebApiWrapperAsync( const updateFlows = useCallback(() => invokeWebApiWrapperAsync(
async () => { async () => {
@ -75,6 +98,10 @@ export const DrillProcessFlow = memo(() => {
updateFlows() updateFlows()
}, [well]) }, [well])
useEffect(() => setTopRightBlock((well) => (
<ImportExportBar well={well} />
)), [setTopRightBlock])
const tableHandlers = useMemo(() => { const tableHandlers = useMemo(() => {
const handlerProps = { const handlerProps = {
service: ProcessMapService, service: ProcessMapService,
@ -88,7 +115,12 @@ export const DrillProcessFlow = memo(() => {
return { return {
add: { ...handlerProps, action: 'insert', actionName: 'Добавление месторождения', recordParser }, add: { ...handlerProps, action: 'insert', actionName: 'Добавление месторождения', recordParser },
edit: { ...handlerProps, action: 'update', actionName: 'Редактирование месторождения', recordParser }, edit: { ...handlerProps, action: 'update', actionName: 'Редактирование месторождения', recordParser },
delete: { ...handlerProps, action: 'delete', actionName: 'Удаление месторождения', permission: 'DrillFlowChart.delete' }, delete: {
...handlerProps,
action: 'delete',
actionName: 'Удаление месторождения',
permission: 'DrillFlowChart.delete',
},
} }
}, [updateFlows, well.id]) }, [updateFlows, well.id])

View File

@ -6,8 +6,6 @@ import { useState, useEffect, memo, useMemo, useCallback } from 'react'
import { useTopRightBlock, useWell } from '@asb/context' import { useTopRightBlock, useWell } from '@asb/context'
import { import {
EditableTable, EditableTable,
makeColumn,
makeNumericColumnOptions,
makeSelectColumn, makeSelectColumn,
makeDateColumn, makeDateColumn,
makeNumericColumn, makeNumericColumn,
@ -72,11 +70,10 @@ export const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => {
const skip = ((pageNumAndPageSize.current - 1) * pageNumAndPageSize.pageSize) || 0 const skip = ((pageNumAndPageSize.current - 1) * pageNumAndPageSize.pageSize) || 0
const take = pageNumAndPageSize.pageSize const take = pageNumAndPageSize.pageSize
const paginatedOperations = await WellOperationService.getOperations(well.id, const paginatedOperations = await WellOperationService.getOperations(well.id,
idType, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, idType, undefined, skip, take)
undefined, undefined, skip, take)
const operations = paginatedOperations?.items ?? [] const operations = paginatedOperations?.items ?? []
setOperations(operations) setOperations(operations)
const total = paginatedOperations.count?? paginatedOperations.items?.length ?? 0 const total = paginatedOperations.count ?? paginatedOperations.items?.length ?? 0
setPaginationTotal(total) setPaginationTotal(total)
}, },
setShowLoader, setShowLoader,

View File

@ -1,5 +1,4 @@
import { memo, useEffect, useMemo, useState } from 'react' import { memo, useEffect, useMemo, useState } from 'react'
import { useElementSize } from 'usehooks-ts'
import { Empty } from 'antd' import { Empty } from 'antd'
import moment from 'moment' import moment from 'moment'
import * as d3 from 'd3' import * as d3 from 'd3'
@ -8,7 +7,7 @@ import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { DetectedOperationService } from '@api' import { DetectedOperationService } from '@api'
import { unique } from '@utils/filters' import { unique } from '@utils/filters'
import { formatDate } from '@utils' import { formatDate, useElementSize } from '@utils'
import { makeTooltipRender } from '../../Telemetry/Operations/OperationsChart' import { makeTooltipRender } from '../../Telemetry/Operations/OperationsChart'
import { makeGetColor } from '.' import { makeGetColor } from '.'

View File

@ -1,5 +1,4 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useElementSize } from 'usehooks-ts'
import { Empty } from 'antd' import { Empty } from 'antd'
import * as d3 from 'd3' import * as d3 from 'd3'
@ -8,6 +7,7 @@ import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { DetectedOperationService } from '@api' import { DetectedOperationService } from '@api'
import { unique } from '@utils/filters' import { unique } from '@utils/filters'
import { useElementSize } from '@utils'
import { makeGetColor } from '.' import { makeGetColor } from '.'

View File

@ -95,10 +95,11 @@ const Well = memo(() => {
<Route path={'*'} element={<NoAccessComponent />} /> <Route path={'*'} element={<NoAccessComponent />} />
<Route path={'telemetry/*'} element={<Telemetry />}> <Route path={'telemetry/*'} element={<Telemetry />}>
<Route index element={<Navigate to={'telemetry'} replace />} /> <Route index element={<Navigate to={'monitoring'} replace />} />
<Route path={'*'} element={<NoAccessComponent />} /> <Route path={'*'} element={<NoAccessComponent />} />
<Route path={'telemetry'} element={<Navigate to={'../monitoring'} replace />} /> {/* TODO: Remove in next release */}
<Route path={'telemetry'} element={<TelemetryView />} /> <Route path={'monitoring'} element={<TelemetryView />} />
<Route path={'messages'} element={<Messages />} /> <Route path={'messages'} element={<Messages />} />
<Route path={'dashboard_nnb/*'} element={<DashboardNNB />} /> <Route path={'dashboard_nnb/*'} element={<DashboardNNB />} />
<Route path={'operations'} element={<Operations />} /> <Route path={'operations'} element={<Operations />} />

View File

@ -11,7 +11,7 @@ export const AccessDenied = memo(() => {
return ( return (
<Result <Result
status={'error'} status={'error'}
title={'Доступ запрешён'} title={'Доступ запрещён'}
subTitle={'Страницы не существует или у вас отсутствует к ней доступ.'} subTitle={'Страницы не существует или у вас отсутствует к ней доступ.'}
> >
<div className={'desc'}> <div className={'desc'}>

View File

@ -0,0 +1,29 @@
.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;
}

View File

@ -1,4 +1,5 @@
export * from './cachedFetch' export * from './cachedFetch'
export * from './functionalValue' export * from './functionalValue'
export * from './useElementSize'
export * from './usePartialProps' export * from './usePartialProps'
export * from './useUserSettings' export * from './useUserSettings'

View File

@ -0,0 +1,24 @@
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]
}