Merge branch 'dev'
0
.gitignore
vendored
Normal file → Executable file
0
.vscode/launch.json
vendored
Normal file → Executable file
0
.vscode/settings.json
vendored
Normal file → Executable file
63
README.md
Normal file → Executable file
@ -1 +1,62 @@
|
|||||||
Проект веб части ASB cloud
|
![ASB Logo](concept/ImagesSrc/logo_Asb.svg)
|
||||||
|
# Проект веб части ASB cloud
|
||||||
|
|
||||||
|
# Порядок запуска
|
||||||
|
## 1. Установка пакетов
|
||||||
|
Для запуска установки необходимо иметь уже установленый [NPM](https://www.npmjs.com).
|
||||||
|
|
||||||
|
Установка выполняется одной командой:
|
||||||
|
```bash
|
||||||
|
npm i
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Автогенерация сервисов
|
||||||
|
Для корректной работы веб-приложения необходимо наличие сервисов работы с RestAPI.
|
||||||
|
|
||||||
|
Для их автогенерации требуется уже запущенная серверная часть.
|
||||||
|
|
||||||
|
Автогенерацию можно запустить с помощью уже прописанных в [package.json](package.json) скриптов, либо вручную.
|
||||||
|
|
||||||
|
Если сервер запущен на текущей машине достаточно написать:
|
||||||
|
```bash
|
||||||
|
npm run update_openapi
|
||||||
|
```
|
||||||
|
|
||||||
|
Для получения сервисов с основного сервера:
|
||||||
|
```bash
|
||||||
|
npm run update_openapi_server
|
||||||
|
```
|
||||||
|
|
||||||
|
или же ручной вариант:
|
||||||
|
```bash
|
||||||
|
npx openapi -i http://{IP_ADDRESS}:{PORT}/swagger/v1/swagger.json -o src/services/api
|
||||||
|
```
|
||||||
|
|
||||||
|
где ***IP_ADDRESS*** и ***PORT*** это соответственно IP-адрес и порт сервера.
|
||||||
|
|
||||||
|
На данный момент имеются следующие IP-адреса:
|
||||||
|
|
||||||
|
| IP-адрес | Описание |
|
||||||
|
|:-|:-|
|
||||||
|
| 127.0.0.1:5000 | Локальный адрес вашей машины (привязан к `update_openapi`) |
|
||||||
|
| 192.168.1.70:5000 | Локальный адрес development-сервера (привязан к `update_openapi_server`) |
|
||||||
|
| 46.146.209.148:89 | Внешний адрес development-сервера |
|
||||||
|
| 46.146.209.148 | Внешний адрес production-сервера |
|
||||||
|
|
||||||
|
## 3. Компиляция production-версии приложения
|
||||||
|
После выполнения вышеописанных пунктов приложение готово к компиляции.
|
||||||
|
|
||||||
|
Для компиляции досточно выполнить команду:
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
После завершения этой команды скомпилированное приложение будет находиться в появившейся директории [build/](build/).
|
||||||
|
|
||||||
|
## 4. Запуск development-версии приложения
|
||||||
|
В [package.json](package.json) необходимо проверить и при необходимости изменить значение в поле ***proxy*** (пара адрес-порт сервера с RestAPI) на актуальное.
|
||||||
|
|
||||||
|
После чего выполнить запуск командой:
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
0
concept/ImagesSrc/logo_32_Asb.png
Normal file → Executable file
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
0
concept/ImagesSrc/logo_32_naftagaz.png
Normal file → Executable file
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
0
concept/ImagesSrc/logo_Asb.svg
Normal file → Executable file
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
0
concept/SludgeDiagram.jsx
Normal file → Executable file
8
concept/Smbo/EquipmentDetails.jsx
Normal file → Executable file
@ -1,8 +1,10 @@
|
|||||||
import {Row, Col} from 'antd'
|
import {Row, Col} from 'antd'
|
||||||
import Documents from '../Documents/DocumentsTemplate'
|
|
||||||
import '../../styles/equipment_details.css'
|
|
||||||
|
|
||||||
export default function EquipmentDetails({id, equipmentTimers, equipmentSensors}) {
|
import Documents from '../Documents/DocumentsTemplate'
|
||||||
|
|
||||||
|
import '@styles/equipment_details.css'
|
||||||
|
|
||||||
|
export default function EquipmentDetails({ id, equipmentTimers, equipmentSensors }) {
|
||||||
let stateOfEquipmentDetails = equipmentTimers.map(timer => {
|
let stateOfEquipmentDetails = equipmentTimers.map(timer => {
|
||||||
return(
|
return(
|
||||||
<p key={timer.label}>{timer.label}: <span className="right-text"><b>{timer.value} {timer.unit}</b></span></p>
|
<p key={timer.label}>{timer.label}: <span className="right-text"><b>{timer.value} {timer.unit}</b></span></p>
|
||||||
|
0
concept/Smbo/SmboPlate.jsx
Normal file → Executable file
0
concept/Smbo/images/RigPlan2.png
Normal file → Executable file
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 180 KiB |
0
concept/Smbo/images/TopDrive_Dummy.png
Normal file → Executable file
Before Width: | Height: | Size: 496 KiB After Width: | Height: | Size: 496 KiB |
0
concept/Smbo/index.jsx
Normal file → Executable file
0
concept/TelemetryAnalysis.jsx
Normal file → Executable file
0
concept/WellStat.jsx
Normal file → Executable file
@ -1,163 +0,0 @@
|
|||||||
import { Table, Select, DatePicker } from "antd";
|
|
||||||
import { TelemetryAnalyticsService } from "../src/services/api";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import notify from "../src/components/notify";
|
|
||||||
import LoaderPortal from "../src/components/LoaderPortal";
|
|
||||||
import moment from "moment";
|
|
||||||
import "../styles/message.css";
|
|
||||||
|
|
||||||
const { Option } = Select;
|
|
||||||
const pageSize = 26;
|
|
||||||
const { RangePicker } = DatePicker;
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: "Название операции",
|
|
||||||
key: "name",
|
|
||||||
dataIndex: "name",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Дата начала операции",
|
|
||||||
key: "beginDate",
|
|
||||||
dataIndex: "beginDate",
|
|
||||||
render: (item) => moment.utc(item).local().format("DD MMM YYYY, HH:mm:ss"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Дата окончания операции",
|
|
||||||
key: "endDate",
|
|
||||||
dataIndex: "endDate",
|
|
||||||
render: (item) => moment.utc(item).local().format("DD MMM YYYY, HH:mm:ss"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Глубина скважины в начале операции",
|
|
||||||
key: "beginWellDepth",
|
|
||||||
dataIndex: "startWellDepth",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Глубина скважины в конце операции",
|
|
||||||
key: "endWellDepth",
|
|
||||||
dataIndex: "endWellDepth",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const filterOptions = [
|
|
||||||
{ label: "Невозможно определить операцию", value: 1 },
|
|
||||||
{ label: "Роторное бурение", value: 2 },
|
|
||||||
{ label: "Слайдирование", value: 3 },
|
|
||||||
{ label: "Подъем с проработкой", value: 4 },
|
|
||||||
{ label: "Спуск с проработкой", value: 5 },
|
|
||||||
{ label: "Подъем с промывкой", value: 6 },
|
|
||||||
{ label: "Спуск с промывкой", value: 7 },
|
|
||||||
{ label: "Спуск в скважину", value: 8 },
|
|
||||||
{ label: "Спуск с вращением", value: 9 },
|
|
||||||
{ label: "Подъем из скважины", value: 10 },
|
|
||||||
{ label: "Подъем с вращением", value: 11 },
|
|
||||||
{ label: "Промывка в покое", value: 12 },
|
|
||||||
{ label: "Промывка с вращением", value: 13 },
|
|
||||||
{ label: "Удержание в клиньях", value: 14 },
|
|
||||||
{ label: "Неподвижное состояние", value: 15 },
|
|
||||||
{ label: "Вращение без циркуляции", value: 16 },
|
|
||||||
{ label: "На поверхности", value: 17 },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function WellTelemetryAnalysis() {
|
|
||||||
let { id } = useParams();
|
|
||||||
|
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [range, setRange] = useState([]);
|
|
||||||
const [categories, setCategories] = useState([]);
|
|
||||||
const [pagination, setPagination] = useState(null);
|
|
||||||
const [operations, setOperations] = useState([]);
|
|
||||||
|
|
||||||
const [loader, setLoader] = useState(false);
|
|
||||||
|
|
||||||
const children = filterOptions.map((line) => (
|
|
||||||
<Option key={line.value}>{line.label}</Option>
|
|
||||||
));
|
|
||||||
|
|
||||||
const onChangeRange = (range) => {
|
|
||||||
setRange(range);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const GetOperations = async () => {
|
|
||||||
setLoader(true);
|
|
||||||
try {
|
|
||||||
let begin = null;
|
|
||||||
let end = null;
|
|
||||||
if (range?.length > 1) {
|
|
||||||
begin = range[0].toISOString();
|
|
||||||
end = range[1].toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
await TelemetryAnalyticsService.getOperationsByWell(
|
|
||||||
`${id}`,
|
|
||||||
(page - 1) * pageSize,
|
|
||||||
pageSize,
|
|
||||||
categories,
|
|
||||||
begin,
|
|
||||||
end
|
|
||||||
).then((paginatedOperations) => {
|
|
||||||
setOperations(
|
|
||||||
paginatedOperations?.items.map((o) => {
|
|
||||||
return {
|
|
||||||
key: o.id,
|
|
||||||
begin: o.date,
|
|
||||||
...o,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setPagination({
|
|
||||||
total: paginatedOperations?.count,
|
|
||||||
current: Math.floor(paginatedOperations?.skip / pageSize),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (ex) {
|
|
||||||
notify(`Не удалось загрузить операции по скважине "${id}"`, "error");
|
|
||||||
console.log(ex);
|
|
||||||
}
|
|
||||||
setLoader(false);
|
|
||||||
};
|
|
||||||
GetOperations();
|
|
||||||
}, [id, categories, range, page]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="filter-group">
|
|
||||||
<h3 className="filter-group-heading">Фильтр операций</h3>
|
|
||||||
<Select
|
|
||||||
mode="multiple"
|
|
||||||
allowClear
|
|
||||||
placeholder="Фильтр операций"
|
|
||||||
className="filter-selector"
|
|
||||||
value={categories}
|
|
||||||
onChange={setCategories}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Select>
|
|
||||||
<RangePicker
|
|
||||||
showTime
|
|
||||||
placeholder={["Дата начала операции", "Дата окончания операции"]}
|
|
||||||
onChange={onChangeRange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<LoaderPortal show={loader}>
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
dataSource={operations}
|
|
||||||
size={"small"}
|
|
||||||
pagination={{
|
|
||||||
pageSize: pageSize,
|
|
||||||
showSizeChanger: false,
|
|
||||||
total: pagination?.total,
|
|
||||||
current: page,
|
|
||||||
onChange: (page) => setPage(page),
|
|
||||||
}}
|
|
||||||
rowKey={(record) => record.id}
|
|
||||||
/>
|
|
||||||
</LoaderPortal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
0
concept/readme.md
Normal file → Executable file
0
craco.config.js
Normal file → Executable file
@ -1,9 +0,0 @@
|
|||||||
# Вопрос Олегу: Какой бит и в каком регистре (или значение регистра) контроллера спинмастера сигнализирует что система спинмастер активана/работает?
|
|
||||||
Ответ: Такого бита нет, но можно понять если etap %MW1600: INT; не равен 6 и не равен 0 тогда система в работе
|
|
||||||
|
|
||||||
# Вопрос Олегу: Какой бит и в каком регистре (или значение регистра) контроллера спинмастера сигнализирует что система стабилизации крутящего момента активна/работает?
|
|
||||||
Ответ: Такого бита по сути тоже нет но можно понять что
|
|
||||||
когда stik_sleep %MX2802.01: BOOL; =true
|
|
||||||
и
|
|
||||||
etap %MW1600: INT; =7
|
|
||||||
тогда торк мастер работает
|
|
1475
package-lock.json
generated
Normal file → Executable file
13
package.json
Normal file → Executable file
@ -4,16 +4,18 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@craco/craco": "^6.1.2",
|
"@craco/craco": "^6.1.2",
|
||||||
"@microsoft/signalr": "^5.0.5",
|
"@microsoft/signalr": "^6.0.4",
|
||||||
"@testing-library/jest-dom": "^5.11.10",
|
"@testing-library/jest-dom": "^5.11.10",
|
||||||
"@testing-library/react": "^11.2.6",
|
"@testing-library/react": "^11.2.6",
|
||||||
"@testing-library/user-event": "^12.8.3",
|
"@testing-library/user-event": "^12.8.3",
|
||||||
|
"@types/react-dom": "^18.0.3",
|
||||||
"antd": "^4.15.0",
|
"antd": "^4.15.0",
|
||||||
"chart.js": "^3.6.0",
|
"chart.js": "^3.6.0",
|
||||||
"chartjs-adapter-moment": "^1.0.0",
|
"chartjs-adapter-moment": "^1.0.0",
|
||||||
"chartjs-plugin-datalabels": "^2.0.0-rc.1",
|
"chartjs-plugin-datalabels": "^2.0.0-rc.1",
|
||||||
"chartjs-plugin-zoom": "^1.1.1",
|
"chartjs-plugin-zoom": "^1.1.1",
|
||||||
"craco-less": "^1.17.1",
|
"craco-less": "^1.17.1",
|
||||||
|
"d3": "^7.4.4",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"pigeon-maps": "^0.19.7",
|
"pigeon-maps": "^0.19.7",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
@ -28,8 +30,10 @@
|
|||||||
"start": "craco start",
|
"start": "craco start",
|
||||||
"build": "craco build",
|
"build": "craco build",
|
||||||
"test": "craco test",
|
"test": "craco test",
|
||||||
"update_openapi": "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",
|
||||||
"update_openapi_server": "npx openapi -i http://192.168.1.70:5000/swagger/v1/swagger.json -o src/services/api",
|
"oud": "npx openapi -i http://192.168.1.70:5000/swagger/v1/swagger.json -o src/services/api",
|
||||||
|
"oug": "npx openapi -i http://46.146.209.148/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",
|
||||||
"react_start": "react-scripts start",
|
"react_start": "react-scripts start",
|
||||||
"react_build": "react-scripts build",
|
"react_build": "react-scripts build",
|
||||||
"react_test": "react-scripts test",
|
"react_test": "react-scripts test",
|
||||||
@ -55,10 +59,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/d3": "^7.1.0",
|
||||||
"@types/react": "^17.0.3",
|
"@types/react": "^17.0.3",
|
||||||
"@types/react-router-dom": "^5.3.2",
|
"@types/react-router-dom": "^5.3.2",
|
||||||
"craco-alias": "^3.0.1",
|
"craco-alias": "^3.0.1",
|
||||||
"openapi-typescript": "^3.4.1",
|
"openapi-typescript": "^3.4.1",
|
||||||
"openapi-typescript-codegen": "^0.9.3"
|
"openapi-typescript-codegen": "^0.21.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
0
public/favicon.ico
Normal file → Executable file
Before Width: | Height: | Size: 354 KiB After Width: | Height: | Size: 354 KiB |
0
public/images/logo_32.png
Normal file → Executable file
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
8
public/index.html
Normal file → Executable file
@ -1,13 +1,15 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="white" />
|
||||||
|
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white" />
|
||||||
|
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="онлайн мониторинг процесса бурения в реальном времени в офисе заказчика"
|
content="Онлайн мониторинг процесса бурения в реальном времени в офисе заказчика"
|
||||||
/>
|
/>
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
<title>АСБ Vision</title>
|
<title>АСБ Vision</title>
|
||||||
|
0
public/manifest.json
Normal file → Executable file
0
public/robots.txt
Normal file → Executable file
@ -3,22 +3,22 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
Route
|
Route
|
||||||
} from 'react-router-dom'
|
} from 'react-router-dom'
|
||||||
|
import { memo } from 'react'
|
||||||
import { ConfigProvider } from 'antd'
|
import { ConfigProvider } from 'antd'
|
||||||
import locale from 'antd/lib/locale/ru_RU'
|
import locale from 'antd/lib/locale/ru_RU'
|
||||||
|
|
||||||
import { OpenAPI } from '@api'
|
|
||||||
import { getUserToken } from '@utils/storage'
|
|
||||||
import { PrivateRoute } from '@components/Private'
|
import { PrivateRoute } from '@components/Private'
|
||||||
|
import { getUserToken } from '@utils/storage'
|
||||||
|
import { OpenAPI } from '@api'
|
||||||
|
|
||||||
import Main from '@pages/Main'
|
import Main from '@pages/Main'
|
||||||
import Login from '@pages/Login'
|
import Login from '@pages/Login'
|
||||||
import Register from '@pages/Register'
|
import Register from '@pages/Register'
|
||||||
|
|
||||||
import '@styles/App.less'
|
import '@styles/App.less'
|
||||||
import { memo } from 'react'
|
|
||||||
|
|
||||||
//OpenAPI.BASE = 'http://localhost:3000'
|
//OpenAPI.BASE = 'http://localhost:3000'
|
||||||
OpenAPI.TOKEN = async () => getUserToken()
|
OpenAPI.TOKEN = async () => getUserToken() ?? ''
|
||||||
OpenAPI.HEADERS = {'Content-Type': 'application/json'}
|
OpenAPI.HEADERS = {'Content-Type': 'application/json'}
|
||||||
|
|
||||||
export const App = memo(() => (
|
export const App = memo(() => (
|
@ -1,41 +0,0 @@
|
|||||||
import moment from 'moment'
|
|
||||||
import { DatePicker } from 'antd'
|
|
||||||
import { useState, useEffect } from 'react'
|
|
||||||
import { useParams } from 'react-router-dom'
|
|
||||||
|
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
|
||||||
import { TelemetryAnalyticsService } from '@api'
|
|
||||||
import { ChartOperationTime } from './charts/ChartOperationTime'
|
|
||||||
|
|
||||||
const { RangePicker } = DatePicker
|
|
||||||
|
|
||||||
const lines = [{ labelAccessorName: 'processName', pieceAccessorName: 'duration' }]
|
|
||||||
|
|
||||||
export const AnalysisOperationTime = () => {
|
|
||||||
const { id } = useParams()
|
|
||||||
const [operationTimeData, setOperationTimeData] = useState([])
|
|
||||||
const [loader, setLoader] = useState(false)
|
|
||||||
const [range, setRange] = useState([moment().subtract(1,'days'), moment()])
|
|
||||||
|
|
||||||
useEffect(() => invokeWebApiWrapperAsync(
|
|
||||||
async () => {
|
|
||||||
const begin = range?.length > 1 ? range[0].toISOString() : null
|
|
||||||
const end = range?.length > 1 ? range[1].toISOString() : null
|
|
||||||
const summary = await TelemetryAnalyticsService.getOperationsSummary(id, begin, end)
|
|
||||||
setOperationTimeData(summary)
|
|
||||||
},
|
|
||||||
setLoader,
|
|
||||||
`Не удалось получить данные для анализа Операция-Время по скважине '${id}' за период с ${begin} по ${end}`,
|
|
||||||
'Получение данных для анализа Операция-Время по скважине'
|
|
||||||
), [id, range])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LoaderPortal show={loader}>
|
|
||||||
<RangePicker showTime onChange={(range) => setRange(range)} />
|
|
||||||
<ChartOperationTime data={operationTimeData} lines={lines} />
|
|
||||||
</LoaderPortal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AnalysisOperationTime
|
|
45
src/components/ChangePassword.tsx
Normal file → Executable file
@ -1,5 +1,5 @@
|
|||||||
import { memo, useState } from 'react'
|
import { memo, useCallback, useMemo, useState } from 'react'
|
||||||
import { useForm } from 'antd/lib/form/Form'
|
import { Rule } from 'antd/lib/form'
|
||||||
import { Form, Input, Modal, FormProps } from 'antd'
|
import { Form, Input, Modal, FormProps } from 'antd'
|
||||||
|
|
||||||
import { AuthService, UserDto } from '@api'
|
import { AuthService, UserDto } from '@api'
|
||||||
@ -21,12 +21,19 @@ export type ChangePasswordProps = {
|
|||||||
|
|
||||||
const fieldRules = [...passwordRules, ...createPasswordRules]
|
const fieldRules = [...passwordRules, ...createPasswordRules]
|
||||||
|
|
||||||
|
const confirmPasswordRules: Rule[] = [({ getFieldValue }) => ({ validator(_, value: string) {
|
||||||
|
if (value !== getFieldValue('new-password'))
|
||||||
|
return Promise.reject('Пароли не совпадают!')
|
||||||
|
return Promise.resolve()
|
||||||
|
}})]
|
||||||
|
|
||||||
export const ChangePassword = memo<ChangePasswordProps>(({ user, visible, onCancel, onOk }) => {
|
export const ChangePassword = memo<ChangePasswordProps>(({ user, visible, onCancel, onOk }) => {
|
||||||
const [showLoader, setShowLoader] = useState<boolean>(false)
|
const [showLoader, setShowLoader] = useState<boolean>(false)
|
||||||
const [password, setPassword] = useState<string>('')
|
|
||||||
const [isDisabled, setIsDisabled] = useState(true)
|
const [isDisabled, setIsDisabled] = useState(true)
|
||||||
|
|
||||||
const [form] = useForm()
|
const userData = useMemo(() => user ?? { id: getUserId(), login: getUserLogin() } as UserDto, [user])
|
||||||
|
|
||||||
|
const [form] = Form.useForm()
|
||||||
|
|
||||||
const onFormChange = async () => await form.validateFields()
|
const onFormChange = async () => await form.validateFields()
|
||||||
.then(() => setIsDisabled(false))
|
.then(() => setIsDisabled(false))
|
||||||
@ -37,15 +44,15 @@ export const ChangePassword = memo<ChangePasswordProps>(({ user, visible, onCanc
|
|||||||
onCancel?.()
|
onCancel?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onFormFinish = () => invokeWebApiWrapperAsync(
|
const onFormFinish = useCallback((values: any) => invokeWebApiWrapperAsync(
|
||||||
async() => {
|
async() => {
|
||||||
await AuthService.changePassword(user?.id ?? getUserId() ?? -1, `"${password}"`)
|
await AuthService.changePassword(userData.id ?? -1, `${values['new-password']}`)
|
||||||
onOk?.()
|
onOk?.()
|
||||||
},
|
},
|
||||||
setShowLoader,
|
setShowLoader,
|
||||||
`Не удалось сменить пароль пользователя ${getUserLogin()}`,
|
`Не удалось сменить пароль пользователя ${userData.login}`,
|
||||||
'Смена пароля пользователя'
|
'Смена пароля пользователя'
|
||||||
)
|
), [userData, onOk])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@ -63,6 +70,8 @@ export const ChangePassword = memo<ChangePasswordProps>(({ user, visible, onCanc
|
|||||||
okButtonProps={{
|
okButtonProps={{
|
||||||
disabled: isDisabled
|
disabled: isDisabled
|
||||||
}}
|
}}
|
||||||
|
// getContainer={false} // Исправляет ошибку с формой, но портит вид модалки при вызове из user menu
|
||||||
|
// TODO: разобраться
|
||||||
>
|
>
|
||||||
<LoaderPortal show={showLoader}>
|
<LoaderPortal show={showLoader}>
|
||||||
<Form
|
<Form
|
||||||
@ -72,24 +81,10 @@ export const ChangePassword = memo<ChangePasswordProps>(({ user, visible, onCanc
|
|||||||
onFinish={onFormFinish}
|
onFinish={onFormFinish}
|
||||||
onChange={onFormChange}
|
onChange={onFormChange}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Form.Item required rules={fieldRules} name={'new-password'} label={'Новый пароль'}>
|
||||||
label={'Новый пароль'}
|
<Input.Password />
|
||||||
name={'new-password'}
|
|
||||||
required={true}
|
|
||||||
rules={fieldRules}
|
|
||||||
>
|
|
||||||
<Input.Password onChange={(e) => setPassword(e.target.value)} value={password} />
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item required name={'confirm-password'} rules={confirmPasswordRules} label={'Подтверждение пароля'}>
|
||||||
label={'Подтверждение пароля'}
|
|
||||||
name={'confirm-password'}
|
|
||||||
required={true}
|
|
||||||
rules={[() => ({ validator(_, value: string) {
|
|
||||||
if (value !== password)
|
|
||||||
return Promise.reject('Пароли не совпадают!')
|
|
||||||
return Promise.resolve()
|
|
||||||
}})]}
|
|
||||||
>
|
|
||||||
<Input.Password />
|
<Input.Password />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -1,19 +1,33 @@
|
|||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { useState, useEffect, memo } from 'react'
|
import { useState, useEffect, memo, ReactNode } from 'react'
|
||||||
import {CaretUpOutlined, CaretDownOutlined, CaretRightOutlined} from '@ant-design/icons'
|
import {CaretUpOutlined, CaretDownOutlined, CaretRightOutlined} from '@ant-design/icons'
|
||||||
|
|
||||||
import '@styles/display.css'
|
import '@styles/display.css'
|
||||||
|
|
||||||
export const formatNumber = (value, format) =>
|
export const formatNumber = (value?: unknown, format?: number) =>
|
||||||
Number.isInteger(format) && Number.isFinite(value)
|
Number.isInteger(format) && Number.isFinite(value)
|
||||||
? (+value).toFixed(format)
|
? Number(value).toFixed(format)
|
||||||
: (+value).toPrecision(4)
|
: Number(value).toPrecision(4)
|
||||||
|
|
||||||
const iconStyle = { color:'#0008' }
|
const iconStyle = { color:'#0008' }
|
||||||
const displayValueStyle = { display: 'flex', flexGrow: 1 }
|
const displayValueStyle = { display: 'flex', flexGrow: 1 }
|
||||||
|
|
||||||
export const ValueDisplay = ({ prefix, value, suffix, isArrowVisible, format, enumeration }) => {
|
export type ValueDisplayProps = {
|
||||||
const [val, setVal] = useState('---')
|
prefix?: ReactNode
|
||||||
|
suffix?: ReactNode
|
||||||
|
format?: number | string
|
||||||
|
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<string>('---')
|
||||||
const [arrowState, setArrowState] = useState({
|
const [arrowState, setArrowState] = useState({
|
||||||
preVal: NaN,
|
preVal: NaN,
|
||||||
preTimestamp: Date.now(),
|
preTimestamp: Date.now(),
|
||||||
@ -28,25 +42,25 @@ export const ValueDisplay = ({ prefix, value, suffix, isArrowVisible, format, en
|
|||||||
if (Number.isFinite(+value)) {
|
if (Number.isFinite(+value)) {
|
||||||
if (isArrowVisible && (arrowState.preTimestamp + 1000 < Date.now())) {
|
if (isArrowVisible && (arrowState.preTimestamp + 1000 < Date.now())) {
|
||||||
let direction = 0
|
let direction = 0
|
||||||
if (value > arrowState.preVal)
|
if (+value > arrowState.preVal)
|
||||||
direction = 1
|
direction = 1
|
||||||
if (value < arrowState.preVal)
|
if (+value < arrowState.preVal)
|
||||||
direction = -1
|
direction = -1
|
||||||
|
|
||||||
setArrowState({
|
setArrowState({
|
||||||
preVal: value,
|
preVal: +value,
|
||||||
preTimestamp: Date.now(),
|
preTimestamp: Date.now(),
|
||||||
direction: direction,
|
direction: direction,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatNumber(value, format)
|
return formatNumber(value, Number(format))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value.length > 4) {
|
if (value.length > 4) {
|
||||||
const valueDate = moment(value)
|
const valueDate = moment(value)
|
||||||
if (valueDate.isValid())
|
if (valueDate.isValid())
|
||||||
return valueDate.format(format)
|
return valueDate.format(String(format))
|
||||||
}
|
}
|
||||||
|
|
||||||
return value
|
return value
|
||||||
@ -74,9 +88,9 @@ export const ValueDisplay = ({ prefix, value, suffix, isArrowVisible, format, en
|
|||||||
{prefix} {val} {suffix}{arrow}
|
{prefix} {val} {suffix}{arrow}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
export const Display = memo(({ className, label, ...other })=> (
|
export const Display = memo<DisplayProps>(({ className, label, ...other })=> (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div className={'display_label'}>{label}</div>
|
<div className={'display_label'}>{label}</div>
|
||||||
<div style={displayValueStyle}>
|
<div style={displayValueStyle}>
|
0
src/components/DownloadLink.tsx
Normal file → Executable file
@ -1,5 +1,7 @@
|
|||||||
export class ErrorFetch extends Error {
|
export class ErrorFetch extends Error {
|
||||||
constructor(status, message) {
|
public status: number
|
||||||
|
|
||||||
|
constructor(status: number, message?: string) {
|
||||||
super(message)
|
super(message)
|
||||||
this.name = 'ErrorFetch'
|
this.name = 'ErrorFetch'
|
||||||
this.status = status
|
this.status = status
|
0
src/components/Grid.tsx
Normal file → Executable file
0
src/components/Layout/AdminLayoutPortal.tsx
Normal file → Executable file
0
src/components/Layout/LayoutPortal.tsx
Normal file → Executable file
0
src/components/Layout/index.ts
Normal file → Executable file
0
src/components/LoaderPortal.tsx
Normal file → Executable file
0
src/components/PageHeader.tsx
Normal file → Executable file
0
src/components/Private/PrivateContent.tsx
Normal file → Executable file
0
src/components/Private/PrivateDefaultRoute.tsx
Normal file → Executable file
49
src/components/Private/PrivateMenu.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { join } from 'path'
|
||||||
|
import { Menu, MenuItemProps, MenuProps } from 'antd'
|
||||||
|
import { Children, cloneElement, memo, ReactElement, useContext, useMemo } from 'react'
|
||||||
|
import { Link, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { RootPathContext } from '@asb/context'
|
||||||
|
import { isURLAvailable } from '@utils/permissions'
|
||||||
|
|
||||||
|
export type PrivateMenuProps = MenuProps & { root?: string }
|
||||||
|
|
||||||
|
export type PrivateMenuLinkProps = MenuItemProps & {
|
||||||
|
tabName?: string
|
||||||
|
path?: string
|
||||||
|
title: string
|
||||||
|
visible?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrivateMenuLink = memo<PrivateMenuLinkProps>(({ tabName = '', path = '', title, ...other }) => {
|
||||||
|
const location = useLocation()
|
||||||
|
return (
|
||||||
|
<Menu.Item key={tabName} {...other}>
|
||||||
|
<Link to={{ pathname: path, state: { from: location.pathname }}}>{title}</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const PrivateMenuMain = memo<PrivateMenuProps>(({ root, children, ...other }) => {
|
||||||
|
const rootContext = useContext(RootPathContext)
|
||||||
|
const rootPath = useMemo(() => root ?? rootContext ?? '', [root, rootContext])
|
||||||
|
|
||||||
|
const items = useMemo(() => Children.toArray(children).map((child) => {
|
||||||
|
const element = child as ReactElement
|
||||||
|
let key = element.key?.toString()
|
||||||
|
const visible: boolean | undefined = element.props.visible
|
||||||
|
if (key && visible !== false) {
|
||||||
|
key = key.slice(key.lastIndexOf('$') + 1) // Ключ автоматический преобразуется в "(.+)\$ключ"
|
||||||
|
const path = join(rootPath, key)
|
||||||
|
if (visible || isURLAvailable(path))
|
||||||
|
return cloneElement(element, { key, path, tabName: key })
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}), [children, rootPath])
|
||||||
|
|
||||||
|
return <Menu children={items} {...other} />
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PrivateMenu = Object.assign(PrivateMenuMain, { Link: PrivateMenuLink })
|
||||||
|
|
||||||
|
export default PrivateMenu
|
10
src/components/Private/PrivateMenuItem.tsx
Normal file → Executable file
@ -1,22 +1,22 @@
|
|||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { Menu, MenuItemProps } from 'antd'
|
import { Menu, MenuItemProps } from 'antd'
|
||||||
import { memo, NamedExoticComponent } from 'react'
|
import { memo, NamedExoticComponent } from 'react'
|
||||||
|
import { Link, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import { isURLAvailable } from '@utils/permissions'
|
import { isURLAvailable } from '@utils/permissions'
|
||||||
import { Link, useLocation } from 'react-router-dom'
|
|
||||||
|
|
||||||
export type PrivateMenuItemProps = MenuItemProps & {
|
export type PrivateMenuItemProps = MenuItemProps & {
|
||||||
root: string
|
root: string
|
||||||
path: string
|
path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PrivateMenuLinkProps = MenuItemProps & {
|
export type PrivateMenuItemLinkProps = MenuItemProps & {
|
||||||
root?: string
|
root?: string
|
||||||
path: string
|
path: string
|
||||||
title: string
|
title: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PrivateMenuItemLink = memo<PrivateMenuLinkProps>(({ root = '', path, title, ...other }) => {
|
export const PrivateMenuItemLink = memo<PrivateMenuItemLinkProps>(({ root = '', path, title, ...other }) => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
return (
|
return (
|
||||||
<PrivateMenuItem key={path} root={root} path={path} {...other}>
|
<PrivateMenuItem key={path} root={root} path={path} {...other}>
|
||||||
@ -26,9 +26,9 @@ export const PrivateMenuItemLink = memo<PrivateMenuLinkProps>(({ root = '', path
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const PrivateMenuItem: NamedExoticComponent<PrivateMenuItemProps> & {
|
export const PrivateMenuItem: NamedExoticComponent<PrivateMenuItemProps> & {
|
||||||
Link: NamedExoticComponent<PrivateMenuLinkProps>
|
Link: NamedExoticComponent<PrivateMenuItemLinkProps>
|
||||||
} = Object.assign(memo<PrivateMenuItemProps>(({ root, path, ...other }) =>
|
} = Object.assign(memo<PrivateMenuItemProps>(({ root, path, ...other }) =>
|
||||||
isURLAvailable(join(root, path)) ? <Menu.Item key={path} {...other}/> : null
|
<Menu.Item key={path} hidden={!isURLAvailable(join(root, path))} {...other} />
|
||||||
), {
|
), {
|
||||||
Link: PrivateMenuItemLink
|
Link: PrivateMenuItemLink
|
||||||
})
|
})
|
||||||
|
2
src/components/Private/PrivateRoute.tsx
Normal file → Executable file
@ -7,7 +7,7 @@ import { getUserId } from '@utils/storage'
|
|||||||
import { isURLAvailable } from '@utils/permissions'
|
import { isURLAvailable } from '@utils/permissions'
|
||||||
|
|
||||||
export type PrivateRouteProps = RouteProps & {
|
export type PrivateRouteProps = RouteProps & {
|
||||||
root: string
|
root?: string
|
||||||
path: string
|
path: string
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
redirect?: (location?: Location<unknown>) => ReactNode
|
redirect?: (location?: Location<unknown>) => ReactNode
|
||||||
|
75
src/components/Private/PrivateSwitch.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { join } from 'path'
|
||||||
|
import { Location } from 'history'
|
||||||
|
import { Children, cloneElement, memo, ReactElement, ReactNode, useCallback, useContext, useMemo } from 'react'
|
||||||
|
import { Redirect, Route, Switch, SwitchProps, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { RootPathContext } from '@asb/context'
|
||||||
|
import { isURLAvailable } from '@utils/permissions'
|
||||||
|
import { getUserId } from '@utils/storage'
|
||||||
|
|
||||||
|
|
||||||
|
export type PrivateSwitchProps = SwitchProps & {
|
||||||
|
root?: string
|
||||||
|
redirect?: (location?: Location<unknown>) => ReactNode
|
||||||
|
elseRedirect?: string | string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDefaultRedirectPath = () => getUserId() ? '/access_denied' : '/login'
|
||||||
|
|
||||||
|
export const defaultRedirect = (location?: Location<unknown>) => (
|
||||||
|
<Redirect to={{ pathname: getDefaultRedirectPath(), state: { from: location?.pathname } }} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export const PrivateSwitch = memo<PrivateSwitchProps>(({ root, elseRedirect, redirect = defaultRedirect, children }) => {
|
||||||
|
const rootContext = useContext(RootPathContext)
|
||||||
|
const rootPath = useMemo(() => root ?? rootContext ?? '', [root, rootContext])
|
||||||
|
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
const toAbsolute = useCallback((path: string) => path.startsWith('/') ? path : join(rootPath, path), [rootPath])
|
||||||
|
|
||||||
|
const items = useMemo(() => Children.toArray(children).map((child) => {
|
||||||
|
const element = child as ReactElement
|
||||||
|
let key = element.key?.toString()
|
||||||
|
if (!key) return null
|
||||||
|
key = key.slice(key.lastIndexOf('$') + 1).replaceAll('=2', ':')
|
||||||
|
// Ключ автоматический преобразуется в "(.+)\$ключ"
|
||||||
|
// Все ":" в ключе заменяются на "=2"
|
||||||
|
// TODO: улучшить метод нормализации ключа
|
||||||
|
const path = toAbsolute(key)
|
||||||
|
return (
|
||||||
|
<Route
|
||||||
|
key={key}
|
||||||
|
path={path}
|
||||||
|
render={({ location }) => isURLAvailable(path) ? cloneElement(element) : redirect(location)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}), [children, redirect, toAbsolute])
|
||||||
|
|
||||||
|
const defaultRoute = useMemo(() => {
|
||||||
|
if (!elseRedirect) {
|
||||||
|
const path = items.map((elm) => elm?.props.path).find((path) => path && isURLAvailable(path))
|
||||||
|
if (path) return path
|
||||||
|
} else if (Array.isArray(elseRedirect)) {
|
||||||
|
const path = elseRedirect.find((path) => {
|
||||||
|
if (!path) return false
|
||||||
|
return isURLAvailable(toAbsolute(path))
|
||||||
|
})
|
||||||
|
if (path) return toAbsolute(path)
|
||||||
|
} else if(elseRedirect && isURLAvailable(toAbsolute(elseRedirect))) {
|
||||||
|
return toAbsolute(elseRedirect)
|
||||||
|
}
|
||||||
|
return getDefaultRedirectPath()
|
||||||
|
}, [items, elseRedirect, toAbsolute])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
{items}
|
||||||
|
<Route path={'/'}>
|
||||||
|
<Redirect to={{ pathname: defaultRoute, state: { from: location.pathname } }} />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default PrivateSwitch
|
12
src/components/Private/index.ts
Normal file → Executable file
@ -1,9 +1,13 @@
|
|||||||
export { PrivateRoute, defaultRedirect } from './PrivateRoute'
|
export { PrivateRoute, defaultRedirect } from './PrivateRoute'
|
||||||
export { PrivateContent } from './PrivateContent'
|
export { PrivateContent } from './PrivateContent' // TODO: Remove
|
||||||
export { PrivateMenuItem, PrivateMenuItemLink } from './PrivateMenuItem'
|
export { PrivateMenuItem, PrivateMenuItemLink } from './PrivateMenuItem' // TODO: Remove
|
||||||
export { PrivateDefaultRoute } from './PrivateDefaultRoute'
|
export { PrivateDefaultRoute } from './PrivateDefaultRoute'
|
||||||
|
export { PrivateMenu, PrivateMenuLink } from './PrivateMenu'
|
||||||
|
export { PrivateSwitch } from './PrivateSwitch'
|
||||||
|
|
||||||
export type { PrivateRouteProps } from './PrivateRoute'
|
export type { PrivateRouteProps } from './PrivateRoute'
|
||||||
export type { PrivateContentProps } from './PrivateContent'
|
export type { PrivateContentProps } from './PrivateContent' // TODO: Remove
|
||||||
export type { PrivateMenuItemProps, PrivateMenuLinkProps } from './PrivateMenuItem'
|
export type { PrivateMenuItemProps, PrivateMenuItemLinkProps } from './PrivateMenuItem' // TODO: Remove
|
||||||
export type { PrivateDefaultRouteProps } from './PrivateDefaultRoute'
|
export type { PrivateDefaultRouteProps } from './PrivateDefaultRoute'
|
||||||
|
export type { PrivateMenuProps, PrivateMenuLinkProps } from './PrivateMenu'
|
||||||
|
export type { PrivateSwitchProps } from './PrivateSwitch'
|
||||||
|
0
src/components/Table/Columns/date.tsx
Normal file → Executable file
0
src/components/Table/Columns/index.ts
Normal file → Executable file
0
src/components/Table/Columns/numeric.tsx
Normal file → Executable file
0
src/components/Table/Columns/plan_fact.tsx
Normal file → Executable file
0
src/components/Table/Columns/select.tsx
Normal file → Executable file
0
src/components/Table/Columns/tag.tsx
Normal file → Executable file
0
src/components/Table/Columns/text.tsx
Normal file → Executable file
7
src/components/Table/Columns/timezone.tsx
Normal file → Executable file
@ -37,14 +37,11 @@ export const TimezoneSelect = memo<TimezoneSelectProps>(({ onChange, value, defa
|
|||||||
useEffect(() => setDefaultTimezone(defaultValue ? findTimezoneId(defaultValue) : null), [defaultValue])
|
useEffect(() => setDefaultTimezone(defaultValue ? findTimezoneId(defaultValue) : null), [defaultValue])
|
||||||
useEffect(() => setId(value ? findTimezoneId(value) : null), [value])
|
useEffect(() => setId(value ? findTimezoneId(value) : null), [value])
|
||||||
|
|
||||||
const onValueChanged = useCallback((id: TimezoneId | null) => {
|
const onValueChanged = useCallback((id: TimezoneId | null) => onChange?.({
|
||||||
console.log(id)
|
|
||||||
onChange?.({
|
|
||||||
timezoneId: id,
|
timezoneId: id,
|
||||||
hours: id ? rawTimezones[id] : 0,
|
hours: id ? rawTimezones[id] : 0,
|
||||||
isOverride: false,
|
isOverride: false,
|
||||||
})
|
}), [onChange])
|
||||||
}, [onChange])
|
|
||||||
|
|
||||||
return (<Select {...other} onChange={onValueChanged} value={id} defaultValue={defaultTimezone} />)
|
return (<Select {...other} onChange={onValueChanged} value={id} defaultValue={defaultTimezone} />)
|
||||||
})
|
})
|
||||||
|
2
src/components/Table/DatePickerWrapper.tsx
Normal file → Executable file
@ -18,7 +18,7 @@ export const DatePickerWrapper = memo<DatePickerWrapperProps>(({ value, onChange
|
|||||||
format={defaultFormat}
|
format={defaultFormat}
|
||||||
defaultValue={moment()}
|
defaultValue={moment()}
|
||||||
onChange={(date) => onChange?.(date)}
|
onChange={(date) => onChange?.(date)}
|
||||||
value={isUTC ? moment.utc(value).local() : moment(value)}
|
value={value && (isUTC ? moment.utc(value).local() : moment(value))}
|
||||||
{...other}
|
{...other}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
0
src/components/Table/DateRangeWrapper.tsx
Normal file → Executable file
0
src/components/Table/EditableCell.tsx
Normal file → Executable file
0
src/components/Table/EditableTable.jsx
Normal file → Executable file
0
src/components/Table/Table.tsx
Normal file → Executable file
0
src/components/Table/TableSettingsChanger.tsx
Normal file → Executable file
3
src/components/Table/index.tsx
Normal file → Executable file
@ -1,6 +1,7 @@
|
|||||||
export { makeDateSorter, makeNumericSorter, makeStringSorter } from './sorters'
|
export { makeDateSorter, makeNumericSorter, makeStringSorter } from './sorters'
|
||||||
export { EditableTable, makeActionHandler } from './EditableTable'
|
export { EditableTable, makeActionHandler } from './EditableTable'
|
||||||
export { DatePickerWrapper } from './DatePickerWrapper'
|
export { DatePickerWrapper } from './DatePickerWrapper'
|
||||||
|
export { DateRangeWrapper } from './DateRangeWrapper'
|
||||||
export { Table } from './Table'
|
export { Table } from './Table'
|
||||||
export {
|
export {
|
||||||
RegExpIsFloat,
|
RegExpIsFloat,
|
||||||
@ -33,6 +34,8 @@ export type {
|
|||||||
TagInputProps,
|
TagInputProps,
|
||||||
columnPropsOther,
|
columnPropsOther,
|
||||||
} from './Columns'
|
} from './Columns'
|
||||||
|
export type { DateRangeWrapperProps } from './DateRangeWrapper'
|
||||||
|
export type { DatePickerWrapperProps } from './DatePickerWrapper'
|
||||||
export type { BaseTableColumn, TableColumns, TableContainer } from './Table'
|
export type { BaseTableColumn, TableColumns, TableContainer } from './Table'
|
||||||
|
|
||||||
export const defaultPagination = {
|
export const defaultPagination = {
|
||||||
|
0
src/components/Table/sorters.ts
Normal file → Executable file
@ -1,22 +1,48 @@
|
|||||||
import { memo, useCallback, useState } from 'react'
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { Upload, Button } from 'antd'
|
import { Upload, Button } from 'antd'
|
||||||
import { UploadOutlined } from '@ant-design/icons'
|
import { UploadOutlined } from '@ant-design/icons'
|
||||||
|
import { UploadFile } from 'antd/lib/upload/interface'
|
||||||
|
import { RcFile } from 'antd/lib/upload'
|
||||||
|
|
||||||
import { upload } from './factory'
|
import { notify, upload } from './factory'
|
||||||
import { ErrorFetch } from './ErrorFetch'
|
import { ErrorFetch } from './ErrorFetch'
|
||||||
|
|
||||||
export const UploadForm = memo(({ url, disabled, accept, style, formData, onUploadStart, onUploadSuccess, onUploadComplete, onUploadError }) => {
|
export type UploadFormProps = {
|
||||||
const [fileList, setfileList] = useState([])
|
url: string
|
||||||
|
disabled?: boolean
|
||||||
|
accept?: string
|
||||||
|
mimeTypes?: string | string[]
|
||||||
|
style?: CSSStyleSheet
|
||||||
|
formData: FormData
|
||||||
|
onUploadStart?: () => void
|
||||||
|
onUploadSuccess?: () => void
|
||||||
|
onUploadComplete?: () => void
|
||||||
|
onUploadError?: (error: unknown) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UploadForm = memo<UploadFormProps>(({ url, disabled, style, formData, mimeTypes, onUploadStart, onUploadSuccess, onUploadComplete, onUploadError }) => {
|
||||||
|
const [fileList, setfileList] = useState<UploadFile<any>[]>([])
|
||||||
|
|
||||||
|
const checkMimeTypes = useCallback((file: RcFile) => {
|
||||||
|
const isAccepted = !mimeTypes || mimeTypes.includes(file.type)
|
||||||
|
if (isAccepted) return false
|
||||||
|
notify(`"${file.name}" является файлом неподходящего типа`, 'error')
|
||||||
|
return Upload.LIST_IGNORE
|
||||||
|
}, [mimeTypes])
|
||||||
|
|
||||||
|
const accept = useMemo(() => Array.isArray(mimeTypes) ? mimeTypes.join(',') : mimeTypes, [mimeTypes])
|
||||||
|
|
||||||
|
useEffect(() => console.log(fileList), [fileList])
|
||||||
|
|
||||||
const handleFileSend = useCallback(async () => {
|
const handleFileSend = useCallback(async () => {
|
||||||
onUploadStart?.()
|
onUploadStart?.()
|
||||||
try {
|
try {
|
||||||
const formDataLocal = new FormData()
|
const formDataLocal = new FormData()
|
||||||
fileList.forEach((val) => formDataLocal.append('files', val.originFileObj))
|
fileList.forEach((val) => formDataLocal.append('files', val.originFileObj as Blob))
|
||||||
|
|
||||||
if(formData)
|
if(formData)
|
||||||
for(const propName in formData)
|
for(const propName in formData)
|
||||||
formDataLocal.append(propName, formData[propName])
|
formDataLocal.append(propName, String(formData.get(propName)))
|
||||||
|
|
||||||
const response = await upload(url, formDataLocal)
|
const response = await upload(url, formDataLocal)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@ -45,6 +71,7 @@ export const UploadForm = memo(({ url, disabled, accept, style, formData, onUplo
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
fileList={fileList}
|
fileList={fileList}
|
||||||
onChange={(props) => setfileList(props.fileList)}
|
onChange={(props) => setfileList(props.fileList)}
|
||||||
|
beforeUpload={checkMimeTypes}
|
||||||
>
|
>
|
||||||
<Button disabled={disabled} icon={<UploadOutlined/>}>Загрузить файл</Button>
|
<Button disabled={disabled} icon={<UploadOutlined/>}>Загрузить файл</Button>
|
||||||
</Upload>
|
</Upload>
|
0
src/components/UserMenu.tsx
Normal file → Executable file
@ -1,68 +0,0 @@
|
|||||||
import moment from 'moment'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
import { ChartOpertationTimeBase } from './ChartOperationTimeBase'
|
|
||||||
|
|
||||||
export const CreateLabels = () => {
|
|
||||||
let labels = []
|
|
||||||
return labels
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CreateData = (lineConfig) => {
|
|
||||||
let datasets = {
|
|
||||||
label: lineConfig.label,
|
|
||||||
data: [],
|
|
||||||
backgroundColor: [
|
|
||||||
'#f00', '#ff0', '#f0f', '#0ff', '#00f', '#0f0'
|
|
||||||
],
|
|
||||||
}
|
|
||||||
return datasets
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChartOperationTime = ({ lines, data, rangeDate }) => {
|
|
||||||
const [opertationTimeDataParams, setOpertationTimeDataParams] = useState({ data: { labels: [], datasets: [] } })
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if ((!lines)
|
|
||||||
|| (!data))
|
|
||||||
return
|
|
||||||
|
|
||||||
let newLabels = lines.map(lineCfg => {
|
|
||||||
let labels = CreateLabels(lineCfg)
|
|
||||||
if (data.length !== 0)
|
|
||||||
labels = data.map(dataItem => {
|
|
||||||
return dataItem[lineCfg.labelAccessorName]
|
|
||||||
})
|
|
||||||
return labels
|
|
||||||
})
|
|
||||||
|
|
||||||
let newDatasets = lines.map(lineCfg => {
|
|
||||||
let datasets = CreateData(lineCfg)
|
|
||||||
if (data.length !== 0)
|
|
||||||
datasets.data = data.map(dataItem => {
|
|
||||||
return dataItem[lineCfg.pieceAccessorName]
|
|
||||||
})
|
|
||||||
return datasets
|
|
||||||
})
|
|
||||||
|
|
||||||
let interval = rangeDate ? (rangeDate[1] - rangeDate[0]) / 1000 : null
|
|
||||||
let startDate = rangeDate ? rangeDate[0] : moment()
|
|
||||||
let newParams = {
|
|
||||||
xInterval: interval,
|
|
||||||
xStart: startDate,
|
|
||||||
data: {
|
|
||||||
labels: newLabels,
|
|
||||||
datasets: newDatasets
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setOpertationTimeDataParams(newParams)
|
|
||||||
|
|
||||||
console.log(newParams)
|
|
||||||
|
|
||||||
}, [data, lines, rangeDate])
|
|
||||||
|
|
||||||
return (<>
|
|
||||||
<ChartOpertationTimeBase dataParams={opertationTimeDataParams} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,177 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
import {
|
|
||||||
Chart,
|
|
||||||
ArcElement,
|
|
||||||
TimeScale,
|
|
||||||
Legend,
|
|
||||||
PointElement,
|
|
||||||
ChartData,
|
|
||||||
ChartTypeRegistry,
|
|
||||||
ChartOptions,
|
|
||||||
DoughnutController,
|
|
||||||
} from 'chart.js'
|
|
||||||
import 'chartjs-adapter-moment'
|
|
||||||
import ChartDataLabels from 'chartjs-plugin-datalabels'
|
|
||||||
|
|
||||||
Chart.register(TimeScale, DoughnutController, PointElement, ArcElement, Legend, ChartDataLabels)
|
|
||||||
|
|
||||||
const defaultOptions = {
|
|
||||||
responsive: true,
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
position: 'top',
|
|
||||||
text: 'Doughnut Chart',
|
|
||||||
fontSize: 18,
|
|
||||||
fontColor: '#111'
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
display: true,
|
|
||||||
position: 'bottom',
|
|
||||||
labels: {
|
|
||||||
fontColor: '#333',
|
|
||||||
fontSize: 16
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ChartTimeData = ChartData<keyof ChartTypeRegistry, {
|
|
||||||
x: String
|
|
||||||
label: number
|
|
||||||
y: number
|
|
||||||
}[], unknown>
|
|
||||||
|
|
||||||
export type ChartTimeDataParams = {
|
|
||||||
data: ChartTimeData,
|
|
||||||
xStart?: Date,
|
|
||||||
xInterval?: number,
|
|
||||||
displayLabels?: Boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ChartTimeBaseProps = {
|
|
||||||
dataParams: ChartTimeDataParams,
|
|
||||||
// TODO: Create good type for options
|
|
||||||
options?: ChartOptions<keyof ChartTypeRegistry> | any,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TimeParams = {
|
|
||||||
unit: String
|
|
||||||
stepSize: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const linesPerInterval = 32
|
|
||||||
|
|
||||||
export const timeUnitByInterval = (intervalSec: number): String => {
|
|
||||||
if (intervalSec <= 60)
|
|
||||||
return 'millisecond'
|
|
||||||
|
|
||||||
if (intervalSec <= 32 * 60)
|
|
||||||
return 'second'
|
|
||||||
|
|
||||||
if (intervalSec <= 32 * 60 * 60)
|
|
||||||
return 'minute'
|
|
||||||
|
|
||||||
if (intervalSec <= 32 * 12 * 60 * 60)
|
|
||||||
return 'hour'
|
|
||||||
|
|
||||||
if (intervalSec <= 32 * 24 * 60 * 60)
|
|
||||||
return 'day'
|
|
||||||
|
|
||||||
if (intervalSec <= 32 * 7 * 24 * 60 * 60)
|
|
||||||
return 'week'
|
|
||||||
|
|
||||||
if (intervalSec <= 32 * 30.4375 * 24 * 60 * 60)
|
|
||||||
return 'month'
|
|
||||||
|
|
||||||
if (intervalSec <= 32 * 121.75 * 24 * 60 * 60)
|
|
||||||
return 'quarter'
|
|
||||||
else
|
|
||||||
return 'year'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const timeParamsByInterval = (intervalSec: number): TimeParams => {
|
|
||||||
let stepSize = intervalSec
|
|
||||||
let unit = timeUnitByInterval(intervalSec)
|
|
||||||
|
|
||||||
switch (unit) {
|
|
||||||
case 'millisecond':
|
|
||||||
stepSize *= 1000
|
|
||||||
break
|
|
||||||
case 'second':
|
|
||||||
//stepSize *= 1
|
|
||||||
break
|
|
||||||
case 'minute':
|
|
||||||
stepSize /= 60
|
|
||||||
break
|
|
||||||
case 'hour':
|
|
||||||
stepSize /= 60 * 60
|
|
||||||
break
|
|
||||||
case 'day':
|
|
||||||
stepSize /= 24 * 60 * 60
|
|
||||||
break
|
|
||||||
case 'week':
|
|
||||||
stepSize /= 7 * 24 * 60 * 60
|
|
||||||
break
|
|
||||||
case 'month':
|
|
||||||
stepSize /= 30 * 24 * 60 * 60
|
|
||||||
break
|
|
||||||
case 'quarter':
|
|
||||||
stepSize /= 91 * 24 * 60 * 60
|
|
||||||
break
|
|
||||||
case 'year':
|
|
||||||
stepSize /= 365.25 * 24 * 60 * 60
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
stepSize = Math.round(stepSize / linesPerInterval)
|
|
||||||
stepSize = stepSize > 0 ? stepSize : 1
|
|
||||||
return { unit, stepSize }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChartOpertationTimeBase: React.FC<ChartTimeBaseProps> = ({ options, dataParams }) => {
|
|
||||||
const chartRef = useRef<HTMLCanvasElement>(null)
|
|
||||||
const [chart, setChart] = useState<any>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if ((chartRef.current) && (!chart)) {
|
|
||||||
let thisOptions = {}
|
|
||||||
Object.assign(thisOptions, defaultOptions, options)
|
|
||||||
|
|
||||||
let newChart = new Chart(chartRef.current, {
|
|
||||||
type: 'doughnut',
|
|
||||||
plugins: [ChartDataLabels],
|
|
||||||
options: thisOptions,
|
|
||||||
data: dataParams.data
|
|
||||||
})
|
|
||||||
setChart(newChart)
|
|
||||||
|
|
||||||
return () => chart?.destroy()
|
|
||||||
}
|
|
||||||
}, [chart, options, dataParams])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!chart)
|
|
||||||
return
|
|
||||||
|
|
||||||
chart.data = dataParams.data
|
|
||||||
chart.options.aspectRatio = options?.aspectRatio
|
|
||||||
if (dataParams.xStart) {
|
|
||||||
let interval = Number(dataParams.xInterval ?? 600)
|
|
||||||
let start = new Date(dataParams.xStart)
|
|
||||||
let end = new Date(dataParams.xStart)
|
|
||||||
end.setSeconds(end.getSeconds() + interval)
|
|
||||||
let { unit, stepSize } = timeParamsByInterval(interval)
|
|
||||||
|
|
||||||
if (chart.options.scales?.x) {
|
|
||||||
chart.options.scales.x.max = end.getTime()
|
|
||||||
chart.options.scales.x.min = start.getTime()
|
|
||||||
chart.options.scales.x.ticks.display = dataParams.displayLabels ?? true
|
|
||||||
chart.options.scales.x.time.unit = unit
|
|
||||||
chart.options.scales.x.time.stepSize = stepSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chart.update()
|
|
||||||
}, [chart, dataParams, options])
|
|
||||||
|
|
||||||
return (<canvas ref={chartRef} />)
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
import {
|
|
||||||
Chart,
|
|
||||||
TimeScale,
|
|
||||||
LinearScale,
|
|
||||||
Legend,
|
|
||||||
LineController,
|
|
||||||
PointElement,
|
|
||||||
LineElement
|
|
||||||
} from 'chart.js'
|
|
||||||
import 'chartjs-adapter-moment'
|
|
||||||
import ChartDataLabels from 'chartjs-plugin-datalabels'
|
|
||||||
import zoomPlugin from 'chartjs-plugin-zoom'
|
|
||||||
|
|
||||||
Chart.register(TimeScale, LinearScale, LineController, LineElement, PointElement, Legend, ChartDataLabels, zoomPlugin)
|
|
||||||
|
|
||||||
const defaultOptions = {
|
|
||||||
responsive: true,
|
|
||||||
aspectRatio: 2.45,
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: 'time',
|
|
||||||
time: {
|
|
||||||
unit: 'hour',
|
|
||||||
displayFormats: {
|
|
||||||
hour: 'MM.DD'
|
|
||||||
},
|
|
||||||
tooltipFormat: 'DD T'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
type: 'linear',
|
|
||||||
position: 'top',
|
|
||||||
reverse: true,
|
|
||||||
// ticks: {
|
|
||||||
// // forces step size to be 50 units
|
|
||||||
// stepSize: 50
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
parsing: {
|
|
||||||
xAxisKey: 'date',
|
|
||||||
yAxisKey: 'depth'
|
|
||||||
},
|
|
||||||
elements: {
|
|
||||||
point: {
|
|
||||||
radius: 1.7,
|
|
||||||
hoverRadius: 5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
datalabels: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeDataset = (data, label, color, width = 1.5, dash) => ({
|
|
||||||
label: label,
|
|
||||||
data: data,
|
|
||||||
backgroundColor: color,
|
|
||||||
borderColor: color,
|
|
||||||
borderWidth: width,
|
|
||||||
borderDash: dash,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const ChartTelemetryDepthToDay = ({ depthData, bitPositionData }) => {
|
|
||||||
const chartRef = useRef(null)
|
|
||||||
const [chart, setChart] = useState()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const data = {
|
|
||||||
datasets: [
|
|
||||||
makeDataset(depthData, 'Глубина', '#0A0'),
|
|
||||||
makeDataset(bitPositionData, 'Положение долота', 'blue'),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
if(chartRef.current && !chart) {
|
|
||||||
const thisOptions = {}
|
|
||||||
Object.assign(thisOptions, defaultOptions)
|
|
||||||
|
|
||||||
const newChart = new Chart(chartRef.current, {
|
|
||||||
type: 'line',
|
|
||||||
plugins: [ChartDataLabels],
|
|
||||||
options: thisOptions,
|
|
||||||
data: data
|
|
||||||
})
|
|
||||||
setChart(newChart)
|
|
||||||
|
|
||||||
return () => chart?.destroy()
|
|
||||||
} else {
|
|
||||||
chart.data = data
|
|
||||||
chart.update()
|
|
||||||
}
|
|
||||||
}, [chart, depthData, bitPositionData])
|
|
||||||
|
|
||||||
return(<canvas ref={chartRef} />)
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
import { Chart, registerables } from 'chart.js'
|
|
||||||
|
|
||||||
Chart.register(...registerables)
|
|
||||||
|
|
||||||
const defaultOptions = {
|
|
||||||
responsive: true,
|
|
||||||
aspectRatio: 2.6,
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Дата начала интервала'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: 'Коэффициент скорости'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
datalabels: {
|
|
||||||
display: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChartTelemetryDepthToInterval = ({ depthToIntervalData }) => {
|
|
||||||
const chartRef = useRef(null)
|
|
||||||
const [chart, setChart] = useState()
|
|
||||||
|
|
||||||
const calculateBarWidth = (dataLength) => {
|
|
||||||
if (dataLength < 3) return 150
|
|
||||||
if (dataLength < 16) return 70
|
|
||||||
return 10
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const xData = depthToIntervalData.map(el => new Date(el.intervalStartDate).toLocaleString())
|
|
||||||
const yData = depthToIntervalData.map(el => el.intervalDepthProgress.toFixed(3))
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
labels: xData,
|
|
||||||
datasets: [{
|
|
||||||
label: 'Скорость проходки за интервал',
|
|
||||||
data: yData,
|
|
||||||
borderColor: '#00b300',
|
|
||||||
borderWidth: 2,
|
|
||||||
backgroundColor: '#0A0',
|
|
||||||
barThickness: calculateBarWidth(xData.length)
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
const thisOptions = {}
|
|
||||||
Object.assign(thisOptions, defaultOptions)
|
|
||||||
|
|
||||||
if (chartRef.current && !chart) {
|
|
||||||
const newChart = new Chart(chartRef.current, {
|
|
||||||
type: 'bar',
|
|
||||||
options: thisOptions,
|
|
||||||
data: data
|
|
||||||
})
|
|
||||||
setChart(newChart)
|
|
||||||
} else {
|
|
||||||
chart.data = data
|
|
||||||
chart.options = thisOptions
|
|
||||||
chart.update()
|
|
||||||
}
|
|
||||||
}, [chart, depthToIntervalData])
|
|
||||||
|
|
||||||
return (<canvas ref={chartRef} />)
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
import { Chart, registerables } from 'chart.js'
|
|
||||||
|
|
||||||
Chart.register(...registerables)
|
|
||||||
|
|
||||||
const transformSecondsToHoursString = (seconds) => {
|
|
||||||
const hours = Math.floor(seconds / 3600)
|
|
||||||
const minutes = Math.floor((seconds % 3600) / 60)
|
|
||||||
const s = seconds - (hours * 3600) - (minutes * 60)
|
|
||||||
|
|
||||||
return `${hours} ч.${minutes} мин.${s} сек.`
|
|
||||||
}
|
|
||||||
|
|
||||||
const transformSecondsToTimeString = (seconds) => {
|
|
||||||
if (seconds === 1) // 1 is default state if null returned (1 is to show chart anyway with 0 sec)
|
|
||||||
return '0 сек.'
|
|
||||||
else if(seconds < 60)
|
|
||||||
return seconds + ' сек.'
|
|
||||||
else if (seconds < 3600)
|
|
||||||
return Math.floor(seconds / 60) + ' мин. ' + (0.6 * (seconds % 60)).toFixed() + ' сек.'
|
|
||||||
else
|
|
||||||
return transformSecondsToHoursString(seconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultOptions = {
|
|
||||||
responsive: true,
|
|
||||||
aspectRatio: 2.8,
|
|
||||||
plugins: {
|
|
||||||
datalabels: {
|
|
||||||
color: '#ffffff',
|
|
||||||
formatter: transformSecondsToTimeString,
|
|
||||||
font: {
|
|
||||||
weight: 'bold'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
callbacks: {
|
|
||||||
label: (tooltipItem) => transformSecondsToTimeString(tooltipItem.parsed),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartPartsColors = [
|
|
||||||
'#54a60c', '#0ca68a', '#0c8aa6', '#0c57a6', '#0c33a6',
|
|
||||||
'#6f10d5', '#d510a1', '#f1bc41', '#c5f141', '#41f196',
|
|
||||||
'#41cbf1', '#4196f1', '#bf41f1', '#41f1c5', '#cbf141',
|
|
||||||
'#f1ce41', '#f17f41', '#f14141', '#34b40e', '#420eb4'
|
|
||||||
]
|
|
||||||
|
|
||||||
export const ChartTelemetryOperationsSummary = ({ operationsData }) => {
|
|
||||||
const chartRef = useRef(null)
|
|
||||||
const [chart, setChart] = useState()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const namesData = operationsData?.map(el => el.operationName) ?? ['Нет операций']
|
|
||||||
const durationsData = operationsData?.map(el => el.duration) ?? [1]
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
labels: namesData,
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: 'Скорость проходки за интервал',
|
|
||||||
data: durationsData,
|
|
||||||
borderColor: chartPartsColors,
|
|
||||||
backgroundColor: chartPartsColors,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const thisOptions = {}
|
|
||||||
Object.assign(thisOptions, defaultOptions)
|
|
||||||
|
|
||||||
if (chartRef.current && !chart) {
|
|
||||||
const newChart = new Chart(chartRef.current, {
|
|
||||||
type: 'doughnut',
|
|
||||||
options: thisOptions,
|
|
||||||
data: data
|
|
||||||
})
|
|
||||||
setChart(newChart)
|
|
||||||
} else {
|
|
||||||
chart.data = data
|
|
||||||
chart.options = thisOptions
|
|
||||||
chart.update()
|
|
||||||
}
|
|
||||||
}, [chart, operationsData])
|
|
||||||
|
|
||||||
return (<canvas ref={chartRef} />)
|
|
||||||
}
|
|
0
src/components/charts/ChartTimeBase.tsx
Normal file → Executable file
0
src/components/charts/Column.tsx
Normal file → Executable file
4
src/components/factory.ts
Normal file → Executable file
@ -11,12 +11,14 @@ const notificationTypeDictionary = new Map([
|
|||||||
['open' , { notifyInstance: notification.info , caption: '' }],
|
['open' , { notifyInstance: notification.info , caption: '' }],
|
||||||
])
|
])
|
||||||
|
|
||||||
|
export type NotifyType = 'error' | 'warning' | 'info'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Вызов оповещений всплывающим окошком.
|
* Вызов оповещений всплывающим окошком.
|
||||||
* @param body string или ReactNode
|
* @param body string или ReactNode
|
||||||
* @param notifyType для параметра типа. Допустимые значение 'error', 'warning', 'info'
|
* @param notifyType для параметра типа. Допустимые значение 'error', 'warning', 'info'
|
||||||
*/
|
*/
|
||||||
export const notify = (body: ReactNode, notifyType: string = 'info', other?: any) => {
|
export const notify = (body: ReactNode, notifyType: NotifyType = 'info', other?: any) => {
|
||||||
if (!body) return
|
if (!body) return
|
||||||
|
|
||||||
const instance = notificationTypeDictionary.get(notifyType) ??
|
const instance = notificationTypeDictionary.get(notifyType) ??
|
||||||
|
0
src/components/icons/Loader.tsx
Normal file → Executable file
0
src/components/icons/PointerIcon.tsx
Normal file → Executable file
0
src/components/icons/WellIcon.tsx
Normal file → Executable file
0
src/components/icons/index.ts
Normal file → Executable file
0
src/components/selectors/PeriodPicker.tsx
Normal file → Executable file
0
src/components/selectors/Poprompt.tsx
Normal file → Executable file
0
src/components/selectors/TelemetrySelect.tsx
Normal file → Executable file
0
src/components/selectors/WellSelector.jsx
Normal file → Executable file
16
src/components/selectors/WellTreeSelector.tsx
Normal file → Executable file
@ -1,11 +1,10 @@
|
|||||||
import { TreeSelect } from 'antd'
|
import { TreeSelect } from 'antd'
|
||||||
|
import { LabelInValueType } from 'rc-select/lib/Select'
|
||||||
|
import { RawValueType } from 'rc-tree-select/lib/TreeSelect'
|
||||||
import { DefaultValueType } from 'rc-tree-select/lib/interface'
|
import { DefaultValueType } from 'rc-tree-select/lib/interface'
|
||||||
import { useState, useEffect, ReactNode, useCallback, memo } from 'react'
|
import { useState, useEffect, ReactNode, useCallback, memo } from 'react'
|
||||||
import { useHistory, useLocation, useRouteMatch } from 'react-router-dom'
|
import { useHistory, useLocation, useRouteMatch } from 'react-router-dom'
|
||||||
|
|
||||||
import { RawValueType } from 'rc-tree-select/lib/TreeSelect'
|
|
||||||
import { LabelInValueType } from 'rc-select/lib/Select'
|
|
||||||
|
|
||||||
import { isRawDate } from '@utils'
|
import { isRawDate } from '@utils'
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
import { WellIcon, WellIconState } from '@components/icons'
|
import { WellIcon, WellIconState } from '@components/icons'
|
||||||
@ -54,7 +53,7 @@ const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined
|
|||||||
break
|
break
|
||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
return value
|
return 'Ошибка! Скважина не найдена!'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WellTreeSelector = memo(({ ...other }) => {
|
export const WellTreeSelector = memo(({ ...other }) => {
|
||||||
@ -100,14 +99,9 @@ export const WellTreeSelector = memo(({ ...other }) => {
|
|||||||
)
|
)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => setValue(getLabel(wellsTree, routeMatch?.url)), [wellsTree, routeMatch])
|
||||||
setValue(getLabel(wellsTree, routeMatch?.url))
|
|
||||||
}, [wellsTree, routeMatch])
|
|
||||||
|
|
||||||
const onChange = useCallback((value: string): void => {
|
const onChange = useCallback((value?: string): void => setValue(getLabel(wellsTree, value)), [wellsTree])
|
||||||
if (wellsTree)
|
|
||||||
setValue(getLabel(wellsTree, value))
|
|
||||||
}, [wellsTree])
|
|
||||||
|
|
||||||
const onSelect = useCallback((value: RawValueType | LabelInValueType): void => {
|
const onSelect = useCallback((value: RawValueType | LabelInValueType): void => {
|
||||||
if (['number', 'string'].includes(typeof value))
|
if (['number', 'string'].includes(typeof value))
|
||||||
|
0
src/components/views/CompanyView.tsx
Normal file → Executable file
0
src/components/views/PermissionView.tsx
Normal file → Executable file
0
src/components/views/RoleView.tsx
Normal file → Executable file
0
src/components/views/TelemetryView.tsx
Normal file → Executable file
0
src/components/views/UserView.tsx
Normal file → Executable file
50
src/components/views/WirelineView.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Fragment, memo } from 'react'
|
||||||
|
import { Button, ButtonProps, Tooltip, TooltipProps } from 'antd'
|
||||||
|
|
||||||
|
import { TelemetryWirelineRunOutDto } from '@api'
|
||||||
|
import { Grid, GridItem } from '../Grid'
|
||||||
|
import { formatDate } from '@utils'
|
||||||
|
|
||||||
|
const lables: Record<keyof TelemetryWirelineRunOutDto, string | {
|
||||||
|
label?: string,
|
||||||
|
formatter?: (v: any) => any,
|
||||||
|
}> = {
|
||||||
|
dateTime: { label: 'Данные актуальны на', formatter: formatDate },
|
||||||
|
hauling: 'Наработка талевого каната с момента перетяжки каната, т*км',
|
||||||
|
haulingWarnSp: 'Наработка талевого каната до сигнализации о необходимости перетяжки, т*км',
|
||||||
|
replace: 'Наработка талевого каната с момента замены каната, т*км',
|
||||||
|
replaceWarnSp: 'Наработка талевого каната до сигнализации о необходимости замены, т*км',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WirelineViewProps = TooltipProps & {
|
||||||
|
wireline?: TelemetryWirelineRunOutDto
|
||||||
|
buttonProps?: ButtonProps
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WirelineView = memo<WirelineViewProps>(({ wireline, buttonProps, ...other }) => (
|
||||||
|
<Tooltip
|
||||||
|
{...other}
|
||||||
|
title={wireline ? (
|
||||||
|
<Grid>
|
||||||
|
{(Object.keys(wireline) as Array<keyof TelemetryWirelineRunOutDto>).map((key, i) => {
|
||||||
|
const label = lables[key]
|
||||||
|
return typeof label === 'string' ? (
|
||||||
|
<Fragment key={i}>
|
||||||
|
<GridItem row={i+1} col={1}>{label}:</GridItem>
|
||||||
|
<GridItem row={i+1} col={2}>{wireline[key]}</GridItem>
|
||||||
|
</Fragment>
|
||||||
|
) : (
|
||||||
|
<Fragment key={i}>
|
||||||
|
<GridItem row={i+1} col={1}>{label?.label ?? key}:</GridItem>
|
||||||
|
<GridItem row={i+1} col={2}>{label?.formatter?.(wireline?.[key]) ?? wireline?.[key]}</GridItem>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Grid>
|
||||||
|
) : 'Нет данных'}
|
||||||
|
>
|
||||||
|
<Button type={'link'} {...buttonProps}>Талевый канат</Button>
|
||||||
|
</Tooltip>
|
||||||
|
))
|
||||||
|
|
||||||
|
export default WirelineView
|
2
src/components/views/index.ts
Normal file → Executable file
@ -3,9 +3,11 @@ export type { TelemetryViewProps } from './TelemetryView'
|
|||||||
export type { CompanyViewProps } from './CompanyView'
|
export type { CompanyViewProps } from './CompanyView'
|
||||||
export type { RoleViewProps } from './RoleView'
|
export type { RoleViewProps } from './RoleView'
|
||||||
export type { UserViewProps } from './UserView'
|
export type { UserViewProps } from './UserView'
|
||||||
|
export type { WirelineViewProps } from './WirelineView'
|
||||||
|
|
||||||
export { PermissionView } from './PermissionView'
|
export { PermissionView } from './PermissionView'
|
||||||
export { TelemetryView, getTelemetryLabel } from './TelemetryView'
|
export { TelemetryView, getTelemetryLabel } from './TelemetryView'
|
||||||
export { CompanyView } from './CompanyView'
|
export { CompanyView } from './CompanyView'
|
||||||
export { RoleView } from './RoleView'
|
export { RoleView } from './RoleView'
|
||||||
export { UserView } from './UserView'
|
export { UserView } from './UserView'
|
||||||
|
export { WirelineView } from './WirelineView'
|
||||||
|
67
src/components/widgets/BaseWidget.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { Button } from 'antd'
|
||||||
|
import { memo, ReactNode, useMemo } from 'react'
|
||||||
|
import { CloseOutlined, SettingOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
|
import '@styles/widgets/base.less'
|
||||||
|
|
||||||
|
export type WidgetSettings<T = any> = {
|
||||||
|
id?: number,
|
||||||
|
unit?: string,
|
||||||
|
label?: string,
|
||||||
|
formatter?: ((v: T) => ReactNode) | null,
|
||||||
|
defaultValue?: ReactNode,
|
||||||
|
|
||||||
|
labelColor?: string,
|
||||||
|
valueColor?: string,
|
||||||
|
backgroundColor?: string,
|
||||||
|
unitColor?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultSettings: WidgetSettings = {
|
||||||
|
unit: '----',
|
||||||
|
label: 'Виджет',
|
||||||
|
formatter: v => isNaN(v) ? v : parseFloat(v).toFixed(2),
|
||||||
|
|
||||||
|
labelColor: '#000000',
|
||||||
|
valueColor: '#000000',
|
||||||
|
backgroundColor: '#f6f6f6',
|
||||||
|
unitColor: '#a0a0a0',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BaseWidgetProps<T = any> = WidgetSettings<T> & {
|
||||||
|
value: T,
|
||||||
|
onRemove: (settings: WidgetSettings<T>) => void,
|
||||||
|
onEdit: (settings: WidgetSettings<T>) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BaseWidget = memo<BaseWidgetProps>(({ value, onRemove, onEdit, ...settings }) => {
|
||||||
|
const sets = useMemo<WidgetSettings>(() => ({ ...defaultSettings, ...settings }), [settings])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'number_widget'} style={{ background: sets.backgroundColor }}>
|
||||||
|
<div className={'widget_head'}>
|
||||||
|
<Button
|
||||||
|
type={'text'}
|
||||||
|
onClick={() => onEdit(sets)}
|
||||||
|
icon={<SettingOutlined />}
|
||||||
|
style={{ visibility: !!onEdit ? 'visible' : 'hidden' }}
|
||||||
|
/>
|
||||||
|
<div className={'widget_label'} style={{ color: sets.labelColor }}>{sets.label}</div>
|
||||||
|
<div className={'widget_close'}>
|
||||||
|
<Button
|
||||||
|
type={'link'}
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
onClick={() => onRemove(settings)}
|
||||||
|
style={{ visibility: !!onRemove ? 'visible' : 'hidden' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={'widget_value'} style={{ color: sets.valueColor }}>
|
||||||
|
{(sets.formatter === null ? value : sets.formatter?.(value)) ?? sets.defaultValue ?? '----'}
|
||||||
|
</div>
|
||||||
|
<div className={'widget_units'} style={{ color: sets.unitColor }}>{sets.unit}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default BaseWidget
|
65
src/components/widgets/WidgetSettingsWindow.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { memo, useEffect } from 'react'
|
||||||
|
import { Form, Input, Modal, ModalProps } from 'antd'
|
||||||
|
|
||||||
|
import { WidgetSettings } from './BaseWidget'
|
||||||
|
|
||||||
|
export type WidgetSettingsWindowProps<T = any> = ModalProps & {
|
||||||
|
settings: WidgetSettings<T>
|
||||||
|
onEdit: (settings: WidgetSettings<T>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Item } = Form
|
||||||
|
|
||||||
|
export const WidgetSettingsWindow = memo<WidgetSettingsWindowProps>(({ settings, onEdit, ...other }) => {
|
||||||
|
const [form] = Form.useForm()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (settings) form.setFieldsValue(settings)
|
||||||
|
}, [form, settings])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
{...other}
|
||||||
|
visible={!!settings}
|
||||||
|
title={(
|
||||||
|
<>
|
||||||
|
Настройка виджета {settings?.label ? `"${settings?.label}"` : ''}
|
||||||
|
<span style={{ color: '#a0a0a0'}}> (id: {settings?.id})</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
onOk={form.submit}
|
||||||
|
getContainer={false}
|
||||||
|
>
|
||||||
|
<Form form={form} onFinish={onEdit}>
|
||||||
|
<Item name={'id'} hidden><Input type={'hidden'} /></Item>
|
||||||
|
<Item
|
||||||
|
label={'Заголовок поля'}
|
||||||
|
name={'label'}
|
||||||
|
rules={[{ required: true, message: 'Пожалуйста, введите заголовок!' }]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Item>
|
||||||
|
<Item
|
||||||
|
label={'Единицы измерения'}
|
||||||
|
name={'unit'}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Item>
|
||||||
|
<Item label={'Цвет заголовка'} name={'labelColor'}>
|
||||||
|
<Input type={'color'} />
|
||||||
|
</Item>
|
||||||
|
<Item label={'Цвет значения'} name={'valueColor'}>
|
||||||
|
<Input type={'color'} />
|
||||||
|
</Item>
|
||||||
|
<Item label={'Цвет фона'} name={'backgroundColor'}>
|
||||||
|
<Input type={'color'} />
|
||||||
|
</Item>
|
||||||
|
<Item label={'Цвет единиц измерения'} name={'unitColor'}>
|
||||||
|
<Input type={'color'} />
|
||||||
|
</Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default WidgetSettingsWindow
|
5
src/components/widgets/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export { WidgetSettingsWindow } from './WidgetSettingsWindow'
|
||||||
|
export { BaseWidget } from './BaseWidget'
|
||||||
|
|
||||||
|
export type { WidgetSettingsWindowProps } from './WidgetSettingsWindow'
|
||||||
|
export type { WidgetSettings, BaseWidgetProps } from './BaseWidget'
|
21
src/context.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { createContext, useContext } from 'react'
|
||||||
|
|
||||||
|
|
||||||
|
/** Контекст текущего ID скважины */
|
||||||
|
export const IdWellContext = createContext<number | null>(null)
|
||||||
|
/** Контекст текущего корневого пути */
|
||||||
|
export const RootPathContext = createContext<string>('')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает текущий ID скважины
|
||||||
|
*
|
||||||
|
* @returns Текущий ID скважины, либо `null`
|
||||||
|
*/
|
||||||
|
export const useIdWell = () => useContext(IdWellContext)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает текущий корневой путь
|
||||||
|
*
|
||||||
|
* @returns Текущий корневой путь
|
||||||
|
*/
|
||||||
|
export const useRootPath = () => useContext(RootPathContext)
|
@ -1,5 +0,0 @@
|
|||||||
import {createContext} from 'react'
|
|
||||||
|
|
||||||
const Context = createContext()
|
|
||||||
|
|
||||||
export default Context
|
|
0
src/images/ClusterIcon.svg
Normal file → Executable file
Before Width: | Height: | Size: 514 B After Width: | Height: | Size: 514 B |
0
src/images/DempherOff.png
Normal file → Executable file
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 974 B |
0
src/images/DempherOn.png
Normal file → Executable file
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
0
src/images/DepositIcon.svg
Normal file → Executable file
Before Width: | Height: | Size: 729 B After Width: | Height: | Size: 729 B |
0
src/images/Logo.tsx
Normal file → Executable file
0
src/images/SpinDisabled.png
Normal file → Executable file
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
0
src/images/SpinEnabled.png
Normal file → Executable file
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |