forked from ddrilling/asb_cloud_front
Compare commits
78 Commits
feature/st
...
dev
Author | SHA1 | Date | |
---|---|---|---|
|
04bd79d563 | ||
|
4b0adeeb7b | ||
|
3b0182216e | ||
|
269b59560a | ||
|
c867c51bf5 | ||
|
5d64102a7c | ||
|
d0c2774774 | ||
|
de58c81e87 | ||
|
860d74f2db | ||
ed43ebb082 | |||
44104f672b | |||
fce6c1909e | |||
c0b5f82ad2 | |||
8d6f5ac1a5 | |||
bef281feb8 | |||
a79cac9d51 | |||
878ab921c1 | |||
8154f2f0ba | |||
94707b3c9a | |||
8825c4c26c | |||
0e25e10785 | |||
b063b5dcd9 | |||
969edda933 | |||
3396050fae | |||
d36cd1acbd | |||
77d65be601 | |||
16fec4e42f | |||
923d469a86 | |||
8f52066bce | |||
bfd1e51cfa | |||
0e63d93fa7 | |||
0ef6d67772 | |||
f4adb528ca | |||
7af33d702d | |||
d235b01c80 | |||
5af996f9e5 | |||
65b9ede580 | |||
08fcf2736e | |||
28a0962793 | |||
8f98cc066c | |||
4b20a44d88 | |||
043f73fde3 | |||
1a737b6afe | |||
17d7b7c41d | |||
4dd57aff98 | |||
ebe3a50fbe | |||
dc0f80fee5 | |||
cb8be79274 | |||
ee289cc619 | |||
e430cdd5b4 | |||
540da341da | |||
20b271d91e | |||
259e2e4be8 | |||
0cbd9559f2 | |||
40d3a77c1c | |||
7731dfe8e7 | |||
7acb7ce2b2 | |||
0aeef42811 | |||
5eb66e5fbc | |||
44afd5f1f0 | |||
a2d641abdd | |||
51ac260c74 | |||
16fb37910f | |||
de7e8fd259 | |||
a9aeeb6da3 | |||
fe52116a9b | |||
d56810700f | |||
e39ce8d410 | |||
bd81aa0401 | |||
59b1d49286 | |||
871e71e777 | |||
fc91cfc6ff | |||
3216a90af3 | |||
11a632c246 | |||
b2c34d07a9 | |||
ec2513b4a0 | |||
6375db5a0b | |||
17ccecb2dd |
15
.vscode/settings.json
vendored
15
.vscode/settings.json
vendored
@ -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
191
CODE_STANDART.md
Normal 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` - для компонентов, применяющихся как виджеты в дашбордах.
|
68
README.md
68
README.md
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
Установка выполняется одной командой:
|
Установка выполняется одной командой:
|
||||||
```bash
|
```bash
|
||||||
npm i
|
npm ci
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2. Автогенерация сервисов
|
## 2. Автогенерация сервисов
|
||||||
@ -19,12 +19,12 @@ npm i
|
|||||||
|
|
||||||
Если сервер запущен на текущей машине достаточно написать:
|
Если сервер запущен на текущей машине достаточно написать:
|
||||||
```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
22
package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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'))
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -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>
|
|
||||||
))
|
|
@ -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(
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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)
|
||||||
|
@ -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 = []
|
@ -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'}>
|
||||||
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
@ -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 ?? '--'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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} />,
|
||||||
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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'}>
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,11 +9,21 @@ 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 [
|
||||||
@ -22,7 +32,7 @@ const normalizeDates = (value?: RangeValue<Moment>, isUTC?: boolean): RangeValue
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
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}
|
||||||
@ -31,7 +41,7 @@ export const DateRangeWrapper = memo<DateRangeWrapperProps>(({ value, isUTC, all
|
|||||||
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}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
@ -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,15 +48,24 @@ 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 Исходные столбцы
|
||||||
|
* @returns Обёрнутые столбцы
|
||||||
|
*/
|
||||||
|
const applyColumnWrappers = <T extends DataSet<any>>(columns: TableColumns<T>): TableColumns<T> => columns.map((column) => {
|
||||||
if ('children' in column) {
|
if ('children' in column) {
|
||||||
return {
|
return {
|
||||||
...column,
|
...column,
|
||||||
@ -55,17 +76,16 @@ const applyColumnWrappers = <T extends DataSet<any>>(columns: BaseTableColumn<T>
|
|||||||
...column,
|
...column,
|
||||||
render: makeColumnRenderWrapper<T>(column.key, column.render),
|
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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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])
|
||||||
|
@ -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 */
|
||||||
|
@ -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])
|
||||||
|
@ -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) => {
|
||||||
@ -463,7 +466,7 @@ 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()
|
||||||
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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 = {
|
||||||
/** Параметры стиля линии */
|
/** Параметры стиля линии */
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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,16 +15,18 @@ 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)
|
||||||
|
|
||||||
@ -31,8 +34,8 @@ export const renderRectArea = <DataType extends BaseDataType>(
|
|||||||
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)))
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
@ -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]
|
||||||
|
@ -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' }}>
|
||||||
|
@ -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>
|
||||||
|
@ -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> )
|
||||||
|
|
||||||
|
@ -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' }}
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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
23
src/context/deposit.ts
Normal 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
5
src/context/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from './deposit'
|
||||||
|
export * from './layout_props'
|
||||||
|
export * from './root_path'
|
||||||
|
export * from './user'
|
||||||
|
export * from './well'
|
31
src/context/layout_props.ts
Normal file
31
src/context/layout_props.ts
Normal 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
11
src/context/root_path.ts
Normal 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
13
src/context/user.ts
Normal 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
13
src/context/well.ts
Normal 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
40
src/images/AsbLogo.svg
Normal 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
33
src/images/Logo.svg
Normal 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 |
@ -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
|
||||||
|
@ -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'}
|
||||||
|
@ -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])
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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 }),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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])
|
||||||
|
@ -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'),
|
||||||
|
@ -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'),
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
|
@ -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} />,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
@ -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 AdminPanel = memo(() => {
|
const layoutProps = {
|
||||||
const location = useLocation()
|
|
||||||
const root = useRootPath()
|
|
||||||
const rootPath = useMemo(() => `${root}/admin`, [root])
|
|
||||||
|
|
||||||
const setLayoutProps = useLayoutProps()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLayoutProps({
|
|
||||||
sider: <AdminNavigationMenu />,
|
sider: <AdminNavigationMenu />,
|
||||||
title: 'Администраторская панель',
|
title: 'Администраторская панель',
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
breadcrumb: makeMenuBreadcrumbItems(menuItems, location.pathname, /^\/admin\//),
|
breadcrumb: makeMenuBreadcrumbItemsRender(menuItems, /^\/admin\//),
|
||||||
})
|
}
|
||||||
}, [location.pathname])
|
|
||||||
|
const AdminPanel = memo(() => {
|
||||||
|
const root = useRootPath()
|
||||||
|
const rootPath = useMemo(() => `${root}/admin`, [root])
|
||||||
|
|
||||||
|
useLayoutProps(layoutProps)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RootPathContext.Provider value={rootPath}>
|
<RootPathContext.Provider value={rootPath}>
|
||||||
|
@ -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)
|
||||||
|
@ -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 }) => {
|
||||||
|
@ -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'])
|
|
25
src/pages/Deposit/DepositNavigationMenu.jsx
Normal file
25
src/pages/Deposit/DepositNavigationMenu.jsx
Normal 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
93
src/pages/Deposit/Map.jsx
Normal 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'])
|
127
src/pages/Deposit/StatisticsADW.jsx
Normal file
127
src/pages/Deposit/StatisticsADW.jsx
Normal 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, [])
|
75
src/pages/Deposit/index.jsx
Normal file
75
src/pages/Deposit/index.jsx
Normal 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, [])
|
@ -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) => {
|
||||||
|
@ -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
|
|
@ -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'}
|
||||||
|
@ -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'))
|
||||||
|
|
||||||
|
@ -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 ?? [])
|
||||||
|
@ -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,
|
||||||
|
@ -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))
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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 })))
|
||||||
|
@ -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>
|
||||||
))
|
))
|
||||||
|
@ -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'}>
|
||||||
|
@ -1,164 +0,0 @@
|
|||||||
import { Descriptions, Progress } from 'antd'
|
|
||||||
import { memo, useMemo, useState } from 'react'
|
|
||||||
|
|
||||||
import { makeColumn, makeNumericColumn, makeNumericRender, Table } from '@components/Table'
|
|
||||||
import { withPermissions } from '@utils'
|
|
||||||
|
|
||||||
import RigMnemo from './RigMnemo'
|
|
||||||
|
|
||||||
import '@styles/start_screen.less'
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
makeColumn('', 'caption'),
|
|
||||||
makeNumericColumn('Ходы насоса', 'count', undefined, undefined, makeNumericRender(0)),
|
|
||||||
makeNumericColumn('Расход на входе, л/с', 'input_flow'),
|
|
||||||
makeNumericColumn('Давление на входе, атм', 'input_pressure'),
|
|
||||||
]
|
|
||||||
|
|
||||||
const data = [
|
|
||||||
{ caption: 'Насос №1', count: 55, input_flow: 16, input_pressure: 32 },
|
|
||||||
{ caption: 'Насос №2', count: 55, input_flow: 16, input_pressure: 34 },
|
|
||||||
]
|
|
||||||
|
|
||||||
const formatMSE = (percent) => (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
||||||
<span>MSE</span>
|
|
||||||
<span>{percent}%</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const formatRPM = (percent) => (
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
||||||
<span>{percent}</span>
|
|
||||||
<span>об/мин</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const StatusDashboardItem = memo(({ status, title }) => {
|
|
||||||
const color = useMemo(() => {
|
|
||||||
switch (status) {
|
|
||||||
case 'off':
|
|
||||||
case 'error':
|
|
||||||
return '#ff4d4f'
|
|
||||||
case 'warn':
|
|
||||||
case 'warning':
|
|
||||||
return '#faad14'
|
|
||||||
case 'ok':
|
|
||||||
case 'success':
|
|
||||||
return '#52c41a'
|
|
||||||
default: return '#1890ff'
|
|
||||||
}
|
|
||||||
}, [status])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Progress
|
|
||||||
type={'dashboard'}
|
|
||||||
format={() => title}
|
|
||||||
trailColor={color}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const StartScreen = memo(() => {
|
|
||||||
const [dataSaub, setDataSaub] = useState([])
|
|
||||||
|
|
||||||
const dataLast = useMemo(() => dataSaub.at(-1), [dataSaub])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={'start-screen'}>
|
|
||||||
<div className={'left-panel'}>
|
|
||||||
<div className={'card customer'}>
|
|
||||||
<div className={'header'}>Заказчик</div>
|
|
||||||
<div className={'content'}>ООО "Газпромнефть-Хантос"</div>
|
|
||||||
</div>
|
|
||||||
<div className={'card current-process'}>
|
|
||||||
<div className={'header'}>Текущий процесс</div>
|
|
||||||
<div className={'content'}>Бурение в слайде с АПД</div>
|
|
||||||
</div>
|
|
||||||
<div className={'card mnemo'}>
|
|
||||||
<div className={'header'}>Конструкция скважины</div>
|
|
||||||
<div style={{ overflow: 'hidden', flex: '1 0 fit-content' }}>
|
|
||||||
<RigMnemo
|
|
||||||
width={'auto'}
|
|
||||||
height={'100%'}
|
|
||||||
wellDepth={dataLast?.wellDepth ?? Number.NaN}
|
|
||||||
bitPosition={dataLast?.bitDepth ?? Number.NaN}
|
|
||||||
blockPosition={dataLast?.blockPosition ?? Number.NaN}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={'right-panel'}>
|
|
||||||
<div className={'top-panel'}>
|
|
||||||
<div className={'main-panel'}>
|
|
||||||
<div className={'card drilling-modes'}>
|
|
||||||
<div className={'header'}>Режимы бурения</div>
|
|
||||||
<div className={'content'}>
|
|
||||||
<Descriptions bordered size={'small'} column={2} style={{ flexGrow: 1 }}>
|
|
||||||
<Descriptions.Item label={'Wд, т'}>12.5</Descriptions.Item>
|
|
||||||
<Descriptions.Item label={'dP, атм'}>32</Descriptions.Item>
|
|
||||||
<Descriptions.Item label={'Nрот, об/мин'}>40</Descriptions.Item>
|
|
||||||
<Descriptions.Item label={'Вес на крюке, т'}>68</Descriptions.Item>
|
|
||||||
<Descriptions.Item label={'Таль. блок, м'}>13.4</Descriptions.Item>
|
|
||||||
<Descriptions.Item label={'Mрот, кН/м'}>40</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
<Progress type={'dashboard'} format={formatMSE} percent={53} status={'normal'} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={'card pumping-equipment'}>
|
|
||||||
<div className={'header'}>Работа насосного оборудования</div>
|
|
||||||
<div className={'content'}>
|
|
||||||
<Table
|
|
||||||
bordered
|
|
||||||
size={'small'}
|
|
||||||
className={'content'}
|
|
||||||
columns={columns}
|
|
||||||
dataSource={data}
|
|
||||||
pagination={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={'card saub-modules-work'}>
|
|
||||||
<div className={'header'}>Работа модулей САУБ</div>
|
|
||||||
<div className={'content'}>
|
|
||||||
<span>Spin Master:</span>
|
|
||||||
<span>кол-во обр. - 4.5обр; V 25 обр/мин</span>
|
|
||||||
<div style={{ display: 'flex', gap: '10px' }}>
|
|
||||||
<Progress type={'dashboard'} format={formatRPM} percent={25} />
|
|
||||||
<StatusDashboardItem title={'АПД'} status={'error'} />
|
|
||||||
<StatusDashboardItem title={'MCE'} status={'warning'} />
|
|
||||||
<StatusDashboardItem title={'Торк'} status={'success'} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={'card drilling-fluid'}>
|
|
||||||
<div className={'header'}>Буровой раствор</div>
|
|
||||||
<div className={'content'}>
|
|
||||||
<Descriptions className={'content'} bordered size={'small'} column={1}>
|
|
||||||
<Descriptions.Item label={'Vсумм, м3'}>68</Descriptions.Item>
|
|
||||||
<Descriptions.Item label={'V 1 емк, м3'}>68</Descriptions.Item>
|
|
||||||
<Descriptions.Item label={'V 2 емк, м3'}>68</Descriptions.Item>
|
|
||||||
<Descriptions.Item label={'V 3 емк, м3'}>68</Descriptions.Item>
|
|
||||||
<Descriptions.Item label={'V 4 емк, м3'}>68</Descriptions.Item>
|
|
||||||
<Descriptions.Item label={'V 5 емк, м3'}>68</Descriptions.Item>
|
|
||||||
<Descriptions.Item label={'V 6 емк, м3'}>68</Descriptions.Item>
|
|
||||||
<Descriptions.Item label={'Vцсго, м3'}>12</Descriptions.Item>
|
|
||||||
<Descriptions.Item label={'Темп вх'}>31</Descriptions.Item>
|
|
||||||
<Descriptions.Item label={'Темп вых'}>31</Descriptions.Item>
|
|
||||||
<Descriptions.Item label={'Плот вх'}>1.23</Descriptions.Item>
|
|
||||||
<Descriptions.Item label={'Темп вых'}>1.25</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={'card operation-journal'}>
|
|
||||||
<div className={'header'}>Журнал операций</div>
|
|
||||||
<div className={'content'}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default withPermissions(StartScreen, [])
|
|
@ -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>
|
|
||||||
Начальная дата:
|
|
||||||
<DatePickerWrapper
|
|
||||||
value={startDate}
|
|
||||||
disabledDate={isDateDisabled}
|
|
||||||
disabledTime={isDateTimeDisabled}
|
|
||||||
onChange={(startDate) => setStartDate(new Date(startDate))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ marginLeft: '1rem' }}>
|
|
||||||
Период:
|
|
||||||
<PeriodPicker value={chartInterval / 1000} onChange={onRangeChange} />
|
|
||||||
</div>
|
|
||||||
<div style={{ marginLeft: '1rem' }}>
|
|
||||||
Прокрутка:
|
|
||||||
<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'])
|
|
@ -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 с принудительным обновлением
|
||||||
|
@ -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 />
|
||||||
|
|
||||||
{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,
|
||||||
|
@ -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),
|
||||||
]
|
]
|
||||||
|
|
||||||
// Выбор доступен только до текущей даты
|
// Выбор доступен только до текущей даты
|
||||||
|
@ -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' : ''
|
||||||
|
@ -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 }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 })
|
||||||
|
|
||||||
|
@ -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 }) => (
|
||||||
|
@ -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()
|
||||||
|
@ -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}>
|
||||||
|
@ -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([])
|
||||||
|
@ -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
|
|
@ -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
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user