forked from ddrilling/asb_cloud_front
Compare commits
5 Commits
dev
...
feature/ed
Author | SHA1 | Date | |
---|---|---|---|
|
4ad6f91c8a | ||
|
32ec3c22d2 | ||
|
44d11d964d | ||
|
41515fdb7e | ||
|
89835f7f00 |
15
.vscode/settings.json
vendored
15
.vscode/settings.json
vendored
@ -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"
|
|
||||||
}
|
}
|
191
CODE_STANDART.md
191
CODE_STANDART.md
@ -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` - для компонентов, применяющихся как виджеты в дашбордах.
|
|
68
README.md
68
README.md
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
Установка выполняется одной командой:
|
Установка выполняется одной командой:
|
||||||
```bash
|
```bash
|
||||||
npm ci
|
npm i
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2. Автогенерация сервисов
|
## 2. Автогенерация сервисов
|
||||||
@ -19,12 +19,12 @@ npm ci
|
|||||||
|
|
||||||
Если сервер запущен на текущей машине достаточно написать:
|
Если сервер запущен на текущей машине достаточно написать:
|
||||||
```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>
|
|
||||||
```
|
|
||||||
|
33
package-lock.json
generated
33
package-lock.json
generated
@ -16,7 +16,9 @@
|
|||||||
"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",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.18.2",
|
"@babel/core": "^7.18.2",
|
||||||
@ -14687,6 +14689,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",
|
||||||
@ -14771,6 +14786,11 @@
|
|||||||
"minimalistic-assert": "^1.0.0"
|
"minimalistic-assert": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/web-vitals": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg=="
|
||||||
|
},
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
@ -26397,6 +26417,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",
|
||||||
@ -26466,6 +26492,11 @@
|
|||||||
"minimalistic-assert": "^1.0.0"
|
"minimalistic-assert": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"web-vitals": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg=="
|
||||||
|
},
|
||||||
"webidl-conversions": {
|
"webidl-conversions": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
10
package.json
10
package.json
@ -11,7 +11,9 @@
|
|||||||
"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",
|
||||||
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
@ -24,11 +26,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",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"short_name": "ЕЦП",
|
"short_name": "React App",
|
||||||
"name": "Единая Цифровая Платформа",
|
"name": "Create React App Sample",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
|
64
src/App.tsx
64
src/App.tsx
@ -1,57 +1,51 @@
|
|||||||
import { BrowserRouter as Router, Navigate, Route, Routes } from 'react-router-dom'
|
import { BrowserRouter as Router, Navigate, Route, Routes } from 'react-router-dom'
|
||||||
import { lazy, memo, Suspense } from 'react'
|
import { memo } from 'react'
|
||||||
|
import { ConfigProvider } from 'antd'
|
||||||
|
import locale from 'antd/lib/locale/ru_RU'
|
||||||
|
|
||||||
import { RootPathContext } from '@asb/context'
|
import { RootPathContext } from '@asb/context'
|
||||||
import SuspenseFallback from '@components/SuspenseFallback'
|
import { getUserToken, NoAccessComponent } from '@utils'
|
||||||
import { NoAccessComponent } from '@utils'
|
import { OpenAPI } from '@api'
|
||||||
|
|
||||||
import '@styles/pages/App.less'
|
import AdminPanel from '@pages/AdminPanel'
|
||||||
|
import Well from '@pages/Well'
|
||||||
|
import Login from '@pages/Login'
|
||||||
|
import Cluster from '@pages/Cluster'
|
||||||
|
import Deposit from '@pages/Deposit'
|
||||||
|
import Register from '@pages/Register'
|
||||||
|
import FileDownload from '@pages/FileDownload'
|
||||||
|
|
||||||
const UserOutlet = lazy(() => import('@components/outlets/UserOutlet'))
|
import '@styles/App.less'
|
||||||
const DepositsOutlet = lazy(() => import('@components/outlets/DepositsOutlet'))
|
import '@styles/include/antd_theme.less'
|
||||||
const LayoutPortal = lazy(() => import('@components/LayoutPortal'))
|
|
||||||
|
|
||||||
const Login = lazy(() => import('@pages/public/Login'))
|
//OpenAPI.BASE = 'http://localhost:3000'
|
||||||
const Register = lazy(() => import('@pages/public/Register'))
|
OpenAPI.TOKEN = async () => getUserToken() ?? ''
|
||||||
const FileDownload = lazy(() => import('@pages/FileDownload'))
|
OpenAPI.HEADERS = {'Content-Type': 'application/json'}
|
||||||
|
|
||||||
const AdminPanel = lazy(() => import('@pages/AdminPanel'))
|
|
||||||
const Deposit = lazy(() => import('@pages/Deposit'))
|
|
||||||
const Cluster = lazy(() => import('@pages/Cluster'))
|
|
||||||
const Well = lazy(() => import('@pages/Well'))
|
|
||||||
|
|
||||||
export const App = memo(() => (
|
export const App = memo(() => (
|
||||||
|
<ConfigProvider locale={locale}>
|
||||||
<RootPathContext.Provider value={''}>
|
<RootPathContext.Provider value={''}>
|
||||||
<Suspense fallback={<SuspenseFallback style={{ minHeight: '100vh' }} />}>
|
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route index element={<Navigate to={'deposit'} replace />} />
|
<Route index element={<Navigate to={Deposit.getKey()} replace />} />
|
||||||
<Route path={'*'} element={<NoAccessComponent />} />
|
<Route path={'*'} element={<NoAccessComponent />} />
|
||||||
|
|
||||||
{/* Public pages */}
|
{/* Public pages */}
|
||||||
<Route path={'/login'} element={<Login />} />
|
<Route path={Login.route} element={<Login />} />
|
||||||
<Route path={'/register'} element={<Register />} />
|
<Route path={Register.route} element={<Register />} />
|
||||||
|
|
||||||
|
{/* Admin pages */}
|
||||||
|
<Route path={AdminPanel.route} element={<AdminPanel />} />
|
||||||
|
|
||||||
{/* User pages */}
|
{/* User pages */}
|
||||||
<Route element={<UserOutlet />}>
|
<Route path={Deposit.route} element={<Deposit />} />
|
||||||
<Route path={'/file_download/:idFile/*'} element={<FileDownload />} />
|
<Route path={Cluster.route} element={<Cluster />} />
|
||||||
|
<Route path={Well.route} element={<Well />} />
|
||||||
<Route element={<DepositsOutlet />}>
|
<Route path={FileDownload.route} element={<FileDownload />} />
|
||||||
<Route element={<LayoutPortal />}>
|
|
||||||
{/* Admin pages */}
|
|
||||||
<Route path={'/admin/*'} element={<AdminPanel />} />
|
|
||||||
|
|
||||||
{/* Client pages */}
|
|
||||||
<Route path={'/deposit/*'} element={<Deposit />} />
|
|
||||||
<Route path={'/cluster/:idCluster'} element={<Cluster />} />
|
|
||||||
<Route path={'/well/:idWell/*'} element={<Well />} />
|
|
||||||
</Route>
|
|
||||||
</Route>
|
|
||||||
</Route>
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
</Suspense>
|
|
||||||
</RootPathContext.Provider>
|
</RootPathContext.Provider>
|
||||||
|
</ConfigProvider>
|
||||||
))
|
))
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
@ -2,8 +2,8 @@ import { memo, useCallback, useMemo, useState } from 'react'
|
|||||||
import { Rule } from 'antd/lib/form'
|
import { Rule } from 'antd/lib/form'
|
||||||
import { Form, Input, Modal, FormProps } from 'antd'
|
import { Form, Input, Modal, FormProps } from 'antd'
|
||||||
|
|
||||||
import { useUser } from '@asb/context'
|
|
||||||
import { AuthService, UserDto } from '@api'
|
import { AuthService, UserDto } from '@api'
|
||||||
|
import { getUserId, getUserLogin } from '@utils'
|
||||||
import { passwordRules, createPasswordRules } from '@utils/validationRules'
|
import { passwordRules, createPasswordRules } from '@utils/validationRules'
|
||||||
|
|
||||||
import LoaderPortal from './LoaderPortal'
|
import LoaderPortal from './LoaderPortal'
|
||||||
@ -31,8 +31,7 @@ export const ChangePassword = memo<ChangePasswordProps>(({ user, visible, onCanc
|
|||||||
const [showLoader, setShowLoader] = useState<boolean>(false)
|
const [showLoader, setShowLoader] = useState<boolean>(false)
|
||||||
const [isDisabled, setIsDisabled] = useState(true)
|
const [isDisabled, setIsDisabled] = useState(true)
|
||||||
|
|
||||||
const userContext = useUser()
|
const userData = useMemo(() => user ?? { id: getUserId(), login: getUserLogin() } as UserDto, [user])
|
||||||
const userData = useMemo(() => user ?? userContext, [user])
|
|
||||||
|
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
|
|
||||||
@ -64,7 +63,7 @@ export const ChangePassword = memo<ChangePasswordProps>(({ user, visible, onCanc
|
|||||||
{user && <> (<UserView user={user} />)</>}
|
{user && <> (<UserView user={user} />)</>}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
open={visible}
|
visible={visible}
|
||||||
onCancel={onModalCancel}
|
onCancel={onModalCancel}
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
okText={'Сохранить'}
|
okText={'Сохранить'}
|
||||||
|
@ -106,7 +106,7 @@ export const ColorPicker = memo<ColorPickerProps>(({ value = '#AA33BB', onChange
|
|||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
trigger={'click'}
|
trigger={'click'}
|
||||||
onOpenChange={onClose}
|
onVisibleChange={onClose}
|
||||||
content={(
|
content={(
|
||||||
<div className={'asb-color-picker-content'}>
|
<div className={'asb-color-picker-content'}>
|
||||||
<div className={'asb-color-picker-sliders'}>
|
<div className={'asb-color-picker-sliders'}>
|
||||||
|
@ -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
101
src/components/Display.tsx
Normal 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>
|
||||||
|
))
|
@ -1,165 +0,0 @@
|
|||||||
import { memo, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
|
||||||
import { BaseSelectRef } from 'rc-select'
|
|
||||||
import { AutoComplete } from 'antd'
|
|
||||||
import { join } from 'path'
|
|
||||||
|
|
||||||
import { useWell } from '@asb/context'
|
|
||||||
import { makeItem, PrivateMenuItem } from './PrivateMenu'
|
|
||||||
import { hasPermission, isURLAvailable } from '@utils'
|
|
||||||
|
|
||||||
import { menuItems as adminMenuItems } from '@pages/AdminPanel/AdminNavigationMenu'
|
|
||||||
import { menuItems as wellMenuItems } from '@pages/Well/WellNavigationMenu'
|
|
||||||
|
|
||||||
import '@styles/components/fast_run_menu.less'
|
|
||||||
|
|
||||||
const transliterationTable = {
|
|
||||||
'q': 'й', 'w': 'ц', 'e': 'у', 'r': 'к', 't': 'е', 'y': 'н', 'u': 'г', 'i': 'ш', 'o': 'щ', 'p': 'з', '[': 'х', ']': 'ъ', '{': 'х', '}': 'ъ',
|
|
||||||
'a': 'ф', 's': 'ы', 'd': 'в', 'f': 'а', 'g': 'п', 'h': 'р', 'j': 'о', 'k': 'л', 'l': 'д', ';': 'ж', "'": 'э', ':': 'ж', '"': 'э',
|
|
||||||
'z': 'я', 'x': 'ч', 'c': 'с', 'v': 'м', 'b': 'и', 'n': 'т', 'm': 'ь', ',': 'б', '.': 'ю', '<': 'б', '>': 'ю',
|
|
||||||
}
|
|
||||||
|
|
||||||
type OptionType = {
|
|
||||||
value: string
|
|
||||||
label: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
const transliterateToRu = (text: string) => Object.entries(transliterationTable).reduce((out, [en, ru]) => out.replaceAll(en, ru), text.toLowerCase())
|
|
||||||
const transliterateToEn = (text: string) => Object.entries(transliterationTable).reduce((out, [en, ru]) => out.replaceAll(ru, en), text.toLowerCase())
|
|
||||||
const applyVars = (route: string, vars?: object): string => !vars ? route :
|
|
||||||
Object.entries(vars).reduce((out, [key, value]) => out.replaceAll(`{${key}}`, value), route)
|
|
||||||
|
|
||||||
const makeOptions = (items: PrivateMenuItem[], vars?: object): OptionType[] => {
|
|
||||||
const out: OptionType[] = []
|
|
||||||
items.forEach((item) => {
|
|
||||||
if (!hasPermission(item.permissions)) return
|
|
||||||
out.push({
|
|
||||||
label: item.title,
|
|
||||||
value: applyVars(item.route, vars),
|
|
||||||
})
|
|
||||||
if (item.children) {
|
|
||||||
const childrenOptions = makeOptions(item.children).map((child) => ({
|
|
||||||
label: `${item.title} > ${child.label}`,
|
|
||||||
value: applyVars(join(item.route, String(child.value)), vars),
|
|
||||||
}))
|
|
||||||
out.push(...childrenOptions)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeFindInString = (text: string) => {
|
|
||||||
const searchText = text.toLowerCase()
|
|
||||||
const toRu = transliterateToRu(searchText)
|
|
||||||
const toEn = transliterateToEn(searchText)
|
|
||||||
|
|
||||||
return (sourceText: string) => {
|
|
||||||
const text = sourceText.toLowerCase()
|
|
||||||
let idx = text.indexOf(searchText)
|
|
||||||
if (idx < 0 && (idx = text.indexOf(toRu)) < 0 && (idx = text.indexOf(toEn)) < 0) return false
|
|
||||||
return { from: idx, to: idx + searchText.length }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FastRunMenu = memo(() => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
|
||||||
const [value, setValue] = useState<string | null>()
|
|
||||||
const [results, setResults] = useState<OptionType[]>([])
|
|
||||||
const ref = useRef<BaseSelectRef | null>(null)
|
|
||||||
|
|
||||||
const [well] = useWell()
|
|
||||||
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
const options = useMemo(() => {
|
|
||||||
const menus = [
|
|
||||||
makeItem('Месторождения', '/deposit', []),
|
|
||||||
]
|
|
||||||
|
|
||||||
if (isURLAvailable('/admin'))
|
|
||||||
menus.push(makeItem('Панель администратора', '/admin', [], undefined, adminMenuItems as PrivateMenuItem[]))
|
|
||||||
|
|
||||||
if (well.id)
|
|
||||||
menus.push(
|
|
||||||
makeItem(`Куст (${well.cluster})`, `/cluster/${well.idCluster}`, []),
|
|
||||||
makeItem(`Скважина (${well.caption})`, '/well/{idWell}', [], undefined, wellMenuItems),
|
|
||||||
)
|
|
||||||
|
|
||||||
return makeOptions(menus, { idWell: well.id })
|
|
||||||
}, [well])
|
|
||||||
|
|
||||||
const onClose = useCallback(() => {
|
|
||||||
setIsOpen(false)
|
|
||||||
setValue(null)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onTextChanged = useCallback((value: any) => {
|
|
||||||
navigate(value, { state: { from: location.pathname } })
|
|
||||||
onClose()
|
|
||||||
}, [onClose])
|
|
||||||
|
|
||||||
const onSearch = useCallback((text: string) => {
|
|
||||||
if (text.trim() === '') {
|
|
||||||
setResults(options)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const findInString = makeFindInString(text)
|
|
||||||
const results = options.map((option) => {
|
|
||||||
const label = String(option.label)
|
|
||||||
const idx = findInString(label.toLowerCase())
|
|
||||||
if (!idx) return findInString(option.value.toLowerCase()) ? option : false
|
|
||||||
|
|
||||||
return {
|
|
||||||
...option,
|
|
||||||
label: <>
|
|
||||||
{label.slice(0, idx.from)}
|
|
||||||
<span className={'fast-run-menu-text-found'}>
|
|
||||||
{label.slice(idx.from, idx.to)}
|
|
||||||
</span>
|
|
||||||
{label.slice(idx.to)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
}).filter(Boolean) as OptionType[]
|
|
||||||
|
|
||||||
setResults(results)
|
|
||||||
}, [options])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const listener = (event: KeyboardEvent) => {
|
|
||||||
if (event.altKey && event.code === 'KeyA')
|
|
||||||
setIsOpen((prev) => !prev)
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('keyup', listener)
|
|
||||||
|
|
||||||
return () => document.removeEventListener('keyup', listener)
|
|
||||||
}, [ref.current])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) return
|
|
||||||
ref.current?.focus()
|
|
||||||
ref.current?.scrollTo(0)
|
|
||||||
}, [isOpen])
|
|
||||||
|
|
||||||
useEffect(() => onSearch(''), [onSearch])
|
|
||||||
|
|
||||||
return isOpen ? (
|
|
||||||
<div className={'fast-run-menu'}>
|
|
||||||
<AutoComplete
|
|
||||||
ref={ref}
|
|
||||||
autoFocus
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
options={results}
|
|
||||||
onBlur={onClose}
|
|
||||||
onChange={setValue}
|
|
||||||
onSearch={onSearch}
|
|
||||||
placeholder={'Введите название страницы...'}
|
|
||||||
onSelect={onTextChanged}
|
|
||||||
value={value}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : <></>
|
|
||||||
})
|
|
||||||
|
|
||||||
export default FastRunMenu
|
|
@ -22,7 +22,7 @@ export const Grid = memo<ComponentProps>(({ children, style, ...other }) => (
|
|||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
|
||||||
export const GridItem = memo<GridItemProps>(({ children, row, col, rowSpan, colSpan, style, className, ...other }) => {
|
export const GridItem = memo<GridItemProps>(({ children, row, col, rowSpan, colSpan, style, ...other }) => {
|
||||||
const localRow = +row
|
const localRow = +row
|
||||||
const localCol = +col
|
const localCol = +col
|
||||||
const localColSpan = colSpan ? colSpan - 1 : 0
|
const localColSpan = colSpan ? colSpan - 1 : 0
|
||||||
@ -32,11 +32,12 @@ export const GridItem = memo<GridItemProps>(({ children, row, col, rowSpan, colS
|
|||||||
gridColumnEnd: localCol + localColSpan,
|
gridColumnEnd: localCol + localColSpan,
|
||||||
gridRowStart: localRow,
|
gridRowStart: localRow,
|
||||||
gridRowEnd: localRow + localRowSpan,
|
gridRowEnd: localRow + localRowSpan,
|
||||||
|
padding: '4px',
|
||||||
...style,
|
...style,
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`asb-grid-item ${className || ''}`} style={gridItemStyle} {...other}>
|
<div style={gridItemStyle} {...other}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
28
src/components/Layout/AdminLayoutPortal.tsx
Executable file
28
src/components/Layout/AdminLayoutPortal.tsx
Executable file
@ -0,0 +1,28 @@
|
|||||||
|
import { memo, ReactNode } from 'react'
|
||||||
|
import { Link, useLocation } from 'react-router-dom'
|
||||||
|
import { Button, Layout, LayoutProps } from 'antd'
|
||||||
|
|
||||||
|
import PageHeader from '@components/PageHeader'
|
||||||
|
|
||||||
|
export type AdminLayoutPortalProps = LayoutProps & {
|
||||||
|
title?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AdminLayoutPortal = memo<AdminLayoutPortalProps>(({ title, ...props }) => {
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout.Content>
|
||||||
|
<PageHeader isAdmin title={title} style={{ backgroundColor: '#900' }}>
|
||||||
|
<Button size={'large'}>
|
||||||
|
<Link to={'/'}>Вернуться на сайт</Link>
|
||||||
|
</Button>
|
||||||
|
</PageHeader>
|
||||||
|
<Layout>
|
||||||
|
<Layout.Content className={'site-layout-background sheet'} {...props}/>
|
||||||
|
</Layout>
|
||||||
|
</Layout.Content>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default AdminLayoutPortal
|
31
src/components/Layout/LayoutPortal.tsx
Executable file
31
src/components/Layout/LayoutPortal.tsx
Executable file
@ -0,0 +1,31 @@
|
|||||||
|
import { Key, memo, ReactNode } from 'react'
|
||||||
|
import { Layout, LayoutProps } from 'antd'
|
||||||
|
|
||||||
|
import PageHeader from '@components/PageHeader'
|
||||||
|
import { WellTreeSelector, WellTreeSelectorProps } from '@components/selectors/WellTreeSelector'
|
||||||
|
import { wrapPrivateComponent } from '@utils'
|
||||||
|
|
||||||
|
export type LayoutPortalProps = LayoutProps & {
|
||||||
|
title?: ReactNode
|
||||||
|
noSheet?: boolean
|
||||||
|
selector?: WellTreeSelectorProps
|
||||||
|
}
|
||||||
|
|
||||||
|
const _LayoutPortal = memo<LayoutPortalProps>(({ title, noSheet, selector, ...props }) => (
|
||||||
|
<Layout.Content>
|
||||||
|
<PageHeader title={title}>
|
||||||
|
<WellTreeSelector {...selector} />
|
||||||
|
</PageHeader>
|
||||||
|
<Layout>
|
||||||
|
{noSheet ? props.children : (
|
||||||
|
<Layout.Content className={'site-layout-background sheet'} {...props}/>
|
||||||
|
)}
|
||||||
|
</Layout>
|
||||||
|
</Layout.Content>
|
||||||
|
))
|
||||||
|
|
||||||
|
export const LayoutPortal = wrapPrivateComponent(_LayoutPortal, {
|
||||||
|
requirements: ['Deposit.get'],
|
||||||
|
})
|
||||||
|
|
||||||
|
export default LayoutPortal
|
5
src/components/Layout/index.ts
Executable file
5
src/components/Layout/index.ts
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
export * from './AdminLayoutPortal'
|
||||||
|
export * from './LayoutPortal'
|
||||||
|
|
||||||
|
export type { AdminLayoutPortalProps } from './AdminLayoutPortal'
|
||||||
|
export type { LayoutPortalProps } from './LayoutPortal'
|
@ -1,139 +0,0 @@
|
|||||||
import { Breadcrumb, Layout, LayoutProps, Menu, SiderProps } from 'antd'
|
|
||||||
import { Key, memo, ReactNode, Suspense, useCallback, useEffect, useMemo, useState } from 'react'
|
|
||||||
import { ItemType } from 'antd/lib/menu/hooks/useItems'
|
|
||||||
import { Link, Outlet, useLocation } from 'react-router-dom'
|
|
||||||
import {
|
|
||||||
ApartmentOutlined,
|
|
||||||
CodeOutlined,
|
|
||||||
HomeOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
} from '@ant-design/icons'
|
|
||||||
|
|
||||||
import { LayoutPropsContext } from '@asb/context'
|
|
||||||
import { UserMenu, UserMenuProps } from '@components/UserMenu'
|
|
||||||
import { WellTreeSelector, WellTreeSelectorProps } from '@components/selectors/WellTreeSelector'
|
|
||||||
import { isURLAvailable, withPermissions } from '@utils'
|
|
||||||
|
|
||||||
import SuspenseFallback from './SuspenseFallback'
|
|
||||||
|
|
||||||
import Logo from '@images/Logo'
|
|
||||||
|
|
||||||
import '@styles/components/layout.less'
|
|
||||||
|
|
||||||
const { Content, Sider } = Layout
|
|
||||||
|
|
||||||
export type LayoutPortalProps = Omit<LayoutProps, 'children'> & {
|
|
||||||
title?: ReactNode
|
|
||||||
sheet?: boolean
|
|
||||||
showSelector?: boolean
|
|
||||||
selectorProps?: WellTreeSelectorProps
|
|
||||||
sider?: boolean | JSX.Element
|
|
||||||
siderProps?: SiderProps & { userMenuProps?: UserMenuProps }
|
|
||||||
isAdmin?: boolean
|
|
||||||
fallback?: JSX.Element
|
|
||||||
breadcrumb?: boolean | ((path: string) => JSX.Element)
|
|
||||||
topRightBlock?: JSX.Element
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultProps: LayoutPortalProps = {
|
|
||||||
title: 'Единая цифровая платформа',
|
|
||||||
sider: true,
|
|
||||||
sheet: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeItem = (title: string, key: Key, icon: JSX.Element, label?: ReactNode, onClick?: () => void) => ({ icon, key, title, label: label ?? title, onClick })
|
|
||||||
|
|
||||||
const _LayoutPortal = memo(() => {
|
|
||||||
const [menuCollapsed, setMenuCollapsed] = useState<boolean>(true)
|
|
||||||
const [wellsTreeOpen, setWellsTreeOpen] = useState<boolean>(false)
|
|
||||||
const [userMenuOpen, setUserMenuOpen] = useState<boolean>(false)
|
|
||||||
const [currentWell, setCurrentWell] = useState<string>('')
|
|
||||||
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 setLayoutProps = useCallback((props: LayoutPortalProps) => setProps({ ...defaultProps, ...props}), [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof showSelector === 'boolean')
|
|
||||||
setWellsTreeOpen(showSelector)
|
|
||||||
}, [showSelector])
|
|
||||||
|
|
||||||
const menuItems = useMemo(() => [
|
|
||||||
!isAdmin && makeItem(currentWell, 'well', <ApartmentOutlined/>, null, () => setWellsTreeOpen((prev) => !prev)),
|
|
||||||
isAdmin && makeItem('Вернуться на сайт', 'go_back', <HomeOutlined />, <Link to={'/'}>Домой</Link>),
|
|
||||||
!isAdmin && isURLAvailable('/admin') && makeItem('Панель администратора', 'admin_panel', <CodeOutlined />, <Link to={'/admin'}>Панель администратора</Link>),
|
|
||||||
makeItem('Профиль', 'profile', <UserOutlined/>, null, () => setUserMenuOpen((prev) => !prev)),
|
|
||||||
].filter(Boolean) as ItemType[], [isAdmin, currentWell])
|
|
||||||
|
|
||||||
const breadcrumbItems = useMemo(() => typeof breadcrumb === 'function' && breadcrumb(location.pathname), [breadcrumb, location.pathname])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout className={`page-layout ${isAdmin ? 'page-layout-admin' : ''}`}>
|
|
||||||
{(sider || siderProps) && (
|
|
||||||
<Sider {...siderProps} collapsedWidth={50} collapsed={menuCollapsed} trigger={null} collapsible className={`menu-sider ${siderProps?.className || ''}`}>
|
|
||||||
<div className={'sider-content'}>
|
|
||||||
<button className={'sider-toogle'} onClick={() => setMenuCollapsed((prev) => !prev)}>
|
|
||||||
<Logo onlyIcon={menuCollapsed} />
|
|
||||||
</button>
|
|
||||||
<div className={'scrollable hide-slider'}>
|
|
||||||
{sider}
|
|
||||||
</div>
|
|
||||||
<Menu
|
|
||||||
mode={'inline'}
|
|
||||||
items={menuItems}
|
|
||||||
theme={'dark'}
|
|
||||||
selectable={false}
|
|
||||||
/>
|
|
||||||
<UserMenu
|
|
||||||
open={userMenuOpen}
|
|
||||||
onClose={() => setUserMenuOpen(false)}
|
|
||||||
isAdmin={isAdmin}
|
|
||||||
{...siderProps?.userMenuProps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Sider>
|
|
||||||
)}
|
|
||||||
{!isAdmin && (
|
|
||||||
<WellTreeSelector
|
|
||||||
open={wellsTreeOpen}
|
|
||||||
onClose={() => setWellsTreeOpen(false)}
|
|
||||||
{...selectorProps}
|
|
||||||
onChange={(well) => setCurrentWell(well ?? 'Выберите месторождение')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Layout className={'page-content'}>
|
|
||||||
<Content {...other} className={`${sheet ? 'site-layout-background sheet' : ''} ${other.className ?? ''}`}>
|
|
||||||
{(breadcrumb || topRightBlock) && (
|
|
||||||
<div className={'breadcrumb-block'}>
|
|
||||||
{breadcrumb && (
|
|
||||||
<Breadcrumb>
|
|
||||||
<Breadcrumb.Item href={'/'}>
|
|
||||||
<HomeOutlined />
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
{!isAdmin && (
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<a style={{ userSelect: 'none' }} onClick={() => setWellsTreeOpen((prev) => !prev)}>{currentWell}</a>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
)}
|
|
||||||
{breadcrumbItems}
|
|
||||||
</Breadcrumb>
|
|
||||||
)}
|
|
||||||
{topRightBlock}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<LayoutPropsContext.Provider value={setLayoutProps}>
|
|
||||||
<Suspense fallback={fallback ?? <SuspenseFallback style={{ minHeight: '100%' }} />}>
|
|
||||||
<Outlet />
|
|
||||||
</Suspense>
|
|
||||||
</LayoutPropsContext.Provider>
|
|
||||||
</Content>
|
|
||||||
</Layout>
|
|
||||||
</Layout>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const LayoutPortal = withPermissions(_LayoutPortal, ['Deposit.get'])
|
|
||||||
|
|
||||||
export default LayoutPortal
|
|
@ -3,22 +3,14 @@ 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const LoaderPortal: React.FC<LoaderPortalProps> = ({ className = '', show, fade = true, children, spinnerProps, ...other }) => (
|
||||||
* @description Добавляет оверлей загрузки над обёрнутым блоком
|
|
||||||
*/
|
|
||||||
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'}>{children}</div>
|
||||||
{show && fade && <div className={'loader-fade'}/>}
|
{show && fade && <div className={'loader-fade'}/>}
|
||||||
{show && <div className={'loader-overlay'}><Loader {...spinnerProps} /></div>}
|
{show && <div className={'loader-overlay'}><Loader {...spinnerProps} /></div>}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
import { Breadcrumb, BreadcrumbItemProps } from 'antd'
|
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import { join } from 'path'
|
|
||||||
|
|
||||||
import { PrivateMenuItem } from '@components/PrivateMenu'
|
|
||||||
import { FunctionalValue, getFunctionalValue, } from '@utils'
|
|
||||||
|
|
||||||
export const makeBreadcrumbItems = (items: PrivateMenuItem[], pathParts: string[], root: string = '/') => {
|
|
||||||
const out = []
|
|
||||||
const parts = [...pathParts]
|
|
||||||
let route = root
|
|
||||||
let arr: PrivateMenuItem[] | undefined = items
|
|
||||||
while (arr && parts.length > 0) {
|
|
||||||
const child: PrivateMenuItem | undefined = arr.find(elm => elm.route.toLowerCase() === parts[0].toLowerCase())
|
|
||||||
if (!child) break
|
|
||||||
route = join(route, child.route)
|
|
||||||
out.push({ ...child, route })
|
|
||||||
parts.splice(0, 1)
|
|
||||||
arr = child.children
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeMenuBreadcrumbItemsRender = (
|
|
||||||
menuItems: PrivateMenuItem[],
|
|
||||||
pathRoot: RegExp = /^\//,
|
|
||||||
itemsProps?: FunctionalValue<(item: PrivateMenuItem) => BreadcrumbItemProps>,
|
|
||||||
itemRender?: (item: PrivateMenuItem) => JSX.Element,
|
|
||||||
) => (path: string) => {
|
|
||||||
const getItemProps = getFunctionalValue(itemsProps)
|
|
||||||
|
|
||||||
const rootPart = pathRoot.exec(path)
|
|
||||||
if (!rootPart || rootPart.length <= 0) return []
|
|
||||||
const root = rootPart[0]
|
|
||||||
const parts = path.trim().slice(root.length).split('/')
|
|
||||||
const items = makeBreadcrumbItems(menuItems, parts, root)
|
|
||||||
|
|
||||||
return items.map((item) => (
|
|
||||||
<Breadcrumb.Item key={item.route} {...getItemProps(item)}>
|
|
||||||
{itemRender ? itemRender(item) : (
|
|
||||||
<Link to={item.route}>{item.title}</Link>
|
|
||||||
)}
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
))
|
|
||||||
}
|
|
30
src/components/PageHeader.tsx
Executable file
30
src/components/PageHeader.tsx
Executable file
@ -0,0 +1,30 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
import { Layout } from 'antd'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { BasicProps } from 'antd/lib/layout/layout'
|
||||||
|
|
||||||
|
import { headerHeight } from '@utils'
|
||||||
|
import { UserMenu } from './UserMenu'
|
||||||
|
|
||||||
|
import Logo from '@images/Logo'
|
||||||
|
|
||||||
|
export type PageHeaderProps = BasicProps & {
|
||||||
|
title?: string
|
||||||
|
isAdmin?: boolean
|
||||||
|
children?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Мониторинг', isAdmin, children, ...other }) => (
|
||||||
|
<Layout>
|
||||||
|
<Layout.Header className={'header'} {...other}>
|
||||||
|
<Link to={'/'} style={{ height: headerHeight }}>
|
||||||
|
<Logo />
|
||||||
|
</Link>
|
||||||
|
<h1 className={'title'}>{title}</h1>
|
||||||
|
{children}
|
||||||
|
<UserMenu isAdmin={isAdmin} />
|
||||||
|
</Layout.Header>
|
||||||
|
</Layout>
|
||||||
|
))
|
||||||
|
|
||||||
|
export default PageHeader
|
14
src/components/Private/PrivateContent.tsx
Executable file
14
src/components/Private/PrivateContent.tsx
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
import { memo, ReactElement } from 'react'
|
||||||
|
|
||||||
|
import { isURLAvailable } from '@utils'
|
||||||
|
|
||||||
|
export type PrivateContentProps = {
|
||||||
|
absolutePath: string
|
||||||
|
children?: ReactElement<any, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrivateContent = memo<PrivateContentProps>(({ absolutePath, children = null }) =>
|
||||||
|
isURLAvailable(absolutePath) ? children : null
|
||||||
|
)
|
||||||
|
|
||||||
|
export default PrivateContent
|
19
src/components/Private/PrivateDefaultRoute.tsx
Executable file
19
src/components/Private/PrivateDefaultRoute.tsx
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
import { Navigate, Route, RouteProps } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { isURLAvailable } from '@utils'
|
||||||
|
|
||||||
|
import { getDefaultRedirectPath } from './PrivateRoutes'
|
||||||
|
|
||||||
|
export type PrivateDefaultRouteProps = RouteProps & {
|
||||||
|
urls: string[]
|
||||||
|
elseRedirect?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrivateDefaultRoute = memo<PrivateDefaultRouteProps>(({ elseRedirect, urls, ...other }) => (
|
||||||
|
<Route {...other} path={'/'} element={(
|
||||||
|
<Navigate replace to={urls.find((url) => isURLAvailable(url)) ?? elseRedirect ?? getDefaultRedirectPath()} />
|
||||||
|
)} />
|
||||||
|
))
|
||||||
|
|
||||||
|
export default PrivateDefaultRoute
|
75
src/components/Private/PrivateMenu.tsx
Normal file
75
src/components/Private/PrivateMenu.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { join } from 'path'
|
||||||
|
import { Menu, MenuProps } from 'antd'
|
||||||
|
import { ItemType } from 'antd/lib/menu/hooks/useItems'
|
||||||
|
import { Link, LinkProps } from 'react-router-dom'
|
||||||
|
import { Children, isValidElement, memo, ReactNode, RefAttributes, useMemo } from 'react'
|
||||||
|
|
||||||
|
import { useRootPath } from '@asb/context'
|
||||||
|
import { getTabname, hasPermission, PrivateComponent, PrivateProps } from '@utils'
|
||||||
|
|
||||||
|
export type PrivateMenuProps = MenuProps & { root?: string }
|
||||||
|
|
||||||
|
export type PrivateMenuLinkProps = Partial<ItemType> & Omit<LinkProps, 'to'> & RefAttributes<HTMLAnchorElement> & {
|
||||||
|
icon?: ReactNode
|
||||||
|
danger?: boolean
|
||||||
|
title?: ReactNode
|
||||||
|
content?: PrivateComponent<any>
|
||||||
|
path?: string
|
||||||
|
visible?: boolean
|
||||||
|
permissions?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrivateMenuLink = memo<PrivateMenuLinkProps>(({ content, path = '', title, ...other }) => (
|
||||||
|
<Link to={path} {...other}>{title ?? content?.title}</Link>
|
||||||
|
))
|
||||||
|
|
||||||
|
const PrivateMenuMain = memo<PrivateMenuProps>(({ selectable, mode, selectedKeys, root, children, ...other }) => {
|
||||||
|
const rootContext = useRootPath()
|
||||||
|
const rootPath = useMemo(() => root ?? rootContext ?? '', [root, rootContext])
|
||||||
|
|
||||||
|
const tab = getTabname()
|
||||||
|
const keys = useMemo(() => selectedKeys ?? (tab ? [tab] : []), [selectedKeys, tab])
|
||||||
|
|
||||||
|
const items = useMemo(() => Children.map(children, (child) => {
|
||||||
|
if (!child || !isValidElement<PrivateMenuLinkProps>(child))
|
||||||
|
return null
|
||||||
|
const content: PrivateProps | undefined = child.props.content
|
||||||
|
const visible: boolean | undefined = child.props.visible
|
||||||
|
|
||||||
|
if (visible === false) return null
|
||||||
|
let key
|
||||||
|
if (content?.key)
|
||||||
|
key = content.key
|
||||||
|
else if (content?.route)
|
||||||
|
key = content.route
|
||||||
|
else if (child.key) {
|
||||||
|
key = child.key?.toString()
|
||||||
|
key = key.slice(key.lastIndexOf('$') + 1)
|
||||||
|
} else return null
|
||||||
|
|
||||||
|
const permissions = child.props.permissions ?? content?.requirements
|
||||||
|
const path = child.props.path ?? join(rootPath, key)
|
||||||
|
|
||||||
|
if (visible || hasPermission(permissions))
|
||||||
|
return {
|
||||||
|
...child.props,
|
||||||
|
key,
|
||||||
|
label: <PrivateMenuLink {...child.props} path={path} />,
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})?.filter((v) => v) ?? [], [children, rootPath])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
selectable={selectable ?? true}
|
||||||
|
mode={mode ?? 'horizontal'}
|
||||||
|
selectedKeys={keys}
|
||||||
|
items={items as ItemType[]}
|
||||||
|
{...other}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PrivateMenu = Object.assign(PrivateMenuMain, { Link: PrivateMenuLink })
|
||||||
|
|
||||||
|
export default PrivateMenu
|
37
src/components/Private/PrivateMenuItem.tsx
Executable file
37
src/components/Private/PrivateMenuItem.tsx
Executable file
@ -0,0 +1,37 @@
|
|||||||
|
import { join } from 'path'
|
||||||
|
import { Menu, MenuItemProps } from 'antd'
|
||||||
|
import { memo, NamedExoticComponent } from 'react'
|
||||||
|
import { Link, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { isURLAvailable } from '@utils'
|
||||||
|
|
||||||
|
export type PrivateMenuItemProps = MenuItemProps & {
|
||||||
|
root: string
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PrivateMenuItemLinkProps = MenuItemProps & {
|
||||||
|
root?: string
|
||||||
|
path: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrivateMenuItemLink = memo<PrivateMenuItemLinkProps>(({ root = '', path, title, ...other }) => {
|
||||||
|
const location = useLocation()
|
||||||
|
return (
|
||||||
|
<PrivateMenuItem key={path} root={root} path={path} {...other}>
|
||||||
|
<Link to={join(root, path)}>{title}</Link>
|
||||||
|
</PrivateMenuItem>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PrivateMenuItem: NamedExoticComponent<PrivateMenuItemProps> & {
|
||||||
|
Link: NamedExoticComponent<PrivateMenuItemLinkProps>
|
||||||
|
} = Object.assign(memo<PrivateMenuItemProps>(({ root, path, ...other }) =>
|
||||||
|
<Menu.Item key={path} hidden={!isURLAvailable(join(root, path))} {...other} />
|
||||||
|
), {
|
||||||
|
Link: PrivateMenuItemLink
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export default PrivateMenuItem
|
30
src/components/Private/PrivateRoute.tsx
Executable file
30
src/components/Private/PrivateRoute.tsx
Executable file
@ -0,0 +1,30 @@
|
|||||||
|
import { join } from 'path'
|
||||||
|
import { memo, ReactNode } from 'react'
|
||||||
|
import { Navigate, Route, RouteProps } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { getUserId, isURLAvailable } from '@utils'
|
||||||
|
|
||||||
|
export type PrivateRouteProps = RouteProps & {
|
||||||
|
root?: string
|
||||||
|
path: string
|
||||||
|
children?: ReactNode
|
||||||
|
redirect?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultRedirect = (
|
||||||
|
<Navigate to={getUserId() ? '/access_denied' : '/login'} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export const PrivateRoute = memo<PrivateRouteProps>(({ root = '', path, children, redirect = defaultRedirect, ...other }) => {
|
||||||
|
const available = isURLAvailable(join(root, path))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Route
|
||||||
|
{...other}
|
||||||
|
path={path}
|
||||||
|
element={available ? children : redirect}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default PrivateRoute
|
67
src/components/Private/PrivateRoutes.tsx
Normal file
67
src/components/Private/PrivateRoutes.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { join } from 'path'
|
||||||
|
import { Navigate, Route, Routes, RoutesProps } from 'react-router-dom'
|
||||||
|
import { Children, cloneElement, memo, ReactElement, ReactNode, useCallback, useMemo } from 'react'
|
||||||
|
|
||||||
|
import { useRootPath } from '@asb/context'
|
||||||
|
import { getUserId, isURLAvailable } from '@utils'
|
||||||
|
|
||||||
|
export type PrivateRoutesProps = RoutesProps & {
|
||||||
|
root?: string
|
||||||
|
redirect?: ReactNode
|
||||||
|
elseRedirect?: string | string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDefaultRedirectPath = () => getUserId() ? '/access_denied' : '/login'
|
||||||
|
|
||||||
|
export const defaultRedirect = (
|
||||||
|
<Navigate to={getDefaultRedirectPath()} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export const PrivateRoutes = memo<PrivateRoutesProps>(({ root, elseRedirect, redirect = defaultRedirect, children }) => {
|
||||||
|
const rootContext = useRootPath()
|
||||||
|
const rootPath = useMemo(() => root ?? rootContext ?? '', [root, rootContext])
|
||||||
|
|
||||||
|
const toAbsolute = useCallback((path: string) => path.startsWith('/') ? path : join(rootPath, path), [rootPath])
|
||||||
|
|
||||||
|
const items = useMemo(() => Children.map(children, (child) => {
|
||||||
|
const element = child as ReactElement
|
||||||
|
let key = element.key?.toString()
|
||||||
|
if (!key) return <></>
|
||||||
|
key = key.slice(key.lastIndexOf('$') + 1).replaceAll('=2', ':')
|
||||||
|
// Ключ автоматический преобразуется в "(.+)\$ключ"
|
||||||
|
// Все ":" в ключе заменяются на "=2"
|
||||||
|
// TODO: улучшить метод нормализации ключа
|
||||||
|
const path = toAbsolute(key)
|
||||||
|
return (
|
||||||
|
<Route
|
||||||
|
key={key}
|
||||||
|
path={path}
|
||||||
|
element={isURLAvailable(path) ? cloneElement(element) : redirect}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}) ?? [], [children, redirect, toAbsolute])
|
||||||
|
|
||||||
|
const defaultRoute = useMemo(() => {
|
||||||
|
const routes: string[] = []
|
||||||
|
if (Array.isArray(elseRedirect))
|
||||||
|
routes.push(...elseRedirect)
|
||||||
|
else if(elseRedirect)
|
||||||
|
routes.push(elseRedirect)
|
||||||
|
|
||||||
|
routes.push(...items.map((elm) => elm?.props?.path))
|
||||||
|
|
||||||
|
const firstAvailableRoute = routes.find((path) => path && isURLAvailable(path))
|
||||||
|
return firstAvailableRoute ? toAbsolute(firstAvailableRoute) : getDefaultRedirectPath()
|
||||||
|
}, [items, elseRedirect, toAbsolute])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
{items}
|
||||||
|
<Route path={'/'} element={(
|
||||||
|
<Navigate to={defaultRoute} />
|
||||||
|
)}/>
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default PrivateRoutes
|
13
src/components/Private/index.ts
Executable file
13
src/components/Private/index.ts
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
export { PrivateRoute, defaultRedirect } from './PrivateRoute'
|
||||||
|
export { PrivateContent } from './PrivateContent' // TODO: Remove
|
||||||
|
export { PrivateMenuItem, PrivateMenuItemLink } from './PrivateMenuItem' // TODO: Remove
|
||||||
|
export { PrivateDefaultRoute } from './PrivateDefaultRoute'
|
||||||
|
export { PrivateMenu, PrivateMenuLink } from './PrivateMenu'
|
||||||
|
export { PrivateRoutes } from './PrivateRoutes'
|
||||||
|
|
||||||
|
export type { PrivateRouteProps } from './PrivateRoute'
|
||||||
|
export type { PrivateContentProps } from './PrivateContent' // TODO: Remove
|
||||||
|
export type { PrivateMenuItemProps, PrivateMenuItemLinkProps } from './PrivateMenuItem' // TODO: Remove
|
||||||
|
export type { PrivateDefaultRouteProps } from './PrivateDefaultRoute'
|
||||||
|
export type { PrivateMenuProps, PrivateMenuLinkProps } from './PrivateMenu'
|
||||||
|
export type { PrivateRoutesProps } from './PrivateRoutes'
|
@ -1,95 +0,0 @@
|
|||||||
import { ItemType } from 'antd/lib/menu/hooks/useItems'
|
|
||||||
import { Menu, MenuProps } from 'antd'
|
|
||||||
import { memo, ReactNode, useMemo } from 'react'
|
|
||||||
import { Link, useLocation } from 'react-router-dom'
|
|
||||||
import { join } from 'path'
|
|
||||||
|
|
||||||
import { hasPermission, Permission } from '@utils'
|
|
||||||
|
|
||||||
export type PrivateMenuItem = {
|
|
||||||
title: string
|
|
||||||
route: string
|
|
||||||
permissions: Permission | Permission[]
|
|
||||||
icon?: ReactNode
|
|
||||||
visible?: boolean
|
|
||||||
children?: PrivateMenuItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeItems = (items: PrivateMenuItem[], parentRoute: string, pathParser?: (path: string, parent: string) => string): ItemType[] => {
|
|
||||||
return items.map((item) => {
|
|
||||||
if (item.visible === false || !(item.visible === true || hasPermission(item.permissions))) return null
|
|
||||||
|
|
||||||
let route = item.route
|
|
||||||
if (pathParser)
|
|
||||||
route = pathParser(item.route, parentRoute)
|
|
||||||
else if (!item.route.startsWith('/') && parentRoute)
|
|
||||||
route = join(parentRoute, item.route)
|
|
||||||
|
|
||||||
const out: ItemType = {
|
|
||||||
key: route,
|
|
||||||
icon: item.icon,
|
|
||||||
title: item.title,
|
|
||||||
label: <Link to={route}>{item.title}</Link>,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.children && item.children.length > 0) {
|
|
||||||
return {
|
|
||||||
...out,
|
|
||||||
children: makeItems(item.children, route, pathParser)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}).filter(Boolean)
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeItemList = (items: PrivateMenuItem[], rootPath: string, variables: Record<string, number | string>): ItemType[] => {
|
|
||||||
const parser = (path: string, parent: string) => {
|
|
||||||
if (!path.startsWith('/'))
|
|
||||||
path = join(parent, path)
|
|
||||||
return Object.entries(variables).reduce((out, [key, value]) => out.replaceAll(`{${key}}`, String(value)), path)
|
|
||||||
}
|
|
||||||
|
|
||||||
return makeItems(items, rootPath, parser)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeItem = (
|
|
||||||
title: string,
|
|
||||||
route: string,
|
|
||||||
permissions: Permission | Permission[],
|
|
||||||
icon?: ReactNode,
|
|
||||||
children?: PrivateMenuItem[],
|
|
||||||
visible?: boolean
|
|
||||||
): PrivateMenuItem => ({
|
|
||||||
title,
|
|
||||||
route,
|
|
||||||
icon,
|
|
||||||
permissions,
|
|
||||||
children,
|
|
||||||
visible,
|
|
||||||
})
|
|
||||||
|
|
||||||
export type PrivateMenuProps = Omit<MenuProps, 'items'> & {
|
|
||||||
variables?: Record<string, number | string>
|
|
||||||
items: PrivateMenuItem[]
|
|
||||||
rootPath?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PrivateMenu = memo<PrivateMenuProps>(({ variables, items, rootPath = '/', ...other }) => {
|
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
const menuItems = useMemo(() => makeItemList(items, rootPath, variables || {}), [items, rootPath, variables])
|
|
||||||
|
|
||||||
const tabKeys = useMemo(() => {
|
|
||||||
const out = []
|
|
||||||
const rx = RegExp(/(?<!^)\//g)
|
|
||||||
let input = location.pathname
|
|
||||||
if (!input.endsWith('/')) input += '/'
|
|
||||||
let match: RegExpExecArray | null
|
|
||||||
while((match = rx.exec(input)) !== null)
|
|
||||||
out.push(input.slice(0, match.index))
|
|
||||||
return out
|
|
||||||
}, [location.pathname])
|
|
||||||
|
|
||||||
return <Menu items={menuItems} selectedKeys={tabKeys} {...other} />
|
|
||||||
})
|
|
@ -1,53 +1,26 @@
|
|||||||
import { Key, ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
import { makeColumn, ColumnProps, SorterMethod } from '.'
|
import { formatDate } from '@utils'
|
||||||
import { DatePickerWrapper, getObjectByDeepKey } from '..'
|
|
||||||
|
import makeColumn, { columnPropsOther } from '.'
|
||||||
|
import { DatePickerWrapper, makeDateSorter } from '..'
|
||||||
import { DatePickerWrapperProps } from '../DatePickerWrapper'
|
import { DatePickerWrapperProps } from '../DatePickerWrapper'
|
||||||
import { formatDate, isRawDate } from '@utils'
|
|
||||||
|
|
||||||
/**
|
export const makeDateColumn = (
|
||||||
* Фабрика методов сортировки столбцов для данных типа **Дата**
|
|
||||||
* @param key Ключ столбца
|
|
||||||
* @returns Метод сортировки
|
|
||||||
*/
|
|
||||||
export const makeDateSorter = <T extends unknown>(key: Key): SorterMethod<T> => (a, b) => {
|
|
||||||
const vA = a ? getObjectByDeepKey(key, a) : null
|
|
||||||
const vB = b ? getObjectByDeepKey(key, b) : null
|
|
||||||
|
|
||||||
if (!isRawDate(vA) || !isRawDate(vB)) return 0
|
|
||||||
if (!isRawDate(vA)) return 1
|
|
||||||
if (!isRawDate(vB)) return -1
|
|
||||||
|
|
||||||
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>(
|
|
||||||
title: ReactNode,
|
title: ReactNode,
|
||||||
key: string,
|
key: string,
|
||||||
utc?: boolean,
|
utc?: boolean,
|
||||||
format?: string,
|
format?: string,
|
||||||
other?: ColumnProps<T>,
|
other?: columnPropsOther,
|
||||||
pickerOther?: DatePickerWrapperProps,
|
pickerOther?: DatePickerWrapperProps,
|
||||||
) => makeColumn<T>(title, key, {
|
) => makeColumn(title, key, {
|
||||||
editable: true,
|
|
||||||
...other,
|
...other,
|
||||||
render: (date) => (
|
render: (date) => (
|
||||||
<div className={'text-align-r-container'}>
|
<div className={'text-align-r-container'}>
|
||||||
<span>{formatDate(date, utc, format) ?? '-'}</span>
|
<span>{formatDate(date, utc, format) ?? '-'}</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
sorter: makeDateSorter<T>(key),
|
sorter: makeDateSorter(key),
|
||||||
input: <DatePickerWrapper {...pickerOther} />,
|
input: <DatePickerWrapper {...pickerOther} />,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,27 +1,37 @@
|
|||||||
import { Key, ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import { Rule } from 'antd/lib/form'
|
import { Rule } from 'antd/lib/form'
|
||||||
import { ColumnType } from 'antd/lib/table'
|
import { ColumnProps } from 'antd/lib/table'
|
||||||
import { RenderedCell } from 'rc-table/lib/interface'
|
|
||||||
|
|
||||||
import { DataSet } from '../Table'
|
export { makeDateColumn } from './date'
|
||||||
import { OmitExtends } from '@utils/types'
|
export { makeTimeColumn } from './time'
|
||||||
|
export {
|
||||||
|
RegExpIsFloat,
|
||||||
|
makeNumericRender,
|
||||||
|
makeNumericColumn,
|
||||||
|
makeNumericColumnOptions,
|
||||||
|
makeNumericColumnPlanFact,
|
||||||
|
makeNumericStartEnd,
|
||||||
|
makeNumericMinMax,
|
||||||
|
makeNumericAvgRange,
|
||||||
|
} from './numeric'
|
||||||
|
export { makeColumnsPlanFact } from './plan_fact'
|
||||||
|
export { makeSelectColumn } from './select'
|
||||||
|
export { makeTagColumn, makeTagInput } from './tag'
|
||||||
|
export { makeFilterTextMatch, makeTextColumn } from './text'
|
||||||
|
export {
|
||||||
|
timezoneOptions,
|
||||||
|
TimezoneSelect,
|
||||||
|
makeTimezoneColumn,
|
||||||
|
makeTimezoneRenderer
|
||||||
|
} from './timezone'
|
||||||
|
|
||||||
export * from './date'
|
export type { TagInputProps } from './tag'
|
||||||
export * from './time'
|
|
||||||
export * from './numeric'
|
|
||||||
export * from './select'
|
|
||||||
export * from './tag'
|
|
||||||
export * from './text'
|
|
||||||
export * from './timezone'
|
|
||||||
|
|
||||||
export type DataType<T = any> = Record<string, T>
|
export type DataType<T = any> = Record<string, T>
|
||||||
export type RenderMethod<T = any, DT = DataSet<T>> = (value: T | undefined, dataset: DT, index: number) => ReactNode | RenderedCell<T> | undefined
|
export type RenderMethod<T = any> = (value: T, dataset?: DataType<T>, index?: number) => ReactNode
|
||||||
export type SorterMethod<T = any, DT = DataSet<T> | null | undefined> = (a: DT, b: DT) => number
|
export type SorterMethod<T = any> = (a?: DataType<T> | null, b?: DataType<T> | null) => number
|
||||||
export type FilterMethod<T = any, DT = DataSet<T>> = (value: string | number | T | undefined, record: DT) => boolean
|
|
||||||
|
|
||||||
export type FilterGenerator<T, DT = DataSet<T>> = (key: Key) => FilterMethod<T, DT>
|
export type columnPropsOther<T = any> = ColumnProps<DataType<T>> & {
|
||||||
|
|
||||||
export type ColumnProps<T = any> = OmitExtends<{
|
|
||||||
// редактируемая колонка
|
// редактируемая колонка
|
||||||
editable?: boolean
|
editable?: boolean
|
||||||
// react компонента редактора
|
// react компонента редактора
|
||||||
@ -35,16 +45,13 @@ export type ColumnProps<T = any> = OmitExtends<{
|
|||||||
// дефолтное значение при добавлении новой строки
|
// дефолтное значение при добавлении новой строки
|
||||||
initialValue?: string | number
|
initialValue?: string | number
|
||||||
|
|
||||||
onFilter?: FilterMethod<T>
|
|
||||||
sorter?: SorterMethod<T>
|
|
||||||
render?: RenderMethod<T>
|
render?: RenderMethod<T>
|
||||||
}, ColumnType<DataSet<T>>>
|
}
|
||||||
|
|
||||||
export const makeColumn = <T = any>(title: ReactNode, key: Key, other?: ColumnProps<T>) => ({
|
export const makeColumn = (title: ReactNode, key: string, other?: columnPropsOther) => ({
|
||||||
title: title,
|
title: title,
|
||||||
key: key,
|
key: key,
|
||||||
dataIndex: key,
|
dataIndex: key,
|
||||||
render: (value: T) => value,
|
|
||||||
...other,
|
...other,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,25 +1,20 @@
|
|||||||
|
import { ColumnFilterItem } from 'antd/lib/table/interface'
|
||||||
import { InputNumber } from 'antd'
|
import { InputNumber } from 'antd'
|
||||||
import { Key, ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
import makeColumn, { ColumnProps, FilterGenerator, makeGroupColumn, RenderMethod, SorterMethod } from '.'
|
import { makeNumericSorter } from '../sorters'
|
||||||
import { getObjectByDeepKey } from '../Table'
|
import makeColumn, { columnPropsOther, DataType, makeGroupColumn, RenderMethod } from '.'
|
||||||
|
|
||||||
export const RegExpIsFloat = /^[-+]?\d+\.?\d*$/
|
export const RegExpIsFloat = /^[-+]?\d+\.?\d*$/
|
||||||
|
|
||||||
export const makeNumericSorter = <T extends number = number>(key: Key): SorterMethod<T> => (a, b) => {
|
type FilterMethod<T> = (value: string | number | boolean, record: DataType<T>) => boolean
|
||||||
if (!a && !b) return 0
|
|
||||||
if (!a) return 1
|
|
||||||
if (!b) return -1
|
|
||||||
|
|
||||||
return Number(getObjectByDeepKey(key, a)) - Number(getObjectByDeepKey(key, b))
|
export const makeNumericRender = <T,>(fixed?: number): RenderMethod<T> => (value: T) => {
|
||||||
}
|
let val = '-'
|
||||||
|
if ((value ?? null) !== null && Number.isFinite(+value)) {
|
||||||
export const makeNumericRender = <T extends unknown>(fixed?: number, defaultValue: string = '-', precision: number = 5): RenderMethod<T> => (value) => {
|
|
||||||
let val = defaultValue
|
|
||||||
if (value !== undefined && value !== null && Number.isFinite(+value)) {
|
|
||||||
val = (fixed ?? null) !== null
|
val = (fixed ?? null) !== null
|
||||||
? (+value).toFixed(fixed)
|
? (+value).toFixed(fixed)
|
||||||
: (+value).toPrecision(precision)
|
: (+value).toPrecision(5)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -29,7 +24,7 @@ export const makeNumericRender = <T extends unknown>(fixed?: number, defaultValu
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeNumericColumnOptions = <T extends number>(fixed?: number, sorterKey?: string): ColumnProps<T> => ({
|
export const makeNumericColumnOptions = <T,>(fixed?: number, sorterKey?: string): columnPropsOther<T> => ({
|
||||||
editable: true,
|
editable: true,
|
||||||
initialValue: 0,
|
initialValue: 0,
|
||||||
width: 100,
|
width: 100,
|
||||||
@ -42,99 +37,75 @@ export const makeNumericColumnOptions = <T extends number>(fixed?: number, sorte
|
|||||||
render: makeNumericRender<T>(fixed),
|
render: makeNumericRender<T>(fixed),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const makeNumericColumn = <T extends number>(
|
export const makeNumericColumn = <T,>(
|
||||||
title: ReactNode,
|
title: ReactNode,
|
||||||
key: Key,
|
key: string,
|
||||||
|
filters?: ColumnFilterItem[],
|
||||||
|
filterDelegate?: (key: string | number) => FilterMethod<T>,
|
||||||
renderDelegate?: RenderMethod<T>,
|
renderDelegate?: RenderMethod<T>,
|
||||||
filterDelegate?: FilterGenerator<T>,
|
|
||||||
width?: string | number,
|
width?: string | number,
|
||||||
other?: ColumnProps<T>,
|
other?: columnPropsOther,
|
||||||
) => makeColumn(title, key, {
|
) => makeColumn(title, key, {
|
||||||
editable: true,
|
filters,
|
||||||
onFilter: filterDelegate ? filterDelegate(key) : undefined,
|
onFilter: filterDelegate ? filterDelegate(key) : undefined,
|
||||||
sorter: makeNumericSorter(key),
|
sorter: makeNumericSorter<T>(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
|
||||||
})
|
})
|
||||||
|
|
||||||
export const makeNumericColumnPlanFact = <T extends number>(
|
export const makeNumericColumnPlanFact = <T,>(
|
||||||
title: ReactNode,
|
title: ReactNode,
|
||||||
key: Key,
|
key: string,
|
||||||
|
filters?: ColumnFilterItem[],
|
||||||
|
filterDelegate?: (key: string | number) => FilterMethod<T>,
|
||||||
renderDelegate?: RenderMethod<T>,
|
renderDelegate?: RenderMethod<T>,
|
||||||
filterDelegate?: FilterGenerator<T>,
|
width?: string | number
|
||||||
width?: string | number,
|
|
||||||
other?: ColumnProps<T>,
|
|
||||||
) => {
|
|
||||||
return {
|
|
||||||
title,
|
|
||||||
children: [
|
|
||||||
makeNumericColumn<T>('План', `${key}.plan`, renderDelegate, filterDelegate, width, other),
|
|
||||||
makeNumericColumn<T>('Факт', `${key}.fact`, renderDelegate, filterDelegate, width, other),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Для значений типа план/факт появилась модель `PlanFactDto`, использование 2 полей с суффиксами неактуально
|
|
||||||
* @param title Заголовок столбца
|
|
||||||
* @param key Ключ столбца
|
|
||||||
* @param filters Список значений для фильтрации
|
|
||||||
* @param filterDelegate Метод фильтрации
|
|
||||||
* @param renderDelegate Render-метод отображения ячейки
|
|
||||||
* @param width Ширина столбца
|
|
||||||
* @param other Дополнительные опции
|
|
||||||
* @returns Объект-столбец для таблицы
|
|
||||||
*/
|
|
||||||
export const makeNumericColumnPlanFactOld = <T extends number>(
|
|
||||||
title: ReactNode,
|
|
||||||
key: Key,
|
|
||||||
renderDelegate?: RenderMethod<T>,
|
|
||||||
filterDelegate?: FilterGenerator<T>,
|
|
||||||
width?: string | number,
|
|
||||||
other?: ColumnProps<T>,
|
|
||||||
) => makeGroupColumn(title, [
|
) => makeGroupColumn(title, [
|
||||||
makeNumericColumn<T>('План', key + 'Plan', renderDelegate, filterDelegate, width, other),
|
makeNumericColumn<T>('п', key + 'Plan', filters, filterDelegate, renderDelegate, width),
|
||||||
makeNumericColumn<T>('Факт', key + 'Fact', renderDelegate, filterDelegate, width, other),
|
makeNumericColumn<T>('ф', key + 'Fact', filters, filterDelegate, renderDelegate, width),
|
||||||
])
|
])
|
||||||
|
|
||||||
export const makeNumericStartEnd = <T extends number>(
|
export const makeNumericStartEnd = <T,>(
|
||||||
title: ReactNode,
|
title: ReactNode,
|
||||||
key: Key,
|
key: string,
|
||||||
fixed: number,
|
fixed: number,
|
||||||
|
filters?: ColumnFilterItem[],
|
||||||
|
filterDelegate?: (key: string | number) => FilterMethod<T>,
|
||||||
renderDelegate?: RenderMethod<T>,
|
renderDelegate?: RenderMethod<T>,
|
||||||
filterDelegate?: FilterGenerator<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<T>(fixed, key + 'Start')),
|
||||||
makeNumericColumn<T>('конец', key + 'End', renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, key + 'End'))
|
makeNumericColumn<T>('конец', key + 'End', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions<T>(fixed, key + 'End'))
|
||||||
])
|
])
|
||||||
|
|
||||||
export const makeNumericMinMax = <T extends number>(
|
export const makeNumericMinMax = <T,>(
|
||||||
title: ReactNode,
|
title: ReactNode,
|
||||||
key: Key,
|
key: string,
|
||||||
fixed: number,
|
fixed: number,
|
||||||
|
filters?: ColumnFilterItem[],
|
||||||
|
filterDelegate?: (key: string | number) => FilterMethod<T>,
|
||||||
renderDelegate?: RenderMethod<T>,
|
renderDelegate?: RenderMethod<T>,
|
||||||
filterDelegate?: FilterGenerator<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<T>(fixed, key + 'Min')),
|
||||||
makeNumericColumn<T>('макс', key + 'Max', renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, key + 'Max')),
|
makeNumericColumn<T>('макс', key + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions<T>(fixed, key + 'Max')),
|
||||||
])
|
])
|
||||||
|
|
||||||
export const makeNumericAvgRange = <T extends number>(
|
export const makeNumericAvgRange = <T,>(
|
||||||
title: ReactNode,
|
title: ReactNode,
|
||||||
key: Key,
|
dataIndex: string,
|
||||||
fixed: number,
|
fixed: number,
|
||||||
renderDelegate?: RenderMethod<T>,
|
filters: ColumnFilterItem[],
|
||||||
filterDelegate?: FilterGenerator<T>,
|
filterDelegate: (key: string | number) => FilterMethod<T>,
|
||||||
width?: string | number,
|
renderDelegate: RenderMethod<T>,
|
||||||
|
width: string
|
||||||
) => makeGroupColumn(title, [
|
) => makeGroupColumn(title, [
|
||||||
makeNumericColumn<T>('мин', `${key}.min`, renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, `${key}.min`)),
|
makeNumericColumn<T>('мин', dataIndex + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions<T>(fixed, dataIndex + 'Min')),
|
||||||
makeNumericColumn<T>('сред', `${key}.avg`, renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, `${key}.avg`)),
|
makeNumericColumn<T>('сред', dataIndex + 'Avg', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions<T>(fixed, dataIndex + 'Avg')),
|
||||||
makeNumericColumn<T>('макс', `${key}.max`, renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, `${key}.max`)),
|
makeNumericColumn<T>('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions<T>(fixed, dataIndex + 'Max'))
|
||||||
])
|
])
|
||||||
|
|
||||||
export default makeNumericColumn
|
export default makeNumericColumn
|
||||||
|
38
src/components/Table/Columns/plan_fact.tsx
Executable file
38
src/components/Table/Columns/plan_fact.tsx
Executable file
@ -0,0 +1,38 @@
|
|||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
import { columnPropsOther, makeColumn } from '.'
|
||||||
|
|
||||||
|
export const makeColumnsPlanFact = (
|
||||||
|
title: string | ReactNode,
|
||||||
|
key: string | string[],
|
||||||
|
columsOther?: columnPropsOther | [columnPropsOther, columnPropsOther],
|
||||||
|
gruopOther?: any
|
||||||
|
) => {
|
||||||
|
let keyPlanLocal: string
|
||||||
|
let keyFactLocal: string
|
||||||
|
|
||||||
|
if (key instanceof Array) {
|
||||||
|
keyPlanLocal = key[0]
|
||||||
|
keyFactLocal = key[1]
|
||||||
|
} else {
|
||||||
|
keyPlanLocal = key + 'Plan'
|
||||||
|
keyFactLocal = key + 'Fact'
|
||||||
|
}
|
||||||
|
|
||||||
|
let columsOtherLocal : any[2]
|
||||||
|
if (columsOther instanceof Array)
|
||||||
|
columsOtherLocal = [columsOther[0], columsOther[1]]
|
||||||
|
else
|
||||||
|
columsOtherLocal = [columsOther, columsOther]
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: title,
|
||||||
|
...gruopOther,
|
||||||
|
children: [
|
||||||
|
makeColumn('план', keyPlanLocal, columsOtherLocal[0]),
|
||||||
|
makeColumn('факт', keyFactLocal, columsOtherLocal[1]),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default makeColumnsPlanFact
|
@ -1,31 +1,21 @@
|
|||||||
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 { ColumnProps, makeColumn } from '.'
|
import { columnPropsOther, makeColumn } from '.'
|
||||||
|
|
||||||
const findOption = <T extends DefaultOptionType>(value: any, options: T[] | undefined) =>
|
export const makeSelectColumn = <T extends unknown = string>(
|
||||||
options?.find((option) => String(option?.value) === String(value))
|
title: string,
|
||||||
|
dataIndex: string,
|
||||||
const SelectWrapper = ({ value, options, ...other }: SelectProps) => {
|
options: DefaultOptionType[],
|
||||||
const selectValue = useMemo(() => findOption(value, options)?.label, [value, options])
|
|
||||||
return <Select value={selectValue} options={options} {...other} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeSelectColumn = <T extends DefaultOptionType>(
|
|
||||||
title: ReactNode,
|
|
||||||
key: Key,
|
|
||||||
options: T[],
|
|
||||||
defaultValue?: T,
|
defaultValue?: T,
|
||||||
other?: ColumnProps<T>,
|
other?: columnPropsOther,
|
||||||
selectOther?: SelectProps<SelectValue>
|
selectOther?: SelectProps<SelectValue>
|
||||||
) => makeColumn(title, key, {
|
) => makeColumn(title, dataIndex, {
|
||||||
editable: true,
|
|
||||||
...other,
|
...other,
|
||||||
input: <SelectWrapper options={options} {...selectOther}/>,
|
input: <Select options={options} {...selectOther}/>,
|
||||||
render: (value, dataset, index) => {
|
render: (value) => {
|
||||||
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?.value) ?? item?.label ?? defaultValue ?? value ?? '--'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import { Select, SelectProps, Tag } from 'antd'
|
|||||||
|
|
||||||
import type { OmitExtends } from '@utils/types'
|
import type { OmitExtends } from '@utils/types'
|
||||||
|
|
||||||
import { ColumnProps, DataType, makeColumn } from '.'
|
import { columnPropsOther, DataType, makeColumn } from '.'
|
||||||
|
|
||||||
export type TagInputProps<T extends DataType> = OmitExtends<{
|
export type TagInputProps<T extends DataType> = OmitExtends<{
|
||||||
options: T[],
|
options: T[],
|
||||||
@ -59,15 +59,14 @@ export const makeTagColumn = <T extends DataType>(
|
|||||||
options: T[],
|
options: T[],
|
||||||
value_key: keyof DataType,
|
value_key: keyof DataType,
|
||||||
label_key: keyof DataType,
|
label_key: keyof DataType,
|
||||||
other?: ColumnProps,
|
other?: columnPropsOther,
|
||||||
tagOther?: TagInputProps<T>
|
tagOther?: TagInputProps<T>
|
||||||
) => {
|
) => {
|
||||||
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[]) => item?.map((elm: T) => <Tag key={elm[label_key]} color={'blue'}>{other?.render?.(elm) ?? elm[label_key]}</Tag>) ?? '-',
|
||||||
input: <InputComponent {...tagOther} options={options} />,
|
input: <InputComponent {...tagOther} options={options} />,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,8 @@
|
|||||||
import { Tooltip } from 'antd'
|
|
||||||
import { ColumnFilterItem } from 'antd/lib/table/interface'
|
import { ColumnFilterItem } from 'antd/lib/table/interface'
|
||||||
import { Key, ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
import { ColumnProps, makeColumn, DataType, RenderMethod, SorterMethod } from '.'
|
import makeColumn, { columnPropsOther, DataType, RenderMethod, SorterMethod } from '.'
|
||||||
import { getObjectByDeepKey } from '../Table'
|
import { makeStringSorter } from '../sorters'
|
||||||
|
|
||||||
export const makeStringSorter = <T extends string>(key: Key): SorterMethod<T> => (a, b) => {
|
|
||||||
const vA = a ? getObjectByDeepKey(key, a) : null
|
|
||||||
const vB = b ? getObjectByDeepKey(key, b) : null
|
|
||||||
|
|
||||||
if (!vA && !vB) return 0
|
|
||||||
if (!vA) return 1
|
|
||||||
if (!vB) return -1
|
|
||||||
|
|
||||||
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
|
||||||
@ -37,13 +13,12 @@ export const makeTextColumn = <T extends unknown = any>(
|
|||||||
filters?: ColumnFilterItem[],
|
filters?: ColumnFilterItem[],
|
||||||
sorter?: SorterMethod<T>,
|
sorter?: SorterMethod<T>,
|
||||||
render?: RenderMethod<T>,
|
render?: RenderMethod<T>,
|
||||||
other?: ColumnProps
|
other?: columnPropsOther
|
||||||
) => 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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,37 +1,25 @@
|
|||||||
import { Key, ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
import { makeColumn, ColumnProps, SorterMethod } from '.'
|
import { formatTime } from '@utils'
|
||||||
import { TimePickerWrapper, TimePickerWrapperProps, getObjectByDeepKey } from '..'
|
|
||||||
import { formatTime, timeToMoment } from '@utils'
|
|
||||||
import { TimeDto } from '@api'
|
|
||||||
|
|
||||||
export const makeTimeSorter = <T extends TimeDto>(key: Key): SorterMethod<T> => (a, b) => {
|
import { makeColumn, columnPropsOther } from '.'
|
||||||
const vA = a ? getObjectByDeepKey(key, a) : null
|
import { makeTimeSorter, TimePickerWrapper, TimePickerWrapperProps } from '..'
|
||||||
const vB = b? getObjectByDeepKey(key, b) : null
|
|
||||||
|
|
||||||
if (!vA && !vB) return 0
|
export const makeTimeColumn = (
|
||||||
if (!vA) return 1
|
|
||||||
if (!vB) return -1
|
|
||||||
|
|
||||||
return timeToMoment(vA).diff(timeToMoment(vB))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeTimeColumn = <T extends TimeDto>(
|
|
||||||
title: ReactNode,
|
title: ReactNode,
|
||||||
key: string,
|
key: string,
|
||||||
utc?: boolean,
|
utc?: boolean,
|
||||||
format?: string,
|
format?: string,
|
||||||
other?: ColumnProps,
|
other?: columnPropsOther,
|
||||||
pickerOther?: TimePickerWrapperProps,
|
pickerOther?: TimePickerWrapperProps,
|
||||||
) => makeColumn<T>(title, key, {
|
) => makeColumn(title, key, {
|
||||||
editable: true,
|
|
||||||
...other,
|
...other,
|
||||||
render: (time) => (
|
render: (time) => (
|
||||||
<div className={'text-align-r-container'}>
|
<div className={'text-align-r-container'}>
|
||||||
<span>{formatTime(time, utc, format) ?? '-'}</span>
|
<span>{formatTime(time, utc, format) ?? '-'}</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
sorter: makeTimeSorter<T>(key),
|
sorter: makeTimeSorter(key),
|
||||||
input: <TimePickerWrapper isUTC={utc} {...pickerOther} />,
|
input: <TimePickerWrapper isUTC={utc} {...pickerOther} />,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { Key, memo, ReactNode, useCallback, useEffect, useState } from 'react'
|
import { memo, ReactNode, useCallback, useEffect, useState } from 'react'
|
||||||
import { Select, SelectProps } from 'antd'
|
import { Select, SelectProps } from 'antd'
|
||||||
|
|
||||||
import { findTimezoneId, rawTimezones, TimezoneId } from '@utils'
|
import { findTimezoneId, rawTimezones, TimezoneId } from '@utils'
|
||||||
import type { OmitExtends } from '@utils/types'
|
import type { OmitExtends } from '@utils/types'
|
||||||
import { SimpleTimezoneDto } from '@api'
|
import { SimpleTimezoneDto } from '@api'
|
||||||
|
|
||||||
import { ColumnProps, makeColumn } from '.'
|
import { columnPropsOther, makeColumn } from '.'
|
||||||
|
|
||||||
const makeTimezoneLabel = (id?: string | null, hours?: number) =>
|
const makeTimezoneLabel = (id?: string | null, hours?: number) =>
|
||||||
`UTC${hours && hours > 0 ? '+':''}${hours ? ('0' + hours).slice(-2) : '~??'} :: ${id ?? 'Неизвестно'}`
|
`UTC${hours && hours > 0 ? '+':''}${hours ? ('0' + hours).slice(-2) : '~??'} :: ${id ?? 'Неизвестно'}`
|
||||||
@ -18,7 +18,7 @@ export const timezoneOptions = Object
|
|||||||
value: id,
|
value: id,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
export const makeTimezoneRender = () => (timezone?: SimpleTimezoneDto) => {
|
export const makeTimezoneRenderer = () => (timezone?: SimpleTimezoneDto) => {
|
||||||
if (!timezone) return 'UTC~?? :: Неизвестно'
|
if (!timezone) return 'UTC~?? :: Неизвестно'
|
||||||
const { hours, timezoneId } = timezone
|
const { hours, timezoneId } = timezone
|
||||||
return makeTimezoneLabel(timezoneId, hours)
|
return makeTimezoneLabel(timezoneId, hours)
|
||||||
@ -46,17 +46,17 @@ export const TimezoneSelect = memo<TimezoneSelectProps>(({ onChange, value, defa
|
|||||||
return (<Select {...other} onChange={onValueChanged} value={id} defaultValue={defaultTimezone} />)
|
return (<Select {...other} onChange={onValueChanged} value={id} defaultValue={defaultTimezone} />)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const makeTimezoneColumn = <T extends SimpleTimezoneDto>(
|
export const makeTimezoneColumn = (
|
||||||
title: ReactNode = 'Зона',
|
title: ReactNode = 'Зона',
|
||||||
key: Key = 'timezone',
|
key: string = 'timezone',
|
||||||
defaultValue?: T,
|
defaultValue?: SimpleTimezoneDto,
|
||||||
allowClear: boolean = true,
|
allowClear: boolean = true,
|
||||||
other?: ColumnProps<T>,
|
other?: columnPropsOther,
|
||||||
selectOther?: TimezoneSelectProps
|
selectOther?: TimezoneSelectProps
|
||||||
) => makeColumn(title, key, {
|
) => makeColumn(title, key, {
|
||||||
width: 100,
|
width: 100,
|
||||||
editable: true,
|
editable: true,
|
||||||
render: makeTimezoneRender(),
|
render: makeTimezoneRenderer(),
|
||||||
input: (
|
input: (
|
||||||
<TimezoneSelect
|
<TimezoneSelect
|
||||||
key={key}
|
key={key}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,21 +9,10 @@ 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Подготавливает значения к передаче в селектор
|
|
||||||
*
|
|
||||||
* @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 [
|
||||||
@ -32,16 +21,16 @@ const normalizeDates = (value?: RangeValue<Moment>, isUTC?: boolean): RangeValue
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DateRangeWrapper = memo<DateRangeWrapperProps>(({ value, isUTC, allowClear, ...other }) => (
|
export const DateRangeWrapper = memo<DateRangeWrapperProps>(({ value, isUTC, ...other }) => (
|
||||||
<RangePicker
|
<RangePicker
|
||||||
showTime
|
showTime
|
||||||
allowClear={allowClear}
|
allowClear={false}
|
||||||
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}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
@ -1,20 +1,19 @@
|
|||||||
import { Key, memo, ReactNode, useMemo } from 'react'
|
import { memo, ReactNode } from 'react'
|
||||||
import { Rule } from 'rc-field-form/lib/interface'
|
|
||||||
import { Form, Input } from 'antd'
|
import { Form, Input } from 'antd'
|
||||||
|
import { NamePath, Rule } from 'rc-field-form/lib/interface'
|
||||||
|
|
||||||
export type EditableCellProps = React.DetailedHTMLProps<React.TdHTMLAttributes<HTMLTableDataCellElement>, HTMLTableDataCellElement> & {
|
type EditableCellProps = React.DetailedHTMLProps<React.TdHTMLAttributes<HTMLTableDataCellElement>, HTMLTableDataCellElement> & {
|
||||||
editing?: boolean
|
editing?: boolean
|
||||||
dataIndex?: Key
|
dataIndex?: NamePath
|
||||||
input?: ReactNode
|
input?: ReactNode
|
||||||
isRequired?: boolean
|
isRequired?: boolean
|
||||||
|
title: string
|
||||||
formItemClass?: string
|
formItemClass?: string
|
||||||
formItemRules?: Rule[]
|
formItemRules?: Rule[]
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
initialValue: any
|
initialValue: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const itemStyle = { margin: 0 }
|
|
||||||
|
|
||||||
export const EditableCell = memo<EditableCellProps>(({
|
export const EditableCell = memo<EditableCellProps>(({
|
||||||
editing,
|
editing,
|
||||||
dataIndex,
|
dataIndex,
|
||||||
@ -25,30 +24,21 @@ export const EditableCell = memo<EditableCellProps>(({
|
|||||||
children,
|
children,
|
||||||
initialValue,
|
initialValue,
|
||||||
...other
|
...other
|
||||||
}) => {
|
}) => (
|
||||||
const rules = useMemo(() => formItemRules || [{
|
<td style={editing ? { padding: 0 } : undefined} {...other}>
|
||||||
|
{!editing ? children : (
|
||||||
|
<Form.Item
|
||||||
|
name={dataIndex}
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
className={formItemClass}
|
||||||
|
rules={formItemRules ?? [{
|
||||||
required: isRequired,
|
required: isRequired,
|
||||||
message: `Это обязательное поле!`,
|
message: `Это обязательное поле!`,
|
||||||
}], [formItemRules, isRequired])
|
}]}
|
||||||
|
|
||||||
const name = useMemo(() => dataIndex ? String(dataIndex).split('.') : undefined, [dataIndex])
|
|
||||||
const tdStyle = useMemo(() => editing ? { padding: 0 } : undefined, [editing])
|
|
||||||
|
|
||||||
const edititngItem = useMemo(() => (
|
|
||||||
<Form.Item
|
|
||||||
name={name}
|
|
||||||
style={itemStyle}
|
|
||||||
className={formItemClass}
|
|
||||||
rules={rules}
|
|
||||||
initialValue={initialValue}
|
initialValue={initialValue}
|
||||||
>
|
>
|
||||||
{input ?? <Input/>}
|
{input ?? <Input/>}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
), [name, rules, formItemClass, initialValue, input])
|
)}
|
||||||
|
|
||||||
return (
|
|
||||||
<td style={tdStyle} {...other}>
|
|
||||||
{editing ? edititngItem : children}
|
|
||||||
</td>
|
</td>
|
||||||
)
|
))
|
||||||
})
|
|
||||||
|
180
src/components/Table/EditableTable.jsx → src/components/Table/EditableTable.tsx
Executable file → Normal file
180
src/components/Table/EditableTable.jsx → src/components/Table/EditableTable.tsx
Executable file → Normal file
@ -1,39 +1,72 @@
|
|||||||
import { memo, useCallback, useState, useEffect, useMemo } from 'react'
|
import { memo, useCallback, useState, useEffect, useMemo, Dispatch, SetStateAction, ReactNode } from 'react'
|
||||||
import { Form, Button, Popconfirm } from 'antd'
|
import { Form, Button, Popconfirm, TableProps } from 'antd'
|
||||||
import { EditOutlined, SaveOutlined, PlusOutlined, CloseCircleOutlined, DeleteOutlined } from '@ant-design/icons'
|
import { EditOutlined, SaveOutlined, PlusOutlined, CloseCircleOutlined, DeleteOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { hasPermission } from '@utils'
|
import { hasPermission, isDev } from '@utils'
|
||||||
|
|
||||||
import { Table } from '.'
|
import { Table, TableColumns } from '.'
|
||||||
import { EditableCell } from './EditableCell'
|
import { EditableCell } from './EditableCell'
|
||||||
|
import { ColumnsType } from 'antd/lib/table'
|
||||||
|
import { ArrayElement } from '@asb/utils/types'
|
||||||
|
|
||||||
const newRowKeyValue = 'newRow'
|
const newRowKeyValue = 'newRow'
|
||||||
|
|
||||||
const actions = {
|
type CrudObject = {
|
||||||
|
id?: string | number
|
||||||
|
key?: string | number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CrudService<T extends CrudObject> {
|
||||||
|
insert: (((data: T) => Promise<number>) | ((idWell: number, data: T) => Promise<number>))
|
||||||
|
insertRange: (((data: T[]) => Promise<number>) | ((idWell: number, data: T[]) => Promise<number>))
|
||||||
|
update: (((data: T) => Promise<number> | ((idWell: number, data: T) => Promise<number>) | ((idWell: number, id: number, data: T) => Promise<number>)))
|
||||||
|
delete: (((id: number) => Promise<number>) | ((idWell: number, id: number) => Promise<number>))
|
||||||
|
new(...args: any[]): any
|
||||||
|
}
|
||||||
|
|
||||||
|
type TableAction<T extends CrudObject> = Record<keyof CrudService<T>, (data: T, idWell?: number, idRecord?: number) => any[]>
|
||||||
|
|
||||||
|
const makeActions = <T extends CrudObject>(): TableAction<T> => ({
|
||||||
insert: (data, idWell) => [idWell, data],
|
insert: (data, idWell) => [idWell, data],
|
||||||
insertRange: (data, idWell) => [idWell, [data].flat(1)],
|
insertRange: (data, idWell) => [idWell, [data].flat(1)],
|
||||||
update: (data, idWell, idRecord) => [idWell, idRecord && data.id, data],
|
update: (data, idWell, idRecord) => [idWell, idRecord && data.id, data],
|
||||||
delete: (data, idWell) => [idWell, data.id],
|
delete: (data, idWell) => [idWell, data.id],
|
||||||
|
})
|
||||||
|
|
||||||
|
type TableActionProps<T extends CrudObject, S extends CrudService<T>> = {
|
||||||
|
service?: S
|
||||||
|
permission?: string | string[]
|
||||||
|
action?: keyof CrudService<T>
|
||||||
|
actionName?: string
|
||||||
|
recordParser?: (record: T) => T
|
||||||
|
idWell?: number
|
||||||
|
idRecord?: number
|
||||||
|
setLoader?: Dispatch<SetStateAction<boolean>>
|
||||||
|
errorMsg?: string
|
||||||
|
onComplete?: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeTableAction = ({
|
export const makeTableAction = <T extends CrudObject, S extends CrudService<T>>({
|
||||||
service,
|
service,
|
||||||
permission,
|
permission,
|
||||||
action,
|
action,
|
||||||
actionName,
|
actionName,
|
||||||
recordParser,
|
recordParser,
|
||||||
idWell,
|
idWell,
|
||||||
idRecord = false,
|
idRecord,
|
||||||
setLoader,
|
setLoader,
|
||||||
errorMsg = 'Не удалось выполнить операцию',
|
errorMsg = 'Не удалось выполнить операцию',
|
||||||
onComplete,
|
onComplete,
|
||||||
}) => hasPermission(permission) && service && action && (
|
}: TableActionProps<T, S>) => {
|
||||||
(record) => invokeWebApiWrapperAsync(
|
if (!hasPermission(permission) || !service || !action) return async (record: T) => {}
|
||||||
|
const actions = makeActions<T>()
|
||||||
|
return async (record: T) => await invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
const data = recordParser?.(record) ?? record
|
const data = recordParser?.(record) ?? record
|
||||||
const params = actions[action]?.(data, idWell, idRecord).filter(Boolean)
|
const params = actions[action]?.(data, idWell, idRecord).filter(Boolean)
|
||||||
if (params?.length > 0)
|
if (params?.length > 0)
|
||||||
|
//@ts-ignore
|
||||||
await service[action](...params)
|
await service[action](...params)
|
||||||
await onComplete?.()
|
await onComplete?.()
|
||||||
},
|
},
|
||||||
@ -41,25 +74,34 @@ export const makeTableAction = ({
|
|||||||
errorMsg,
|
errorMsg,
|
||||||
{ actionName }
|
{ actionName }
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
export const tryAddKeys = (items) => {
|
export const tryAddKeys = <T extends object & Record<string, any>>(items?: T[]): T[] => {
|
||||||
if (!items?.length || !items[0])
|
if (!items?.length || !items[0]) return items ?? []
|
||||||
return []
|
if (items[0].key) return items
|
||||||
if (items[0].key)
|
|
||||||
return items
|
|
||||||
return items.map((item, index) => ({...item, key: item.key ?? item.id ?? index }))
|
return items.map((item, index) => ({...item, key: item.key ?? item.id ?? index }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditableTableComponents = { body: { cell: EditableCell }}
|
const EditableTableComponents = { body: { cell: EditableCell }}
|
||||||
|
|
||||||
|
export type EditableTableProps<T extends CrudObject, S extends CrudService<T>> = TableProps<T> & {
|
||||||
|
columns: ColumnsType<T> & TableColumns<T>
|
||||||
|
dataSource?: T[]
|
||||||
|
onChange?: ((data: T[]) => Promise<void>)
|
||||||
|
onRowAdd?: TableActionProps<T, S> | ((record: T) => Promise<void>)
|
||||||
|
onRowEdit?: TableActionProps<T, S> | ((record: T) => Promise<void>)
|
||||||
|
onRowDelete?: TableActionProps<T, S> | ((record: T) => Promise<void>)
|
||||||
|
additionalButtons?: (record: T, editingKey: string | number) => ReactNode
|
||||||
|
buttonsWidth
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param onChange - Метод вызывается со всем dataSource с измененными элементами после любого действия
|
* @param onChange - Метод вызывается со всем dataSource с измененными элементами после любого действия
|
||||||
* @param onRowAdd - Метод вызывается с новой добавленной записью. Если метод не определен, то кнопка добавления строки не показывается
|
* @param onRowAdd - Метод вызывается с новой добавленной записью. Если метод не определен, то кнопка добавления строки не показывается
|
||||||
* @param onRowEdit - Метод вызывается с новой отредактированной записью. Если метод не поределен, то кнопка редактирования строки не показывается
|
* @param onRowEdit - Метод вызывается с новой отредактированной записью. Если метод не поределен, то кнопка редактирования строки не показывается
|
||||||
* @param onRowDelete - Метод вызывается с удаленной записью. Если метод не поределен, то кнопка удаления строки не показывается
|
* @param onRowDelete - Метод вызывается с удаленной записью. Если метод не поределен, то кнопка удаления строки не показывается
|
||||||
*/
|
*/
|
||||||
export const EditableTable = memo(({
|
const _EditableTable = <T extends CrudObject, S extends CrudService<T>>({
|
||||||
columns,
|
columns,
|
||||||
dataSource,
|
dataSource,
|
||||||
onChange,
|
onChange,
|
||||||
@ -69,47 +111,55 @@ export const EditableTable = memo(({
|
|||||||
additionalButtons,
|
additionalButtons,
|
||||||
buttonsWidth,
|
buttonsWidth,
|
||||||
...otherTableProps
|
...otherTableProps
|
||||||
}) => {
|
}: EditableTableProps<T, S>) => {
|
||||||
|
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
const [data, setData] = useState(tryAddKeys(dataSource))
|
const [data, setData] = useState<T[]>(tryAddKeys(dataSource))
|
||||||
const [editingKey, setEditingKey] = useState('')
|
const [editingKey, setEditingKey] = useState<string | number>('')
|
||||||
|
|
||||||
const onAdd = useMemo(() => onRowAdd && typeof onRowAdd !== 'function' ? makeTableAction(onRowAdd) : onRowAdd, [onRowAdd])
|
const onAdd = useMemo(() => onRowAdd && typeof onRowAdd !== 'function' ? makeTableAction(onRowAdd) : onRowAdd, [onRowAdd])
|
||||||
const onEdit = useMemo(() => onRowEdit && typeof onRowEdit !== 'function' ? makeTableAction(onRowEdit) : onRowEdit, [onRowEdit])
|
const onEdit = useMemo(() => onRowEdit && typeof onRowEdit !== 'function' ? makeTableAction(onRowEdit) : onRowEdit, [onRowEdit])
|
||||||
const onDelete = useMemo(() => onRowDelete && typeof onRowDelete !== 'function' ? makeTableAction(onRowDelete) : onRowDelete, [onRowDelete])
|
const onDelete = useMemo(() => onRowDelete && typeof onRowDelete !== 'function' ? makeTableAction(onRowDelete) : onRowDelete, [onRowDelete])
|
||||||
|
|
||||||
const isEditing = useCallback((record) => record?.key === editingKey, [editingKey])
|
const isEditing = useCallback((record: T) => record?.key === editingKey, [editingKey])
|
||||||
|
|
||||||
const edit = useCallback((record) => {
|
const edit = useCallback((record: T) => {
|
||||||
|
if (!record.key) return
|
||||||
form.setFieldsValue({...record})
|
form.setFieldsValue({...record})
|
||||||
setEditingKey(record.key)
|
setEditingKey(record.key)
|
||||||
}, [form])
|
}, [form])
|
||||||
|
|
||||||
const cancel = useCallback(() => {
|
const cancel = useCallback(() => {
|
||||||
if (editingKey === newRowKeyValue) {
|
setEditingKey((prev) => {
|
||||||
const newData = [...data]
|
if (prev === newRowKeyValue) {
|
||||||
|
setData((prev) => {
|
||||||
|
const newData = [...prev]
|
||||||
const index = newData.findIndex((item) => newRowKeyValue === item.key)
|
const index = newData.findIndex((item) => newRowKeyValue === item.key)
|
||||||
newData.splice(index, 1)
|
newData.splice(index, 1)
|
||||||
setData(newData)
|
return newData
|
||||||
|
})
|
||||||
}
|
}
|
||||||
setEditingKey('')
|
|
||||||
}, [data, editingKey])
|
return ''
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
const addNewRow = useCallback(async () => {
|
const addNewRow = useCallback(async () => {
|
||||||
let newRow = {
|
const newRow: any = { key: newRowKeyValue } // TODO: Исправить тип
|
||||||
...form.initialValues,
|
setData((prevData) => [newRow, ...prevData])
|
||||||
key:newRowKeyValue
|
edit(newRow)
|
||||||
|
}, [edit])
|
||||||
|
|
||||||
|
const save = useCallback(async (record: T) => {
|
||||||
|
let row
|
||||||
|
try {
|
||||||
|
row = await form.validateFields()
|
||||||
|
} catch (errInfo) {
|
||||||
|
if (isDev())
|
||||||
|
console.warn('Validate Failed:', errInfo)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newData = [newRow, ...data]
|
|
||||||
setData(newData)
|
|
||||||
edit(newRow)
|
|
||||||
}, [data, edit, form.initialValues])
|
|
||||||
|
|
||||||
const save = useCallback(async (record) => {
|
|
||||||
try {
|
|
||||||
const row = await form.validateFields()
|
|
||||||
const newData = [...data]
|
const newData = [...data]
|
||||||
const index = newData.findIndex((item) => record.key === item.key)
|
const index = newData.findIndex((item) => record.key === item.key)
|
||||||
const item = newData[index]
|
const item = newData[index]
|
||||||
@ -118,47 +168,35 @@ export const EditableTable = memo(({
|
|||||||
newData.splice(index, 1, newItem)
|
newData.splice(index, 1, newItem)
|
||||||
|
|
||||||
if (item.key === newRowKeyValue)
|
if (item.key === newRowKeyValue)
|
||||||
item.key = newRowKeyValue + newData.length
|
item.key += newData.length
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (editingKey === newRowKeyValue)
|
||||||
|
await onAdd?.(newItem)
|
||||||
|
else
|
||||||
|
await onEdit?.(newItem)
|
||||||
|
await onChange?.(newData)
|
||||||
|
} catch (err) {
|
||||||
|
if (isDev())
|
||||||
|
console.warn(err)
|
||||||
|
}
|
||||||
|
|
||||||
const isAdding = editingKey === newRowKeyValue
|
|
||||||
setEditingKey('')
|
setEditingKey('')
|
||||||
setData(newData)
|
setData(newData)
|
||||||
|
|
||||||
if (isAdding)
|
|
||||||
try {
|
|
||||||
onAdd(newItem)
|
|
||||||
} catch (err) {
|
|
||||||
console.log('callback onRowAdd fault:', err)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
try {
|
|
||||||
onEdit(newItem)
|
|
||||||
} catch (err) {
|
|
||||||
console.log('callback onRowEdit fault:', err)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
onChange?.(newData)
|
|
||||||
} catch (err) {
|
|
||||||
console.log('callback onChange fault:', err)
|
|
||||||
}
|
|
||||||
} catch (errInfo) {
|
|
||||||
console.log('Validate Failed:', errInfo)
|
|
||||||
}
|
|
||||||
}, [data, editingKey, form, onChange, onAdd, onEdit])
|
}, [data, editingKey, form, onChange, onAdd, onEdit])
|
||||||
|
|
||||||
const deleteRow = useCallback((record) => {
|
const deleteRow = useCallback((record: T) => {
|
||||||
const newData = [...data]
|
const newData = [...data]
|
||||||
const index = newData.findIndex((item) => record.key === item.key)
|
const index = newData.findIndex((item) => record.key === item.key)
|
||||||
|
|
||||||
newData.splice(index, 1)
|
newData.splice(index, 1)
|
||||||
setData(newData)
|
setData(newData)
|
||||||
|
|
||||||
onDelete(record)
|
onDelete?.(record)
|
||||||
onChange?.(newData)
|
onChange?.(newData)
|
||||||
}, [data, onChange, onDelete])
|
}, [data, onChange, onDelete])
|
||||||
|
|
||||||
const handleColumn = useCallback((col) => {
|
const handleColumn = useCallback((col: any) => { // TODO: Исправить тип
|
||||||
if (col.children)
|
if (col.children)
|
||||||
col.children = col.children.map(handleColumn)
|
col.children = col.children.map(handleColumn)
|
||||||
|
|
||||||
@ -167,7 +205,7 @@ export const EditableTable = memo(({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...col,
|
...col,
|
||||||
onCell: (record) => ({
|
onCell: (record: T) => ({
|
||||||
...col.onCell?.(record),
|
...col.onCell?.(record),
|
||||||
editing: isEditing(record),
|
editing: isEditing(record),
|
||||||
record,
|
record,
|
||||||
@ -194,7 +232,7 @@ export const EditableTable = memo(({
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
dataIndex: 'operation',
|
dataIndex: 'operation',
|
||||||
render: (_, record) => isEditing(record) ? (
|
render: (_: any, record: T) => isEditing(record) ? (
|
||||||
<span>
|
<span>
|
||||||
<Button onClick={() => save(record)} icon={<SaveOutlined/>}/>
|
<Button onClick={() => save(record)} icon={<SaveOutlined/>}/>
|
||||||
<Button onClick={cancel} icon={<CloseCircleOutlined/>}/>
|
<Button onClick={cancel} icon={<CloseCircleOutlined/>}/>
|
||||||
@ -219,13 +257,13 @@ export const EditableTable = memo(({
|
|||||||
),
|
),
|
||||||
}), [onAdd, onEdit, onDelete, isEditing, editingKey, save, cancel, edit, deleteRow])
|
}), [onAdd, onEdit, onDelete, isEditing, editingKey, save, cancel, edit, deleteRow])
|
||||||
|
|
||||||
const mergedColumns = useMemo(() => [...columns.map(handleColumn), operationColumn], [columns, handleColumn, operationColumn])
|
const mergedColumns: TableColumns<T> = useMemo(() => [...columns.map(handleColumn), operationColumn], [columns, handleColumn, operationColumn])
|
||||||
|
|
||||||
useEffect(() => setData(tryAddKeys(dataSource)), [dataSource])
|
useEffect(() => setData(tryAddKeys(dataSource)), [dataSource])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form form={form}>
|
<Form form={form}>
|
||||||
<Table
|
<Table<T>
|
||||||
components={EditableTableComponents}
|
components={EditableTableComponents}
|
||||||
columns={mergedColumns}
|
columns={mergedColumns}
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
@ -233,4 +271,8 @@ export const EditableTable = memo(({
|
|||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
export const EditableTable = memo(_EditableTable) as typeof _EditableTable
|
||||||
|
|
||||||
|
export default EditableTable
|
@ -1,96 +1,74 @@
|
|||||||
import { Key, memo, useCallback, useEffect, useState } from 'react'
|
import { memo, ReactNode, 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 { NamePath } from 'antd/lib/form/interface'
|
||||||
|
|
||||||
import { RenderMethod } from './Columns'
|
|
||||||
import { tryAddKeys } from './EditableTable'
|
|
||||||
import TableSettingsChanger from './TableSettingsChanger'
|
|
||||||
import type { OmitExtends } from '@utils/types'
|
import type { OmitExtends } from '@utils/types'
|
||||||
import { applyTableSettings, getTableSettings, setTableSettings, TableColumnSettings, TableSettings } from '@utils'
|
import { applyTableSettings, getTableSettings, setTableSettings, TableColumnSettings, TableSettings } from '@utils'
|
||||||
|
|
||||||
|
import TableSettingsChanger from './TableSettingsChanger'
|
||||||
|
import { DataType, RenderMethod } from './Columns'
|
||||||
|
import { tryAddKeys } from './EditableTable'
|
||||||
|
|
||||||
import '@styles/index.css'
|
import '@styles/index.css'
|
||||||
|
|
||||||
export type BaseTableColumn<T> = ColumnGroupType<T> | ColumnType<T>
|
export type BaseTableColumn<T> = ColumnGroupType<T> | ColumnType<T>
|
||||||
export type TableColumn<T> = OmitExtends<BaseTableColumn<T>, TableColumnSettings>
|
export type TableColumns<T> = (OmitExtends<Omit<BaseTableColumn<T>, 'key' | 'render'>, TableColumnSettings> & {
|
||||||
export type TableColumns<T> = TableColumn<T>[]
|
key: NamePath
|
||||||
|
render: RenderMethod<T | null>
|
||||||
|
})[]
|
||||||
|
|
||||||
export type TableProps<T> = RawTableProps<T> & {
|
type MergedTableColumns<T> = (OmitExtends<Omit<BaseTableColumn<T>, 'render'>, TableColumnSettings> & {
|
||||||
/** Массив колонок таблицы с настройками (описаны в `TableColumnSettings`) */
|
render: RenderMethod<T | null>
|
||||||
columns: TableColumn<T>[]
|
})[]
|
||||||
/** Название таблицы для сохранения настроек */
|
|
||||||
|
export type TableContainer<T> = Omit<TableProps<T>, 'columns' | 'dataSource'> & {
|
||||||
|
columns: TableColumns<T>
|
||||||
tableName?: string
|
tableName?: string
|
||||||
/** Отображать ли кнопку настроек */
|
|
||||||
showSettingsChanger?: boolean
|
showSettingsChanger?: boolean
|
||||||
|
dataSource: DataType<T>[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataSet<T, D = any> {
|
export const keyToString = (key: NamePath): string => Array.isArray(key) ? key.join('.') : String(key)
|
||||||
[k: Key]: DataSet<T, D> | T | D
|
|
||||||
|
const defaultRender = <T,>(data: T): ReactNode => <>{data}</>
|
||||||
|
|
||||||
|
const getValueFromKey = <T,>(key: NamePath, record: DataType<T>): T | null => {
|
||||||
|
if (!key) return null
|
||||||
|
if (!Array.isArray(key))
|
||||||
|
return record[String(key)]
|
||||||
|
if (key.length <= 0) return null
|
||||||
|
const child = record[key[0]]
|
||||||
|
if (key.length === 1) return child
|
||||||
|
if (typeof child !== 'object') return null
|
||||||
|
return getValueFromKey(key.slice(1), child as DataType<T>)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const columnRenderWrapper = <T,>(key: NamePath, render?: RenderMethod<T | null>): RenderMethod<T | null> => (_: T | null, record?: DataType<T | null>, index?: number) => {
|
||||||
* Получить значение из объекта по составному ключу
|
if (!record) return '-'
|
||||||
*
|
if (render)
|
||||||
* Составной ключ имеет вид: `<поле 1>[.<поле 2>...]`
|
return render(getValueFromKey(key, record), record, index)
|
||||||
*
|
return defaultRender(getValueFromKey(key, record))
|
||||||
* @param key Составной ключ
|
|
||||||
* @param data Объект из которого будет полученно значение
|
|
||||||
* @returns Значение, найденное по ключу, либо `undefined`
|
|
||||||
*/
|
|
||||||
export const getObjectByDeepKey = <T,>(key: Key | undefined, data: DataSet<T>): T | undefined => {
|
|
||||||
if (!key) return undefined
|
|
||||||
const parts = String(key).split('.')
|
|
||||||
let out = data
|
|
||||||
for (let i = 0; i < parts.length && out; i++) {
|
|
||||||
const key = parts[i]
|
|
||||||
if (!(key in out)) return undefined // Если ключ не найдем, считаем значение null
|
|
||||||
out = out[key] as DataSet<T> // Углубляемся внутрь объекта
|
|
||||||
}
|
|
||||||
return out as T
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const _Table = <T extends Record<string, any>>({ columns, dataSource, tableName, showSettingsChanger, ...other }: TableContainer<T>) => {
|
||||||
* Фабрика обёрток render-функций ячеек с поддержкой составных ключей
|
const [newColumns, setNewColumns] = useState<MergedTableColumns<T>>([])
|
||||||
* @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> =>
|
|
||||||
(_: any, dataset: T, index: number) => {
|
|
||||||
const renderFunc: RenderMethod<T, T> = typeof render === 'function' ? render : (record) => String(record)
|
|
||||||
return renderFunc(getObjectByDeepKey<T>(key, dataset), dataset, index)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Применяет необходимые обёртки ко всем столбцам таблицы
|
|
||||||
* @param columns Исходные столбцы
|
|
||||||
* @returns Обёрнутые столбцы
|
|
||||||
*/
|
|
||||||
const applyColumnWrappers = <T extends DataSet<any>>(columns: TableColumns<T>): TableColumns<T> => columns.map((column) => {
|
|
||||||
if ('children' in column) {
|
|
||||||
return {
|
|
||||||
...column,
|
|
||||||
children: applyColumnWrappers(column.children),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...column,
|
|
||||||
render: makeColumnRenderWrapper<T>(column.key, column.render),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function _Table<T extends DataSet<any>>({ columns, dataSource, tableName, showSettingsChanger, ...other }: TableProps<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])
|
||||||
useEffect(() => setNewColumns(() => {
|
useEffect(() => setNewColumns(() => {
|
||||||
const newColumns = applyTableSettings(applyColumnWrappers(columns), settings)
|
const mergedColumns = applyTableSettings(columns, settings)
|
||||||
|
const newColumns = mergedColumns.map((column) => ({
|
||||||
|
...column,
|
||||||
|
render: columnRenderWrapper(column.key, column.render),
|
||||||
|
key: keyToString(column.key),
|
||||||
|
}))
|
||||||
if (tableName && showSettingsChanger) {
|
if (tableName && showSettingsChanger) {
|
||||||
const oldTitle = newColumns[0].title
|
const oldTitle = newColumns[0].title
|
||||||
newColumns[0].title = (props) => (
|
newColumns[0].title = (props) => (
|
||||||
@ -104,21 +82,14 @@ function _Table<T extends DataSet<any>>({ columns, dataSource, tableName, showSe
|
|||||||
}), [settings, columns, onSettingsChanged, showSettingsChanger, tableName])
|
}), [settings, columns, onSettingsChanged, showSettingsChanger, tableName])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RawTable
|
<RawTable<DataType<T>>
|
||||||
columns={newColumns}
|
columns={newColumns}
|
||||||
dataSource={tryAddKeys(dataSource)}
|
dataSource={tryAddKeys<DataType<T>>(dataSource)}
|
||||||
{...other}
|
{...other}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Обёртка над компонентом таблицы AntD
|
|
||||||
*
|
|
||||||
* Особенности:
|
|
||||||
* * Поддержка составных ключей столбцов
|
|
||||||
* * Работа с настройками столбцов таблицы
|
|
||||||
*/
|
|
||||||
export const Table = memo(_Table) as typeof _Table
|
export const Table = memo(_Table) as typeof _Table
|
||||||
|
|
||||||
export default Table
|
export default Table
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { memo, useCallback, useEffect, useState } from 'react'
|
import { memo, useCallback, useEffect, useState } from 'react'
|
||||||
import { ColumnsType } from 'antd/lib/table'
|
import { ColumnsType } from 'antd/lib/table'
|
||||||
import { Button, Modal, Switch } from 'antd'
|
import { Button, Modal, Switch, Table } from 'antd'
|
||||||
import { SettingOutlined } from '@ant-design/icons'
|
import { SettingOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
import { TableColumnSettings, makeTableSettings, mergeTableSettings, TableSettings } from '@utils'
|
import { TableColumnSettings, makeTableSettings, mergeTableSettings, TableSettings } from '@utils'
|
||||||
import { Table, TableColumns } from './Table'
|
import { TableColumns } from './Table'
|
||||||
import { makeColumn, makeTextColumn } from '.'
|
import { makeColumn } from '.'
|
||||||
|
|
||||||
const parseSettings = <T extends object>(columns?: TableColumns<T>, settings?: TableSettings | null): TableColumnSettings[] => {
|
const parseSettings = <T extends object>(columns?: TableColumns<T>, settings?: TableSettings | null): TableColumnSettings[] => {
|
||||||
const newSettings = mergeTableSettings(makeTableSettings(columns ?? []), settings ?? {})
|
const newSettings = mergeTableSettings(makeTableSettings(columns ?? []), settings ?? {})
|
||||||
@ -46,8 +46,8 @@ const _TableSettingsChanger = <T extends object>({ title, columns, settings, onC
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTableColumns([
|
setTableColumns([
|
||||||
makeTextColumn<string>('Название', 'title'),
|
makeColumn('Название', 'title'),
|
||||||
makeColumn<any>(null, 'visible', {
|
makeColumn(null, 'visible', {
|
||||||
title: () => (
|
title: () => (
|
||||||
<>
|
<>
|
||||||
Показать
|
Показать
|
||||||
@ -56,7 +56,7 @@ const _TableSettingsChanger = <T extends object>({ title, columns, settings, onC
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
render: (visible, _, index = NaN) => (
|
render: (visible: boolean, _?: TableColumnSettings, index: number = NaN) => (
|
||||||
<Switch
|
<Switch
|
||||||
checked={visible}
|
checked={visible}
|
||||||
checkedChildren={'Отображён'}
|
checkedChildren={'Отображён'}
|
||||||
@ -84,7 +84,7 @@ const _TableSettingsChanger = <T extends object>({ title, columns, settings, onC
|
|||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
centered
|
centered
|
||||||
open={visible}
|
visible={visible}
|
||||||
onCancel={onModalCancel}
|
onCancel={onModalCancel}
|
||||||
onOk={onModalOk}
|
onOk={onModalOk}
|
||||||
title={title ?? 'Настройка отображения таблицы'}
|
title={title ?? 'Настройка отображения таблицы'}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,30 +1,59 @@
|
|||||||
export * from './EditableTable'
|
export { makeDateSorter, makeNumericSorter, makeStringSorter, makeTimeSorter } from './sorters'
|
||||||
export * from './DatePickerWrapper'
|
export { EditableTable, makeTableAction } from './EditableTable'
|
||||||
export * from './TimePickerWrapper'
|
export { DatePickerWrapper } from './DatePickerWrapper'
|
||||||
export * from './DateRangeWrapper'
|
export { TimePickerWrapper } from './TimePickerWrapper'
|
||||||
export * from './Table'
|
export { DateRangeWrapper } from './DateRangeWrapper'
|
||||||
export * from './Columns'
|
export { Table } from './Table'
|
||||||
|
export {
|
||||||
|
RegExpIsFloat,
|
||||||
|
timezoneOptions,
|
||||||
|
TimezoneSelect,
|
||||||
|
makeDateColumn,
|
||||||
|
makeTimeColumn,
|
||||||
|
makeGroupColumn,
|
||||||
|
makeColumn,
|
||||||
|
makeColumnsPlanFact,
|
||||||
|
makeFilterTextMatch,
|
||||||
|
makeNumericRender,
|
||||||
|
makeNumericColumn,
|
||||||
|
makeNumericColumnOptions,
|
||||||
|
makeNumericColumnPlanFact,
|
||||||
|
makeNumericStartEnd,
|
||||||
|
makeNumericMinMax,
|
||||||
|
makeNumericAvgRange,
|
||||||
|
makeSelectColumn,
|
||||||
|
makeTagColumn,
|
||||||
|
makeTagInput,
|
||||||
|
makeTextColumn,
|
||||||
|
makeTimezoneColumn,
|
||||||
|
makeTimezoneRenderer,
|
||||||
|
} from './Columns'
|
||||||
|
|
||||||
|
export type {
|
||||||
|
DataType,
|
||||||
|
RenderMethod,
|
||||||
|
SorterMethod,
|
||||||
|
TagInputProps,
|
||||||
|
columnPropsOther,
|
||||||
|
} from './Columns'
|
||||||
|
export type { DateRangeWrapperProps } from './DateRangeWrapper'
|
||||||
|
export type { DatePickerWrapperProps } from './DatePickerWrapper'
|
||||||
|
export type { TimePickerWrapperProps } from './TimePickerWrapper'
|
||||||
|
export type { BaseTableColumn, TableColumns, TableContainer } from './Table'
|
||||||
|
|
||||||
export const defaultPagination = {
|
export const defaultPagination = {
|
||||||
defaultPageSize: 14,
|
defaultPageSize: 14,
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PaginationContainer<T> = {
|
type PaginationContainer = {
|
||||||
skip?: number
|
skip?: number
|
||||||
take?: number
|
take?: number
|
||||||
count?: number
|
count?: number
|
||||||
items?: T[] | null
|
items?: any[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export const makePaginationObject = (сontainer: PaginationContainer, ...other: any) => ({
|
||||||
* Генерирует объект пагинации для компонента `Table` из данных от сервисов
|
|
||||||
*
|
|
||||||
* @param сontainer данные от сервиса
|
|
||||||
* @param other Дополнительные поля (передаются в объект напрямую в приоритете)
|
|
||||||
* @returns Объект пагинации
|
|
||||||
*/
|
|
||||||
export const makePaginationObject = <T, M extends object>(сontainer: PaginationContainer<T>, other: M) => ({
|
|
||||||
...other,
|
...other,
|
||||||
pageSize: сontainer.take,
|
pageSize: сontainer.take,
|
||||||
total: сontainer.count ?? сontainer.items?.length ?? 0,
|
total: сontainer.count ?? сontainer.items?.length ?? 0,
|
||||||
|
42
src/components/Table/sorters.ts
Executable file
42
src/components/Table/sorters.ts
Executable file
@ -0,0 +1,42 @@
|
|||||||
|
import { timeToMoment } from '@utils'
|
||||||
|
import { isRawDate } from '@utils'
|
||||||
|
import { TimeDto } from '@api'
|
||||||
|
|
||||||
|
import { DataType } from './Columns'
|
||||||
|
import { CompareFn } from 'antd/lib/table/interface'
|
||||||
|
|
||||||
|
export const makeNumericSorter = <T extends unknown>(key: keyof DataType<T>): CompareFn<DataType<T>> =>
|
||||||
|
(a: DataType<T>, b: DataType<T>) => Number(a[key]) - Number(b[key])
|
||||||
|
|
||||||
|
export const makeNumericObjSorter = (key: [string, string]) =>
|
||||||
|
(a: DataType, b: DataType) => Number(a[key[0]][key[1]]) - Number(b[key[0]][key[1]])
|
||||||
|
|
||||||
|
export const makeStringSorter = <T extends unknown>(key: keyof DataType<T>) => (a?: DataType<T> | null, b?: DataType<T> | null) => {
|
||||||
|
if (!a && !b) return 0
|
||||||
|
if (!a) return 1
|
||||||
|
if (!b) return -1
|
||||||
|
|
||||||
|
return String(a[key]).localeCompare(String(b[key]))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeDateSorter = <T extends unknown>(key: keyof DataType<T>) => (a: DataType<T>, b: DataType<T>) => {
|
||||||
|
const adate = a[key]
|
||||||
|
const bdate = b[key]
|
||||||
|
if (!isRawDate(adate) || !isRawDate(bdate))
|
||||||
|
throw new Error('Date column contains not date formatted string(s)')
|
||||||
|
|
||||||
|
const date = new Date(adate)
|
||||||
|
|
||||||
|
return date.getTime() - new Date(bdate).getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeTimeSorter = <T extends TimeDto>(key: keyof DataType<T>) => (a: DataType<T>, b: DataType<T>) => {
|
||||||
|
const elma = a[key]
|
||||||
|
const elmb = b[key]
|
||||||
|
|
||||||
|
if (!elma && !elmb) return 0
|
||||||
|
if (!elma) return 1
|
||||||
|
if (!elmb) return -1
|
||||||
|
|
||||||
|
return timeToMoment(elma).diff(timeToMoment(elmb))
|
||||||
|
}
|
@ -1,110 +1,56 @@
|
|||||||
import { memo, ReactNode, useCallback, useState } from 'react'
|
import { memo, MouseEventHandler, useCallback, useState } from 'react'
|
||||||
import { useNavigate, useLocation } from 'react-router-dom'
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
import { Button, Collapse, Drawer, DrawerProps, Form, FormRule, Input, Popconfirm } from 'antd'
|
import { Button, Dropdown, DropDownProps } from 'antd'
|
||||||
import { useForm } from 'antd/lib/form/Form'
|
import { UserOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
import { useUser } from '@asb/context'
|
import { getUserLogin, removeUser } from '@utils'
|
||||||
import { Grid, GridItem } from '@components/Grid'
|
import { ChangePassword } from './ChangePassword'
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import { PrivateMenu } from './Private'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
|
||||||
import { isURLAvailable, removeUser } from '@utils'
|
|
||||||
import { AuthService } from '@api'
|
|
||||||
|
|
||||||
import '@styles/components/user_menu.less'
|
import AdminPanel from '@pages/AdminPanel'
|
||||||
|
|
||||||
export type UserMenuProps = DrawerProps & {
|
type UserMenuProps = Omit<DropDownProps, 'overlay'> & { isAdmin?: boolean }
|
||||||
isAdmin?: boolean
|
|
||||||
additional?: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChangePasswordForm = {
|
export const UserMenu = memo<UserMenuProps>(({ isAdmin, ...other }) => {
|
||||||
'new-password': string
|
const [isModalVisible, setIsModalVisible] = useState<boolean>(false)
|
||||||
}
|
|
||||||
|
|
||||||
const newPasswordRules: FormRule[] = [{ required: true, message: 'Пожалуйста, введите новый пароль!' }]
|
|
||||||
|
|
||||||
const confirmPasswordRules: FormRule[] = [({ getFieldValue }) => ({ validator(_, value: string) {
|
|
||||||
if (value !== getFieldValue('new-password'))
|
|
||||||
return Promise.reject('Пароли не совпадают!')
|
|
||||||
return Promise.resolve()
|
|
||||||
}})]
|
|
||||||
|
|
||||||
export const UserMenu = memo<UserMenuProps>(({ isAdmin, additional, ...other }) => {
|
|
||||||
const [showLoader, setShowLoader] = useState<boolean>(false)
|
|
||||||
|
|
||||||
const [changeLoginForm] = useForm<ChangePasswordForm>()
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const user = useUser()
|
|
||||||
|
|
||||||
const navigateTo = useCallback((to: string) => navigate(to, { state: { from: location.pathname }}), [navigate, location.pathname])
|
const onChangePasswordClick: MouseEventHandler = useCallback((e) => {
|
||||||
|
setIsModalVisible(true)
|
||||||
|
e.preventDefault()
|
||||||
|
}, [])
|
||||||
|
|
||||||
const onChangePasswordOk = useCallback(() => invokeWebApiWrapperAsync(
|
const onChangePasswordOk = useCallback(() => {
|
||||||
async (values: any) => {
|
setIsModalVisible(false)
|
||||||
await AuthService.changePassword(user.id ?? -1, `${values['new-password']}`)
|
navigate('/login', { state: { from: location.pathname }})
|
||||||
removeUser()
|
}, [navigate, location])
|
||||||
navigateTo('/login')
|
|
||||||
},
|
|
||||||
setShowLoader,
|
|
||||||
`Не удалось сменить пароль пользователя ${user.login}`,
|
|
||||||
{ actionName: 'Смена пароля пользователя' },
|
|
||||||
), [navigateTo])
|
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
|
||||||
removeUser()
|
|
||||||
navigateTo('/login')
|
|
||||||
}, [navigateTo])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<>
|
||||||
closable
|
<Dropdown
|
||||||
placement={'left'}
|
|
||||||
className={'user-menu'}
|
|
||||||
title={'Профиль пользователя'}
|
|
||||||
{...other}
|
{...other}
|
||||||
>
|
placement={'bottomRight'}
|
||||||
<div className={'profile-links'}>
|
overlay={(
|
||||||
|
<PrivateMenu mode={'vertical'} style={{ textAlign: 'right' }}>
|
||||||
{isAdmin ? (
|
{isAdmin ? (
|
||||||
<Button onClick={() => navigateTo('/')}>Вернуться на сайт</Button>
|
<PrivateMenu.Link visible key={'/'} path={'/'} title={'Вернуться на сайт'} />
|
||||||
) : isURLAvailable('/admin') && (
|
) : (
|
||||||
<Button onClick={() => navigateTo('/admin')}>Панель администратора</Button>
|
<PrivateMenu.Link path={'/admin'} content={AdminPanel} />
|
||||||
)}
|
)}
|
||||||
<Button type={'ghost'} onClick={logout}>Выход</Button>
|
<PrivateMenu.Link visible key={'change_password'} onClick={onChangePasswordClick} title={'Сменить пароль'} />
|
||||||
</div>
|
<PrivateMenu.Link visible key={'login'} path={'/login'} onClick={removeUser} title={'Выход'} />
|
||||||
<Collapse>
|
</PrivateMenu>
|
||||||
<Collapse.Panel header={'Данные'} key={'summary'}>
|
)}
|
||||||
<Grid>
|
>
|
||||||
<GridItem row={1} col={1}>Логин:</GridItem>
|
<Button icon={<UserOutlined/>}>{getUserLogin()}</Button>
|
||||||
<GridItem row={1} col={2}>{user.login}</GridItem>
|
</Dropdown>
|
||||||
<GridItem row={2} col={1}>Фамилия:</GridItem>
|
<ChangePassword
|
||||||
<GridItem row={2} col={2}>{user.surname}</GridItem>
|
visible={isModalVisible}
|
||||||
<GridItem row={3} col={1}>Имя:</GridItem>
|
onOk={onChangePasswordOk}
|
||||||
<GridItem row={3} col={2}>{user.name}</GridItem>
|
onCancel={() => setIsModalVisible(false)}
|
||||||
<GridItem row={4} col={1}>Отчество:</GridItem>
|
/>
|
||||||
<GridItem row={4} col={2}>{user.patronymic}</GridItem>
|
</>
|
||||||
<GridItem row={5} col={1}>E-mail:</GridItem>
|
|
||||||
<GridItem row={5} col={2}>{user.email}</GridItem>
|
|
||||||
</Grid>
|
|
||||||
</Collapse.Panel>
|
|
||||||
<Collapse.Panel header={'Смена пароля'} key={'change-password'}>
|
|
||||||
<LoaderPortal show={showLoader}>
|
|
||||||
<Form name={'change-password'} form={changeLoginForm} autoComplete={'off'} onFinish={onChangePasswordOk}>
|
|
||||||
<Form.Item name={'new-password'} label={'Новый пароль'} rules={newPasswordRules}>
|
|
||||||
<Input.Password placeholder={'Впишите новый пароль'} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item required name={'confirm-password'} rules={confirmPasswordRules} label={'Подтверждение пароля'}>
|
|
||||||
<Input.Password />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item>
|
|
||||||
<Popconfirm title={'Вы уверены что хотите сменить пароль?'} onConfirm={changeLoginForm.submit} placement={'topRight'}>
|
|
||||||
<Button type={'primary'}>Сменить</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</LoaderPortal>
|
|
||||||
</Collapse.Panel>
|
|
||||||
{additional}
|
|
||||||
</Collapse>
|
|
||||||
</Drawer>
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
|
import { memo, 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
|
||||||
@ -19,7 +20,6 @@ export type D3HorizontalChartProps = {
|
|||||||
height?: Property.Height
|
height?: Property.Height
|
||||||
data: PercentChartDataType[]
|
data: PercentChartDataType[]
|
||||||
offset?: Partial<ChartOffset>
|
offset?: Partial<ChartOffset>
|
||||||
afterDraw?: (d: d3.Selection<SVGRectElement, PercentChartDataType, SVGGElement, unknown>) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultOffset = { top: 50, right: 100, bottom: 50, left: 100 }
|
const defaultOffset = { top: 50, right: 100, bottom: 50, left: 100 }
|
||||||
@ -29,14 +29,13 @@ export const D3HorizontalPercentChart = memo<D3HorizontalChartProps>(({
|
|||||||
height: givenHeight = '100%',
|
height: givenHeight = '100%',
|
||||||
offset: givenOffset,
|
offset: givenOffset,
|
||||||
data,
|
data,
|
||||||
afterDraw,
|
|
||||||
}) => {
|
}) => {
|
||||||
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 = useMemo(() => rootRef.current ? d3.select(rootRef.current) : null, [rootRef.current])
|
||||||
|
|
||||||
const inlineWidth = useMemo(() => width - offset.left - offset.right, [width])
|
const inlineWidth = useMemo(() => width - offset.left - offset.right, [width])
|
||||||
const inlineHeight = useMemo(() => height - offset.top - offset.bottom, [height])
|
const inlineHeight = useMemo(() => height - offset.top - offset.bottom, [height])
|
||||||
@ -45,50 +44,42 @@ export const D3HorizontalPercentChart = memo<D3HorizontalChartProps>(({
|
|||||||
const yScale = useMemo(() => d3.scaleBand().domain(data.map((d) => d.name)).range([0, inlineHeight]).padding(0.25), [data, inlineHeight])
|
const yScale = useMemo(() => d3.scaleBand().domain(data.map((d) => d.name)).range([0, inlineHeight]).padding(0.25), [data, inlineHeight])
|
||||||
|
|
||||||
useEffect(() => { /// Отрисовываем оси X сверху и снизу
|
useEffect(() => { /// Отрисовываем оси X сверху и снизу
|
||||||
const r = root()
|
if (width < 100 || height < 100 || !root) return
|
||||||
if (width < 100 || height < 100 || !r) return
|
|
||||||
const xAxisTop = d3.axisTop(xScale).tickFormat((d) => `${d}%`).ticks(4).tickSize(-inlineHeight)
|
const xAxisTop = d3.axisTop(xScale).tickFormat((d) => `${d}%`).ticks(4).tickSize(-inlineHeight)
|
||||||
const xAxisBottom = d3.axisBottom(xScale).tickFormat((d) => `${d}%`).ticks(4)
|
const xAxisBottom = d3.axisBottom(xScale).tickFormat((d) => `${d}%`).ticks(4)
|
||||||
|
|
||||||
r.selectChild<SVGGElement>('.axis.x.bottom').call(xAxisBottom)
|
root.selectChild<SVGGElement>('.axis.x.bottom').call(xAxisBottom)
|
||||||
r.selectChild<SVGGElement>('.axis.x.top').call(xAxisTop)
|
root.selectChild<SVGGElement>('.axis.x.top').call(xAxisTop)
|
||||||
.selectAll('.tick')
|
.selectAll('.tick')
|
||||||
.attr('class', 'tick grid-line')
|
.attr('class', 'tick grid-line')
|
||||||
}, [root, width, height, xScale, inlineHeight])
|
}, [root, width, height, xScale, inlineHeight])
|
||||||
|
|
||||||
useEffect(() => { /// Отрисовываем ось Y слева
|
useEffect(() => { /// Отрисовываем ось Y слева
|
||||||
const r = root()
|
if (width < 100 || height < 100 || !root) return
|
||||||
if (width < 100 || height < 100 || !r) return
|
root.selectChild<SVGGElement>('.axis.y.left').call(d3.axisLeft(yScale))
|
||||||
r.selectChild<SVGGElement>('.axis.y.left').call(d3.axisLeft(yScale))
|
|
||||||
}, [root, width, height, yScale])
|
}, [root, width, height, yScale])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const r = root()
|
if (width < 100 || height < 100 || !root) return
|
||||||
if (width < 100 || height < 100 || !r) return
|
|
||||||
|
|
||||||
const delay = d3.transition().duration(500).ease(d3.easeLinear)
|
const delay = d3.transition().duration(500).ease(d3.easeLinear)
|
||||||
|
|
||||||
const rects = r.selectChild('.data').selectAll('rect').data(data)
|
const rects = root.selectChild('.data').selectAll('rect').data(data)
|
||||||
rects.enter().append('rect')
|
rects.enter().append('rect')
|
||||||
rects.exit().remove()
|
rects.exit().remove()
|
||||||
|
root.selectChild<SVGGElement>('.data')
|
||||||
const selectedRects = r.selectChild<SVGGElement>('.data')
|
|
||||||
.selectAll<SVGRectElement, PercentChartDataType>('rect')
|
.selectAll<SVGRectElement, PercentChartDataType>('rect')
|
||||||
|
.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) => xScale(d.percent))
|
||||||
|
}, [data, width, height, root, yScale, xScale])
|
||||||
afterDraw?.(selectedRects)
|
|
||||||
|
|
||||||
}, [data, width, height, root, yScale, xScale, afterDraw])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoaderPortal show={false} style={{ width: givenWidth, height: givenHeight }}>
|
<LoaderPortal show={false} style={{ width: givenWidth, height: givenHeight }}>
|
||||||
<div ref={divRef} style={{ width: '100%', height: '100%' }}>
|
<div ref={divRef} style={{ width: '100%', height: '100%' }}>
|
||||||
<svg width={'100%'} height={'100%'}>
|
<svg id={'d3-horizontal-chart'} width={'100%'} height={'100%'}>
|
||||||
<g ref={rootRef} transform={`translate(${offset.left}, ${offset.top})`}>
|
<g ref={rootRef} transform={`translate(${offset.left}, ${offset.top})`}>
|
||||||
<g className={'axis x top'}></g>
|
<g className={'axis x top'}></g>
|
||||||
<g className={'axis x bottom'} transform={`translate(0, ${inlineHeight})`}></g>
|
<g className={'axis x bottom'} transform={`translate(0, ${inlineHeight})`}></g>
|
||||||
|
@ -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 */
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
export * from './D3Chart'
|
export * from './D3Chart'
|
||||||
|
export type { D3ChartProps } from './D3Chart'
|
||||||
|
|
||||||
export * from './types'
|
export * from './types'
|
||||||
|
@ -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])
|
||||||
|
@ -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,
|
||||||
@ -196,7 +193,6 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
|||||||
methods,
|
methods,
|
||||||
|
|
||||||
className = '',
|
className = '',
|
||||||
style,
|
|
||||||
...other
|
...other
|
||||||
}: D3MonitoringChartsProps<DataType>) => {
|
}: D3MonitoringChartsProps<DataType>) => {
|
||||||
const [datasets, setDatasets, resetDatasets] = useUserSettings(chartName, datasetGroups)
|
const [datasets, setDatasets, resetDatasets] = useUserSettings(chartName, datasetGroups)
|
||||||
@ -212,7 +208,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 +241,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) => {
|
||||||
@ -466,7 +462,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
|||||||
groups.forEach((group, i) => {
|
groups.forEach((group, i) => {
|
||||||
group()
|
group()
|
||||||
.attr('transform', `translate(${sizes.groupLeft(group.key)}, 0)`)
|
.attr('transform', `translate(${sizes.groupLeft(group.key)}, 0)`)
|
||||||
.attr('clip-path', `url(#chart-group-clip)`)
|
.attr('clip-path', `url(#chart-clip)`)
|
||||||
|
|
||||||
group.charts.forEach((chart) => {
|
group.charts.forEach((chart) => {
|
||||||
chart()
|
chart()
|
||||||
@ -495,7 +491,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 +505,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
|
||||||
@ -517,7 +513,6 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
|||||||
style={{
|
style={{
|
||||||
width: givenWidth,
|
width: givenWidth,
|
||||||
height: givenHeight,
|
height: givenHeight,
|
||||||
...style,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -533,7 +528,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 +569,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>
|
||||||
|
@ -135,7 +135,7 @@ const _D3MonitoringEditor = <DataType extends BaseDataType>({
|
|||||||
<Modal
|
<Modal
|
||||||
centered
|
centered
|
||||||
width={800}
|
width={800}
|
||||||
open={visible}
|
visible={visible}
|
||||||
title={'Настройка групп графиков'}
|
title={'Настройка групп графиков'}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
footer={(
|
footer={(
|
||||||
|
@ -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>
|
|
||||||
<clipPath id={`chart-limit-clip`}>
|
|
||||||
{/* Сдвиг во все стороны на 1 px чтобы линии на краях было видно */}
|
|
||||||
<rect x={0} y={0} width={width} height={height} />
|
|
||||||
</clipPath>
|
|
||||||
<clipPath id={`chart-limit-fill-clip`}>
|
|
||||||
<rect x={-zoneWidth} y={0} width={zoneWidth} height={height} />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
<g ref={setRef} >
|
<g ref={setRef} >
|
||||||
<g className={'bars'} strokeWidth={0} clipPath={`url(#chart-limit-clip)`} />
|
<g className={'bars'} strokeWidth={0} />
|
||||||
{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>
|
||||||
|
@ -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 = {
|
||||||
/** Параметры стиля линии */
|
/** Параметры стиля линии */
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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,18 +14,16 @@ 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)
|
||||||
|
|
||||||
@ -34,8 +31,8 @@ export const renderRectArea = <DataType extends BaseDataType>(
|
|||||||
rects.enter().append('rect')
|
rects.enter().append('rect')
|
||||||
|
|
||||||
appendTransition(chart().selectAll<SVGRectElement, Record<string, any>>('rect'), chart)
|
appendTransition(chart().selectAll<SVGRectElement, Record<string, any>>('rect'), chart)
|
||||||
.attr('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)))
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,8 @@ import { ArgsProps } from 'antd/lib/notification'
|
|||||||
import { Dispatch, ReactNode, SetStateAction } from 'react'
|
import { Dispatch, ReactNode, SetStateAction } from 'react'
|
||||||
|
|
||||||
import { WellView } from '@components/views'
|
import { WellView } from '@components/views'
|
||||||
import { FunctionalValue, getFunctionalValue, getUser, isDev } from '@utils'
|
import { getUserToken } from '@utils'
|
||||||
|
import { FunctionalValue, getFunctionalValue, isDev } from '@utils'
|
||||||
import { ApiError, FileInfoDto, WellDto } from '@api'
|
import { ApiError, FileInfoDto, WellDto } from '@api'
|
||||||
|
|
||||||
export type NotifyType = 'error' | 'warning' | 'info'
|
export type NotifyType = 'error' | 'warning' | 'info'
|
||||||
@ -96,7 +97,7 @@ export const invokeWebApiWrapperAsync = async (
|
|||||||
export const download = async (url: string, fileName?: string) => {
|
export const download = async (url: string, fileName?: string) => {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${getUser().token}`
|
Authorization: `Bearer ${getUserToken()}`
|
||||||
},
|
},
|
||||||
method: 'Get'
|
method: 'Get'
|
||||||
})
|
})
|
||||||
@ -124,7 +125,7 @@ export const download = async (url: string, fileName?: string) => {
|
|||||||
export const upload = async (url: string, formData: FormData) => {
|
export const upload = async (url: string, formData: FormData) => {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${getUser().token}`
|
Authorization: `Bearer ${getUserToken()}`
|
||||||
},
|
},
|
||||||
method: 'Post',
|
method: 'Post',
|
||||||
body: formData,
|
body: formData,
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
export * from './PointerIcon'
|
export type { PointerIconColors, PointerIconProps } from './PointerIcon'
|
||||||
export * from './WellIcon'
|
export type { WellIconColors, WellIconProps, WellIconState } from './WellIcon'
|
||||||
export * from './Loader'
|
|
||||||
|
export { PointerIcon } from './PointerIcon'
|
||||||
|
export { WellIcon } from './WellIcon'
|
||||||
|
export { Loader } from './Loader'
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
import { memo, useEffect, useState } from 'react'
|
|
||||||
import { Outlet } from 'react-router-dom'
|
|
||||||
|
|
||||||
import { DepositListContext } from '@asb/context'
|
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
|
||||||
import { DepositDto, DepositService } from '@api'
|
|
||||||
import { arrayOrDefault } from '@utils'
|
|
||||||
|
|
||||||
export const DepositsOutlet = memo(() => {
|
|
||||||
const [deposits, setDeposits] = useState<DepositDto[]>([])
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
invokeWebApiWrapperAsync(
|
|
||||||
async () => {
|
|
||||||
const deposits = await DepositService.getDeposits()
|
|
||||||
setDeposits(arrayOrDefault(deposits))
|
|
||||||
},
|
|
||||||
setIsLoading,
|
|
||||||
`Не удалось загрузить список кустов`,
|
|
||||||
{ actionName: 'Получить список кустов' }
|
|
||||||
)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DepositListContext.Provider value={deposits}>
|
|
||||||
<LoaderPortal show={isLoading}>
|
|
||||||
<Outlet />
|
|
||||||
</LoaderPortal>
|
|
||||||
</DepositListContext.Provider>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default DepositsOutlet
|
|
@ -1,30 +0,0 @@
|
|||||||
import { memo, useEffect, useState } from 'react'
|
|
||||||
import { Outlet } from 'react-router-dom'
|
|
||||||
|
|
||||||
import { UserContext } from '@asb/context'
|
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
|
||||||
import { getUser, setUser as setStorageUser } from '@utils'
|
|
||||||
import { AuthService, UserTokenDto } from '@api'
|
|
||||||
|
|
||||||
export const UserOutlet = memo(() => {
|
|
||||||
const [user, setUser] = useState<UserTokenDto>({})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
invokeWebApiWrapperAsync(async () => {
|
|
||||||
let user = getUser()
|
|
||||||
if (!user.id) {
|
|
||||||
user = await AuthService.refresh()
|
|
||||||
setStorageUser(user)
|
|
||||||
}
|
|
||||||
setUser(user)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UserContext.Provider value={user}>
|
|
||||||
<Outlet />
|
|
||||||
</UserContext.Provider>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default UserOutlet
|
|
@ -1,2 +0,0 @@
|
|||||||
export * from './DepositsOutlet'
|
|
||||||
export * from './UserOutlet'
|
|
@ -39,8 +39,8 @@ export const Poprompt = memo<PopromptProps>(({ formProps, buttonProps, footer, c
|
|||||||
)}
|
)}
|
||||||
trigger={'click'}
|
trigger={'click'}
|
||||||
{...other}
|
{...other}
|
||||||
open={visible}
|
visible={visible}
|
||||||
onOpenChange={(visible) => setVisible(visible)}
|
onVisibleChange={(visible) => setVisible(visible)}
|
||||||
>
|
>
|
||||||
<Button {...buttonProps}>{text}</Button>
|
<Button {...buttonProps}>{text}</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
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 { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { hasPermission } from '@utils'
|
import { hasPermission } from '@utils'
|
||||||
|
import { DepositService } from '@api'
|
||||||
|
|
||||||
export const getTreeData = async (deposits) => {
|
export const getTreeData = async () => {
|
||||||
|
const deposits = await DepositService.getDeposits()
|
||||||
const wellsTree = deposits.map((deposit, dIdx) => ({
|
const wellsTree = deposits.map((deposit, dIdx) => ({
|
||||||
title: deposit.caption,
|
title: deposit.caption,
|
||||||
key: `0-${dIdx}`,
|
key: `0-${dIdx}`,
|
||||||
@ -39,12 +40,10 @@ 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()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
invokeWebApiWrapperAsync(
|
invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
const wellsTree = treeData ?? await getTreeData(deposits)
|
const wellsTree = treeData ?? await getTreeData()
|
||||||
const labels = treeLabels ?? getTreeLabels(wellsTree)
|
const labels = treeLabels ?? getTreeLabels(wellsTree)
|
||||||
setWellsTree(wellsTree)
|
setWellsTree(wellsTree)
|
||||||
setWellLabels(labels)
|
setWellLabels(labels)
|
||||||
@ -53,13 +52,12 @@ export const WellSelector = memo(({ value, onChange, treeData, treeLabels, ...ot
|
|||||||
'Не удалось загрузить список скважин',
|
'Не удалось загрузить список скважин',
|
||||||
{ actionName: 'Получение списка скважин' }
|
{ actionName: 'Получение списка скважин' }
|
||||||
)
|
)
|
||||||
}, [deposits, treeData, treeLabels])
|
}, [treeData, treeLabels])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TreeSelect
|
<TreeSelect
|
||||||
multiple
|
multiple
|
||||||
treeCheckable
|
treeCheckable
|
||||||
maxTagCount={'responsive'}
|
|
||||||
showCheckedStrategy={TreeSelect.SHOW_CHILD}
|
showCheckedStrategy={TreeSelect.SHOW_CHILD}
|
||||||
treeDefaultExpandAll
|
treeDefaultExpandAll
|
||||||
treeData={wellsTree}
|
treeData={wellsTree}
|
||||||
|
@ -1,50 +1,41 @@
|
|||||||
import { Drawer, Tree, TreeDataNode, TreeProps } from 'antd'
|
import { Button, Drawer, Skeleton, Tree, TreeDataNode, TreeProps, Typography } from 'antd'
|
||||||
import { useState, useEffect, useCallback, memo, Key, useMemo } from 'react'
|
import { useState, useEffect, useCallback, memo, Key } from 'react'
|
||||||
import { useNavigate, useLocation } from 'react-router-dom'
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import { useDepositList } from '@asb/context'
|
|
||||||
import { WellIcon, WellIconState } from '@components/icons'
|
import { WellIcon, WellIconState } from '@components/icons'
|
||||||
import { DepositDto, WellDto } from '@api'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
import { DepositService, DepositDto, WellDto } from '@api'
|
||||||
import { isRawDate } from '@utils'
|
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)
|
||||||
@ -84,9 +74,6 @@ export type WellTreeSelectorProps = TreeProps<TreeDataNode> & {
|
|||||||
show?: boolean
|
show?: boolean
|
||||||
expand?: boolean | Key[]
|
expand?: boolean | Key[]
|
||||||
current?: Key
|
current?: Key
|
||||||
onClose?: () => void
|
|
||||||
onChange?: (value: string | undefined) => void
|
|
||||||
open?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getExpandKeys = (treeData: TreeDataNode[], depositKeys?: Key[] | boolean): Key[] => {
|
const getExpandKeys = (treeData: TreeDataNode[], depositKeys?: Key[] | boolean): Key[] => {
|
||||||
@ -101,7 +88,34 @@ const getExpandKeys = (treeData: TreeDataNode[], depositKeys?: Key[] | boolean):
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
const makeWellsTreeData = (deposits: DepositDto[]): TreeDataNode[] => deposits.map(deposit =>({
|
export const WellTreeSelector = memo<WellTreeSelectorProps>(({ show, expand, current, ...other }) => {
|
||||||
|
const [wellsTree, setWellsTree] = useState<TreeDataNode[]>([])
|
||||||
|
const [showLoader, setShowLoader] = useState<boolean>(false)
|
||||||
|
const [visible, setVisible] = useState<boolean>(false)
|
||||||
|
const [expanded, setExpanded] = useState<Key[]>([])
|
||||||
|
const [selected, setSelected] = useState<Key[]>([])
|
||||||
|
const [value, setValue] = useState<string>()
|
||||||
|
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
console.log(location.pathname)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (current) setSelected([current])
|
||||||
|
}, [current])
|
||||||
|
|
||||||
|
useEffect(() => setVisible((prev) => show ?? prev), [show])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setExpanded((prev) => expand ? getExpandKeys(wellsTree, expand) : prev)
|
||||||
|
}, [wellsTree, expand])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
invokeWebApiWrapperAsync(
|
||||||
|
async () => {
|
||||||
|
const deposits: Array<DepositDto> = await DepositService.getDeposits()
|
||||||
|
const wellsTree: TreeDataNode[] = deposits.map(deposit =>({
|
||||||
title: deposit.caption,
|
title: deposit.caption,
|
||||||
key: `/deposit/${deposit.id}`,
|
key: `/deposit/${deposit.id}`,
|
||||||
value: `/deposit/${deposit.id}`,
|
value: `/deposit/${deposit.id}`,
|
||||||
@ -129,26 +143,23 @@ const makeWellsTreeData = (deposits: DepositDto[]): TreeDataNode[] => deposits.m
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
setWellsTree(wellsTree)
|
||||||
|
},
|
||||||
|
setShowLoader,
|
||||||
|
`Не удалось загрузить список скважин`,
|
||||||
|
{ actionName: 'Получить список скважин' }
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
export const WellTreeSelector = memo<WellTreeSelectorProps>(({ expand, current, onChange, onClose, open, ...other }) => {
|
const onChange = useCallback((value?: string): void => {
|
||||||
const [expanded, setExpanded] = useState<Key[]>([])
|
|
||||||
const [selected, setSelected] = useState<Key[]>([])
|
|
||||||
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const location = useLocation()
|
|
||||||
const deposits = useDepositList()
|
|
||||||
|
|
||||||
const wellsTree = useMemo(() => makeWellsTreeData(deposits), [deposits])
|
|
||||||
|
|
||||||
const onValueChange = useCallback((value?: string): void => {
|
|
||||||
const key = getKeyByUrl(value)[0]
|
const key = getKeyByUrl(value)[0]
|
||||||
setSelected(key ? [key] : [])
|
setSelected(key ? [key] : [])
|
||||||
onChange?.(getLabel(wellsTree, value))
|
setValue(getLabel(wellsTree, value))
|
||||||
}, [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]
|
||||||
@ -161,18 +172,14 @@ export const WellTreeSelector = memo<WellTreeSelectorProps>(({ expand, current,
|
|||||||
navigate(newPath, { state: { from: location.pathname }})
|
navigate(newPath, { state: { from: location.pathname }})
|
||||||
}, [navigate, location])
|
}, [navigate, location])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => onChange(location.pathname), [onChange, location])
|
||||||
if (current) setSelected([current])
|
|
||||||
}, [current])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setExpanded((prev) => expand ? getExpandKeys(wellsTree, expand) : prev)
|
|
||||||
}, [wellsTree, expand])
|
|
||||||
|
|
||||||
useEffect(() => onValueChange(location.pathname), [onValueChange, location.pathname])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer open={open} mask={false} onClose={onClose} title={'Список скважин'}>
|
<>
|
||||||
|
<Button loading={showLoader} onClick={() => setVisible(true)}>{value ?? 'Выберите месторождение'}</Button>
|
||||||
|
<Drawer visible={visible} mask={false} onClose={() => setVisible(false)}>
|
||||||
|
<Typography.Title level={3}>Список скважин</Typography.Title>
|
||||||
|
<Skeleton active loading={showLoader}>
|
||||||
<Tree
|
<Tree
|
||||||
{...other}
|
{...other}
|
||||||
showIcon
|
showIcon
|
||||||
@ -182,7 +189,9 @@ export const WellTreeSelector = memo<WellTreeSelectorProps>(({ expand, current,
|
|||||||
onExpand={setExpanded}
|
onExpand={setExpanded}
|
||||||
expandedKeys={expanded}
|
expandedKeys={expanded}
|
||||||
/>
|
/>
|
||||||
|
</Skeleton>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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' }}>
|
||||||
|
@ -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>
|
||||||
|
@ -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> )
|
||||||
|
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { Fragment, memo } from 'react'
|
import { Fragment, memo } from 'react'
|
||||||
import { Tooltip } from 'antd'
|
import { Tooltip } from 'antd'
|
||||||
|
|
||||||
import { Grid, GridItem } from '@components/Grid'
|
|
||||||
import { formatDate } from '@utils'
|
|
||||||
import { TelemetryDto, TelemetryInfoDto } from '@api'
|
import { TelemetryDto, TelemetryInfoDto } from '@api'
|
||||||
|
import { Grid, GridItem } from '@components/Grid'
|
||||||
|
|
||||||
export const lables: Record<string, string> = {
|
export const lables: Record<string, string> = {
|
||||||
timeZoneId: 'Временная зона',
|
timeZoneId: 'Временная зона',
|
||||||
@ -19,12 +18,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,23 +25,17 @@ 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' }}
|
||||||
title={
|
title={
|
||||||
<Grid>
|
<Grid>
|
||||||
{(Object.keys(telemetry.info) as Array<keyof TelemetryInfoDto>).map((key, i) => {
|
{(Object.keys(telemetry.info) as Array<keyof TelemetryInfoDto>).map((key, i) => (
|
||||||
let value = telemetry.info?.[key]
|
|
||||||
value = key === 'drillingStartDate' ? formatDate(value) : value
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment key={i}>
|
<Fragment key={i}>
|
||||||
<GridItem row={i+1} col={1}>{lables[key] ?? key}:</GridItem>
|
<GridItem row={i+1} col={1}>{lables[key] ?? key}:</GridItem>
|
||||||
<GridItem row={i+1} col={2}>{value}</GridItem>
|
<GridItem row={i+1} col={2}>{telemetry.info?.[key]}</GridItem>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
))}
|
||||||
})}
|
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -1,7 +1,14 @@
|
|||||||
export * from './PermissionView'
|
export type { PermissionViewProps } from './PermissionView'
|
||||||
export * from './TelemetryView'
|
export type { TelemetryViewProps } from './TelemetryView'
|
||||||
export * from './CompanyView'
|
export type { CompanyViewProps } from './CompanyView'
|
||||||
export * from './RoleView'
|
export type { RoleViewProps } from './RoleView'
|
||||||
export * from './UserView'
|
export type { UserViewProps } from './UserView'
|
||||||
export * from './WirelineView'
|
export type { WirelineViewProps } from './WirelineView'
|
||||||
|
|
||||||
|
export { PermissionView } from './PermissionView'
|
||||||
|
export { TelemetryView, getTelemetryLabel } from './TelemetryView'
|
||||||
|
export { CompanyView } from './CompanyView'
|
||||||
|
export { RoleView } from './RoleView'
|
||||||
|
export { UserView } from './UserView'
|
||||||
|
export { WirelineView } from './WirelineView'
|
||||||
export * from './WellView'
|
export * from './WellView'
|
||||||
|
@ -20,7 +20,7 @@ export const WidgetSettingsWindow = memo<WidgetSettingsWindowProps>(({ settings,
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
{...other}
|
{...other}
|
||||||
open={!!settings}
|
visible={!!settings}
|
||||||
title={(
|
title={(
|
||||||
<>
|
<>
|
||||||
Настройка виджета {settings?.label ? `"${settings?.label}"` : ''}
|
Настройка виджета {settings?.label ? `"${settings?.label}"` : ''}
|
||||||
|
@ -1,2 +1,5 @@
|
|||||||
export * from './WidgetSettingsWindow'
|
export { WidgetSettingsWindow } from './WidgetSettingsWindow'
|
||||||
export * from './BaseWidget'
|
export { BaseWidget } from './BaseWidget'
|
||||||
|
|
||||||
|
export type { WidgetSettingsWindowProps } from './WidgetSettingsWindow'
|
||||||
|
export type { WidgetSettings, BaseWidgetProps } from './BaseWidget'
|
||||||
|
22
src/context.ts
Normal file
22
src/context.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { createContext, useContext } from 'react'
|
||||||
|
|
||||||
|
import { WellDto } from '@api'
|
||||||
|
|
||||||
|
/** Контекст текущей скважины */
|
||||||
|
export const WellContext = createContext<[WellDto, (well: WellDto) => void]>([{}, () => {}])
|
||||||
|
/** Контекст текущего корневого пути */
|
||||||
|
export const RootPathContext = createContext<string>('')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение текущей скважины
|
||||||
|
*
|
||||||
|
* @returns Текущая скважина, либо `null`
|
||||||
|
*/
|
||||||
|
export const useWell = () => useContext(WellContext)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает текущий корневой путь
|
||||||
|
*
|
||||||
|
* @returns Текущий корневой путь
|
||||||
|
*/
|
||||||
|
export const useRootPath = () => useContext(RootPathContext)
|
@ -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)
|
|
@ -1,5 +0,0 @@
|
|||||||
export * from './deposit'
|
|
||||||
export * from './layout_props'
|
|
||||||
export * from './root_path'
|
|
||||||
export * from './user'
|
|
||||||
export * from './well'
|
|
@ -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)
|
|
@ -1,11 +0,0 @@
|
|||||||
import { createContext, useContext } from 'react'
|
|
||||||
|
|
||||||
/** Контекст текущего корневого пути */
|
|
||||||
export const RootPathContext = createContext<string>('/')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Получить текущий корневой путь
|
|
||||||
*
|
|
||||||
* @returns Текущий корневой путь
|
|
||||||
*/
|
|
||||||
export const useRootPath = () => useContext(RootPathContext)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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 |
@ -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 |
@ -1,14 +1,11 @@
|
|||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
|
|
||||||
import { ReactComponent as RawLogo } from './Logo.svg'
|
import { ReactComponent as AsbLogo } from '@images/dd_logo_white_opt.svg'
|
||||||
|
|
||||||
export type LogoProps = React.SVGProps<SVGSVGElement> & {
|
export type LogoProps = React.SVGProps<SVGSVGElement> & { size?: number }
|
||||||
size?: number
|
|
||||||
onlyIcon?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Logo = memo<LogoProps>(({ size = 170, onlyIcon, ...props }) => (
|
export const Logo = memo<LogoProps>(({ size = 200, ...props }) => (
|
||||||
<RawLogo style={{ width: size, height: 282/896*size }} {...props} />
|
<AsbLogo className={'logo'} height={'100%'} {...props} />
|
||||||
))
|
))
|
||||||
|
|
||||||
export default Logo
|
export default Logo
|
||||||
|
@ -1,28 +1,21 @@
|
|||||||
import locale from 'antd/lib/locale/ru_RU'
|
|
||||||
import { ConfigProvider } from 'antd'
|
|
||||||
import { createRoot } from 'react-dom/client'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
|
||||||
import { getUser } from '@utils'
|
import reportWebVitals from './reportWebVitals'
|
||||||
import { OpenAPI } from '@api'
|
|
||||||
|
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
|
||||||
import '@styles/include/antd_theme.less'
|
|
||||||
import '@styles/index.css'
|
import '@styles/index.css'
|
||||||
|
|
||||||
// OpenAPI.BASE = 'http://localhost:3000'
|
|
||||||
// TODO: Удалить взятие из 'token' в следующем релизе, вставлено для совместимости
|
|
||||||
OpenAPI.TOKEN = async () => getUser().token || localStorage.getItem('token') || ''
|
|
||||||
OpenAPI.HEADERS = { 'Content-Type': 'application/json' }
|
|
||||||
|
|
||||||
const container = document.getElementById('root') ?? document.body
|
const container = document.getElementById('root') ?? document.body
|
||||||
const root = createRoot(container)
|
const root = createRoot(container)
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<ConfigProvider locale={locale}>
|
|
||||||
<App />
|
<App />
|
||||||
</ConfigProvider>
|
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
|
reportWebVitals()
|
||||||
|
2
src/pages/public/AccessDenied.jsx → src/pages/AccessDenied.jsx
Normal file → Executable file
2
src/pages/public/AccessDenied.jsx → src/pages/AccessDenied.jsx
Normal file → Executable file
@ -11,7 +11,7 @@ export const AccessDenied = memo(() => {
|
|||||||
return (
|
return (
|
||||||
<Result
|
<Result
|
||||||
status={'error'}
|
status={'error'}
|
||||||
title={'Доступ запрещён'}
|
title={'Доступ запрешён'}
|
||||||
subTitle={'Страницы не существует или у вас отсутствует к ней доступ.'}
|
subTitle={'Страницы не существует или у вас отсутствует к ней доступ.'}
|
||||||
>
|
>
|
||||||
<div className={'desc'}>
|
<div className={'desc'}>
|
@ -1,46 +0,0 @@
|
|||||||
import { memo } from 'react'
|
|
||||||
import {
|
|
||||||
ApiOutlined,
|
|
||||||
BankOutlined,
|
|
||||||
BranchesOutlined,
|
|
||||||
DashboardOutlined,
|
|
||||||
FileSearchOutlined,
|
|
||||||
FolderOutlined,
|
|
||||||
IdcardOutlined,
|
|
||||||
MonitorOutlined,
|
|
||||||
TeamOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
} from '@ant-design/icons'
|
|
||||||
|
|
||||||
import { makeItem, PrivateMenu } from '@components/PrivateMenu'
|
|
||||||
import { isDev } from '@utils'
|
|
||||||
|
|
||||||
export const menuItems = [
|
|
||||||
makeItem('Месторождения', 'deposit', [], <FolderOutlined />),
|
|
||||||
makeItem('Кусты', 'cluster', [], <FolderOutlined />),
|
|
||||||
makeItem('Скважины', 'well', [], <FolderOutlined />),
|
|
||||||
makeItem('Пользователи', 'user', [], <UserOutlined />),
|
|
||||||
makeItem('Компании', 'company', [], <BankOutlined />),
|
|
||||||
makeItem('Типы компаний', 'company_type', [], <BankOutlined />),
|
|
||||||
makeItem('Роли', 'role', [], <TeamOutlined />),
|
|
||||||
makeItem('Разрешения', 'permission', [], <IdcardOutlined />),
|
|
||||||
makeItem('Телеметрия', 'telemetry', [], <DashboardOutlined />, [
|
|
||||||
makeItem('Просмотр', 'viewer', [], <MonitorOutlined />),
|
|
||||||
makeItem('Объединение', 'merger', [], <BranchesOutlined />),
|
|
||||||
]),
|
|
||||||
makeItem('Журнал посещений', 'visit_log', [], <FileSearchOutlined />),
|
|
||||||
isDev() && makeItem('API', '/swagger/index.html', [], <ApiOutlined />),
|
|
||||||
].filter(Boolean)
|
|
||||||
|
|
||||||
export const AdminNavigationMenu = memo((props) => (
|
|
||||||
<PrivateMenu
|
|
||||||
{...props}
|
|
||||||
items={menuItems}
|
|
||||||
rootPath={'/admin'}
|
|
||||||
selectable={false}
|
|
||||||
mode={'inline'}
|
|
||||||
theme={'dark'}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
export default AdminNavigationMenu
|
|
@ -3,18 +3,19 @@ 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'
|
||||||
import { arrayOrDefault, coordsFormat, withPermissions } from '@utils'
|
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
|
||||||
import { min1 } from '@utils/validationRules'
|
import { min1 } from '@utils/validationRules'
|
||||||
|
|
||||||
|
import { coordsFixed } from './DepositController'
|
||||||
|
|
||||||
const ClusterController = memo(() => {
|
const ClusterController = memo(() => {
|
||||||
const [deposits, setDeposits] = useState([])
|
const [deposits, setDeposits] = useState([])
|
||||||
const [clusters, setClusters] = useState([])
|
const [clusters, setClusters] = useState([])
|
||||||
@ -31,11 +32,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: coordsFixed }),
|
||||||
|
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFixed }),
|
||||||
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
|
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
|
||||||
], [deposits])
|
], [deposits])
|
||||||
|
|
||||||
@ -101,10 +108,13 @@ const ClusterController = memo(() => {
|
|||||||
onRowEdit={tableHandlers.edit}
|
onRowEdit={tableHandlers.edit}
|
||||||
onRowDelete={tableHandlers.delete}
|
onRowDelete={tableHandlers.delete}
|
||||||
tableName={'admin_cluster_controller'}
|
tableName={'admin_cluster_controller'}
|
||||||
scroll={{ x: true }}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default withPermissions(ClusterController, ['AdminDeposit.get', 'AdminCluster.get'])
|
export default wrapPrivateComponent(ClusterController, {
|
||||||
|
requirements: ['AdminDeposit.get', 'AdminCluster.get'],
|
||||||
|
title: 'Кусты',
|
||||||
|
route: 'cluster',
|
||||||
|
})
|
||||||
|
@ -3,13 +3,14 @@ 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'
|
||||||
import { arrayOrDefault, withPermissions } from '@utils'
|
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
|
||||||
import { min1 } from '@utils/validationRules'
|
import { min1 } from '@utils/validationRules'
|
||||||
|
|
||||||
const CompanyController = memo(() => {
|
const CompanyController = memo(() => {
|
||||||
@ -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()
|
||||||
@ -88,10 +97,13 @@ const CompanyController = memo(() => {
|
|||||||
onRowEdit={tableHandlers.edit}
|
onRowEdit={tableHandlers.edit}
|
||||||
onRowDelete={tableHandlers.delete}
|
onRowDelete={tableHandlers.delete}
|
||||||
tableName={'admin_company_controller'}
|
tableName={'admin_company_controller'}
|
||||||
scroll={{ x: true }}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default withPermissions(CompanyController, ['AdminCompany.get', 'AdminCompanyType.get'])
|
export default wrapPrivateComponent(CompanyController, {
|
||||||
|
requirements: ['AdminCompany.get', 'AdminCompanyType.get'],
|
||||||
|
title: 'Компании',
|
||||||
|
route: 'company',
|
||||||
|
})
|
||||||
|
@ -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, wrapPrivateComponent } 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(() => {
|
||||||
@ -70,10 +75,13 @@ const CompanyTypeController = memo(() => {
|
|||||||
onRowEdit={tableHandlers.edit}
|
onRowEdit={tableHandlers.edit}
|
||||||
onRowDelete={tableHandlers.delete}
|
onRowDelete={tableHandlers.delete}
|
||||||
tableName={'admin_company_type_controller'}
|
tableName={'admin_company_type_controller'}
|
||||||
scroll={{ x: true }}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default withPermissions(CompanyTypeController, ['AdminCompanyType.get'])
|
export default wrapPrivateComponent(CompanyTypeController, {
|
||||||
|
requirements: ['AdminCompanyType.get'],
|
||||||
|
title: 'Типы компаний',
|
||||||
|
route: 'company_type',
|
||||||
|
})
|
||||||
|
@ -2,15 +2,17 @@ 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, wrapPrivateComponent } from '@utils'
|
||||||
import { min1 } from '@utils/validationRules'
|
import { min1 } from '@utils/validationRules'
|
||||||
import { AdminDepositService } from '@api'
|
import { AdminDepositService } from '@api'
|
||||||
|
|
||||||
|
export const coordsFixed = (coords) => coords && isFinite(coords) ? (+coords).toPrecision(10) : '-'
|
||||||
|
|
||||||
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: coordsFixed }),
|
||||||
makeNumericColumn('Долгота', 'longitude', coordsFormat, undefined, 150),
|
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFixed }),
|
||||||
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
|
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -75,10 +77,13 @@ const DepositController = memo(() => {
|
|||||||
onRowEdit={tableHandlers.edit}
|
onRowEdit={tableHandlers.edit}
|
||||||
onRowDelete={tableHandlers.delete}
|
onRowDelete={tableHandlers.delete}
|
||||||
tableName={'admin_deposit_controller'}
|
tableName={'admin_deposit_controller'}
|
||||||
scroll={{ x: true }}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default withPermissions(DepositController, ['AdminDeposit.get'])
|
export default wrapPrivateComponent(DepositController, {
|
||||||
|
requirements: ['AdminDeposit.get'],
|
||||||
|
title: 'Месторождения',
|
||||||
|
route: 'deposit',
|
||||||
|
})
|
||||||
|
@ -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, wrapPrivateComponent } 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(() => {
|
||||||
@ -74,10 +82,13 @@ const PermissionController = memo(() => {
|
|||||||
onRowEdit={tableHandlers.edit}
|
onRowEdit={tableHandlers.edit}
|
||||||
onRowDelete={tableHandlers.delete}
|
onRowDelete={tableHandlers.delete}
|
||||||
tableName={'admin_permission_controller'}
|
tableName={'admin_permission_controller'}
|
||||||
scroll={{ x: true }}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default withPermissions(PermissionController, ['AdminPermission.get'])
|
export default wrapPrivateComponent(PermissionController, {
|
||||||
|
requirements: ['AdminPermission.get'],
|
||||||
|
title: 'Разрешения',
|
||||||
|
route: 'permission',
|
||||||
|
})
|
||||||
|
@ -5,7 +5,7 @@ import { PermissionView, RoleView } from '@components/views'
|
|||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { EditableTable, makeTagColumn, makeTextColumn } from '@components/Table'
|
import { EditableTable, makeTagColumn, makeTextColumn } from '@components/Table'
|
||||||
import { AdminPermissionService, AdminUserRoleService } from '@api'
|
import { AdminPermissionService, AdminUserRoleService } from '@api'
|
||||||
import { arrayOrDefault, withPermissions } from '@utils'
|
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
|
||||||
import { min1 } from '@utils/validationRules'
|
import { min1 } from '@utils/validationRules'
|
||||||
|
|
||||||
const RoleController = memo(() => {
|
const RoleController = memo(() => {
|
||||||
@ -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])
|
||||||
@ -87,10 +89,13 @@ const RoleController = memo(() => {
|
|||||||
onRowEdit={tableHandlers.edit}
|
onRowEdit={tableHandlers.edit}
|
||||||
onRowDelete={tableHandlers.delete}
|
onRowDelete={tableHandlers.delete}
|
||||||
tableName={'admin_role_controller'}
|
tableName={'admin_role_controller'}
|
||||||
scroll={{ x: true }}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default withPermissions(RoleController, ['AdminPermission.get', 'AdminUserRole.get'])
|
export default wrapPrivateComponent(RoleController, {
|
||||||
|
requirements: ['AdminPermission.get', 'AdminUserRole.get'],
|
||||||
|
title: 'Роли',
|
||||||
|
route: 'role',
|
||||||
|
})
|
||||||
|
@ -8,7 +8,7 @@ import LoaderPortal from '@components/LoaderPortal'
|
|||||||
import { lables } from '@components/views/TelemetryView'
|
import { lables } from '@components/views/TelemetryView'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import TelemetrySelect from '@components/selectors/TelemetrySelect'
|
import TelemetrySelect from '@components/selectors/TelemetrySelect'
|
||||||
import { arrayOrDefault, withPermissions } from '@utils'
|
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
|
||||||
import { AdminTelemetryService } from '@api'
|
import { AdminTelemetryService } from '@api'
|
||||||
|
|
||||||
const { Item } = Descriptions
|
const { Item } = Descriptions
|
||||||
@ -134,4 +134,9 @@ const TelemetryMerger = memo(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default withPermissions(TelemetryMerger)
|
export default wrapPrivateComponent(TelemetryMerger, {
|
||||||
|
requirements: [],
|
||||||
|
title: 'Объединение',
|
||||||
|
route: 'merger',
|
||||||
|
key: 'merger',
|
||||||
|
})
|
||||||
|
@ -6,7 +6,7 @@ import { Button, Input } from 'antd'
|
|||||||
import {
|
import {
|
||||||
defaultPagination,
|
defaultPagination,
|
||||||
makeColumn,
|
makeColumn,
|
||||||
makeDateColumn,
|
makeDateSorter,
|
||||||
makeNumericColumn,
|
makeNumericColumn,
|
||||||
makeNumericRender,
|
makeNumericRender,
|
||||||
makeTextColumn,
|
makeTextColumn,
|
||||||
@ -14,7 +14,7 @@ import {
|
|||||||
} from '@components/Table'
|
} from '@components/Table'
|
||||||
import Poprompt from '@components/selectors/Poprompt'
|
import Poprompt from '@components/selectors/Poprompt'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { arrayOrDefault, withPermissions } from '@utils'
|
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
|
||||||
import { AdminTelemetryService } from '@api'
|
import { AdminTelemetryService } from '@api'
|
||||||
|
|
||||||
const TelemetryController = memo(() => {
|
const TelemetryController = memo(() => {
|
||||||
@ -50,10 +50,10 @@ 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'),
|
makeTextColumn('Дата начала бурения', 'drillingStartDate', null, makeDateSorter('drillingStartDate')),
|
||||||
makeTextColumn('Часовой пояс', 'timeZoneId'),
|
makeTextColumn('Часовой пояс', 'timeZoneId'),
|
||||||
makeTextColumn('Скважина', 'well'),
|
makeTextColumn('Скважина', 'well'),
|
||||||
makeTextColumn('Куст', 'cluster'),
|
makeTextColumn('Куст', 'cluster'),
|
||||||
@ -115,10 +115,14 @@ const TelemetryController = memo(() => {
|
|||||||
pagination={defaultPagination}
|
pagination={defaultPagination}
|
||||||
dataSource={filteredTelemetryData}
|
dataSource={filteredTelemetryData}
|
||||||
tableName={'admin_telemetry_controller'}
|
tableName={'admin_telemetry_controller'}
|
||||||
scroll={{ x: true }}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default withPermissions(TelemetryController)
|
export default wrapPrivateComponent(TelemetryController, {
|
||||||
|
requirements: [],
|
||||||
|
title: 'Просмотр',
|
||||||
|
route: 'viewer',
|
||||||
|
key: 'viewer',
|
||||||
|
})
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
|
import { Layout } from 'antd'
|
||||||
import { memo, useMemo } from 'react'
|
import { memo, useMemo } from 'react'
|
||||||
import { Outlet } from 'react-router-dom'
|
import { Navigate, Route, Routes } from 'react-router-dom'
|
||||||
|
|
||||||
import { RootPathContext, useRootPath } from '@asb/context'
|
import { RootPathContext, useRootPath } from '@asb/context'
|
||||||
import { withPermissions } from '@utils'
|
import { PrivateMenu } from '@components/Private'
|
||||||
|
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
|
||||||
|
|
||||||
|
import TelemetryViewer from './TelemetryViewer'
|
||||||
|
import TelemetryMerger from './TelemetryMerger'
|
||||||
|
|
||||||
const Telemetry = memo(() => {
|
const Telemetry = memo(() => {
|
||||||
const root = useRootPath()
|
const root = useRootPath()
|
||||||
@ -10,9 +15,30 @@ const Telemetry = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RootPathContext.Provider value={rootPath}>
|
<RootPathContext.Provider value={rootPath}>
|
||||||
<Outlet />
|
<Layout>
|
||||||
|
<PrivateMenu>
|
||||||
|
<PrivateMenu.Link content={TelemetryViewer} />
|
||||||
|
<PrivateMenu.Link content={TelemetryMerger} />
|
||||||
|
</PrivateMenu>
|
||||||
|
|
||||||
|
<Layout>
|
||||||
|
<Layout.Content className={'site-layout-background'}>
|
||||||
|
<Routes>
|
||||||
|
<Route index element={<Navigate to={TelemetryViewer.route} replace />} />
|
||||||
|
<Route path={'*'} element={<NoAccessComponent />} />
|
||||||
|
<Route path={TelemetryViewer.route} element={<TelemetryViewer />} />
|
||||||
|
<Route path={TelemetryMerger.route} element={<TelemetryMerger />} />
|
||||||
|
</Routes>
|
||||||
|
</Layout.Content>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
</RootPathContext.Provider>
|
</RootPathContext.Provider>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default withPermissions(Telemetry, ['AdminTelemetry.get'])
|
export default wrapPrivateComponent(Telemetry, {
|
||||||
|
requirements: ['AdminTelemetry.get'],
|
||||||
|
title: 'Телеметрия',
|
||||||
|
key: 'telemetry',
|
||||||
|
route: 'telemetry/*',
|
||||||
|
})
|
||||||
|
@ -17,7 +17,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory'
|
|||||||
import { AdminCompanyService, AdminUserRoleService, AdminUserService } from '@api'
|
import { AdminCompanyService, AdminUserRoleService, AdminUserService } from '@api'
|
||||||
import { createLoginRules, nameRules, phoneRules, emailRules } from '@utils/validationRules'
|
import { createLoginRules, nameRules, phoneRules, emailRules } from '@utils/validationRules'
|
||||||
import { makeTextOnFilter, makeTextFilters, makeArrayOnFilter } from '@utils/filters'
|
import { makeTextOnFilter, makeTextFilters, makeArrayOnFilter } from '@utils/filters'
|
||||||
import { arrayOrDefault, withPermissions } from '@utils'
|
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
|
||||||
|
|
||||||
import RoleTag from './RoleTag'
|
import RoleTag from './RoleTag'
|
||||||
|
|
||||||
@ -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'),
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
@ -206,7 +214,6 @@ const UserController = memo(() => {
|
|||||||
buttonsWidth={120}
|
buttonsWidth={120}
|
||||||
pagination={defaultPagination}
|
pagination={defaultPagination}
|
||||||
tableName={'admin_user_controller'}
|
tableName={'admin_user_controller'}
|
||||||
scroll={{ x: true }}
|
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
<ChangePassword
|
<ChangePassword
|
||||||
@ -219,4 +226,8 @@ const UserController = memo(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default withPermissions(UserController, ['AdminUser.get', 'AdminCompany.get', 'AdminUserRole.get'])
|
export default wrapPrivateComponent(UserController, {
|
||||||
|
requirements: ['AdminUser.get', 'AdminCompany.get', 'AdminUserRole.get'],
|
||||||
|
title: 'Пользователи',
|
||||||
|
route: 'user',
|
||||||
|
})
|
||||||
|
@ -3,7 +3,7 @@ import { Input } from 'antd'
|
|||||||
|
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { defaultPagination, makeColumn, makeDateSorter, makeStringSorter, Table } from '@components/Table'
|
import { defaultPagination, makeColumn, makeDateSorter, makeStringSorter, Table } from '@components/Table'
|
||||||
import { arrayOrDefault, formatDate, withPermissions } from '@utils'
|
import { arrayOrDefault, formatDate, wrapPrivateComponent } from '@utils'
|
||||||
import { RequestTrackerService } from '@api'
|
import { RequestTrackerService } from '@api'
|
||||||
|
|
||||||
const logRecordCount = 1000
|
const logRecordCount = 1000
|
||||||
@ -59,10 +59,13 @@ const VisitLog = memo(() => {
|
|||||||
dataSource={filteredLogData}
|
dataSource={filteredLogData}
|
||||||
pagination={defaultPagination}
|
pagination={defaultPagination}
|
||||||
tableName={'visit_log'}
|
tableName={'visit_log'}
|
||||||
scroll={{ x: true }}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default withPermissions(VisitLog, ['RequestTracker.get'])
|
export default wrapPrivateComponent(VisitLog, {
|
||||||
|
requirements: ['RequestTracker.get'],
|
||||||
|
title: 'Журнал посещений',
|
||||||
|
route: 'visit_log',
|
||||||
|
})
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user