Compare commits

..

1 Commits

Author SHA1 Message Date
a491f2c191 Добавлен макет начального экрана 2022-11-16 11:08:03 +05:00
174 changed files with 2160 additions and 2951 deletions

15
.vscode/settings.json vendored
View File

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

View File

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

View File

@ -7,7 +7,7 @@
Установка выполняется одной командой: Установка выполняется одной командой:
```bash ```bash
npm ci npm i
``` ```
## 2. Автогенерация сервисов ## 2. Автогенерация сервисов
@ -17,14 +17,14 @@ npm ci
Автогенерацию можно запустить с помощью уже прописанных в [package.json](package.json) скриптов, либо вручную. Автогенерацию можно запустить с помощью уже прописанных в [package.json](package.json) скриптов, либо вручную.
Если сервер запущен на текущей машине достаточно написать: Если сервер запущен на текущей машине достаточно написать:
```bash ```bash
npm run oul npm run update_openapi
``` ```
Для получения сервисов с основного сервера: Для получения сервисов с основного сервера:
```bash ```bash
npm run oug_dev npm run update_openapi_server
``` ```
или же ручной вариант: или же ручной вариант:
@ -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 | oul | Локальный адрес вашей машины | | 127.0.0.1:5000 | Локальный адрес вашей машины (привязан к `update_openapi`) |
| 192.168.1.113:5000 | oud | Локальный адрес development-сервера | | 192.168.1.113:5000 | Локальный адрес development-сервера (привязан к `update_openapi_server`) |
| 46.146.207.184:80 | oug_dev | Внешний адрес development-сервера | | 46.146.209.148:89 | Внешний адрес development-сервера |
| cloud.digitaldrilling.ru | oug | Внешний адрес production-сервера | | cloud.digitaldrilling.ru | Внешний адрес production-сервера |
## 3. Компиляция production-версии приложения ## 3. Компиляция production-версии приложения
После выполнения вышеописанных пунктов приложение готово к компиляции. После выполнения вышеописанных пунктов приложение готово к компиляции.
@ -60,53 +60,3 @@ npm run build
```bash ```bash
npm start npm start
``` ```
## 5. Подготовка к работе с гит репозиторием
### 5.1. Генерация SSH-ключей
Для генерации ключей, в **Git Bash**, либо в **bash** консоли необходимо ввести команду:
```bash
ssh-keygen
```
Предложенный путь сохранения ключа оставить без изменений
Пароль для ускорения работы можно не задавать
После чего публичный ключ необходимо занести ключ в [Gitea](http://46.146.207.184:8080/), в настройках пользователя.
Чтобы получить публичный ключ необходимо ввести в консоли команду:
```bash
cat ~/.ssh/id_rsa.pub
```
Далее ключ небходимо проверить, для этого необходимо нажать соответствующую кнопку в Gitea, скопировать и выполнить предложенную команду в консоли, после чего вывод вставить в поле на странице.
### 5.2. Генерация GPG-ключей
Для генерации ключей, в **Git Bash**, либо в **bash** консоли необходимо ввести команду:
```bash
gpg --full-generate-key
```
Тип ключа выбираем *RSA and RSA* (по умолчанию 1). Длину ключа рекомендуется задавать 4096. Далее необходимо заполнить все опрошенные данные, пароль оставить пустым.
После чего публичный ключ необходимо занести ключ в [Gitea](http://46.146.207.184:8080/), в настройках пользователя.
Чтобы получить публичный ключ необходимо ввести в консоли команду:
```bash
gpg --export --armor <email>
```
Где вместо `<email>` необходимо подставить электронную почту, указанную к ключу.
Далее ключ небходимо проверить, для этого необходимо нажать соответствующую кнопку в Gitea, скопировать и выполнить предложенную команду в консоли, после чего вывод вставить в поле на странице.
### 5.3. Настройка подписания коммитов (требуется GPG-ключ)
Перед началом необходимо получить ID GPG-ключа, для этого выполним команду:
```bash
gpg --list-keys <email>
```
Где вместо `<email>` необходимо подставить электронную почту, указанную к ключу. Из полученного вывода нам нужна только строка под строкой `rsa4096`. Эту строку мы передадим в следующую команду на место `<key-id>`:
```bash
git config --user.signingkey <key-id>
```

22
package-lock.json generated
View File

@ -16,7 +16,8 @@
"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",
@ -14687,6 +14688,19 @@
"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",
@ -26397,6 +26411,12 @@
"requires-port": "^1.0.0" "requires-port": "^1.0.0"
} }
}, },
"usehooks-ts": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.7.2.tgz",
"integrity": "sha512-DeLqSnGg9VvpwPZA+6lKVURJKM9EBu7bbIXuYclQ9COO3w4lacnJa0uP0iJbC/lAmY7GlmPinjZfGNNmDTlUpg==",
"requires": {}
},
"util-deprecate": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@ -11,7 +11,8 @@
"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",
@ -24,11 +25,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.10:5000/swagger/v1/swagger.json -o src/services/api", "oud": "npx openapi -i http://192.168.1.113:5000/swagger/v1/swagger.json -o src/services/api",
"oug": "npx openapi -i https://cloud.digitaldrilling.ru/swagger/v1/swagger.json -o src/services/api", "oug": "npx openapi -i https://cloud.digitaldrilling.ru/swagger/v1/swagger.json -o src/services/api",
"oug_dev": "npx openapi -i http://46.146.207.184/swagger/v1/swagger.json -o src/services/api" "oug_dev": "npx openapi -i http://46.146.209.148:89/swagger/v1/swagger.json -o src/services/api"
}, },
"proxy": "http://46.146.207.184", "proxy": "http://46.146.209.148:89",
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
"react-app", "react-app",

View File

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

View File

@ -1,5 +1,5 @@
import { cloneElement, memo, useCallback, useMemo, useState } from 'react' import { cloneElement, memo, useCallback, useMemo, useState } from 'react'
import { Button, ButtonProps, Tooltip } from 'antd' import { Button, ButtonProps } 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,9 +43,11 @@ 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}>
<Tooltip title={'Скопировать URL в буфер обмена'}> <Button
<Button icon={<CopyOutlined />} {...other} /> icon={<CopyOutlined />}
</Tooltip> title={'Скопировать URL в буфер обмена'}
{...other}
/>
</CopyUrl> </CopyUrl>
) )
}) })

101
src/components/Display.tsx Normal file
View File

@ -0,0 +1,101 @@
import moment from 'moment'
import { useState, useEffect, memo, ReactNode } from 'react'
import {CaretUpOutlined, CaretDownOutlined, CaretRightOutlined} from '@ant-design/icons'
import '@styles/display.less'
export const formatNumber = (value?: unknown, format?: number) =>
Number.isInteger(format) && Number.isFinite(value)
? Number(value).toFixed(format)
: Number(value).toPrecision(4)
const iconStyle = { color:'#0008' }
const displayValueStyle = { display: 'flex', flexGrow: 1 }
export type ValueDisplayProps = {
prefix?: ReactNode
suffix?: ReactNode
format?: number | string | ((arg: string) => ReactNode)
isArrowVisible?: boolean
enumeration?: Record<string, string>
value: string
}
export type DisplayProps = ValueDisplayProps & {
className?: string
label?: ReactNode
}
export const ValueDisplay = memo<ValueDisplayProps>(({ prefix, value, suffix, isArrowVisible, format, enumeration }) => {
const [val, setVal] = useState<ReactNode>('---')
const [arrowState, setArrowState] = useState({
preVal: NaN,
preTimestamp: Date.now(),
direction: 0,
})
useEffect(() => {
setVal((preVal) => {
if ((value ?? '-') === '-' || value === '--') return '---'
if (typeof format === 'function') return format(enumeration?.[value] ?? value)
if (enumeration?.[value]) return enumeration[value]
if (Number.isFinite(+value)) {
if (isArrowVisible && (arrowState.preTimestamp + 1000 < Date.now())) {
let direction = 0
if (+value > arrowState.preVal)
direction = 1
if (+value < arrowState.preVal)
direction = -1
setArrowState({
preVal: +value,
preTimestamp: Date.now(),
direction: direction,
})
}
return formatNumber(value, Number(format))
}
if (value.length > 4) {
const valueDate = moment(value)
if (valueDate.isValid())
return valueDate.format(String(format))
}
return value
})
},[value, isArrowVisible, arrowState, format, enumeration])
let arrow = null
if(isArrowVisible)
switch (arrowState.direction){
case 0:
arrow = <CaretRightOutlined style={iconStyle}/>
break
case 1:
arrow = <CaretUpOutlined style={iconStyle}/>
break
case -1:
arrow = <CaretDownOutlined style={iconStyle}/>
break
default:
break
}
return(
<span className={'display_value'}>
{prefix} {val} {suffix}{arrow}
</span>
)
})
export const Display = memo<DisplayProps>(({ className, label, ...other })=> (
<div className={className}>
<div className={'display_label'}>{label}</div>
<div style={displayValueStyle}>
<ValueDisplay {...other}/>
</div>
</div>
))

View File

@ -5,13 +5,13 @@ import { AutoComplete } from 'antd'
import { join } from 'path' import { join } from 'path'
import { useWell } from '@asb/context' import { useWell } from '@asb/context'
import { makeItem, PrivateMenuItem } from './PrivateMenu' import { makeItem, PrivateWellMenuItem } from './PrivateWellMenu'
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/WellNavigationMenu' import { menuItems as wellMenuItems } from '@pages/Well/NavigationMenu'
import '@styles/components/fast_run_menu.less' import '@styles/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: PrivateMenuItem[], vars?: object): OptionType[] => { const makeOptions = (items: PrivateWellMenuItem[], 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 PrivateMenuItem[])) menus.push(makeItem('Панель администратора', '/admin', [], undefined, adminMenuItems as PrivateWellMenuItem[]))
if (well.id) if (well.id)
menus.push( menus.push(

View File

@ -1,7 +1,7 @@
import { Breadcrumb, Layout, LayoutProps, Menu, SiderProps } from 'antd' import { Breadcrumb, Layout, LayoutProps, Menu, SiderProps } from 'antd'
import { Key, memo, ReactNode, Suspense, useCallback, useEffect, useMemo, useState } from 'react' import { Key, memo, ReactNode, Suspense, useCallback, useEffect, useMemo, useState } from 'react'
import { ItemType } from 'antd/lib/menu/hooks/useItems' import { ItemType } from 'antd/lib/menu/hooks/useItems'
import { Link, Outlet, useLocation } from 'react-router-dom' import { Link, Outlet } 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/components/layout.less' import '@styles/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 | ((path: string) => JSX.Element) breadcrumb?: boolean | JSX.Element
topRightBlock?: JSX.Element topRightBlock?: JSX.Element
} }
@ -49,7 +49,6 @@ 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])
@ -67,8 +66,6 @@ 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) && (
@ -117,7 +114,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>
)} )}
{breadcrumbItems} {breadcrumb !== true && breadcrumb}
</Breadcrumb> </Breadcrumb>
)} )}
{topRightBlock} {topRightBlock}

View File

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

View File

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

View File

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

View File

@ -5,11 +5,6 @@ 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
@ -21,17 +16,6 @@ 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,
@ -40,7 +24,6 @@ export const makeDateColumn = <T extends unknown>(
other?: ColumnProps<T>, other?: ColumnProps<T>,
pickerOther?: DatePickerWrapperProps, pickerOther?: DatePickerWrapperProps,
) => makeColumn<T>(title, key, { ) => makeColumn<T>(title, key, {
editable: true,
...other, ...other,
render: (date) => ( render: (date) => (
<div className={'text-align-r-container'}> <div className={'text-align-r-container'}>

View File

@ -9,6 +9,7 @@ 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'
@ -44,7 +45,6 @@ export const makeColumn = <T = any>(title: ReactNode, key: Key, other?: ColumnPr
title: title, title: title,
key: key, key: key,
dataIndex: key, dataIndex: key,
render: (value: T) => value,
...other, ...other,
}) })

View File

@ -1,3 +1,4 @@
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'
@ -45,17 +46,18 @@ 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,
renderDelegate?: RenderMethod<T>, filters?: ColumnFilterItem[],
filterDelegate?: FilterGenerator<T>, filterDelegate?: FilterGenerator<T>,
renderDelegate?: RenderMethod<T>,
width?: string | number, width?: string | number,
other?: ColumnProps<T>, other?: ColumnProps<T>,
) => makeColumn(title, key, { ) => makeColumn(title, key, {
editable: true, filters,
onFilter: filterDelegate ? filterDelegate(key) : undefined, onFilter: filterDelegate ? filterDelegate(key) : undefined,
sorter: makeNumericSorter(key), sorter: makeNumericSorter(key),
width, width,
input: <InputNumber style={{ width: '100%' }} defaultValue={0} />, input: <InputNumber style={{ width: '100%' }}/>,
render: renderDelegate || makeNumericRender<T>(2), render: renderDelegate ?? makeNumericRender<T>(2),
align: 'right', align: 'right',
...other ...other
}) })
@ -63,78 +65,54 @@ 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,
renderDelegate?: RenderMethod<T>, filters?: ColumnFilterItem[],
filterDelegate?: FilterGenerator<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>, 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', renderDelegate, filterDelegate, width, other), makeNumericColumn<T>('п', key + 'Plan', filters, filterDelegate, renderDelegate, width, other),
makeNumericColumn<T>('Факт', key + 'Fact', renderDelegate, filterDelegate, width, other), makeNumericColumn<T>('ф', key + 'Fact', filters, filterDelegate, renderDelegate, 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,
renderDelegate?: RenderMethod<T>, filters?: ColumnFilterItem[],
filterDelegate?: FilterGenerator<T>, filterDelegate?: FilterGenerator<T>,
renderDelegate?: RenderMethod<T>,
width?: string | number, width?: string | number,
) => makeGroupColumn(title, [ ) => makeGroupColumn(title, [
makeNumericColumn<T>('старт', key + 'Start', renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, key + 'Start')), makeNumericColumn<T>('старт', key + 'Start', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'Start')),
makeNumericColumn<T>('конец', key + 'End', renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, key + 'End')) makeNumericColumn<T>('конец', key + 'End', filters, filterDelegate, renderDelegate, 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,
renderDelegate?: RenderMethod<T>, filters?: ColumnFilterItem[],
filterDelegate?: FilterGenerator<T>, filterDelegate?: FilterGenerator<T>,
renderDelegate?: RenderMethod<T>,
width?: string | number, width?: string | number,
) => makeGroupColumn(title, [ ) => makeGroupColumn(title, [
makeNumericColumn<T>('мин', key + 'Min', renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, key + 'Min')), makeNumericColumn<T>('мин', key + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'Min')),
makeNumericColumn<T>('макс', key + 'Max', renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, key + 'Max')), makeNumericColumn<T>('макс', key + 'Max', filters, filterDelegate, renderDelegate, 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,
renderDelegate?: RenderMethod<T>, filters?: ColumnFilterItem[],
filterDelegate?: FilterGenerator<T>, filterDelegate?: FilterGenerator<T>,
renderDelegate?: RenderMethod<T>,
width?: string | number, width?: string | number,
) => makeGroupColumn(title, [ ) => makeGroupColumn(title, [
makeNumericColumn<T>('мин', `${key}.min`, renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, `${key}.min`)), makeNumericColumn<T>('мин', `${key}.min`, filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, `${key}.min`)),
makeNumericColumn<T>('сред', `${key}.avg`, renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, `${key}.avg`)), makeNumericColumn<T>('сред', `${key}.avg`, filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, `${key}.avg`)),
makeNumericColumn<T>('макс', `${key}.max`, renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, `${key}.max`)), makeNumericColumn<T>('макс', `${key}.max`, filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, `${key}.max`)),
]) ])
export default makeNumericColumn export default makeNumericColumn

View File

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

View File

@ -1,17 +1,9 @@
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, useMemo } from 'react' import { Key, ReactNode } 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,
@ -20,11 +12,10 @@ 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: <SelectWrapper options={options} {...selectOther}/>, input: <Select options={options} {...selectOther}/>,
render: (value, dataset, index) => { render: (value, dataset, index) => {
const item = findOption(value, options) const item = options?.find(option => String(option?.value) === String(value))
return other?.render?.(item, dataset, index) ?? item?.label ?? defaultValue?.label ?? value?.label ?? '--' return other?.render?.(item, dataset, index) ?? item?.label ?? defaultValue?.label ?? value?.label ?? '--'
} }
}) })

View File

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

View File

@ -1,4 +1,3 @@
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'
@ -16,18 +15,6 @@ 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
@ -39,11 +26,10 @@ 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 || makeTextRender(), render: render,
...other ...other
}) })

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { Key, memo, useCallback, useEffect, useState } from 'react' import { Key, memo, useCallback, useEffect, useState } from 'react'
import { ColumnGroupType, ColumnType } from 'antd/lib/table' import { ColumnGroupType, ColumnType } from 'antd/lib/table'
import { Table as RawTable, TableProps as RawTableProps } from 'antd' import { Table as RawTable, TableProps } from 'antd'
import { RenderMethod } from './Columns' import { RenderMethod } from './Columns'
import { tryAddKeys } from './EditableTable' import { tryAddKeys } from './EditableTable'
@ -14,28 +14,16 @@ 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 TableProps<T> = RawTableProps<T> & { export type TableContainer<T> = TableProps<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, D> | T | D [k: Key]: DataSet<T> | 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('.')
@ -48,44 +36,36 @@ 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>[] => {
* @param columns Исходные столбцы return columns.map((column) => {
* @returns Обёрнутые столбцы if ('children' in column) {
*/ return {
const applyColumnWrappers = <T extends DataSet<any>>(columns: TableColumns<T>): TableColumns<T> => columns.map((column) => { ...column,
if ('children' in column) { children: applyColumnWrappers(column.children),
}
}
return { return {
...column, ...column,
children: applyColumnWrappers(column.children), render: makeColumnRenderWrapper<T>(column.key, column.render),
} }
} })
return { }
...column,
render: makeColumnRenderWrapper<T>(column.key, column.render),
}
})
function _Table<T extends DataSet<any>>({ columns, dataSource, tableName, showSettingsChanger, ...other }: TableProps<T>) { function _Table<T extends DataSet<any>>({ columns, dataSource, tableName, showSettingsChanger, ...other }: TableContainer<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])
@ -112,13 +92,6 @@ function _Table<T extends DataSet<any>>({ columns, dataSource, tableName, showSe
) )
} }
/**
* Обёртка над компонентом таблицы AntD
*
* Особенности:
* * Поддержка составных ключей столбцов
* * Работа с настройками столбцов таблицы
*/
export const Table = memo(_Table) as typeof _Table export const Table = memo(_Table) as typeof _Table
export default Table export default Table

View File

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

View File

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

View File

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

View File

@ -1,10 +1,11 @@
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, useElementSize, usePartialProps } from '@utils' import { isDev, usePartialProps } from '@utils'
import D3MouseZone from './D3MouseZone' import D3MouseZone from './D3MouseZone'
import { getChartClass } from './functions' import { getChartClass } from './functions'
@ -35,7 +36,7 @@ import type {
ChartTicks ChartTicks
} from './types' } from './types'
import '@styles/components/d3.less' import '@styles/d3.less'
const defaultOffsets: ChartOffset = { const defaultOffsets: ChartOffset = {
top: 10, top: 10,
@ -130,7 +131,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<HTMLDivElement>() const [rootRef, { width, height }] = useElementSize()
const xAxis = useMemo(() => { const xAxis = useMemo(() => {
if (!data) return if (!data) return

View File

@ -1,12 +1,13 @@
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/components/d3.less' import '@styles/d3.less'
import { usePartialProps } from '@asb/utils'
export type PercentChartDataType = { export type PercentChartDataType = {
name: string name: string
@ -33,7 +34,7 @@ export const D3HorizontalPercentChart = memo<D3HorizontalChartProps>(({
}) => { }) => {
const offset = usePartialProps<ChartOffset>(givenOffset, defaultOffset) const offset = usePartialProps<ChartOffset>(givenOffset, defaultOffset)
const [divRef, { width, height }] = useElementSize<HTMLDivElement>() const [divRef, { width, height }] = useElementSize()
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])
@ -73,13 +74,13 @@ export const D3HorizontalPercentChart = memo<D3HorizontalChartProps>(({
rects.exit().remove() rects.exit().remove()
const selectedRects = r.selectChild<SVGGElement>('.data') const selectedRects = r.selectChild<SVGGElement>('.data')
.selectAll<SVGRectElement, PercentChartDataType>('rect') .selectAll<SVGRectElement, PercentChartDataType>('rect')
selectedRects.attr('fill', (d) => d.color || 'black') selectedRects.attr('fill', (d) => d.color || 'black')
.attr('y', (d) => yScale(d.name) ?? null) .attr('y', (d) => yScale(d.name) ?? null)
.attr('height', yScale.bandwidth()) .attr('height', yScale.bandwidth())
.transition(delay) .transition(delay)
.attr('width', (d) => d.percent > 0 ? xScale(d.percent) : 0) .attr('width', (d) => d.percent > 0 ? xScale(d.percent) : 0)
afterDraw?.(selectedRects) afterDraw?.(selectedRects)

View File

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

View File

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

View File

@ -1,10 +1,11 @@
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, useElementSize, usePartialProps, useUserSettings } from '@utils' import { isDev, usePartialProps, useUserSettings } from '@utils'
import { import {
BaseDataType, BaseDataType,
@ -34,14 +35,13 @@ 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> => {
const round = getNear(Math.abs((mm.max ?? 0) - (mm.min ?? 0))) || 10 let round = Math.abs((mm.max ?? 0) - (mm.min ?? 0))
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: 0, top: 10,
bottom: 0, bottom: 10,
left: 100, left: 100,
right: 20, right: 20,
} }
@ -115,8 +115,6 @@ 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-значения */
@ -182,7 +180,6 @@ 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,
@ -212,7 +209,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<HTMLDivElement>() const [rootRef, { width, height }] = useElementSize()
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])
@ -245,11 +242,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]) }, [groups, data, yDomain, sizes.chartsHeight])
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) => {
@ -456,7 +453,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
.tickSize(yTicks.visible ? -width + offset.left + offset.right : 0) .tickSize(yTicks.visible ? -width + offset.left + offset.right : 0)
.ticks(yTicks.count) as any // TODO: Исправить тип .ticks(yTicks.count) as any // TODO: Исправить тип
) )
yAxisArea().selectAll('.tick line').attr('stroke', yTicks.color) yAxisArea().selectAll('.tick line').attr('stroke', yTicks.color)
}, [yAxisArea, yAxis, animDurationMs, width, offset, yTicks]) }, [yAxisArea, yAxis, animDurationMs, width, offset, yTicks])
@ -466,8 +463,8 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
groups.forEach((group, i) => { groups.forEach((group, i) => {
group() group()
.attr('transform', `translate(${sizes.groupLeft(group.key)}, 0)`) .attr('transform', `translate(${sizes.groupLeft(group.key)}, 0)`)
.attr('clip-path', `url(#chart-group-clip)`) .attr('clip-path', `url(#chart-clip)`)
group.charts.forEach((chart) => { group.charts.forEach((chart) => {
chart() chart()
.attr('color', chart.color || null) .attr('color', chart.color || null)
@ -495,7 +492,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, flowData) renderRectArea<DataType>(xAxis, yAxis, chart)
break break
default: default:
break break
@ -509,7 +506,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
chart.afterDraw?.(chart) chart.afterDraw?.(chart)
}) })
}) })
}, [data, flowData, groups, height, offset, sizes, chartDomains, yAxis]) }, [data, groups, height, offset, sizes, chartDomains])
return ( return (
<LoaderPortal <LoaderPortal
@ -533,7 +530,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-group-clip`}> <clipPath id={`chart-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>
@ -574,7 +571,6 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
sizes={sizes} sizes={sizes}
spaceBetweenGroups={spaceBetweenGroups} spaceBetweenGroups={spaceBetweenGroups}
data={data} data={data}
flowData={flowData}
height={height} height={height}
/> />
</D3MouseZone> </D3MouseZone>

View File

@ -92,15 +92,6 @@ 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(() => {
@ -114,7 +105,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 || 'black') .attr('fill', (d) => regulators[d.id].color)
.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 })
@ -139,24 +130,14 @@ 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'}>
<defs> <g ref={setRef} >
<clipPath id={`chart-limit-clip`}> <g className={'bars'} strokeWidth={0} />
{/* Сдвиг во все стороны на 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={selectedRegulator?.color} stroke={regulators[selected.id].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} />
@ -167,7 +148,7 @@ const _D3MonitoringLimitChart = <DataType extends TelemetryDataSaubDto>({
y={zoneY1} y={zoneY1}
width={zoneWidth} width={zoneWidth}
height={zoneY2 - zoneY1} height={zoneY2 - zoneY1}
fill={selectedRegulator?.color} fill={regulators[selected.id].color}
/> />
</g> </g>
)} )}
@ -177,7 +158,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>{selectedRegulator?.label}</span> <span>{regulators[selected.id].label}</span>
<Grid style={{ margin: 0, padding: 0 }}> <Grid style={{ margin: 0, padding: 0 }}>
<GridItem row={1} col={1}>Начало:</GridItem> <GridItem row={1} col={1}>Начало:</GridItem>
<GridItem row={1} col={2}>{formatDate(selected.dateStart)}</GridItem> <GridItem row={1} col={2}>{formatDate(selected.dateStart)}</GridItem>

View File

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

View File

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

View File

@ -6,8 +6,7 @@ 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' ||
@ -15,27 +14,25 @@ export const renderRectArea = <DataType extends BaseDataType>(
!chart.maxXAccessor || !chart.maxXAccessor ||
!chart.minYAccessor || !chart.minYAccessor ||
!chart.maxYAccessor || !chart.maxYAccessor ||
!data !chart.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() chart().attr('fill', 'currentColor')
.attr('fill', 'currentColor')
.attr('fill-opacity', '0.15')
.attr('stroke-opacity', '0.3')
const rects = chart().selectAll<SVGRectElement, null>('rect').data(data) const rects = chart().selectAll<SVGRectElement, null>('rect').data(data)
rects.exit().remove() rects.exit().remove()
rects.enter().append('rect') rects.enter().append('rect')
appendTransition(chart().selectAll<SVGRectElement, Record<string, any>>('rect'), chart) appendTransition(chart().selectAll<SVGRectElement, Record<string, any>>('rect'), chart)
.attr('x', (d) => xAxis(xMin(d))) .attr('x1', (d) => xAxis(xMin(d)))
.attr('y', (d) => yAxis(yMin(d))) .attr('x2', (d) => xAxis(xMax(d)))
.attr('width', (d) => xAxis(xMax(d)) - xAxis(xMin(d))) .attr('y1', (d) => yAxis(yMin(d)))
.attr('height', (d) => yAxis(yMax(d)) - yAxis(yMin(d))) .attr('y2', (d) => yAxis(yMax(d)))
} }

View File

@ -1,7 +1,7 @@
import { memo, useEffect, useState } from 'react' import { memo, useEffect, useState } from 'react'
import { Outlet } from 'react-router-dom' import { Outlet } from 'react-router-dom'
import { DepositListContext } from '@asb/context' import { DepositsContext } 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 (
<DepositListContext.Provider value={deposits}> <DepositsContext.Provider value={deposits}>
<LoaderPortal show={isLoading}> <LoaderPortal show={isLoading}>
<Outlet /> <Outlet />
</LoaderPortal> </LoaderPortal>
</DepositListContext.Provider> </DepositsContext.Provider>
) )
}) })

View File

@ -1,7 +1,7 @@
import { Tag, TreeSelect } from 'antd' import { Tag, TreeSelect } from 'antd'
import { memo, useEffect, useState } from 'react' import { memo, useEffect, useState } from 'react'
import { useDepositList } from '@asb/context' import { useDeposits } 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 = useDepositList() const deposits = useDeposits()
useEffect(() => { useEffect(() => {
invokeWebApiWrapperAsync( invokeWebApiWrapperAsync(
@ -59,7 +59,6 @@ export const WellSelector = memo(({ value, onChange, treeData, treeLabels, ...ot
<TreeSelect <TreeSelect
multiple multiple
treeCheckable treeCheckable
maxTagCount={'responsive'}
showCheckedStrategy={TreeSelect.SHOW_CHILD} showCheckedStrategy={TreeSelect.SHOW_CHILD}
treeDefaultExpandAll treeDefaultExpandAll
treeData={wellsTree} treeData={wellsTree}

View File

@ -2,7 +2,7 @@ import { Drawer, Tree, TreeDataNode, TreeProps } from 'antd'
import { useState, useEffect, useCallback, memo, Key, useMemo } from 'react' import { useState, useEffect, useCallback, memo, Key, useMemo } from 'react'
import { useNavigate, useLocation } from 'react-router-dom' import { useNavigate, useLocation } from 'react-router-dom'
import { useDepositList } from '@asb/context' import { useDeposits } 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,41 +10,32 @@ 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/components/well_tree_select.css' import '@styles/wellTreeSelect.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, number | null] => { const getKeyByUrl = (url?: string): [Key | null, string | null] => {
const result = url?.match(URL_REGEX) // pattern "/:type/:id" const result = url?.match(/^\/([^\/]+)\/([^\/?]+)/) // pattern "/:type/:id"
if (!result) return [null, null, null] if (!result) return [null, null]
return [result[0], result[1], result[2] && result[2] !== 'null' ? Number(result[2]) : null] return [result[0], result[1]]
} }
const getLabel = (wellsTree: TreeDataNode[], value?: string): string | undefined => { const getLabel = (wellsTree: TreeDataNode[], value?: string): string | undefined => {
const [url, type, key] = getKeyByUrl(value) const [url, type] = 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)
)) ))
@ -53,7 +44,6 @@ 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)
@ -136,7 +126,7 @@ export const WellTreeSelector = memo<WellTreeSelectorProps>(({ expand, current,
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const deposits = useDepositList() const deposits = useDeposits()
const wellsTree = useMemo(() => makeWellsTreeData(deposits), [deposits]) const wellsTree = useMemo(() => makeWellsTreeData(deposits), [deposits])
@ -147,8 +137,8 @@ export const WellTreeSelector = memo<WellTreeSelectorProps>(({ expand, current,
}, [wellsTree]) }, [wellsTree])
const onSelect = useCallback((value: Key[]): void => { const onSelect = useCallback((value: Key[]): void => {
const newRoot = URL_REGEX.exec(String(value)) const newRoot = /\/(\w+)\/\d+/.exec(String(value))
const oldRoot = URL_REGEX.exec(location.pathname) const oldRoot = /\/(\w+)(?:\/\d+)?/.exec(location.pathname)
if (!newRoot || !oldRoot) return if (!newRoot || !oldRoot) return
let newPath = newRoot[0] let newPath = newRoot[0]

View File

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

View File

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

View File

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

View File

@ -19,12 +19,6 @@ 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 ?? '-'}`
@ -32,7 +26,6 @@ export type TelemetryViewProps = {
telemetry?: TelemetryDto telemetry?: TelemetryDto
} }
/** Компонент для отображения информации о телеметрии */
export const TelemetryView = memo<TelemetryViewProps>(({ telemetry }) => telemetry?.info ? ( export const TelemetryView = memo<TelemetryViewProps>(({ telemetry }) => telemetry?.info ? (
<Tooltip <Tooltip
overlayInnerStyle={{ width: '400px' }} overlayInnerStyle={{ width: '400px' }}

View File

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

View File

@ -1,4 +1,4 @@
import { DetailedHTMLProps, HTMLAttributes, memo } from 'react' import { 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,19 +15,9 @@ 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>
@ -57,12 +47,10 @@ export const WellView = memo<WellViewProps>(({ well, iconProps, labelProps, ...o
<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' }} {...iconProps}> <span role={'img'} style={{ marginRight: 8, lineHeight: 0, verticalAlign: '-0.25em' }}>
<WellIcon state={wellState[well.idState || 0].enum} width={'1em'} height={'1em'} /> <WellIcon state={wellState[well.idState || 0].enum} width={'1em'} height={'1em'} />
</span> </span>
<span {...labelProps}> {well.caption}
{getWellTitle(well)}
</span>
</Tooltip> </Tooltip>
) : ( ) : (
<Tooltip title={'нет скважины'}>-</Tooltip> <Tooltip title={'нет скважины'}>-</Tooltip>

View File

@ -21,7 +21,6 @@ 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}

67
src/context.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

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

Before

Width:  |  Height:  |  Size: 8.7 KiB

View File

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

View File

@ -12,7 +12,7 @@ import {
UserOutlined, UserOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import { makeItem, PrivateMenu } from '@components/PrivateMenu' import { makeItem, PrivateWellMenu } from '@components/PrivateWellMenu'
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) => (
<PrivateMenu <PrivateWellMenu
{...props} {...props}
items={menuItems} items={menuItems}
rootPath={'/admin'} rootPath={'/admin'}

View File

@ -3,12 +3,11 @@ 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'
@ -31,11 +30,17 @@ 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')
}), }),
makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: 200, formItemRules: min1 }), makeColumn('Название', 'caption', {
makeNumericColumn('Широта', 'latitude', coordsFormat, undefined, 150), width: 200,
makeNumericColumn('Долгота', 'longitude', coordsFormat, undefined, 150), editable: true,
sorter: makeStringSorter('caption'),
formItemRules: min1,
}),
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFormat }),
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFormat }),
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }), makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
], [deposits]) ], [deposits])

View File

@ -3,9 +3,10 @@ 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'
@ -36,8 +37,16 @@ const CompanyController = memo(() => {
})) }))
setColumns([ setColumns([
makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: 200, formItemRules: min1 }), makeColumn('Название', 'caption', {
makeSelectColumn('Тип компании', 'idCompanyType', companyTypes, null, { width: 200 }), width: 200,
editable: true,
sorter: makeStringSorter('caption'),
formItemRules: min1,
}),
makeSelectColumn('Тип компании', 'idCompanyType', companyTypes, null, {
width: 200,
editable: true
}),
]) ])
await updateTable() await updateTable()

View File

@ -1,14 +1,19 @@
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, defaultPagination, makeTextColumn } from '@components/Table' import { EditableTable, makeColumn, makeStringSorter, defaultPagination } 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 = [
makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: 200, formItemRules: min1 }), makeColumn('Название', 'caption', {
width: 200,
editable: true,
sorter: makeStringSorter('caption'),
formItemRules: min1,
}),
] ]
const CompanyTypeController = memo(() => { const CompanyTypeController = memo(() => {

View File

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

View File

@ -1,15 +1,23 @@
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, makeTextColumn } from '@components/Table' import { EditableTable, makeColumn, makeStringSorter } 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 = [
makeTextColumn('Название', 'name', undefined, undefined, undefined, { isRequired: true, formItemRules: min1 }), makeColumn('Название', 'name', {
makeTextColumn('Описание', 'description'), editable: true,
sorter: makeStringSorter('name'),
isRequired: true,
formItemRules: min1,
}),
makeColumn('Описание', 'description', {
editable: true,
sorter: makeStringSorter('description'),
}),
] ]
const PermissionController = memo(() => { const PermissionController = memo(() => {

View File

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

View File

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

View File

@ -115,6 +115,7 @@ 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,
@ -129,34 +130,41 @@ 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), makeTextColumn('Должность', 'position', null, null, null, { editable: true }),
makeTextColumn('Роли', 'roleNames', roleFilters, null, rolesRender, { makeTextColumn('Роли', 'roleNames', roleFilters, null, rolesRender, {
editable: true,
input: <RoleTag roles={roles} />, input: <RoleTag roles={roles} />,
onFilter: makeArrayOnFilter('roleNames'), onFilter: makeArrayOnFilter('roleNames'),
}), }),
makeSelectColumn('Компания', 'idCompany', companies, '--', { makeSelectColumn('Компания', 'idCompany', companies, '--', {
editable: true,
sorter: makeNumericSorter('idCompany'), sorter: makeNumericSorter('idCompany'),
}) })
]) ])

View File

@ -12,12 +12,11 @@ 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'
@ -82,11 +81,23 @@ const WellController = memo(() => {
})) }))
setColumns([ setColumns([
makeSelectColumn('Куст', 'idCluster', clusters, '--', { width: '5rem', sorter: makeNumericSorter('idCluster') }), makeSelectColumn('Куст', 'idCluster', clusters, '--', {
makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: '5rem' }), width: '5rem',
makeSelectColumn('Тип', 'idWellType', wellTypes, '--', { width: 150, sorter: makeNumericSorter('idWellType') }), editable: true,
makeNumericColumn('Широта', 'latitude', coordsFormat, undefined, 150), sorter: makeNumericSorter('idCluster'),
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} />,
@ -94,6 +105,7 @@ const WellController = memo(() => {
}, ), }, ),
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 170 }), makeTimezoneColumn('Зона', 'timezone', null, true, { width: 170 }),
makeTagColumn('Компании', 'companies', companies, 'id', 'caption', { makeTagColumn('Компании', 'companies', companies, 'id', 'caption', {
editable: true,
render: (company) => <CompanyView company={company} />, render: (company) => <CompanyView company={company} />,
}), }),
]) ])

View File

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

View File

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

View File

@ -1,14 +1,14 @@
import { memo, useMemo } from 'react' import { memo, useMemo } from 'react'
import { Table, makeTextColumn, makeNumericColumnPlanFactOld } from '@components/Table' import { Table, makeTextColumn, makeNumericColumnPlanFact } 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'),
makeNumericColumnPlanFactOld('Глубина забоя', 'depth', (number) => getPrecision(number)), makeNumericColumnPlanFact('Глубина забоя', 'depth', null, null, (number) => getPrecision(number)),
makeNumericColumnPlanFactOld('Часы', 'durationHours', (number) => getPrecision(number)), makeNumericColumnPlanFact('Часы', 'durationHours', null, null, (number) => getPrecision(number)),
makeNumericColumnPlanFactOld('Комментарий', 'comment', (text) => text ?? '-'), makeNumericColumnPlanFact('Комментарий', 'comment', null, null, (text) => text ?? '-')
] ]
export const WellOperationsTable = memo(({ wellOperations }) => { export const WellOperationsTable = memo(({ wellOperations }) => {

106
src/pages/Deposit.jsx Executable file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import { OperationStatService, WellOperationService } from '@api'
import { arrayOrDefault, withPermissions } from '@utils' import { arrayOrDefault, withPermissions } from '@utils'
import '@styles/index.css' import '@styles/index.css'
import '@styles/pages/statistics.less' import '@styles/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, (section => numericRender(section?.depth)), undefined, 100, { makeNumericColumn('Проходка', key, null, null, (section => numericRender(section?.depth)), 100, {
sorter: makeSectionSorter(key, 'depth'), sorter: makeSectionSorter(key, 'depth'),
}), }),
makeNumericColumn('Время', key, (section => numericRender(section?.time)), undefined, 100, { makeNumericColumn('Время', key, null, null, (section => numericRender(section?.time)), 100, {
sorter: makeSectionSorter(key, 'time'), sorter: makeSectionSorter(key, 'time'),
}), }),
makeNumericColumn((<>V<sub>рейсовая</sub></>), key, speedRender ?? speedNumericRender, undefined, 100, { makeNumericColumn((<>V<sub>рейсовая</sub></>), key, null, null, speedRender ?? speedNumericRender, 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: '25vh' } const scrollSettings = { scrollToFirstRowOnChange: true, x: 100, y: 200 }
const summaryColSpan = 1 /// TODO: Когда добавится куст изменить на 2 const summaryColSpan = 1 /// TODO: Когда добавится куст изменить на 2
const getWellData = async (wellsList) => { const getWellData = async (wellsList) => {

View File

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

View File

@ -1,13 +1,12 @@
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 } from 'antd' import { Button, Badge, Divider, Modal, Row, Col } 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, makeNumericColumnPlanFactOld, makeNumericColumn } from '@components/Table' import { Table, makeTextColumn, makeNumericColumnPlanFact, makeNumericColumn } from '@components/Table'
import { WellCompositeService } from '@api' import { WellCompositeService } from '@api'
import { import {
hasPermission, hasPermission,
@ -17,6 +16,8 @@ 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'))
@ -145,15 +146,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 ?? '-', { width: 100 }), makeTextColumn('Секция', 'sectionType', filtersSectionsType, sortBySectionId, (text) => text ?? '-'),
makeNumericColumnPlanFactOld('Глубина, м', 'sectionWellDepth', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }), makeNumericColumnPlanFact('Глубина, м', 'sectionWellDepth', filtersMinMax, makeFilterMinMaxFunction),
makeNumericColumnPlanFactOld('Продолжительность, ч', 'sectionBuildDays', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }), makeNumericColumnPlanFact('Продолжительность, ч', 'sectionBuildDays', filtersMinMax, makeFilterMinMaxFunction),
makeNumericColumnPlanFactOld('МСП, м/ч', 'sectionRateOfPenetration', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }), makeNumericColumnPlanFact('МСП, м/ч', 'sectionRateOfPenetration', filtersMinMax, makeFilterMinMaxFunction),
makeNumericColumnPlanFactOld('Рейсовая скорость, м/ч', 'sectionRouteSpeed', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }), makeNumericColumnPlanFact('Рейсовая скорость, м/ч', 'sectionRouteSpeed', filtersMinMax, makeFilterMinMaxFunction),
makeNumericColumnPlanFactOld('Спуск КНБК, м/ч', 'sectionBhaDownSpeed', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }), makeNumericColumnPlanFact('Спуск КНБК, м/ч', 'sectionBhaDownSpeed', filtersMinMax, makeFilterMinMaxFunction),
makeNumericColumnPlanFactOld('Подъем КНБК, м/ч', 'sectionBhaUpSpeed', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }), makeNumericColumnPlanFact('Подъем КНБК, м/ч', 'sectionBhaUpSpeed', filtersMinMax, makeFilterMinMaxFunction),
makeNumericColumnPlanFactOld('Скорость спуска ОК, м/ч', 'sectionCasingDownSpeed', undefined, makeFilterMinMaxFunction, undefined, { filters: filtersMinMax }), makeNumericColumnPlanFact('Скорость спуска ОК, м/ч', 'sectionCasingDownSpeed', filtersMinMax, makeFilterMinMaxFunction),
makeNumericColumn('НПВ, ч', 'nonProductiveHours', undefined, makeFilterMinMaxFunction, '80px', { filters: filtersMinMax }), makeNumericColumn('НПВ, ч', 'nonProductiveHours', filtersMinMax, makeFilterMinMaxFunction, null, '80px'),
{ {
title: 'TVD', title: 'TVD',
render: (value) => ( render: (value) => (
@ -199,11 +200,11 @@ const WellCompositeSections = memo(({ statsWells, selectedSections }) => {
dataSource={rows} dataSource={rows}
size={'small'} size={'small'}
bordered bordered
scroll={{ x: true, y: '30vh' }} scroll={{ x: true, y: 620 }}
rowSelection={rowSelection} rowSelection={rowSelection}
pagination={false} pagination={false}
/> />
<Divider style={{ marginTop: 0 }} /> <Divider />
<Badge.Ribbon text={'комбинированная скважина'} color={'gray'}> <Badge.Ribbon text={'комбинированная скважина'} color={'gray'}>
<h3>Выбранные секции</h3> <h3>Выбранные секции</h3>
</Badge.Ribbon> </Badge.Ribbon>
@ -213,9 +214,12 @@ const WellCompositeSections = memo(({ statsWells, selectedSections }) => {
rowSelection={rowSelection} rowSelection={rowSelection}
size={'small'} size={'small'}
bordered bordered
scroll={{ x: true, y: '30vh' }} scroll={{ x: true }}
pagination={false} pagination={false}
/> />
<Row justify={'end'} style={{ margin: '1rem 0' }}>
<Col><NewParamsTable selectedWellsKeys={selectedWellsKeys} /></Col>
</Row>
<Modal <Modal
title={'TVD'} title={'TVD'}

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ import { FileService } from '@api'
import MarksCard from './MarksCard' import MarksCard from './MarksCard'
import '@styles/pages/drilling_program.less' import '@styles/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.id, idCategory, companyName, fileName, begin, end, false, skip, pageSize) const paginatedHistory = await FileService.getFilesInfo(well.caption, idCategory, companyName, fileName, begin, end, false, skip, pageSize)
setTotal(paginatedHistory?.count ?? 0) setTotal(paginatedHistory?.count ?? 0)
setData(arrayOrDefault(paginatedHistory?.items)) setData(arrayOrDefault(paginatedHistory?.items))
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,10 @@ import {
AlertOutlined, AlertOutlined,
BarChartOutlined, BarChartOutlined,
BuildOutlined, BuildOutlined,
ControlOutlined,
DatabaseOutlined,
DeploymentUnitOutlined, DeploymentUnitOutlined,
DesktopOutlined,
ExperimentOutlined, ExperimentOutlined,
FilePdfOutlined, FilePdfOutlined,
FolderOutlined, FolderOutlined,
@ -12,12 +15,14 @@ import {
TableOutlined, TableOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import { makeItem, PrivateMenu } from '@components/PrivateMenu' import { makeItem, PrivateWellMenu } from '@components/PrivateWellMenu'
export const menuItems = [ export const menuItems = [
makeItem('Начальная страница', 'start_screen', [], <DesktopOutlined />),
makeItem('Телеметрия', 'telemetry', [], <FundViewOutlined />, [ makeItem('Телеметрия', 'telemetry', [], <FundViewOutlined />, [
makeItem('Мониторинг', 'monitoring', [], <FundViewOutlined />), makeItem('Мониторинг', 'telemetry', [], <FundViewOutlined />),
makeItem('Сообщения', 'messages', [], <AlertOutlined />), makeItem('Сообщения', 'messages', [], <AlertOutlined />),
makeItem('Архив', 'archive', [], <DatabaseOutlined />),
makeItem('ННБ', 'dashboard_nnb', [], <FolderOutlined />), makeItem('ННБ', 'dashboard_nnb', [], <FolderOutlined />),
makeItem('Операции', 'operations', [], <FolderOutlined />), makeItem('Операции', 'operations', [], <FolderOutlined />),
makeItem('Наработка', 'operation_time', [], <FolderOutlined />), makeItem('Наработка', 'operation_time', [], <FolderOutlined />),
@ -39,6 +44,7 @@ export const menuItems = [
makeItem('План', 'plan', [], <TableOutlined />), makeItem('План', 'plan', [], <TableOutlined />),
makeItem('Факт', 'fact', [], <TableOutlined />), makeItem('Факт', 'fact', [], <TableOutlined />),
makeItem('РТК', 'drillProcessFlow', [], <BarChartOutlined />), makeItem('РТК', 'drillProcessFlow', [], <BarChartOutlined />),
makeItem('Режимы', 'params', [], <ControlOutlined />),
]), ]),
makeItem('Документы', 'document', [], <FolderOutlined />, [ makeItem('Документы', 'document', [], <FolderOutlined />, [
makeItem('Растворный сервис', 'fluidService', [], <FolderOutlined />), makeItem('Растворный сервис', 'fluidService', [], <FolderOutlined />),
@ -57,15 +63,15 @@ export const menuItems = [
makeItem('Дело скважины', 'well_case', [], <FolderOutlined />), makeItem('Дело скважины', 'well_case', [], <FolderOutlined />),
] ]
export const WellNavigationMenu = memo((props) => ( export const NavigationMenu = memo((props) => (
<PrivateMenu <PrivateWellMenu
{...props} {...props}
items={menuItems} items={menuItems}
rootPath={'/well/{idWell}'} rootPath={'/well/{wellId}'}
mode={'inline'} mode={'inline'}
theme={'dark'} theme={'dark'}
style={{ backgroundColor: 'transparent' }} style={{ backgroundColor: 'transparent' }}
/> />
)) ))
export default WellNavigationMenu export default NavigationMenu

View File

@ -51,7 +51,7 @@ const styleBlock = {
// fill: '#b3b3b3', // fill: '#b3b3b3',
// }; // };
export const RigMnemo = memo(({ blockPosition, bitPosition, wellDepth }) => { export const RigMnemo = memo(({ blockPosition, bitPosition, wellDepth, ...other }) => {
let blockPositionY = 0 let blockPositionY = 0
let bitPositionY = 0 let bitPositionY = 0
@ -81,7 +81,7 @@ export const RigMnemo = memo(({ blockPosition, bitPosition, wellDepth }) => {
const bitTranslate = `translate(0, ${bitPositionY})` const bitTranslate = `translate(0, ${bitPositionY})`
return ( return (
<svg width={'170'} height={'540'} viewBox={'20 0 40 145'} version={'1.1'}> <svg width={'170'} height={'540'} viewBox={'20 0 40 145'} version={'1.1'} {...other}>
<g id={'Casing'} style={styleCasing}> <g id={'Casing'} style={styleCasing}>
<path d={'m 29.897917,67.46875 v -1.322917 l 4.497916,-10e-7 v -1.322916 h 3.96875 v 1.322916 l 4.497917,10e-7 v 1.322917 z'} /> <path d={'m 29.897917,67.46875 v -1.322917 l 4.497916,-10e-7 v -1.322916 h 3.96875 v 1.322916 l 4.497917,10e-7 v 1.322917 z'} />
<path d={'M 31.75,97.895832 V 67.468749 h 9.260416 v 30.427083 z'} /> <path d={'M 31.75,97.895832 V 67.468749 h 9.260416 v 30.427083 z'} />

View File

@ -0,0 +1,164 @@
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, [])

View File

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

View File

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

View File

@ -11,8 +11,8 @@ import { makeColumn, makeDateColumn, makeNumericColumn, makeNumericSorter, makeT
import { withPermissions } from '@utils' import { withPermissions } from '@utils'
import { MessageService } from '@api' import { MessageService } from '@api'
import '@styles/components/filter.less' import '@styles/filter.less'
import '@styles/pages/message.less' import '@styles/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', (depth, item) => ( makeNumericColumn('Глубина, м', 'wellDepth', null, null, (depth, item) => (
<Tooltip title={'Нажмите для перехода в архив'}> <Tooltip title={'Нажмите для перехода в архив'}>
<Link <Link
style={{ color: 'inherit'}} style={{ color: 'inherit'}}
to={`/well/${idWell}/telemetry/monitoring?range=1800&end=${moment(item?.date).add(27, 'minute').local().toISOString()}`} to={`/well/${idWell}/telemetry/archive?range=1800&start=${moment(item?.date).subtract(3, 'minute').local().toISOString()}`}
> >
<LinkOutlined /> <LinkOutlined />
&nbsp; &nbsp;
{depth.toFixed(2)} {depth.toFixed(2)}
</Link> </Link>
</Tooltip> </Tooltip>
), undefined, '7rem'), ), '7rem'),
makeColumn('Категория', 'categoryId', { makeColumn('Категория', 'categoryId', {
width: '8rem', width: '8rem',
render: (_, item) => categoryDictionary[item.categoryId].title, render: (_, item) => categoryDictionary[item.categoryId].title,

View File

@ -10,8 +10,8 @@ import { DateRangeWrapper, makeColumn, makeNumericColumn, makeNumericRender, mak
import { arrayOrDefault, range, withPermissions } from '@utils' import { arrayOrDefault, range, withPermissions } from '@utils'
import { SubsystemOperationTimeService } from '@api' import { SubsystemOperationTimeService } from '@api'
import '@styles/components/filter.less' import '@styles/filter.less'
import '@styles/pages/operation_time.less' import '@styles/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', val => (+val * 100).toFixed(2), undefined, 200), makeNumericColumn('Использование, %', 'kUsage', undefined, undefined, val => (+val * 100).toFixed(2), 200),
makeNumericColumn('Проходка, м', 'sumDepthInterval', undefined, undefined, 200), makeNumericColumn('Проходка, м', 'sumDepthInterval', undefined, undefined, undefined, 200),
makeNumericColumn('Время работы, ч', 'usedTimeHours', undefined, undefined, 200), makeNumericColumn('Время работы, ч', 'usedTimeHours', undefined, undefined, undefined, 200),
makeNumericColumn('Кол-во запусков', 'operationCount', makeNumericRender(0), undefined, 200), makeNumericColumn('Кол-во запусков', 'operationCount', undefined, undefined, makeNumericRender(0), 200),
] ]
// Выбор доступен только до текущей даты // Выбор доступен только до текущей даты

View File

@ -5,13 +5,14 @@ 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'), makeTextColumn('Отчество', 'patronymic', undefined, undefined, undefined, { editable: true }),
] ]
const rowClassName = (record) => record.has ? 'driller_list_active' : '' const rowClassName = (record) => record.has ? 'driller_list_active' : ''

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { memo } from 'react'
import { Table, makeTextColumn, makeNumericColumn, makeNumericRender } from '@components/Table' import { Table, makeTextColumn, makeNumericColumn, makeNumericRender } from '@components/Table'
import '@styles/pages/detected_operations.less' import '@styles/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', (value) => parseInt(value), undefined, 150), makeNumericColumn('Кол-во операций', 'count', null, null, (value) => parseInt(value), 150),
makeNumericColumn('Среднее по ключевому показателю', 'averageValue', numericRender, undefined, 150), makeNumericColumn('Среднее по ключевому показателю', 'averageValue', null, null, numericRender, 150),
makeNumericColumn('Среднее целевого показателя', 'averageTargetValue', numericRender, undefined, 150), makeNumericColumn('Среднее целевого показателя', 'averageTargetValue', null, null, numericRender, 150),
makeNumericColumn('Эффективность (%)', 'efficiency', numericRender, undefined, 150), makeNumericColumn('Эффективность (%)', 'efficiency', null, null, numericRender, 150),
makeNumericColumn('Коэффициент потерь', 'loss', numericRender, undefined, 100), makeNumericColumn('Коэффициент потерь', 'loss', null, null, numericRender, 100),
] ]
export const OperationsTable = memo(({ data, height, ...other }) => ( export const OperationsTable = memo(({ data, height, ...other }) => (

View File

@ -4,17 +4,18 @@ 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 { OperationValueService } from '@api' import { DetectedOperationService, 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, options }) => { export const TargetEditor = memo(({ loading, onChange }) => {
const [targets, setTargets] = useState([]) const [targets, setTargets] = useState([])
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
@ -62,17 +63,20 @@ export const TargetEditor = memo(({ loading, onChange, options }) => {
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', numericRender, undefined, 150, columnOptions), makeNumericColumn('Цель', 'targetValue', undefined, undefined, numericRender, 150, columnOptions),
makeNumericColumn('Норм.', 'standardValue', numericRender, undefined, 150, columnOptions), makeNumericColumn('Норм.', 'standardValue', undefined, undefined, numericRender, 150, columnOptions),
makeGroupColumn('Глубина, м', [ makeGroupColumn('Глубина, м', [
makeNumericColumn('Начало', 'depthStart', numericRender, undefined, 150, columnOptions), makeNumericColumn('Начало', 'depthStart', undefined, undefined, numericRender, 150, columnOptions),
makeNumericColumn('Окончание', 'depthEnd', numericRender, undefined, 150, columnOptions), makeNumericColumn('Окончание', 'depthEnd', undefined, undefined, numericRender, 150, columnOptions),
]), ]),
]) ])
}, },
@ -80,7 +84,7 @@ export const TargetEditor = memo(({ loading, onChange, options }) => {
`Не удалось получить список категорий целей`, `Не удалось получить список категорий целей`,
{ actionName: 'Получение списка категорий целей', well } { actionName: 'Получение списка категорий целей', well }
) )
}, [options]) }, [well])
useEffect(() => { useEffect(() => {
updateTable() updateTable()

View File

@ -7,8 +7,8 @@ import LoaderPortal from '@components/LoaderPortal'
import { DateRangeWrapper } from '@components/Table' import { DateRangeWrapper } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { unique } from '@utils/filters' import { unique } from '@utils/filters'
import { getPermissions, arrayOrDefault, range, withPermissions, prettify } from '@utils' import { getPermissions, arrayOrDefault, range, withPermissions, pretify } from '@utils'
import { DetectedOperationService, DrillerService, TelemetryDataSaubService, WellOperationService } from '@api' import { DetectedOperationService, DrillerService, TelemetryDataSaubService } 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/pages/detected_operations.less' import '@styles/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(5011) const [selectedCategory, setSelectedCategory] = useState(14)
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(prettify(Math.max(maxTarget, value))) setYDomain(pretify(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 WellOperationService.getCategories(well.id)) const categories = arrayOrDefault(await DetectedOperationService.getCategories())
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,7 +135,6 @@ const Operations = memo(() => {
onChange={setYDomain} onChange={setYDomain}
addonAfter={'мин'} addonAfter={'мин'}
addonBefore={'Верхняя граница'} addonBefore={'Верхняя граница'}
style={{width: '20em'}}
/> />
{permissions.driller.get && ( {permissions.driller.get && (
<> <>
@ -143,8 +142,8 @@ const Operations = memo(() => {
<DrillerList drillers={drillers} loading={drillersLoader} onChange={updateDrillers} /> <DrillerList drillers={drillers} loading={drillersLoader} onChange={updateDrillers} />
</> </>
)} )}
{permissions.detectedOperation.get && permissions.operationValue.get && categories && ( {permissions.detectedOperation.get && permissions.operationValue.get && (
<TargetEditor onChange={updateData} options={categories} /> <TargetEditor onChange={updateData} />
)} )}
</div> </div>
<LoaderPortal show={isLoading}> <LoaderPortal show={isLoading}>

View File

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

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