Compare commits

...

78 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
923d469a86 Merge branch 'dev' into feature/documentation 2022-12-19 08:07:37 +05:00
8f52066bce Добавлена документация компонента LoaderPortal 2022-12-19 08:07:08 +05:00
bfd1e51cfa Контексты разнесены по файлам 2022-12-19 08:05:30 +05:00
0e63d93fa7 Добавлена первая версия стандарта кода 2022-12-19 08:05:00 +05:00
0ef6d67772 * Страница РТК приведена к новой модели
* РТК вырезано из мониторинга
* Удалена страница "Режимы"
* Удалено формирование новых режимов на странице "Композитная скважина"
2022-12-13 18:30:05 +05:00
f4adb528ca * Изменена сигнатура функций makeNumericColumn и её обёрток
* Улучшено отображение некоторых элементов
2022-12-13 18:28:30 +05:00
7af33d702d Документирована часть комментариев и функций 2022-12-13 15:29:19 +05:00
d235b01c80 Стили разделены по директориям 2022-12-08 12:13:58 +05:00
5af996f9e5 Стили вынесены в отдельный файл 2022-12-08 11:48:30 +05:00
65b9ede580 Файл страницы Наработки АКБ переименован 2022-12-08 11:18:42 +05:00
08fcf2736e Таблица на странице Наработка АКБ переработана в подсказку 2022-12-08 11:14:37 +05:00
28a0962793 Доработка страницы Наработки АКБ
* Добавлены цвета
* Добавлены иконки для подсистем
* Добавлена фильтрация на null
2022-12-08 10:55:11 +05:00
8f98cc066c Функция range ускорена в 5 раз 2022-12-08 08:26:10 +05:00
4b20a44d88 Добавлена базовая версия страницы 2022-12-06 04:44:03 +05:00
043f73fde3 Крошки переработаны 2022-12-06 00:56:02 +05:00
1a737b6afe Добавлена обработка в случае, когда не выбрано месторождение/куст/скважина 2022-12-06 00:51:41 +05:00
17d7b7c41d * Добавленна ссылка на месторождение в подсказке значка на карте
* Исправлено выделение месторождения в селекторе
2022-12-06 00:23:45 +05:00
4dd57aff98 Добавлено меню навигации для раздела месторождения 2022-12-05 22:49:55 +05:00
ebe3a50fbe WellTreeSelector доработан для работы с нецифровыми значениями в местах смены переменных 2022-12-05 21:07:17 +05:00
dc0f80fee5 Исправлено название для контекста списка месторождений 2022-12-05 20:39:14 +05:00
cb8be79274 Переработан PrivateMenu для работы с любыми указанными переменными 2022-12-05 20:35:29 +05:00
ee289cc619 Удалена колонка общего времени работы уставки 2022-11-29 12:17:17 +05:00
e430cdd5b4 Добавлена использование новых полей 2022-11-29 12:11:33 +05:00
540da341da Merge branch 'master' into dev 2022-11-28 10:15:21 +05:00
20b271d91e Логотип вынесен в отдельный SVG. Добавлен логотип АСБ на случай замены 2022-11-28 10:15:01 +05:00
259e2e4be8 Добавлен черновик окна статистики использования уставок 2022-11-28 10:13:40 +05:00
0cbd9559f2 Исправлена ошибка с ключами при отображении подсказки мониторинга 2022-11-28 07:35:42 +05:00
40d3a77c1c Исправлена работа истории программы бурения 2022-11-28 05:59:20 +05:00
7731dfe8e7 Добавлена кнопка оповещения ответственных по категориям на странице "Дело скважины" 2022-11-28 05:52:24 +05:00
7acb7ce2b2 Улучшено отображение РТК в подсказке мониторинга 2022-11-28 05:18:55 +05:00
0aeef42811 комбинация map и flat заменена на flatMap 2022-11-28 05:18:16 +05:00
5eb66e5fbc Упоминания archive устаранены 2022-11-28 05:17:33 +05:00
44afd5f1f0 * Данные РТК вынесены в отдельный проп графика
* Добавлена прозрачность РТК
* Добавлен метод расчёта данных для РТК
2022-11-22 09:26:44 +05:00
a2d641abdd Hotfix Отображение верхнего блока 2022-11-22 07:24:23 +05:00
51ac260c74 Исправлена работа крошек 2022-11-21 12:04:48 +05:00
16fb37910f Дополнены кофигурации webpack 2022-11-21 11:31:33 +05:00
de7e8fd259 Исправлена ошибка работы панели импорта/экспорта 2022-11-21 11:20:40 +05:00
a9aeeb6da3 Исправлена ошибка выхода из режима архива при резком прокручивании графика вниз 2022-11-21 11:02:50 +05:00
fe52116a9b Улучшено отображение при переходе в архив 2022-11-21 10:58:02 +05:00
d56810700f Страница архива удалена 2022-11-21 10:41:33 +05:00
e39ce8d410 Улучшено отображение автоопределяемых диапазонов 2022-11-21 10:40:47 +05:00
bd81aa0401 Добавлена фильтрация отображаемых данных на телеметрии 2022-11-21 10:40:27 +05:00
59b1d49286 Добавлена подгрузка данных для архива 2022-11-21 08:56:44 +05:00
871e71e777 Дата удалена из текущих значений 2022-11-21 08:49:21 +05:00
fc91cfc6ff Исправлена ошибка отображения регуляторов 2022-11-21 08:49:03 +05:00
3216a90af3 К кнопке копирования URL добавлена подсказка 2022-11-21 08:47:59 +05:00
11a632c246 * Мониторинг переписан на flex
* Блок текущих значений перемещён наверх и переписан
* Выделена строка управления графиком
* Удалена мнемосхема
2022-11-16 11:58:03 +05:00
b2c34d07a9 Добавлена replace для handleDataSaub 2022-11-16 11:29:53 +05:00
ec2513b4a0 Merge branch 'dev' into feature/merging-telemetry-view-and-archive 2022-11-16 11:29:06 +05:00
6375db5a0b Merge branch 'dev' into feature/merging-telemetry-view-and-archive 2022-11-08 08:13:33 +05:00
17ccecb2dd Выделены методы генерации датасетов для графика мониторинга 2022-10-31 05:12:33 +05:00
171 changed files with 2938 additions and 1886 deletions

15
.vscode/settings.json vendored
View File

@ -1,6 +1,17 @@
{ {
"cSpell.words": [ "cSpell.words": [
"день" "день",
"спиннера",
"Saub",
"КНБК",
"САУБ",
"antd",
"Poprompt",
"saub",
"setpoint",
"Setpoints",
"usehooks"
], ],
"liveServer.settings.port": 5501 "liveServer.settings.port": 5501,
"cSpell.language": "en,ru"
} }

191
CODE_STANDART.md Normal file
View File

@ -0,0 +1,191 @@
## 1. Общие положения
1. Все несамостоятельные компоненты должны быть написаны на TypeScript. Для самостоятельных компонентов (использующихся как страницы) (далее страницы) допускается использование JavaScript для ускорения написания;
### 1.1. Файловая структура проекта
1. Компоненты должны распределяться по директориям в соответствии со своим назначением:
* `src/context` - Для контекстов приложения;
* `src/components` - Для несамостоятельных компонентов, применяющихся многократно;
* `src/pages` - Для страниц и компонентов, использующихся исключительно в единственном экземпляре;
* `src/images` - Для компонентов-изображений.
2. Если страница описывается 1 файлом она должна именоваться в соответствии с содержимым, в ином случае должна быть создана директория с соответствующим названием, внутри которой будут находиться файлы страницы. Основной файл в таком случае должен быть переименован в `index.jsx`;
3. Файлы именуются в соответствии с таблицей:
| Тип содержимого файла | Расширение | Стиль именования |
|--------------------------------------|------------|--------------------------|
| Компонент или страница | jsx/tsx | **PascalCase** |
| Файл стилей | css/less | **snake_case** |
| Вспомогательные методы или константы | js/ts | **snake_case** |
| Описательные документы | md | **SCREAMING_SNAKE_CASE** |
### 1.2. Стилизация кода
1. Все строки должны по возможности описываться одинарными кавычками или при необходимости обратными:
```js
const name = 'world'
const msg = 'Hello, \'' + name + '\'!'
const toPrint = `Message: ${msg}`
```
2. Все переменные по возможности должны инициализироваться как `const`, применение `var` не допускается;
3. Переменные именуются в соответствии с таблицей:
| Тип переменной | Стиль именования |
|-------------------|--------------------------|
| Метод, переменная | **camelCase** |
| Константы | **SCREAMING_SNAKE_CASE** |
| Компонент | **PascalCase** |
### 1.3. Импортирование / Экспортирование
1. Импортированные файлы (в том числе lazy import) необходимо указывать в самом верху документа в следующем порядке с разделением пустой строкой:
1. Внешние зависимости (`react`, `antd`, `webpack` и т.д.);
2. Локальные компоненты по порядку:
1. Контексты (`@asb/context`);
2. Компоненты (`@components/Table`);
3. Вспомогательные методы (`@utils`);
4. Сервисы API (`@api`).
3. Изображения и компоненты-изображения (`@images`);
4. Стили (`@styles`);
5. Lazy import (`const page = React.lazy(() => import('./page'))`).
2. При импорте локальных файлов стоит пользоваться alias'ами:
| Путь | Alias |
|------------------|--------------|
| src/components | @components |
| src/context | @asb/context |
| src/images | @images |
| src/pages | @pages |
| src/services/api | @api |
| src/styles | @styles |
| src/utils | @utils |
3. По возможности импортировать из пакетов и файлов только использующиеся сущности:
```tsx
// вместо
import React from 'react'
const page: React.ReactNode = React.lazy(() => import (...))
// стоит использовать
import { lazy, ReactNode } from 'react'
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. При документации страниц необходимо указать её название, краткое описание и описание получаемых параметров:
```jsx
import { memo } from 'react'
import LoaderPortal from '@components/LoaderPortal'
/**
* Тестовая страница
*
* @description Данная страница не имеет смысла и просто выводит переданное название и контент
* @param title - Название страницы
* @param content - Контент страницы
* @param loading - Отображать ли оверлей загрузки над блоком страницы
*/
export const TestPage = memo(({ title, content, loading }) => (
<LoaderPortal show={loading}>
<div className={'dd-test-page'}>
<div className={'dd-test-page-title'}>{title}</div>
<div className={'dd-test-page-content'}>{content}</div>
</div>
</LoaderPortal>
))
export default TestPage
```
## 3. TS
1. Методы, константы и переменные документируются в соответствии с `TSDoc`;
2. При документации компонентов необходимо указать их название, краткое описание, а также описать параметры в типе:
```tsx
import { memo } from 'react'
import LoaderPortal from '@components/LoaderPortal'
export type TestPageProps = {
/** Название страницы */
title: ReactNode
/** Контент страницы */
content: ReactNode
/** Отображать ли оверлей загрузки над блоком страницы */
loading: boolean
}
/**
* Тестовая страница
*
* @description Данная страница не имеет смысла и просто выводит переданное название и контент
*/
export const TestPage = memo<TestPageProps>(({ title, content, loading }) => (
<LoaderPortal show={loading}>
<div className={'dd-test-page'}>
<div className={'dd-test-page-title'}>{title}</div>
<div className={'dd-test-page-content'}>{content}</div>
</div>
</LoaderPortal>
))
export default TestPage
```
3. Использование `any` в типах допустимо только, если значение используется только в параметрах компонентов, обозначенных типом `any`. Если метод предполагает работу с разными типами значений стоит описать его как обобщённый.
## 4. JSX/TSX
### 4.1. Стилизация кода
1. Все указываемые к компоненту параметры должны быть обёрнуты в фигурные скобки, кроме параметров флагов со значением `true`:
```jsx
<Button disabled title={'Hello, world!'} type={'ghost'}>Click me!</Button>
```
2. Если описание параметров компонента не укладывается в ширину в 120 строк стоит перенести их в соответствии с шаблоном:
```jsx
<Button
disabled
title={'Hello, world!'}
type={'ghost'}
>
Click me!
</Button>
```
3. Если JSX код передаётся как значение стоит обернуть его в круглые скобки:
```jsx
const a = (
<Button disabled title={'Hello, world!'} type={'ghost'}>Click me!</Button>
)
```
### 4.2. Логика поведения
1. Не допускается создание значений ссылочных типов в области рендера. Они должны быть вынесены в переменные или константы;
2. Не допускается создание переменных в функциональных компонентов без использования хуков `useMemo`/`useCallback`/`useState`;
3. Если переменные или методы не имеют зависимостей и не вызывают методы, доступные исключительно внутри компонента, они должны быть вынесены выше кода компонента.
## 5. LESS
1. Использование id должно быть сведено к минимуму;
2. Все классы именуются с префиксом компании "`dd-`";
3. Слова в классах разделяются тире ("`-`");
4. Файлы именуются в соответствии с компонентом, к которому относятся;
5. В одном файле описываются стили либо к конкретному компоненту, либо к странице;
6. Файл со стилями должен подключаться не более чем к одному компоненту (странице);
7. Файлы поделены на директории виду компонента, к которому применяются стили:
* `styles/components` - для компонентов, не использующихся самостоятельно;
* `styles/pages` - для компонентов, использующихся как страница;
* `styles/widgets` - для компонентов, применяющихся как виджеты в дашбордах.

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

@ -5,7 +5,7 @@ import { RootPathContext } from '@asb/context'
import SuspenseFallback from '@components/SuspenseFallback' import SuspenseFallback from '@components/SuspenseFallback'
import { NoAccessComponent } from '@utils' import { NoAccessComponent } from '@utils'
import '@styles/App.less' import '@styles/pages/App.less'
const UserOutlet = lazy(() => import('@components/outlets/UserOutlet')) const UserOutlet = lazy(() => import('@components/outlets/UserOutlet'))
const DepositsOutlet = lazy(() => import('@components/outlets/DepositsOutlet')) const DepositsOutlet = lazy(() => import('@components/outlets/DepositsOutlet'))

View File

@ -1,5 +1,5 @@
import { cloneElement, memo, useCallback, useMemo, useState } from 'react' import { cloneElement, memo, useCallback, useMemo, useState } from 'react'
import { Button, ButtonProps } from 'antd' import { Button, ButtonProps, Tooltip } from 'antd'
import { CopyOutlined } from '@ant-design/icons' import { CopyOutlined } from '@ant-design/icons'
import { invokeWebApiWrapperAsync, notify } from './factory' import { invokeWebApiWrapperAsync, notify } from './factory'
@ -43,11 +43,9 @@ export type CopyUrlButtonProps = Omit<CopyUrlProps, 'children'> & ButtonProps
export const CopyUrlButton = memo<CopyUrlButtonProps>(({ sendLoading, hideUnsupported, onCopy, ...other }) => { export const CopyUrlButton = memo<CopyUrlButtonProps>(({ sendLoading, hideUnsupported, onCopy, ...other }) => {
return ( return (
<CopyUrl sendLoading={sendLoading} hideUnsupported={hideUnsupported} onCopy={onCopy}> <CopyUrl sendLoading={sendLoading} hideUnsupported={hideUnsupported} onCopy={onCopy}>
<Button <Tooltip title={'Скопировать URL в буфер обмена'}>
icon={<CopyOutlined />} <Button icon={<CopyOutlined />} {...other} />
title={'Скопировать URL в буфер обмена'} </Tooltip>
{...other}
/>
</CopyUrl> </CopyUrl>
) )
}) })

View File

@ -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<string, string>
value: string
}
export type DisplayProps = ValueDisplayProps & {
className?: string
label?: ReactNode
}
export const ValueDisplay = memo<ValueDisplayProps>(({ prefix, value, suffix, isArrowVisible, format, enumeration }) => {
const [val, setVal] = useState<ReactNode>('---')
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 = <CaretRightOutlined style={iconStyle}/>
break
case 1:
arrow = <CaretUpOutlined style={iconStyle}/>
break
case -1:
arrow = <CaretDownOutlined style={iconStyle}/>
break
default:
break
}
return(
<span className={'display_value'}>
{prefix} {val} {suffix}{arrow}
</span>
)
})
export const Display = memo<DisplayProps>(({ className, label, ...other })=> (
<div className={className}>
<div className={'display_label'}>{label}</div>
<div style={displayValueStyle}>
<ValueDisplay {...other}/>
</div>
</div>
))

View File

@ -5,13 +5,13 @@ import { AutoComplete } from 'antd'
import { join } from 'path' import { join } from 'path'
import { useWell } from '@asb/context' import { useWell } from '@asb/context'
import { makeItem, PrivateWellMenuItem } from './PrivateWellMenu' import { makeItem, PrivateMenuItem } from './PrivateMenu'
import { hasPermission, isURLAvailable } from '@utils' import { hasPermission, isURLAvailable } from '@utils'
import { menuItems as adminMenuItems } from '@pages/AdminPanel/AdminNavigationMenu' import { menuItems as adminMenuItems } from '@pages/AdminPanel/AdminNavigationMenu'
import { menuItems as wellMenuItems } from '@pages/Well/NavigationMenu' import { menuItems as wellMenuItems } from '@pages/Well/WellNavigationMenu'
import '@styles/fast_run_menu.less' import '@styles/components/fast_run_menu.less'
const transliterationTable = { const transliterationTable = {
'q': 'й', 'w': 'ц', 'e': 'у', 'r': 'к', 't': 'е', 'y': 'н', 'u': 'г', 'i': 'ш', 'o': 'щ', 'p': 'з', '[': 'х', ']': 'ъ', '{': 'х', '}': 'ъ', 'q': 'й', 'w': 'ц', 'e': 'у', 'r': 'к', 't': 'е', 'y': 'н', 'u': 'г', 'i': 'ш', 'o': 'щ', 'p': 'з', '[': 'х', ']': 'ъ', '{': 'х', '}': 'ъ',
@ -29,7 +29,7 @@ const transliterateToEn = (text: string) => Object.entries(transliterationTable)
const applyVars = (route: string, vars?: object): string => !vars ? route : const applyVars = (route: string, vars?: object): string => !vars ? route :
Object.entries(vars).reduce((out, [key, value]) => out.replaceAll(`{${key}}`, value), route) Object.entries(vars).reduce((out, [key, value]) => out.replaceAll(`{${key}}`, value), route)
const makeOptions = (items: PrivateWellMenuItem[], vars?: object): OptionType[] => { const makeOptions = (items: PrivateMenuItem[], vars?: object): OptionType[] => {
const out: OptionType[] = [] const out: OptionType[] = []
items.forEach((item) => { items.forEach((item) => {
if (!hasPermission(item.permissions)) return if (!hasPermission(item.permissions)) return
@ -78,7 +78,7 @@ export const FastRunMenu = memo(() => {
] ]
if (isURLAvailable('/admin')) if (isURLAvailable('/admin'))
menus.push(makeItem('Панель администратора', '/admin', [], undefined, adminMenuItems as PrivateWellMenuItem[])) menus.push(makeItem('Панель администратора', '/admin', [], undefined, adminMenuItems as PrivateMenuItem[]))
if (well.id) if (well.id)
menus.push( menus.push(

View File

@ -1,7 +1,7 @@
import { Breadcrumb, Layout, LayoutProps, Menu, SiderProps } from 'antd' import { Breadcrumb, Layout, LayoutProps, Menu, SiderProps } from 'antd'
import { Key, memo, ReactNode, Suspense, useCallback, useEffect, useMemo, useState } from 'react' import { Key, memo, ReactNode, Suspense, useCallback, useEffect, useMemo, useState } from 'react'
import { ItemType } from 'antd/lib/menu/hooks/useItems' import { ItemType } from 'antd/lib/menu/hooks/useItems'
import { Link, Outlet } from 'react-router-dom' import { Link, Outlet, useLocation } from 'react-router-dom'
import { import {
ApartmentOutlined, ApartmentOutlined,
CodeOutlined, CodeOutlined,
@ -18,7 +18,7 @@ import SuspenseFallback from './SuspenseFallback'
import Logo from '@images/Logo' import Logo from '@images/Logo'
import '@styles/layout.less' import '@styles/components/layout.less'
const { Content, Sider } = Layout const { Content, Sider } = Layout
@ -31,7 +31,7 @@ export type LayoutPortalProps = Omit<LayoutProps, 'children'> & {
siderProps?: SiderProps & { userMenuProps?: UserMenuProps } siderProps?: SiderProps & { userMenuProps?: UserMenuProps }
isAdmin?: boolean isAdmin?: boolean
fallback?: JSX.Element fallback?: JSX.Element
breadcrumb?: boolean | JSX.Element breadcrumb?: boolean | ((path: string) => JSX.Element)
topRightBlock?: JSX.Element topRightBlock?: JSX.Element
} }
@ -49,6 +49,7 @@ const _LayoutPortal = memo(() => {
const [userMenuOpen, setUserMenuOpen] = useState<boolean>(false) const [userMenuOpen, setUserMenuOpen] = useState<boolean>(false)
const [currentWell, setCurrentWell] = useState<string>('') const [currentWell, setCurrentWell] = useState<string>('')
const [props, setProps] = useState<LayoutPortalProps>(defaultProps) const [props, setProps] = useState<LayoutPortalProps>(defaultProps)
const location = useLocation()
const { isAdmin, title, sheet, showSelector, selectorProps, sider, siderProps, fallback, breadcrumb, topRightBlock, ...other } = useMemo(() => props, [props]) const { isAdmin, title, sheet, showSelector, selectorProps, sider, siderProps, fallback, breadcrumb, topRightBlock, ...other } = useMemo(() => props, [props])
@ -66,6 +67,8 @@ const _LayoutPortal = memo(() => {
makeItem('Профиль', 'profile', <UserOutlined/>, null, () => setUserMenuOpen((prev) => !prev)), makeItem('Профиль', 'profile', <UserOutlined/>, null, () => setUserMenuOpen((prev) => !prev)),
].filter(Boolean) as ItemType[], [isAdmin, currentWell]) ].filter(Boolean) as ItemType[], [isAdmin, currentWell])
const breadcrumbItems = useMemo(() => typeof breadcrumb === 'function' && breadcrumb(location.pathname), [breadcrumb, location.pathname])
return ( return (
<Layout className={`page-layout ${isAdmin ? 'page-layout-admin' : ''}`}> <Layout className={`page-layout ${isAdmin ? 'page-layout-admin' : ''}`}>
{(sider || siderProps) && ( {(sider || siderProps) && (
@ -114,7 +117,7 @@ const _LayoutPortal = memo(() => {
<a style={{ userSelect: 'none' }} onClick={() => setWellsTreeOpen((prev) => !prev)}>{currentWell}</a> <a style={{ userSelect: 'none' }} onClick={() => setWellsTreeOpen((prev) => !prev)}>{currentWell}</a>
</Breadcrumb.Item> </Breadcrumb.Item>
)} )}
{breadcrumb !== true && breadcrumb} {breadcrumbItems}
</Breadcrumb> </Breadcrumb>
)} )}
{topRightBlock} {topRightBlock}

View File

@ -3,12 +3,19 @@ import { HTMLAttributes } from 'react'
import { Loader } from '@components/icons' import { Loader } from '@components/icons'
type LoaderPortalProps = HTMLAttributes<HTMLDivElement> & { type LoaderPortalProps = HTMLAttributes<HTMLDivElement> & {
/** Показать ли загрузку */
show?: boolean, show?: boolean,
/** Затемнять ли дочерний блок */
fade?: boolean, fade?: boolean,
/** Параметры спиннера */
spinnerProps?: HTMLAttributes<HTMLDivElement>, spinnerProps?: HTMLAttributes<HTMLDivElement>,
/** Заполнять ли контент на 100% */
fillContent?: boolean fillContent?: boolean
} }
/**
* @description Добавляет оверлей загрузки над обёрнутым блоком
*/
export const LoaderPortal: React.FC<LoaderPortalProps> = ({ className = '', show, fade = true, children, spinnerProps, fillContent, ...other }) => ( export const LoaderPortal: React.FC<LoaderPortalProps> = ({ className = '', show, fade = true, children, spinnerProps, fillContent, ...other }) => (
<div className={`loader-container ${className}`} {...other}> <div className={`loader-container ${className}`} {...other}>
<div className={`loader-content${fillContent ? ' loader-content-fill' : ''}`}>{children}</div> <div className={`loader-content${fillContent ? ' loader-content-fill' : ''}`}>{children}</div>

View File

@ -2,16 +2,16 @@ import { Breadcrumb, BreadcrumbItemProps } from 'antd'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { join } from 'path' import { join } from 'path'
import { PrivateWellMenuItem } from '@components/PrivateWellMenu' import { PrivateMenuItem } from '@components/PrivateMenu'
import { FunctionalValue, getFunctionalValue, } from '@utils' import { FunctionalValue, getFunctionalValue, } from '@utils'
export const makeBreadcrumbItems = (items: PrivateWellMenuItem[], pathParts: string[], root: string = '/') => { export const makeBreadcrumbItems = (items: PrivateMenuItem[], pathParts: string[], root: string = '/') => {
const out = [] const out = []
const parts = [...pathParts] const parts = [...pathParts]
let route = root let route = root
let arr: PrivateWellMenuItem[] | undefined = items let arr: PrivateMenuItem[] | undefined = items
while (arr && parts.length > 0) { while (arr && parts.length > 0) {
const child: PrivateWellMenuItem | undefined = arr.find(elm => elm.route.toLowerCase() === parts[0].toLowerCase()) const child: PrivateMenuItem | undefined = arr.find(elm => elm.route.toLowerCase() === parts[0].toLowerCase())
if (!child) break if (!child) break
route = join(route, child.route) route = join(route, child.route)
out.push({ ...child, route }) out.push({ ...child, route })
@ -21,13 +21,12 @@ export const makeBreadcrumbItems = (items: PrivateWellMenuItem[], pathParts: str
return out return out
} }
export const makeMenuBreadcrumbItems = ( export const makeMenuBreadcrumbItemsRender = (
menuItems: PrivateWellMenuItem[], menuItems: PrivateMenuItem[],
path: string,
pathRoot: RegExp = /^\//, pathRoot: RegExp = /^\//,
itemsProps?: FunctionalValue<(item: PrivateWellMenuItem) => BreadcrumbItemProps>, itemsProps?: FunctionalValue<(item: PrivateMenuItem) => BreadcrumbItemProps>,
itemRender?: (item: PrivateWellMenuItem) => JSX.Element, itemRender?: (item: PrivateMenuItem) => JSX.Element,
) => { ) => (path: string) => {
const getItemProps = getFunctionalValue(itemsProps) const getItemProps = getFunctionalValue(itemsProps)
const rootPart = pathRoot.exec(path) const rootPart = pathRoot.exec(path)

View File

@ -1,21 +1,21 @@
import { ItemType } from 'antd/lib/menu/hooks/useItems' import { ItemType } from 'antd/lib/menu/hooks/useItems'
import { Menu, MenuProps } from 'antd'
import { memo, ReactNode, useMemo } from 'react' import { memo, ReactNode, useMemo } from 'react'
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { join } from 'path' import { join } from 'path'
import { Menu, MenuProps } from 'antd'
import { hasPermission, Permission } from '@utils' import { hasPermission, Permission } from '@utils'
export type PrivateWellMenuItem = { export type PrivateMenuItem = {
title: string title: string
route: string route: string
permissions: Permission | Permission[] permissions: Permission | Permission[]
icon?: ReactNode icon?: ReactNode
visible?: boolean visible?: boolean
children?: PrivateWellMenuItem[] children?: PrivateMenuItem[]
} }
const makeItems = (items: PrivateWellMenuItem[], parentRoute: string, pathParser?: (path: string, parent: string) => string): ItemType[] => { const makeItems = (items: PrivateMenuItem[], parentRoute: string, pathParser?: (path: string, parent: string) => string): ItemType[] => {
return items.map((item) => { return items.map((item) => {
if (item.visible === false || !(item.visible === true || hasPermission(item.permissions))) return null if (item.visible === false || !(item.visible === true || hasPermission(item.permissions))) return null
@ -43,11 +43,11 @@ const makeItems = (items: PrivateWellMenuItem[], parentRoute: string, pathParser
}).filter(Boolean) }).filter(Boolean)
} }
const makeItemList = (items: PrivateWellMenuItem[], rootPath: string, wellId?: number): ItemType[] => { const makeItemList = (items: PrivateMenuItem[], rootPath: string, variables: Record<string, number | string>): ItemType[] => {
const parser = (path: string, parent: string) => { const parser = (path: string, parent: string) => {
if (!path.startsWith('/')) if (!path.startsWith('/'))
path = join(parent, path) path = join(parent, path)
return path.replace(/\{wellId\}/, String(wellId)) return Object.entries(variables).reduce((out, [key, value]) => out.replaceAll(`{${key}}`, String(value)), path)
} }
return makeItems(items, rootPath, parser) return makeItems(items, rootPath, parser)
@ -58,9 +58,9 @@ export const makeItem = (
route: string, route: string,
permissions: Permission | Permission[], permissions: Permission | Permission[],
icon?: ReactNode, icon?: ReactNode,
children?: PrivateWellMenuItem[], children?: PrivateMenuItem[],
visible?: boolean visible?: boolean
): PrivateWellMenuItem => ({ ): PrivateMenuItem => ({
title, title,
route, route,
icon, icon,
@ -69,16 +69,16 @@ export const makeItem = (
visible, visible,
}) })
export type PrivateWellMenuProps = Omit<MenuProps, 'items'> & { export type PrivateMenuProps = Omit<MenuProps, 'items'> & {
idWell?: number variables?: Record<string, number | string>
items: PrivateWellMenuItem[] items: PrivateMenuItem[]
rootPath?: string rootPath?: string
} }
export const PrivateWellMenu = memo<PrivateWellMenuProps>(({ idWell, items, rootPath = '/', ...other }) => { export const PrivateMenu = memo<PrivateMenuProps>(({ variables, items, rootPath = '/', ...other }) => {
const location = useLocation() const location = useLocation()
const menuItems = useMemo(() => makeItemList(items, rootPath, idWell), [items, rootPath, idWell]) const menuItems = useMemo(() => makeItemList(items, rootPath, variables || {}), [items, rootPath, variables])
const tabKeys = useMemo(() => { const tabKeys = useMemo(() => {
const out = [] const out = []

View File

@ -5,6 +5,11 @@ import { DatePickerWrapper, getObjectByDeepKey } from '..'
import { DatePickerWrapperProps } from '../DatePickerWrapper' import { DatePickerWrapperProps } from '../DatePickerWrapper'
import { formatDate, isRawDate } from '@utils' import { formatDate, isRawDate } from '@utils'
/**
* Фабрика методов сортировки столбцов для данных типа **Дата**
* @param key Ключ столбца
* @returns Метод сортировки
*/
export const makeDateSorter = <T extends unknown>(key: Key): SorterMethod<T> => (a, b) => { export const makeDateSorter = <T extends unknown>(key: Key): SorterMethod<T> => (a, b) => {
const vA = a ? getObjectByDeepKey(key, a) : null const vA = a ? getObjectByDeepKey(key, a) : null
const vB = b ? getObjectByDeepKey(key, b) : null const vB = b ? getObjectByDeepKey(key, b) : null
@ -16,6 +21,17 @@ export const makeDateSorter = <T extends unknown>(key: Key): SorterMethod<T> =>
return (new Date(vA)).getTime() - (new Date(vB)).getTime() return (new Date(vA)).getTime() - (new Date(vB)).getTime()
} }
/**
* Фабрика объектов-столбцов для компонента `Table` для работы с данными типа **Дата**
*
* @param title Название столбца
* @param key Ключ столбца
* @param utc Конвертировать ли дату в UTC
* @param format Формат отображения даты
* @param other Дополнительные опции столбца
* @param pickerOther Опции компонента селектора даты
* @returns Объект-столбец для работы с данными типа **Дата**
*/
export const makeDateColumn = <T extends unknown>( export const makeDateColumn = <T extends unknown>(
title: ReactNode, title: ReactNode,
key: string, key: string,
@ -24,6 +40,7 @@ export const makeDateColumn = <T extends unknown>(
other?: ColumnProps<T>, other?: ColumnProps<T>,
pickerOther?: DatePickerWrapperProps, pickerOther?: DatePickerWrapperProps,
) => makeColumn<T>(title, key, { ) => makeColumn<T>(title, key, {
editable: true,
...other, ...other,
render: (date) => ( render: (date) => (
<div className={'text-align-r-container'}> <div className={'text-align-r-container'}>

View File

@ -9,7 +9,6 @@ import { OmitExtends } from '@utils/types'
export * from './date' export * from './date'
export * from './time' export * from './time'
export * from './numeric' export * from './numeric'
export * from './plan_fact'
export * from './select' export * from './select'
export * from './tag' export * from './tag'
export * from './text' export * from './text'
@ -45,6 +44,7 @@ export const makeColumn = <T = any>(title: ReactNode, key: Key, other?: ColumnPr
title: title, title: title,
key: key, key: key,
dataIndex: key, dataIndex: key,
render: (value: T) => value,
...other, ...other,
}) })

View File

@ -1,4 +1,3 @@
import { ColumnFilterItem } from 'antd/lib/table/interface'
import { InputNumber } from 'antd' import { InputNumber } from 'antd'
import { Key, ReactNode } from 'react' import { Key, ReactNode } from 'react'
@ -46,18 +45,17 @@ export const makeNumericColumnOptions = <T extends number>(fixed?: number, sorte
export const makeNumericColumn = <T extends number>( export const makeNumericColumn = <T extends number>(
title: ReactNode, title: ReactNode,
key: Key, key: Key,
filters?: ColumnFilterItem[],
filterDelegate?: FilterGenerator<T>,
renderDelegate?: RenderMethod<T>, renderDelegate?: RenderMethod<T>,
filterDelegate?: FilterGenerator<T>,
width?: string | number, width?: string | number,
other?: ColumnProps<T>, other?: ColumnProps<T>,
) => makeColumn(title, key, { ) => makeColumn(title, key, {
filters, editable: true,
onFilter: filterDelegate ? filterDelegate(key) : undefined, onFilter: filterDelegate ? filterDelegate(key) : undefined,
sorter: makeNumericSorter(key), sorter: makeNumericSorter(key),
width, width,
input: <InputNumber style={{ width: '100%' }}/>, input: <InputNumber style={{ width: '100%' }} defaultValue={0} />,
render: renderDelegate ?? makeNumericRender<T>(2), render: renderDelegate || makeNumericRender<T>(2),
align: 'right', align: 'right',
...other ...other
}) })
@ -65,54 +63,78 @@ export const makeNumericColumn = <T extends number>(
export const makeNumericColumnPlanFact = <T extends number>( export const makeNumericColumnPlanFact = <T extends number>(
title: ReactNode, title: ReactNode,
key: Key, key: Key,
filters?: ColumnFilterItem[],
filterDelegate?: FilterGenerator<T>,
renderDelegate?: RenderMethod<T>, renderDelegate?: RenderMethod<T>,
filterDelegate?: FilterGenerator<T>,
width?: string | number,
other?: ColumnProps<T>,
) => {
return {
title,
children: [
makeNumericColumn<T>('План', `${key}.plan`, renderDelegate, filterDelegate, width, other),
makeNumericColumn<T>('Факт', `${key}.fact`, renderDelegate, filterDelegate, width, other),
]
}
}
/**
* @deprecated Для значений типа план/факт появилась модель `PlanFactDto`, использование 2 полей с суффиксами неактуально
* @param title Заголовок столбца
* @param key Ключ столбца
* @param filters Список значений для фильтрации
* @param filterDelegate Метод фильтрации
* @param renderDelegate Render-метод отображения ячейки
* @param width Ширина столбца
* @param other Дополнительные опции
* @returns Объект-столбец для таблицы
*/
export const makeNumericColumnPlanFactOld = <T extends number>(
title: ReactNode,
key: Key,
renderDelegate?: RenderMethod<T>,
filterDelegate?: FilterGenerator<T>,
width?: string | number, width?: string | number,
other?: ColumnProps<T>, other?: ColumnProps<T>,
) => makeGroupColumn(title, [ ) => makeGroupColumn(title, [
makeNumericColumn<T>('п', key + 'Plan', filters, filterDelegate, renderDelegate, width, other), makeNumericColumn<T>('План', key + 'Plan', renderDelegate, filterDelegate, width, other),
makeNumericColumn<T>('ф', key + 'Fact', filters, filterDelegate, renderDelegate, width, other), makeNumericColumn<T>('Факт', key + 'Fact', renderDelegate, filterDelegate, width, other),
]) ])
export const makeNumericStartEnd = <T extends number>( export const makeNumericStartEnd = <T extends number>(
title: ReactNode, title: ReactNode,
key: Key, key: Key,
fixed: number, fixed: number,
filters?: ColumnFilterItem[],
filterDelegate?: FilterGenerator<T>,
renderDelegate?: RenderMethod<T>, renderDelegate?: RenderMethod<T>,
filterDelegate?: FilterGenerator<T>,
width?: string | number, width?: string | number,
) => makeGroupColumn(title, [ ) => makeGroupColumn(title, [
makeNumericColumn<T>('старт', key + 'Start', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'Start')), makeNumericColumn<T>('старт', key + 'Start', renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, key + 'Start')),
makeNumericColumn<T>('конец', key + 'End', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'End')) makeNumericColumn<T>('конец', key + 'End', renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, key + 'End'))
]) ])
export const makeNumericMinMax = <T extends number>( export const makeNumericMinMax = <T extends number>(
title: ReactNode, title: ReactNode,
key: Key, key: Key,
fixed: number, fixed: number,
filters?: ColumnFilterItem[],
filterDelegate?: FilterGenerator<T>,
renderDelegate?: RenderMethod<T>, renderDelegate?: RenderMethod<T>,
filterDelegate?: FilterGenerator<T>,
width?: string | number, width?: string | number,
) => makeGroupColumn(title, [ ) => makeGroupColumn(title, [
makeNumericColumn<T>('мин', key + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'Min')), makeNumericColumn<T>('мин', key + 'Min', renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, key + 'Min')),
makeNumericColumn<T>('макс', key + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'Max')), makeNumericColumn<T>('макс', key + 'Max', renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, key + 'Max')),
]) ])
export const makeNumericAvgRange = <T extends number>( export const makeNumericAvgRange = <T extends number>(
title: ReactNode, title: ReactNode,
key: Key, key: Key,
fixed: number, fixed: number,
filters?: ColumnFilterItem[],
filterDelegate?: FilterGenerator<T>,
renderDelegate?: RenderMethod<T>, renderDelegate?: RenderMethod<T>,
filterDelegate?: FilterGenerator<T>,
width?: string | number, width?: string | number,
) => makeGroupColumn(title, [ ) => makeGroupColumn(title, [
makeNumericColumn<T>('мин', `${key}.min`, filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, `${key}.min`)), makeNumericColumn<T>('мин', `${key}.min`, renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, `${key}.min`)),
makeNumericColumn<T>('сред', `${key}.avg`, filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, `${key}.avg`)), makeNumericColumn<T>('сред', `${key}.avg`, renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, `${key}.avg`)),
makeNumericColumn<T>('макс', `${key}.max`, filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, `${key}.max`)), makeNumericColumn<T>('макс', `${key}.max`, renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, `${key}.max`)),
]) ])
export default makeNumericColumn export default makeNumericColumn

View File

@ -1,22 +0,0 @@
import { Key, ReactNode } from 'react'
import { ColumnProps, makeColumn } from '.'
export const makeColumnsPlanFact = <T,>(
title: string | ReactNode,
key: Key | [Key, Key],
columsOther?: ColumnProps<T> | [ColumnProps<T>, ColumnProps<T>],
) => {
const keys = Array.isArray(key) ? key : [`${key}Plan`, `${key}Fact`]
const others = Array.isArray(columsOther) ? columsOther : [columsOther, columsOther]
return {
title,
children: [
makeColumn<T>('план', keys[0], others[0]),
makeColumn<T>('факт', keys[1], others[1]),
]
}
}
export default makeColumnsPlanFact

View File

@ -1,9 +1,17 @@
import { Select, SelectProps } from 'antd' import { Select, SelectProps } from 'antd'
import { DefaultOptionType, SelectValue } from 'antd/lib/select' import { DefaultOptionType, SelectValue } from 'antd/lib/select'
import { Key, ReactNode } from 'react' import { Key, ReactNode, useMemo } from 'react'
import { ColumnProps, makeColumn } from '.' import { ColumnProps, makeColumn } from '.'
const findOption = <T extends DefaultOptionType>(value: any, options: T[] | undefined) =>
options?.find((option) => String(option?.value) === String(value))
const SelectWrapper = ({ value, options, ...other }: SelectProps) => {
const selectValue = useMemo(() => findOption(value, options)?.label, [value, options])
return <Select value={selectValue} options={options} {...other} />
}
export const makeSelectColumn = <T extends DefaultOptionType>( export const makeSelectColumn = <T extends DefaultOptionType>(
title: ReactNode, title: ReactNode,
key: Key, key: Key,
@ -12,10 +20,11 @@ export const makeSelectColumn = <T extends DefaultOptionType>(
other?: ColumnProps<T>, other?: ColumnProps<T>,
selectOther?: SelectProps<SelectValue> selectOther?: SelectProps<SelectValue>
) => makeColumn(title, key, { ) => makeColumn(title, key, {
editable: true,
...other, ...other,
input: <Select options={options} {...selectOther}/>, input: <SelectWrapper options={options} {...selectOther}/>,
render: (value, dataset, index) => { render: (value, dataset, index) => {
const item = options?.find(option => String(option?.value) === String(value)) const item = findOption(value, options)
return other?.render?.(item, dataset, index) ?? item?.label ?? defaultValue?.label ?? value?.label ?? '--' return other?.render?.(item, dataset, index) ?? item?.label ?? defaultValue?.label ?? value?.label ?? '--'
} }
}) })

View File

@ -65,6 +65,7 @@ export const makeTagColumn = <T extends DataType>(
const InputComponent = makeTagInput<T>(value_key, label_key) const InputComponent = makeTagInput<T>(value_key, label_key)
return makeColumn(title, dataIndex, { return makeColumn(title, dataIndex, {
editable: true,
...other, ...other,
render: (item: T[] | undefined, dataset, index) => item?.map((elm: T) => <Tag key={elm[label_key]} color={'blue'}>{other?.render?.(elm, dataset, index) ?? elm[label_key]}</Tag>) ?? '-', render: (item: T[] | undefined, dataset, index) => item?.map((elm: T) => <Tag key={elm[label_key]} color={'blue'}>{other?.render?.(elm, dataset, index) ?? elm[label_key]}</Tag>) ?? '-',
input: <InputComponent {...tagOther} options={options} />, input: <InputComponent {...tagOther} options={options} />,

View File

@ -1,3 +1,4 @@
import { Tooltip } from 'antd'
import { ColumnFilterItem } from 'antd/lib/table/interface' import { ColumnFilterItem } from 'antd/lib/table/interface'
import { Key, ReactNode } from 'react' import { Key, ReactNode } from 'react'
@ -15,6 +16,18 @@ export const makeStringSorter = <T extends string>(key: Key): SorterMethod<T> =>
return String(vA).localeCompare(String(vB)) return String(vA).localeCompare(String(vB))
} }
export const makeTextRender = <T extends string>(def = '---', stringCutter?: (text: string) => string) => (value: T) => {
if (!value) return def
if (stringCutter) {
return (
<Tooltip title={value}>
{stringCutter(value)}
</Tooltip>
)
}
return value
}
export const makeFilterTextMatch = <T extends unknown>(key: keyof DataType<T>) => export const makeFilterTextMatch = <T extends unknown>(key: keyof DataType<T>) =>
(filterValue: T, dataItem: DataType<T>) => dataItem[key] === filterValue (filterValue: T, dataItem: DataType<T>) => dataItem[key] === filterValue
@ -26,10 +39,11 @@ export const makeTextColumn = <T extends unknown = any>(
render?: RenderMethod<T>, render?: RenderMethod<T>,
other?: ColumnProps other?: ColumnProps
) => makeColumn(title, key, { ) => makeColumn(title, key, {
editable: true,
filters, filters,
onFilter: filters ? makeFilterTextMatch(key) : undefined, onFilter: filters ? makeFilterTextMatch(key) : undefined,
sorter: sorter ?? makeStringSorter(key), sorter: sorter || makeStringSorter(key),
render: render, render: render || makeTextRender(),
...other ...other
}) })

View File

@ -24,6 +24,7 @@ export const makeTimeColumn = <T extends TimeDto>(
other?: ColumnProps, other?: ColumnProps,
pickerOther?: TimePickerWrapperProps, pickerOther?: TimePickerWrapperProps,
) => makeColumn<T>(title, key, { ) => makeColumn<T>(title, key, {
editable: true,
...other, ...other,
render: (time) => ( render: (time) => (
<div className={'text-align-r-container'}> <div className={'text-align-r-container'}>

View File

@ -6,8 +6,11 @@ import moment, { Moment } from 'moment'
import { defaultFormat } from '@utils' import { defaultFormat } from '@utils'
export type DatePickerWrapperProps = PickerDateProps<Moment> & { export type DatePickerWrapperProps = PickerDateProps<Moment> & {
/** Значение селектора */
value?: Moment, value?: Moment,
/** Метод вызывается при изменений даты */
onChange?: (date: Moment | null) => any onChange?: (date: Moment | null) => any
/** Конвертировать ли значение в UTC */
isUTC?: boolean isUTC?: boolean
} }

View File

@ -9,31 +9,41 @@ import { defaultFormat } from '@utils'
const { RangePicker } = DatePicker const { RangePicker } = DatePicker
export type DateRangeWrapperProps = RangePickerSharedProps<Moment> & { export type DateRangeWrapperProps = RangePickerSharedProps<Moment> & {
value?: RangeValue<Moment>, /** Значение селектора в виде массива из 2 элементов (от, до) */
value?: RangeValue<Moment>
/** Конвертировать ли значения в UTC */
isUTC?: boolean isUTC?: boolean
/** Разрешить сброс значения селектора */
allowClear?: boolean allowClear?: boolean
} }
/**
* Подготавливает значения к передаче в селектор
*
* @param value Массиз из 2 дат
* @param isUTC Конвертировать ли значения в UTC
* @returns Подготовленные даты
*/
const normalizeDates = (value?: RangeValue<Moment>, isUTC?: boolean): RangeValue<Moment> => { const normalizeDates = (value?: RangeValue<Moment>, isUTC?: boolean): RangeValue<Moment> => {
if (!value) return [null, null] if (!value) return [null, null]
return [ return [
value[0] ? (isUTC ? moment.utc(value[0]).local() : moment(value[0])) : null, value[0] ? (isUTC ? moment.utc(value[0]).local() : moment(value[0])) : null,
value[1] ? (isUTC ? moment.utc(value[1]).local() : moment(value[1])) : null, value[1] ? (isUTC ? moment.utc(value[1]).local() : moment(value[1])) : null,
] ]
} }
export const DateRangeWrapper = memo<DateRangeWrapperProps>(({ value, isUTC, allowClear = false, ...other }) => ( export const DateRangeWrapper = memo<DateRangeWrapperProps>(({ value, isUTC, allowClear, ...other }) => (
<RangePicker <RangePicker
showTime showTime
allowClear={allowClear} allowClear={allowClear}
format={defaultFormat} format={defaultFormat}
defaultValue={[ defaultValue={[
moment().subtract(1, 'days').startOf('day'), moment().subtract(1, 'days').startOf('day'),
moment().startOf('day'), moment().startOf('day'),
]} ]}
value={normalizeDates(value)} value={normalizeDates(value, isUTC)}
{...other} {...other}
/> />
)) ))
export default DateRangeWrapper export default DateRangeWrapper

View File

@ -1,6 +1,6 @@
import { Key, memo, useCallback, useEffect, useState } from 'react' import { Key, memo, useCallback, useEffect, useState } from 'react'
import { ColumnGroupType, ColumnType } from 'antd/lib/table' import { ColumnGroupType, ColumnType } from 'antd/lib/table'
import { Table as RawTable, TableProps } from 'antd' import { Table as RawTable, TableProps as RawTableProps } from 'antd'
import { RenderMethod } from './Columns' import { RenderMethod } from './Columns'
import { tryAddKeys } from './EditableTable' import { tryAddKeys } from './EditableTable'
@ -14,16 +14,28 @@ export type BaseTableColumn<T> = ColumnGroupType<T> | ColumnType<T>
export type TableColumn<T> = OmitExtends<BaseTableColumn<T>, TableColumnSettings> export type TableColumn<T> = OmitExtends<BaseTableColumn<T>, TableColumnSettings>
export type TableColumns<T> = TableColumn<T>[] export type TableColumns<T> = TableColumn<T>[]
export type TableContainer<T> = TableProps<T> & { export type TableProps<T> = RawTableProps<T> & {
/** Массив колонок таблицы с настройками (описаны в `TableColumnSettings`) */
columns: TableColumn<T>[] columns: TableColumn<T>[]
/** Название таблицы для сохранения настроек */
tableName?: string tableName?: string
/** Отображать ли кнопку настроек */
showSettingsChanger?: boolean showSettingsChanger?: boolean
} }
export interface DataSet<T, D = any> { export interface DataSet<T, D = any> {
[k: Key]: DataSet<T> | T | D [k: Key]: DataSet<T, D> | T | D
} }
/**
* Получить значение из объекта по составному ключу
*
* Составной ключ имеет вид: `<поле 1>[.<поле 2>...]`
*
* @param key Составной ключ
* @param data Объект из которого будет полученно значение
* @returns Значение, найденное по ключу, либо `undefined`
*/
export const getObjectByDeepKey = <T,>(key: Key | undefined, data: DataSet<T>): T | undefined => { export const getObjectByDeepKey = <T,>(key: Key | undefined, data: DataSet<T>): T | undefined => {
if (!key) return undefined if (!key) return undefined
const parts = String(key).split('.') const parts = String(key).split('.')
@ -36,36 +48,44 @@ export const getObjectByDeepKey = <T,>(key: Key | undefined, data: DataSet<T>):
return out as T return out as T
} }
/**
* Фабрика обёрток render-функций ячеек с поддержкой составных ключей
* @param key Составной ключ
* @param render Стандартная render-функция
* @returns Обёрнутая render-функция
*/
export const makeColumnRenderWrapper = <T extends DataSet<any>>(key: Key | undefined, render: RenderMethod<T, T> | undefined): RenderMethod<T, T> => export const makeColumnRenderWrapper = <T extends DataSet<any>>(key: Key | undefined, render: RenderMethod<T, T> | undefined): RenderMethod<T, T> =>
(_: any, dataset: T, index: number) => { (_: any, dataset: T, index: number) => {
const renderFunc: RenderMethod<T, T> = typeof render === 'function' ? render : (record) => String(record) const renderFunc: RenderMethod<T, T> = typeof render === 'function' ? render : (record) => String(record)
return renderFunc(getObjectByDeepKey<T>(key, dataset), dataset, index) return renderFunc(getObjectByDeepKey<T>(key, dataset), dataset, index)
} }
/**
const applyColumnWrappers = <T extends DataSet<any>>(columns: BaseTableColumn<T>[]): BaseTableColumn<T>[] => { * Применяет необходимые обёртки ко всем столбцам таблицы
return columns.map((column) => { * @param columns Исходные столбцы
if ('children' in column) { * @returns Обёрнутые столбцы
return { */
...column, const applyColumnWrappers = <T extends DataSet<any>>(columns: TableColumns<T>): TableColumns<T> => columns.map((column) => {
children: applyColumnWrappers(column.children), if ('children' in column) {
}
}
return { return {
...column, ...column,
render: makeColumnRenderWrapper<T>(column.key, column.render), children: applyColumnWrappers(column.children),
} }
}) }
} return {
...column,
render: makeColumnRenderWrapper<T>(column.key, column.render),
}
})
function _Table<T extends DataSet<any>>({ columns, dataSource, tableName, showSettingsChanger, ...other }: TableContainer<T>) { function _Table<T extends DataSet<any>>({ columns, dataSource, tableName, showSettingsChanger, ...other }: TableProps<T>) {
const [newColumns, setNewColumns] = useState<TableColumn<T>[]>([]) const [newColumns, setNewColumns] = useState<TableColumn<T>[]>([])
const [settings, setSettings] = useState<TableSettings>({}) const [settings, setSettings] = useState<TableSettings>({})
const onSettingsChanged = useCallback((settings?: TableSettings | null) => { const onSettingsChanged = useCallback((settings?: TableSettings | null) => {
if (tableName) if (tableName)
setTableSettings(tableName, settings) setTableSettings(tableName, settings)
setSettings(settings ?? {}) setSettings(settings || {})
}, [tableName]) }, [tableName])
useEffect(() => setSettings(tableName ? getTableSettings(tableName) : {}), [tableName]) useEffect(() => setSettings(tableName ? getTableSettings(tableName) : {}), [tableName])
@ -92,6 +112,13 @@ function _Table<T extends DataSet<any>>({ columns, dataSource, tableName, showSe
) )
} }
/**
* Обёртка над компонентом таблицы AntD
*
* Особенности:
* * Поддержка составных ключей столбцов
* * Работа с настройками столбцов таблицы
*/
export const Table = memo(_Table) as typeof _Table export const Table = memo(_Table) as typeof _Table
export default Table export default Table

View File

@ -6,8 +6,11 @@ import { defaultTimeFormat, momentToTime, timeToMoment } from '@utils'
import { TimeDto } from '@api' import { TimeDto } from '@api'
export type TimePickerWrapperProps = Omit<Omit<TimePickerProps, 'value'>, 'onChange'> & { export type TimePickerWrapperProps = Omit<Omit<TimePickerProps, 'value'>, 'onChange'> & {
/** Текущее значение */
value?: TimeDto, value?: TimeDto,
/** Метод вызывается при изменений времени */
onChange?: (date: TimeDto | null) => any onChange?: (date: TimeDto | null) => any
/** Конвертировать ли время в UTC */
isUTC?: boolean isUTC?: boolean
} }

View File

@ -17,6 +17,13 @@ export type PaginationContainer<T> = {
items?: T[] | null items?: T[] | null
} }
/**
* Генерирует объект пагинации для компонента `Table` из данных от сервисов
*
* @param сontainer данные от сервиса
* @param other Дополнительные поля (передаются в объект напрямую в приоритете)
* @returns Объект пагинации
*/
export const makePaginationObject = <T, M extends object>(сontainer: PaginationContainer<T>, other: M) => ({ export const makePaginationObject = <T, M extends object>(сontainer: PaginationContainer<T>, other: M) => ({
...other, ...other,
pageSize: сontainer.take, pageSize: сontainer.take,

View File

@ -10,7 +10,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory'
import { isURLAvailable, removeUser } from '@utils' import { isURLAvailable, removeUser } from '@utils'
import { AuthService } from '@api' import { AuthService } from '@api'
import '@styles/user_menu.less' import '@styles/components/user_menu.less'
export type UserMenuProps = DrawerProps & { export type UserMenuProps = DrawerProps & {
isAdmin?: boolean isAdmin?: boolean

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'
@ -36,7 +35,7 @@ import type {
ChartTicks ChartTicks
} from './types' } from './types'
import '@styles/d3.less' import '@styles/components/d3.less'
const defaultOffsets: ChartOffset = { const defaultOffsets: ChartOffset = {
top: 10, top: 10,
@ -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,13 +1,12 @@
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 { useElementSize, usePartialProps } from '@utils'
import { ChartOffset } from './types' import { ChartOffset } from './types'
import '@styles/d3.less' import '@styles/components/d3.less'
import { usePartialProps } from '@asb/utils'
export type PercentChartDataType = { export type PercentChartDataType = {
name: string name: string
@ -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

@ -3,7 +3,7 @@ import * as d3 from 'd3'
import { ChartOffset } from './types' import { ChartOffset } from './types'
import '@styles/d3.less' import '@styles/components/d3.less'
export type D3MouseState = { export type D3MouseState = {
/** Позиция мыши по оси X */ /** Позиция мыши по оси X */

View File

@ -9,9 +9,9 @@ import { getChartIcon, isDev, usePartialProps } from '@utils'
import { BaseDataType } from '../types' import { BaseDataType } from '../types'
import { ChartGroup, ChartSizes } from './D3MonitoringCharts' import { ChartGroup, ChartSizes } from './D3MonitoringCharts'
import '@styles/d3.less' import '@styles/components/d3.less'
type D3GroupRenderFunction<DataType extends BaseDataType> = (group: ChartGroup<DataType>, data: DataType[]) => ReactNode type D3GroupRenderFunction<DataType extends BaseDataType> = (group: ChartGroup<DataType>, data: DataType[], flowData: DataType[] | undefined) => ReactNode
export type D3HorizontalCursorSettings<DataType extends BaseDataType> = { export type D3HorizontalCursorSettings<DataType extends BaseDataType> = {
width?: number width?: number
@ -27,6 +27,7 @@ export type D3HorizontalCursorSettings<DataType extends BaseDataType> = {
export type D3HorizontalCursorProps<DataType extends BaseDataType> = D3HorizontalCursorSettings<DataType> & { export type D3HorizontalCursorProps<DataType extends BaseDataType> = D3HorizontalCursorSettings<DataType> & {
groups: ChartGroup<DataType>[] groups: ChartGroup<DataType>[]
data: DataType[] data: DataType[]
flowData: DataType[] | undefined
sizes: ChartSizes sizes: ChartSizes
yAxis?: d3.ScaleTime<number, number> yAxis?: d3.ScaleTime<number, number>
spaceBetweenGroups?: number spaceBetweenGroups?: number
@ -38,7 +39,7 @@ const defaultLineStyle: SVGProps<SVGLineElement> = {
const offsetY = 5 const offsetY = 5
const makeDefaultRender = <DataType extends BaseDataType>(): D3GroupRenderFunction<DataType> => (group, data) => ( const makeDefaultRender = <DataType extends BaseDataType>(): D3GroupRenderFunction<DataType> => (group, data, flowData) => (
<> <>
{data.length > 0 ? group.charts.map((chart) => { {data.length > 0 ? group.charts.map((chart) => {
const xFormat = (d: number | Date) => chart.xAxis.format?.(d) ?? `${(+d).toFixed(2)} ${chart.xAxis.unit ?? ''}` const xFormat = (d: number | Date) => chart.xAxis.format?.(d) ?? `${(+d).toFixed(2)} ${chart.xAxis.unit ?? ''}`
@ -74,6 +75,7 @@ const _D3HorizontalCursor = <DataType extends BaseDataType>({
lineStyle: _lineStyle, lineStyle: _lineStyle,
data, data,
flowData,
groups, groups,
sizes, sizes,
yAxis, yAxis,
@ -167,7 +169,7 @@ const _D3HorizontalCursor = <DataType extends BaseDataType>({
return (date >= currentDate - limitInS) && (date <= currentDate + limitInS) return (date >= currentDate - limitInS) && (date <= currentDate + limitInS)
}) })
const bodies = groups.map((group) => render(group, chartData)) const bodies = groups.map((group) => render(group, chartData, flowData))
setTooltipBodies(bodies) setTooltipBodies(bodies)
}, [groups, data, yAxis, lineY, fixed, mouseState.visible]) }, [groups, data, yAxis, lineY, fixed, mouseState.visible])
@ -190,7 +192,7 @@ const _D3HorizontalCursor = <DataType extends BaseDataType>({
> >
<div className={'tooltip-wrapper'}> <div className={'tooltip-wrapper'}>
<div className={`adaptive-tooltip tooltip ${position} ${className}`} <div className={`adaptive-tooltip tooltip ${position} ${className}`}
style={{height: 'auto', bottom: `${position === 'bottom' ? '0' : ''}`}} style={{height: 'auto', bottom: `${position === 'bottom' ? '0' : ''}`}}
> >
<div className={'tooltip-content'}> <div className={'tooltip-content'}>
{tooltipBodies[i]} {tooltipBodies[i]}

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,
@ -35,13 +34,14 @@ const roundTo = (v: number, to: number = 50) => {
return (v > 0 ? Math.ceil : Math.round)(v / to) * to return (v > 0 ? Math.ceil : Math.round)(v / to) * to
} }
const getNear = (n: number) => {
let k = 0
for (let c = Math.abs(n); c >= 1; c /= 10) k++
return Math.pow(10, k) * Math.sign(n)
}
const calculateDomain = (mm: MinMax): Required<MinMax> => { const calculateDomain = (mm: MinMax): Required<MinMax> => {
let round = Math.abs((mm.max ?? 0) - (mm.min ?? 0)) const round = getNear(Math.abs((mm.max ?? 0) - (mm.min ?? 0))) || 10
if (round < 10) round = 10
else if (round < 100) round = roundTo(round, 10)
else if (round < 1000) round = roundTo(round, 100)
else if (round < 10000) round = roundTo(round, 1000)
else round = 0
let min = roundTo(mm.min ?? 0, round) let min = roundTo(mm.min ?? 0, round)
let max = roundTo(mm.max ?? round, round) let max = roundTo(mm.max ?? round, round)
if (round && Math.abs(min - max) < round) { if (round && Math.abs(min - max) < round) {
@ -73,8 +73,8 @@ export type ChartGroup<DataType extends BaseDataType> = {
} }
const defaultOffsets: ChartOffset = { const defaultOffsets: ChartOffset = {
top: 10, top: 0,
bottom: 10, bottom: 0,
left: 100, left: 100,
right: 20, right: 20,
} }
@ -115,6 +115,8 @@ export type D3MonitoringChartsProps<DataType extends BaseDataType> = Omit<React.
loading?: boolean loading?: boolean
/** Массив отображаемых данных */ /** Массив отображаемых данных */
data?: DataType[] data?: DataType[]
/** Массив данных для прямоугольников */
flowData?: DataType[]
/** Отступы графика от края SVG */ /** Отступы графика от края SVG */
offset?: Partial<ChartOffset> offset?: Partial<ChartOffset>
/** Цвет фона в формате CSS-значения */ /** Цвет фона в формате CSS-значения */
@ -180,6 +182,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
loading = false, loading = false,
datasetGroups, datasetGroups,
data, data,
flowData,
plugins, plugins,
offset: _offset, offset: _offset,
yAxis: _yAxisConfig, yAxis: _yAxisConfig,
@ -209,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])
@ -242,11 +245,11 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
if (!data) return if (!data) return
const yAxis = d3.scaleTime() const yAxis = d3.scaleTime()
.domain([yDomain?.min ?? 0, yDomain?.max ?? 0]) .domain([yDomain?.min || 0, yDomain?.max || 0])
.range([0, sizes.chartsHeight]) .range([0, sizes.chartsHeight])
return yAxis return yAxis
}, [groups, data, yDomain, sizes.chartsHeight]) }, [groups, data, yDomain, sizes])
const chartDomains = useMemo(() => groups.map((group) => { const chartDomains = useMemo(() => groups.map((group) => {
const out: [string | number, ChartDomain][] = group.charts.map((chart) => { const out: [string | number, ChartDomain][] = group.charts.map((chart) => {
@ -453,7 +456,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
.tickSize(yTicks.visible ? -width + offset.left + offset.right : 0) .tickSize(yTicks.visible ? -width + offset.left + offset.right : 0)
.ticks(yTicks.count) as any // TODO: Исправить тип .ticks(yTicks.count) as any // TODO: Исправить тип
) )
yAxisArea().selectAll('.tick line').attr('stroke', yTicks.color) yAxisArea().selectAll('.tick line').attr('stroke', yTicks.color)
}, [yAxisArea, yAxis, animDurationMs, width, offset, yTicks]) }, [yAxisArea, yAxis, animDurationMs, width, offset, yTicks])
@ -463,8 +466,8 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
groups.forEach((group, i) => { groups.forEach((group, i) => {
group() group()
.attr('transform', `translate(${sizes.groupLeft(group.key)}, 0)`) .attr('transform', `translate(${sizes.groupLeft(group.key)}, 0)`)
.attr('clip-path', `url(#chart-clip)`) .attr('clip-path', `url(#chart-group-clip)`)
group.charts.forEach((chart) => { group.charts.forEach((chart) => {
chart() chart()
.attr('color', chart.color || null) .attr('color', chart.color || null)
@ -492,7 +495,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
chartData = renderArea<DataType>(xAxis, yAxis, chart, chartData) chartData = renderArea<DataType>(xAxis, yAxis, chart, chartData)
break break
case 'rect_area': case 'rect_area':
renderRectArea<DataType>(xAxis, yAxis, chart) renderRectArea<DataType>(xAxis, yAxis, chart, flowData)
break break
default: default:
break break
@ -506,7 +509,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
chart.afterDraw?.(chart) chart.afterDraw?.(chart)
}) })
}) })
}, [data, groups, height, offset, sizes, chartDomains]) }, [data, flowData, groups, height, offset, sizes, chartDomains, yAxis])
return ( return (
<LoaderPortal <LoaderPortal
@ -530,7 +533,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
> >
<svg ref={setSvgRef} width={'100%'} height={'100%'}> <svg ref={setSvgRef} width={'100%'} height={'100%'}>
<defs> <defs>
<clipPath id={`chart-clip`}> <clipPath id={`chart-group-clip`}>
{/* Сдвиг во все стороны на 1 px чтобы линии на краях было видно */} {/* Сдвиг во все стороны на 1 px чтобы линии на краях было видно */}
<rect x={-1} y={-1} width={sizes.groupWidth + 2} height={sizes.chartsHeight + 2} /> <rect x={-1} y={-1} width={sizes.groupWidth + 2} height={sizes.chartsHeight + 2} />
</clipPath> </clipPath>
@ -571,6 +574,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
sizes={sizes} sizes={sizes}
spaceBetweenGroups={spaceBetweenGroups} spaceBetweenGroups={spaceBetweenGroups}
data={data} data={data}
flowData={flowData}
height={height} height={height}
/> />
</D3MouseZone> </D3MouseZone>

View File

@ -92,6 +92,15 @@ const _D3MonitoringLimitChart = <DataType extends TelemetryDataSaubDto>({
const [ref, setRef] = useState<SVGGElement | null>(null) const [ref, setRef] = useState<SVGGElement | null>(null)
const [selected, setSelected] = useState<LimitChartData & { x: number, y: number, visible: boolean }>() const [selected, setSelected] = useState<LimitChartData & { x: number, y: number, visible: boolean }>()
const selectedRegulator = useMemo(() => {
if (!selected) return null
const out = regulators[selected.id] || {
label: `ID = ${selected.id}`,
color: 'black',
}
return out
}, [selected, regulators])
const data = useMemo(() => calcualteData(chartData), [chartData]) const data = useMemo(() => calcualteData(chartData), [chartData])
useEffect(() => { useEffect(() => {
@ -105,7 +114,7 @@ const _D3MonitoringLimitChart = <DataType extends TelemetryDataSaubDto>({
.attr('width', width) .attr('width', width)
.attr('height', (d) => Math.max(yAxis(d.dateEnd) - yAxis(d.dateStart), 1)) .attr('height', (d) => Math.max(yAxis(d.dateEnd) - yAxis(d.dateStart), 1))
.attr('y', (d) => yAxis(d.dateStart)) .attr('y', (d) => yAxis(d.dateStart))
.attr('fill', (d) => regulators[d.id].color) .attr('fill', (d) => regulators[d.id]?.color || 'black')
.on('mouseover', (_, d) => { .on('mouseover', (_, d) => {
const y = yAxis(d.dateStart) - tooltipHeight const y = yAxis(d.dateStart) - tooltipHeight
setSelected({ ...d, y, x: -tooltipWidth - 10, visible: true }) setSelected({ ...d, y, x: -tooltipWidth - 10, visible: true })
@ -130,14 +139,24 @@ const _D3MonitoringLimitChart = <DataType extends TelemetryDataSaubDto>({
return ( return (
<g transform={`translate(${left}, ${top})`} stroke={'#333'} strokeWidth={1} fill={'none'}> <g transform={`translate(${left}, ${top})`} stroke={'#333'} strokeWidth={1} fill={'none'}>
<g ref={setRef} > <defs>
<g className={'bars'} strokeWidth={0} /> <clipPath id={`chart-limit-clip`}>
{/* Сдвиг во все стороны на 1 px чтобы линии на краях было видно */}
<rect x={0} y={0} width={width} height={height} />
</clipPath>
<clipPath id={`chart-limit-fill-clip`}>
<rect x={-zoneWidth} y={0} width={zoneWidth} height={height} />
</clipPath>
</defs>
<g ref={setRef}>
<g className={'bars'} strokeWidth={0} clipPath={`url(#chart-limit-clip)`} />
{selected && ( {selected && (
<g <g
style={opacityStyle} style={opacityStyle}
pointerEvents={'none'} pointerEvents={'none'}
strokeOpacity={0.4} strokeOpacity={0.4}
stroke={regulators[selected.id].color} stroke={selectedRegulator?.color}
clipPath={`url(#chart-limit-fill-clip)`}
> >
<line x1={-zoneWidth} x2={0} y1={zoneY1} y2={zoneY1} /> <line x1={-zoneWidth} x2={0} y1={zoneY1} y2={zoneY1} />
<line x1={-zoneWidth} x2={0} y1={zoneY2} y2={zoneY2} /> <line x1={-zoneWidth} x2={0} y1={zoneY2} y2={zoneY2} />
@ -148,7 +167,7 @@ const _D3MonitoringLimitChart = <DataType extends TelemetryDataSaubDto>({
y={zoneY1} y={zoneY1}
width={zoneWidth} width={zoneWidth}
height={zoneY2 - zoneY1} height={zoneY2 - zoneY1}
fill={regulators[selected.id].color} fill={selectedRegulator?.color}
/> />
</g> </g>
)} )}
@ -158,7 +177,7 @@ const _D3MonitoringLimitChart = <DataType extends TelemetryDataSaubDto>({
<foreignObject width={tooltipWidth} height={tooltipHeight} x={selected.x} y={selected.y} pointerEvents={'none'}> <foreignObject width={tooltipWidth} height={tooltipHeight} x={selected.x} y={selected.y} pointerEvents={'none'}>
<div className={'tooltip bottom'} style={tooltipStyle}> <div className={'tooltip bottom'} style={tooltipStyle}>
<span>Ограничивающий параметр</span> <span>Ограничивающий параметр</span>
<span>{regulators[selected.id].label}</span> <span>{selectedRegulator?.label}</span>
<Grid style={{ margin: 0, padding: 0 }}> <Grid style={{ margin: 0, padding: 0 }}>
<GridItem row={1} col={1}>Начало:</GridItem> <GridItem row={1} col={1}>Начало:</GridItem>
<GridItem row={1} col={2}>{formatDate(selected.dateStart)}</GridItem> <GridItem row={1} col={2}>{formatDate(selected.dateStart)}</GridItem>

View File

@ -6,7 +6,7 @@ import { usePartialProps } from '@utils'
import { wrapPlugin } from './base' import { wrapPlugin } from './base'
import '@styles/d3.less' import '@styles/components/d3.less'
export type D3CursorSettings = { export type D3CursorSettings = {
/** Параметры стиля линии */ /** Параметры стиля линии */

View File

@ -8,7 +8,7 @@ import { BaseDataType, ChartRegistry } from '@components/d3/types'
import { D3MouseState, useD3MouseZone } from '@components/d3/D3MouseZone' import { D3MouseState, useD3MouseZone } from '@components/d3/D3MouseZone'
import { getTouchedElements, wrapPlugin } from './base' import { getTouchedElements, wrapPlugin } from './base'
import '@styles/d3.less' import '@styles/components/d3.less'
export type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none' export type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none'

View File

@ -6,7 +6,8 @@ import { appendTransition } from './base'
export const renderRectArea = <DataType extends BaseDataType>( export const renderRectArea = <DataType extends BaseDataType>(
xAxis: (value: d3.NumberValue) => number, xAxis: (value: d3.NumberValue) => number,
yAxis: (value: d3.NumberValue) => number, yAxis: (value: d3.NumberValue) => number,
chart: ChartRegistry<DataType> chart: ChartRegistry<DataType>,
data: DataType[] | undefined,
) => { ) => {
if ( if (
chart.type !== 'rect_area' || chart.type !== 'rect_area' ||
@ -14,25 +15,27 @@ export const renderRectArea = <DataType extends BaseDataType>(
!chart.maxXAccessor || !chart.maxXAccessor ||
!chart.minYAccessor || !chart.minYAccessor ||
!chart.maxYAccessor || !chart.maxYAccessor ||
!chart.data !data
) return ) return
const data = chart.data
const xMin = getByAccessor(chart.minXAccessor) const xMin = getByAccessor(chart.minXAccessor)
const xMax = getByAccessor(chart.maxXAccessor) const xMax = getByAccessor(chart.maxXAccessor)
const yMin = getByAccessor(chart.minYAccessor) const yMin = getByAccessor(chart.minYAccessor)
const yMax = getByAccessor(chart.maxYAccessor) const yMax = getByAccessor(chart.maxYAccessor)
chart().attr('fill', 'currentColor') chart()
.attr('fill', 'currentColor')
.attr('fill-opacity', '0.15')
.attr('stroke-opacity', '0.3')
const rects = chart().selectAll<SVGRectElement, null>('rect').data(data) const rects = chart().selectAll<SVGRectElement, null>('rect').data(data)
rects.exit().remove() rects.exit().remove()
rects.enter().append('rect') rects.enter().append('rect')
appendTransition(chart().selectAll<SVGRectElement, Record<string, any>>('rect'), chart) appendTransition(chart().selectAll<SVGRectElement, Record<string, any>>('rect'), chart)
.attr('x1', (d) => xAxis(xMin(d))) .attr('x', (d) => xAxis(xMin(d)))
.attr('x2', (d) => xAxis(xMax(d))) .attr('y', (d) => yAxis(yMin(d)))
.attr('y1', (d) => yAxis(yMin(d))) .attr('width', (d) => xAxis(xMax(d)) - xAxis(xMin(d)))
.attr('y2', (d) => yAxis(yMax(d))) .attr('height', (d) => yAxis(yMax(d)) - yAxis(yMin(d)))
} }

View File

@ -1,7 +1,7 @@
import { memo, useEffect, useState } from 'react' import { memo, useEffect, useState } from 'react'
import { Outlet } from 'react-router-dom' import { Outlet } from 'react-router-dom'
import { DepositsContext } from '@asb/context' import { DepositListContext } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { DepositDto, DepositService } from '@api' import { DepositDto, DepositService } from '@api'
@ -24,11 +24,11 @@ export const DepositsOutlet = memo(() => {
}, []) }, [])
return ( return (
<DepositsContext.Provider value={deposits}> <DepositListContext.Provider value={deposits}>
<LoaderPortal show={isLoading}> <LoaderPortal show={isLoading}>
<Outlet /> <Outlet />
</LoaderPortal> </LoaderPortal>
</DepositsContext.Provider> </DepositListContext.Provider>
) )
}) })

View File

@ -1,7 +1,7 @@
import { Tag, TreeSelect } from 'antd' import { Tag, TreeSelect } from 'antd'
import { memo, useEffect, useState } from 'react' import { memo, useEffect, useState } from 'react'
import { useDeposits } from '@asb/context' import { useDepositList } from '@asb/context'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { hasPermission } from '@utils' import { hasPermission } from '@utils'
@ -39,7 +39,7 @@ export const WellSelector = memo(({ value, onChange, treeData, treeLabels, ...ot
const [wellsTree, setWellsTree] = useState([]) const [wellsTree, setWellsTree] = useState([])
const [wellLabels, setWellLabels] = useState([]) const [wellLabels, setWellLabels] = useState([])
const deposits = useDeposits() const deposits = useDepositList()
useEffect(() => { useEffect(() => {
invokeWebApiWrapperAsync( invokeWebApiWrapperAsync(
@ -59,6 +59,7 @@ export const WellSelector = memo(({ value, onChange, treeData, treeLabels, ...ot
<TreeSelect <TreeSelect
multiple multiple
treeCheckable treeCheckable
maxTagCount={'responsive'}
showCheckedStrategy={TreeSelect.SHOW_CHILD} showCheckedStrategy={TreeSelect.SHOW_CHILD}
treeDefaultExpandAll treeDefaultExpandAll
treeData={wellsTree} treeData={wellsTree}

View File

@ -2,7 +2,7 @@ import { Drawer, Tree, TreeDataNode, TreeProps } from 'antd'
import { useState, useEffect, useCallback, memo, Key, useMemo } from 'react' import { useState, useEffect, useCallback, memo, Key, useMemo } from 'react'
import { useNavigate, useLocation } from 'react-router-dom' import { useNavigate, useLocation } from 'react-router-dom'
import { useDeposits } from '@asb/context' import { useDepositList } from '@asb/context'
import { WellIcon, WellIconState } from '@components/icons' import { WellIcon, WellIconState } from '@components/icons'
import { DepositDto, WellDto } from '@api' import { DepositDto, WellDto } from '@api'
import { isRawDate } from '@utils' import { isRawDate } from '@utils'
@ -10,32 +10,41 @@ import { isRawDate } from '@utils'
import { ReactComponent as DepositIcon } from '@images/DepositIcon.svg' import { ReactComponent as DepositIcon } from '@images/DepositIcon.svg'
import { ReactComponent as ClusterIcon } from '@images/ClusterIcon.svg' import { ReactComponent as ClusterIcon } from '@images/ClusterIcon.svg'
import '@styles/wellTreeSelect.css' import '@styles/components/well_tree_select.css'
/**
* Для поиска в URL текущего раздела по шаблону `/{type}/{id}`
*
* Если найдено совпадение может вернуть 1 или 2 группы соответственно
*/
const URL_REGEX = /^\/([^\/?#]+)(?:\/([^\/?#]+))?/
export const getWellState = (idState?: number): WellIconState => idState === 1 ? 'active' : 'unknown' export const getWellState = (idState?: number): WellIconState => idState === 1 ? 'active' : 'unknown'
export const checkIsWellOnline = (lastTelemetryDate: unknown): boolean => export const checkIsWellOnline = (lastTelemetryDate: unknown): boolean =>
isRawDate(lastTelemetryDate) && (Date.now() - +new Date(lastTelemetryDate) < 600_000) isRawDate(lastTelemetryDate) && (Date.now() - +new Date(lastTelemetryDate) < 600_000)
const getKeyByUrl = (url?: string): [Key | null, string | null] => { const getKeyByUrl = (url?: string): [Key | null, string | null, number | null] => {
const result = url?.match(/^\/([^\/]+)\/([^\/?]+)/) // pattern "/:type/:id" const result = url?.match(URL_REGEX) // pattern "/:type/:id"
if (!result) return [null, null] if (!result) return [null, null, null]
return [result[0], result[1]] return [result[0], result[1], result[2] && result[2] !== 'null' ? Number(result[2]) : null]
} }
const getLabel = (wellsTree: TreeDataNode[], value?: string): string | undefined => { const getLabel = (wellsTree: TreeDataNode[], value?: string): string | undefined => {
const [url, type] = getKeyByUrl(value) const [url, type, key] = getKeyByUrl(value)
if (!url) return if (!url) return
let deposit: TreeDataNode | undefined let deposit: TreeDataNode | undefined
let cluster: TreeDataNode | undefined let cluster: TreeDataNode | undefined
let well: TreeDataNode | undefined let well: TreeDataNode | undefined
switch (type) { switch (type) {
case 'deposit': case 'deposit':
if (key === null) return 'Месторождение не выбрано'
deposit = wellsTree.find((deposit) => deposit.key === url) deposit = wellsTree.find((deposit) => deposit.key === url)
if (deposit) if (deposit)
return `${deposit.title}` return `${deposit.title}`
return 'Ошибка! Месторождение не найдено!' return 'Ошибка! Месторождение не найдено!'
case 'cluster': case 'cluster':
if (key === null) return 'Куст не выбран'
deposit = wellsTree.find((deposit) => ( deposit = wellsTree.find((deposit) => (
cluster = deposit.children?.find((cluster: TreeDataNode) => cluster.key === url) cluster = deposit.children?.find((cluster: TreeDataNode) => cluster.key === url)
)) ))
@ -44,6 +53,7 @@ const getLabel = (wellsTree: TreeDataNode[], value?: string): string | undefined
return 'Ошибка! Куст не найден!' return 'Ошибка! Куст не найден!'
case 'well': case 'well':
if (key === null) return 'Скважина не выбрана'
deposit = wellsTree.find((deposit) => ( deposit = wellsTree.find((deposit) => (
cluster = deposit.children?.find((cluster: TreeDataNode) => ( cluster = deposit.children?.find((cluster: TreeDataNode) => (
well = cluster.children?.find((well: TreeDataNode) => well.key === url) well = cluster.children?.find((well: TreeDataNode) => well.key === url)
@ -126,7 +136,7 @@ export const WellTreeSelector = memo<WellTreeSelectorProps>(({ expand, current,
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const deposits = useDeposits() const deposits = useDepositList()
const wellsTree = useMemo(() => makeWellsTreeData(deposits), [deposits]) const wellsTree = useMemo(() => makeWellsTreeData(deposits), [deposits])
@ -137,8 +147,8 @@ export const WellTreeSelector = memo<WellTreeSelectorProps>(({ expand, current,
}, [wellsTree]) }, [wellsTree])
const onSelect = useCallback((value: Key[]): void => { const onSelect = useCallback((value: Key[]): void => {
const newRoot = /\/(\w+)\/\d+/.exec(String(value)) const newRoot = URL_REGEX.exec(String(value))
const oldRoot = /\/(\w+)(?:\/\d+)?/.exec(location.pathname) const oldRoot = URL_REGEX.exec(location.pathname)
if (!newRoot || !oldRoot) return if (!newRoot || !oldRoot) return
let newPath = newRoot[0] let newPath = newRoot[0]

View File

@ -9,6 +9,7 @@ export type CompanyViewProps = {
company?: CompanyDto company?: CompanyDto
} }
/** Компонент для отображения информации о компании */
export const CompanyView = memo<CompanyViewProps>(({ company }) => company ? ( export const CompanyView = memo<CompanyViewProps>(({ company }) => company ? (
<Tooltip title={ <Tooltip title={
<Grid style={{ columnGap: '8px' }}> <Grid style={{ columnGap: '8px' }}>

View File

@ -8,6 +8,7 @@ export type PermissionViewProps = {
info?: PermissionDto info?: PermissionDto
} }
/** Компонент для отображения информации о разрешении */
export const PermissionView = memo<PermissionViewProps>(({ info }) => info ? ( export const PermissionView = memo<PermissionViewProps>(({ info }) => info ? (
<Tooltip overlayInnerStyle={{ width: '400px' }} title={ <Tooltip overlayInnerStyle={{ width: '400px' }} title={
<Grid> <Grid>

View File

@ -9,6 +9,7 @@ export type RoleViewProps = {
role?: UserRoleDto role?: UserRoleDto
} }
/** Компонент для отображения информации о роли */
export const RoleView = memo<RoleViewProps>(({ role }) => { export const RoleView = memo<RoleViewProps>(({ role }) => {
if (!role) return ( <Tooltip title={'нет данных'}>-</Tooltip> ) if (!role) return ( <Tooltip title={'нет данных'}>-</Tooltip> )

View File

@ -19,6 +19,12 @@ export const lables: Record<string, string> = {
spinPlcVersion: 'Версия Спин Мастер', spinPlcVersion: 'Версия Спин Мастер',
} }
/**
* Строит название для телеметрии
*
* @param telemetry Объект телеметрии
* @returns Название
*/
export const getTelemetryLabel = (telemetry?: TelemetryDto) => export const getTelemetryLabel = (telemetry?: TelemetryDto) =>
`${telemetry?.id ?? '-'} / ${telemetry?.info?.deposit ?? '-'} / ${telemetry?.info?.cluster ?? '-'} / ${telemetry?.info?.well ?? '-'}` `${telemetry?.id ?? '-'} / ${telemetry?.info?.deposit ?? '-'} / ${telemetry?.info?.cluster ?? '-'} / ${telemetry?.info?.well ?? '-'}`
@ -26,6 +32,7 @@ export type TelemetryViewProps = {
telemetry?: TelemetryDto telemetry?: TelemetryDto
} }
/** Компонент для отображения информации о телеметрии */
export const TelemetryView = memo<TelemetryViewProps>(({ telemetry }) => telemetry?.info ? ( export const TelemetryView = memo<TelemetryViewProps>(({ telemetry }) => telemetry?.info ? (
<Tooltip <Tooltip
overlayInnerStyle={{ width: '400px' }} overlayInnerStyle={{ width: '400px' }}

View File

@ -10,6 +10,7 @@ export type UserViewProps = HTMLProps<HTMLSpanElement> & {
user?: UserDto user?: UserDto
} }
/** Компонент для отображения информации о пользователе */
export const UserView = memo<UserViewProps>(({ user, ...other }) => export const UserView = memo<UserViewProps>(({ user, ...other }) =>
user ? ( user ? (
<Tooltip <Tooltip

View File

@ -1,4 +1,4 @@
import { memo } from 'react' import { DetailedHTMLProps, HTMLAttributes, memo } from 'react'
import { Tooltip, TooltipProps } from 'antd' import { Tooltip, TooltipProps } from 'antd'
import { Grid, GridItem } from '@components/Grid' import { Grid, GridItem } from '@components/Grid'
@ -15,9 +15,19 @@ const wellState: Record<number, { enum: WellIconState, label: string }> = {
export type WellViewProps = TooltipProps & { export type WellViewProps = TooltipProps & {
well?: WellDto well?: WellDto
iconProps?: DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
labelProps?: DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
} }
export const WellView = memo<WellViewProps>(({ well, ...other }) => well ? ( /**
* Получить название скважины
* @param well Объект с данными скважины
* @returns Название скважины
*/
export const getWellTitle = (well: WellDto) => `${well.deposit || '-'} / ${well.cluster || '-'} / ${well.caption || '-'}`
/** Компонент для отображения информации о скважине */
export const WellView = memo<WellViewProps>(({ well, iconProps, labelProps, ...other }) => well ? (
<Tooltip {...other} title={( <Tooltip {...other} title={(
<Grid style={{ columnGap: '8px' }}> <Grid style={{ columnGap: '8px' }}>
<GridItem row={1} col={1}>Название:</GridItem> <GridItem row={1} col={1}>Название:</GridItem>
@ -47,10 +57,12 @@ export const WellView = memo<WellViewProps>(({ well, ...other }) => well ? (
<GridItem row={8} col={2}>{well.id ?? '---'}</GridItem> <GridItem row={8} col={2}>{well.id ?? '---'}</GridItem>
</Grid> </Grid>
)}> )}>
<span role={'img'} style={{ marginRight: 8, lineHeight: 0, verticalAlign: '-0.25em' }}> <span role={'img'} style={{ marginRight: 8, lineHeight: 0, verticalAlign: '-0.25em' }} {...iconProps}>
<WellIcon state={wellState[well.idState || 0].enum} width={'1em'} height={'1em'} /> <WellIcon state={wellState[well.idState || 0].enum} width={'1em'} height={'1em'} />
</span> </span>
{well.caption} <span {...labelProps}>
{getWellTitle(well)}
</span>
</Tooltip> </Tooltip>
) : ( ) : (
<Tooltip title={'нет скважины'}>-</Tooltip> <Tooltip title={'нет скважины'}>-</Tooltip>

View File

@ -21,6 +21,7 @@ export type WirelineViewProps = TooltipProps & {
buttonProps?: ButtonProps buttonProps?: ButtonProps
} }
/** Компонент для отображения информации о талевом канате */
export const WirelineView = memo<WirelineViewProps>(({ wireline, buttonProps, ...other }) => ( export const WirelineView = memo<WirelineViewProps>(({ wireline, buttonProps, ...other }) => (
<Tooltip <Tooltip
{...other} {...other}

View File

@ -1,67 +0,0 @@
import { createContext, useContext, useEffect } from 'react'
import { LayoutPortalProps } from '@components/LayoutPortal'
import { DepositDto, UserTokenDto, WellDto } from '@api'
/** Контекст текущей скважины */
export const WellContext = createContext<[WellDto, (well: WellDto) => void]>([{}, () => {}])
/** Контекст текущего корневого пути */
export const RootPathContext = createContext<string>('/')
/** Контекст текущего пользователя */
export const UserContext = createContext<UserTokenDto>({})
/** Контекст метода редактирования параметров заголовка и меню */
export const LayoutPropsContext = createContext<(props: LayoutPortalProps) => void>(() => {})
/** Контекст для блока справа от крошек на страницах скважин и админки */
export const TopRightBlockContext = createContext<(block: JSX.Element) => void>(() => {})
/** Контекст со списком месторождений */
export const DepositsContext = createContext<DepositDto[]>([])
/**
* Получить текущую скважину
*
* @returns Текущая скважина, либо `null`
*/
export const useWell = () => useContext(WellContext)
/**
* Получить текущий корневой путь
*
* @returns Текущий корневой путь
*/
export const useRootPath = () => useContext(RootPathContext)
/**
* Получить текущего пользователя
*
* @returns Текущий пользователь, либо `null`
*/
export const useUser = () => useContext(UserContext)
/**
* Получить список скважин
*
* @returns Список скважин
*/
export const useDeposits = () => useContext(DepositsContext)
/**
* Получить метод задания элементов справа от крошек
*
* @returns Метод задания элементов справа от крошек
*/
export const useTopRightBlock = () => useContext(TopRightBlockContext)
/**
* Получить метод задания параметров заголовка и меню
*
* @returns Получить метод задания параметров заголовка и меню
*/
export const useLayoutProps = (props?: LayoutPortalProps) => {
const setLayoutProps = useContext(LayoutPropsContext)
useEffect(() => {
if (props) setLayoutProps(props)
}, [setLayoutProps, props])
return setLayoutProps
}

23
src/context/deposit.ts Normal file
View File

@ -0,0 +1,23 @@
import { createContext, useContext } from 'react'
import { DepositDto } from '@api'
/** Контекст текущего месторождения */
export const DepositContext = createContext<DepositDto | null>(null)
/**
* Получить текущее месторождение
*
* @returns Текущее месторождение, либо `null`
*/
export const useDeposit = () => useContext(DepositContext)
/** Контекст со списком месторождений */
export const DepositListContext = createContext<DepositDto[]>([])
/**
* Получить список скважин
*
* @returns Список скважин
*/
export const useDepositList = () => useContext(DepositListContext)

5
src/context/index.ts Normal file
View File

@ -0,0 +1,5 @@
export * from './deposit'
export * from './layout_props'
export * from './root_path'
export * from './user'
export * from './well'

View File

@ -0,0 +1,31 @@
import { createContext, useContext, useEffect } from 'react'
import { LayoutPortalProps } from '@components/LayoutPortal'
/** Контекст метода редактирования параметров заголовка и меню */
export const LayoutPropsContext = createContext<(props: LayoutPortalProps) => void>(() => {})
/**
* Получить метод задания параметров заголовка и меню
*
* @returns Получить метод задания параметров заголовка и меню
*/
export const useLayoutProps = (props?: LayoutPortalProps) => {
const setLayoutProps = useContext(LayoutPropsContext)
useEffect(() => {
if (props) setLayoutProps(props)
}, [setLayoutProps, props])
return setLayoutProps
}
/** Контекст для блока справа от крошек на страницах скважин и админки */
export const TopRightBlockContext = createContext<(block: JSX.Element) => void>(() => {})
/**
* Получить метод задания элементов справа от крошек
*
* @returns Метод задания элементов справа от крошек
*/
export const useTopRightBlock = () => useContext(TopRightBlockContext)

11
src/context/root_path.ts Normal file
View File

@ -0,0 +1,11 @@
import { createContext, useContext } from 'react'
/** Контекст текущего корневого пути */
export const RootPathContext = createContext<string>('/')
/**
* Получить текущий корневой путь
*
* @returns Текущий корневой путь
*/
export const useRootPath = () => useContext(RootPathContext)

13
src/context/user.ts Normal file
View File

@ -0,0 +1,13 @@
import { createContext, useContext } from 'react'
import { UserTokenDto } from '@api'
/** Контекст текущего пользователя */
export const UserContext = createContext<UserTokenDto>({})
/**
* Получить текущего пользователя
*
* @returns Текущий пользователь, либо `null`
*/
export const useUser = () => useContext(UserContext)

13
src/context/well.ts Normal file
View File

@ -0,0 +1,13 @@
import { createContext, useContext } from 'react'
import { WellDto } from '@api'
/** Контекст текущей скважины */
export const WellContext = createContext<[WellDto, (well: WellDto) => void]>([{}, () => {}])
/**
* Получить текущую скважину
*
* @returns Текущая скважина, либо пустой объект
*/
export const useWell = () => useContext(WellContext)

40
src/images/AsbLogo.svg Normal file
View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 1800 8000 4000" version="1.1" alt="АСБ" fill="#2d2242">
<defs>
<clipPath id="logo-clip-path-id0">
<path d="m 5189,716 c 587,0 1063,476 1063,1063 0,587 -476,1063 -1063,1063 -588,0 -1064,-476 -1064,-1063 0,-587 476,-1063 1064,-1063 z" />
</clipPath>
</defs>
<g transform="translate(-982.80503,2106.4728)">
<g fill="#e31e24">
<path d="M 1756,3564 H 1018 L 3236,2 3848,3 4452,0 4400,184 C 4637,66 4905,0 5189,0 5751,0 6253,261 6579,669 l -233,810 C 6213,964 5745,584 5189,584 c -528,0 -975,341 -1134,815 l -30,108 c -20,87 -31,178 -31,272 0,660 535,1195 1195,1195 318,0 607,-125 821,-327 l -220,764 c -185,88 -388,147 -601,147 -636,0 -1194,-334 -1508,-836 l -239,842 h -702 l 187,-595 H 2146 Z M 3082,2443 3703,446 2463,2444 Z" />
<path d="m 7725,3574 c -534.9685,-1.0406 -1176.3914,-0.3681 -1863.0925,-0.3084 L 6882,2 l 1790,1 -136,559 -1176,9 -121,462 h 836 c 570,93 953,697 950,1254 -3,656 -585,1291 -1300,1287 z m -995,-606 c 333,0 665,0 998,2 381,2 691,-335 693,-686 1,-291 -206,-632 -510,-673 h -824 z"/>
</g>
<path d="m 5347,1437 h -242 v -122 h 242 z" />
<path d="m 5455,1555 h -463 v -86 h 463 z" />
<path d="m 5597,2523 h -737 l 167,-936 h 392 z" />
<path d="m 5246,2523 h -46 v -788 h 46 z" />
<g fill="#fefefe">
<path d="m 5166,1737 -105,93 28,-154 z" />
<path d="m 5288,1737 105,93 -28,-154 z" />
<path d="m 5224,1696 61,-42 h -113 z" />
<path d="m 5143,2007 -124,55 20,-110 z" />
<path d="m 5310,2007 125,55 -20,-110 z" />
<path d="m 5091,1894 138,68 136,-68 -136,-111 z" />
<path d="m 5052,2132 180,119 180,-121 -183,-87 z" />
<path d="m 5163,2297 -214,148 47,-261 z" />
<path d="m 5292,2297 213,148 -47,-261 z" />
<path d="m 5226,2337 271,186 h -539 z" />
</g>
<g clip-path="url(#logo-clip-path-id0)">
<g fill="9d9e9e">
<path d="m 5136,177 c -688,66 -1152,378 -1415,911 l 1475,-196 783,591 z" />
<path d="M 6684,1229 C 6401,599 5957,260 5367,182 l 659,1333 -308,931 z" />
</g>
<path d="m 6189,3044 c 509,-466 692,-994 581,-1579 l -1059,1044 -981,-1 z" />
<path d="m 4267,3105 c 598,345 1157,360 1681,78 L 4633,2488 4337,1552 Z" />
<path d="m 3626,1346 c -142,676 17,1212 447,1622 l 253,-1466 798,-571 z" />
</g>
<path fill="none" d="m 5189,716 c 587,0 1063,476 1063,1063 0,587 -476,1063 -1063,1063 -588,0 -1064,-476 -1064,-1063 0,-587 476,-1063 1064,-1063 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

33
src/images/Logo.svg Normal file
View File

@ -0,0 +1,33 @@
<svg version="1.1" viewBox="0 0 896 282" fill="#f3f6e8" className="logo" overflow="visible">
<g className="logo-icon">
<path fill="#9e1937" d="m126 32.2h-92.5c-2.58 0-4.67-2.09-4.67-4.67s2.09-4.67 4.67-4.67h92.5c2.58 0 4.67 2.09 4.67 4.67s-2.09 4.67-4.67 4.67" />
<path d="m30.5 274h98.3l-36.1-194h-26.2zm104 9.33h-110c-1.39 0-2.7-0.617-3.59-1.68-0.887-1.07-1.25-2.47-0.999-3.83l37.8-203c0.41-2.21 2.34-3.82 4.59-3.82h34c2.25 0 4.18 1.6 4.59 3.82l37.8 203c0.253 1.36-0.112 2.77-0.999 3.83-0.887 1.07-2.2 1.68-3.59 1.68" />
<path d="m113 10.3h-66.9c-2.58 0-4.67-2.09-4.67-4.67 0-2.58 2.09-4.67 4.67-4.67h66.9c2.58 0 4.67 2.09 4.67 4.67 0 2.58-2.09 4.67-4.67 4.67" />
<path d="m155 262c-2.17 0-4.12-1.53-4.57-3.74l-41.1-203h-58.8l-39.9 197h85.9l-44.2-33.2c-1.61-1.21-2.26-3.3-1.62-5.21 0.635-1.91 2.42-3.19 4.43-3.19h37.1l-34.6-28.7c-1.51-1.26-2.08-3.33-1.41-5.17 0.668-1.85 2.42-3.08 4.39-3.08h27.8l-25.3-25.5c-1.33-1.34-1.72-3.34-1-5.08 0.725-1.74 2.42-2.87 4.31-2.87h18.3l-16.8-19c-1.22-1.37-1.51-3.33-0.759-5.01 0.754-1.67 2.42-2.75 4.25-2.75h17.6c2.58 0 4.67 2.09 4.67 4.67s-2.09 4.67-4.67 4.67h-7.23l16.8 19c1.22 1.38 1.51 3.34 0.759 5.01-0.754 1.67-2.42 2.75-4.25 2.75h-17.4l25.3 25.5c1.33 1.34 1.72 3.34 1 5.08-0.724 1.74-2.42 2.87-4.31 2.87h-26.1l34.6 28.7c1.51 1.26 2.08 3.33 1.41 5.17-0.668 1.85-2.42 3.08-4.39 3.08h-36.1l44.2 33.2c1.61 1.21 2.26 3.3 1.62 5.21-0.635 1.91-2.42 3.19-4.43 3.19h-106c-1.4 0-2.73-0.629-3.61-1.71-0.886-1.09-1.24-2.51-0.961-3.88l41.8-206c0.441-2.18 2.35-3.74 4.57-3.74h66.5c2.22 0 4.13 1.56 4.57 3.74l41.8 206c0.512 2.53-1.12 4.99-3.65 5.5-0.312 0.0625-0.624 0.0948-0.932 0.0948" />
</g>
<g className="logo-label">
<path fill="#9e1937" d="m316 140c2.76-2.67 5.01-1.71 5.01 2.13v30.3c0 3.84-3.14 6.98-6.98 6.98h-2.38c-3.84 0-6.98-3.14-6.98-6.98v-14.5c0-3.84 2.26-9.16 5.01-11.8l6.31-6.09" />
<path d="m647 159c0 3.84-3.14 6.98-6.97 6.98h-3.84c-3.84 0-6.97-3.14-6.97-6.98v-118c0-3.84 3.14-6.98 6.97-6.98h3.84c3.84 0 6.97 3.14 6.97 6.98v118" />
<path d="m707 144c0 3.84 3.14 6.97 6.98 6.97h52.7c3.84 0 6.98 3.14 6.98 6.97v1.84c0 3.84-3.14 6.98-6.98 6.98h-70.4c-3.84 0-6.97-3.14-6.97-6.98v-118c0-3.84 3.14-6.98 6.97-6.98h3.84c3.84 0 6.97 3.14 6.97 6.98v102" />
<path d="m827 144c0 3.84 3.14 6.97 6.97 6.97h52.7c3.84 0 6.98 3.14 6.98 6.97v1.84c0 3.84-3.14 6.98-6.98 6.98h-70.4c-3.84 0-6.98-3.14-6.98-6.98v-118c0-3.84 3.14-6.98 6.98-6.98h3.84c3.84 0 6.98 3.14 6.98 6.98v102" />
<path d="m279 101c0 33.2-15.2 49.9-39.4 49.9h-19.3c-3.84 0-6.97-3.14-6.97-6.97v-87.3c0-3.84 3.14-6.98 6.97-6.98h20.5c23 0 38.1 18.1 38.1 51.4zm18.3 1.09c0-29.6-12.9-67.7-56.1-67.7h-38.7c-3.84 0-6.98 3.14-6.98 6.98v118c0 3.84 3.14 6.98 6.98 6.98h39.4c34.3 0 55.4-26.1 55.4-64.1" />
<path d="m432 101c0 33.2-15.2 49.9-39.4 49.9h-19.3c-3.84 0-6.97-3.14-6.97-6.97v-87.3c0-3.84 3.14-6.98 6.97-6.98h20.5c23 0 38.1 18.1 38.1 51.4zm18.3 1.09c0-29.6-12.9-67.7-56.1-67.7h-38.7c-3.84 0-6.98 3.14-6.98 6.98v118c0 3.84 3.14 6.98 6.98 6.98h39.4c34.3 0 55.4-26.1 55.4-64.1" />
<path d="m539 94.6h-33c-3.84 0-6.98-3.14-6.98-6.98v-30.9c0-3.84 3.14-6.98 6.98-6.98h36.3c8.89 0 23.6 1.63 23.6 22 0 19.4-13.8 22.9-26.9 22.9zm26.9 6.9c8.35-4.9 18.3-12.2 18.3-31.6 0-27.8-21.8-35.4-43.4-35.4h-52.6c-3.84 0-6.98 3.14-6.98 6.98v118c0 3.84 3.14 6.97 6.98 6.97h3.84c3.84 0 6.97-3.14 6.97-6.97v-42.5c0-3.84 3.14-6.98 6.98-6.98h34.9c21.4 0 23.6 12.5 23.6 23.4 0.308 6.29 0 16.5 0 23.9s3.76 9.1 8.94 9.1h8.85v-40.1c0-18.5-10.3-20.7-16.3-24.7" />
<path d="m220 256c-0.964 0-1.77-0.809-1.77-1.77v-2.85h-17.7c-0.962 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.807-1.77 1.77-1.77h2.62c0.962 0 1.77 0.809 1.77 1.77v19.4h9.25v-19.4c0-0.965 0.807-1.77 1.77-1.77h2.62c0.964 0 1.77 0.809 1.77 1.77v19.4h1.89c0.962 0 1.77 0.807 1.77 1.77v6.86c0 0.962-0.809 1.77-1.77 1.77h-2.23" />
<path d="m267 251c-0.964 0-1.77-0.806-1.77-1.77v-15l-11.2 15.6c-0.463 0.694-1.2 1.12-2.01 1.12h-2.39c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.807-1.77 1.77-1.77h2.62c0.962 0 1.77 0.809 1.77 1.77v15l11.2-15.6c0.463-0.691 1.19-1.12 2-1.12h2.39c0.964 0 1.77 0.809 1.77 1.77v23.4c0 0.965-0.809 1.77-1.77 1.77h-2.62" />
<path d="m319 238c0-2.54-1.39-4.78-5.51-4.89v9.78c4.04-0.076 5.51-1.73 5.51-4.89zm-17.1 0c0 3.12 1.66 4.81 5.43 4.89v-9.78c-3.93 0.115-5.43 2.27-5.43 4.89zm11.6 12.4c0 0.965-0.807 1.77-1.77 1.77h-2.62c-0.962 0-1.77-0.809-1.77-1.77v-1.85c-7.2-0.0758-12-3.82-12-10.6 0-6.66 4.89-10.4 12-10.6v-1.08c0-0.965 0.812-1.77 1.77-1.77h2.62c0.964 0 1.77 0.809 1.77 1.77v1.08c7.01 0.158 12.1 3.97 12.1 10.6 0 6.7-4.89 10.5-12.1 10.6v1.85" />
<path d="m356 236c0.886 0.115 1.92 0.194 2.81 0.194 1.62 0 3.62-0.694 3.62-3.31 0-2.39-1.54-3.12-3.74-3.12-0.807 0-1.42 0.0393-2.69 0.0788zm13-3.08c0 4.81-3.74 9.05-9.98 9.05-0.576 0-2.04 0-3-0.115v7.4c0 0.965-0.809 1.77-1.77 1.77h-2.62c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-1 0.807-1.81 1.77-1.81 2.04-0.0393 4.97-0.0758 6.47-0.0758 8.2 0 10.9 4.28 10.9 8.94" />
<path d="m405 246c5.12 0 7.78-3.62 7.78-8.17 0-4.93-3.43-8.16-7.78-8.16-4.47 0-7.78 3.24-7.78 8.16 0 4.62 3.47 8.17 7.78 8.17zm0-22.1c8.2 0 14.3 5.35 14.3 13.9 0 8.17-6.13 13.9-14.3 13.9-8.21 0-14.3-5.35-14.3-13.9 0-7.82 5.74-13.9 14.3-13.9" />
<path d="m450 246c0.423 0.115 0.923 0.232 2.08 0.232 2.39 0 3.58-1.04 3.58-2.85 0-1.7-1.27-2.43-3.27-2.43h-2.39zm2.04-10.4c1.58 0 2.85-0.654 2.85-2.62 0-1.62-1.46-2.43-2.96-2.43-0.77 0-1.23 0.0768-1.93 0.156v4.89zm5.93 1.93c1.96 0.771 3.85 2.73 3.85 6.2 0 5.66-4.39 8.32-10.2 8.32-1.85 0-4.2-0.0364-5.97-0.113-0.925-0.0393-1.77-0.923-1.77-1.85v-23.3c0-0.964 0.809-1.81 1.77-1.85 1.81-0.0759 4.32-0.155 6.39-0.155 6.43 0 9.05 2.97 9.05 6.7 0 2.81-1.08 4.7-3.08 6.05" />
<path d="m500 246c5.12 0 7.78-3.62 7.78-8.17 0-4.93-3.43-8.16-7.78-8.16-4.47 0-7.78 3.24-7.78 8.16 0 4.62 3.47 8.17 7.78 8.17zm0-22.1c8.2 0 14.3 5.35 14.3 13.9 0 8.17-6.13 13.9-14.3 13.9-8.21 0-14.3-5.35-14.3-13.9 0-7.82 5.74-13.9 14.3-13.9" />
<path d="m555 250c0 0.965-0.807 1.77-1.77 1.77h-12.8c-0.962 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.809-1.77 1.77-1.77h12.4c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.96-0.809 1.77-1.77 1.77h-8.05v4.74h6.89c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.965-0.809 1.77-1.77 1.77h-6.89v4.89h8.43c0.964 0 1.77 0.807 1.77 1.77v2.23" />
<path d="m613 246c1 0.116 1.62 0.192 2.31 0.192 2.54 0 3.35-1.27 3.35-2.78 0-1.58-0.849-3-3.08-3-0.77 0-1.66 0.077-2.58 0.232zm3.08-11.4c5.2 0 8.74 3.24 8.74 8.32 0 5.62-3.74 9.01-10.5 9.01-2.39 0-4.28-0.0758-5.74-0.153-1-0.0393-1.77-0.846-1.77-1.85v-23.4c0-0.965 0.809-1.77 1.77-1.77h12.7c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.96-0.809 1.77-1.77 1.77h-8.32v4.28c0.733-0.157 2.23-0.233 3.08-0.233" />
<path d="m661 238 4.93-11.9c0.347-0.771 1.08-1.27 1.89-1.27h2.2c1.04 0 1.73 0.733 1.73 1.66 0 0.268-0.076 0.538-0.192 0.807l-7.93 18.1c-1.81 4.12-4.16 6.39-7.9 6.39-1.04 0-2.12-0.191-3.16-0.654-0.541-0.268-0.886-0.733-0.886-1.42 0-0.233 0.037-0.502 0.153-0.809l0.733-1.89c0.347-0.923 0.925-1.2 1.62-1.2 0.231 0 0.463 0.0393 0.694 0.0759 0.502 0.118 0.846 0.118 1 0.118 0.809 0 1.46-0.31 1.81-1.08l0.347-0.809-9.98-16.6c-0.192-0.35-0.268-0.697-0.268-1.04 0-0.889 0.615-1.66 1.69-1.66h2.58c0.807 0 1.62 0.462 2.04 1.19l6.89 12" />
<path d="m702 236c0.886 0.115 1.92 0.194 2.81 0.194 1.62 0 3.62-0.694 3.62-3.31 0-2.39-1.54-3.12-3.74-3.12-0.807 0-1.42 0.0393-2.69 0.0788zm13-3.08c0 4.81-3.74 9.05-9.98 9.05-0.578 0-2.04 0-3-0.115v7.4c0 0.965-0.809 1.77-1.77 1.77h-2.62c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-1 0.807-1.81 1.77-1.81 2.04-0.0393 4.97-0.0758 6.47-0.0758 8.2 0 10.9 4.28 10.9 8.94" />
<path d="m756 250c0 0.965-0.807 1.77-1.77 1.77h-12.8c-0.962 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.809-1.77 1.77-1.77h12.4c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.96-0.809 1.77-1.77 1.77h-8.05v4.74h6.89c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.965-0.809 1.77-1.77 1.77h-6.89v4.89h8.43c0.964 0 1.77 0.807 1.77 1.77v2.23" />
<path d="m803 250c0 0.965-0.809 1.77-1.77 1.77h-2.62c-0.964 0-1.77-0.806-1.77-1.77v-9.09h-9.82v9.09c0 0.965-0.812 1.77-1.77 1.77h-2.62c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.807-1.77 1.77-1.77h2.62c0.962 0 1.77 0.809 1.77 1.77v8.55h9.82v-8.55c0-0.965 0.807-1.77 1.77-1.77h2.62c0.964 0 1.77 0.809 1.77 1.77v23.4" />
<path d="m849 251c-0.962 0-1.77-0.806-1.77-1.77v-15l-11.2 15.6c-0.463 0.694-1.19 1.12-2 1.12h-2.39c-0.962 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.809-1.77 1.77-1.77h2.62c0.962 0 1.77 0.809 1.77 1.77v15l11.2-15.6c0.463-0.691 1.2-1.12 2-1.12h2.39c0.962 0 1.77 0.809 1.77 1.77v23.4c0 0.965-0.809 1.77-1.77 1.77h-2.62" />
<path d="m896 250c0 0.965-0.809 1.77-1.77 1.77h-12.8c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.807-1.77 1.77-1.77h12.4c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.96-0.809 1.77-1.77 1.77h-8.05v4.74h6.89c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.965-0.809 1.77-1.77 1.77h-6.89v4.89h8.43c0.962 0 1.77 0.807 1.77 1.77v2.23" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -1,44 +1,14 @@
import { memo } from 'react' import { memo } from 'react'
import { ReactComponent as RawLogo } from './Logo.svg'
export type LogoProps = React.SVGProps<SVGSVGElement> & { export type LogoProps = React.SVGProps<SVGSVGElement> & {
size?: number size?: number
onlyIcon?: boolean onlyIcon?: boolean
} }
export const Logo = memo<LogoProps>(({ size = 170, onlyIcon, ...props }) => ( export const Logo = memo<LogoProps>(({ size = 170, onlyIcon, ...props }) => (
<svg version={'1.1'} viewBox={`0 0 896 282`} fill={'#f3f6e8'} className={'logo'} style={{ width: size, height: 282/896*size, overflow: 'visible' }} {...props}> <RawLogo style={{ width: size, height: 282/896*size }} {...props} />
<g className={'logo-icon'}>
<path fill={'#9e1937'} d={'m126 32.2h-92.5c-2.58 0-4.67-2.09-4.67-4.67s2.09-4.67 4.67-4.67h92.5c2.58 0 4.67 2.09 4.67 4.67s-2.09 4.67-4.67 4.67'} />
<path d={'m30.5 274h98.3l-36.1-194h-26.2zm104 9.33h-110c-1.39 0-2.7-0.617-3.59-1.68-0.887-1.07-1.25-2.47-0.999-3.83l37.8-203c0.41-2.21 2.34-3.82 4.59-3.82h34c2.25 0 4.18 1.6 4.59 3.82l37.8 203c0.253 1.36-0.112 2.77-0.999 3.83-0.887 1.07-2.2 1.68-3.59 1.68'} />
<path d={'m113 10.3h-66.9c-2.58 0-4.67-2.09-4.67-4.67 0-2.58 2.09-4.67 4.67-4.67h66.9c2.58 0 4.67 2.09 4.67 4.67 0 2.58-2.09 4.67-4.67 4.67'} />
<path d={'m155 262c-2.17 0-4.12-1.53-4.57-3.74l-41.1-203h-58.8l-39.9 197h85.9l-44.2-33.2c-1.61-1.21-2.26-3.3-1.62-5.21 0.635-1.91 2.42-3.19 4.43-3.19h37.1l-34.6-28.7c-1.51-1.26-2.08-3.33-1.41-5.17 0.668-1.85 2.42-3.08 4.39-3.08h27.8l-25.3-25.5c-1.33-1.34-1.72-3.34-1-5.08 0.725-1.74 2.42-2.87 4.31-2.87h18.3l-16.8-19c-1.22-1.37-1.51-3.33-0.759-5.01 0.754-1.67 2.42-2.75 4.25-2.75h17.6c2.58 0 4.67 2.09 4.67 4.67s-2.09 4.67-4.67 4.67h-7.23l16.8 19c1.22 1.38 1.51 3.34 0.759 5.01-0.754 1.67-2.42 2.75-4.25 2.75h-17.4l25.3 25.5c1.33 1.34 1.72 3.34 1 5.08-0.724 1.74-2.42 2.87-4.31 2.87h-26.1l34.6 28.7c1.51 1.26 2.08 3.33 1.41 5.17-0.668 1.85-2.42 3.08-4.39 3.08h-36.1l44.2 33.2c1.61 1.21 2.26 3.3 1.62 5.21-0.635 1.91-2.42 3.19-4.43 3.19h-106c-1.4 0-2.73-0.629-3.61-1.71-0.886-1.09-1.24-2.51-0.961-3.88l41.8-206c0.441-2.18 2.35-3.74 4.57-3.74h66.5c2.22 0 4.13 1.56 4.57 3.74l41.8 206c0.512 2.53-1.12 4.99-3.65 5.5-0.312 0.0625-0.624 0.0948-0.932 0.0948'} />
</g>
<g className={'logo-label'}>
<path fill={'#9e1937'} d={'m316 140c2.76-2.67 5.01-1.71 5.01 2.13v30.3c0 3.84-3.14 6.98-6.98 6.98h-2.38c-3.84 0-6.98-3.14-6.98-6.98v-14.5c0-3.84 2.26-9.16 5.01-11.8l6.31-6.09'} />
<path d={'m647 159c0 3.84-3.14 6.98-6.97 6.98h-3.84c-3.84 0-6.97-3.14-6.97-6.98v-118c0-3.84 3.14-6.98 6.97-6.98h3.84c3.84 0 6.97 3.14 6.97 6.98v118'} />
<path d={'m707 144c0 3.84 3.14 6.97 6.98 6.97h52.7c3.84 0 6.98 3.14 6.98 6.97v1.84c0 3.84-3.14 6.98-6.98 6.98h-70.4c-3.84 0-6.97-3.14-6.97-6.98v-118c0-3.84 3.14-6.98 6.97-6.98h3.84c3.84 0 6.97 3.14 6.97 6.98v102'} />
<path d={'m827 144c0 3.84 3.14 6.97 6.97 6.97h52.7c3.84 0 6.98 3.14 6.98 6.97v1.84c0 3.84-3.14 6.98-6.98 6.98h-70.4c-3.84 0-6.98-3.14-6.98-6.98v-118c0-3.84 3.14-6.98 6.98-6.98h3.84c3.84 0 6.98 3.14 6.98 6.98v102'} />
<path d={'m279 101c0 33.2-15.2 49.9-39.4 49.9h-19.3c-3.84 0-6.97-3.14-6.97-6.97v-87.3c0-3.84 3.14-6.98 6.97-6.98h20.5c23 0 38.1 18.1 38.1 51.4zm18.3 1.09c0-29.6-12.9-67.7-56.1-67.7h-38.7c-3.84 0-6.98 3.14-6.98 6.98v118c0 3.84 3.14 6.98 6.98 6.98h39.4c34.3 0 55.4-26.1 55.4-64.1'} />
<path d={'m432 101c0 33.2-15.2 49.9-39.4 49.9h-19.3c-3.84 0-6.97-3.14-6.97-6.97v-87.3c0-3.84 3.14-6.98 6.97-6.98h20.5c23 0 38.1 18.1 38.1 51.4zm18.3 1.09c0-29.6-12.9-67.7-56.1-67.7h-38.7c-3.84 0-6.98 3.14-6.98 6.98v118c0 3.84 3.14 6.98 6.98 6.98h39.4c34.3 0 55.4-26.1 55.4-64.1'} />
<path d={'m539 94.6h-33c-3.84 0-6.98-3.14-6.98-6.98v-30.9c0-3.84 3.14-6.98 6.98-6.98h36.3c8.89 0 23.6 1.63 23.6 22 0 19.4-13.8 22.9-26.9 22.9zm26.9 6.9c8.35-4.9 18.3-12.2 18.3-31.6 0-27.8-21.8-35.4-43.4-35.4h-52.6c-3.84 0-6.98 3.14-6.98 6.98v118c0 3.84 3.14 6.97 6.98 6.97h3.84c3.84 0 6.97-3.14 6.97-6.97v-42.5c0-3.84 3.14-6.98 6.98-6.98h34.9c21.4 0 23.6 12.5 23.6 23.4 0.308 6.29 0 16.5 0 23.9s3.76 9.1 8.94 9.1h8.85v-40.1c0-18.5-10.3-20.7-16.3-24.7'} />
<path d={'m220 256c-0.964 0-1.77-0.809-1.77-1.77v-2.85h-17.7c-0.962 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.807-1.77 1.77-1.77h2.62c0.962 0 1.77 0.809 1.77 1.77v19.4h9.25v-19.4c0-0.965 0.807-1.77 1.77-1.77h2.62c0.964 0 1.77 0.809 1.77 1.77v19.4h1.89c0.962 0 1.77 0.807 1.77 1.77v6.86c0 0.962-0.809 1.77-1.77 1.77h-2.23'} />
<path d={'m267 251c-0.964 0-1.77-0.806-1.77-1.77v-15l-11.2 15.6c-0.463 0.694-1.2 1.12-2.01 1.12h-2.39c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.807-1.77 1.77-1.77h2.62c0.962 0 1.77 0.809 1.77 1.77v15l11.2-15.6c0.463-0.691 1.19-1.12 2-1.12h2.39c0.964 0 1.77 0.809 1.77 1.77v23.4c0 0.965-0.809 1.77-1.77 1.77h-2.62'} />
<path d={'m319 238c0-2.54-1.39-4.78-5.51-4.89v9.78c4.04-0.076 5.51-1.73 5.51-4.89zm-17.1 0c0 3.12 1.66 4.81 5.43 4.89v-9.78c-3.93 0.115-5.43 2.27-5.43 4.89zm11.6 12.4c0 0.965-0.807 1.77-1.77 1.77h-2.62c-0.962 0-1.77-0.809-1.77-1.77v-1.85c-7.2-0.0758-12-3.82-12-10.6 0-6.66 4.89-10.4 12-10.6v-1.08c0-0.965 0.812-1.77 1.77-1.77h2.62c0.964 0 1.77 0.809 1.77 1.77v1.08c7.01 0.158 12.1 3.97 12.1 10.6 0 6.7-4.89 10.5-12.1 10.6v1.85'} />
<path d={'m356 236c0.886 0.115 1.92 0.194 2.81 0.194 1.62 0 3.62-0.694 3.62-3.31 0-2.39-1.54-3.12-3.74-3.12-0.807 0-1.42 0.0393-2.69 0.0788zm13-3.08c0 4.81-3.74 9.05-9.98 9.05-0.576 0-2.04 0-3-0.115v7.4c0 0.965-0.809 1.77-1.77 1.77h-2.62c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-1 0.807-1.81 1.77-1.81 2.04-0.0393 4.97-0.0758 6.47-0.0758 8.2 0 10.9 4.28 10.9 8.94'} />
<path d={'m405 246c5.12 0 7.78-3.62 7.78-8.17 0-4.93-3.43-8.16-7.78-8.16-4.47 0-7.78 3.24-7.78 8.16 0 4.62 3.47 8.17 7.78 8.17zm0-22.1c8.2 0 14.3 5.35 14.3 13.9 0 8.17-6.13 13.9-14.3 13.9-8.21 0-14.3-5.35-14.3-13.9 0-7.82 5.74-13.9 14.3-13.9'} />
<path d={'m450 246c0.423 0.115 0.923 0.232 2.08 0.232 2.39 0 3.58-1.04 3.58-2.85 0-1.7-1.27-2.43-3.27-2.43h-2.39zm2.04-10.4c1.58 0 2.85-0.654 2.85-2.62 0-1.62-1.46-2.43-2.96-2.43-0.77 0-1.23 0.0768-1.93 0.156v4.89zm5.93 1.93c1.96 0.771 3.85 2.73 3.85 6.2 0 5.66-4.39 8.32-10.2 8.32-1.85 0-4.2-0.0364-5.97-0.113-0.925-0.0393-1.77-0.923-1.77-1.85v-23.3c0-0.964 0.809-1.81 1.77-1.85 1.81-0.0759 4.32-0.155 6.39-0.155 6.43 0 9.05 2.97 9.05 6.7 0 2.81-1.08 4.7-3.08 6.05'} />
<path d={'m500 246c5.12 0 7.78-3.62 7.78-8.17 0-4.93-3.43-8.16-7.78-8.16-4.47 0-7.78 3.24-7.78 8.16 0 4.62 3.47 8.17 7.78 8.17zm0-22.1c8.2 0 14.3 5.35 14.3 13.9 0 8.17-6.13 13.9-14.3 13.9-8.21 0-14.3-5.35-14.3-13.9 0-7.82 5.74-13.9 14.3-13.9'} />
<path d={'m555 250c0 0.965-0.807 1.77-1.77 1.77h-12.8c-0.962 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.809-1.77 1.77-1.77h12.4c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.96-0.809 1.77-1.77 1.77h-8.05v4.74h6.89c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.965-0.809 1.77-1.77 1.77h-6.89v4.89h8.43c0.964 0 1.77 0.807 1.77 1.77v2.23'} />
<path d={'m613 246c1 0.116 1.62 0.192 2.31 0.192 2.54 0 3.35-1.27 3.35-2.78 0-1.58-0.849-3-3.08-3-0.77 0-1.66 0.077-2.58 0.232zm3.08-11.4c5.2 0 8.74 3.24 8.74 8.32 0 5.62-3.74 9.01-10.5 9.01-2.39 0-4.28-0.0758-5.74-0.153-1-0.0393-1.77-0.846-1.77-1.85v-23.4c0-0.965 0.809-1.77 1.77-1.77h12.7c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.96-0.809 1.77-1.77 1.77h-8.32v4.28c0.733-0.157 2.23-0.233 3.08-0.233'} />
<path d={'m661 238 4.93-11.9c0.347-0.771 1.08-1.27 1.89-1.27h2.2c1.04 0 1.73 0.733 1.73 1.66 0 0.268-0.076 0.538-0.192 0.807l-7.93 18.1c-1.81 4.12-4.16 6.39-7.9 6.39-1.04 0-2.12-0.191-3.16-0.654-0.541-0.268-0.886-0.733-0.886-1.42 0-0.233 0.037-0.502 0.153-0.809l0.733-1.89c0.347-0.923 0.925-1.2 1.62-1.2 0.231 0 0.463 0.0393 0.694 0.0759 0.502 0.118 0.846 0.118 1 0.118 0.809 0 1.46-0.31 1.81-1.08l0.347-0.809-9.98-16.6c-0.192-0.35-0.268-0.697-0.268-1.04 0-0.889 0.615-1.66 1.69-1.66h2.58c0.807 0 1.62 0.462 2.04 1.19l6.89 12'} />
<path d={'m702 236c0.886 0.115 1.92 0.194 2.81 0.194 1.62 0 3.62-0.694 3.62-3.31 0-2.39-1.54-3.12-3.74-3.12-0.807 0-1.42 0.0393-2.69 0.0788zm13-3.08c0 4.81-3.74 9.05-9.98 9.05-0.578 0-2.04 0-3-0.115v7.4c0 0.965-0.809 1.77-1.77 1.77h-2.62c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-1 0.807-1.81 1.77-1.81 2.04-0.0393 4.97-0.0758 6.47-0.0758 8.2 0 10.9 4.28 10.9 8.94'} />
<path d={'m756 250c0 0.965-0.807 1.77-1.77 1.77h-12.8c-0.962 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.809-1.77 1.77-1.77h12.4c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.96-0.809 1.77-1.77 1.77h-8.05v4.74h6.89c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.965-0.809 1.77-1.77 1.77h-6.89v4.89h8.43c0.964 0 1.77 0.807 1.77 1.77v2.23'} />
<path d={'m803 250c0 0.965-0.809 1.77-1.77 1.77h-2.62c-0.964 0-1.77-0.806-1.77-1.77v-9.09h-9.82v9.09c0 0.965-0.812 1.77-1.77 1.77h-2.62c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.807-1.77 1.77-1.77h2.62c0.962 0 1.77 0.809 1.77 1.77v8.55h9.82v-8.55c0-0.965 0.807-1.77 1.77-1.77h2.62c0.964 0 1.77 0.809 1.77 1.77v23.4'} />
<path d={'m849 251c-0.962 0-1.77-0.806-1.77-1.77v-15l-11.2 15.6c-0.463 0.694-1.19 1.12-2 1.12h-2.39c-0.962 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.809-1.77 1.77-1.77h2.62c0.962 0 1.77 0.809 1.77 1.77v15l11.2-15.6c0.463-0.691 1.2-1.12 2-1.12h2.39c0.962 0 1.77 0.809 1.77 1.77v23.4c0 0.965-0.809 1.77-1.77 1.77h-2.62'} />
<path d={'m896 250c0 0.965-0.809 1.77-1.77 1.77h-12.8c-0.964 0-1.77-0.806-1.77-1.77v-23.4c0-0.965 0.807-1.77 1.77-1.77h12.4c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.96-0.809 1.77-1.77 1.77h-8.05v4.74h6.89c0.962 0 1.77 0.809 1.77 1.77v2.23c0 0.965-0.809 1.77-1.77 1.77h-6.89v4.89h8.43c0.962 0 1.77 0.807 1.77 1.77v2.23'} />
</g>
</svg>
)) ))
export default Logo export default Logo

View File

@ -12,7 +12,7 @@ import {
UserOutlined, UserOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import { makeItem, PrivateWellMenu } from '@components/PrivateWellMenu' import { makeItem, PrivateMenu } from '@components/PrivateMenu'
import { isDev } from '@utils' import { isDev } from '@utils'
export const menuItems = [ export const menuItems = [
@ -33,7 +33,7 @@ export const menuItems = [
].filter(Boolean) ].filter(Boolean)
export const AdminNavigationMenu = memo((props) => ( export const AdminNavigationMenu = memo((props) => (
<PrivateWellMenu <PrivateMenu
{...props} {...props}
items={menuItems} items={menuItems}
rootPath={'/admin'} rootPath={'/admin'}

View File

@ -3,11 +3,12 @@ import { Input } from 'antd'
import { import {
EditableTable, EditableTable,
makeColumn,
makeSelectColumn, makeSelectColumn,
makeStringSorter, makeStringSorter,
defaultPagination, defaultPagination,
makeTimezoneColumn makeTimezoneColumn,
makeNumericColumn,
makeTextColumn
} from '@components/Table' } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminClusterService, AdminDepositService } from '@api' import { AdminClusterService, AdminDepositService } from '@api'
@ -30,17 +31,11 @@ const ClusterController = memo(() => {
const clusterColumns = useMemo(() => [ const clusterColumns = useMemo(() => [
makeSelectColumn('Месторождение', 'idDeposit', deposits, '--', { makeSelectColumn('Месторождение', 'idDeposit', deposits, '--', {
width: 200, width: 200,
editable: true,
sorter: makeStringSorter('idDeposit') sorter: makeStringSorter('idDeposit')
}), }),
makeColumn('Название', 'caption', { makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: 200, formItemRules: min1 }),
width: 200, makeNumericColumn('Широта', 'latitude', coordsFormat, undefined, 150),
editable: true, makeNumericColumn('Долгота', 'longitude', coordsFormat, undefined, 150),
sorter: makeStringSorter('caption'),
formItemRules: min1,
}),
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFormat }),
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFormat }),
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }), makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
], [deposits]) ], [deposits])

View File

@ -3,10 +3,9 @@ import { Input } from 'antd'
import { import {
EditableTable, EditableTable,
makeColumn,
makeStringSorter,
makeSelectColumn, makeSelectColumn,
defaultPagination defaultPagination,
makeTextColumn
} from '@components/Table' } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminCompanyService, AdminCompanyTypeService } from '@api' import { AdminCompanyService, AdminCompanyTypeService } from '@api'
@ -37,16 +36,8 @@ const CompanyController = memo(() => {
})) }))
setColumns([ setColumns([
makeColumn('Название', 'caption', { makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: 200, formItemRules: min1 }),
width: 200, makeSelectColumn('Тип компании', 'idCompanyType', companyTypes, null, { width: 200 }),
editable: true,
sorter: makeStringSorter('caption'),
formItemRules: min1,
}),
makeSelectColumn('Тип компании', 'idCompanyType', companyTypes, null, {
width: 200,
editable: true
}),
]) ])
await updateTable() await updateTable()

View File

@ -1,19 +1,14 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Input } from 'antd' import { Input } from 'antd'
import { EditableTable, makeColumn, makeStringSorter, defaultPagination } from '@components/Table' import { EditableTable, defaultPagination, makeTextColumn } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, withPermissions } from '@utils' import { arrayOrDefault, withPermissions } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
import { AdminCompanyTypeService } from '@api' import { AdminCompanyTypeService } from '@api'
const columns = [ const columns = [
makeColumn('Название', 'caption', { makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: 200, formItemRules: min1 }),
width: 200,
editable: true,
sorter: makeStringSorter('caption'),
formItemRules: min1,
}),
] ]
const CompanyTypeController = memo(() => { const CompanyTypeController = memo(() => {

View File

@ -2,15 +2,15 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Input } from 'antd' import { Input } from 'antd'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { EditableTable, makeColumn, defaultPagination, makeTimezoneColumn } from '@components/Table' import { EditableTable, defaultPagination, makeTimezoneColumn, makeTextColumn, makeNumericColumn } from '@components/Table'
import { arrayOrDefault, coordsFormat, withPermissions } from '@utils' import { arrayOrDefault, coordsFormat, withPermissions } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
import { AdminDepositService } from '@api' import { AdminDepositService } from '@api'
const depositColumns = [ const depositColumns = [
makeColumn('Название', 'caption', { width: 200, editable: true, formItemRules: min1 }), makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: 200, formItemRules: min1 }),
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFormat }), makeNumericColumn('Широта', 'latitude', coordsFormat, undefined, 150),
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFormat }), makeNumericColumn('Долгота', 'longitude', coordsFormat, undefined, 150),
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }), makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
] ]

View File

@ -1,23 +1,15 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Input } from 'antd' import { Input } from 'antd'
import { EditableTable, makeColumn, makeStringSorter } from '@components/Table' import { EditableTable, makeTextColumn } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, withPermissions } from '@utils' import { arrayOrDefault, withPermissions } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
import { AdminPermissionService } from '@api' import { AdminPermissionService } from '@api'
const columns = [ const columns = [
makeColumn('Название', 'name', { makeTextColumn('Название', 'name', undefined, undefined, undefined, { isRequired: true, formItemRules: min1 }),
editable: true, makeTextColumn('Описание', 'description'),
sorter: makeStringSorter('name'),
isRequired: true,
formItemRules: min1,
}),
makeColumn('Описание', 'description', {
editable: true,
sorter: makeStringSorter('description'),
}),
] ]
const PermissionController = memo(() => { const PermissionController = memo(() => {

View File

@ -19,15 +19,13 @@ const RoleController = memo(() => {
)), [roles, searchValue]) )), [roles, searchValue])
const columns = useMemo(() => [ const columns = useMemo(() => [
makeTextColumn('Название', 'caption', null, null, null, { width: 100, editable: true, formItemRules: min1 }), makeTextColumn('Название', 'caption', null, null, null, { width: 100, formItemRules: min1 }),
makeTagColumn('Включённые роли', 'roles', roles, 'id', 'caption', { makeTagColumn('Включённые роли', 'roles', roles, 'id', 'caption', {
width: 400, width: 400,
editable: true,
render: (role) => <RoleView role={role} /> render: (role) => <RoleView role={role} />
}, { allowClear: true }), }, { allowClear: true }),
makeTagColumn('Разрешения', 'permissions', permissions, 'id', 'name', { makeTagColumn('Разрешения', 'permissions', permissions, 'id', 'name', {
width: 600, width: 600,
editable: true,
render: (permission) => <PermissionView info={permission} />, render: (permission) => <PermissionView info={permission} />,
}), }),
], [roles, permissions]) ], [roles, permissions])

View File

@ -50,7 +50,7 @@ const TelemetryController = memo(() => {
const columns = useMemo(() => [ const columns = useMemo(() => [
makeColumn('', 'hasParent', { render: mergeRender }), makeColumn('', 'hasParent', { render: mergeRender }),
makeNumericColumn('ID', 'id', null, null, makeNumericRender(0)), makeNumericColumn('ID', 'id', makeNumericRender(0)),
makeTextColumn('UID', 'remoteUid'), makeTextColumn('UID', 'remoteUid'),
makeTextColumn('Назначена на скважину', 'realWell'), makeTextColumn('Назначена на скважину', 'realWell'),
makeDateColumn('Дата начала бурения', 'drillingStartDate'), makeDateColumn('Дата начала бурения', 'drillingStartDate'),

View File

@ -115,7 +115,6 @@ const UserController = memo(() => {
setColumns([ setColumns([
makeTextColumn('Логин', 'login', null, null, null, { makeTextColumn('Логин', 'login', null, null, null, {
editable: true,
formItemRules: [ formItemRules: [
{ required: true }, { required: true },
...createLoginRules, ...createLoginRules,
@ -130,41 +129,34 @@ const UserController = memo(() => {
], ],
}), }),
makeTextColumn('Фамилия', 'surname', filters.surname, null, null, { makeTextColumn('Фамилия', 'surname', filters.surname, null, null, {
editable: true,
formItemRules: [{ required: true }, ...nameRules], formItemRules: [{ required: true }, ...nameRules],
filterSearch: true, filterSearch: true,
onFilter: makeTextOnFilter('surname'), onFilter: makeTextOnFilter('surname'),
}), }),
makeTextColumn('Имя', 'name', filters.name, null, null, { makeTextColumn('Имя', 'name', filters.name, null, null, {
editable: true,
formItemRules: nameRules, formItemRules: nameRules,
filterSearch: true, filterSearch: true,
onFilter: makeTextOnFilter('name'), onFilter: makeTextOnFilter('name'),
}), }),
makeTextColumn('Отчество', 'patronymic', filters.partonymic, null, null, { makeTextColumn('Отчество', 'patronymic', filters.partonymic, null, null, {
editable: true,
formItemRules: nameRules, formItemRules: nameRules,
filterSearch: true, filterSearch: true,
onFilter: makeTextOnFilter('patronymic'), onFilter: makeTextOnFilter('patronymic'),
}), }),
makeTextColumn('E-mail', 'email', filters.email, null, null, { makeTextColumn('E-mail', 'email', filters.email, null, null, {
editable: true,
formItemRules: [{ required: true }, ...emailRules], formItemRules: [{ required: true }, ...emailRules],
filterSearch: true, filterSearch: true,
onFilter: makeTextOnFilter('email'), onFilter: makeTextOnFilter('email'),
}), }),
makeTextColumn('Номер телефона', 'phone', null, null, null, { makeTextColumn('Номер телефона', 'phone', null, null, null, {
editable: true,
formItemRules: phoneRules, formItemRules: phoneRules,
}), }),
makeTextColumn('Должность', 'position', null, null, null, { editable: true }), makeTextColumn('Должность', 'position', null, null, null),
makeTextColumn('Роли', 'roleNames', roleFilters, null, rolesRender, { makeTextColumn('Роли', 'roleNames', roleFilters, null, rolesRender, {
editable: true,
input: <RoleTag roles={roles} />, input: <RoleTag roles={roles} />,
onFilter: makeArrayOnFilter('roleNames'), onFilter: makeArrayOnFilter('roleNames'),
}), }),
makeSelectColumn('Компания', 'idCompany', companies, '--', { makeSelectColumn('Компания', 'idCompany', companies, '--', {
editable: true,
sorter: makeNumericSorter('idCompany'), sorter: makeNumericSorter('idCompany'),
}) })
]) ])

View File

@ -12,11 +12,12 @@ import {
EditableTable, EditableTable,
makeColumn, makeColumn,
makeSelectColumn, makeSelectColumn,
makeStringSorter,
makeNumericSorter, makeNumericSorter,
makeTagColumn, makeTagColumn,
defaultPagination, defaultPagination,
makeTimezoneColumn, makeTimezoneColumn,
makeTextColumn,
makeNumericColumn,
} from '@components/Table' } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { TelemetryView, CompanyView } from '@components/views' import { TelemetryView, CompanyView } from '@components/views'
@ -81,23 +82,11 @@ const WellController = memo(() => {
})) }))
setColumns([ setColumns([
makeSelectColumn('Куст', 'idCluster', clusters, '--', { makeSelectColumn('Куст', 'idCluster', clusters, '--', { width: '5rem', sorter: makeNumericSorter('idCluster') }),
width: '5rem', makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: '5rem' }),
editable: true, makeSelectColumn('Тип', 'idWellType', wellTypes, '--', { width: 150, sorter: makeNumericSorter('idWellType') }),
sorter: makeNumericSorter('idCluster'), makeNumericColumn('Широта', 'latitude', coordsFormat, undefined, 150),
}), makeNumericColumn('Долгота', 'longitude', coordsFormat, undefined, 150),
makeColumn('Название', 'caption', {
width: '5rem',
editable: true,
sorter: makeStringSorter('caption'),
}),
makeSelectColumn('Тип', 'idWellType', wellTypes, '--', {
width: 150,
editable: true,
sorter: makeNumericSorter('idWellType'),
}),
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFormat }),
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFormat }),
makeColumn('Телеметрия', 'telemetry', { makeColumn('Телеметрия', 'telemetry', {
editable: true, editable: true,
render: (telemetry) => <TelemetryView telemetry={telemetry} />, render: (telemetry) => <TelemetryView telemetry={telemetry} />,
@ -105,7 +94,6 @@ const WellController = memo(() => {
}, ), }, ),
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 170 }), makeTimezoneColumn('Зона', 'timezone', null, true, { width: 170 }),
makeTagColumn('Компании', 'companies', companies, 'id', 'caption', { makeTagColumn('Компании', 'companies', companies, 'id', 'caption', {
editable: true,
render: (company) => <CompanyView company={company} />, render: (company) => <CompanyView company={company} />,
}), }),
]) ])

View File

@ -1,9 +1,9 @@
import { Navigate, Route, Routes, useLocation } from 'react-router-dom' import { Navigate, Route, Routes } from 'react-router-dom'
import { lazy, memo, useEffect, useMemo } from 'react' import { lazy, memo, useEffect, useMemo } from 'react'
import { RootPathContext, useLayoutProps, useRootPath } from '@asb/context' import { RootPathContext, useLayoutProps, useRootPath } from '@asb/context'
import { FastRunMenu } from '@components/FastRunMenu' import { FastRunMenu } from '@components/FastRunMenu'
import { makeMenuBreadcrumbItems } from '@components/MenuBreadcrumb' import { makeMenuBreadcrumbItemsRender } from '@components/MenuBreadcrumb'
import { NoAccessComponent, withPermissions } from '@utils' import { NoAccessComponent, withPermissions } from '@utils'
import { AdminNavigationMenu, menuItems } from './AdminNavigationMenu' import { AdminNavigationMenu, menuItems } from './AdminNavigationMenu'
@ -21,21 +21,18 @@ const TelemetryViewer = lazy(() => import('./Telemetry/TelemetryViewer'))
const TelemetryMerger = lazy(() => import('./Telemetry/TelemetryMerger')) const TelemetryMerger = lazy(() => import('./Telemetry/TelemetryMerger'))
const VisitLog = lazy(() => import('./VisitLog')) const VisitLog = lazy(() => import('./VisitLog'))
const layoutProps = {
sider: <AdminNavigationMenu />,
title: 'Администраторская панель',
isAdmin: true,
breadcrumb: makeMenuBreadcrumbItemsRender(menuItems, /^\/admin\//),
}
const AdminPanel = memo(() => { const AdminPanel = memo(() => {
const location = useLocation()
const root = useRootPath() const root = useRootPath()
const rootPath = useMemo(() => `${root}/admin`, [root]) const rootPath = useMemo(() => `${root}/admin`, [root])
const setLayoutProps = useLayoutProps() useLayoutProps(layoutProps)
useEffect(() => {
setLayoutProps({
sider: <AdminNavigationMenu />,
title: 'Администраторская панель',
isAdmin: true,
breadcrumb: makeMenuBreadcrumbItems(menuItems, location.pathname, /^\/admin\//),
})
}, [location.pathname])
return ( return (
<RootPathContext.Provider value={rootPath}> <RootPathContext.Provider value={rootPath}>

View File

@ -7,7 +7,7 @@ import {
makeTextColumn, makeTextColumn,
makeGroupColumn, makeGroupColumn,
makeColumn, makeColumn,
makeNumericColumnPlanFact, makeNumericColumnPlanFactOld,
Table, Table,
makeNumericRender, makeNumericRender,
makeNumericColumn, makeNumericColumn,
@ -117,7 +117,10 @@ const ClusterWells = memo(({ statsWells }) => {
const columns = useMemo(() => [ const columns = useMemo(() => [
makeTextColumn('скв №', 'caption', null, null, makeTextColumn('скв №', 'caption', null, null,
(_, item) => ( (_, item) => (
<Link to={{ pathname: `/well/${item.id}`, state: { from: location.pathname }}} style={{display: 'flex', alignItems: 'center'}}> <Link
to={{ pathname: `/well/${item.id}`, state: { from: location.pathname }}}
style={{ display: 'flex', alignItems: 'center' }}
>
<PointerIcon <PointerIcon
state={item.idState === 1 ? 'active' : 'unknown'} state={item.idState === 1 ? 'active' : 'unknown'}
width={32} width={32}
@ -133,10 +136,10 @@ const ClusterWells = memo(({ statsWells }) => {
makeDateColumn('начало', 'factStart'), makeDateColumn('начало', 'factStart'),
makeDateColumn('окончание', 'factEnd'), makeDateColumn('окончание', 'factEnd'),
]), ]),
makeNumericColumnPlanFact('Продолжительность, сут', 'period', filtersMinMax, makeFilterMinMaxFunction, numericRender), makeNumericColumnPlanFactOld('Продолжительность, сут', 'period', numericRender, makeFilterMinMaxFunction, { filters: filtersMinMax }),
makeNumericColumnPlanFact('МСП, м/ч', 'rateOfPenetration', filtersMinMax, makeFilterMinMaxFunction, numericRender), makeNumericColumnPlanFactOld('МСП, м/ч', 'rateOfPenetration', numericRender, makeFilterMinMaxFunction, { filters: filtersMinMax }),
makeNumericColumnPlanFact('Рейсовая скорость, м/ч', 'routeSpeed', filtersMinMax, makeFilterMinMaxFunction, numericRender), makeNumericColumnPlanFactOld('Рейсовая скорость, м/ч', 'routeSpeed', numericRender, makeFilterMinMaxFunction, { filters: filtersMinMax }),
makeNumericColumn('НПВ, ч', 'notProductiveTimeFact', filtersMinMax, makeFilterMinMaxFunction, numericRender), makeNumericColumn('НПВ, ч', 'notProductiveTimeFact', numericRender, makeFilterMinMaxFunction, { filters: filtersMinMax }),
makeColumn('TVD', 'tvd', { align: 'center', render: (_, value) => ( makeColumn('TVD', 'tvd', { align: 'center', render: (_, value) => (
<Button onClick={() => { <Button onClick={() => {
setSelectedWell(value) setSelectedWell(value)

View File

@ -1,14 +1,14 @@
import { memo, useMemo } from 'react' import { memo, useMemo } from 'react'
import { Table, makeTextColumn, makeNumericColumnPlanFact } from '@components/Table' import { Table, makeTextColumn, makeNumericColumnPlanFactOld } from '@components/Table'
import { getPrecision } from '@utils/functions' import { getPrecision } from '@utils/functions'
const columns = [ const columns = [
makeTextColumn('Конструкция секции', 'sectionType'), makeTextColumn('Конструкция секции', 'sectionType'),
makeTextColumn('Операция', 'operationName'), makeTextColumn('Операция', 'operationName'),
makeNumericColumnPlanFact('Глубина забоя', 'depth', null, null, (number) => getPrecision(number)), makeNumericColumnPlanFactOld('Глубина забоя', 'depth', (number) => getPrecision(number)),
makeNumericColumnPlanFact('Часы', 'durationHours', null, null, (number) => getPrecision(number)), makeNumericColumnPlanFactOld('Часы', 'durationHours', (number) => getPrecision(number)),
makeNumericColumnPlanFact('Комментарий', 'comment', null, null, (text) => text ?? '-') makeNumericColumnPlanFactOld('Комментарий', 'comment', (text) => text ?? '-'),
] ]
export const WellOperationsTable = memo(({ wellOperations }) => { export const WellOperationsTable = memo(({ wellOperations }) => {

View File

@ -1,106 +0,0 @@
import { useEffect, memo, useMemo, useCallback } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { Map, Overlay } from 'pigeon-maps'
import { Popover, Badge } from 'antd'
import { useDeposits, useLayoutProps } from '@asb/context'
import { PointerIcon } from '@components/icons'
import { FastRunMenu } from '@components/FastRunMenu'
import { limitValue, withPermissions } from '@utils'
import '@styles/index.css'
const zoomLimit = limitValue(5, 15)
const calcViewParams = (clusters) => {
if ((clusters?.length ?? 0) <= 0)
return { center: [60.81226, 70.0562], zoom: 5 }
const center = clusters.reduce((sum, cluster) => {
sum[0] += cluster.latitude
sum[1] += cluster.longitude
return sum
}, [0, 0]).map((elm) => elm / clusters.length)
const maxDeg = clusters.reduce((max, cluster) => {
const dLatitude = Math.abs(center[0] - cluster.latitude)
const dLongitude = Math.abs(center[1] - cluster.longitude)
return Math.max(Math.max(dLatitude, dLongitude), max)
}, 0)
// zoom max = 20 (too close)
// zoom min = 1 (mega far)
// 4 - full Russia (161.6 deg)
// 13.5 - Khanty-Mansiysk
const zoom = zoomLimit(5 + 5 / (maxDeg + 0.5))
return { center, zoom }
}
const Deposit = memo(() => {
const deposits = useDeposits()
const setLayoutProps = useLayoutProps()
const location = useLocation()
const makeDepositLinks = useCallback((clusters) => (
<div>
{clusters.map(cluster => (
<Link
key={cluster.id}
to={{
pathname: `/cluster/${cluster.id}`,
state: { from: location.pathname }
}}
>
<div>{cluster.caption}</div>
</Link>
))}
</div>
), [location.pathname])
const viewParams = useMemo(() => calcViewParams(deposits), [deposits])
useEffect(() => {
const hasId = location.pathname.length > '/deposit/'.length
const selectorProps = {
expand: hasId ? [location.pathname] : true,
current: hasId ? location.pathname : undefined,
}
setLayoutProps({
sheet: false,
showSelector: true,
selectorProps,
title: 'Месторождение',
})
}, [setLayoutProps, location.pathname])
return (
<>
<FastRunMenu />
<div className={'deposit-page'}>
<Map {...viewParams}>
{deposits.map(deposit => {
const anchor = [deposit.latitude, deposit.longitude]
const links = makeDepositLinks(deposit.clusters)
return (
<Overlay width={32} anchor={anchor} key={anchor.join(' ')}>
<Popover content={links} trigger={['click']} title={deposit.caption}>
<div className={'pointer'}>
<Badge count={deposit.clusters.length}>
<PointerIcon state={'active'} width={48} height={59} />
</Badge>
</div>
</Popover>
</Overlay>
)
})}
</Map>
</div>
</>
)
})
export default withPermissions(Deposit, ['Cluster.get'])

View File

@ -0,0 +1,25 @@
import { memo } from 'react'
import {
FundOutlined,
HeatMapOutlined,
} from '@ant-design/icons'
import { makeItem, PrivateMenu } from '@components/PrivateMenu'
export const menuItems = [
makeItem('Карта', 'map', [], <HeatMapOutlined />),
makeItem('Наработка АКБ', 'statistics_adw', [], <FundOutlined />),
]
export const DepositNavigationMenu = memo((props) => (
<PrivateMenu
{...props}
items={menuItems}
rootPath={'/deposit/{idDeposit}'}
mode={'inline'}
theme={'dark'}
style={{ backgroundColor: 'transparent' }}
/>
))
export default DepositNavigationMenu

93
src/pages/Deposit/Map.jsx Normal file
View File

@ -0,0 +1,93 @@
import { memo, useMemo, useCallback } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { Map as PigeonMap, Overlay } from 'pigeon-maps'
import { Popover, Badge } from 'antd'
import { useDepositList } from '@asb/context'
import { PointerIcon } from '@components/icons'
import { limitValue, withPermissions } from '@utils'
import '@styles/index.css'
const zoomLimit = limitValue(5, 15)
const calcViewParams = (clusters) => {
if ((clusters?.length ?? 0) <= 0)
return { center: [60.81226, 70.0562], zoom: 5 }
const center = clusters.reduce((sum, cluster) => {
sum[0] += cluster.latitude
sum[1] += cluster.longitude
return sum
}, [0, 0]).map((elm) => elm / clusters.length)
const maxDeg = clusters.reduce((max, cluster) => {
const dLatitude = Math.abs(center[0] - cluster.latitude)
const dLongitude = Math.abs(center[1] - cluster.longitude)
return Math.max(Math.max(dLatitude, dLongitude), max)
}, 0)
// zoom max = 20 (too close)
// zoom min = 1 (mega far)
// 4 - full Russia (161.6 deg)
// 13.5 - Khanty-Mansiysk
const zoom = zoomLimit(5 + 5 / (maxDeg + 0.5))
return { center, zoom }
}
const Map = memo(() => {
const deposits = useDepositList()
const location = useLocation()
const makeDepositLinks = useCallback((clusters) => (
<div>
{clusters.map(cluster => (
<Link
key={cluster.id}
to={{
pathname: `/cluster/${cluster.id}`,
state: { from: location.pathname }
}}
>
<div>{cluster.caption}</div>
</Link>
))}
</div>
), [location.pathname])
const viewParams = useMemo(() => calcViewParams(deposits), [deposits])
return (
<div className={'deposit-page'}>
<PigeonMap {...viewParams}>
{deposits.map(deposit => {
const anchor = [deposit.latitude, deposit.longitude]
const links = makeDepositLinks(deposit.clusters)
return (
<Overlay width={32} anchor={anchor} key={anchor.join(' ')}>
<Popover
content={links}
trigger={['click']}
title={(
<Link to={{ pathname: `/deposit/${deposit.id}` }}>
{deposit.caption}
</Link>
)}
>
<div className={'pointer'}>
<Badge count={deposit.clusters.length}>
<PointerIcon state={'active'} width={48} height={59} />
</Badge>
</div>
</Popover>
</Overlay>
)
})}
</PigeonMap>
</div>
)
})
export default withPermissions(Map, ['Cluster.get'])

View File

@ -0,0 +1,127 @@
import { CheckOutlined, StopOutlined, QuestionCircleOutlined, WarningOutlined } from '@ant-design/icons'
import { memo, useEffect, useMemo, useState } from 'react'
import { Card, Empty, Popover } from 'antd'
import LoaderPortal from '@components/LoaderPortal'
import { getWellTitle, WellView } from '@components/views'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { DateRangeWrapper, makeNumericColumn, makeNumericRender, makeTextColumn, Table } from '@components/Table'
import { arrayOrDefault, withPermissions } from '@utils'
import { SubsystemOperationTimeService } from '@api'
import '@styles/pages/statistics_adw.less'
const numericRender = makeNumericRender(2)
const columns = [
makeTextColumn('Подсистема', 'subsystemName', undefined, undefined, (value, row) => value || row.key),
makeNumericColumn('Проходка, м', 'sumDepthInterval'),
makeNumericColumn('Время работы, ч', 'usedTimeHours'),
makeNumericColumn('Кол-во запусков', 'operationCount'),
makeNumericColumn('Коэф. использования, %', 'kUsage', (value) => numericRender(value * 100)),
]
const getSubsystemState = (subsystem) => {
if (!subsystem || typeof subsystem.kUsage !== 'number') return null
if (subsystem.kUsage <= 0.2) return 'error'
if (subsystem.kUsage <= 0.7) return 'warn'
return 'ok'
}
const getSubsystemIcon = (state) => {
switch (state) {
case 'ok': return <CheckOutlined />
case 'warn': return <WarningOutlined />
case 'error': return <StopOutlined />
default: return <QuestionCircleOutlined />
}
}
const getCardState = (subsystems) => {
if (subsystems.length <= 0) return null
const states = subsystems.map(getSubsystemState)
if (states.some((state) => state === 'error')) return 'error'
if (states.some((state) => state === 'warn')) return 'warn'
return 'ok'
}
const generateSubsystem = (subsystem) => {
const state = getSubsystemState(subsystem)
return (
<div className={`subsystem-status status-${state}`}>
{getSubsystemIcon(state)}
<span key={subsystem.key}>{subsystem.subsystemName || subsystem.key}</span>
</div>
)
}
const onRow = (record) => ({ className: `status-${getSubsystemState(record)}` })
const objectToArray = (obj) => Object.entries(obj).filter(([_, v]) => v).map(([key, v]) => ({ key, ...v }))
const GeneralSubsystemStatistics = memo(() => {
const [isLoading, setIsLoading] = useState(false)
const [dates, setDates] = useState([null, null])
const [data, setData] = useState([])
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const data = await SubsystemOperationTimeService.getStatByWell(dates?.[0]?.toISOString(), dates?.[1]?.toISOString())
const out = arrayOrDefault(data).map(({ well, ...ss }) => ({ well, subsystems: objectToArray(ss) }))
setData(out)
},
setIsLoading,
'Не удалось загрузить статистику наработки подсистем',
{ actionName: 'Загрузка статистики наработки подсистем' },
)
}, [dates])
const cards = useMemo(() => data.map((row) => {
const state = getCardState(row.subsystems)
const card = (
<Card className={`subsystem-card status-${state}`} key={row.well.id} title={getWellTitle(row.well)}>
<div className={'subsystem-card-body'}>
{state ? row.subsystems.map((ss) => generateSubsystem(ss)) : <Empty />}
</div>
</Card>
)
if (!state) return card
return (
<Popover
title={(
<>
<span style={{ paddingRight: 15 }}>Детальная информация по скважине</span>
<WellView well={row.well} />
</>
)}
content={(
<Table
size={'small'}
pagination={false}
dataSource={row.subsystems}
columns={columns}
onRow={onRow}
/>
)}
>{card}</Popover>
)
}), [data])
return (
<LoaderPortal show={isLoading} style={{ flex: 1 }}>
<div className={'statistics-adw-page'}>
<div className={'filter-block'}>
<span>Диапазон дат:</span>
<DateRangeWrapper allowClear onChange={setDates} value={dates} />
</div>
<div className={'well-cards'}>{cards}</div>
</div>
</LoaderPortal>
)
})
export default withPermissions(GeneralSubsystemStatistics, [])

View File

@ -0,0 +1,75 @@
import { Navigate, Route, Routes, useParams } from 'react-router-dom'
import { lazy, memo, useEffect, useMemo } from 'react'
import { DepositContext, RootPathContext, useDepositList, useLayoutProps, useRootPath } from '@asb/context'
import FastRunMenu from '@components/FastRunMenu'
import { makeMenuBreadcrumbItemsRender } from '@components/MenuBreadcrumb'
import { NoAccessComponent, withPermissions } from '@utils'
import { DepositNavigationMenu, menuItems } from './DepositNavigationMenu'
const Map = lazy(() => import('./Map'))
const StatisticsADW = lazy(() => import('./StatisticsADW'))
const breadcrumb = makeMenuBreadcrumbItemsRender(menuItems, /^\/deposit\/[^\/#?]+\//)
const Deposit = memo(() => {
const { '*': param } = useParams()
const setLayoutProps = useLayoutProps()
const deposits = useDepositList()
const root = useRootPath()
const rootPath = useMemo(() => `${root}/deposit`, [root])
const [idDeposit, isMap] = useMemo(() => {
const result = /^([^\/#?]+)(:?\/([^\/#?]+))?/.exec(param)
if (!result) return [null, false]
console.log(result)
return [
result[1] !== 'null' ? Number(result[1]) : null,
result[3] === 'map',
]
}, [param])
const deposit = useMemo(() => deposits.find((deposit) => deposit.id === idDeposit) || null, [deposits, idDeposit])
useEffect(() => {
const key = idDeposit ? `/deposit/${idDeposit}` : null
const selectorProps = {
expand: key ? [key] : true,
current: key || undefined,
}
setLayoutProps({
breadcrumb: !isMap && breadcrumb,
sheet: !isMap,
sider: <DepositNavigationMenu variables={{ idDeposit: idDeposit }} />,
showSelector: isMap,
selectorProps,
title: 'Месторождение',
})
}, [setLayoutProps, idDeposit, isMap])
return (
<RootPathContext.Provider value={rootPath}>
<DepositContext.Provider value={deposit}>
<FastRunMenu />
<Routes>
<Route index element={<Navigate to={'null/'} />} />
<Route path={':idDeposit'}>
<Route index element={<Navigate to={'map'} replace />} />
<Route path={'*'} element={<NoAccessComponent />} />
<Route path={'map'} element={<Map />} />
<Route path={'statistics_adw'} element={<StatisticsADW />} />
</Route>
</Routes>
</DepositContext.Provider>
</RootPathContext.Provider>
)
})
export default withPermissions(Deposit, [])

View File

@ -10,7 +10,7 @@ import { OperationStatService, WellOperationService } from '@api'
import { arrayOrDefault, withPermissions } from '@utils' import { arrayOrDefault, withPermissions } from '@utils'
import '@styles/index.css' import '@styles/index.css'
import '@styles/statistics.less' import '@styles/pages/statistics.less'
const { Text } = Typography const { Text } = Typography
const { Summary } = RawTable const { Summary } = RawTable
@ -21,13 +21,13 @@ const speedNumericRender = (section) => numericRender(section?.speed)
const makeSectionSorter = (key, name) => (a, b) => (a?.[key]?.[name] ?? 0) - (b?.[key]?.[name] ?? 0) const makeSectionSorter = (key, name) => (a, b) => (a?.[key]?.[name] ?? 0) - (b?.[key]?.[name] ?? 0)
export const makeSectionColumn = (title, key, { speedRender } = {}) => makeGroupColumn(title, [ export const makeSectionColumn = (title, key, { speedRender } = {}) => makeGroupColumn(title, [
makeNumericColumn('Проходка', key, null, null, (section => numericRender(section?.depth)), 100, { makeNumericColumn('Проходка', key, (section => numericRender(section?.depth)), undefined, 100, {
sorter: makeSectionSorter(key, 'depth'), sorter: makeSectionSorter(key, 'depth'),
}), }),
makeNumericColumn('Время', key, null, null, (section => numericRender(section?.time)), 100, { makeNumericColumn('Время', key, (section => numericRender(section?.time)), undefined, 100, {
sorter: makeSectionSorter(key, 'time'), sorter: makeSectionSorter(key, 'time'),
}), }),
makeNumericColumn((<>V<sub>рейсовая</sub></>), key, null, null, speedRender ?? speedNumericRender, 100, { makeNumericColumn((<>V<sub>рейсовая</sub></>), key, speedRender ?? speedNumericRender, undefined, 100, {
sorter: makeSectionSorter(key, 'speed'), sorter: makeSectionSorter(key, 'speed'),
}), }),
]) ])
@ -37,7 +37,7 @@ export const defaultColumns = [
makeTextColumn('Скважина', 'caption', null, null, null, { fixed: 'left', width: 100 }), makeTextColumn('Скважина', 'caption', null, null, null, { fixed: 'left', width: 100 }),
] ]
const scrollSettings = { scrollToFirstRowOnChange: true, x: 100, y: 200 } const scrollSettings = { scrollToFirstRowOnChange: true, x: 100, y: '25vh' }
const summaryColSpan = 1 /// TODO: Когда добавится куст изменить на 2 const summaryColSpan = 1 /// TODO: Когда добавится куст изменить на 2
const getWellData = async (wellsList) => { const getWellData = async (wellsList) => {

View File

@ -1,146 +0,0 @@
import { memo, useCallback, useEffect, useState } from 'react'
import { Button, Modal, Popconfirm } from 'antd'
import { useWell } from '@asb/context'
import { makeColumn, makeGroupColumn, makeNumericRender, makeSelectColumn, Table } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { DrillParamsService, WellOperationService } from '@api'
const getDeepValue = (data, key) => {
if (!key || key.trim() === '') return null
const keys = key.split('.')
let out = data
while (keys.length > 0) {
if (!(keys[0] in out)) return null
out = out[keys[0]]
keys.splice(0, 1)
}
return out
}
const makeNumericSorter = (keys) => (a, b) => getDeepValue(a, keys) - getDeepValue(b, keys)
const numericRender = makeNumericRender(1)
const makeNumericColumn = (title, dataIndex, render, other) => makeColumn(title, dataIndex, {
sorter: makeNumericSorter(dataIndex),
render: (_, record, index) => {
const func = render ?? ((value) => <>{value}</>)
const item = getDeepValue(record, dataIndex)
return func(item, record, index)
},
align: 'right',
...other,
})
const makeAvgRender = (dataIndex) => (avg, record) => {
const max = record[dataIndex]?.max
const fillW = (max - avg) / max * 100
return (
<div className={'avg-column'}>
<div className={'avg-fill'} style={{ width: `${fillW}%` }} />
<div className={'avg-value'}>
{numericRender(avg)}
</div>
</div>
)
}
const makeNumericAvgRange = (title, dataIndex, defaultRender = false) => makeGroupColumn(title, [
makeNumericColumn('мин', `${dataIndex}.min`),
makeNumericColumn('сред', `${dataIndex}.avg`, defaultRender ? undefined : makeAvgRender(dataIndex)),
makeNumericColumn('макс', `${dataIndex}.max`),
])
export const getColumns = async (idWell) => {
let sectionTypes = await WellOperationService.getSectionTypes(idWell)
sectionTypes = Object.entries(sectionTypes).map(([id, value]) => ({
label: value,
value: id,
}))
return [
makeSelectColumn('Конструкция секции','idWellSectionType', sectionTypes, null, {
width: 160,
sorter: makeNumericSorter('idWellSectionType'),
}),
makeNumericAvgRange('Нагрузка, т', 'axialLoad'),
makeNumericAvgRange('Давление, атм', 'pressure'),
makeNumericAvgRange('Момент на ВСП, кН·м', 'rotorTorque', true),
makeNumericAvgRange('Обороты на ВСП, об/мин', 'rotorSpeed'),
makeNumericAvgRange('Расход, л/с', 'flow'),
]
}
export const NewParamsTable = memo(({ selectedWellsKeys }) => {
const [params, setParams] = useState([])
const [paramsColumns, setParamsColumns] = useState([])
const [showParamsLoader, setShowParamsLoader] = useState(false)
const [isParamsModalVisible, setIsParamsModalVisible] = useState(false)
const [well] = useWell()
useEffect(() => {
invokeWebApiWrapperAsync(async () => setParamsColumns(await getColumns(well.id)))
}, [well])
const onParamButtonClick = useCallback(() => invokeWebApiWrapperAsync(
async () => {
setIsParamsModalVisible(true)
const params = await DrillParamsService.getCompositeAll(well.id)
setParams(params)
},
setShowParamsLoader,
`Не удалось загрузить список режимов`,
{ actionName: 'Получение списка режимов скважины', well }
), [well])
const onParamsAddClick = useCallback(() => invokeWebApiWrapperAsync(
async () => {
await DrillParamsService.save(well.id, params)
setIsParamsModalVisible(false)
},
setShowParamsLoader,
`Не удалось добавить режимы в список`,
{ actionName: 'Добавление режима скважины', well }
), [well, params])
return (
<>
<Button
size={'large'}
disabled={selectedWellsKeys.length <= 0}
onClick={onParamButtonClick}
>
Заполнить режимы текущей скважины
</Button>
<Modal
title={'Заполнить режимы текущей скважины'}
centered
open={isParamsModalVisible}
onCancel={() => setIsParamsModalVisible(false)}
width={1700}
footer={(
<Popconfirm title={'Заменить существующие режимы выбранными?'} onConfirm={onParamsAddClick}>
<Button
size={'large'}
disabled={params.length <= 0}
>Сохранить</Button>
</Popconfirm>
)}
>
<LoaderPortal show={showParamsLoader}>
<Table
bordered
size={'small'}
columns={paramsColumns}
dataSource={params}
pagination={false}
/>
</LoaderPortal>
</Modal>
</>
)
})
export default NewParamsTable

View File

@ -1,12 +1,13 @@
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { useState, useEffect, memo, useMemo, lazy, Suspense } from 'react' import { useState, useEffect, memo, useMemo, lazy, Suspense } from 'react'
import { LineChartOutlined, ProfileOutlined, TeamOutlined } from '@ant-design/icons' import { LineChartOutlined, ProfileOutlined, TeamOutlined } from '@ant-design/icons'
import { Button, Badge, Divider, Modal, Row, Col } from 'antd' import { Button, Badge, Divider, Modal } from 'antd'
import { useWell } from '@asb/context' import { useWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import SuspenseFallback from '@components/SuspenseFallback'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { Table, makeTextColumn, makeNumericColumnPlanFact, makeNumericColumn } from '@components/Table' import { Table, makeTextColumn, makeNumericColumnPlanFactOld, makeNumericColumn } from '@components/Table'
import { WellCompositeService } from '@api' import { WellCompositeService } from '@api'
import { import {
hasPermission, hasPermission,
@ -16,8 +17,6 @@ import {
getOperations getOperations
} from '@utils' } from '@utils'
import NewParamsTable from './NewParamsTable'
import SuspenseFallback from '@asb/components/SuspenseFallback'
const Tvd = lazy(() => import('@pages/Well/WellOperations/Tvd')) const Tvd = lazy(() => import('@pages/Well/WellOperations/Tvd'))
const CompaniesTable = lazy(() => import('@pages/Cluster/CompaniesTable')) const CompaniesTable = lazy(() => import('@pages/Cluster/CompaniesTable'))
@ -146,15 +145,15 @@ const WellCompositeSections = memo(({ statsWells, selectedSections }) => {
makeTextColumn('скв №', 'caption', null, null, makeTextColumn('скв №', 'caption', null, null,
(text, item) => <Link to={{ pathname: `/well/${item?.id}`, state: { from: location.pathname }}}>{text ?? '-'}</Link> (text, item) => <Link to={{ pathname: `/well/${item?.id}`, state: { from: location.pathname }}}>{text ?? '-'}</Link>
), ),
makeTextColumn('Секция', 'sectionType', filtersSectionsType, sortBySectionId, (text) => text ?? '-'), makeTextColumn('Секция', 'sectionType', filtersSectionsType, sortBySectionId, (text) => text ?? '-', { width: 100 }),
makeNumericColumnPlanFact('Глубина, м', 'sectionWellDepth', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFactOld('Глубина, м', 'sectionWellDepth', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }),
makeNumericColumnPlanFact('Продолжительность, ч', 'sectionBuildDays', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFactOld('Продолжительность, ч', 'sectionBuildDays', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }),
makeNumericColumnPlanFact('МСП, м/ч', 'sectionRateOfPenetration', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFactOld('МСП, м/ч', 'sectionRateOfPenetration', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }),
makeNumericColumnPlanFact('Рейсовая скорость, м/ч', 'sectionRouteSpeed', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFactOld('Рейсовая скорость, м/ч', 'sectionRouteSpeed', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }),
makeNumericColumnPlanFact('Спуск КНБК, м/ч', 'sectionBhaDownSpeed', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFactOld('Спуск КНБК, м/ч', 'sectionBhaDownSpeed', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }),
makeNumericColumnPlanFact('Подъем КНБК, м/ч', 'sectionBhaUpSpeed', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFactOld('Подъем КНБК, м/ч', 'sectionBhaUpSpeed', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }),
makeNumericColumnPlanFact('Скорость спуска ОК, м/ч', 'sectionCasingDownSpeed', filtersMinMax, makeFilterMinMaxFunction), makeNumericColumnPlanFactOld('Скорость спуска ОК, м/ч', 'sectionCasingDownSpeed', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }),
makeNumericColumn('НПВ, ч', 'nonProductiveHours', filtersMinMax, makeFilterMinMaxFunction, null, '80px'), makeNumericColumn('НПВ, ч', 'nonProductiveHours', undefined, makeFilterMinMaxFunction, '80px', { filters: filtersMinMax }),
{ {
title: 'TVD', title: 'TVD',
render: (value) => ( render: (value) => (
@ -200,11 +199,11 @@ const WellCompositeSections = memo(({ statsWells, selectedSections }) => {
dataSource={rows} dataSource={rows}
size={'small'} size={'small'}
bordered bordered
scroll={{ x: true, y: 620 }} scroll={{ x: true, y: '30vh' }}
rowSelection={rowSelection} rowSelection={rowSelection}
pagination={false} pagination={false}
/> />
<Divider /> <Divider style={{ marginTop: 0 }} />
<Badge.Ribbon text={'комбинированная скважина'} color={'gray'}> <Badge.Ribbon text={'комбинированная скважина'} color={'gray'}>
<h3>Выбранные секции</h3> <h3>Выбранные секции</h3>
</Badge.Ribbon> </Badge.Ribbon>
@ -214,12 +213,9 @@ const WellCompositeSections = memo(({ statsWells, selectedSections }) => {
rowSelection={rowSelection} rowSelection={rowSelection}
size={'small'} size={'small'}
bordered bordered
scroll={{ x: true }} scroll={{ x: true, y: '30vh' }}
pagination={false} pagination={false}
/> />
<Row justify={'end'} style={{ margin: '1rem 0' }}>
<Col><NewParamsTable selectedWellsKeys={selectedWellsKeys} /></Col>
</Row>
<Modal <Modal
title={'TVD'} title={'TVD'}

View File

@ -12,7 +12,7 @@ import { OperationStatService, WellCompositeService } from '@api'
import WellCompositeSections from './WellCompositeSections' import WellCompositeSections from './WellCompositeSections'
import '@styles/well_composite.less' import '@styles/pages/well_composite.less'
const ClusterWells = lazy(() => import('@pages/Cluster/ClusterWells')) const ClusterWells = lazy(() => import('@pages/Cluster/ClusterWells'))

View File

@ -40,7 +40,7 @@ export const DocumentsTemplate = ({ idCategory, well: givenWell, mimeTypes, head
), ),
}, },
makeDateColumn('Дата загрузки', 'uploadDate'), makeDateColumn('Дата загрузки', 'uploadDate'),
makeNumericColumn('Размер', 'size', null, null, (value) => formatBytes(value)), makeNumericColumn('Размер', 'size', (value) => formatBytes(value)),
makeColumn('Автор', 'author', { render: item => <UserView user={item}/> }), makeColumn('Автор', 'author', { render: item => <UserView user={item}/> }),
makeColumn('Компания', 'company', { render: (_, record) => <CompanyView company={record?.author?.company}/> }), makeColumn('Компания', 'company', { render: (_, record) => <CompanyView company={record?.author?.company}/> }),
...(customColumns ?? []) ...(customColumns ?? [])

View File

@ -8,7 +8,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory'
import { DrillingProgramService } from '@api' import { DrillingProgramService } from '@api'
import '@styles/drilling_program.less' import '@styles/pages/drilling_program.less'
const catSelectorRules = [{ const catSelectorRules = [{
required: true, required: true,

View File

@ -12,7 +12,7 @@ import { FileService } from '@api'
import MarksCard from './MarksCard' import MarksCard from './MarksCard'
import '@styles/drilling_program.less' import '@styles/pages/drilling_program.less'
const { RangePicker } = DatePicker const { RangePicker } = DatePicker
const { Search } = Input const { Search } = Input
@ -74,7 +74,7 @@ export const CategoryHistory = ({ idCategory, visible, onClose }) => {
const [begin, end] = range?.length > 1 ? [range[0].toISOString(), range[1].toISOString()] : [null, null] const [begin, end] = range?.length > 1 ? [range[0].toISOString(), range[1].toISOString()] : [null, null]
const skip = (page - 1) * pageSize const skip = (page - 1) * pageSize
const paginatedHistory = await FileService.getFilesInfo(well.caption, idCategory, companyName, fileName, begin, end, false, skip, pageSize) const paginatedHistory = await FileService.getFilesInfo(well.id, idCategory, companyName, fileName, begin, end, false, skip, pageSize)
setTotal(paginatedHistory?.count ?? 0) setTotal(paginatedHistory?.count ?? 0)
setData(arrayOrDefault(paginatedHistory?.items)) setData(arrayOrDefault(paginatedHistory?.items))
}, },

View File

@ -18,7 +18,7 @@ import { formatDate, MimeTypes } from '@utils'
import MarksCard from './MarksCard' import MarksCard from './MarksCard'
import '@styles/drilling_program.less' import '@styles/pages/drilling_program.less'
const CommentPrompt = memo(({ isRequired = true, ...props }) => ( const CommentPrompt = memo(({ isRequired = true, ...props }) => (
<Poprompt <Poprompt

View File

@ -21,7 +21,7 @@ import CategoryRender from './CategoryRender'
import CategoryEditor from './CategoryEditor' import CategoryEditor from './CategoryEditor'
import CategoryHistory from './CategoryHistory' import CategoryHistory from './CategoryHistory'
import '@styles/drilling_program.less' import '@styles/pages/drilling_program.less'
const ID_STATE = { const ID_STATE = {
NotInitialized: 0, NotInitialized: 0,

View File

@ -18,7 +18,7 @@ import { MeasureService } from '@api'
import { View } from './View' import { View } from './View'
import '@styles/index.css' import '@styles/index.css'
import '@styles/measure.css' import '@styles/pages/measure.css'
const createEditingColumns = (cols, renderDelegate) => const createEditingColumns = (cols, renderDelegate) =>
cols.map(col => col.map(col => ({ render: renderDelegate, ...col }))) cols.map(col => col.map(col => ({ render: renderDelegate, ...col })))

View File

@ -4,13 +4,13 @@ import { Empty, Form } from 'antd'
import { Grid, GridItem } from '@components/Grid' import { Grid, GridItem } from '@components/Grid'
import '@styles/index.css' import '@styles/index.css'
import '@styles/measure.css' import '@styles/pages/measure.css'
export const View = memo(({ columns, item }) => !item || !columns?.length ? ( export const View = memo(({ columns, item }) => !item || !columns?.length ? (
<Empty key={'empty'} image={Empty.PRESENTED_IMAGE_SIMPLE} /> <Empty key={'empty'} image={Empty.PRESENTED_IMAGE_SIMPLE} />
) : ( ) : (
<Grid> <Grid>
{columns.map((cols, i) => { {columns.flatMap((cols, i) => {
const columnPosition = 1 + i * 2 const columnPosition = 1 + i * 2
return cols.map((column, j) => ( return cols.map((column, j) => (
<Fragment key={column.key}> <Fragment key={column.key}>
@ -46,6 +46,6 @@ export const View = memo(({ columns, item }) => !item || !columns?.length ? (
</GridItem> </GridItem>
</Fragment> </Fragment>
)) ))
}).flat()} })}
</Grid> </Grid>
)) ))

View File

@ -2,7 +2,7 @@ import { Input } from 'antd'
import { RegExpIsFloat } from '@components/Table' import { RegExpIsFloat } from '@components/Table'
import '@styles/measure.css' import '@styles/pages/measure.css'
export const v = (text) => ( export const v = (text) => (
<div className={'v-div'}> <div className={'v-div'}>

View File

@ -1,270 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useState, useEffect, memo, useCallback, useMemo } from 'react'
import { useSearchParams } from 'react-router-dom'
import { Select } from 'antd'
import { useWell } from '@asb/context'
import { Flex } from '@components/Grid'
import { D3MonitoringCharts } from '@components/d3/monitoring'
import { CopyUrlButton } from '@components/CopyUrl'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { DatePickerWrapper, makeDateSorter } from '@components/Table'
import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
import { formatDate, range, withPermissions } from '@utils'
import { TelemetryDataSaubService } from '@api'
import { normalizeData, yAxis } from '../TelemetryView'
import { makeChartGroups } from '../TelemetryView/datasets'
import { cursorRender } from '../TelemetryView/cursorRender'
const DATA_COUNT = 2048 // Колличество точек на подгрузку графика
const ADDITIVE_PAGES = 2 // Дополнительные данные для графиков
const LOADING_TRIGGER = 0.5
const scrollOptions = [
{ label: '10%', value: 0.1 },
{ label: '15%', value: 0.15 },
{ label: '25%', value: 0.25 },
]
const getLoadingInterval = (loaded, startDate, interval) => {
// Если данные загружены и дата не заходит за тригер дозагрузка не требуется
if (
loaded &&
+startDate - interval * LOADING_TRIGGER > loaded.start &&
+startDate + interval * (LOADING_TRIGGER + 1) < loaded.end
)
return { loadingStartDate: startDate, newLoaded: loaded, loadingInterval: 0 }
let loadingStartDate = +startDate - interval * ADDITIVE_PAGES
let loadingEndDate = +startDate + interval * (ADDITIVE_PAGES + 1)
const newLoaded = {
start: loadingStartDate,
end: loadingEndDate
}
if (loaded) {
if (loadingStartDate >= loaded.start)
loadingStartDate = loaded.end
if (loadingEndDate <= loaded.end)
loadingEndDate = loaded.start
newLoaded.start = Math.min(loaded.start, loadingStartDate)
newLoaded.end = Math.max(loaded.end, loadingEndDate)
}
const loadingInterval = Math.trunc((loadingEndDate - loadingStartDate) / 1000)
return {
loadingStartDate: new Date(loadingStartDate),
newLoaded: {
start: new Date(newLoaded.start),
end: new Date(newLoaded.end)
},
loadingInterval
}
}
const interpolationSearch = (data, begin, end, accessor) => {
const fy = (i) => new Date(data[i]?.[accessor] ?? 0)
const fx = (y, b, e) => Math.round(b + (y - fy(b)) * (e - b) / (fy(e) - fy(b)))
const findIdx = (val, startIdx, c) => {
let x = startIdx
let endIdx = data.length - 1
if(val < fy(startIdx))
return startIdx
if(val > fy(endIdx))
return endIdx
for(let i = 0; i < c; i++){
x = fx(val, startIdx, endIdx)
if(fy(x) < val)
startIdx = x
else
endIdx = x
if ((startIdx === endIdx)||(fy(startIdx) === fy(endIdx)))
return x
}
return x
}
let x0 = findIdx(begin, 0, 100)
let x1 = findIdx(end, x0, 100)
return { start: x0, end: x1, count: x1 - x0 }
}
export const cutData = (data, beginDate, endDate) => {
if (data?.length > 0) {
let { start, end } = interpolationSearch(data, beginDate, endDate, 'date')
if (start > 0) start--
if (end + 1 < end.length) end++
return data.slice(start, end)
}
return data
}
const chartGroups = makeChartGroups([])
const Archive = memo(() => {
const [dataSaub, setDataSaub] = useState([])
const [dateLimit, setDateLimit] = useState({ from: 0, to: new Date() })
const [showLoader, setShowLoader] = useState(false)
const [loaded, setLoaded] = useState(null)
const [well] = useWell()
const [search, setSearchParams] = useSearchParams()
const getInitialRange = useCallback(() => parseInt(search.get('range') ?? defaultPeriod) * 1000, [search])
const [scrollPercent, setScrollPercent] = useState(0.15)
const [chartInterval, setChartInterval] = useState(getInitialRange)
const getInitialDate = useCallback(() => new Date(search.get('start') ?? (Date.now() - chartInterval)), [search, chartInterval])
const [startDate, setStartDate] = useState(getInitialDate)
const onGraphWheel = useCallback((e) => {
if (loaded && dateLimit.from && dateLimit.to) {
setStartDate((prevStartDate) => {
const offset = e.deltaY / 100 * chartInterval * scrollPercent
const nextStartDate = +prevStartDate + offset
const firstPossibleDate = Math.max(loaded.start, dateLimit.from)
const lastPossibleDate = Math.min(dateLimit.to, (loaded.end ?? Date.now())) - chartInterval
return new Date(Math.max(firstPossibleDate, Math.min(nextStartDate, lastPossibleDate)))
})
}
}, [loaded, dateLimit, chartInterval, scrollPercent])
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])
useEffect(() => {
const params = {}
if (startDate)
params.start = startDate.toISOString()
if (chartInterval)
params.range = chartInterval / 1000
setSearchParams(params)
}, [startDate, chartInterval])
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
let dates = await TelemetryDataSaubService.getDataDatesRange(well.id)
dates = {
from: new Date(dates?.from ?? 0),
to: new Date(dates?.to ?? 0)
}
setDateLimit(dates)
},
setShowLoader,
`Не удалось загрузить диапозон телеметрии`,
{ actionName: 'Загрузка диапозона телеметрии', well }
)
}, [well])
useEffect(() => {
setStartDate((prev) => new Date(Math.max(dateLimit.from, Math.min(+prev, +dateLimit.to - chartInterval))))
}, [chartInterval, dateLimit])
useEffect(() => {
if (showLoader) return
const { loadingStartDate, loadingInterval, newLoaded } = getLoadingInterval(loaded, startDate, chartInterval)
if (loadingInterval <= 0) return
invokeWebApiWrapperAsync(
async () => {
const data = await TelemetryDataSaubService.getData(well.id, loadingStartDate.toISOString(), loadingInterval, DATA_COUNT)
const loadedStartDate = new Date(Math.max(+newLoaded.start, +startDate - chartInterval * ADDITIVE_PAGES))
const loadedEndDate = new Date(Math.min(+newLoaded.end, +startDate + chartInterval * (ADDITIVE_PAGES + 1)))
setLoaded({ start: loadedStartDate, end: loadedEndDate })
if (data) {
data.forEach(elm => elm.date = new Date(elm.date))
setDataSaub((prevDataSaub) => {
const newData = [...prevDataSaub, ...normalizeData(data)]
newData.sort(makeDateSorter('date'))
return cutData(newData, loadedStartDate, loadedEndDate)
})
}
},
setShowLoader,
`Не удалось загрузить данные c ${formatDate(startDate)} по ${formatDate(+startDate + chartInterval)}`,
{ actionName: 'Загрузка телеметрий в диапозоне', well }
)
}, [well, chartInterval, loaded, startDate])
const onRangeChange = useCallback((value) => {
setChartInterval(value * 1000)
setDataSaub([])
setLoaded(null)
}, [])
const domain = useMemo(() => ({ min: startDate, max: new Date(+startDate + chartInterval)}), [startDate, chartInterval])
const chartData = useMemo(() => cutData(dataSaub, domain.min, domain.max), [dataSaub, domain])
return (
<LoaderPortal show={showLoader} style={{ flexGrow: 1 }}>
<Flex style={{margin: '8px 8px 0'}}>
<div>
Начальная дата:&nbsp;
<DatePickerWrapper
value={startDate}
disabledDate={isDateDisabled}
disabledTime={isDateTimeDisabled}
onChange={(startDate) => setStartDate(new Date(startDate))}
/>
</div>
<div style={{ marginLeft: '1rem' }}>
Период:&nbsp;
<PeriodPicker value={chartInterval / 1000} onChange={onRangeChange} />
</div>
<div style={{ marginLeft: '1rem' }}>
Прокрутка:&nbsp;
<Select options={scrollOptions} value={scrollPercent} onChange={setScrollPercent} />
</div>
<CopyUrlButton style={{ marginLeft: '1rem' }} />
</Flex>
<D3MonitoringCharts
datasetGroups={chartGroups}
data={chartData}
yDomain={domain}
yAxis={yAxis}
yTicks={{
visible: true,
format: (d) => formatDate(d)
}}
plugins={{
menu: { enabled: false },
cursor: {
render: cursorRender,
}
}}
style={{ flexGrow: 1 }}
height={'76vh'}
onWheel={onGraphWheel}
/>
</LoaderPortal>
)
})
export default withPermissions(Archive, ['TelemetryDataSaub.get'])

View File

@ -22,7 +22,7 @@ import {
import AddGroupWindow from './AddGroupWindow' import AddGroupWindow from './AddGroupWindow'
import AddWidgetWindow, { makeWidgetFromWits } from './AddWidgetWindow' import AddWidgetWindow, { makeWidgetFromWits } from './AddWidgetWindow'
import '@styles/dashboard_nnb.less' import '@styles/pages/dashboard_nnb.less'
const getWitsInfo = async () => { const getWitsInfo = async () => {
// TODO: Добавить expire с принудительным обновлением // TODO: Добавить expire с принудительным обновлением

View File

@ -11,8 +11,8 @@ import { makeColumn, makeDateColumn, makeNumericColumn, makeNumericSorter, makeT
import { withPermissions } from '@utils' import { withPermissions } from '@utils'
import { MessageService } from '@api' import { MessageService } from '@api'
import '@styles/filter.less' import '@styles/components/filter.less'
import '@styles/message.less' import '@styles/pages/message.less'
const pageSize = 26 const pageSize = 26
const { Search } = Input const { Search } = Input
@ -29,18 +29,18 @@ const categoryDictionary = {
// Конфигурация таблицы // Конфигурация таблицы
export const makeMessageColumns = (idWell) => [ export const makeMessageColumns = (idWell) => [
makeDateColumn('Дата', 'date', undefined, undefined, { width: '120px' }), makeDateColumn('Дата', 'date', undefined, undefined, { width: '120px' }),
makeNumericColumn('Глубина, м', 'wellDepth', null, null, (depth, item) => ( makeNumericColumn('Глубина, м', 'wellDepth', (depth, item) => (
<Tooltip title={'Нажмите для перехода в архив'}> <Tooltip title={'Нажмите для перехода в архив'}>
<Link <Link
style={{ color: 'inherit'}} style={{ color: 'inherit'}}
to={`/well/${idWell}/telemetry/archive?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;
{depth.toFixed(2)} {depth.toFixed(2)}
</Link> </Link>
</Tooltip> </Tooltip>
), '7rem'), ), undefined, '7rem'),
makeColumn('Категория', 'categoryId', { makeColumn('Категория', 'categoryId', {
width: '8rem', width: '8rem',
render: (_, item) => categoryDictionary[item.categoryId].title, render: (_, item) => categoryDictionary[item.categoryId].title,

View File

@ -10,8 +10,8 @@ import { DateRangeWrapper, makeColumn, makeNumericColumn, makeNumericRender, mak
import { arrayOrDefault, range, withPermissions } from '@utils' import { arrayOrDefault, range, withPermissions } from '@utils'
import { SubsystemOperationTimeService } from '@api' import { SubsystemOperationTimeService } from '@api'
import '@styles/filter.less' import '@styles/components/filter.less'
import '@styles/operation_time.less' import '@styles/pages/operation_time.less'
const subsystemColors = [ const subsystemColors = [
'#1abc9c', '#16a085', '#2ecc71', '#27ae60', '#1abc9c', '#16a085', '#2ecc71', '#27ae60',
@ -26,10 +26,10 @@ const tableColumns = [
<div className={'table_color'} style={{ backgroundColor }} /> <div className={'table_color'} style={{ backgroundColor }} />
)}), )}),
makeTextColumn('Подсистема', 'subsystemName'), makeTextColumn('Подсистема', 'subsystemName'),
makeNumericColumn('Использование, %', 'kUsage', undefined, undefined, val => (+val * 100).toFixed(2), 200), makeNumericColumn('Использование, %', 'kUsage', val => (+val * 100).toFixed(2), undefined, 200),
makeNumericColumn('Проходка, м', 'sumDepthInterval', undefined, undefined, undefined, 200), makeNumericColumn('Проходка, м', 'sumDepthInterval', undefined, undefined, 200),
makeNumericColumn('Время работы, ч', 'usedTimeHours', undefined, undefined, undefined, 200), makeNumericColumn('Время работы, ч', 'usedTimeHours', undefined, undefined, 200),
makeNumericColumn('Кол-во запусков', 'operationCount', undefined, undefined, makeNumericRender(0), 200), makeNumericColumn('Кол-во запусков', 'operationCount', makeNumericRender(0), undefined, 200),
] ]
// Выбор доступен только до текущей даты // Выбор доступен только до текущей даты

View File

@ -5,14 +5,13 @@ import { EditableTable, makeTextColumn } from '@components/Table'
import { DrillerService } from '@api' import { DrillerService } from '@api'
const columnOptions = { const columnOptions = {
editable: true,
formItemRules: [{ message: 'Обязательное поле!', required: true }] formItemRules: [{ message: 'Обязательное поле!', required: true }]
} }
const columns = [ const columns = [
makeTextColumn('Фамилия', 'surname', undefined, undefined, undefined, columnOptions), makeTextColumn('Фамилия', 'surname', undefined, undefined, undefined, columnOptions),
makeTextColumn('Имя', 'name', undefined, undefined, undefined, columnOptions), makeTextColumn('Имя', 'name', undefined, undefined, undefined, columnOptions),
makeTextColumn('Отчество', 'patronymic', undefined, undefined, undefined, { editable: true }), makeTextColumn('Отчество', 'patronymic'),
] ]
const rowClassName = (record) => record.has ? 'driller_list_active' : '' const rowClassName = (record) => record.has ? 'driller_list_active' : ''

View File

@ -14,7 +14,6 @@ import { arrayOrDefault } from '@utils'
import { ScheduleService } from '@api' import { ScheduleService } from '@api'
const columnOptions = { const columnOptions = {
editable: true,
formItemRules: [{ message: 'Обязательное поле!', required: true }] formItemRules: [{ message: 'Обязательное поле!', required: true }]
} }

View File

@ -4,7 +4,7 @@ import { D3Chart } from '@components/d3'
import { Grid, GridItem } from '@components/Grid' import { Grid, GridItem } from '@components/Grid'
import { formatDate, makeDisplayValue } from '@utils' import { formatDate, makeDisplayValue } from '@utils'
import '@styles/detected_operations.less' import '@styles/pages/detected_operations.less'
const displayNumber = makeDisplayValue({ fixed: 2 }) const displayNumber = makeDisplayValue({ fixed: 2 })

View File

@ -2,7 +2,7 @@ import { memo } from 'react'
import { Table, makeTextColumn, makeNumericColumn, makeNumericRender } from '@components/Table' import { Table, makeTextColumn, makeNumericColumn, makeNumericRender } from '@components/Table'
import '@styles/detected_operations.less' import '@styles/pages/detected_operations.less'
const numericRender = makeNumericRender(2) const numericRender = makeNumericRender(2)
@ -24,11 +24,11 @@ const makeDrillerSorter = (key) => (a, b) => {
export const columns = [ export const columns = [
makeTextColumn('Бурильщик', 'driller', null, makeDrillerSorter('driller'), drillerRender, { width: 200 }), makeTextColumn('Бурильщик', 'driller', null, makeDrillerSorter('driller'), drillerRender, { width: 200 }),
makeNumericColumn('Кол-во операций', 'count', null, null, (value) => parseInt(value), 150), makeNumericColumn('Кол-во операций', 'count', (value) => parseInt(value), undefined, 150),
makeNumericColumn('Среднее по ключевому показателю', 'averageValue', null, null, numericRender, 150), makeNumericColumn('Среднее по ключевому показателю', 'averageValue', numericRender, undefined, 150),
makeNumericColumn('Среднее целевого показателя', 'averageTargetValue', null, null, numericRender, 150), makeNumericColumn('Среднее целевого показателя', 'averageTargetValue', numericRender, undefined, 150),
makeNumericColumn('Эффективность (%)', 'efficiency', null, null, numericRender, 150), makeNumericColumn('Эффективность (%)', 'efficiency', numericRender, undefined, 150),
makeNumericColumn('Коэффициент потерь', 'loss', null, null, numericRender, 100), makeNumericColumn('Коэффициент потерь', 'loss', numericRender, undefined, 100),
] ]
export const OperationsTable = memo(({ data, height, ...other }) => ( export const OperationsTable = memo(({ data, height, ...other }) => (

View File

@ -4,18 +4,17 @@ 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 = {
editable: true,
formItemRules: [{ message: 'Обязательное поле!', required: true }] formItemRules: [{ message: 'Обязательное поле!', required: true }]
} }
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)
@ -63,20 +62,17 @@ 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,
filterOption: (input, option) => filterOption: (input, option) =>
String(option?.label ?? '').toLowerCase().indexOf(input.toLowerCase()) >= 0 String(option?.label ?? '').toLowerCase().indexOf(input.toLowerCase()) >= 0
}), }),
makeNumericColumn('Цель', 'targetValue', undefined, undefined, numericRender, 150, columnOptions), makeNumericColumn('Цель', 'targetValue', numericRender, undefined, 150, columnOptions),
makeNumericColumn('Норм.', 'standardValue', undefined, undefined, numericRender, 150, columnOptions), makeNumericColumn('Норм.', 'standardValue', numericRender, undefined, 150, columnOptions),
makeGroupColumn('Глубина, м', [ makeGroupColumn('Глубина, м', [
makeNumericColumn('Начало', 'depthStart', undefined, undefined, numericRender, 150, columnOptions), makeNumericColumn('Начало', 'depthStart', numericRender, undefined, 150, columnOptions),
makeNumericColumn('Окончание', 'depthEnd', undefined, undefined, numericRender, 150, columnOptions), makeNumericColumn('Окончание', 'depthEnd', numericRender, undefined, 150, columnOptions),
]), ]),
]) ])
}, },
@ -84,7 +80,7 @@ export const TargetEditor = memo(({ loading, onChange }) => {
`Не удалось получить список категорий целей`, `Не удалось получить список категорий целей`,
{ actionName: 'Получение списка категорий целей', well } { actionName: 'Получение списка категорий целей', well }
) )
}, [well]) }, [options])
useEffect(() => { useEffect(() => {
updateTable() updateTable()

View File

@ -7,8 +7,8 @@ import LoaderPortal from '@components/LoaderPortal'
import { DateRangeWrapper } from '@components/Table' 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, pretify } 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'
@ -16,7 +16,7 @@ import DrillerSchedule from './DrillerSchedule'
import OperationsChart from './OperationsChart' import OperationsChart from './OperationsChart'
import OperationsTable from './OperationsTable' import OperationsTable from './OperationsTable'
import '@styles/detected_operations.less' import '@styles/pages/detected_operations.less'
const Operations = memo(() => { const Operations = memo(() => {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
@ -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()
@ -67,7 +67,7 @@ const Operations = memo(() => {
const maxTarget = Math.max(...data.operations?.map((op) => op.operationValue?.targetValue || 0)) const maxTarget = Math.max(...data.operations?.map((op) => op.operationValue?.targetValue || 0))
const uniqueOps = data.operations?.map((op) => op.value || 0).filter(unique) const uniqueOps = data.operations?.map((op) => op.value || 0).filter(unique)
const value = uniqueOps.reduce((out, op) => out + op, 0) / uniqueOps.length * 3 / 2 const value = uniqueOps.reduce((out, op) => out + op, 0) / uniqueOps.length * 3 / 2
setYDomain(pretify(Math.max(maxTarget, value))) setYDomain(prettify(Math.max(maxTarget, value)))
}, [data]) }, [data])
useEffect(() => { useEffect(() => {
@ -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

@ -9,7 +9,7 @@ import { MessageService } from '@api'
import { makeMessageColumns } from '../Messages' import { makeMessageColumns } from '../Messages'
import '@styles/message.less' import '@styles/pages/message.less'
export const ActiveMessagesOnline = memo(({ well: givenWell }) => { export const ActiveMessagesOnline = memo(({ well: givenWell }) => {
const [messages, setMessages] = useState([]) const [messages, setMessages] = useState([])

View File

@ -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 (
<Tooltip title={`Время последних данных: ${date.format('DD.MM.YYYY HH:mm:ss')}`}>
{date.isSame(new Date(), 'day') || (
<Typography.Text disabled style={{ fontSize: '12px', marginRight: '5px' }}>{date.format('DD.MM.YYYY')}</Typography.Text>
)}
{date.format('HH:mm:ss')}
</Tooltip>
)
}
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 => (
<Display
className={'border_small display_flex_container'}
key={param.label}
label={param.label}
value={param.value}
suffix={param.units}
format={param.format}
isArrowVisible={param.isArrowVisible}
/>
))}
<RigMnemo
wellDepth={dataLast?.wellDepth ?? Number.NaN}
bitPosition={dataLast?.bitDepth ?? Number.NaN}
blockPosition={dataLast?.blockPosition ?? Number.NaN}
/>
</>
)
})
export default CustomColumn

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

@ -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 }) => (
<Display
className={'border_small display_flex_container user_card'}
label={'Режим:'}
value={data?.[data?.length - 1]?.mode}
enumeration={modeNames}
/>
))

Some files were not shown because too many files have changed in this diff Show More