diff --git a/.vscode/settings.json b/.vscode/settings.json index a8ab85f..01228b7 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,17 @@ { "cSpell.words": [ - "день" + "день", + "спиннера", + "Saub", + "КНБК", + "САУБ", + "antd", + "Poprompt", + "saub", + "setpoint", + "Setpoints", + "usehooks" ], - "liveServer.settings.port": 5501 + "liveServer.settings.port": 5501, + "cSpell.language": "en,ru" } \ No newline at end of file diff --git a/CODE_STANDART.md b/CODE_STANDART.md new file mode 100644 index 0000000..92dce3a --- /dev/null +++ b/CODE_STANDART.md @@ -0,0 +1,170 @@ +## 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 (...)) + ``` + +## 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 }) => ( + +
+
{title}
+
{content}
+
+
+ )) + + 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(({ title, content, loading }) => ( + +
+
{title}
+
{content}
+
+
+ )) + + export default TestPage + ``` +3. Использование `any` в типах допустимо только, если значение используется только в параметрах компонентов, обозначенных типом `any`. Если метод предполагает работу с разными типами значений стоит описать его как обобщённый. + + +## 4. JSX/TSX + +### 4.1. Стилизация кода +1. Все указываемые к компоненту параметры должны быть обёрнуты в фигурные скобки, кроме параметров флагов со значением `true`: + ```jsx + + ``` +2. Если описание параметров компонента не укладывается в ширину в 120 строк стоит перенести их в соответствии с шаблоном: + ```jsx + + ``` +3. Если JSX код передаётся как значение стоит обернуть его в круглые скобки: + ```jsx + const a = ( + + ) + ``` + +### 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` - для компонентов, применяющихся как виджеты в дашбордах. diff --git a/README.md b/README.md index cc48f9f..8772bdf 100755 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Установка выполняется одной командой: ```bash -npm i +npm ci ``` ## 2. Автогенерация сервисов @@ -17,14 +17,14 @@ npm i Автогенерацию можно запустить с помощью уже прописанных в [package.json](package.json) скриптов, либо вручную. -Если сервер запущен на текущей машине достаточно написать: +Если сервер запущен на текущей машине достаточно написать: ```bash -npm run update_openapi +npm run oul ``` -Для получения сервисов с основного сервера: +Для получения сервисов с основного сервера: ```bash -npm run update_openapi_server +npm run oug_dev ``` или же ручной вариант: @@ -36,12 +36,12 @@ npx openapi -i http://{IP_ADDRESS}:{PORT}/swagger/v1/swagger.json -o src/service На данный момент имеются следующие IP-адреса: -| IP-адрес | Описание | -|:-|:-| -| 127.0.0.1:5000 | Локальный адрес вашей машины (привязан к `update_openapi`) | -| 192.168.1.113:5000 | Локальный адрес development-сервера (привязан к `update_openapi_server`) | -| 46.146.209.148:89 | Внешний адрес development-сервера | -| cloud.digitaldrilling.ru | Внешний адрес production-сервера | +| IP-адрес | Команда | Описание | +|:-------------------------|:--------|:------------------------------------| +| 127.0.0.1:5000 | oul | Локальный адрес вашей машины | +| 192.168.1.113:5000 | oud | Локальный адрес development-сервера | +| 46.146.209.148:89 | oug_dev | Внешний адрес development-сервера | +| cloud.digitaldrilling.ru | oug | Внешний адрес production-сервера | ## 3. Компиляция production-версии приложения После выполнения вышеописанных пунктов приложение готово к компиляции. diff --git a/src/App.tsx b/src/App.tsx index 0d84e96..8f11027 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,7 @@ import { RootPathContext } from '@asb/context' import SuspenseFallback from '@components/SuspenseFallback' import { NoAccessComponent } from '@utils' -import '@styles/App.less' +import '@styles/pages/App.less' const UserOutlet = lazy(() => import('@components/outlets/UserOutlet')) const DepositsOutlet = lazy(() => import('@components/outlets/DepositsOutlet')) diff --git a/src/components/FastRunMenu.tsx b/src/components/FastRunMenu.tsx index dfaed0c..b21db2f 100644 --- a/src/components/FastRunMenu.tsx +++ b/src/components/FastRunMenu.tsx @@ -5,13 +5,13 @@ import { AutoComplete } from 'antd' import { join } from 'path' import { useWell } from '@asb/context' -import { makeItem, PrivateWellMenuItem } from './PrivateWellMenu' +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/NavigationMenu' +import { menuItems as wellMenuItems } from '@pages/Well/WellNavigationMenu' -import '@styles/fast_run_menu.less' +import '@styles/components/fast_run_menu.less' const transliterationTable = { 'q': 'й', 'w': 'ц', 'e': 'у', 'r': 'к', 't': 'е', 'y': 'н', 'u': 'г', 'i': 'ш', 'o': 'щ', 'p': 'з', '[': 'х', ']': 'ъ', '{': 'х', '}': 'ъ', @@ -29,7 +29,7 @@ const transliterateToEn = (text: string) => Object.entries(transliterationTable) const applyVars = (route: string, vars?: object): string => !vars ? route : Object.entries(vars).reduce((out, [key, value]) => out.replaceAll(`{${key}}`, value), route) -const makeOptions = (items: PrivateWellMenuItem[], vars?: object): OptionType[] => { +const makeOptions = (items: PrivateMenuItem[], vars?: object): OptionType[] => { const out: OptionType[] = [] items.forEach((item) => { if (!hasPermission(item.permissions)) return @@ -78,7 +78,7 @@ export const FastRunMenu = memo(() => { ] if (isURLAvailable('/admin')) - menus.push(makeItem('Панель администратора', '/admin', [], undefined, adminMenuItems as PrivateWellMenuItem[])) + menus.push(makeItem('Панель администратора', '/admin', [], undefined, adminMenuItems as PrivateMenuItem[])) if (well.id) menus.push( diff --git a/src/components/LayoutPortal.tsx b/src/components/LayoutPortal.tsx index c8c3f0b..ef88f30 100644 --- a/src/components/LayoutPortal.tsx +++ b/src/components/LayoutPortal.tsx @@ -1,7 +1,7 @@ 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 } from 'react-router-dom' +import { Link, Outlet, useLocation } from 'react-router-dom' import { ApartmentOutlined, CodeOutlined, @@ -18,7 +18,7 @@ import SuspenseFallback from './SuspenseFallback' import Logo from '@images/Logo' -import '@styles/layout.less' +import '@styles/components/layout.less' const { Content, Sider } = Layout @@ -31,7 +31,7 @@ export type LayoutPortalProps = Omit & { siderProps?: SiderProps & { userMenuProps?: UserMenuProps } isAdmin?: boolean fallback?: JSX.Element - breadcrumb?: boolean | JSX.Element + breadcrumb?: boolean | ((path: string) => JSX.Element) topRightBlock?: JSX.Element } @@ -49,6 +49,7 @@ const _LayoutPortal = memo(() => { const [userMenuOpen, setUserMenuOpen] = useState(false) const [currentWell, setCurrentWell] = useState('') const [props, setProps] = useState(defaultProps) + const location = useLocation() const { isAdmin, title, sheet, showSelector, selectorProps, sider, siderProps, fallback, breadcrumb, topRightBlock, ...other } = useMemo(() => props, [props]) @@ -66,6 +67,8 @@ const _LayoutPortal = memo(() => { makeItem('Профиль', 'profile', , null, () => setUserMenuOpen((prev) => !prev)), ].filter(Boolean) as ItemType[], [isAdmin, currentWell]) + const breadcrumbItems = useMemo(() => typeof breadcrumb === 'function' && breadcrumb(location.pathname), [breadcrumb, location.pathname]) + return ( {(sider || siderProps) && ( @@ -114,7 +117,7 @@ const _LayoutPortal = memo(() => { setWellsTreeOpen((prev) => !prev)}>{currentWell} )} - {breadcrumb !== true && breadcrumb} + {breadcrumbItems} )} {topRightBlock} diff --git a/src/components/LoaderPortal.tsx b/src/components/LoaderPortal.tsx index 770c37a..66bce54 100755 --- a/src/components/LoaderPortal.tsx +++ b/src/components/LoaderPortal.tsx @@ -3,12 +3,19 @@ import { HTMLAttributes } from 'react' import { Loader } from '@components/icons' type LoaderPortalProps = HTMLAttributes & { + /** Показать ли загрузку */ show?: boolean, + /** Затемнять ли дочерний блок */ fade?: boolean, + /** Параметры спиннера */ spinnerProps?: HTMLAttributes, + /** Заполнять ли контент на 100% */ fillContent?: boolean } +/** + * @description Добавляет оверлей загрузки над обёрнутым блоком + */ export const LoaderPortal: React.FC = ({ className = '', show, fade = true, children, spinnerProps, fillContent, ...other }) => (
{children}
diff --git a/src/components/MenuBreadcrumb.tsx b/src/components/MenuBreadcrumb.tsx index 5299039..ccbda20 100644 --- a/src/components/MenuBreadcrumb.tsx +++ b/src/components/MenuBreadcrumb.tsx @@ -2,16 +2,16 @@ import { Breadcrumb, BreadcrumbItemProps } from 'antd' import { Link } from 'react-router-dom' import { join } from 'path' -import { PrivateWellMenuItem } from '@components/PrivateWellMenu' +import { PrivateMenuItem } from '@components/PrivateMenu' import { FunctionalValue, getFunctionalValue, } from '@utils' -export const makeBreadcrumbItems = (items: PrivateWellMenuItem[], pathParts: string[], root: string = '/') => { +export const makeBreadcrumbItems = (items: PrivateMenuItem[], pathParts: string[], root: string = '/') => { const out = [] const parts = [...pathParts] let route = root - let arr: PrivateWellMenuItem[] | undefined = items + let arr: PrivateMenuItem[] | undefined = items while (arr && parts.length > 0) { - const child: PrivateWellMenuItem | undefined = arr.find(elm => elm.route.toLowerCase() === parts[0].toLowerCase()) + const child: PrivateMenuItem | undefined = arr.find(elm => elm.route.toLowerCase() === parts[0].toLowerCase()) if (!child) break route = join(route, child.route) out.push({ ...child, route }) @@ -21,13 +21,12 @@ export const makeBreadcrumbItems = (items: PrivateWellMenuItem[], pathParts: str return out } -export const makeMenuBreadcrumbItems = ( - menuItems: PrivateWellMenuItem[], - path: string, +export const makeMenuBreadcrumbItemsRender = ( + menuItems: PrivateMenuItem[], pathRoot: RegExp = /^\//, - itemsProps?: FunctionalValue<(item: PrivateWellMenuItem) => BreadcrumbItemProps>, - itemRender?: (item: PrivateWellMenuItem) => JSX.Element, -) => { + itemsProps?: FunctionalValue<(item: PrivateMenuItem) => BreadcrumbItemProps>, + itemRender?: (item: PrivateMenuItem) => JSX.Element, +) => (path: string) => { const getItemProps = getFunctionalValue(itemsProps) const rootPart = pathRoot.exec(path) diff --git a/src/components/PrivateWellMenu.tsx b/src/components/PrivateMenu.tsx similarity index 73% rename from src/components/PrivateWellMenu.tsx rename to src/components/PrivateMenu.tsx index 78e6d6c..f19fddf 100644 --- a/src/components/PrivateWellMenu.tsx +++ b/src/components/PrivateMenu.tsx @@ -1,21 +1,21 @@ 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 { Menu, MenuProps } from 'antd' import { hasPermission, Permission } from '@utils' -export type PrivateWellMenuItem = { +export type PrivateMenuItem = { title: string route: string permissions: Permission | Permission[] icon?: ReactNode visible?: boolean - children?: PrivateWellMenuItem[] + children?: PrivateMenuItem[] } -const makeItems = (items: PrivateWellMenuItem[], parentRoute: string, pathParser?: (path: string, parent: string) => string): ItemType[] => { +const makeItems = (items: PrivateMenuItem[], parentRoute: string, pathParser?: (path: string, parent: string) => string): ItemType[] => { return items.map((item) => { if (item.visible === false || !(item.visible === true || hasPermission(item.permissions))) return null @@ -43,11 +43,11 @@ const makeItems = (items: PrivateWellMenuItem[], parentRoute: string, pathParser }).filter(Boolean) } -const makeItemList = (items: PrivateWellMenuItem[], rootPath: string, wellId?: number): ItemType[] => { +const makeItemList = (items: PrivateMenuItem[], rootPath: string, variables: Record): ItemType[] => { const parser = (path: string, parent: string) => { if (!path.startsWith('/')) path = join(parent, path) - return path.replace(/\{wellId\}/, String(wellId)) + return Object.entries(variables).reduce((out, [key, value]) => out.replaceAll(`{${key}}`, String(value)), path) } return makeItems(items, rootPath, parser) @@ -58,9 +58,9 @@ export const makeItem = ( route: string, permissions: Permission | Permission[], icon?: ReactNode, - children?: PrivateWellMenuItem[], + children?: PrivateMenuItem[], visible?: boolean -): PrivateWellMenuItem => ({ +): PrivateMenuItem => ({ title, route, icon, @@ -69,16 +69,16 @@ export const makeItem = ( visible, }) -export type PrivateWellMenuProps = Omit & { - idWell?: number - items: PrivateWellMenuItem[] +export type PrivateMenuProps = Omit & { + variables?: Record + items: PrivateMenuItem[] rootPath?: string } -export const PrivateWellMenu = memo(({ idWell, items, rootPath = '/', ...other }) => { +export const PrivateMenu = memo(({ variables, items, rootPath = '/', ...other }) => { const location = useLocation() - const menuItems = useMemo(() => makeItemList(items, rootPath, idWell), [items, rootPath, idWell]) + const menuItems = useMemo(() => makeItemList(items, rootPath, variables || {}), [items, rootPath, variables]) const tabKeys = useMemo(() => { const out = [] diff --git a/src/components/Table/Columns/date.tsx b/src/components/Table/Columns/date.tsx index a401a04..5342be1 100755 --- a/src/components/Table/Columns/date.tsx +++ b/src/components/Table/Columns/date.tsx @@ -5,6 +5,11 @@ import { DatePickerWrapper, getObjectByDeepKey } from '..' import { DatePickerWrapperProps } from '../DatePickerWrapper' import { formatDate, isRawDate } from '@utils' +/** + * Фабрика методов сортировки столбцов для данных типа **Дата** + * @param key Ключ столбца + * @returns Метод сортировки + */ export const makeDateSorter = (key: Key): SorterMethod => (a, b) => { const vA = a ? getObjectByDeepKey(key, a) : null const vB = b ? getObjectByDeepKey(key, b) : null @@ -16,6 +21,17 @@ export const makeDateSorter = (key: Key): SorterMethod => 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 = ( title: ReactNode, key: string, @@ -24,6 +40,7 @@ export const makeDateColumn = ( other?: ColumnProps, pickerOther?: DatePickerWrapperProps, ) => makeColumn(title, key, { + editable: true, ...other, render: (date) => (
diff --git a/src/components/Table/Columns/index.ts b/src/components/Table/Columns/index.ts index 778bffd..e21c93c 100755 --- a/src/components/Table/Columns/index.ts +++ b/src/components/Table/Columns/index.ts @@ -9,7 +9,6 @@ import { OmitExtends } from '@utils/types' export * from './date' export * from './time' export * from './numeric' -export * from './plan_fact' export * from './select' export * from './tag' export * from './text' @@ -45,6 +44,7 @@ export const makeColumn = (title: ReactNode, key: Key, other?: ColumnPr title: title, key: key, dataIndex: key, + render: (value: T) => value, ...other, }) diff --git a/src/components/Table/Columns/numeric.tsx b/src/components/Table/Columns/numeric.tsx index 36f89b2..c15ee52 100755 --- a/src/components/Table/Columns/numeric.tsx +++ b/src/components/Table/Columns/numeric.tsx @@ -1,4 +1,3 @@ -import { ColumnFilterItem } from 'antd/lib/table/interface' import { InputNumber } from 'antd' import { Key, ReactNode } from 'react' @@ -46,18 +45,17 @@ export const makeNumericColumnOptions = (fixed?: number, sorte export const makeNumericColumn = ( title: ReactNode, key: Key, - filters?: ColumnFilterItem[], - filterDelegate?: FilterGenerator, renderDelegate?: RenderMethod, + filterDelegate?: FilterGenerator, width?: string | number, other?: ColumnProps, ) => makeColumn(title, key, { - filters, + editable: true, onFilter: filterDelegate ? filterDelegate(key) : undefined, sorter: makeNumericSorter(key), width, - input: , - render: renderDelegate ?? makeNumericRender(2), + input: , + render: renderDelegate || makeNumericRender(2), align: 'right', ...other }) @@ -65,54 +63,78 @@ export const makeNumericColumn = ( export const makeNumericColumnPlanFact = ( title: ReactNode, key: Key, - filters?: ColumnFilterItem[], - filterDelegate?: FilterGenerator, renderDelegate?: RenderMethod, + filterDelegate?: FilterGenerator, + width?: string | number, + other?: ColumnProps, +) => { + return { + title, + children: [ + makeNumericColumn('План', `${key}.plan`, renderDelegate, filterDelegate, width, other), + makeNumericColumn('Факт', `${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 = ( + title: ReactNode, + key: Key, + renderDelegate?: RenderMethod, + filterDelegate?: FilterGenerator, width?: string | number, other?: ColumnProps, ) => makeGroupColumn(title, [ - makeNumericColumn('п', key + 'Plan', filters, filterDelegate, renderDelegate, width, other), - makeNumericColumn('ф', key + 'Fact', filters, filterDelegate, renderDelegate, width, other), + makeNumericColumn('План', key + 'Plan', renderDelegate, filterDelegate, width, other), + makeNumericColumn('Факт', key + 'Fact', renderDelegate, filterDelegate, width, other), ]) export const makeNumericStartEnd = ( title: ReactNode, key: Key, fixed: number, - filters?: ColumnFilterItem[], - filterDelegate?: FilterGenerator, renderDelegate?: RenderMethod, + filterDelegate?: FilterGenerator, width?: string | number, ) => makeGroupColumn(title, [ - makeNumericColumn('старт', key + 'Start', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'Start')), - makeNumericColumn('конец', key + 'End', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'End')) + makeNumericColumn('старт', key + 'Start', renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, key + 'Start')), + makeNumericColumn('конец', key + 'End', renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, key + 'End')) ]) export const makeNumericMinMax = ( title: ReactNode, key: Key, fixed: number, - filters?: ColumnFilterItem[], - filterDelegate?: FilterGenerator, renderDelegate?: RenderMethod, + filterDelegate?: FilterGenerator, width?: string | number, ) => makeGroupColumn(title, [ - makeNumericColumn('мин', key + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'Min')), - makeNumericColumn('макс', key + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'Max')), + makeNumericColumn('мин', key + 'Min', renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, key + 'Min')), + makeNumericColumn('макс', key + 'Max', renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, key + 'Max')), ]) export const makeNumericAvgRange = ( title: ReactNode, key: Key, fixed: number, - filters?: ColumnFilterItem[], - filterDelegate?: FilterGenerator, renderDelegate?: RenderMethod, + filterDelegate?: FilterGenerator, width?: string | number, ) => makeGroupColumn(title, [ - makeNumericColumn('мин', `${key}.min`, filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, `${key}.min`)), - makeNumericColumn('сред', `${key}.avg`, filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, `${key}.avg`)), - makeNumericColumn('макс', `${key}.max`, filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, `${key}.max`)), + makeNumericColumn('мин', `${key}.min`, renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, `${key}.min`)), + makeNumericColumn('сред', `${key}.avg`, renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, `${key}.avg`)), + makeNumericColumn('макс', `${key}.max`, renderDelegate, filterDelegate, width, makeNumericColumnOptions(fixed, `${key}.max`)), ]) export default makeNumericColumn diff --git a/src/components/Table/Columns/plan_fact.tsx b/src/components/Table/Columns/plan_fact.tsx deleted file mode 100755 index 852b9e4..0000000 --- a/src/components/Table/Columns/plan_fact.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Key, ReactNode } from 'react' - -import { ColumnProps, makeColumn } from '.' - -export const makeColumnsPlanFact = ( - title: string | ReactNode, - key: Key | [Key, Key], - columsOther?: ColumnProps | [ColumnProps, ColumnProps], -) => { - const keys = Array.isArray(key) ? key : [`${key}Plan`, `${key}Fact`] - const others = Array.isArray(columsOther) ? columsOther : [columsOther, columsOther] - - return { - title, - children: [ - makeColumn('план', keys[0], others[0]), - makeColumn('факт', keys[1], others[1]), - ] - } -} - -export default makeColumnsPlanFact diff --git a/src/components/Table/Columns/select.tsx b/src/components/Table/Columns/select.tsx index 098d983..54e948b 100755 --- a/src/components/Table/Columns/select.tsx +++ b/src/components/Table/Columns/select.tsx @@ -1,9 +1,17 @@ import { Select, SelectProps } from 'antd' import { DefaultOptionType, SelectValue } from 'antd/lib/select' -import { Key, ReactNode } from 'react' +import { Key, ReactNode, useMemo } from 'react' import { ColumnProps, makeColumn } from '.' +const findOption = (value: any, options: T[] | undefined) => + options?.find((option) => String(option?.value) === String(value)) + +const SelectWrapper = ({ value, options, ...other }: SelectProps) => { + const selectValue = useMemo(() => findOption(value, options)?.label, [value, options]) + return , + input: , render: (value, dataset, index) => { - const item = options?.find(option => String(option?.value) === String(value)) + const item = findOption(value, options) return other?.render?.(item, dataset, index) ?? item?.label ?? defaultValue?.label ?? value?.label ?? '--' } }) diff --git a/src/components/Table/Columns/tag.tsx b/src/components/Table/Columns/tag.tsx index e683f19..c83329b 100755 --- a/src/components/Table/Columns/tag.tsx +++ b/src/components/Table/Columns/tag.tsx @@ -65,6 +65,7 @@ export const makeTagColumn = ( const InputComponent = makeTagInput(value_key, label_key) return makeColumn(title, dataIndex, { + editable: true, ...other, render: (item: T[] | undefined, dataset, index) => item?.map((elm: T) => {other?.render?.(elm, dataset, index) ?? elm[label_key]}) ?? '-', input: , diff --git a/src/components/Table/Columns/text.tsx b/src/components/Table/Columns/text.tsx index 91431ad..69db9a3 100755 --- a/src/components/Table/Columns/text.tsx +++ b/src/components/Table/Columns/text.tsx @@ -1,3 +1,4 @@ +import { Tooltip } from 'antd' import { ColumnFilterItem } from 'antd/lib/table/interface' import { Key, ReactNode } from 'react' @@ -15,6 +16,18 @@ export const makeStringSorter = (key: Key): SorterMethod => return String(vA).localeCompare(String(vB)) } +export const makeTextRender = (def = '---', stringCutter?: (text: string) => string) => (value: T) => { + if (!value) return def + if (stringCutter) { + return ( + + {stringCutter(value)} + + ) + } + return value +} + export const makeFilterTextMatch = (key: keyof DataType) => (filterValue: T, dataItem: DataType) => dataItem[key] === filterValue @@ -26,10 +39,11 @@ export const makeTextColumn = ( render?: RenderMethod, other?: ColumnProps ) => makeColumn(title, key, { + editable: true, filters, onFilter: filters ? makeFilterTextMatch(key) : undefined, - sorter: sorter ?? makeStringSorter(key), - render: render, + sorter: sorter || makeStringSorter(key), + render: render || makeTextRender(), ...other }) diff --git a/src/components/Table/Columns/time.tsx b/src/components/Table/Columns/time.tsx index 7547814..116840c 100644 --- a/src/components/Table/Columns/time.tsx +++ b/src/components/Table/Columns/time.tsx @@ -24,6 +24,7 @@ export const makeTimeColumn = ( other?: ColumnProps, pickerOther?: TimePickerWrapperProps, ) => makeColumn(title, key, { + editable: true, ...other, render: (time) => (
diff --git a/src/components/Table/DatePickerWrapper.tsx b/src/components/Table/DatePickerWrapper.tsx index 8aaa9ba..188d985 100755 --- a/src/components/Table/DatePickerWrapper.tsx +++ b/src/components/Table/DatePickerWrapper.tsx @@ -6,8 +6,11 @@ import moment, { Moment } from 'moment' import { defaultFormat } from '@utils' export type DatePickerWrapperProps = PickerDateProps & { + /** Значение селектора */ value?: Moment, + /** Метод вызывается при изменений даты */ onChange?: (date: Moment | null) => any + /** Конвертировать ли значение в UTC */ isUTC?: boolean } diff --git a/src/components/Table/DateRangeWrapper.tsx b/src/components/Table/DateRangeWrapper.tsx index 4ad140f..c9246c5 100755 --- a/src/components/Table/DateRangeWrapper.tsx +++ b/src/components/Table/DateRangeWrapper.tsx @@ -9,31 +9,41 @@ import { defaultFormat } from '@utils' const { RangePicker } = DatePicker export type DateRangeWrapperProps = RangePickerSharedProps & { - value?: RangeValue, + /** Значение селектора в виде массива из 2 элементов (от, до) */ + value?: RangeValue + /** Конвертировать ли значения в UTC */ isUTC?: boolean + /** Разрешить сброс значения селектора */ allowClear?: boolean } +/** + * Подготавливает значения к передаче в селектор + * + * @param value Массиз из 2 дат + * @param isUTC Конвертировать ли значения в UTC + * @returns Подготовленные даты + */ const normalizeDates = (value?: RangeValue, isUTC?: boolean): RangeValue => { - if (!value) return [null, null] - return [ - value[0] ? (isUTC ? moment.utc(value[0]).local() : moment(value[0])) : null, - value[1] ? (isUTC ? moment.utc(value[1]).local() : moment(value[1])) : null, - ] + if (!value) return [null, null] + return [ + value[0] ? (isUTC ? moment.utc(value[0]).local() : moment(value[0])) : null, + value[1] ? (isUTC ? moment.utc(value[1]).local() : moment(value[1])) : null, + ] } -export const DateRangeWrapper = memo(({ value, isUTC, allowClear = false, ...other }) => ( - +export const DateRangeWrapper = memo(({ value, isUTC, allowClear, ...other }) => ( + )) export default DateRangeWrapper diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index c9ecbbe..e1b7f8c 100755 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -1,6 +1,6 @@ import { Key, memo, useCallback, useEffect, useState } from 'react' import { ColumnGroupType, ColumnType } from 'antd/lib/table' -import { Table as RawTable, TableProps } from 'antd' +import { Table as RawTable, TableProps as RawTableProps } from 'antd' import { RenderMethod } from './Columns' import { tryAddKeys } from './EditableTable' @@ -14,16 +14,28 @@ export type BaseTableColumn = ColumnGroupType | ColumnType export type TableColumn = OmitExtends, TableColumnSettings> export type TableColumns = TableColumn[] -export type TableContainer = TableProps & { +export type TableProps = RawTableProps & { + /** Массив колонок таблицы с настройками (описаны в `TableColumnSettings`) */ columns: TableColumn[] + /** Название таблицы для сохранения настроек */ tableName?: string + /** Отображать ли кнопку настроек */ showSettingsChanger?: boolean } export interface DataSet { - [k: Key]: DataSet | T | D + [k: Key]: DataSet | T | D } +/** + * Получить значение из объекта по составному ключу + * + * Составной ключ имеет вид: `<поле 1>[.<поле 2>...]` + * + * @param key Составной ключ + * @param data Объект из которого будет полученно значение + * @returns Значение, найденное по ключу, либо `undefined` + */ export const getObjectByDeepKey = (key: Key | undefined, data: DataSet): T | undefined => { if (!key) return undefined const parts = String(key).split('.') @@ -36,36 +48,44 @@ export const getObjectByDeepKey = (key: Key | undefined, data: DataSet): return out as T } +/** + * Фабрика обёрток render-функций ячеек с поддержкой составных ключей + * @param key Составной ключ + * @param render Стандартная render-функция + * @returns Обёрнутая render-функция + */ export const makeColumnRenderWrapper = >(key: Key | undefined, render: RenderMethod | undefined): RenderMethod => (_: any, dataset: T, index: number) => { const renderFunc: RenderMethod = typeof render === 'function' ? render : (record) => String(record) return renderFunc(getObjectByDeepKey(key, dataset), dataset, index) } - -const applyColumnWrappers = >(columns: BaseTableColumn[]): BaseTableColumn[] => { - return columns.map((column) => { - if ('children' in column) { - return { - ...column, - children: applyColumnWrappers(column.children), - } - } +/** + * Применяет необходимые обёртки ко всем столбцам таблицы + * @param columns Исходные столбцы + * @returns Обёрнутые столбцы + */ +const applyColumnWrappers = >(columns: TableColumns): TableColumns => columns.map((column) => { + if ('children' in column) { return { ...column, - render: makeColumnRenderWrapper(column.key, column.render), + children: applyColumnWrappers(column.children), } - }) -} + } + return { + ...column, + render: makeColumnRenderWrapper(column.key, column.render), + } +}) -function _Table>({ columns, dataSource, tableName, showSettingsChanger, ...other }: TableContainer) { +function _Table>({ columns, dataSource, tableName, showSettingsChanger, ...other }: TableProps) { const [newColumns, setNewColumns] = useState[]>([]) const [settings, setSettings] = useState({}) const onSettingsChanged = useCallback((settings?: TableSettings | null) => { if (tableName) setTableSettings(tableName, settings) - setSettings(settings ?? {}) + setSettings(settings || {}) }, [tableName]) useEffect(() => setSettings(tableName ? getTableSettings(tableName) : {}), [tableName]) @@ -92,6 +112,13 @@ function _Table>({ columns, dataSource, tableName, showSe ) } +/** + * Обёртка над компонентом таблицы AntD + * + * Особенности: + * * Поддержка составных ключей столбцов + * * Работа с настройками столбцов таблицы + */ export const Table = memo(_Table) as typeof _Table export default Table diff --git a/src/components/Table/TimePickerWrapper.tsx b/src/components/Table/TimePickerWrapper.tsx index de748d8..e47848a 100644 --- a/src/components/Table/TimePickerWrapper.tsx +++ b/src/components/Table/TimePickerWrapper.tsx @@ -6,8 +6,11 @@ import { defaultTimeFormat, momentToTime, timeToMoment } from '@utils' import { TimeDto } from '@api' export type TimePickerWrapperProps = Omit, 'onChange'> & { + /** Текущее значение */ value?: TimeDto, + /** Метод вызывается при изменений времени */ onChange?: (date: TimeDto | null) => any + /** Конвертировать ли время в UTC */ isUTC?: boolean } diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx index 3f4eca8..bb9a292 100755 --- a/src/components/Table/index.tsx +++ b/src/components/Table/index.tsx @@ -17,6 +17,13 @@ export type PaginationContainer = { items?: T[] | null } +/** + * Генерирует объект пагинации для компонента `Table` из данных от сервисов + * + * @param сontainer данные от сервиса + * @param other Дополнительные поля (передаются в объект напрямую в приоритете) + * @returns Объект пагинации + */ export const makePaginationObject = (сontainer: PaginationContainer, other: M) => ({ ...other, pageSize: сontainer.take, diff --git a/src/components/UserMenu.tsx b/src/components/UserMenu.tsx index f222420..f9a380b 100755 --- a/src/components/UserMenu.tsx +++ b/src/components/UserMenu.tsx @@ -10,7 +10,7 @@ import { invokeWebApiWrapperAsync } from '@components/factory' import { isURLAvailable, removeUser } from '@utils' import { AuthService } from '@api' -import '@styles/user_menu.less' +import '@styles/components/user_menu.less' export type UserMenuProps = DrawerProps & { isAdmin?: boolean diff --git a/src/components/d3/D3Chart.tsx b/src/components/d3/D3Chart.tsx index fa7822d..c6d37ab 100644 --- a/src/components/d3/D3Chart.tsx +++ b/src/components/d3/D3Chart.tsx @@ -36,7 +36,7 @@ import type { ChartTicks } from './types' -import '@styles/d3.less' +import '@styles/components/d3.less' const defaultOffsets: ChartOffset = { top: 10, diff --git a/src/components/d3/D3HorizontalPercentChart.tsx b/src/components/d3/D3HorizontalPercentChart.tsx index 0b04bd0..17ea77b 100644 --- a/src/components/d3/D3HorizontalPercentChart.tsx +++ b/src/components/d3/D3HorizontalPercentChart.tsx @@ -4,10 +4,10 @@ import { Property } from 'csstype' import * as d3 from 'd3' import LoaderPortal from '@components/LoaderPortal' +import { usePartialProps } from '@utils' import { ChartOffset } from './types' -import '@styles/d3.less' -import { usePartialProps } from '@asb/utils' +import '@styles/components/d3.less' export type PercentChartDataType = { name: string diff --git a/src/components/d3/D3MouseZone.tsx b/src/components/d3/D3MouseZone.tsx index 464d814..72b1730 100644 --- a/src/components/d3/D3MouseZone.tsx +++ b/src/components/d3/D3MouseZone.tsx @@ -3,7 +3,7 @@ import * as d3 from 'd3' import { ChartOffset } from './types' -import '@styles/d3.less' +import '@styles/components/d3.less' export type D3MouseState = { /** Позиция мыши по оси X */ diff --git a/src/components/d3/monitoring/D3HorizontalCursor.tsx b/src/components/d3/monitoring/D3HorizontalCursor.tsx index 97dea97..d1caa2e 100644 --- a/src/components/d3/monitoring/D3HorizontalCursor.tsx +++ b/src/components/d3/monitoring/D3HorizontalCursor.tsx @@ -9,7 +9,7 @@ import { getChartIcon, isDev, usePartialProps } from '@utils' import { BaseDataType } from '../types' import { ChartGroup, ChartSizes } from './D3MonitoringCharts' -import '@styles/d3.less' +import '@styles/components/d3.less' type D3GroupRenderFunction = (group: ChartGroup, data: DataType[], flowData: DataType[] | undefined) => ReactNode diff --git a/src/components/d3/plugins/D3Cursor.tsx b/src/components/d3/plugins/D3Cursor.tsx index 2b2adab..3769981 100644 --- a/src/components/d3/plugins/D3Cursor.tsx +++ b/src/components/d3/plugins/D3Cursor.tsx @@ -6,7 +6,7 @@ import { usePartialProps } from '@utils' import { wrapPlugin } from './base' -import '@styles/d3.less' +import '@styles/components/d3.less' export type D3CursorSettings = { /** Параметры стиля линии */ diff --git a/src/components/d3/plugins/D3Tooltip.tsx b/src/components/d3/plugins/D3Tooltip.tsx index 3517b5f..1d16cb7 100644 --- a/src/components/d3/plugins/D3Tooltip.tsx +++ b/src/components/d3/plugins/D3Tooltip.tsx @@ -8,7 +8,7 @@ import { BaseDataType, ChartRegistry } from '@components/d3/types' import { D3MouseState, useD3MouseZone } from '@components/d3/D3MouseZone' import { getTouchedElements, wrapPlugin } from './base' -import '@styles/d3.less' +import '@styles/components/d3.less' export type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none' diff --git a/src/components/outlets/DepositsOutlet.tsx b/src/components/outlets/DepositsOutlet.tsx index d191222..80e2303 100644 --- a/src/components/outlets/DepositsOutlet.tsx +++ b/src/components/outlets/DepositsOutlet.tsx @@ -1,7 +1,7 @@ import { memo, useEffect, useState } from 'react' import { Outlet } from 'react-router-dom' -import { DepositsContext } from '@asb/context' +import { DepositListContext } from '@asb/context' import LoaderPortal from '@components/LoaderPortal' import { invokeWebApiWrapperAsync } from '@components/factory' import { DepositDto, DepositService } from '@api' @@ -24,11 +24,11 @@ export const DepositsOutlet = memo(() => { }, []) return ( - + - + ) }) diff --git a/src/components/selectors/WellSelector.jsx b/src/components/selectors/WellSelector.jsx index da7d95c..1894021 100755 --- a/src/components/selectors/WellSelector.jsx +++ b/src/components/selectors/WellSelector.jsx @@ -1,7 +1,7 @@ import { Tag, TreeSelect } from 'antd' import { memo, useEffect, useState } from 'react' -import { useDeposits } from '@asb/context' +import { useDepositList } from '@asb/context' import { invokeWebApiWrapperAsync } from '@components/factory' import { hasPermission } from '@utils' @@ -39,7 +39,7 @@ export const WellSelector = memo(({ value, onChange, treeData, treeLabels, ...ot const [wellsTree, setWellsTree] = useState([]) const [wellLabels, setWellLabels] = useState([]) - const deposits = useDeposits() + const deposits = useDepositList() useEffect(() => { invokeWebApiWrapperAsync( @@ -59,6 +59,7 @@ export const WellSelector = memo(({ value, onChange, treeData, treeLabels, ...ot idState === 1 ? 'active' : 'unknown' export const checkIsWellOnline = (lastTelemetryDate: unknown): boolean => isRawDate(lastTelemetryDate) && (Date.now() - +new Date(lastTelemetryDate) < 600_000) -const getKeyByUrl = (url?: string): [Key | null, string | null] => { - const result = url?.match(/^\/([^\/]+)\/([^\/?]+)/) // pattern "/:type/:id" - if (!result) return [null, null] - return [result[0], result[1]] +const getKeyByUrl = (url?: string): [Key | null, string | null, number | null] => { + const result = url?.match(URL_REGEX) // pattern "/:type/:id" + if (!result) return [null, null, null] + return [result[0], result[1], result[2] && result[2] !== 'null' ? Number(result[2]) : null] } const getLabel = (wellsTree: TreeDataNode[], value?: string): string | undefined => { - const [url, type] = getKeyByUrl(value) + const [url, type, key] = getKeyByUrl(value) if (!url) return let deposit: TreeDataNode | undefined let cluster: TreeDataNode | undefined let well: TreeDataNode | undefined switch (type) { case 'deposit': + if (key === null) return 'Месторождение не выбрано' deposit = wellsTree.find((deposit) => deposit.key === url) if (deposit) return `${deposit.title}` return 'Ошибка! Месторождение не найдено!' case 'cluster': + if (key === null) return 'Куст не выбран' deposit = wellsTree.find((deposit) => ( cluster = deposit.children?.find((cluster: TreeDataNode) => cluster.key === url) )) @@ -44,6 +53,7 @@ const getLabel = (wellsTree: TreeDataNode[], value?: string): string | undefined return 'Ошибка! Куст не найден!' case 'well': + if (key === null) return 'Скважина не выбрана' deposit = wellsTree.find((deposit) => ( cluster = deposit.children?.find((cluster: TreeDataNode) => ( well = cluster.children?.find((well: TreeDataNode) => well.key === url) @@ -126,7 +136,7 @@ export const WellTreeSelector = memo(({ expand, current, const navigate = useNavigate() const location = useLocation() - const deposits = useDeposits() + const deposits = useDepositList() const wellsTree = useMemo(() => makeWellsTreeData(deposits), [deposits]) @@ -137,8 +147,8 @@ export const WellTreeSelector = memo(({ expand, current, }, [wellsTree]) const onSelect = useCallback((value: Key[]): void => { - const newRoot = /\/(\w+)\/\d+/.exec(String(value)) - const oldRoot = /\/(\w+)(?:\/\d+)?/.exec(location.pathname) + const newRoot = URL_REGEX.exec(String(value)) + const oldRoot = URL_REGEX.exec(location.pathname) if (!newRoot || !oldRoot) return let newPath = newRoot[0] diff --git a/src/components/views/CompanyView.tsx b/src/components/views/CompanyView.tsx index db012a5..647100a 100755 --- a/src/components/views/CompanyView.tsx +++ b/src/components/views/CompanyView.tsx @@ -9,6 +9,7 @@ export type CompanyViewProps = { company?: CompanyDto } +/** Компонент для отображения информации о компании */ export const CompanyView = memo(({ company }) => company ? ( diff --git a/src/components/views/PermissionView.tsx b/src/components/views/PermissionView.tsx index 7708be5..9c185a7 100755 --- a/src/components/views/PermissionView.tsx +++ b/src/components/views/PermissionView.tsx @@ -8,6 +8,7 @@ export type PermissionViewProps = { info?: PermissionDto } +/** Компонент для отображения информации о разрешении */ export const PermissionView = memo(({ info }) => info ? ( diff --git a/src/components/views/RoleView.tsx b/src/components/views/RoleView.tsx index 8974419..66c0ad3 100755 --- a/src/components/views/RoleView.tsx +++ b/src/components/views/RoleView.tsx @@ -9,6 +9,7 @@ export type RoleViewProps = { role?: UserRoleDto } +/** Компонент для отображения информации о роли */ export const RoleView = memo(({ role }) => { if (!role) return ( - ) diff --git a/src/components/views/TelemetryView.tsx b/src/components/views/TelemetryView.tsx index 8758353..cec6093 100755 --- a/src/components/views/TelemetryView.tsx +++ b/src/components/views/TelemetryView.tsx @@ -19,6 +19,12 @@ export const lables: Record = { spinPlcVersion: 'Версия Спин Мастер', } +/** + * Строит название для телеметрии + * + * @param telemetry Объект телеметрии + * @returns Название + */ export const getTelemetryLabel = (telemetry?: TelemetryDto) => `${telemetry?.id ?? '-'} / ${telemetry?.info?.deposit ?? '-'} / ${telemetry?.info?.cluster ?? '-'} / ${telemetry?.info?.well ?? '-'}` @@ -26,6 +32,7 @@ export type TelemetryViewProps = { telemetry?: TelemetryDto } +/** Компонент для отображения информации о телеметрии */ export const TelemetryView = memo(({ telemetry }) => telemetry?.info ? ( & { user?: UserDto } +/** Компонент для отображения информации о пользователе */ export const UserView = memo(({ user, ...other }) => user ? ( = { export type WellViewProps = TooltipProps & { well?: WellDto + iconProps?: DetailedHTMLProps, HTMLSpanElement> + labelProps?: DetailedHTMLProps, HTMLSpanElement> } -export const WellView = memo(({ well, ...other }) => well ? ( +/** + * Получить название скважины + * @param well Объект с данными скважины + * @returns Название скважины + */ +export const getWellTitle = (well: WellDto) => `${well.deposit || '-'} / ${well.cluster || '-'} / ${well.caption || '-'}` + +/** Компонент для отображения информации о скважине */ +export const WellView = memo(({ well, iconProps, labelProps, ...other }) => well ? ( Название: @@ -47,10 +57,12 @@ export const WellView = memo(({ well, ...other }) => well ? ( {well.id ?? '---'} )}> - + - {well.caption} + + {getWellTitle(well)} + ) : ( - diff --git a/src/components/views/WirelineView.tsx b/src/components/views/WirelineView.tsx index 5ea9bd0..be0b3f9 100644 --- a/src/components/views/WirelineView.tsx +++ b/src/components/views/WirelineView.tsx @@ -21,6 +21,7 @@ export type WirelineViewProps = TooltipProps & { buttonProps?: ButtonProps } +/** Компонент для отображения информации о талевом канате */ export const WirelineView = memo(({ wireline, buttonProps, ...other }) => ( void]>([{}, () => {}]) -/** Контекст текущего корневого пути */ -export const RootPathContext = createContext('/') -/** Контекст текущего пользователя */ -export const UserContext = createContext({}) -/** Контекст метода редактирования параметров заголовка и меню */ -export const LayoutPropsContext = createContext<(props: LayoutPortalProps) => void>(() => {}) -/** Контекст для блока справа от крошек на страницах скважин и админки */ -export const TopRightBlockContext = createContext<(block: JSX.Element) => void>(() => {}) -/** Контекст со списком месторождений */ -export const DepositsContext = createContext([]) - -/** - * Получить текущую скважину - * - * @returns Текущая скважина, либо `null` - */ -export const useWell = () => useContext(WellContext) - -/** - * Получить текущий корневой путь - * - * @returns Текущий корневой путь - */ -export const useRootPath = () => useContext(RootPathContext) - -/** - * Получить текущего пользователя - * - * @returns Текущий пользователь, либо `null` - */ -export const useUser = () => useContext(UserContext) - -/** - * Получить список скважин - * - * @returns Список скважин - */ -export const useDeposits = () => useContext(DepositsContext) - -/** - * Получить метод задания элементов справа от крошек - * - * @returns Метод задания элементов справа от крошек - */ -export const useTopRightBlock = () => useContext(TopRightBlockContext) - -/** - * Получить метод задания параметров заголовка и меню - * - * @returns Получить метод задания параметров заголовка и меню - */ -export const useLayoutProps = (props?: LayoutPortalProps) => { - const setLayoutProps = useContext(LayoutPropsContext) - - useEffect(() => { - if (props) setLayoutProps(props) - }, [setLayoutProps, props]) - - return setLayoutProps -} diff --git a/src/context/deposit.ts b/src/context/deposit.ts new file mode 100644 index 0000000..86fa5a2 --- /dev/null +++ b/src/context/deposit.ts @@ -0,0 +1,23 @@ +import { createContext, useContext } from 'react' + +import { DepositDto } from '@api' + +/** Контекст текущего месторождения */ +export const DepositContext = createContext(null) + +/** + * Получить текущее месторождение + * + * @returns Текущее месторождение, либо `null` + */ +export const useDeposit = () => useContext(DepositContext) + +/** Контекст со списком месторождений */ +export const DepositListContext = createContext([]) + +/** + * Получить список скважин + * + * @returns Список скважин + */ +export const useDepositList = () => useContext(DepositListContext) diff --git a/src/context/index.ts b/src/context/index.ts new file mode 100644 index 0000000..34fcba9 --- /dev/null +++ b/src/context/index.ts @@ -0,0 +1,5 @@ +export * from './deposit' +export * from './layout_props' +export * from './root_path' +export * from './user' +export * from './well' diff --git a/src/context/layout_props.ts b/src/context/layout_props.ts new file mode 100644 index 0000000..cd07ff3 --- /dev/null +++ b/src/context/layout_props.ts @@ -0,0 +1,31 @@ +import { createContext, useContext, useEffect } from 'react' + +import { LayoutPortalProps } from '@components/LayoutPortal' + +/** Контекст метода редактирования параметров заголовка и меню */ +export const LayoutPropsContext = createContext<(props: LayoutPortalProps) => void>(() => {}) + +/** + * Получить метод задания параметров заголовка и меню + * + * @returns Получить метод задания параметров заголовка и меню + */ +export const useLayoutProps = (props?: LayoutPortalProps) => { + const setLayoutProps = useContext(LayoutPropsContext) + + useEffect(() => { + if (props) setLayoutProps(props) + }, [setLayoutProps, props]) + + return setLayoutProps +} + +/** Контекст для блока справа от крошек на страницах скважин и админки */ +export const TopRightBlockContext = createContext<(block: JSX.Element) => void>(() => {}) + +/** + * Получить метод задания элементов справа от крошек + * + * @returns Метод задания элементов справа от крошек + */ +export const useTopRightBlock = () => useContext(TopRightBlockContext) diff --git a/src/context/root_path.ts b/src/context/root_path.ts new file mode 100644 index 0000000..6c03374 --- /dev/null +++ b/src/context/root_path.ts @@ -0,0 +1,11 @@ +import { createContext, useContext } from 'react' + +/** Контекст текущего корневого пути */ +export const RootPathContext = createContext('/') + +/** + * Получить текущий корневой путь + * + * @returns Текущий корневой путь + */ +export const useRootPath = () => useContext(RootPathContext) diff --git a/src/context/user.ts b/src/context/user.ts new file mode 100644 index 0000000..671d565 --- /dev/null +++ b/src/context/user.ts @@ -0,0 +1,13 @@ +import { createContext, useContext } from 'react' + +import { UserTokenDto } from '@api' + +/** Контекст текущего пользователя */ +export const UserContext = createContext({}) + +/** + * Получить текущего пользователя + * + * @returns Текущий пользователь, либо `null` + */ +export const useUser = () => useContext(UserContext) diff --git a/src/context/well.ts b/src/context/well.ts new file mode 100644 index 0000000..54a2075 --- /dev/null +++ b/src/context/well.ts @@ -0,0 +1,13 @@ +import { createContext, useContext } from 'react' + +import { WellDto } from '@api' + +/** Контекст текущей скважины */ +export const WellContext = createContext<[WellDto, (well: WellDto) => void]>([{}, () => {}]) + +/** + * Получить текущую скважину + * + * @returns Текущая скважина, либо пустой объект + */ +export const useWell = () => useContext(WellContext) diff --git a/src/images/AsbLogo.svg b/src/images/AsbLogo.svg new file mode 100644 index 0000000..1707292 --- /dev/null +++ b/src/images/AsbLogo.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/images/Logo.svg b/src/images/Logo.svg new file mode 100644 index 0000000..dbafbae --- /dev/null +++ b/src/images/Logo.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/images/Logo.tsx b/src/images/Logo.tsx index 2dd724b..32f9634 100755 --- a/src/images/Logo.tsx +++ b/src/images/Logo.tsx @@ -1,44 +1,14 @@ import { memo } from 'react' +import { ReactComponent as RawLogo } from './Logo.svg' + export type LogoProps = React.SVGProps & { size?: number onlyIcon?: boolean } export const Logo = memo(({ size = 170, onlyIcon, ...props }) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + )) export default Logo diff --git a/src/pages/AdminPanel/AdminNavigationMenu.jsx b/src/pages/AdminPanel/AdminNavigationMenu.jsx index 0474af2..98a098a 100644 --- a/src/pages/AdminPanel/AdminNavigationMenu.jsx +++ b/src/pages/AdminPanel/AdminNavigationMenu.jsx @@ -12,7 +12,7 @@ import { UserOutlined, } from '@ant-design/icons' -import { makeItem, PrivateWellMenu } from '@components/PrivateWellMenu' +import { makeItem, PrivateMenu } from '@components/PrivateMenu' import { isDev } from '@utils' export const menuItems = [ @@ -33,7 +33,7 @@ export const menuItems = [ ].filter(Boolean) export const AdminNavigationMenu = memo((props) => ( - { const clusterColumns = useMemo(() => [ makeSelectColumn('Месторождение', 'idDeposit', deposits, '--', { width: 200, - editable: true, sorter: makeStringSorter('idDeposit') }), - makeColumn('Название', 'caption', { - width: 200, - editable: true, - sorter: makeStringSorter('caption'), - formItemRules: min1, - }), - makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFormat }), - makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFormat }), + makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: 200, formItemRules: min1 }), + makeNumericColumn('Широта', 'latitude', coordsFormat, undefined, 150), + makeNumericColumn('Долгота', 'longitude', coordsFormat, undefined, 150), makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }), ], [deposits]) diff --git a/src/pages/AdminPanel/CompanyController.jsx b/src/pages/AdminPanel/CompanyController.jsx index 454b16c..5577e1f 100755 --- a/src/pages/AdminPanel/CompanyController.jsx +++ b/src/pages/AdminPanel/CompanyController.jsx @@ -3,10 +3,9 @@ import { Input } from 'antd' import { EditableTable, - makeColumn, - makeStringSorter, makeSelectColumn, - defaultPagination + defaultPagination, + makeTextColumn } from '@components/Table' import { invokeWebApiWrapperAsync } from '@components/factory' import { AdminCompanyService, AdminCompanyTypeService } from '@api' @@ -37,16 +36,8 @@ const CompanyController = memo(() => { })) setColumns([ - makeColumn('Название', 'caption', { - width: 200, - editable: true, - sorter: makeStringSorter('caption'), - formItemRules: min1, - }), - makeSelectColumn('Тип компании', 'idCompanyType', companyTypes, null, { - width: 200, - editable: true - }), + makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: 200, formItemRules: min1 }), + makeSelectColumn('Тип компании', 'idCompanyType', companyTypes, null, { width: 200 }), ]) await updateTable() diff --git a/src/pages/AdminPanel/CompanyTypeController.jsx b/src/pages/AdminPanel/CompanyTypeController.jsx index 3debe2c..601d1aa 100755 --- a/src/pages/AdminPanel/CompanyTypeController.jsx +++ b/src/pages/AdminPanel/CompanyTypeController.jsx @@ -1,19 +1,14 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { Input } from 'antd' -import { EditableTable, makeColumn, makeStringSorter, defaultPagination } from '@components/Table' +import { EditableTable, defaultPagination, makeTextColumn } from '@components/Table' import { invokeWebApiWrapperAsync } from '@components/factory' import { arrayOrDefault, withPermissions } from '@utils' import { min1 } from '@utils/validationRules' import { AdminCompanyTypeService } from '@api' const columns = [ - makeColumn('Название', 'caption', { - width: 200, - editable: true, - sorter: makeStringSorter('caption'), - formItemRules: min1, - }), + makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: 200, formItemRules: min1 }), ] const CompanyTypeController = memo(() => { diff --git a/src/pages/AdminPanel/DepositController.jsx b/src/pages/AdminPanel/DepositController.jsx index 748490b..974a420 100755 --- a/src/pages/AdminPanel/DepositController.jsx +++ b/src/pages/AdminPanel/DepositController.jsx @@ -2,15 +2,15 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { Input } from 'antd' import { invokeWebApiWrapperAsync } from '@components/factory' -import { EditableTable, makeColumn, defaultPagination, makeTimezoneColumn } from '@components/Table' +import { EditableTable, defaultPagination, makeTimezoneColumn, makeTextColumn, makeNumericColumn } from '@components/Table' import { arrayOrDefault, coordsFormat, withPermissions } from '@utils' import { min1 } from '@utils/validationRules' import { AdminDepositService } from '@api' const depositColumns = [ - makeColumn('Название', 'caption', { width: 200, editable: true, formItemRules: min1 }), - makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFormat }), - makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFormat }), + makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: 200, formItemRules: min1 }), + makeNumericColumn('Широта', 'latitude', coordsFormat, undefined, 150), + makeNumericColumn('Долгота', 'longitude', coordsFormat, undefined, 150), makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }), ] diff --git a/src/pages/AdminPanel/PermissionController.jsx b/src/pages/AdminPanel/PermissionController.jsx index 73db56a..0ee736e 100755 --- a/src/pages/AdminPanel/PermissionController.jsx +++ b/src/pages/AdminPanel/PermissionController.jsx @@ -1,23 +1,15 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { Input } from 'antd' -import { EditableTable, makeColumn, makeStringSorter } from '@components/Table' +import { EditableTable, makeTextColumn } from '@components/Table' import { invokeWebApiWrapperAsync } from '@components/factory' import { arrayOrDefault, withPermissions } from '@utils' import { min1 } from '@utils/validationRules' import { AdminPermissionService } from '@api' const columns = [ - makeColumn('Название', 'name', { - editable: true, - sorter: makeStringSorter('name'), - isRequired: true, - formItemRules: min1, - }), - makeColumn('Описание', 'description', { - editable: true, - sorter: makeStringSorter('description'), - }), + makeTextColumn('Название', 'name', undefined, undefined, undefined, { isRequired: true, formItemRules: min1 }), + makeTextColumn('Описание', 'description'), ] const PermissionController = memo(() => { diff --git a/src/pages/AdminPanel/RoleController.jsx b/src/pages/AdminPanel/RoleController.jsx index b995fde..6276ac5 100755 --- a/src/pages/AdminPanel/RoleController.jsx +++ b/src/pages/AdminPanel/RoleController.jsx @@ -19,15 +19,13 @@ const RoleController = memo(() => { )), [roles, searchValue]) const columns = useMemo(() => [ - makeTextColumn('Название', 'caption', null, null, null, { width: 100, editable: true, formItemRules: min1 }), + makeTextColumn('Название', 'caption', null, null, null, { width: 100, formItemRules: min1 }), makeTagColumn('Включённые роли', 'roles', roles, 'id', 'caption', { width: 400, - editable: true, render: (role) => }, { allowClear: true }), makeTagColumn('Разрешения', 'permissions', permissions, 'id', 'name', { width: 600, - editable: true, render: (permission) => , }), ], [roles, permissions]) diff --git a/src/pages/AdminPanel/Telemetry/TelemetryViewer.jsx b/src/pages/AdminPanel/Telemetry/TelemetryViewer.jsx index f90ff30..3836aff 100755 --- a/src/pages/AdminPanel/Telemetry/TelemetryViewer.jsx +++ b/src/pages/AdminPanel/Telemetry/TelemetryViewer.jsx @@ -50,7 +50,7 @@ const TelemetryController = memo(() => { const columns = useMemo(() => [ makeColumn('', 'hasParent', { render: mergeRender }), - makeNumericColumn('ID', 'id', null, null, makeNumericRender(0)), + makeNumericColumn('ID', 'id', makeNumericRender(0)), makeTextColumn('UID', 'remoteUid'), makeTextColumn('Назначена на скважину', 'realWell'), makeDateColumn('Дата начала бурения', 'drillingStartDate'), diff --git a/src/pages/AdminPanel/UserController/index.jsx b/src/pages/AdminPanel/UserController/index.jsx index 2f6b301..5bb3ef2 100755 --- a/src/pages/AdminPanel/UserController/index.jsx +++ b/src/pages/AdminPanel/UserController/index.jsx @@ -115,7 +115,6 @@ const UserController = memo(() => { setColumns([ makeTextColumn('Логин', 'login', null, null, null, { - editable: true, formItemRules: [ { required: true }, ...createLoginRules, @@ -130,41 +129,34 @@ const UserController = memo(() => { ], }), makeTextColumn('Фамилия', 'surname', filters.surname, null, null, { - editable: true, formItemRules: [{ required: true }, ...nameRules], filterSearch: true, onFilter: makeTextOnFilter('surname'), }), makeTextColumn('Имя', 'name', filters.name, null, null, { - editable: true, formItemRules: nameRules, filterSearch: true, onFilter: makeTextOnFilter('name'), }), makeTextColumn('Отчество', 'patronymic', filters.partonymic, null, null, { - editable: true, formItemRules: nameRules, filterSearch: true, onFilter: makeTextOnFilter('patronymic'), }), makeTextColumn('E-mail', 'email', filters.email, null, null, { - editable: true, formItemRules: [{ required: true }, ...emailRules], filterSearch: true, onFilter: makeTextOnFilter('email'), }), makeTextColumn('Номер телефона', 'phone', null, null, null, { - editable: true, formItemRules: phoneRules, }), - makeTextColumn('Должность', 'position', null, null, null, { editable: true }), + makeTextColumn('Должность', 'position', null, null, null), makeTextColumn('Роли', 'roleNames', roleFilters, null, rolesRender, { - editable: true, input: , onFilter: makeArrayOnFilter('roleNames'), }), makeSelectColumn('Компания', 'idCompany', companies, '--', { - editable: true, sorter: makeNumericSorter('idCompany'), }) ]) diff --git a/src/pages/AdminPanel/WellController/index.jsx b/src/pages/AdminPanel/WellController/index.jsx index 6fe163e..ed74ca6 100755 --- a/src/pages/AdminPanel/WellController/index.jsx +++ b/src/pages/AdminPanel/WellController/index.jsx @@ -12,11 +12,12 @@ import { EditableTable, makeColumn, makeSelectColumn, - makeStringSorter, makeNumericSorter, makeTagColumn, defaultPagination, makeTimezoneColumn, + makeTextColumn, + makeNumericColumn, } from '@components/Table' import { invokeWebApiWrapperAsync } from '@components/factory' import { TelemetryView, CompanyView } from '@components/views' @@ -81,23 +82,11 @@ const WellController = memo(() => { })) setColumns([ - makeSelectColumn('Куст', 'idCluster', clusters, '--', { - width: '5rem', - editable: true, - sorter: makeNumericSorter('idCluster'), - }), - makeColumn('Название', 'caption', { - width: '5rem', - editable: true, - sorter: makeStringSorter('caption'), - }), - makeSelectColumn('Тип', 'idWellType', wellTypes, '--', { - width: 150, - editable: true, - sorter: makeNumericSorter('idWellType'), - }), - makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFormat }), - makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFormat }), + makeSelectColumn('Куст', 'idCluster', clusters, '--', { width: '5rem', sorter: makeNumericSorter('idCluster') }), + makeTextColumn('Название', 'caption', undefined, undefined, undefined, { width: '5rem' }), + makeSelectColumn('Тип', 'idWellType', wellTypes, '--', { width: 150, sorter: makeNumericSorter('idWellType') }), + makeNumericColumn('Широта', 'latitude', coordsFormat, undefined, 150), + makeNumericColumn('Долгота', 'longitude', coordsFormat, undefined, 150), makeColumn('Телеметрия', 'telemetry', { editable: true, render: (telemetry) => , @@ -105,7 +94,6 @@ const WellController = memo(() => { }, ), makeTimezoneColumn('Зона', 'timezone', null, true, { width: 170 }), makeTagColumn('Компании', 'companies', companies, 'id', 'caption', { - editable: true, render: (company) => , }), ]) diff --git a/src/pages/AdminPanel/index.jsx b/src/pages/AdminPanel/index.jsx index e4c11b8..98c8e65 100755 --- a/src/pages/AdminPanel/index.jsx +++ b/src/pages/AdminPanel/index.jsx @@ -1,9 +1,9 @@ -import { Navigate, Route, Routes, useLocation } from 'react-router-dom' +import { Navigate, Route, Routes } from 'react-router-dom' import { lazy, memo, useEffect, useMemo } from 'react' import { RootPathContext, useLayoutProps, useRootPath } from '@asb/context' import { FastRunMenu } from '@components/FastRunMenu' -import { makeMenuBreadcrumbItems } from '@components/MenuBreadcrumb' +import { makeMenuBreadcrumbItemsRender } from '@components/MenuBreadcrumb' import { NoAccessComponent, withPermissions } from '@utils' import { AdminNavigationMenu, menuItems } from './AdminNavigationMenu' @@ -21,21 +21,18 @@ const TelemetryViewer = lazy(() => import('./Telemetry/TelemetryViewer')) const TelemetryMerger = lazy(() => import('./Telemetry/TelemetryMerger')) const VisitLog = lazy(() => import('./VisitLog')) +const layoutProps = { + sider: , + title: 'Администраторская панель', + isAdmin: true, + breadcrumb: makeMenuBreadcrumbItemsRender(menuItems, /^\/admin\//), +} + const AdminPanel = memo(() => { - const location = useLocation() const root = useRootPath() const rootPath = useMemo(() => `${root}/admin`, [root]) - const setLayoutProps = useLayoutProps() - - useEffect(() => { - setLayoutProps({ - sider: , - title: 'Администраторская панель', - isAdmin: true, - breadcrumb: makeMenuBreadcrumbItems(menuItems, location.pathname, /^\/admin\//), - }) - }, [location.pathname]) + useLayoutProps(layoutProps) return ( diff --git a/src/pages/Cluster/ClusterWells.jsx b/src/pages/Cluster/ClusterWells.jsx index a84e9cb..81410ec 100755 --- a/src/pages/Cluster/ClusterWells.jsx +++ b/src/pages/Cluster/ClusterWells.jsx @@ -7,7 +7,7 @@ import { makeTextColumn, makeGroupColumn, makeColumn, - makeNumericColumnPlanFact, + makeNumericColumnPlanFactOld, Table, makeNumericRender, makeNumericColumn, @@ -117,7 +117,10 @@ const ClusterWells = memo(({ statsWells }) => { const columns = useMemo(() => [ makeTextColumn('скв №', 'caption', null, null, (_, item) => ( - + { makeDateColumn('начало', 'factStart'), makeDateColumn('окончание', 'factEnd'), ]), - makeNumericColumnPlanFact('Продолжительность, сут', 'period', filtersMinMax, makeFilterMinMaxFunction, numericRender), - makeNumericColumnPlanFact('МСП, м/ч', 'rateOfPenetration', filtersMinMax, makeFilterMinMaxFunction, numericRender), - makeNumericColumnPlanFact('Рейсовая скорость, м/ч', 'routeSpeed', filtersMinMax, makeFilterMinMaxFunction, numericRender), - makeNumericColumn('НПВ, ч', 'notProductiveTimeFact', filtersMinMax, makeFilterMinMaxFunction, numericRender), + makeNumericColumnPlanFactOld('Продолжительность, сут', 'period', numericRender, makeFilterMinMaxFunction, { filters: filtersMinMax }), + makeNumericColumnPlanFactOld('МСП, м/ч', 'rateOfPenetration', numericRender, makeFilterMinMaxFunction, { filters: filtersMinMax }), + makeNumericColumnPlanFactOld('Рейсовая скорость, м/ч', 'routeSpeed', numericRender, makeFilterMinMaxFunction, { filters: filtersMinMax }), + makeNumericColumn('НПВ, ч', 'notProductiveTimeFact', numericRender, makeFilterMinMaxFunction, { filters: filtersMinMax }), makeColumn('TVD', 'tvd', { align: 'center', render: (_, value) => (