forked from ddrilling/asb_cloud_front
Merge remote-tracking branch 'origin/dev'
This commit is contained in:
commit
d7669a2317
4
.gitignore
vendored
4
.gitignore
vendored
@ -11,8 +11,10 @@
|
|||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# production
|
# build directories
|
||||||
/build
|
/build
|
||||||
|
/dev_build
|
||||||
|
/prod_build
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
@ -38,10 +38,10 @@ npx openapi -i http://{IP_ADDRESS}:{PORT}/swagger/v1/swagger.json -o src/service
|
|||||||
|
|
||||||
| IP-адрес | Описание |
|
| IP-адрес | Описание |
|
||||||
|:-|:-|
|
|:-|:-|
|
||||||
| 127.0.0.1:5000 | Локальный адрес вашей машины (привязан к `update_openapi`) |
|
| 127.0.0.1:5000 | Локальный адрес вашей машины (привязан к `update_openapi`) |
|
||||||
| 192.168.1.70:5000 | Локальный адрес development-сервера (привязан к `update_openapi_server`) |
|
| 192.168.1.113:5000 | Локальный адрес development-сервера (привязан к `update_openapi_server`) |
|
||||||
| 46.146.209.148:89 | Внешний адрес development-сервера |
|
| 46.146.209.148:89 | Внешний адрес development-сервера |
|
||||||
| 46.146.209.148 | Внешний адрес production-сервера |
|
| cloud.digitaldrilling.ru | Внешний адрес production-сервера |
|
||||||
|
|
||||||
## 3. Компиляция production-версии приложения
|
## 3. Компиляция production-версии приложения
|
||||||
После выполнения вышеописанных пунктов приложение готово к компиляции.
|
После выполнения вышеописанных пунктов приложение готово к компиляции.
|
||||||
|
15925
package-lock.json
generated
15925
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@ -16,12 +16,18 @@
|
|||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "webpack-dev-server --mode=development --open --hot",
|
|
||||||
"build": "webpack --mode=production",
|
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
|
"build": "webpack --env=\"ENV=prod\"",
|
||||||
|
"dev_build": "webpack --env=\"ENV=dev\"",
|
||||||
|
"prod_build": "webpack --env=\"ENV=prod\"",
|
||||||
|
|
||||||
|
"start": "webpack-dev-server --env=\"ENV=dev\" --open --hot",
|
||||||
|
"prod": "webpack-dev-server --env=\"ENV=prod\" --open --hot",
|
||||||
|
"dev": "webpack-dev-server --env=\"ENV=dev\" --open --hot",
|
||||||
|
|
||||||
"oul": "npx openapi -i http://127.0.0.1:5000/swagger/v1/swagger.json -o src/services/api",
|
"oul": "npx openapi -i http://127.0.0.1:5000/swagger/v1/swagger.json -o src/services/api",
|
||||||
"oud": "npx openapi -i http://192.168.1.70:5000/swagger/v1/swagger.json -o src/services/api",
|
"oud": "npx openapi -i http://192.168.1.113:5000/swagger/v1/swagger.json -o src/services/api",
|
||||||
"oug": "npx openapi -i http://46.146.209.148/swagger/v1/swagger.json -o src/services/api",
|
"oug": "npx openapi -i https://cloud.digitaldrilling.ru/swagger/v1/swagger.json -o src/services/api",
|
||||||
"oug_dev": "npx openapi -i http://46.146.209.148:89/swagger/v1/swagger.json -o src/services/api"
|
"oug_dev": "npx openapi -i http://46.146.209.148:89/swagger/v1/swagger.json -o src/services/api"
|
||||||
},
|
},
|
||||||
"proxy": "http://46.146.209.148:89",
|
"proxy": "http://46.146.209.148:89",
|
||||||
@ -83,24 +89,30 @@
|
|||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"babel-jest": "^28.1.0",
|
"babel-jest": "^28.1.0",
|
||||||
"babel-loader": "^8.2.5",
|
"babel-loader": "^8.2.5",
|
||||||
|
"colors": "^1.4.0",
|
||||||
"css-loader": "^6.7.1",
|
"css-loader": "^6.7.1",
|
||||||
|
"css-minimizer-webpack-plugin": "^4.2.0",
|
||||||
|
"extract-loader": "^5.1.0",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"interpolate-html-plugin": "^4.0.0",
|
"interpolate-html-plugin": "^4.0.0",
|
||||||
"jest": "^28.1.0",
|
"jest": "^28.1.0",
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"less-loader": "^11.0.0",
|
"less-loader": "^11.0.0",
|
||||||
|
"mini-css-extract-plugin": "^2.6.1",
|
||||||
"openapi-typescript": "^5.4.0",
|
"openapi-typescript": "^5.4.0",
|
||||||
"openapi-typescript-codegen": "^0.23.0",
|
"openapi-typescript-codegen": "^0.23.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"react-test-renderer": "^18.1.0",
|
"react-test-renderer": "^18.1.0",
|
||||||
"source-map-loader": "^3.0.1",
|
"source-map-loader": "^3.0.1",
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^3.3.1",
|
||||||
|
"terser-webpack-plugin": "^5.3.6",
|
||||||
"ts-loader": "^9.3.0",
|
"ts-loader": "^9.3.0",
|
||||||
"typescript": "^4.7.4",
|
"typescript": "^4.7.4",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"webpack": "^5.73.0",
|
"webpack": "^5.73.0",
|
||||||
"webpack-cli": "^4.9.2",
|
"webpack-cli": "^4.9.2",
|
||||||
"webpack-dev-server": "^4.9.1"
|
"webpack-dev-server": "^4.9.1",
|
||||||
|
"webpack-merge": "^5.8.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"short_name": "React App",
|
"short_name": "ЕЦП",
|
||||||
"name": "Create React App Sample",
|
"name": "Единая Цифровая Платформа",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
|
72
src/App.tsx
72
src/App.tsx
@ -1,48 +1,62 @@
|
|||||||
import { BrowserRouter as Router, Navigate, Route, Routes } from 'react-router-dom'
|
import { BrowserRouter as Router, Navigate, Route, Routes } from 'react-router-dom'
|
||||||
import { memo } from 'react'
|
import { lazy, memo, Suspense } from 'react'
|
||||||
import { ConfigProvider } from 'antd'
|
|
||||||
import locale from 'antd/lib/locale/ru_RU'
|
import locale from 'antd/lib/locale/ru_RU'
|
||||||
|
import { ConfigProvider } from 'antd'
|
||||||
|
|
||||||
import { RootPathContext } from '@asb/context'
|
import { RootPathContext } from '@asb/context'
|
||||||
import { getUserToken, NoAccessComponent } from '@utils'
|
import { UserOutlet } from '@components/outlets'
|
||||||
|
import LayoutPortal from '@components/LayoutPortal'
|
||||||
|
import SuspenseFallback from '@components/SuspenseFallback'
|
||||||
|
import { getUser, NoAccessComponent } from '@utils'
|
||||||
import { OpenAPI } from '@api'
|
import { OpenAPI } from '@api'
|
||||||
|
|
||||||
import AdminPanel from '@pages/AdminPanel'
|
import '@styles/include/antd_theme.less'
|
||||||
import Well from '@pages/Well'
|
|
||||||
import Login from '@pages/Login'
|
|
||||||
import Cluster from '@pages/Cluster'
|
|
||||||
import Deposit from '@pages/Deposit'
|
|
||||||
import Register from '@pages/Register'
|
|
||||||
import FileDownload from '@pages/FileDownload'
|
|
||||||
|
|
||||||
import '@styles/App.less'
|
import '@styles/App.less'
|
||||||
|
|
||||||
|
const Login = lazy(() => import('@pages/public/Login'))
|
||||||
|
const Register = lazy(() => import('@pages/public/Register'))
|
||||||
|
const FileDownload = lazy(() => import('@pages/FileDownload'))
|
||||||
|
|
||||||
|
const AdminPanel = lazy(() => import('@pages/AdminPanel'))
|
||||||
|
const Deposit = lazy(() => import('@pages/Deposit'))
|
||||||
|
const Cluster = lazy(() => import('@pages/Cluster'))
|
||||||
|
const Well = lazy(() => import('@asb/pages/Well'))
|
||||||
|
|
||||||
//OpenAPI.BASE = 'http://localhost:3000'
|
//OpenAPI.BASE = 'http://localhost:3000'
|
||||||
OpenAPI.TOKEN = async () => getUserToken() ?? ''
|
// TODO: Удалить взятие из 'token' в следующем релизе, вставлено для совместимости
|
||||||
OpenAPI.HEADERS = {'Content-Type': 'application/json'}
|
OpenAPI.TOKEN = async () => getUser().token || localStorage.getItem('token') || ''
|
||||||
|
OpenAPI.HEADERS = { 'Content-Type': 'application/json' }
|
||||||
|
|
||||||
export const App = memo(() => (
|
export const App = memo(() => (
|
||||||
<ConfigProvider locale={locale}>
|
<ConfigProvider locale={locale}>
|
||||||
<RootPathContext.Provider value={''}>
|
<RootPathContext.Provider value={''}>
|
||||||
<Router>
|
<Suspense fallback={<SuspenseFallback style={{ minHeight: '100vh' }} />}>
|
||||||
<Routes>
|
<Router>
|
||||||
<Route index element={<Navigate to={Deposit.getKey()} replace />} />
|
<Routes>
|
||||||
<Route path={'*'} element={<NoAccessComponent />} />
|
<Route index element={<Navigate to={'deposit'} replace />} />
|
||||||
|
<Route path={'*'} element={<NoAccessComponent />} />
|
||||||
|
|
||||||
{/* Public pages */}
|
{/* Public pages */}
|
||||||
<Route path={Login.route} element={<Login />} />
|
<Route path={'/login'} element={<Login />} />
|
||||||
<Route path={Register.route} element={<Register />} />
|
<Route path={'/register'} element={<Register />} />
|
||||||
|
|
||||||
{/* Admin pages */}
|
{/* User pages */}
|
||||||
<Route path={AdminPanel.route} element={<AdminPanel />} />
|
<Route element={<UserOutlet />}>
|
||||||
|
<Route path={'/file_download/:idWell/:idFile/*'} element={<FileDownload />} />
|
||||||
|
|
||||||
{/* User pages */}
|
<Route element={<LayoutPortal />}>
|
||||||
<Route path={Deposit.route} element={<Deposit />} />
|
{/* Admin pages */}
|
||||||
<Route path={Cluster.route} element={<Cluster />} />
|
<Route path={'/admin/*'} element={<AdminPanel />} />
|
||||||
<Route path={Well.route} element={<Well />} />
|
|
||||||
<Route path={FileDownload.route} element={<FileDownload />} />
|
{/* Client pages */}
|
||||||
</Routes>
|
<Route path={'/deposit/*'} element={<Deposit />} />
|
||||||
</Router>
|
<Route path={'/cluster/:idCluster'} element={<Cluster />} />
|
||||||
|
<Route path={'/well/:idWell/*'} element={<Well />} />
|
||||||
|
</Route>
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
</Suspense>
|
||||||
</RootPathContext.Provider>
|
</RootPathContext.Provider>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
))
|
))
|
||||||
|
@ -2,8 +2,8 @@ import { memo, useCallback, useMemo, useState } from 'react'
|
|||||||
import { Rule } from 'antd/lib/form'
|
import { Rule } from 'antd/lib/form'
|
||||||
import { Form, Input, Modal, FormProps } from 'antd'
|
import { Form, Input, Modal, FormProps } from 'antd'
|
||||||
|
|
||||||
|
import { useUser } from '@asb/context'
|
||||||
import { AuthService, UserDto } from '@api'
|
import { AuthService, UserDto } from '@api'
|
||||||
import { getUserId, getUserLogin } from '@utils'
|
|
||||||
import { passwordRules, createPasswordRules } from '@utils/validationRules'
|
import { passwordRules, createPasswordRules } from '@utils/validationRules'
|
||||||
|
|
||||||
import LoaderPortal from './LoaderPortal'
|
import LoaderPortal from './LoaderPortal'
|
||||||
@ -31,7 +31,8 @@ export const ChangePassword = memo<ChangePasswordProps>(({ user, visible, onCanc
|
|||||||
const [showLoader, setShowLoader] = useState<boolean>(false)
|
const [showLoader, setShowLoader] = useState<boolean>(false)
|
||||||
const [isDisabled, setIsDisabled] = useState(true)
|
const [isDisabled, setIsDisabled] = useState(true)
|
||||||
|
|
||||||
const userData = useMemo(() => user ?? { id: getUserId(), login: getUserLogin() } as UserDto, [user])
|
const userContext = useUser()
|
||||||
|
const userData = useMemo(() => user ?? userContext, [user])
|
||||||
|
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
|
|
||||||
@ -63,7 +64,7 @@ export const ChangePassword = memo<ChangePasswordProps>(({ user, visible, onCanc
|
|||||||
{user && <> (<UserView user={user} />)</>}
|
{user && <> (<UserView user={user} />)</>}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
visible={visible}
|
open={visible}
|
||||||
onCancel={onModalCancel}
|
onCancel={onModalCancel}
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
okText={'Сохранить'}
|
okText={'Сохранить'}
|
||||||
|
@ -106,7 +106,7 @@ export const ColorPicker = memo<ColorPickerProps>(({ value = '#AA33BB', onChange
|
|||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
trigger={'click'}
|
trigger={'click'}
|
||||||
onVisibleChange={onClose}
|
onOpenChange={onClose}
|
||||||
content={(
|
content={(
|
||||||
<div className={'asb-color-picker-content'}>
|
<div className={'asb-color-picker-content'}>
|
||||||
<div className={'asb-color-picker-sliders'}>
|
<div className={'asb-color-picker-sliders'}>
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
import { memo, ReactNode } from 'react'
|
|
||||||
import { Link, useLocation } from 'react-router-dom'
|
|
||||||
import { Button, Layout, LayoutProps } from 'antd'
|
|
||||||
|
|
||||||
import PageHeader from '@components/PageHeader'
|
|
||||||
|
|
||||||
export type AdminLayoutPortalProps = LayoutProps & {
|
|
||||||
title?: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AdminLayoutPortal = memo<AdminLayoutPortalProps>(({ title, ...props }) => {
|
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout.Content>
|
|
||||||
<PageHeader isAdmin title={title} style={{ backgroundColor: '#900' }}>
|
|
||||||
<Button size={'large'}>
|
|
||||||
<Link to={'/'}>Вернуться на сайт</Link>
|
|
||||||
</Button>
|
|
||||||
</PageHeader>
|
|
||||||
<Layout>
|
|
||||||
<Layout.Content className={'site-layout-background sheet'} {...props}/>
|
|
||||||
</Layout>
|
|
||||||
</Layout.Content>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default AdminLayoutPortal
|
|
@ -1,31 +0,0 @@
|
|||||||
import { memo, ReactNode } from 'react'
|
|
||||||
import { Layout, LayoutProps } from 'antd'
|
|
||||||
|
|
||||||
import PageHeader from '@components/PageHeader'
|
|
||||||
import WellTreeSelector from '@components/selectors/WellTreeSelector'
|
|
||||||
import { wrapPrivateComponent } from '@utils'
|
|
||||||
|
|
||||||
export type LayoutPortalProps = LayoutProps & {
|
|
||||||
title?: ReactNode
|
|
||||||
noSheet?: boolean
|
|
||||||
showSelector?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const _LayoutPortal = memo<LayoutPortalProps>(({ title, noSheet, showSelector, ...props }) => (
|
|
||||||
<Layout.Content>
|
|
||||||
<PageHeader title={title}>
|
|
||||||
<WellTreeSelector show={showSelector} />
|
|
||||||
</PageHeader>
|
|
||||||
<Layout>
|
|
||||||
{noSheet ? props.children : (
|
|
||||||
<Layout.Content className={'site-layout-background sheet'} {...props}/>
|
|
||||||
)}
|
|
||||||
</Layout>
|
|
||||||
</Layout.Content>
|
|
||||||
))
|
|
||||||
|
|
||||||
export const LayoutPortal = wrapPrivateComponent(_LayoutPortal, {
|
|
||||||
requirements: ['Deposit.get'],
|
|
||||||
})
|
|
||||||
|
|
||||||
export default LayoutPortal
|
|
@ -1,5 +0,0 @@
|
|||||||
export * from './AdminLayoutPortal'
|
|
||||||
export * from './LayoutPortal'
|
|
||||||
|
|
||||||
export type { AdminLayoutPortalProps } from './AdminLayoutPortal'
|
|
||||||
export type { LayoutPortalProps } from './LayoutPortal'
|
|
128
src/components/LayoutPortal.tsx
Normal file
128
src/components/LayoutPortal.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import { Button, 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 {
|
||||||
|
ApartmentOutlined,
|
||||||
|
CodeOutlined,
|
||||||
|
HomeOutlined,
|
||||||
|
MenuFoldOutlined,
|
||||||
|
MenuUnfoldOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
|
||||||
|
import { LayoutPropsContext } from '@asb/context'
|
||||||
|
import PageHeader from '@components/PageHeader'
|
||||||
|
import { UserMenu, UserMenuProps } from '@components/UserMenu'
|
||||||
|
import { WellTreeSelector, WellTreeSelectorProps } from '@components/selectors/WellTreeSelector'
|
||||||
|
import { isURLAvailable, wrapPrivateComponent } from '@utils'
|
||||||
|
|
||||||
|
import SuspenseFallback from './SuspenseFallback'
|
||||||
|
|
||||||
|
import '@styles/layout.less'
|
||||||
|
|
||||||
|
const { Content, Sider } = Layout
|
||||||
|
|
||||||
|
export type LayoutPortalProps = Omit<LayoutProps, 'children'> & {
|
||||||
|
title?: ReactNode
|
||||||
|
sheet?: boolean
|
||||||
|
showSelector?: boolean
|
||||||
|
selectorProps?: WellTreeSelectorProps
|
||||||
|
sider?: boolean | JSX.Element
|
||||||
|
siderProps?: SiderProps & { userMenuProps?: UserMenuProps }
|
||||||
|
isAdmin?: boolean
|
||||||
|
fallback?: JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultProps: LayoutPortalProps = {
|
||||||
|
title: 'Единая цифровая платформа',
|
||||||
|
sider: true,
|
||||||
|
sheet: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeItem = (title: string, key: Key, icon: JSX.Element, label?: ReactNode, onClick?: () => void) => ({ icon, key, title, label: label ?? title, onClick })
|
||||||
|
|
||||||
|
const _LayoutPortal = memo(() => {
|
||||||
|
const [menuCollapsed, setMenuCollapsed] = useState<boolean>(true)
|
||||||
|
const [wellsTreeOpen, setWellsTreeOpen] = useState<boolean>(false)
|
||||||
|
const [userMenuOpen, setUserMenuOpen] = useState<boolean>(false)
|
||||||
|
const [currentWell, setCurrentWell] = useState<string>('')
|
||||||
|
const [props, setProps] = useState<LayoutPortalProps>(defaultProps)
|
||||||
|
|
||||||
|
const { isAdmin, title, sheet, showSelector, selectorProps, sider, siderProps, fallback, ...other } = useMemo(() => props, [props])
|
||||||
|
|
||||||
|
const setLayoutProps = useCallback((props: LayoutPortalProps) => setProps({ ...defaultProps, ...props}), [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof showSelector === 'boolean')
|
||||||
|
setWellsTreeOpen(showSelector)
|
||||||
|
}, [showSelector])
|
||||||
|
|
||||||
|
const menuItems = useMemo(() => [
|
||||||
|
!isAdmin && makeItem(currentWell, 'well', <ApartmentOutlined/>, null, () => setWellsTreeOpen((prev) => !prev)),
|
||||||
|
isAdmin && makeItem('Вернуться на сайт', 'go_back', <HomeOutlined />, <Link to={'/'}>Домой</Link>),
|
||||||
|
!isAdmin && isURLAvailable('/admin') && makeItem('Панель администратора', 'admin_panel', <CodeOutlined />, <Link to={'/admin'}>Панель администратора</Link>),
|
||||||
|
makeItem('Профиль', 'profile', <UserOutlined/>, null, () => setUserMenuOpen((prev) => !prev)),
|
||||||
|
].filter(Boolean) as ItemType[], [isAdmin, currentWell])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout className={`page-layout ${isAdmin ? 'page-layout-admin' : ''}`}>
|
||||||
|
{(sider || siderProps) && (
|
||||||
|
<Sider {...siderProps} collapsedWidth={50} collapsed={menuCollapsed} trigger={null} collapsible className={`menu-sider ${siderProps?.className || ''}`}>
|
||||||
|
<div className={'sider-content'}>
|
||||||
|
<button className={'sider-toogle'} onClick={() => setMenuCollapsed((prev) => !prev)}>
|
||||||
|
{menuCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||||
|
</button>
|
||||||
|
<div className={'scrollable hide-slider'}>
|
||||||
|
{sider}
|
||||||
|
</div>
|
||||||
|
<Menu
|
||||||
|
mode={'inline'}
|
||||||
|
items={menuItems}
|
||||||
|
theme={'dark'}
|
||||||
|
selectable={false}
|
||||||
|
/>
|
||||||
|
<UserMenu
|
||||||
|
open={userMenuOpen}
|
||||||
|
onClose={() => setUserMenuOpen(false)}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
{...siderProps?.userMenuProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Sider>
|
||||||
|
)}
|
||||||
|
<Layout className={'page-content'}>
|
||||||
|
<PageHeader title={title}>
|
||||||
|
{isAdmin ? (
|
||||||
|
<Button size={'large'}>
|
||||||
|
<Link to={'/'}>Вернуться на сайт</Link>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<WellTreeSelector
|
||||||
|
open={wellsTreeOpen}
|
||||||
|
onClose={() => setWellsTreeOpen(false)}
|
||||||
|
{...selectorProps}
|
||||||
|
onChange={(well) => setCurrentWell(well ?? 'Выберите месторождение')}
|
||||||
|
/>
|
||||||
|
<Button onClick={() => setWellsTreeOpen((prev) => !prev)}>{currentWell}</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</PageHeader>
|
||||||
|
<Content {...other} className={`${sheet ? 'site-layout-background sheet' : ''} ${other.className ?? ''}`}>
|
||||||
|
<LayoutPropsContext.Provider value={setLayoutProps}>
|
||||||
|
<Suspense fallback={fallback ?? <SuspenseFallback style={{ minHeight: '100%' }} />}>
|
||||||
|
<Outlet />
|
||||||
|
</Suspense>
|
||||||
|
</LayoutPropsContext.Provider>
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const LayoutPortal = wrapPrivateComponent(_LayoutPortal, {
|
||||||
|
requirements: ['Deposit.get'],
|
||||||
|
})
|
||||||
|
|
||||||
|
export default LayoutPortal
|
@ -1,34 +1,27 @@
|
|||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { Layout } from 'antd'
|
import { Layout } from 'antd'
|
||||||
import { Link, useLocation } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { BasicProps } from 'antd/lib/layout/layout'
|
import { BasicProps } from 'antd/lib/layout/layout'
|
||||||
|
|
||||||
import { headerHeight } from '@utils'
|
import { headerHeight } from '@utils'
|
||||||
import { UserMenu } from './UserMenu'
|
|
||||||
|
|
||||||
import Logo from '@images/Logo'
|
import Logo from '@images/Logo'
|
||||||
|
|
||||||
|
import '@styles/layout.less'
|
||||||
|
|
||||||
export type PageHeaderProps = BasicProps & {
|
export type PageHeaderProps = BasicProps & {
|
||||||
title?: string
|
title?: string
|
||||||
isAdmin?: boolean
|
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Мониторинг', isAdmin, children, ...other }) => {
|
export const PageHeader: React.FC<PageHeaderProps> = memo(({ title, children, ...other }) => (
|
||||||
const location = useLocation()
|
<Layout.Header className={'header'} {...other}>
|
||||||
|
<Link to={'/'} style={{ height: headerHeight }}>
|
||||||
return (
|
<Logo />
|
||||||
<Layout>
|
</Link>
|
||||||
<Layout.Header className={'header'} {...other}>
|
<h1 className={'title'}>{title}</h1>
|
||||||
<Link to={'/'} style={{ height: headerHeight }}>
|
{children}
|
||||||
<Logo />
|
</Layout.Header>
|
||||||
</Link>
|
))
|
||||||
<h1 className={'title'}>{title}</h1>
|
|
||||||
{children}
|
|
||||||
<UserMenu isAdmin={isAdmin} />
|
|
||||||
</Layout.Header>
|
|
||||||
</Layout>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default PageHeader
|
export default PageHeader
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
import { memo, ReactElement } from 'react'
|
|
||||||
|
|
||||||
import { isURLAvailable } from '@utils'
|
|
||||||
|
|
||||||
export type PrivateContentProps = {
|
|
||||||
absolutePath: string
|
|
||||||
children?: ReactElement<any, any>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PrivateContent = memo<PrivateContentProps>(({ absolutePath, children = null }) =>
|
|
||||||
isURLAvailable(absolutePath) ? children : null
|
|
||||||
)
|
|
||||||
|
|
||||||
export default PrivateContent
|
|
@ -1,19 +0,0 @@
|
|||||||
import { memo } from 'react'
|
|
||||||
import { Navigate, Route, RouteProps } from 'react-router-dom'
|
|
||||||
|
|
||||||
import { isURLAvailable } from '@utils'
|
|
||||||
|
|
||||||
import { getDefaultRedirectPath } from './PrivateRoutes'
|
|
||||||
|
|
||||||
export type PrivateDefaultRouteProps = RouteProps & {
|
|
||||||
urls: string[]
|
|
||||||
elseRedirect?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PrivateDefaultRoute = memo<PrivateDefaultRouteProps>(({ elseRedirect, urls, ...other }) => (
|
|
||||||
<Route {...other} path={'/'} element={(
|
|
||||||
<Navigate replace to={urls.find((url) => isURLAvailable(url)) ?? elseRedirect ?? getDefaultRedirectPath()} />
|
|
||||||
)} />
|
|
||||||
))
|
|
||||||
|
|
||||||
export default PrivateDefaultRoute
|
|
@ -1,75 +0,0 @@
|
|||||||
import { join } from 'path'
|
|
||||||
import { Menu, MenuProps } from 'antd'
|
|
||||||
import { ItemType } from 'antd/lib/menu/hooks/useItems'
|
|
||||||
import { Link, LinkProps } from 'react-router-dom'
|
|
||||||
import { Children, isValidElement, memo, ReactNode, RefAttributes, useMemo } from 'react'
|
|
||||||
|
|
||||||
import { useRootPath } from '@asb/context'
|
|
||||||
import { getTabname, hasPermission, PrivateComponent, PrivateProps } from '@utils'
|
|
||||||
|
|
||||||
export type PrivateMenuProps = MenuProps & { root?: string }
|
|
||||||
|
|
||||||
export type PrivateMenuLinkProps = Partial<ItemType> & Omit<LinkProps, 'to'> & RefAttributes<HTMLAnchorElement> & {
|
|
||||||
icon?: ReactNode
|
|
||||||
danger?: boolean
|
|
||||||
title?: ReactNode
|
|
||||||
content?: PrivateComponent<any>
|
|
||||||
path?: string
|
|
||||||
visible?: boolean
|
|
||||||
permissions?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PrivateMenuLink = memo<PrivateMenuLinkProps>(({ content, path = '', title, ...other }) => (
|
|
||||||
<Link to={path} {...other}>{title ?? content?.title}</Link>
|
|
||||||
))
|
|
||||||
|
|
||||||
const PrivateMenuMain = memo<PrivateMenuProps>(({ selectable, mode, selectedKeys, root, children, ...other }) => {
|
|
||||||
const rootContext = useRootPath()
|
|
||||||
const rootPath = useMemo(() => root ?? rootContext ?? '', [root, rootContext])
|
|
||||||
|
|
||||||
const tab = getTabname()
|
|
||||||
const keys = useMemo(() => selectedKeys ?? (tab ? [tab] : []), [selectedKeys, tab])
|
|
||||||
|
|
||||||
const items = useMemo(() => Children.map(children, (child) => {
|
|
||||||
if (!child || !isValidElement<PrivateMenuLinkProps>(child))
|
|
||||||
return null
|
|
||||||
const content: PrivateProps | undefined = child.props.content
|
|
||||||
const visible: boolean | undefined = child.props.visible
|
|
||||||
|
|
||||||
if (visible === false) return null
|
|
||||||
let key
|
|
||||||
if (content?.key)
|
|
||||||
key = content.key
|
|
||||||
else if (content?.route)
|
|
||||||
key = content.route
|
|
||||||
else if (child.key) {
|
|
||||||
key = child.key?.toString()
|
|
||||||
key = key.slice(key.lastIndexOf('$') + 1)
|
|
||||||
} else return null
|
|
||||||
|
|
||||||
const permissions = child.props.permissions ?? content?.requirements
|
|
||||||
const path = child.props.path ?? join(rootPath, key)
|
|
||||||
|
|
||||||
if (visible || hasPermission(permissions))
|
|
||||||
return {
|
|
||||||
...child.props,
|
|
||||||
key,
|
|
||||||
label: <PrivateMenuLink {...child.props} path={path} />,
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})?.filter((v) => v) ?? [], [children, rootPath])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu
|
|
||||||
selectable={selectable ?? true}
|
|
||||||
mode={mode ?? 'horizontal'}
|
|
||||||
selectedKeys={keys}
|
|
||||||
items={items as ItemType[]}
|
|
||||||
{...other}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const PrivateMenu = Object.assign(PrivateMenuMain, { Link: PrivateMenuLink })
|
|
||||||
|
|
||||||
export default PrivateMenu
|
|
@ -1,37 +0,0 @@
|
|||||||
import { join } from 'path'
|
|
||||||
import { Menu, MenuItemProps } from 'antd'
|
|
||||||
import { memo, NamedExoticComponent } from 'react'
|
|
||||||
import { Link, useLocation } from 'react-router-dom'
|
|
||||||
|
|
||||||
import { isURLAvailable } from '@utils'
|
|
||||||
|
|
||||||
export type PrivateMenuItemProps = MenuItemProps & {
|
|
||||||
root: string
|
|
||||||
path: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PrivateMenuItemLinkProps = MenuItemProps & {
|
|
||||||
root?: string
|
|
||||||
path: string
|
|
||||||
title: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PrivateMenuItemLink = memo<PrivateMenuItemLinkProps>(({ root = '', path, title, ...other }) => {
|
|
||||||
const location = useLocation()
|
|
||||||
return (
|
|
||||||
<PrivateMenuItem key={path} root={root} path={path} {...other}>
|
|
||||||
<Link to={join(root, path)}>{title}</Link>
|
|
||||||
</PrivateMenuItem>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const PrivateMenuItem: NamedExoticComponent<PrivateMenuItemProps> & {
|
|
||||||
Link: NamedExoticComponent<PrivateMenuItemLinkProps>
|
|
||||||
} = Object.assign(memo<PrivateMenuItemProps>(({ root, path, ...other }) =>
|
|
||||||
<Menu.Item key={path} hidden={!isURLAvailable(join(root, path))} {...other} />
|
|
||||||
), {
|
|
||||||
Link: PrivateMenuItemLink
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export default PrivateMenuItem
|
|
@ -1,30 +0,0 @@
|
|||||||
import { join } from 'path'
|
|
||||||
import { memo, ReactNode } from 'react'
|
|
||||||
import { Navigate, Route, RouteProps } from 'react-router-dom'
|
|
||||||
|
|
||||||
import { getUserId, isURLAvailable } from '@utils'
|
|
||||||
|
|
||||||
export type PrivateRouteProps = RouteProps & {
|
|
||||||
root?: string
|
|
||||||
path: string
|
|
||||||
children?: ReactNode
|
|
||||||
redirect?: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultRedirect = (
|
|
||||||
<Navigate to={getUserId() ? '/access_denied' : '/login'} />
|
|
||||||
)
|
|
||||||
|
|
||||||
export const PrivateRoute = memo<PrivateRouteProps>(({ root = '', path, children, redirect = defaultRedirect, ...other }) => {
|
|
||||||
const available = isURLAvailable(join(root, path))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Route
|
|
||||||
{...other}
|
|
||||||
path={path}
|
|
||||||
element={available ? children : redirect}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default PrivateRoute
|
|
@ -1,67 +0,0 @@
|
|||||||
import { join } from 'path'
|
|
||||||
import { Navigate, Route, Routes, RoutesProps } from 'react-router-dom'
|
|
||||||
import { Children, cloneElement, memo, ReactElement, ReactNode, useCallback, useMemo } from 'react'
|
|
||||||
|
|
||||||
import { useRootPath } from '@asb/context'
|
|
||||||
import { getUserId, isURLAvailable } from '@utils'
|
|
||||||
|
|
||||||
export type PrivateRoutesProps = RoutesProps & {
|
|
||||||
root?: string
|
|
||||||
redirect?: ReactNode
|
|
||||||
elseRedirect?: string | string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getDefaultRedirectPath = () => getUserId() ? '/access_denied' : '/login'
|
|
||||||
|
|
||||||
export const defaultRedirect = (
|
|
||||||
<Navigate to={getDefaultRedirectPath()} />
|
|
||||||
)
|
|
||||||
|
|
||||||
export const PrivateRoutes = memo<PrivateRoutesProps>(({ root, elseRedirect, redirect = defaultRedirect, children }) => {
|
|
||||||
const rootContext = useRootPath()
|
|
||||||
const rootPath = useMemo(() => root ?? rootContext ?? '', [root, rootContext])
|
|
||||||
|
|
||||||
const toAbsolute = useCallback((path: string) => path.startsWith('/') ? path : join(rootPath, path), [rootPath])
|
|
||||||
|
|
||||||
const items = useMemo(() => Children.map(children, (child) => {
|
|
||||||
const element = child as ReactElement
|
|
||||||
let key = element.key?.toString()
|
|
||||||
if (!key) return <></>
|
|
||||||
key = key.slice(key.lastIndexOf('$') + 1).replaceAll('=2', ':')
|
|
||||||
// Ключ автоматический преобразуется в "(.+)\$ключ"
|
|
||||||
// Все ":" в ключе заменяются на "=2"
|
|
||||||
// TODO: улучшить метод нормализации ключа
|
|
||||||
const path = toAbsolute(key)
|
|
||||||
return (
|
|
||||||
<Route
|
|
||||||
key={key}
|
|
||||||
path={path}
|
|
||||||
element={isURLAvailable(path) ? cloneElement(element) : redirect}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}) ?? [], [children, redirect, toAbsolute])
|
|
||||||
|
|
||||||
const defaultRoute = useMemo(() => {
|
|
||||||
const routes: string[] = []
|
|
||||||
if (Array.isArray(elseRedirect))
|
|
||||||
routes.push(...elseRedirect)
|
|
||||||
else if(elseRedirect)
|
|
||||||
routes.push(elseRedirect)
|
|
||||||
|
|
||||||
routes.push(...items.map((elm) => elm?.props?.path))
|
|
||||||
|
|
||||||
const firstAvailableRoute = routes.find((path) => path && isURLAvailable(path))
|
|
||||||
return firstAvailableRoute ? toAbsolute(firstAvailableRoute) : getDefaultRedirectPath()
|
|
||||||
}, [items, elseRedirect, toAbsolute])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Routes>
|
|
||||||
{items}
|
|
||||||
<Route path={'/'} element={(
|
|
||||||
<Navigate to={defaultRoute} />
|
|
||||||
)}/>
|
|
||||||
</Routes>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default PrivateRoutes
|
|
@ -1,13 +0,0 @@
|
|||||||
export { PrivateRoute, defaultRedirect } from './PrivateRoute'
|
|
||||||
export { PrivateContent } from './PrivateContent' // TODO: Remove
|
|
||||||
export { PrivateMenuItem, PrivateMenuItemLink } from './PrivateMenuItem' // TODO: Remove
|
|
||||||
export { PrivateDefaultRoute } from './PrivateDefaultRoute'
|
|
||||||
export { PrivateMenu, PrivateMenuLink } from './PrivateMenu'
|
|
||||||
export { PrivateRoutes } from './PrivateRoutes'
|
|
||||||
|
|
||||||
export type { PrivateRouteProps } from './PrivateRoute'
|
|
||||||
export type { PrivateContentProps } from './PrivateContent' // TODO: Remove
|
|
||||||
export type { PrivateMenuItemProps, PrivateMenuItemLinkProps } from './PrivateMenuItem' // TODO: Remove
|
|
||||||
export type { PrivateDefaultRouteProps } from './PrivateDefaultRoute'
|
|
||||||
export type { PrivateMenuProps, PrivateMenuLinkProps } from './PrivateMenu'
|
|
||||||
export type { PrivateRoutesProps } from './PrivateRoutes'
|
|
95
src/components/PrivateWellMenu.tsx
Normal file
95
src/components/PrivateWellMenu.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { ItemType } from 'antd/lib/menu/hooks/useItems'
|
||||||
|
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'
|
||||||
|
|
||||||
|
type PrivateWellMenuItem = {
|
||||||
|
title: string
|
||||||
|
route: string
|
||||||
|
permissions: Permission | Permission[]
|
||||||
|
icon?: ReactNode
|
||||||
|
visible?: boolean
|
||||||
|
children?: PrivateWellMenuItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeItems = (items: PrivateWellMenuItem[], parentRoute: string, pathParser?: (path: string, parent: string) => string): ItemType[] => {
|
||||||
|
return items.map((item) => {
|
||||||
|
if (item.visible === false || !(item.visible === true || hasPermission(item.permissions))) return null
|
||||||
|
|
||||||
|
let route = item.route
|
||||||
|
if (pathParser)
|
||||||
|
route = pathParser(item.route, parentRoute)
|
||||||
|
else if (!item.route.startsWith('/') && parentRoute)
|
||||||
|
route = join(parentRoute, item.route)
|
||||||
|
|
||||||
|
const out: ItemType = {
|
||||||
|
key: route,
|
||||||
|
icon: item.icon,
|
||||||
|
title: item.title,
|
||||||
|
label: <Link to={route}>{item.title}</Link>,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
return {
|
||||||
|
...out,
|
||||||
|
children: makeItems(item.children, route, pathParser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeItemList = (items: PrivateWellMenuItem[], rootPath: string, wellId?: number): ItemType[] => {
|
||||||
|
const parser = (path: string, parent: string) => {
|
||||||
|
if (!path.startsWith('/'))
|
||||||
|
path = join(parent, path)
|
||||||
|
return path.replace(/\{wellId\}/, String(wellId))
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeItems(items, rootPath, parser)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeItem = (
|
||||||
|
title: string,
|
||||||
|
route: string,
|
||||||
|
permissions: Permission | Permission[],
|
||||||
|
icon?: ReactNode,
|
||||||
|
children?: PrivateWellMenuItem[],
|
||||||
|
visible?: boolean
|
||||||
|
): PrivateWellMenuItem => ({
|
||||||
|
title,
|
||||||
|
route,
|
||||||
|
icon,
|
||||||
|
permissions,
|
||||||
|
children,
|
||||||
|
visible,
|
||||||
|
})
|
||||||
|
|
||||||
|
export type PrivateWellMenuProps = MenuProps & {
|
||||||
|
idWell?: number
|
||||||
|
items: PrivateWellMenuItem[]
|
||||||
|
rootPath?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrivateWellMenu = memo<PrivateWellMenuProps>(({ idWell, items, rootPath = '/', ...other }) => {
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
const menuItems = useMemo(() => makeItemList(items, rootPath, idWell), [items, rootPath, idWell])
|
||||||
|
|
||||||
|
const tabKeys = useMemo(() => {
|
||||||
|
const out = []
|
||||||
|
const rx = RegExp(/(?<!^)\//g)
|
||||||
|
let input = location.pathname
|
||||||
|
if (!input.endsWith('/')) input += '/'
|
||||||
|
let match: RegExpExecArray | null
|
||||||
|
while((match = rx.exec(input)) !== null)
|
||||||
|
out.push(input.slice(0, match.index))
|
||||||
|
return out
|
||||||
|
}, [location.pathname])
|
||||||
|
|
||||||
|
return <Menu items={menuItems} selectedKeys={tabKeys} {...other} />
|
||||||
|
})
|
0
src/pages/SuspenseFallback.tsx → src/components/SuspenseFallback.tsx
Executable file → Normal file
0
src/pages/SuspenseFallback.tsx → src/components/SuspenseFallback.tsx
Executable file → Normal file
@ -12,7 +12,6 @@ export {
|
|||||||
makeNumericColumnPlanFact,
|
makeNumericColumnPlanFact,
|
||||||
makeNumericStartEnd,
|
makeNumericStartEnd,
|
||||||
makeNumericMinMax,
|
makeNumericMinMax,
|
||||||
makeNumericAvgRange
|
|
||||||
} from './numeric'
|
} from './numeric'
|
||||||
export { makeColumnsPlanFact } from './plan_fact'
|
export { makeColumnsPlanFact } from './plan_fact'
|
||||||
export { makeSelectColumn } from './select'
|
export { makeSelectColumn } from './select'
|
||||||
@ -31,7 +30,7 @@ export type DataType<T = any> = Record<string, T>
|
|||||||
export type RenderMethod<T = any> = (value: T, dataset?: DataType<T>, index?: number) => ReactNode
|
export type RenderMethod<T = any> = (value: T, dataset?: DataType<T>, index?: number) => ReactNode
|
||||||
export type SorterMethod<T = any> = (a?: DataType<T> | null, b?: DataType<T> | null) => number
|
export type SorterMethod<T = any> = (a?: DataType<T> | null, b?: DataType<T> | null) => number
|
||||||
|
|
||||||
export type columnPropsOther<T = any> = ColumnProps<T> & {
|
export type columnPropsOther<T = any> = ColumnProps<DataType<T>> & {
|
||||||
// редактируемая колонка
|
// редактируемая колонка
|
||||||
editable?: boolean
|
editable?: boolean
|
||||||
// react компонента редактора
|
// react компонента редактора
|
||||||
|
@ -2,11 +2,14 @@ import { InputNumber } from 'antd'
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
import { makeNumericSorter } from '../sorters'
|
import { makeNumericSorter } from '../sorters'
|
||||||
import { columnPropsOther, makeGroupColumn, RenderMethod } from '.'
|
import makeColumn, { columnPropsOther, DataType, makeGroupColumn, RenderMethod } from '.'
|
||||||
|
import { ColumnFilterItem, CompareFn } from 'antd/lib/table/interface'
|
||||||
|
|
||||||
export const RegExpIsFloat = /^[-+]?\d+\.?\d*$/
|
export const RegExpIsFloat = /^[-+]?\d+\.?\d*$/
|
||||||
|
|
||||||
export const makeNumericRender = <T extends unknown>(fixed?: number): RenderMethod<T> => (value) => {
|
type FilterMethod<T> = (value: string | number | boolean, record: DataType<T>) => boolean
|
||||||
|
|
||||||
|
export const makeNumericRender = <T extends unknown>(fixed?: number): RenderMethod<T> => (value: T) => {
|
||||||
let val = '-'
|
let val = '-'
|
||||||
if ((value ?? null) !== null && Number.isFinite(+value)) {
|
if ((value ?? null) !== null && Number.isFinite(+value)) {
|
||||||
val = (fixed ?? null) !== null
|
val = (fixed ?? null) !== null
|
||||||
@ -21,91 +24,74 @@ export const makeNumericRender = <T extends unknown>(fixed?: number): RenderMeth
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeNumericColumnOptions = (fixed?: number, sorterKey?: string): columnPropsOther => ({
|
export const makeNumericColumnOptions = <T extends unknown = any>(fixed?: number, sorterKey?: string): columnPropsOther<T> => ({
|
||||||
editable: true,
|
editable: true,
|
||||||
initialValue: 0,
|
initialValue: 0,
|
||||||
width: 100,
|
width: 100,
|
||||||
sorter: sorterKey ? makeNumericSorter(sorterKey) : undefined,
|
sorter: sorterKey ? makeNumericSorter<T>(sorterKey) : undefined,
|
||||||
formItemRules: [{
|
formItemRules: [{
|
||||||
required: true,
|
required: true,
|
||||||
message: 'Введите число',
|
message: 'Введите число',
|
||||||
pattern: RegExpIsFloat,
|
pattern: RegExpIsFloat,
|
||||||
}],
|
}],
|
||||||
render: makeNumericRender(fixed),
|
render: makeNumericRender<T>(fixed),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const makeNumericColumn = (
|
export const makeNumericColumn = <T extends unknown = any>(
|
||||||
title: ReactNode,
|
title: ReactNode,
|
||||||
dataIndex: string,
|
key: string,
|
||||||
filters: object[],
|
filters?: ColumnFilterItem[],
|
||||||
filterDelegate: (key: string | number) => any,
|
filterDelegate?: (key: string | number) => FilterMethod<T>,
|
||||||
renderDelegate: (_: any, row: object) => any,
|
renderDelegate?: RenderMethod<T>,
|
||||||
width: string,
|
width?: string | number,
|
||||||
other?: columnPropsOther
|
other?: columnPropsOther,
|
||||||
) => ({
|
) => makeColumn(title, key, {
|
||||||
title: title,
|
filters,
|
||||||
dataIndex: dataIndex,
|
onFilter: filterDelegate ? filterDelegate(key) : undefined,
|
||||||
key: dataIndex,
|
sorter: makeNumericSorter(key),
|
||||||
filters: filters,
|
width,
|
||||||
onFilter: filterDelegate ? filterDelegate(dataIndex) : null,
|
|
||||||
sorter: makeNumericSorter(dataIndex),
|
|
||||||
width: width,
|
|
||||||
input: <InputNumber style={{ width: '100%' }}/>,
|
input: <InputNumber style={{ width: '100%' }}/>,
|
||||||
render: renderDelegate ?? makeNumericRender(),
|
render: renderDelegate ?? makeNumericRender<T>(2),
|
||||||
align: 'right',
|
align: 'right',
|
||||||
...other
|
...other
|
||||||
})
|
})
|
||||||
|
|
||||||
export const makeNumericColumnPlanFact = (
|
export const makeNumericColumnPlanFact = <T extends unknown = any>(
|
||||||
title: ReactNode,
|
title: ReactNode,
|
||||||
dataIndex: string,
|
key: string,
|
||||||
filters: object[],
|
filters?: ColumnFilterItem[],
|
||||||
filterDelegate: (key: string | number) => any,
|
filterDelegate?: (key: string | number) => FilterMethod<T>,
|
||||||
renderDelegate: (_: any, row: object) => any,
|
renderDelegate?: RenderMethod<T>,
|
||||||
width: string
|
width?: string | number
|
||||||
) => makeGroupColumn(title, [
|
) => makeGroupColumn(title, [
|
||||||
makeNumericColumn('п', dataIndex + 'Plan', filters, filterDelegate, renderDelegate, width),
|
makeNumericColumn<T>('п', key + 'Plan', filters, filterDelegate, renderDelegate, width),
|
||||||
makeNumericColumn('ф', dataIndex + 'Fact', filters, filterDelegate, renderDelegate, width),
|
makeNumericColumn<T>('ф', key + 'Fact', filters, filterDelegate, renderDelegate, width),
|
||||||
])
|
])
|
||||||
|
|
||||||
export const makeNumericStartEnd = (
|
export const makeNumericStartEnd = <T extends unknown = any>(
|
||||||
title: ReactNode,
|
title: ReactNode,
|
||||||
dataIndex: string,
|
key: string,
|
||||||
fixed: number,
|
fixed: number,
|
||||||
filters: object[],
|
filters?: ColumnFilterItem[],
|
||||||
filterDelegate: (key: string | number) => any,
|
filterDelegate?: (key: string | number) => FilterMethod<T>,
|
||||||
renderDelegate: (_: any, row: object) => any,
|
renderDelegate?: RenderMethod<T>,
|
||||||
width: string,
|
width?: string | number,
|
||||||
) => makeGroupColumn(title, [
|
) => makeGroupColumn(title, [
|
||||||
makeNumericColumn('старт', dataIndex + 'Start', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Start')),
|
makeNumericColumn<T>('старт', key + 'Start', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'Start')),
|
||||||
makeNumericColumn('конец', dataIndex + 'End', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'End'))
|
makeNumericColumn<T>('конец', key + 'End', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'End'))
|
||||||
])
|
])
|
||||||
|
|
||||||
export const makeNumericMinMax = (
|
export const makeNumericMinMax = <T extends unknown = any>(
|
||||||
title: ReactNode,
|
title: ReactNode,
|
||||||
dataIndex: string,
|
key: string,
|
||||||
fixed: number,
|
fixed: number,
|
||||||
filters: object[],
|
filters?: ColumnFilterItem[],
|
||||||
filterDelegate: (key: string | number) => any,
|
filterDelegate?: (key: string | number) => FilterMethod<T>,
|
||||||
renderDelegate: (_: any, row: object) => any,
|
renderDelegate?: RenderMethod<T>,
|
||||||
width: string,
|
width?: string | number,
|
||||||
) => makeGroupColumn(title, [
|
) => makeGroupColumn(title, [
|
||||||
makeNumericColumn('мин', dataIndex + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Min')),
|
makeNumericColumn<T>('мин', key + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'Min')),
|
||||||
makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max')),
|
makeNumericColumn<T>('макс', key + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, key + 'Max')),
|
||||||
])
|
|
||||||
|
|
||||||
export const makeNumericAvgRange = (
|
|
||||||
title: ReactNode,
|
|
||||||
dataIndex: string,
|
|
||||||
fixed: number,
|
|
||||||
filters: object[],
|
|
||||||
filterDelegate: (key: string | number) => any,
|
|
||||||
renderDelegate: (_: any, row: object) => any,
|
|
||||||
width: string
|
|
||||||
) => makeGroupColumn(title, [
|
|
||||||
makeNumericColumn('мин', dataIndex + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Min')),
|
|
||||||
makeNumericColumn('сред', dataIndex + 'Avg', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Avg')),
|
|
||||||
makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max'))
|
|
||||||
])
|
])
|
||||||
|
|
||||||
export default makeNumericColumn
|
export default makeNumericColumn
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import { ColumnFilterItem } from 'antd/lib/table/interface'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
import { columnPropsOther, DataType, RenderMethod, SorterMethod } from '.'
|
import makeColumn, { columnPropsOther, DataType, RenderMethod, SorterMethod } from '.'
|
||||||
import { makeStringSorter } from '../sorters'
|
import { makeStringSorter } from '../sorters'
|
||||||
|
|
||||||
export const makeFilterTextMatch = <T extends unknown>(key: keyof DataType<T>) =>
|
export const makeFilterTextMatch = <T extends unknown>(key: keyof DataType<T>) =>
|
||||||
@ -8,18 +9,15 @@ export const makeFilterTextMatch = <T extends unknown>(key: keyof DataType<T>) =
|
|||||||
|
|
||||||
export const makeTextColumn = <T extends unknown = any>(
|
export const makeTextColumn = <T extends unknown = any>(
|
||||||
title: ReactNode,
|
title: ReactNode,
|
||||||
dataIndex: string,
|
key: string,
|
||||||
filters: object[],
|
filters?: ColumnFilterItem[],
|
||||||
sorter?: SorterMethod<T>,
|
sorter?: SorterMethod<T>,
|
||||||
render?: RenderMethod<T>,
|
render?: RenderMethod<T>,
|
||||||
other?: columnPropsOther
|
other?: columnPropsOther
|
||||||
) => ({
|
) => makeColumn(title, key, {
|
||||||
title: title,
|
filters,
|
||||||
dataIndex: dataIndex,
|
onFilter: filters ? makeFilterTextMatch(key) : undefined,
|
||||||
key: dataIndex,
|
sorter: sorter ?? makeStringSorter(key),
|
||||||
filters: filters,
|
|
||||||
onFilter: filters ? makeFilterTextMatch(dataIndex) : null,
|
|
||||||
sorter: sorter ?? makeStringSorter(dataIndex),
|
|
||||||
render: render,
|
render: render,
|
||||||
...other
|
...other
|
||||||
})
|
})
|
||||||
|
@ -11,6 +11,7 @@ const { RangePicker } = DatePicker
|
|||||||
export type DateRangeWrapperProps = RangePickerSharedProps<Moment> & {
|
export type DateRangeWrapperProps = RangePickerSharedProps<Moment> & {
|
||||||
value?: RangeValue<Moment>,
|
value?: RangeValue<Moment>,
|
||||||
isUTC?: boolean
|
isUTC?: boolean
|
||||||
|
allowClear?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeDates = (value?: RangeValue<Moment>, isUTC?: boolean): RangeValue<Moment> => {
|
const normalizeDates = (value?: RangeValue<Moment>, isUTC?: boolean): RangeValue<Moment> => {
|
||||||
@ -21,10 +22,10 @@ const normalizeDates = (value?: RangeValue<Moment>, isUTC?: boolean): RangeValue
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DateRangeWrapper = memo<DateRangeWrapperProps>(({ value, isUTC, ...other }) => (
|
export const DateRangeWrapper = memo<DateRangeWrapperProps>(({ value, isUTC, allowClear = false, ...other }) => (
|
||||||
<RangePicker
|
<RangePicker
|
||||||
showTime
|
showTime
|
||||||
allowClear={false}
|
allowClear={allowClear}
|
||||||
format={defaultFormat}
|
format={defaultFormat}
|
||||||
defaultValue={[
|
defaultValue={[
|
||||||
moment().subtract(1, 'days').startOf('day'),
|
moment().subtract(1, 'days').startOf('day'),
|
||||||
|
@ -10,18 +10,17 @@ import { tryAddKeys } from './EditableTable'
|
|||||||
|
|
||||||
import '@styles/index.css'
|
import '@styles/index.css'
|
||||||
|
|
||||||
export type BaseTableColumn<T = any> = ColumnGroupType<T> | ColumnType<T>
|
export type BaseTableColumn<T> = ColumnGroupType<T> | ColumnType<T>
|
||||||
export type TableColumns<T = any> = OmitExtends<BaseTableColumn<T>, TableColumnSettings>[]
|
export type TableColumns<T> = OmitExtends<BaseTableColumn<T>, TableColumnSettings>[]
|
||||||
|
|
||||||
export type TableContainer = TableProps<any> & {
|
export type TableContainer<T> = TableProps<T> & {
|
||||||
columns: TableColumns
|
columns: TableColumns<T>
|
||||||
dataSource: any[]
|
|
||||||
tableName?: string
|
tableName?: string
|
||||||
showSettingsChanger?: boolean
|
showSettingsChanger?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Table = memo<TableContainer>(({ columns, dataSource, tableName, showSettingsChanger, ...other }) => {
|
const _Table = <T extends object>({ columns, dataSource, tableName, showSettingsChanger, ...other }: TableContainer<T>) => {
|
||||||
const [newColumns, setNewColumns] = useState<TableColumns>([])
|
const [newColumns, setNewColumns] = useState<TableColumns<T>>([])
|
||||||
const [settings, setSettings] = useState<TableSettings>({})
|
const [settings, setSettings] = useState<TableSettings>({})
|
||||||
|
|
||||||
const onSettingsChanged = useCallback((settings?: TableSettings | null) => {
|
const onSettingsChanged = useCallback((settings?: TableSettings | null) => {
|
||||||
@ -52,6 +51,8 @@ export const Table = memo<TableContainer>(({ columns, dataSource, tableName, sho
|
|||||||
{...other}
|
{...other}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
export const Table = memo(_Table) as typeof _Table
|
||||||
|
|
||||||
export default Table
|
export default Table
|
||||||
|
@ -7,7 +7,7 @@ import { TableColumnSettings, makeTableSettings, mergeTableSettings, TableSettin
|
|||||||
import { TableColumns } from './Table'
|
import { TableColumns } from './Table'
|
||||||
import { makeColumn } from '.'
|
import { makeColumn } from '.'
|
||||||
|
|
||||||
const parseSettings = (columns?: TableColumns, settings?: TableSettings | null): TableColumnSettings[] => {
|
const parseSettings = <T extends object>(columns?: TableColumns<T>, settings?: TableSettings | null): TableColumnSettings[] => {
|
||||||
const newSettings = mergeTableSettings(makeTableSettings(columns ?? []), settings ?? {})
|
const newSettings = mergeTableSettings(makeTableSettings(columns ?? []), settings ?? {})
|
||||||
return Object.values(newSettings).map((set, i) => ({ ...set, key: i }))
|
return Object.values(newSettings).map((set, i) => ({ ...set, key: i }))
|
||||||
}
|
}
|
||||||
@ -15,14 +15,14 @@ const parseSettings = (columns?: TableColumns, settings?: TableSettings | null):
|
|||||||
const unparseSettings = (columns: TableColumnSettings[]): TableSettings =>
|
const unparseSettings = (columns: TableColumnSettings[]): TableSettings =>
|
||||||
Object.fromEntries(columns.map((column) => [column.columnName, column]))
|
Object.fromEntries(columns.map((column) => [column.columnName, column]))
|
||||||
|
|
||||||
export type TableSettingsChangerProps = {
|
export type TableSettingsChangerProps<T extends object> = {
|
||||||
title?: string
|
title?: string
|
||||||
columns?: TableColumns
|
columns?: TableColumns<T>
|
||||||
settings?: TableSettings | null
|
settings?: TableSettings | null
|
||||||
onChange: (settings: TableSettings | null) => void
|
onChange: (settings: TableSettings | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TableSettingsChanger = memo<TableSettingsChangerProps>(({ title, columns, settings, onChange }) => {
|
const _TableSettingsChanger = <T extends object>({ title, columns, settings, onChange }: TableSettingsChangerProps<T>) => {
|
||||||
const [visible, setVisible] = useState<boolean>(false)
|
const [visible, setVisible] = useState<boolean>(false)
|
||||||
const [newSettings, setNewSettings] = useState<TableColumnSettings[]>(parseSettings(columns, settings))
|
const [newSettings, setNewSettings] = useState<TableColumnSettings[]>(parseSettings(columns, settings))
|
||||||
const [tableColumns, setTableColumns] = useState<ColumnsType<TableColumnSettings>>([])
|
const [tableColumns, setTableColumns] = useState<ColumnsType<TableColumnSettings>>([])
|
||||||
@ -36,10 +36,12 @@ export const TableSettingsChanger = memo<TableSettingsChangerProps>(({ title, co
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const toogleAll = useCallback((show: boolean) => {
|
const toogleAll = useCallback((show: boolean) => {
|
||||||
setNewSettings((oldSettings) => oldSettings.map((column) => {
|
setNewSettings((oldSettings) =>
|
||||||
column.visible = show
|
oldSettings.map((column) => {
|
||||||
return column
|
column.visible = show
|
||||||
}))
|
return column
|
||||||
|
})
|
||||||
|
)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -49,7 +51,9 @@ export const TableSettingsChanger = memo<TableSettingsChangerProps>(({ title, co
|
|||||||
title: () => (
|
title: () => (
|
||||||
<>
|
<>
|
||||||
Показать
|
Показать
|
||||||
<Button type={'link'} onClick={() => toogleAll(true)}>Показать все</Button>
|
<Button type={'link'} onClick={() => toogleAll(true)}>
|
||||||
|
Показать все
|
||||||
|
</Button>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
render: (visible: boolean, _?: TableColumnSettings, index: number = NaN) => (
|
render: (visible: boolean, _?: TableColumnSettings, index: number = NaN) => (
|
||||||
@ -59,7 +63,7 @@ export const TableSettingsChanger = memo<TableSettingsChangerProps>(({ title, co
|
|||||||
unCheckedChildren={'Скрыт'}
|
unCheckedChildren={'Скрыт'}
|
||||||
onChange={(visible) => onVisibilityChange(index, visible)}
|
onChange={(visible) => onVisibilityChange(index, visible)}
|
||||||
/>
|
/>
|
||||||
)
|
),
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
}, [toogleAll, onVisibilityChange])
|
}, [toogleAll, onVisibilityChange])
|
||||||
@ -80,7 +84,7 @@ export const TableSettingsChanger = memo<TableSettingsChangerProps>(({ title, co
|
|||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
centered
|
centered
|
||||||
visible={visible}
|
open={visible}
|
||||||
onCancel={onModalCancel}
|
onCancel={onModalCancel}
|
||||||
onOk={onModalOk}
|
onOk={onModalOk}
|
||||||
title={title ?? 'Настройка отображения таблицы'}
|
title={title ?? 'Настройка отображения таблицы'}
|
||||||
@ -88,9 +92,17 @@ export const TableSettingsChanger = memo<TableSettingsChangerProps>(({ title, co
|
|||||||
>
|
>
|
||||||
<Table columns={tableColumns} dataSource={newSettings} />
|
<Table columns={tableColumns} dataSource={newSettings} />
|
||||||
</Modal>
|
</Modal>
|
||||||
<Button size={'small'} style={{ position: 'absolute', left: 0, top: 0, opacity: .5 }} type={'link'} onClick={() => setVisible(true)} icon={<SettingOutlined />}/>
|
<Button
|
||||||
|
size={'small'}
|
||||||
|
style={{ position: 'absolute', left: 0, top: 0, opacity: 0.5 }}
|
||||||
|
type={'link'}
|
||||||
|
onClick={() => setVisible(true)}
|
||||||
|
icon={<SettingOutlined />}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
export const TableSettingsChanger = memo(_TableSettingsChanger) as typeof _TableSettingsChanger
|
||||||
|
|
||||||
export default TableSettingsChanger
|
export default TableSettingsChanger
|
||||||
|
@ -20,7 +20,6 @@ export {
|
|||||||
makeNumericColumnPlanFact,
|
makeNumericColumnPlanFact,
|
||||||
makeNumericStartEnd,
|
makeNumericStartEnd,
|
||||||
makeNumericMinMax,
|
makeNumericMinMax,
|
||||||
makeNumericAvgRange,
|
|
||||||
makeSelectColumn,
|
makeSelectColumn,
|
||||||
makeTagColumn,
|
makeTagColumn,
|
||||||
makeTagInput,
|
makeTagInput,
|
||||||
|
@ -3,10 +3,14 @@ import { isRawDate } from '@utils'
|
|||||||
import { TimeDto } from '@api'
|
import { TimeDto } from '@api'
|
||||||
|
|
||||||
import { DataType } from './Columns'
|
import { DataType } from './Columns'
|
||||||
|
import { CompareFn } from 'antd/lib/table/interface'
|
||||||
|
|
||||||
export const makeNumericSorter = <T extends unknown>(key: keyof DataType<T>) =>
|
export const makeNumericSorter = <T extends unknown>(key: keyof DataType<T>): CompareFn<DataType<T>> =>
|
||||||
(a: DataType<T>, b: DataType<T>) => Number(a[key]) - Number(b[key])
|
(a: DataType<T>, b: DataType<T>) => Number(a[key]) - Number(b[key])
|
||||||
|
|
||||||
|
export const makeNumericObjSorter = (key: [string, string]) =>
|
||||||
|
(a: DataType, b: DataType) => Number(a[key[0]][key[1]]) - Number(b[key[0]][key[1]])
|
||||||
|
|
||||||
export const makeStringSorter = <T extends unknown>(key: keyof DataType<T>) => (a?: DataType<T> | null, b?: DataType<T> | null) => {
|
export const makeStringSorter = <T extends unknown>(key: keyof DataType<T>) => (a?: DataType<T> | null, b?: DataType<T> | null) => {
|
||||||
if (!a && !b) return 0
|
if (!a && !b) return 0
|
||||||
if (!a) return 1
|
if (!a) return 1
|
||||||
|
@ -10,6 +10,7 @@ import { notify, upload } from './factory'
|
|||||||
import { ErrorFetch } from './ErrorFetch'
|
import { ErrorFetch } from './ErrorFetch'
|
||||||
|
|
||||||
export type UploadFormProps = {
|
export type UploadFormProps = {
|
||||||
|
multiple?: boolean
|
||||||
url: string
|
url: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
accept?: string
|
accept?: string
|
||||||
@ -22,7 +23,7 @@ export type UploadFormProps = {
|
|||||||
onUploadError?: (error: unknown) => void
|
onUploadError?: (error: unknown) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UploadForm = memo<UploadFormProps>(({ url, disabled, style, formData, mimeTypes, onUploadStart, onUploadSuccess, onUploadComplete, onUploadError }) => {
|
export const UploadForm = memo<UploadFormProps>(({ url, multiple, disabled, style, formData, mimeTypes, onUploadStart, onUploadSuccess, onUploadComplete, onUploadError }) => {
|
||||||
const [fileList, setfileList] = useState<UploadFile<any>[]>([])
|
const [fileList, setfileList] = useState<UploadFile<any>[]>([])
|
||||||
|
|
||||||
const checkMimeTypes = useCallback((file: RcFile) => {
|
const checkMimeTypes = useCallback((file: RcFile) => {
|
||||||
@ -38,7 +39,7 @@ export const UploadForm = memo<UploadFormProps>(({ url, disabled, style, formDat
|
|||||||
onUploadStart?.()
|
onUploadStart?.()
|
||||||
try {
|
try {
|
||||||
const formDataLocal = new FormData()
|
const formDataLocal = new FormData()
|
||||||
fileList.forEach((val) => formDataLocal.append('files', val.originFileObj as Blob))
|
fileList.forEach((val) => formDataLocal.append(multiple ? 'files' : 'file', val.originFileObj as Blob))
|
||||||
|
|
||||||
if(formData)
|
if(formData)
|
||||||
for(const propName in formData)
|
for(const propName in formData)
|
||||||
@ -60,7 +61,7 @@ export const UploadForm = memo<UploadFormProps>(({ url, disabled, style, formDat
|
|||||||
setfileList([])
|
setfileList([])
|
||||||
onUploadComplete?.()
|
onUploadComplete?.()
|
||||||
}
|
}
|
||||||
}, [fileList, formData, onUploadComplete, onUploadError, onUploadStart, onUploadSuccess, url])
|
}, [fileList, formData, onUploadComplete, onUploadError, onUploadStart, onUploadSuccess, url, multiple])
|
||||||
|
|
||||||
const isSendButtonEnabled = fileList.length > 0
|
const isSendButtonEnabled = fileList.length > 0
|
||||||
return(
|
return(
|
||||||
@ -72,6 +73,7 @@ export const UploadForm = memo<UploadFormProps>(({ url, disabled, style, formDat
|
|||||||
fileList={fileList}
|
fileList={fileList}
|
||||||
onChange={(props) => setfileList(props.fileList)}
|
onChange={(props) => setfileList(props.fileList)}
|
||||||
beforeUpload={checkMimeTypes}
|
beforeUpload={checkMimeTypes}
|
||||||
|
maxCount={multiple ? undefined : 1}
|
||||||
>
|
>
|
||||||
<Button disabled={disabled} icon={<UploadOutlined/>}>Загрузить файл</Button>
|
<Button disabled={disabled} icon={<UploadOutlined/>}>Загрузить файл</Button>
|
||||||
</Upload>
|
</Upload>
|
||||||
|
@ -1,56 +1,110 @@
|
|||||||
import { memo, MouseEventHandler, useCallback, useState } from 'react'
|
import { memo, ReactNode, useCallback, useState } from 'react'
|
||||||
import { useNavigate, useLocation } from 'react-router-dom'
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
import { Button, Dropdown, DropDownProps } from 'antd'
|
import { Button, Collapse, Drawer, DrawerProps, Form, FormRule, Input, Popconfirm } from 'antd'
|
||||||
import { UserOutlined } from '@ant-design/icons'
|
import { useForm } from 'antd/lib/form/Form'
|
||||||
|
|
||||||
import { getUserLogin, removeUser } from '@utils'
|
import { useUser } from '@asb/context'
|
||||||
import { ChangePassword } from './ChangePassword'
|
import { Grid, GridItem } from '@components/Grid'
|
||||||
import { PrivateMenu } from './Private'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
import { isURLAvailable, removeUser } from '@utils'
|
||||||
|
import { AuthService } from '@api'
|
||||||
|
|
||||||
import AdminPanel from '@pages/AdminPanel'
|
import '@styles/user_menu.less'
|
||||||
|
|
||||||
type UserMenuProps = Omit<DropDownProps, 'overlay'> & { isAdmin?: boolean }
|
export type UserMenuProps = DrawerProps & {
|
||||||
|
isAdmin?: boolean
|
||||||
|
additional?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
export const UserMenu = memo<UserMenuProps>(({ isAdmin, ...other }) => {
|
type ChangePasswordForm = {
|
||||||
const [isModalVisible, setIsModalVisible] = useState<boolean>(false)
|
'new-password': string
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPasswordRules: FormRule[] = [{ required: true, message: 'Пожалуйста, введите новый пароль!' }]
|
||||||
|
|
||||||
|
const confirmPasswordRules: FormRule[] = [({ getFieldValue }) => ({ validator(_, value: string) {
|
||||||
|
if (value !== getFieldValue('new-password'))
|
||||||
|
return Promise.reject('Пароли не совпадают!')
|
||||||
|
return Promise.resolve()
|
||||||
|
}})]
|
||||||
|
|
||||||
|
export const UserMenu = memo<UserMenuProps>(({ isAdmin, additional, ...other }) => {
|
||||||
|
const [showLoader, setShowLoader] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const [changeLoginForm] = useForm<ChangePasswordForm>()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
const onChangePasswordClick: MouseEventHandler = useCallback((e) => {
|
const navigateTo = useCallback((to: string) => navigate(to, { state: { from: location.pathname }}), [navigate, location.pathname])
|
||||||
setIsModalVisible(true)
|
|
||||||
e.preventDefault()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onChangePasswordOk = useCallback(() => {
|
const onChangePasswordOk = useCallback(() => invokeWebApiWrapperAsync(
|
||||||
setIsModalVisible(false)
|
async (values: any) => {
|
||||||
navigate('/login', { state: { from: location.pathname }})
|
await AuthService.changePassword(user.id ?? -1, `${values['new-password']}`)
|
||||||
}, [navigate, location])
|
removeUser()
|
||||||
|
navigateTo('/login')
|
||||||
|
},
|
||||||
|
setShowLoader,
|
||||||
|
`Не удалось сменить пароль пользователя ${user.login}`,
|
||||||
|
{ actionName: 'Смена пароля пользователя' },
|
||||||
|
), [navigateTo])
|
||||||
|
|
||||||
|
const logout = useCallback(() => {
|
||||||
|
removeUser()
|
||||||
|
navigateTo('/login')
|
||||||
|
}, [navigateTo])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Drawer
|
||||||
<Dropdown
|
closable
|
||||||
{...other}
|
placement={'left'}
|
||||||
placement={'bottomRight'}
|
className={'user-menu'}
|
||||||
overlay={(
|
title={'Профиль пользователя'}
|
||||||
<PrivateMenu mode={'vertical'} style={{ textAlign: 'right' }}>
|
{...other}
|
||||||
{isAdmin ? (
|
>
|
||||||
<PrivateMenu.Link visible key={'/'} path={'/'} title={'Вернуться на сайт'} />
|
<div className={'profile-links'}>
|
||||||
) : (
|
{isAdmin ? (
|
||||||
<PrivateMenu.Link path={'/admin'} content={AdminPanel} />
|
<Button onClick={() => navigateTo('/')}>Вернуться на сайт</Button>
|
||||||
)}
|
) : isURLAvailable('/admin') && (
|
||||||
<PrivateMenu.Link visible key={'change_password'} onClick={onChangePasswordClick} title={'Сменить пароль'} />
|
<Button onClick={() => navigateTo('/admin')}>Панель администратора</Button>
|
||||||
<PrivateMenu.Link visible key={'login'} path={'/login'} onClick={removeUser} title={'Выход'} />
|
|
||||||
</PrivateMenu>
|
|
||||||
)}
|
)}
|
||||||
>
|
<Button type={'ghost'} onClick={logout}>Выход</Button>
|
||||||
<Button icon={<UserOutlined/>}>{getUserLogin()}</Button>
|
</div>
|
||||||
</Dropdown>
|
<Collapse>
|
||||||
<ChangePassword
|
<Collapse.Panel header={'Данные'} key={'summary'}>
|
||||||
visible={isModalVisible}
|
<Grid>
|
||||||
onOk={onChangePasswordOk}
|
<GridItem row={1} col={1}>Логин:</GridItem>
|
||||||
onCancel={() => setIsModalVisible(false)}
|
<GridItem row={1} col={2}>{user.login}</GridItem>
|
||||||
/>
|
<GridItem row={2} col={1}>Фамилия:</GridItem>
|
||||||
</>
|
<GridItem row={2} col={2}>{user.surname}</GridItem>
|
||||||
|
<GridItem row={3} col={1}>Имя:</GridItem>
|
||||||
|
<GridItem row={3} col={2}>{user.name}</GridItem>
|
||||||
|
<GridItem row={4} col={1}>Отчество:</GridItem>
|
||||||
|
<GridItem row={4} col={2}>{user.patronymic}</GridItem>
|
||||||
|
<GridItem row={5} col={1}>E-mail:</GridItem>
|
||||||
|
<GridItem row={5} col={2}>{user.email}</GridItem>
|
||||||
|
</Grid>
|
||||||
|
</Collapse.Panel>
|
||||||
|
<Collapse.Panel header={'Смена пароля'} key={'change-password'}>
|
||||||
|
<LoaderPortal show={showLoader}>
|
||||||
|
<Form name={'change-password'} form={changeLoginForm} autoComplete={'off'} onFinish={onChangePasswordOk}>
|
||||||
|
<Form.Item name={'new-password'} label={'Новый пароль'} rules={newPasswordRules}>
|
||||||
|
<Input.Password placeholder={'Впишите новый пароль'} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item required name={'confirm-password'} rules={confirmPasswordRules} label={'Подтверждение пароля'}>
|
||||||
|
<Input.Password />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Popconfirm title={'Вы уверены что хотите сменить пароль?'} onConfirm={changeLoginForm.submit} placement={'topRight'}>
|
||||||
|
<Button type={'primary'}>Сменить</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</LoaderPortal>
|
||||||
|
</Collapse.Panel>
|
||||||
|
{additional}
|
||||||
|
</Collapse>
|
||||||
|
</Drawer>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -27,6 +27,7 @@ import {
|
|||||||
D3TooltipSettings,
|
D3TooltipSettings,
|
||||||
} from './plugins'
|
} from './plugins'
|
||||||
import type {
|
import type {
|
||||||
|
BaseDataType,
|
||||||
ChartAxis,
|
ChartAxis,
|
||||||
ChartDataset,
|
ChartDataset,
|
||||||
ChartDomain,
|
ChartDomain,
|
||||||
@ -50,13 +51,13 @@ export const getByAccessor = <DataType extends Record<any, any>, R>(accessor: ke
|
|||||||
return (d) => d[accessor]
|
return (d) => d[accessor]
|
||||||
}
|
}
|
||||||
|
|
||||||
const createAxis = <DataType,>(config: ChartAxis<DataType>) => {
|
const createAxis = <DataType extends BaseDataType>(config: ChartAxis<DataType>) => {
|
||||||
if (config.type === 'time')
|
if (config.type === 'time')
|
||||||
return d3.scaleTime()
|
return d3.scaleTime()
|
||||||
return d3.scaleLinear()
|
return d3.scaleLinear()
|
||||||
}
|
}
|
||||||
|
|
||||||
export type D3ChartProps<DataType> = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
export type D3ChartProps<DataType extends BaseDataType> = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||||
/** Параметры общей горизонтальной оси */
|
/** Параметры общей горизонтальной оси */
|
||||||
xAxis: ChartAxis<DataType>
|
xAxis: ChartAxis<DataType>
|
||||||
/** Параметры графиков */
|
/** Параметры графиков */
|
||||||
@ -94,7 +95,7 @@ export type D3ChartProps<DataType> = React.DetailedHTMLProps<React.HTMLAttribute
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDefaultXAxisConfig = <DataType,>(): ChartAxis<DataType> => ({
|
const getDefaultXAxisConfig = <DataType extends BaseDataType>(): ChartAxis<DataType> => ({
|
||||||
type: 'time',
|
type: 'time',
|
||||||
accessor: (d: any) => new Date(d.date)
|
accessor: (d: any) => new Date(d.date)
|
||||||
})
|
})
|
||||||
|
105
src/components/d3/D3HorizontalPercentChart.tsx
Normal file
105
src/components/d3/D3HorizontalPercentChart.tsx
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
|
import { useElementSize } from 'usehooks-ts'
|
||||||
|
import { Property } from 'csstype'
|
||||||
|
import * as d3 from 'd3'
|
||||||
|
|
||||||
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
|
import { ChartOffset } from './types'
|
||||||
|
|
||||||
|
import '@styles/d3.less'
|
||||||
|
import { usePartialProps } from '@asb/utils'
|
||||||
|
|
||||||
|
export type PercentChartDataType = {
|
||||||
|
name: string
|
||||||
|
percent: number
|
||||||
|
color?: Property.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
export type D3HorizontalChartProps = {
|
||||||
|
width?: Property.Width
|
||||||
|
height?: Property.Height
|
||||||
|
data: PercentChartDataType[]
|
||||||
|
offset?: Partial<ChartOffset>
|
||||||
|
afterDraw?: (d: d3.Selection<SVGRectElement, PercentChartDataType, SVGGElement, unknown>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultOffset = { top: 50, right: 100, bottom: 50, left: 100 }
|
||||||
|
|
||||||
|
export const D3HorizontalPercentChart = memo<D3HorizontalChartProps>(({
|
||||||
|
width: givenWidth = '100%',
|
||||||
|
height: givenHeight = '100%',
|
||||||
|
offset: givenOffset,
|
||||||
|
data,
|
||||||
|
afterDraw,
|
||||||
|
}) => {
|
||||||
|
const offset = usePartialProps<ChartOffset>(givenOffset, defaultOffset)
|
||||||
|
|
||||||
|
const [divRef, { width, height }] = useElementSize()
|
||||||
|
const rootRef = useRef<SVGGElement | null>(null)
|
||||||
|
|
||||||
|
const root = useCallback(() => rootRef.current ? d3.select(rootRef.current) : null, [rootRef.current])
|
||||||
|
|
||||||
|
const inlineWidth = useMemo(() => width - offset.left - offset.right, [width])
|
||||||
|
const inlineHeight = useMemo(() => height - offset.top - offset.bottom, [height])
|
||||||
|
|
||||||
|
const xScale = useMemo(() => d3.scaleLinear().domain([0, 100]).range([0, inlineWidth]), [inlineWidth])
|
||||||
|
const yScale = useMemo(() => d3.scaleBand().domain(data.map((d) => d.name)).range([0, inlineHeight]).padding(0.25), [data, inlineHeight])
|
||||||
|
|
||||||
|
useEffect(() => { /// Отрисовываем оси X сверху и снизу
|
||||||
|
const r = root()
|
||||||
|
if (width < 100 || height < 100 || !r) return
|
||||||
|
const xAxisTop = d3.axisTop(xScale).tickFormat((d) => `${d}%`).ticks(4).tickSize(-inlineHeight)
|
||||||
|
const xAxisBottom = d3.axisBottom(xScale).tickFormat((d) => `${d}%`).ticks(4)
|
||||||
|
|
||||||
|
r.selectChild<SVGGElement>('.axis.x.bottom').call(xAxisBottom)
|
||||||
|
r.selectChild<SVGGElement>('.axis.x.top').call(xAxisTop)
|
||||||
|
.selectAll('.tick')
|
||||||
|
.attr('class', 'tick grid-line')
|
||||||
|
}, [root, width, height, xScale, inlineHeight])
|
||||||
|
|
||||||
|
useEffect(() => { /// Отрисовываем ось Y слева
|
||||||
|
const r = root()
|
||||||
|
if (width < 100 || height < 100 || !r) return
|
||||||
|
r.selectChild<SVGGElement>('.axis.y.left').call(d3.axisLeft(yScale))
|
||||||
|
}, [root, width, height, yScale])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const r = root()
|
||||||
|
if (width < 100 || height < 100 || !r) return
|
||||||
|
|
||||||
|
const delay = d3.transition().duration(500).ease(d3.easeLinear)
|
||||||
|
|
||||||
|
const rects = r.selectChild('.data').selectAll('rect').data(data)
|
||||||
|
rects.enter().append('rect')
|
||||||
|
rects.exit().remove()
|
||||||
|
|
||||||
|
const selectedRects = r.selectChild<SVGGElement>('.data')
|
||||||
|
.selectAll<SVGRectElement, PercentChartDataType>('rect')
|
||||||
|
|
||||||
|
selectedRects.attr('fill', (d) => d.color || 'black')
|
||||||
|
.attr('y', (d) => yScale(d.name) ?? null)
|
||||||
|
.attr('height', yScale.bandwidth())
|
||||||
|
.transition(delay)
|
||||||
|
.attr('width', (d) => d.percent > 0 ? xScale(d.percent) : 0)
|
||||||
|
|
||||||
|
afterDraw?.(selectedRects)
|
||||||
|
|
||||||
|
}, [data, width, height, root, yScale, xScale, afterDraw])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoaderPortal show={false} style={{ width: givenWidth, height: givenHeight }}>
|
||||||
|
<div ref={divRef} style={{ width: '100%', height: '100%' }}>
|
||||||
|
<svg width={'100%'} height={'100%'}>
|
||||||
|
<g ref={rootRef} transform={`translate(${offset.left}, ${offset.top})`}>
|
||||||
|
<g className={'axis x top'}></g>
|
||||||
|
<g className={'axis x bottom'} transform={`translate(0, ${inlineHeight})`}></g>
|
||||||
|
<g className={'data'}></g>
|
||||||
|
<g className={'axis y left'}></g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</LoaderPortal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default D3HorizontalPercentChart
|
@ -6,13 +6,14 @@ import { useD3MouseZone } from '@components/d3/D3MouseZone'
|
|||||||
import { D3TooltipPosition } from '@components/d3/plugins/D3Tooltip'
|
import { D3TooltipPosition } from '@components/d3/plugins/D3Tooltip'
|
||||||
import { getChartIcon, isDev, usePartialProps } from '@utils'
|
import { getChartIcon, isDev, usePartialProps } from '@utils'
|
||||||
|
|
||||||
|
import { BaseDataType } from '../types'
|
||||||
import { ChartGroup, ChartSizes } from './D3MonitoringCharts'
|
import { ChartGroup, ChartSizes } from './D3MonitoringCharts'
|
||||||
|
|
||||||
import '@styles/d3.less'
|
import '@styles/d3.less'
|
||||||
|
|
||||||
type D3GroupRenderFunction<DataType> = (group: ChartGroup<DataType>, data: DataType[]) => ReactNode
|
type D3GroupRenderFunction<DataType extends BaseDataType> = (group: ChartGroup<DataType>, data: DataType[]) => ReactNode
|
||||||
|
|
||||||
export type D3HorizontalCursorSettings<DataType> = {
|
export type D3HorizontalCursorSettings<DataType extends BaseDataType> = {
|
||||||
width?: number
|
width?: number
|
||||||
height?: number
|
height?: number
|
||||||
render?: D3GroupRenderFunction<DataType>
|
render?: D3GroupRenderFunction<DataType>
|
||||||
@ -23,11 +24,12 @@ export type D3HorizontalCursorSettings<DataType> = {
|
|||||||
lineStyle?: SVGProps<SVGLineElement>
|
lineStyle?: SVGProps<SVGLineElement>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type D3HorizontalCursorProps<DataType> = D3HorizontalCursorSettings<DataType> & {
|
export type D3HorizontalCursorProps<DataType extends BaseDataType> = D3HorizontalCursorSettings<DataType> & {
|
||||||
groups: ChartGroup<DataType>[]
|
groups: ChartGroup<DataType>[]
|
||||||
data: DataType[]
|
data: DataType[]
|
||||||
sizes: ChartSizes
|
sizes: ChartSizes
|
||||||
yAxis?: d3.ScaleTime<number, number>
|
yAxis?: d3.ScaleTime<number, number>
|
||||||
|
spaceBetweenGroups?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultLineStyle: SVGProps<SVGLineElement> = {
|
const defaultLineStyle: SVGProps<SVGLineElement> = {
|
||||||
@ -36,7 +38,7 @@ const defaultLineStyle: SVGProps<SVGLineElement> = {
|
|||||||
|
|
||||||
const offsetY = 5
|
const offsetY = 5
|
||||||
|
|
||||||
const makeDefaultRender = <DataType,>(): D3GroupRenderFunction<DataType> => (group, data) => (
|
const makeDefaultRender = <DataType extends BaseDataType>(): D3GroupRenderFunction<DataType> => (group, data) => (
|
||||||
<>
|
<>
|
||||||
{data.length > 0 ? group.charts.map((chart) => {
|
{data.length > 0 ? group.charts.map((chart) => {
|
||||||
const xFormat = (d: number | Date) => chart.xAxis.format?.(d) ?? `${(+d).toFixed(2)} ${chart.xAxis.unit ?? ''}`
|
const xFormat = (d: number | Date) => chart.xAxis.format?.(d) ?? `${(+d).toFixed(2)} ${chart.xAxis.unit ?? ''}`
|
||||||
@ -61,8 +63,8 @@ const makeDefaultRender = <DataType,>(): D3GroupRenderFunction<DataType> => (gro
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
const _D3HorizontalCursor = <DataType,>({
|
const _D3HorizontalCursor = <DataType extends BaseDataType>({
|
||||||
width = 220,
|
spaceBetweenGroups = 30,
|
||||||
height = 200,
|
height = 200,
|
||||||
render = makeDefaultRender<DataType>(),
|
render = makeDefaultRender<DataType>(),
|
||||||
position: _position = 'bottom',
|
position: _position = 'bottom',
|
||||||
@ -139,7 +141,7 @@ const _D3HorizontalCursor = <DataType,>({
|
|||||||
if (!mouseState.visible || fixed) return
|
if (!mouseState.visible || fixed) return
|
||||||
|
|
||||||
let top = mouseState.y + offsetY
|
let top = mouseState.y + offsetY
|
||||||
if (top + height >= sizes.chartsHeight) {
|
if (mouseState.y >= sizes.chartsHeight / 2) {
|
||||||
setPosition('bottom')
|
setPosition('bottom')
|
||||||
top = mouseState.y - offsetY - height
|
top = mouseState.y - offsetY - height
|
||||||
} else {
|
} else {
|
||||||
@ -178,17 +180,21 @@ const _D3HorizontalCursor = <DataType,>({
|
|||||||
{groups.map((_, i) => (
|
{groups.map((_, i) => (
|
||||||
<foreignObject
|
<foreignObject
|
||||||
key={`${i}`}
|
key={`${i}`}
|
||||||
width={width}
|
width={sizes.groupWidth + spaceBetweenGroups}
|
||||||
height={height}
|
height={height}
|
||||||
x={sizes.groupLeft(i) + (sizes.groupWidth - width) / 2}
|
x={sizes.groupLeft(i) - spaceBetweenGroups / 2}
|
||||||
y={tooltipY}
|
y={tooltipY}
|
||||||
opacity={fixed || mouseState.visible ? 1 : 0}
|
opacity={fixed || mouseState.visible ? 1 : 0}
|
||||||
pointerEvents={fixed ? 'all' : 'none'}
|
pointerEvents={fixed ? 'all' : 'none'}
|
||||||
style={{ transition: 'opacity .1s ease-out', userSelect: fixed ? 'auto' : 'none' }}
|
style={{ transition: 'opacity .1s ease-out', userSelect: fixed ? 'auto' : 'none' }}
|
||||||
>
|
>
|
||||||
<div className={`tooltip ${position} ${className}`}>
|
<div className={'tooltip-wrapper'}>
|
||||||
<div className={'tooltip-content'}>
|
<div className={`adaptive-tooltip tooltip ${position} ${className}`}
|
||||||
{tooltipBodies[i]}
|
style={{height: 'auto', bottom: `${position === 'bottom' ? '0' : ''}`}}
|
||||||
|
>
|
||||||
|
<div className={'tooltip-content'}>
|
||||||
|
{tooltipBodies[i]}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</foreignObject>
|
</foreignObject>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Button, Form, FormItemProps, Input, InputNumber, Select, Tooltip } from 'antd'
|
import { Button, Checkbox, Form, FormItemProps, Input, InputNumber, Select, Tooltip } from 'antd'
|
||||||
import { memo, useCallback, useEffect, useMemo } from 'react'
|
import { memo, useCallback, useEffect, useMemo } from 'react'
|
||||||
|
|
||||||
import { MinMax } from '@components/d3/types'
|
import { BaseDataType, MinMax } from '@components/d3/types'
|
||||||
import { ColorPicker, Color } from '@components/ColorPicker'
|
import { ColorPicker, Color } from '@components/ColorPicker'
|
||||||
|
|
||||||
import { ExtendedChartDataset } from './D3MonitoringCharts'
|
import { ExtendedChartDataset } from './D3MonitoringCharts'
|
||||||
@ -18,13 +18,13 @@ const lineTypes = [
|
|||||||
{ value: 'needle', label: 'Иглы' },
|
{ value: 'needle', label: 'Иглы' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export type D3MonitoringChartEditorProps<DataType> = {
|
export type D3MonitoringChartEditorProps<DataType extends BaseDataType> = {
|
||||||
group: ExtendedChartDataset<DataType>[]
|
group: ExtendedChartDataset<DataType>[]
|
||||||
chart: ExtendedChartDataset<DataType>
|
chart: ExtendedChartDataset<DataType>
|
||||||
onChange: (value: ExtendedChartDataset<DataType>) => boolean
|
onChange: (value: ExtendedChartDataset<DataType>) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const _D3MonitoringChartEditor = <DataType,>({
|
const _D3MonitoringChartEditor = <DataType extends BaseDataType>({
|
||||||
group,
|
group,
|
||||||
chart: value,
|
chart: value,
|
||||||
onChange,
|
onChange,
|
||||||
@ -43,10 +43,12 @@ const _D3MonitoringChartEditor = <DataType,>({
|
|||||||
}, [value])
|
}, [value])
|
||||||
|
|
||||||
const onDomainChange = useCallback((mm: MinMax) => {
|
const onDomainChange = useCallback((mm: MinMax) => {
|
||||||
onSave({ xDomain: {
|
onSave({
|
||||||
min: ('min' in mm) ? mm.min : value.xDomain?.min,
|
xDomain: {
|
||||||
max: ('max' in mm) ? mm.max : value.xDomain?.max,
|
min: ('min' in mm) ? mm.min : value.xDomain?.min,
|
||||||
}})
|
max: ('max' in mm) ? mm.max : value.xDomain?.max,
|
||||||
|
}
|
||||||
|
})
|
||||||
}, [value])
|
}, [value])
|
||||||
|
|
||||||
const onColorChange = useCallback((color: Color) => {
|
const onColorChange = useCallback((color: Color) => {
|
||||||
@ -91,8 +93,8 @@ const _D3MonitoringChartEditor = <DataType,>({
|
|||||||
</Item>
|
</Item>
|
||||||
<Item label={'Диапазон'}>
|
<Item label={'Диапазон'}>
|
||||||
<Input.Group compact>
|
<Input.Group compact>
|
||||||
<InputNumber disabled={!!value.linkedTo} value={value.xDomain?.min} onChange={(min) => onDomainChange({ min })} placeholder={'Мин'} />
|
<InputNumber disabled={!!value.linkedTo} value={value.xDomain?.min} onChange={(min) => onDomainChange({ min: min ?? undefined })} placeholder={'Мин'} />
|
||||||
<InputNumber disabled={!!value.linkedTo} value={value.xDomain?.max} onChange={(max) => onDomainChange({ max })} placeholder={'Макс'} />
|
<InputNumber disabled={!!value.linkedTo} value={value.xDomain?.max} onChange={(max) => onDomainChange({ max: max ?? undefined })} placeholder={'Макс'} />
|
||||||
<Button
|
<Button
|
||||||
disabled={!!value.linkedTo || (!Number.isFinite(value.xDomain?.min) && !Number.isFinite(value.xDomain?.max))}
|
disabled={!!value.linkedTo || (!Number.isFinite(value.xDomain?.min) && !Number.isFinite(value.xDomain?.max))}
|
||||||
onClick={() => onDomainChange({ min: undefined, max: undefined })}
|
onClick={() => onDomainChange({ min: undefined, max: undefined })}
|
||||||
@ -100,6 +102,14 @@ const _D3MonitoringChartEditor = <DataType,>({
|
|||||||
</Input.Group>
|
</Input.Group>
|
||||||
</Item>
|
</Item>
|
||||||
<Item label={'Цвет линий'}><ColorPicker onChange={onColorChange} value={value.color} /></Item>
|
<Item label={'Цвет линий'}><ColorPicker onChange={onColorChange} value={value.color} /></Item>
|
||||||
|
<Item>
|
||||||
|
<Checkbox
|
||||||
|
checked={value.showCurrentValue}
|
||||||
|
onChange={(e) => onSave({ showCurrentValue: e.target.checked })}
|
||||||
|
>
|
||||||
|
Показать текущее значение сверху
|
||||||
|
</Checkbox>
|
||||||
|
</Item>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import LoaderPortal from '@components/LoaderPortal'
|
|||||||
import { isDev, usePartialProps, useUserSettings } from '@utils'
|
import { isDev, usePartialProps, useUserSettings } from '@utils'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
BaseDataType,
|
||||||
ChartAxis,
|
ChartAxis,
|
||||||
ChartDataset,
|
ChartDataset,
|
||||||
ChartOffset,
|
ChartOffset,
|
||||||
@ -25,6 +26,7 @@ import { getByAccessor, getChartClass, getGroupClass, getTicks } from '@componen
|
|||||||
import { renderArea, renderLine, renderNeedle, renderPoint, renderRectArea } from '@components/d3/renders'
|
import { renderArea, renderLine, renderNeedle, renderPoint, renderRectArea } from '@components/d3/renders'
|
||||||
|
|
||||||
import D3MonitoringEditor from './D3MonitoringEditor'
|
import D3MonitoringEditor from './D3MonitoringEditor'
|
||||||
|
import D3MonitoringCurrentValues from './D3MonitoringCurrentValues'
|
||||||
import { D3HorizontalCursor, D3HorizontalCursorSettings } from './D3HorizontalCursor'
|
import { D3HorizontalCursor, D3HorizontalCursorSettings } from './D3HorizontalCursor'
|
||||||
import D3MonitoringLimitChart, { TelemetryRegulators } from './D3MonitoringLimitChart'
|
import D3MonitoringLimitChart, { TelemetryRegulators } from './D3MonitoringLimitChart'
|
||||||
|
|
||||||
@ -50,16 +52,18 @@ const calculateDomain = (mm: MinMax): Required<MinMax> => {
|
|||||||
return { min, max }
|
return { min, max }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExtendedChartDataset<DataType> = ChartDataset<DataType> & {
|
export type ExtendedChartDataset<DataType extends BaseDataType> = ChartDataset<DataType> & {
|
||||||
/** Диапазон отображаемых значений по горизонтальной оси */
|
/** Диапазон отображаемых значений по горизонтальной оси */
|
||||||
xDomain: MinMax
|
xDomain: MinMax
|
||||||
/** Скрыть отображение шкалы графика */
|
/** Скрыть отображение шкалы графика */
|
||||||
hideLabel?: boolean
|
hideLabel?: boolean
|
||||||
|
/** Показать последнее значение сверху графика */
|
||||||
|
showCurrentValue?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExtendedChartRegistry<DataType> = ChartRegistry<DataType> & ExtendedChartDataset<DataType>
|
export type ExtendedChartRegistry<DataType extends BaseDataType> = ChartRegistry<DataType> & ExtendedChartDataset<DataType>
|
||||||
|
|
||||||
export type ChartGroup<DataType> = {
|
export type ChartGroup<DataType extends BaseDataType> = {
|
||||||
/** Получить D3 выборку, содержащую корневой G-элемент группы */
|
/** Получить D3 выборку, содержащую корневой G-элемент группы */
|
||||||
(): d3.Selection<SVGGElement, any, any, any>
|
(): d3.Selection<SVGGElement, any, any, any>
|
||||||
/** Уникальный ключ группы (индекс) */
|
/** Уникальный ключ группы (индекс) */
|
||||||
@ -83,12 +87,12 @@ const defaultRegulators: TelemetryRegulators = {
|
|||||||
5: { color: '#007070', label: 'Расход' },
|
5: { color: '#007070', label: 'Расход' },
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDefaultYAxisConfig = <DataType,>(): ChartAxis<DataType> => ({
|
const getDefaultYAxisConfig = <DataType extends BaseDataType>(): ChartAxis<DataType> => ({
|
||||||
type: 'time',
|
type: 'time',
|
||||||
accessor: (d: any) => new Date(d.date)
|
accessor: (d: any) => new Date(d.date)
|
||||||
})
|
})
|
||||||
|
|
||||||
const getDefaultYTicks = <DataType,>(): Required<ChartTick<DataType>> => ({
|
const getDefaultYTicks = <DataType extends BaseDataType>(): Required<ChartTick<DataType>> => ({
|
||||||
visible: false,
|
visible: false,
|
||||||
format: (d: d3.NumberValue, idx: number, data?: DataType) => String(d),
|
format: (d: d3.NumberValue, idx: number, data?: DataType) => String(d),
|
||||||
color: 'lightgray',
|
color: 'lightgray',
|
||||||
@ -98,7 +102,7 @@ const getDefaultYTicks = <DataType,>(): Required<ChartTick<DataType>> => ({
|
|||||||
/**
|
/**
|
||||||
* @template DataType тип данных отображаемых записей
|
* @template DataType тип данных отображаемых записей
|
||||||
*/
|
*/
|
||||||
export type D3MonitoringChartsProps<DataType extends Record<string, any>> = Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, 'ref'> & {
|
export type D3MonitoringChartsProps<DataType extends BaseDataType> = Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, 'ref'> & {
|
||||||
/** Двумерный массив датасетов (группа-график) */
|
/** Двумерный массив датасетов (группа-график) */
|
||||||
datasetGroups: ExtendedChartDataset<DataType>[][]
|
datasetGroups: ExtendedChartDataset<DataType>[][]
|
||||||
/** Ширина графика числом пикселей или CSS-значением (px/%/em/rem) */
|
/** Ширина графика числом пикселей или CSS-значением (px/%/em/rem) */
|
||||||
@ -291,9 +295,9 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
|||||||
resetDatasets()
|
resetDatasets()
|
||||||
}, [resetDatasets, resetRegulators])
|
}, [resetDatasets, resetRegulators])
|
||||||
|
|
||||||
useEffect(() => methods?.({ setSettingsVisible }), [methods])
|
useEffect(() => methods?.({ setSettingsVisible }), [methods]) /// Возвращаем в делегат доступные методы
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { /// Обновляем группы
|
||||||
if (isDev()) {
|
if (isDev()) {
|
||||||
datasets.forEach((sets, i) => {
|
datasets.forEach((sets, i) => {
|
||||||
sets.forEach((set, j) => {
|
sets.forEach((set, j) => {
|
||||||
@ -366,7 +370,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
|||||||
})
|
})
|
||||||
}, [yAxisConfig, chartArea, datasets, animDurationMs, createAxesGroup])
|
}, [yAxisConfig, chartArea, datasets, animDurationMs, createAxesGroup])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { /// Обновляем группы и горизонтальные оси
|
||||||
const axesGroups = axesArea()
|
const axesGroups = axesArea()
|
||||||
.selectAll('.charts-group')
|
.selectAll('.charts-group')
|
||||||
.data(groups)
|
.data(groups)
|
||||||
@ -542,7 +546,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
|||||||
return <line key={`${i}`} x1={x} x2={x} y1={sizes.chartsTop} y2={offset.top + sizes.inlineHeight} />
|
return <line key={`${i}`} x1={x} x2={x} y1={sizes.chartsTop} y2={offset.top + sizes.inlineHeight} />
|
||||||
})}
|
})}
|
||||||
</g>
|
</g>
|
||||||
<D3MonitoringLimitChart
|
<D3MonitoringLimitChart<DataType>
|
||||||
regulators={regulators}
|
regulators={regulators}
|
||||||
data={data}
|
data={data}
|
||||||
yAxis={yAxis}
|
yAxis={yAxis}
|
||||||
@ -552,13 +556,20 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
|||||||
top={sizes.chartsTop}
|
top={sizes.chartsTop}
|
||||||
zoneWidth={sizes.inlineWidth}
|
zoneWidth={sizes.inlineWidth}
|
||||||
/>
|
/>
|
||||||
|
<D3MonitoringCurrentValues<DataType>
|
||||||
|
groups={groups}
|
||||||
|
data={data}
|
||||||
|
sizes={sizes}
|
||||||
|
/>
|
||||||
<D3MouseZone width={width} height={height} offset={{ ...offset, top: sizes.chartsTop }}>
|
<D3MouseZone width={width} height={height} offset={{ ...offset, top: sizes.chartsTop }}>
|
||||||
<D3HorizontalCursor
|
<D3HorizontalCursor
|
||||||
{...plugins?.cursor}
|
{...plugins?.cursor}
|
||||||
yAxis={yAxis}
|
yAxis={yAxis}
|
||||||
groups={groups}
|
groups={groups}
|
||||||
sizes={sizes}
|
sizes={sizes}
|
||||||
|
spaceBetweenGroups={spaceBetweenGroups}
|
||||||
data={data}
|
data={data}
|
||||||
|
height={height}
|
||||||
/>
|
/>
|
||||||
</D3MouseZone>
|
</D3MouseZone>
|
||||||
</svg>
|
</svg>
|
||||||
@ -581,6 +592,6 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const D3MonitoringCharts = memo(_D3MonitoringCharts)
|
export const D3MonitoringCharts = memo(_D3MonitoringCharts) as typeof _D3MonitoringCharts
|
||||||
|
|
||||||
export default D3MonitoringCharts
|
export default D3MonitoringCharts
|
||||||
|
46
src/components/d3/monitoring/D3MonitoringCurrentValues.tsx
Normal file
46
src/components/d3/monitoring/D3MonitoringCurrentValues.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
|
||||||
|
import { BaseDataType } from '@components/d3/types'
|
||||||
|
import { ChartGroup, ChartSizes } from '@components/d3/monitoring/D3MonitoringCharts'
|
||||||
|
import { makeDisplayValue } from '@utils'
|
||||||
|
|
||||||
|
export type D3MonitoringCurrentValuesProps<DataType extends BaseDataType> = {
|
||||||
|
/** Группы графиков */
|
||||||
|
groups: ChartGroup<DataType>[]
|
||||||
|
/** Массив данных графика */
|
||||||
|
data: DataType[]
|
||||||
|
/** Объект, хранящий полезные размеры и отступы графика (нужен только groupWidth, chartsTop и groupLeft) */
|
||||||
|
sizes: ChartSizes
|
||||||
|
}
|
||||||
|
|
||||||
|
const display = makeDisplayValue({ def: '---', fixed: 2 })
|
||||||
|
|
||||||
|
/// `Array.at` вместе с `??` возвращает странный тип, поэтому его пока пришлось пометить как `any`
|
||||||
|
/// TODO: Исправить тип
|
||||||
|
const _D3MonitoringCurrentValues = <DataType extends BaseDataType>({ groups, data, sizes }: D3MonitoringCurrentValuesProps<DataType>) => (
|
||||||
|
<g transform={`translate(${sizes.left}, ${sizes.chartsTop})`} pointerEvents={'none'}>
|
||||||
|
{groups.map((group) => (
|
||||||
|
<g key={group.key} transform={`translate(${sizes.groupLeft(group.key)}, 0)`}>
|
||||||
|
{group.charts.filter((chart) => chart.showCurrentValue).map((chart, i) => (
|
||||||
|
<g key={chart.key} stroke={'white'} fill={chart.color} strokeWidth={4} paintOrder={'stroke'} style={{ fontWeight: 600 }}>
|
||||||
|
<text x={sizes.groupWidth / 2 - 10} textAnchor={'end'} y={15 + i * 20}>{chart.shortLabel ?? chart.label}:</text>
|
||||||
|
<text x={sizes.groupWidth / 2 + 10} textAnchor={'start'} y={15 + i * 20}>{display(chart.x((data.at(-1) ?? {}) as any))}</text>
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отрисовывает последние значения графиков
|
||||||
|
*
|
||||||
|
* @typeParam DataType - тип данных для отрисовки графиков
|
||||||
|
*
|
||||||
|
* @param groups - Массив групп графиков
|
||||||
|
* @param data - Массив данных графиков
|
||||||
|
* @param sizes - Объект с полезными размерами и отступами внутри svg
|
||||||
|
*/
|
||||||
|
export const D3MonitoringCurrentValues = memo(_D3MonitoringCurrentValues) as typeof _D3MonitoringCurrentValues
|
||||||
|
|
||||||
|
export default D3MonitoringCurrentValues
|
@ -1,17 +1,18 @@
|
|||||||
import { CSSProperties, Key, memo, useCallback, useEffect, useMemo, useState } from 'react'
|
import { CSSProperties, Key, memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { Button, Divider, Empty, Modal, Popconfirm, Tooltip, Tree } from 'antd'
|
import { Button, Divider, Empty, Modal, Popconfirm, Tooltip, Tree, TreeDataNode } from 'antd'
|
||||||
import { UndoOutlined } from '@ant-design/icons'
|
import { UndoOutlined } from '@ant-design/icons'
|
||||||
import { EventDataNode } from 'antd/lib/tree'
|
import { EventDataNode } from 'antd/lib/tree'
|
||||||
|
|
||||||
import { notify } from '@components/factory'
|
import { notify } from '@components/factory'
|
||||||
import { getChartIcon } from '@utils'
|
import { getChartIcon } from '@utils'
|
||||||
|
|
||||||
|
import { BaseDataType } from '../types'
|
||||||
import { ExtendedChartDataset } from './D3MonitoringCharts'
|
import { ExtendedChartDataset } from './D3MonitoringCharts'
|
||||||
import { TelemetryRegulators } from './D3MonitoringLimitChart'
|
import { TelemetryRegulators } from './D3MonitoringLimitChart'
|
||||||
import D3MonitoringChartEditor from './D3MonitoringChartEditor'
|
import D3MonitoringChartEditor from './D3MonitoringChartEditor'
|
||||||
import D3MonitoringLimitEditor from './D3MonitoringLimitEditor'
|
import D3MonitoringLimitEditor from './D3MonitoringLimitEditor'
|
||||||
|
|
||||||
export type D3MonitoringGroupsEditorProps<DataType> = {
|
export type D3MonitoringGroupsEditorProps<DataType extends BaseDataType> = {
|
||||||
visible?: boolean
|
visible?: boolean
|
||||||
groups: ExtendedChartDataset<DataType>[][]
|
groups: ExtendedChartDataset<DataType>[][]
|
||||||
regulators: TelemetryRegulators
|
regulators: TelemetryRegulators
|
||||||
@ -20,7 +21,7 @@ export type D3MonitoringGroupsEditorProps<DataType> = {
|
|||||||
onReset: () => void
|
onReset: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const getChartLabel = <DataType,>(chart: ExtendedChartDataset<DataType>) => (
|
const getChartLabel = <DataType extends BaseDataType>(chart: ExtendedChartDataset<DataType>) => (
|
||||||
<Tooltip title={chart.label}>
|
<Tooltip title={chart.label}>
|
||||||
{getChartIcon(chart)} {chart.label}
|
{getChartIcon(chart)} {chart.label}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -34,14 +35,14 @@ const divStyle: CSSProperties = {
|
|||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
const getNodePos = (node: EventDataNode): { group: number, chart?: number } => {
|
const getNodePos = (node: EventDataNode<TreeDataNode>): { group: number, chart?: number } => {
|
||||||
const out = node.pos.split('-').map(Number)
|
const out = node.pos.split('-').map(Number)
|
||||||
return { group: out[1], chart: out[2] }
|
return { group: out[1], chart: out[2] }
|
||||||
}
|
}
|
||||||
|
|
||||||
type EditingMode = null | 'limit' | 'chart'
|
type EditingMode = null | 'limit' | 'chart'
|
||||||
|
|
||||||
const _D3MonitoringEditor = <DataType,>({
|
const _D3MonitoringEditor = <DataType extends BaseDataType>({
|
||||||
visible,
|
visible,
|
||||||
groups: oldGroups,
|
groups: oldGroups,
|
||||||
regulators: oldRegulators,
|
regulators: oldRegulators,
|
||||||
@ -61,8 +62,8 @@ const _D3MonitoringEditor = <DataType,>({
|
|||||||
const onModalOk = useCallback(() => onChange(groups, regulators), [groups, regulators])
|
const onModalOk = useCallback(() => onChange(groups, regulators), [groups, regulators])
|
||||||
|
|
||||||
const onDrop = useCallback((info: {
|
const onDrop = useCallback((info: {
|
||||||
node: EventDataNode
|
node: EventDataNode<TreeDataNode>
|
||||||
dragNode: EventDataNode
|
dragNode: EventDataNode<TreeDataNode>
|
||||||
dropPosition: number
|
dropPosition: number
|
||||||
}) => {
|
}) => {
|
||||||
const { dragNode, dropPosition, node } = info
|
const { dragNode, dropPosition, node } = info
|
||||||
@ -134,7 +135,7 @@ const _D3MonitoringEditor = <DataType,>({
|
|||||||
<Modal
|
<Modal
|
||||||
centered
|
centered
|
||||||
width={800}
|
width={800}
|
||||||
visible={visible}
|
open={visible}
|
||||||
title={'Настройка групп графиков'}
|
title={'Настройка групп графиков'}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
footer={(
|
footer={(
|
||||||
@ -152,18 +153,18 @@ const _D3MonitoringEditor = <DataType,>({
|
|||||||
<Tree
|
<Tree
|
||||||
draggable
|
draggable
|
||||||
selectable
|
selectable
|
||||||
onExpand={(keys) => setExpand(keys)}
|
onExpand={(keys: Key[]) => setExpand(keys)}
|
||||||
expandedKeys={expand}
|
expandedKeys={expand}
|
||||||
selectedKeys={selected}
|
selectedKeys={selected}
|
||||||
treeData={treeItems}
|
treeData={treeItems}
|
||||||
onDrop={onDrop}
|
onDrop={onDrop}
|
||||||
onSelect={(value) => {
|
onSelect={(value: Key[]) => {
|
||||||
setSelected(value)
|
setSelected(value)
|
||||||
setMode('chart')
|
setMode('chart')
|
||||||
}}
|
}}
|
||||||
height={250}
|
height={250}
|
||||||
/>
|
/>
|
||||||
<Button onClick={() => setMode('limit')}>Ограничение подачи</Button>
|
{/* <Button onClick={() => setMode('limit')}>Ограничение подачи</Button> */}
|
||||||
</div>
|
</div>
|
||||||
<Divider type={'vertical'} style={{ height: '100%', padding: '0 5px' }} />
|
<Divider type={'vertical'} style={{ height: '100%', padding: '0 5px' }} />
|
||||||
<div style={divStyle}>
|
<div style={divStyle}>
|
||||||
|
@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef } from 'react'
|
|||||||
import { Property } from 'csstype'
|
import { Property } from 'csstype'
|
||||||
import * as d3 from 'd3'
|
import * as d3 from 'd3'
|
||||||
|
|
||||||
import { ChartRegistry } from '@components/d3/types'
|
import { BaseDataType, ChartRegistry } from '@components/d3/types'
|
||||||
import { useD3MouseZone } from '@components/d3/D3MouseZone'
|
import { useD3MouseZone } from '@components/d3/D3MouseZone'
|
||||||
import { usePartialProps } from '@utils'
|
import { usePartialProps } from '@utils'
|
||||||
|
|
||||||
@ -32,12 +32,12 @@ export type D3LegendSettings = {
|
|||||||
|
|
||||||
const defaultOffset = { x: 10, y: 10 }
|
const defaultOffset = { x: 10, y: 10 }
|
||||||
|
|
||||||
export type D3LegendProps<DataType> = D3LegendSettings & {
|
export type D3LegendProps<DataType extends BaseDataType> = D3LegendSettings & {
|
||||||
/** Массив графиков */
|
/** Массив графиков */
|
||||||
charts: ChartRegistry<DataType>[]
|
charts: ChartRegistry<DataType>[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _D3Legend = <DataType,>({
|
const _D3Legend = <DataType extends BaseDataType>({
|
||||||
charts,
|
charts,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
@ -4,7 +4,7 @@ import * as d3 from 'd3'
|
|||||||
|
|
||||||
import { isDev } from '@utils'
|
import { isDev } from '@utils'
|
||||||
|
|
||||||
import { ChartRegistry } from '@components/d3/types'
|
import { BaseDataType, ChartRegistry } from '@components/d3/types'
|
||||||
import { D3MouseState, useD3MouseZone } from '@components/d3/D3MouseZone'
|
import { D3MouseState, useD3MouseZone } from '@components/d3/D3MouseZone'
|
||||||
import { getTouchedElements, wrapPlugin } from './base'
|
import { getTouchedElements, wrapPlugin } from './base'
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ import '@styles/d3.less'
|
|||||||
|
|
||||||
export type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none'
|
export type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none'
|
||||||
|
|
||||||
export type D3RenderData<DataType> = {
|
export type D3RenderData<DataType extends BaseDataType> = {
|
||||||
/** Параметры графика */
|
/** Параметры графика */
|
||||||
chart: ChartRegistry<DataType>
|
chart: ChartRegistry<DataType>
|
||||||
/** Данные графика */
|
/** Данные графика */
|
||||||
@ -21,9 +21,9 @@ export type D3RenderData<DataType> = {
|
|||||||
selection?: d3.Selection<any, DataType, any, any>
|
selection?: d3.Selection<any, DataType, any, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type D3RenderFunction<DataType> = (data: D3RenderData<DataType>[], mouseState: D3MouseState) => ReactNode
|
export type D3RenderFunction<DataType extends BaseDataType> = (data: D3RenderData<DataType>[], mouseState: D3MouseState) => ReactNode
|
||||||
|
|
||||||
export type D3TooltipSettings<DataType> = {
|
export type D3TooltipSettings<DataType extends BaseDataType> = {
|
||||||
/** Функция отрисоки тултипа */
|
/** Функция отрисоки тултипа */
|
||||||
render?: D3RenderFunction<DataType>
|
render?: D3RenderFunction<DataType>
|
||||||
/** Ширина тултипа */
|
/** Ширина тултипа */
|
||||||
@ -39,7 +39,7 @@ export type D3TooltipSettings<DataType> = {
|
|||||||
limit?: number
|
limit?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const makeDefaultRender = <DataType,>(): D3RenderFunction<DataType> => (data, mouseState) => (
|
export const makeDefaultRender = <DataType extends BaseDataType>(): D3RenderFunction<DataType> => (data, mouseState) => (
|
||||||
<>
|
<>
|
||||||
{data.length > 0 ? data.map(({ chart, data }) => {
|
{data.length > 0 ? data.map(({ chart, data }) => {
|
||||||
let Icon
|
let Icon
|
||||||
@ -74,11 +74,11 @@ export const makeDefaultRender = <DataType,>(): D3RenderFunction<DataType> => (d
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
export type D3TooltipProps<DataType> = Partial<D3TooltipSettings<DataType>> & {
|
export type D3TooltipProps<DataType extends BaseDataType> = Partial<D3TooltipSettings<DataType>> & {
|
||||||
charts: ChartRegistry<DataType>[],
|
charts: ChartRegistry<DataType>[],
|
||||||
}
|
}
|
||||||
|
|
||||||
function _D3Tooltip<DataType extends Record<string, unknown>>({
|
function _D3Tooltip<DataType extends BaseDataType>({
|
||||||
width = 200,
|
width = 200,
|
||||||
height = 120,
|
height = 120,
|
||||||
render = makeDefaultRender<DataType>(),
|
render = makeDefaultRender<DataType>(),
|
||||||
|
@ -3,7 +3,7 @@ import * as d3 from 'd3'
|
|||||||
|
|
||||||
import { getDistance, TouchType } from '@utils'
|
import { getDistance, TouchType } from '@utils'
|
||||||
|
|
||||||
import { ChartRegistry } from '../types'
|
import { BaseDataType, ChartRegistry } from '../types'
|
||||||
|
|
||||||
export type BasePluginSettings = {
|
export type BasePluginSettings = {
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
@ -16,7 +16,7 @@ export const wrapPlugin = <TProps,>(
|
|||||||
const wrappedComponent = ({ enabled, ...props }: TProps & BasePluginSettings) => {
|
const wrappedComponent = ({ enabled, ...props }: TProps & BasePluginSettings) => {
|
||||||
if (!(enabled ?? defaultEnabled)) return <></>
|
if (!(enabled ?? defaultEnabled)) return <></>
|
||||||
|
|
||||||
return <Component {...(props as TProps)} />
|
return <Component {...(props as (TProps & JSX.IntrinsicAttributes))} /> // IntrinsicAttributes добавлено как необходимое ограничение
|
||||||
}
|
}
|
||||||
|
|
||||||
return wrappedComponent
|
return wrappedComponent
|
||||||
@ -89,7 +89,7 @@ const makeIsRectTouched = (x: number, y: number, limit: number, type: TouchType
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTouchedElements = <DataType,>(
|
export const getTouchedElements = <DataType extends BaseDataType>(
|
||||||
chart: ChartRegistry<DataType>,
|
chart: ChartRegistry<DataType>,
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import * as d3 from 'd3'
|
import * as d3 from 'd3'
|
||||||
|
|
||||||
import { ChartRegistry } from '../types'
|
import { BaseDataType, ChartRegistry } from '../types'
|
||||||
|
|
||||||
export const appendTransition = <DataType, BaseType extends d3.BaseType, Datum, PElement extends d3.BaseType, PDatum>(
|
export const appendTransition = <DataType extends BaseDataType, BaseType extends d3.BaseType, Datum, PElement extends d3.BaseType, PDatum>(
|
||||||
elms: d3.Selection<BaseType, Datum, PElement, PDatum>,
|
elms: d3.Selection<BaseType, Datum, PElement, PDatum>,
|
||||||
chart: ChartRegistry<DataType>
|
chart: ChartRegistry<DataType>
|
||||||
): d3.Selection<BaseType, Datum, PElement, PDatum> => {
|
): d3.Selection<BaseType, Datum, PElement, PDatum> => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ChartRegistry, PointChartDataset } from '@components/d3/types'
|
import { BaseDataType, ChartRegistry, PointChartDataset } from '@components/d3/types'
|
||||||
|
|
||||||
import { appendTransition } from './base'
|
import { appendTransition } from './base'
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ const defaultConfig: Required<Omit<PointChartDataset, 'type'>> = {
|
|||||||
fillOpacity: 1,
|
fillOpacity: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPointsRoot = <DataType,>(chart: ChartRegistry<DataType>, embeded?: boolean): d3.Selection<SVGGElement, any, any, any> => {
|
const getPointsRoot = <DataType extends BaseDataType>(chart: ChartRegistry<DataType>, embeded?: boolean): d3.Selection<SVGGElement, any, any, any> => {
|
||||||
const root = chart()
|
const root = chart()
|
||||||
if (!embeded) return root
|
if (!embeded) return root
|
||||||
if (root.select('.points').empty())
|
if (root.select('.points').empty())
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { getByAccessor } from '@components/d3/functions'
|
import { getByAccessor } from '@components/d3/functions'
|
||||||
import { ChartRegistry } from '@components/d3/types'
|
import { BaseDataType, ChartRegistry } from '@components/d3/types'
|
||||||
|
|
||||||
import { appendTransition } from './base'
|
import { appendTransition } from './base'
|
||||||
|
|
||||||
export const renderRectArea = <DataType extends Record<string, any>>(
|
export const renderRectArea = <DataType extends BaseDataType>(
|
||||||
xAxis: (value: d3.NumberValue) => number,
|
xAxis: (value: d3.NumberValue) => number,
|
||||||
yAxis: (value: d3.NumberValue) => number,
|
yAxis: (value: d3.NumberValue) => number,
|
||||||
chart: ChartRegistry<DataType>
|
chart: ChartRegistry<DataType>
|
||||||
|
@ -3,9 +3,11 @@ import { Property } from 'csstype'
|
|||||||
|
|
||||||
import { D3TooltipSettings } from './plugins'
|
import { D3TooltipSettings } from './plugins'
|
||||||
|
|
||||||
export type AxisAccessor<DataType extends Record<string, any>> = keyof DataType | ((d: DataType) => any)
|
export type BaseDataType = Record<string, any>
|
||||||
|
|
||||||
export type ChartAxis<DataType> = {
|
export type AxisAccessor<DataType extends BaseDataType> = keyof DataType | ((d: DataType) => any)
|
||||||
|
|
||||||
|
export type ChartAxis<DataType extends BaseDataType> = {
|
||||||
/** Тип шкалы */
|
/** Тип шкалы */
|
||||||
type: 'linear' | 'time',
|
type: 'linear' | 'time',
|
||||||
/** Ключ записи или метод по которому будет извлекаться значение оси из массива данных */
|
/** Ключ записи или метод по которому будет извлекаться значение оси из массива данных */
|
||||||
@ -34,7 +36,7 @@ export type PointChartDataset = {
|
|||||||
fillOpacity?: number
|
fillOpacity?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BaseChartDataset<DataType> = {
|
export type BaseChartDataset<DataType extends BaseDataType> = {
|
||||||
/** Уникальный ключ графика */
|
/** Уникальный ключ графика */
|
||||||
key: string | number
|
key: string | number
|
||||||
/** Параметры вертикальной оси */
|
/** Параметры вертикальной оси */
|
||||||
@ -101,7 +103,7 @@ export type NeedleChartDataset = {
|
|||||||
type: 'needle'
|
type: 'needle'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChartDataset<DataType> = BaseChartDataset<DataType> & (
|
export type ChartDataset<DataType extends BaseDataType> = BaseChartDataset<DataType> & (
|
||||||
AreaChartDataset |
|
AreaChartDataset |
|
||||||
LineChartDataset |
|
LineChartDataset |
|
||||||
NeedleChartDataset |
|
NeedleChartDataset |
|
||||||
@ -154,7 +156,7 @@ export type ChartTicks<DataType> = {
|
|||||||
y?: ChartTick<DataType>
|
y?: ChartTick<DataType>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChartRegistry<DataType> = ChartDataset<DataType> & {
|
export type ChartRegistry<DataType extends BaseDataType> = ChartDataset<DataType> & {
|
||||||
/** Получить D3 выборку, содержащую корневой G-элемент графика */
|
/** Получить D3 выборку, содержащую корневой G-элемент графика */
|
||||||
(): d3.Selection<SVGGElement, DataType, any, any>
|
(): d3.Selection<SVGGElement, DataType, any, any>
|
||||||
/** Получить значение по вертикальной оси из предоставленой записи */
|
/** Получить значение по вертикальной оси из предоставленой записи */
|
||||||
|
@ -3,8 +3,7 @@ import { ArgsProps } from 'antd/lib/notification'
|
|||||||
import { Dispatch, ReactNode, SetStateAction } from 'react'
|
import { Dispatch, ReactNode, SetStateAction } from 'react'
|
||||||
|
|
||||||
import { WellView } from '@components/views'
|
import { WellView } from '@components/views'
|
||||||
import { getUserToken } from '@utils'
|
import { FunctionalValue, getFunctionalValue, getUser, isDev } from '@utils'
|
||||||
import { FunctionalValue, getFunctionalValue, isDev } from '@utils'
|
|
||||||
import { ApiError, FileInfoDto, WellDto } from '@api'
|
import { ApiError, FileInfoDto, WellDto } from '@api'
|
||||||
|
|
||||||
export type NotifyType = 'error' | 'warning' | 'info'
|
export type NotifyType = 'error' | 'warning' | 'info'
|
||||||
@ -30,7 +29,7 @@ export const notify = (body?: ReactNode, notifyType: NotifyType = 'info', well?:
|
|||||||
const message = (
|
const message = (
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<span>{instance.message}</span>
|
<span>{instance.message}</span>
|
||||||
<WellView well={well} />
|
<WellView placement={'leftBottom'} well={well} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -104,7 +103,7 @@ export const invokeWebApiWrapperAsync = (
|
|||||||
export const download = async (url: string, fileName?: string) => {
|
export const download = async (url: string, fileName?: string) => {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${getUserToken()}`
|
Authorization: `Bearer ${getUser().token}`
|
||||||
},
|
},
|
||||||
method: 'Get'
|
method: 'Get'
|
||||||
})
|
})
|
||||||
@ -132,7 +131,7 @@ export const download = async (url: string, fileName?: string) => {
|
|||||||
export const upload = async (url: string, formData: FormData) => {
|
export const upload = async (url: string, formData: FormData) => {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${getUserToken()}`
|
Authorization: `Bearer ${getUser().token}`
|
||||||
},
|
},
|
||||||
method: 'Post',
|
method: 'Post',
|
||||||
body: formData,
|
body: formData,
|
||||||
|
30
src/components/outlets/UserOutlet.tsx
Normal file
30
src/components/outlets/UserOutlet.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { memo, useEffect, useState } from 'react'
|
||||||
|
import { Outlet } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { UserContext } from '@asb/context'
|
||||||
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
import { getUser, setUser as setStorageUser } from '@utils'
|
||||||
|
import { AuthService, UserTokenDto } from '@api'
|
||||||
|
|
||||||
|
export const UserOutlet = memo(() => {
|
||||||
|
const [user, setUser] = useState<UserTokenDto>({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
invokeWebApiWrapperAsync(async () => {
|
||||||
|
let user = getUser()
|
||||||
|
if (!user.id) {
|
||||||
|
user = await AuthService.refresh()
|
||||||
|
setStorageUser(user)
|
||||||
|
}
|
||||||
|
setUser(user)
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UserContext.Provider value={user}>
|
||||||
|
<Outlet />
|
||||||
|
</UserContext.Provider>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default UserOutlet
|
1
src/components/outlets/index.ts
Normal file
1
src/components/outlets/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './UserOutlet'
|
@ -39,8 +39,8 @@ export const Poprompt = memo<PopromptProps>(({ formProps, buttonProps, footer, c
|
|||||||
)}
|
)}
|
||||||
trigger={'click'}
|
trigger={'click'}
|
||||||
{...other}
|
{...other}
|
||||||
visible={visible}
|
open={visible}
|
||||||
onVisibleChange={(visible) => setVisible(visible)}
|
onOpenChange={(visible) => setVisible(visible)}
|
||||||
>
|
>
|
||||||
<Button {...buttonProps}>{text}</Button>
|
<Button {...buttonProps}>{text}</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Button, Drawer, Skeleton, Tree, TreeProps, Typography } from 'antd'
|
import { Button, Drawer, Skeleton, Tree, TreeDataNode, TreeProps, Typography } from 'antd'
|
||||||
import { DefaultValueType } from 'rc-tree-select/lib/interface'
|
import { useState, useEffect, useCallback, memo, Key } from 'react'
|
||||||
import { useState, useEffect, ReactNode, useCallback, memo, Key } from 'react'
|
|
||||||
import { useNavigate, useLocation } from 'react-router-dom'
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import { WellIcon, WellIconState } from '@components/icons'
|
import { WellIcon, WellIconState } from '@components/icons'
|
||||||
@ -17,26 +16,18 @@ export const getWellState = (idState?: number): WellIconState => idState === 1 ?
|
|||||||
export const checkIsWellOnline = (lastTelemetryDate: unknown): boolean =>
|
export const checkIsWellOnline = (lastTelemetryDate: unknown): boolean =>
|
||||||
isRawDate(lastTelemetryDate) && (Date.now() - +new Date(lastTelemetryDate) < 600_000)
|
isRawDate(lastTelemetryDate) && (Date.now() - +new Date(lastTelemetryDate) < 600_000)
|
||||||
|
|
||||||
export type TreeNodeData = {
|
|
||||||
title?: string | null
|
|
||||||
key?: string
|
|
||||||
value?: DefaultValueType
|
|
||||||
icon?: ReactNode
|
|
||||||
children?: TreeNodeData[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const getKeyByUrl = (url?: string): [Key | null, string | null] => {
|
const getKeyByUrl = (url?: string): [Key | null, string | null] => {
|
||||||
const result = url?.match(/^\/([^\/]+)\/([^\/?]+)/) // pattern "/:type/:id"
|
const result = url?.match(/^\/([^\/]+)\/([^\/?]+)/) // pattern "/:type/:id"
|
||||||
if (!result) return [null, null]
|
if (!result) return [null, null]
|
||||||
return [result[0], result[1]]
|
return [result[0], result[1]]
|
||||||
}
|
}
|
||||||
|
|
||||||
const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined => {
|
const getLabel = (wellsTree: TreeDataNode[], value?: string): string | undefined => {
|
||||||
const [url, type] = getKeyByUrl(value)
|
const [url, type] = getKeyByUrl(value)
|
||||||
if (!url) return
|
if (!url) return
|
||||||
let deposit: TreeNodeData | undefined
|
let deposit: TreeDataNode | undefined
|
||||||
let cluster: TreeNodeData | undefined
|
let cluster: TreeDataNode | undefined
|
||||||
let well: TreeNodeData | undefined
|
let well: TreeDataNode | undefined
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'deposit':
|
case 'deposit':
|
||||||
deposit = wellsTree.find((deposit) => deposit.key === url)
|
deposit = wellsTree.find((deposit) => deposit.key === url)
|
||||||
@ -46,7 +37,7 @@ const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined
|
|||||||
|
|
||||||
case 'cluster':
|
case 'cluster':
|
||||||
deposit = wellsTree.find((deposit) => (
|
deposit = wellsTree.find((deposit) => (
|
||||||
cluster = deposit.children?.find((cluster: TreeNodeData) => cluster.key === url)
|
cluster = deposit.children?.find((cluster: TreeDataNode) => cluster.key === url)
|
||||||
))
|
))
|
||||||
if (deposit && cluster)
|
if (deposit && cluster)
|
||||||
return `${deposit.title} / ${cluster.title}`
|
return `${deposit.title} / ${cluster.title}`
|
||||||
@ -54,8 +45,8 @@ const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined
|
|||||||
|
|
||||||
case 'well':
|
case 'well':
|
||||||
deposit = wellsTree.find((deposit) => (
|
deposit = wellsTree.find((deposit) => (
|
||||||
cluster = deposit.children?.find((cluster: TreeNodeData) => (
|
cluster = deposit.children?.find((cluster: TreeDataNode) => (
|
||||||
well = cluster.children?.find((well: TreeNodeData) => well.key === url)
|
well = cluster.children?.find((well: TreeDataNode) => well.key === url)
|
||||||
))
|
))
|
||||||
))
|
))
|
||||||
if (deposit && cluster && well)
|
if (deposit && cluster && well)
|
||||||
@ -79,38 +70,49 @@ const sortWellsByActive = (a: WellDto, b: WellDto): number => {
|
|||||||
return (a.caption || '')?.localeCompare(b.caption || '')
|
return (a.caption || '')?.localeCompare(b.caption || '')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WellTreeSelector = memo(({ show, ...other }: TreeProps<TreeNodeData> & { show?: boolean }) => {
|
export type WellTreeSelectorProps = TreeProps<TreeDataNode> & {
|
||||||
const [wellsTree, setWellsTree] = useState<TreeNodeData[]>([])
|
show?: boolean
|
||||||
|
expand?: boolean | Key[]
|
||||||
|
current?: Key
|
||||||
|
onClose?: () => void
|
||||||
|
onChange?: (value: string | undefined) => void
|
||||||
|
open?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const getExpandKeys = (treeData: TreeDataNode[], depositKeys?: Key[] | boolean): Key[] => {
|
||||||
|
const out: Key[] = []
|
||||||
|
treeData.forEach((deposit) => {
|
||||||
|
if (Array.isArray(depositKeys) && !depositKeys.includes(deposit.key)) return
|
||||||
|
if (deposit.key) out.push(deposit.key)
|
||||||
|
deposit.children?.forEach((cluster) => {
|
||||||
|
if (cluster.key) out.push(cluster.key)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WellTreeSelector = memo<WellTreeSelectorProps>(({ expand, current, onChange, onClose, open, ...other }) => {
|
||||||
|
const [wellsTree, setWellsTree] = useState<TreeDataNode[]>([])
|
||||||
const [showLoader, setShowLoader] = useState<boolean>(false)
|
const [showLoader, setShowLoader] = useState<boolean>(false)
|
||||||
const [visible, setVisible] = useState<boolean>(false)
|
|
||||||
const [expanded, setExpanded] = useState<Key[]>([])
|
const [expanded, setExpanded] = useState<Key[]>([])
|
||||||
const [selected, setSelected] = useState<Key[]>([])
|
const [selected, setSelected] = useState<Key[]>([])
|
||||||
const [value, setValue] = useState<string>()
|
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setVisible((prev) => show ?? prev)
|
if (current) setSelected([current])
|
||||||
setExpanded((prev) => {
|
}, [current])
|
||||||
if (typeof show === 'undefined') return prev
|
|
||||||
if (!show) return []
|
useEffect(() => {
|
||||||
const out: Key[] = []
|
setExpanded((prev) => expand ? getExpandKeys(wellsTree, expand) : prev)
|
||||||
wellsTree.forEach((deposit) => {
|
}, [wellsTree, expand])
|
||||||
if (deposit.key) out.push(deposit.key)
|
|
||||||
deposit.children?.forEach((cluster) => {
|
|
||||||
if (cluster.key) out.push(cluster.key)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return out
|
|
||||||
})
|
|
||||||
}, [wellsTree, show])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
invokeWebApiWrapperAsync(
|
invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
const deposits: Array<DepositDto> = await DepositService.getDeposits()
|
const deposits: Array<DepositDto> = await DepositService.getDeposits()
|
||||||
const wellsTree: TreeNodeData[] = deposits.map(deposit =>({
|
const wellsTree: TreeDataNode[] = deposits.map(deposit =>({
|
||||||
title: deposit.caption,
|
title: deposit.caption,
|
||||||
key: `/deposit/${deposit.id}`,
|
key: `/deposit/${deposit.id}`,
|
||||||
value: `/deposit/${deposit.id}`,
|
value: `/deposit/${deposit.id}`,
|
||||||
@ -146,36 +148,43 @@ export const WellTreeSelector = memo(({ show, ...other }: TreeProps<TreeNodeData
|
|||||||
)
|
)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onChange = useCallback((value?: string): void => {
|
const onValueChange = useCallback((value?: string): void => {
|
||||||
const key = getKeyByUrl(value)[0]
|
const key = getKeyByUrl(value)[0]
|
||||||
setSelected(key ? [key] : [])
|
setSelected(key ? [key] : [])
|
||||||
setValue(getLabel(wellsTree, value))
|
onChange?.(getLabel(wellsTree, value))
|
||||||
}, [wellsTree])
|
}, [wellsTree])
|
||||||
|
|
||||||
const onSelect = useCallback((value: Key[]): void => {
|
const onSelect = useCallback((value: Key[]): void => {
|
||||||
navigate(String(value), { state: { from: location.pathname }})
|
const newRoot = /\/(\w+)\/\d+/.exec(String(value))
|
||||||
|
const oldRoot = /\/(\w+)(?:\/\d+)?/.exec(location.pathname)
|
||||||
|
if (!newRoot || !oldRoot) return
|
||||||
|
|
||||||
|
let newPath = newRoot[0]
|
||||||
|
if (oldRoot[1] === newRoot[1]) {
|
||||||
|
/// Если типы страницы одинаковые (deposit, cluster, well), добавляем остаток старого пути
|
||||||
|
const url = location.pathname.substring(oldRoot[0].length)
|
||||||
|
newPath = newPath + url
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(newPath, { state: { from: location.pathname }})
|
||||||
}, [navigate, location])
|
}, [navigate, location])
|
||||||
|
|
||||||
useEffect(() => onChange(location.pathname), [onChange, location])
|
useEffect(() => onValueChange(location.pathname), [onValueChange, location])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Drawer open={open} mask={false} onClose={onClose} title={'Список скважин'}>
|
||||||
<Button loading={showLoader} onClick={() => setVisible(true)}>{value ?? 'Выберите месторождение'}</Button>
|
<Skeleton active loading={showLoader}>
|
||||||
<Drawer visible={visible} mask={false} onClose={() => setVisible(false)}>
|
<Tree
|
||||||
<Typography.Title level={3}>Список скважин</Typography.Title>
|
{...other}
|
||||||
<Skeleton active loading={showLoader}>
|
showIcon
|
||||||
<Tree
|
selectedKeys={selected}
|
||||||
{...other}
|
treeData={wellsTree}
|
||||||
showIcon
|
onSelect={onSelect}
|
||||||
selectedKeys={selected}
|
onExpand={setExpanded}
|
||||||
treeData={wellsTree}
|
expandedKeys={expanded}
|
||||||
onSelect={onSelect}
|
/>
|
||||||
onExpand={setExpanded}
|
</Skeleton>
|
||||||
expandedKeys={expanded}
|
</Drawer>
|
||||||
/>
|
|
||||||
</Skeleton>
|
|
||||||
</Drawer>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Fragment, memo } from 'react'
|
import { Fragment, memo } from 'react'
|
||||||
import { Tooltip } from 'antd'
|
import { Tooltip } from 'antd'
|
||||||
|
|
||||||
import { TelemetryDto, TelemetryInfoDto } from '@api'
|
|
||||||
import { Grid, GridItem } from '@components/Grid'
|
import { Grid, GridItem } from '@components/Grid'
|
||||||
|
import { formatDate } from '@utils'
|
||||||
|
import { TelemetryDto, TelemetryInfoDto } from '@api'
|
||||||
|
|
||||||
export const lables: Record<string, string> = {
|
export const lables: Record<string, string> = {
|
||||||
timeZoneId: 'Временная зона',
|
timeZoneId: 'Временная зона',
|
||||||
@ -30,12 +31,17 @@ export const TelemetryView = memo<TelemetryViewProps>(({ telemetry }) => telemet
|
|||||||
overlayInnerStyle={{ width: '400px' }}
|
overlayInnerStyle={{ width: '400px' }}
|
||||||
title={
|
title={
|
||||||
<Grid>
|
<Grid>
|
||||||
{(Object.keys(telemetry.info) as Array<keyof TelemetryInfoDto>).map((key, i) => (
|
{(Object.keys(telemetry.info) as Array<keyof TelemetryInfoDto>).map((key, i) => {
|
||||||
<Fragment key={i}>
|
let value = telemetry.info?.[key]
|
||||||
<GridItem row={i+1} col={1}>{lables[key] ?? key}:</GridItem>
|
value = key === 'drillingStartDate' ? formatDate(value) : value
|
||||||
<GridItem row={i+1} col={2}>{telemetry.info?.[key]}</GridItem>
|
|
||||||
</Fragment>
|
return (
|
||||||
))}
|
<Fragment key={i}>
|
||||||
|
<GridItem row={i+1} col={1}>{lables[key] ?? key}:</GridItem>
|
||||||
|
<GridItem row={i+1} col={2}>{value}</GridItem>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { memo } from 'react'
|
import { HTMLProps, memo } from 'react'
|
||||||
import { Tooltip } from 'antd'
|
import { Tooltip } from 'antd'
|
||||||
import { UserOutlined } from '@ant-design/icons'
|
import { UserOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
@ -6,33 +6,58 @@ import { UserDto } from '@api'
|
|||||||
import { Grid, GridItem } from '@components/Grid'
|
import { Grid, GridItem } from '@components/Grid'
|
||||||
import { CompanyView } from './CompanyView'
|
import { CompanyView } from './CompanyView'
|
||||||
|
|
||||||
export type UserViewProps = {
|
export type UserViewProps = HTMLProps<HTMLSpanElement> & {
|
||||||
user?: UserDto
|
user?: UserDto
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserView = memo<UserViewProps>(({ user }) => user ? (
|
export const UserView = memo<UserViewProps>(({ user, ...other }) =>
|
||||||
<Tooltip title={(
|
user ? (
|
||||||
<Grid style={{ columnGap: '8px' }}>
|
<Tooltip
|
||||||
<GridItem row={1} col={1}>Фамилия:</GridItem>
|
title={
|
||||||
<GridItem row={1} col={2}>{user?.surname}</GridItem>
|
<Grid style={{ columnGap: '8px' }}>
|
||||||
|
<GridItem row={1} col={1}>
|
||||||
|
Фамилия:
|
||||||
|
</GridItem>
|
||||||
|
<GridItem row={1} col={2}>
|
||||||
|
{user?.surname}
|
||||||
|
</GridItem>
|
||||||
|
|
||||||
<GridItem row={2} col={1}>Имя:</GridItem>
|
<GridItem row={2} col={1}>
|
||||||
<GridItem row={2} col={2}>{user?.name}</GridItem>
|
Имя:
|
||||||
|
</GridItem>
|
||||||
|
<GridItem row={2} col={2}>
|
||||||
|
{user?.name}
|
||||||
|
</GridItem>
|
||||||
|
|
||||||
<GridItem row={3} col={1}>Отчество:</GridItem>
|
<GridItem row={3} col={1}>
|
||||||
<GridItem row={3} col={2}>{user?.patronymic}</GridItem>
|
Отчество:
|
||||||
|
</GridItem>
|
||||||
|
<GridItem row={3} col={2}>
|
||||||
|
{user?.patronymic}
|
||||||
|
</GridItem>
|
||||||
|
|
||||||
<GridItem row={4} col={1}>Компания:</GridItem>
|
<GridItem row={4} col={1}>
|
||||||
<GridItem row={4} col={2}>
|
Компания:
|
||||||
<CompanyView company={user?.company}/>
|
</GridItem>
|
||||||
</GridItem>
|
<GridItem row={4} col={2}>
|
||||||
</Grid>
|
<CompanyView company={user?.company} />
|
||||||
)}>
|
</GridItem>
|
||||||
<UserOutlined style={{ marginRight: 8 }}/>
|
</Grid>
|
||||||
{user?.login}
|
}
|
||||||
</Tooltip>
|
>
|
||||||
) : (
|
<span {...other}>
|
||||||
<Tooltip title='нет пользователя'>-</Tooltip>
|
<UserOutlined style={{ marginRight: 8 }} />
|
||||||
))
|
{user?.login}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Tooltip title={'нет пользователя'}>
|
||||||
|
<span {...other}>
|
||||||
|
<UserOutlined style={{ marginRight: 8 }} />
|
||||||
|
---
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
export default UserView
|
export default UserView
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { Tooltip } from 'antd'
|
import { Tooltip, TooltipProps } from 'antd'
|
||||||
|
|
||||||
import { Grid, GridItem } from '@components/Grid'
|
import { Grid, GridItem } from '@components/Grid'
|
||||||
import { WellIcon, WellIconState } from '@components/icons'
|
import { WellIcon, WellIconState } from '@components/icons'
|
||||||
@ -13,12 +13,12 @@ const wellState: Record<number, { enum: WellIconState, label: string }> = {
|
|||||||
2: { enum: 'inactive', label: 'Завершена' },
|
2: { enum: 'inactive', label: 'Завершена' },
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WellViewProps = {
|
export type WellViewProps = TooltipProps & {
|
||||||
well?: WellDto
|
well?: WellDto
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WellView = memo<WellViewProps>(({ well }) => well ? (
|
export const WellView = memo<WellViewProps>(({ well, ...other }) => well ? (
|
||||||
<Tooltip title={(
|
<Tooltip {...other} title={(
|
||||||
<Grid style={{ columnGap: '8px' }}>
|
<Grid style={{ columnGap: '8px' }}>
|
||||||
<GridItem row={1} col={1}>Название:</GridItem>
|
<GridItem row={1} col={1}>Название:</GridItem>
|
||||||
<GridItem row={1} col={2}>{well.caption ?? '---'}</GridItem>
|
<GridItem row={1} col={2}>{well.caption ?? '---'}</GridItem>
|
||||||
|
@ -20,7 +20,7 @@ export const WidgetSettingsWindow = memo<WidgetSettingsWindowProps>(({ settings,
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
{...other}
|
{...other}
|
||||||
visible={!!settings}
|
open={!!settings}
|
||||||
title={(
|
title={(
|
||||||
<>
|
<>
|
||||||
Настройка виджета {settings?.label ? `"${settings?.label}"` : ''}
|
Настройка виджета {settings?.label ? `"${settings?.label}"` : ''}
|
||||||
|
@ -1,22 +1,49 @@
|
|||||||
import { createContext, useContext } from 'react'
|
import { createContext, useContext, useEffect } from 'react'
|
||||||
|
|
||||||
import { WellDto } from '@api'
|
import { LayoutPortalProps } from '@components/LayoutPortal'
|
||||||
|
import { UserTokenDto, WellDto } from '@api'
|
||||||
|
|
||||||
/** Контекст текущей скважины */
|
/** Контекст текущей скважины */
|
||||||
export const WellContext = createContext<[WellDto, (well: WellDto) => void]>([{}, () => {}])
|
export const WellContext = createContext<[WellDto, (well: WellDto) => void]>([{}, () => {}])
|
||||||
/** Контекст текущего корневого пути */
|
/** Контекст текущего корневого пути */
|
||||||
export const RootPathContext = createContext<string>('')
|
export const RootPathContext = createContext<string>('/')
|
||||||
|
/** Контекст текущего пользователя */
|
||||||
|
export const UserContext = createContext<UserTokenDto>({})
|
||||||
|
/** Контекст метода редактирования параметров заголовка и меню */
|
||||||
|
export const LayoutPropsContext = createContext<(props: LayoutPortalProps) => void>(() => {})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получение текущей скважины
|
* Получить текущую скважину
|
||||||
*
|
*
|
||||||
* @returns Текущая скважина, либо `null`
|
* @returns Текущая скважина, либо `null`
|
||||||
*/
|
*/
|
||||||
export const useWell = () => useContext(WellContext)
|
export const useWell = () => useContext(WellContext)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получает текущий корневой путь
|
* Получить текущий корневой путь
|
||||||
*
|
*
|
||||||
* @returns Текущий корневой путь
|
* @returns Текущий корневой путь
|
||||||
*/
|
*/
|
||||||
export const useRootPath = () => useContext(RootPathContext)
|
export const useRootPath = () => useContext(RootPathContext)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить текущего пользователя
|
||||||
|
*
|
||||||
|
* @returns Текущий пользователь, либо `null`
|
||||||
|
*/
|
||||||
|
export const useUser = () => useContext(UserContext)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить метод задания параметров заголовка и меню
|
||||||
|
*
|
||||||
|
* @returns Получить метод задания параметров заголовка и меню
|
||||||
|
*/
|
||||||
|
export const useLayoutProps = (props?: LayoutPortalProps) => {
|
||||||
|
const setLayoutProps = useContext(LayoutPropsContext)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props) setLayoutProps(props)
|
||||||
|
}, [setLayoutProps, props])
|
||||||
|
|
||||||
|
return setLayoutProps
|
||||||
|
}
|
||||||
|
46
src/pages/AdminPanel/AdminNavigationMenu.jsx
Normal file
46
src/pages/AdminPanel/AdminNavigationMenu.jsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
import {
|
||||||
|
ApiOutlined,
|
||||||
|
BankOutlined,
|
||||||
|
BranchesOutlined,
|
||||||
|
DashboardOutlined,
|
||||||
|
FileSearchOutlined,
|
||||||
|
FolderOutlined,
|
||||||
|
IdcardOutlined,
|
||||||
|
MonitorOutlined,
|
||||||
|
TeamOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
|
||||||
|
import { makeItem, PrivateWellMenu } from '@components/PrivateWellMenu'
|
||||||
|
import { isDev } from '@asb/utils'
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
makeItem('Месторождения', 'deposit', [], <FolderOutlined />),
|
||||||
|
makeItem('Кусты', 'cluster', [], <FolderOutlined />),
|
||||||
|
makeItem('Скважины', 'well', [], <FolderOutlined />),
|
||||||
|
makeItem('Пользователи', 'user', [], <UserOutlined />),
|
||||||
|
makeItem('Компании', 'company', [], <BankOutlined />),
|
||||||
|
makeItem('Типы компаний', 'company_type', [], <BankOutlined />),
|
||||||
|
makeItem('Роли', 'role', [], <TeamOutlined />),
|
||||||
|
makeItem('Разрешения', 'permission', [], <IdcardOutlined />),
|
||||||
|
makeItem('Телеметрия', 'telemetry', [], <DashboardOutlined />, [
|
||||||
|
makeItem('Просмотр', 'viewer', [], <MonitorOutlined />),
|
||||||
|
makeItem('Объединение', 'merger', [], <BranchesOutlined />),
|
||||||
|
]),
|
||||||
|
makeItem('Журнал посещений', 'visit_log', [], <FileSearchOutlined />),
|
||||||
|
isDev() && makeItem('API', '/swagger/index.html', [], <ApiOutlined />),
|
||||||
|
].filter(Boolean)
|
||||||
|
|
||||||
|
export const AdminNavigationMenu = memo((props) => (
|
||||||
|
<PrivateWellMenu
|
||||||
|
{...props}
|
||||||
|
items={menuItems}
|
||||||
|
rootPath={'/admin'}
|
||||||
|
selectable={false}
|
||||||
|
mode={'inline'}
|
||||||
|
theme={'dark'}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
export default AdminNavigationMenu
|
@ -11,11 +11,9 @@ import {
|
|||||||
} from '@components/Table'
|
} from '@components/Table'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { AdminClusterService, AdminDepositService } from '@api'
|
import { AdminClusterService, AdminDepositService } from '@api'
|
||||||
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
|
import { arrayOrDefault, coordsFormat, wrapPrivateComponent } from '@utils'
|
||||||
import { min1 } from '@utils/validationRules'
|
import { min1 } from '@utils/validationRules'
|
||||||
|
|
||||||
import { coordsFixed } from './DepositController'
|
|
||||||
|
|
||||||
const ClusterController = memo(() => {
|
const ClusterController = memo(() => {
|
||||||
const [deposits, setDeposits] = useState([])
|
const [deposits, setDeposits] = useState([])
|
||||||
const [clusters, setClusters] = useState([])
|
const [clusters, setClusters] = useState([])
|
||||||
@ -41,8 +39,8 @@ const ClusterController = memo(() => {
|
|||||||
sorter: makeStringSorter('caption'),
|
sorter: makeStringSorter('caption'),
|
||||||
formItemRules: min1,
|
formItemRules: min1,
|
||||||
}),
|
}),
|
||||||
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFixed }),
|
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFormat }),
|
||||||
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFixed }),
|
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFormat }),
|
||||||
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
|
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
|
||||||
], [deposits])
|
], [deposits])
|
||||||
|
|
||||||
@ -108,6 +106,7 @@ const ClusterController = memo(() => {
|
|||||||
onRowEdit={tableHandlers.edit}
|
onRowEdit={tableHandlers.edit}
|
||||||
onRowDelete={tableHandlers.delete}
|
onRowDelete={tableHandlers.delete}
|
||||||
tableName={'admin_cluster_controller'}
|
tableName={'admin_cluster_controller'}
|
||||||
|
scroll={{ x: true }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -97,6 +97,7 @@ const CompanyController = memo(() => {
|
|||||||
onRowEdit={tableHandlers.edit}
|
onRowEdit={tableHandlers.edit}
|
||||||
onRowDelete={tableHandlers.delete}
|
onRowDelete={tableHandlers.delete}
|
||||||
tableName={'admin_company_controller'}
|
tableName={'admin_company_controller'}
|
||||||
|
scroll={{ x: true }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -75,6 +75,7 @@ const CompanyTypeController = memo(() => {
|
|||||||
onRowEdit={tableHandlers.edit}
|
onRowEdit={tableHandlers.edit}
|
||||||
onRowDelete={tableHandlers.delete}
|
onRowDelete={tableHandlers.delete}
|
||||||
tableName={'admin_company_type_controller'}
|
tableName={'admin_company_type_controller'}
|
||||||
|
scroll={{ x: true }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -3,16 +3,14 @@ import { Input } from 'antd'
|
|||||||
|
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { EditableTable, makeColumn, defaultPagination, makeTimezoneColumn } from '@components/Table'
|
import { EditableTable, makeColumn, defaultPagination, makeTimezoneColumn } from '@components/Table'
|
||||||
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
|
import { arrayOrDefault, coordsFormat, wrapPrivateComponent } from '@utils'
|
||||||
import { min1 } from '@utils/validationRules'
|
import { min1 } from '@utils/validationRules'
|
||||||
import { AdminDepositService } from '@api'
|
import { AdminDepositService } from '@api'
|
||||||
|
|
||||||
export const coordsFixed = (coords) => coords && isFinite(coords) ? (+coords).toPrecision(10) : '-'
|
|
||||||
|
|
||||||
const depositColumns = [
|
const depositColumns = [
|
||||||
makeColumn('Название', 'caption', { width: 200, editable: true, formItemRules: min1 }),
|
makeColumn('Название', 'caption', { width: 200, editable: true, formItemRules: min1 }),
|
||||||
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFixed }),
|
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFormat }),
|
||||||
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFixed }),
|
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFormat }),
|
||||||
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
|
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -77,6 +75,7 @@ const DepositController = memo(() => {
|
|||||||
onRowEdit={tableHandlers.edit}
|
onRowEdit={tableHandlers.edit}
|
||||||
onRowDelete={tableHandlers.delete}
|
onRowDelete={tableHandlers.delete}
|
||||||
tableName={'admin_deposit_controller'}
|
tableName={'admin_deposit_controller'}
|
||||||
|
scroll={{ x: true }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -82,6 +82,7 @@ const PermissionController = memo(() => {
|
|||||||
onRowEdit={tableHandlers.edit}
|
onRowEdit={tableHandlers.edit}
|
||||||
onRowDelete={tableHandlers.delete}
|
onRowDelete={tableHandlers.delete}
|
||||||
tableName={'admin_permission_controller'}
|
tableName={'admin_permission_controller'}
|
||||||
|
scroll={{ x: true }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -89,6 +89,7 @@ const RoleController = memo(() => {
|
|||||||
onRowEdit={tableHandlers.edit}
|
onRowEdit={tableHandlers.edit}
|
||||||
onRowDelete={tableHandlers.delete}
|
onRowDelete={tableHandlers.delete}
|
||||||
tableName={'admin_role_controller'}
|
tableName={'admin_role_controller'}
|
||||||
|
scroll={{ x: true }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -6,7 +6,7 @@ import { Button, Input } from 'antd'
|
|||||||
import {
|
import {
|
||||||
defaultPagination,
|
defaultPagination,
|
||||||
makeColumn,
|
makeColumn,
|
||||||
makeDateSorter,
|
makeDateColumn,
|
||||||
makeNumericColumn,
|
makeNumericColumn,
|
||||||
makeNumericRender,
|
makeNumericRender,
|
||||||
makeTextColumn,
|
makeTextColumn,
|
||||||
@ -53,7 +53,7 @@ const TelemetryController = memo(() => {
|
|||||||
makeNumericColumn('ID', 'id', null, null, makeNumericRender(0)),
|
makeNumericColumn('ID', 'id', null, null, makeNumericRender(0)),
|
||||||
makeTextColumn('UID', 'remoteUid'),
|
makeTextColumn('UID', 'remoteUid'),
|
||||||
makeTextColumn('Назначена на скважину', 'realWell'),
|
makeTextColumn('Назначена на скважину', 'realWell'),
|
||||||
makeTextColumn('Дата начала бурения', 'drillingStartDate', null, makeDateSorter('drillingStartDate')),
|
makeDateColumn('Дата начала бурения', 'drillingStartDate'),
|
||||||
makeTextColumn('Часовой пояс', 'timeZoneId'),
|
makeTextColumn('Часовой пояс', 'timeZoneId'),
|
||||||
makeTextColumn('Скважина', 'well'),
|
makeTextColumn('Скважина', 'well'),
|
||||||
makeTextColumn('Куст', 'cluster'),
|
makeTextColumn('Куст', 'cluster'),
|
||||||
@ -115,6 +115,7 @@ const TelemetryController = memo(() => {
|
|||||||
pagination={defaultPagination}
|
pagination={defaultPagination}
|
||||||
dataSource={filteredTelemetryData}
|
dataSource={filteredTelemetryData}
|
||||||
tableName={'admin_telemetry_controller'}
|
tableName={'admin_telemetry_controller'}
|
||||||
|
scroll={{ x: true }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
import { Layout } from 'antd'
|
|
||||||
import { memo, useMemo } from 'react'
|
import { memo, useMemo } from 'react'
|
||||||
import { Navigate, Route, Routes } from 'react-router-dom'
|
import { Outlet } from 'react-router-dom'
|
||||||
|
|
||||||
import { RootPathContext, useRootPath } from '@asb/context'
|
import { RootPathContext, useRootPath } from '@asb/context'
|
||||||
import { PrivateMenu } from '@components/Private'
|
import { wrapPrivateComponent } from '@utils'
|
||||||
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
|
|
||||||
|
|
||||||
import TelemetryViewer from './TelemetryViewer'
|
|
||||||
import TelemetryMerger from './TelemetryMerger'
|
|
||||||
|
|
||||||
const Telemetry = memo(() => {
|
const Telemetry = memo(() => {
|
||||||
const root = useRootPath()
|
const root = useRootPath()
|
||||||
@ -15,23 +10,7 @@ const Telemetry = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RootPathContext.Provider value={rootPath}>
|
<RootPathContext.Provider value={rootPath}>
|
||||||
<Layout>
|
<Outlet />
|
||||||
<PrivateMenu>
|
|
||||||
<PrivateMenu.Link content={TelemetryViewer} />
|
|
||||||
<PrivateMenu.Link content={TelemetryMerger} />
|
|
||||||
</PrivateMenu>
|
|
||||||
|
|
||||||
<Layout>
|
|
||||||
<Layout.Content className={'site-layout-background'}>
|
|
||||||
<Routes>
|
|
||||||
<Route index element={<Navigate to={TelemetryViewer.route} replace />} />
|
|
||||||
<Route path={'*'} element={<NoAccessComponent />} />
|
|
||||||
<Route path={TelemetryViewer.route} element={<TelemetryViewer />} />
|
|
||||||
<Route path={TelemetryMerger.route} element={<TelemetryMerger />} />
|
|
||||||
</Routes>
|
|
||||||
</Layout.Content>
|
|
||||||
</Layout>
|
|
||||||
</Layout>
|
|
||||||
</RootPathContext.Provider>
|
</RootPathContext.Provider>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -214,6 +214,7 @@ const UserController = memo(() => {
|
|||||||
buttonsWidth={120}
|
buttonsWidth={120}
|
||||||
pagination={defaultPagination}
|
pagination={defaultPagination}
|
||||||
tableName={'admin_user_controller'}
|
tableName={'admin_user_controller'}
|
||||||
|
scroll={{ x: true }}
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
<ChangePassword
|
<ChangePassword
|
||||||
|
@ -59,6 +59,7 @@ const VisitLog = memo(() => {
|
|||||||
dataSource={filteredLogData}
|
dataSource={filteredLogData}
|
||||||
pagination={defaultPagination}
|
pagination={defaultPagination}
|
||||||
tableName={'visit_log'}
|
tableName={'visit_log'}
|
||||||
|
scroll={{ x: true }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -21,9 +21,7 @@ import {
|
|||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { TelemetryView, CompanyView } from '@components/views'
|
import { TelemetryView, CompanyView } from '@components/views'
|
||||||
import TelemetrySelect from '@components/selectors/TelemetrySelect'
|
import TelemetrySelect from '@components/selectors/TelemetrySelect'
|
||||||
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
|
import { arrayOrDefault, coordsFormat, wrapPrivateComponent } from '@utils'
|
||||||
|
|
||||||
import { coordsFixed } from '../DepositController'
|
|
||||||
|
|
||||||
const wellTypes = [
|
const wellTypes = [
|
||||||
{ value: 1, label: 'Наклонно-направленная' },
|
{ value: 1, label: 'Наклонно-направленная' },
|
||||||
@ -98,8 +96,8 @@ const WellController = memo(() => {
|
|||||||
editable: true,
|
editable: true,
|
||||||
sorter: makeNumericSorter('idWellType'),
|
sorter: makeNumericSorter('idWellType'),
|
||||||
}),
|
}),
|
||||||
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFixed }),
|
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFormat }),
|
||||||
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFixed }),
|
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFormat }),
|
||||||
makeColumn('Телеметрия', 'telemetry', {
|
makeColumn('Телеметрия', 'telemetry', {
|
||||||
editable: true,
|
editable: true,
|
||||||
render: (telemetry) => <TelemetryView telemetry={telemetry} />,
|
render: (telemetry) => <TelemetryView telemetry={telemetry} />,
|
||||||
|
@ -1,62 +1,57 @@
|
|||||||
import { Navigate, Route, Routes } from 'react-router-dom'
|
import { Navigate, Route, Routes } from 'react-router-dom'
|
||||||
import { memo, useMemo } from 'react'
|
import { lazy, memo, useMemo } from 'react'
|
||||||
import { Layout } from 'antd'
|
|
||||||
|
|
||||||
import { RootPathContext, useRootPath } from '@asb/context'
|
import { RootPathContext, useLayoutProps, useRootPath } from '@asb/context'
|
||||||
import { AdminLayoutPortal } from '@components/Layout'
|
|
||||||
import { PrivateMenu } from '@components/Private'
|
|
||||||
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
|
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
|
||||||
|
|
||||||
import ClusterController from './ClusterController'
|
import AdminNavigationMenu from './AdminNavigationMenu'
|
||||||
import CompanyController from './CompanyController'
|
|
||||||
import DepositController from './DepositController'
|
const ClusterController = lazy(() => import('./ClusterController'))
|
||||||
import UserController from './UserController'
|
const CompanyController = lazy(() => import('./CompanyController'))
|
||||||
import WellController from './WellController'
|
const DepositController = lazy(() => import('./DepositController'))
|
||||||
import RoleController from './RoleController'
|
const UserController = lazy(() => import('./UserController'))
|
||||||
import CompanyTypeController from './CompanyTypeController'
|
const WellController = lazy(() => import('./WellController'))
|
||||||
import PermissionController from './PermissionController'
|
const RoleController = lazy(() => import('./RoleController'))
|
||||||
import Telemetry from './Telemetry'
|
const CompanyTypeController = lazy(() => import('./CompanyTypeController'))
|
||||||
import VisitLog from './VisitLog'
|
const PermissionController = lazy(() => import('./PermissionController'))
|
||||||
|
const Telemetry = lazy(() => import('./Telemetry'))
|
||||||
|
const TelemetryViewer = lazy(() => import('./Telemetry/TelemetryViewer'))
|
||||||
|
const TelemetryMerger = lazy(() => import('./Telemetry/TelemetryMerger'))
|
||||||
|
const VisitLog = lazy(() => import('./VisitLog'))
|
||||||
|
|
||||||
|
const layoutProps = {
|
||||||
|
sider: <AdminNavigationMenu />,
|
||||||
|
title: 'Администраторская панель',
|
||||||
|
isAdmin: true,
|
||||||
|
}
|
||||||
|
|
||||||
const AdminPanel = memo(() => {
|
const AdminPanel = memo(() => {
|
||||||
const root = useRootPath()
|
const root = useRootPath()
|
||||||
const rootPath = useMemo(() => `${root}/admin`, [root])
|
const rootPath = useMemo(() => `${root}/admin`, [root])
|
||||||
|
|
||||||
|
useLayoutProps(layoutProps)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RootPathContext.Provider value={rootPath}>
|
<RootPathContext.Provider value={rootPath}>
|
||||||
<AdminLayoutPortal title={'Администраторская панель'}>
|
<Routes>
|
||||||
<PrivateMenu>
|
<Route index element={<Navigate to={'visit_log'} replace />} />
|
||||||
<PrivateMenu.Link content={DepositController} />
|
<Route path={'*'} element={<NoAccessComponent />} />
|
||||||
<PrivateMenu.Link content={ClusterController} />
|
<Route path={'deposit'} element={<DepositController />} />
|
||||||
<PrivateMenu.Link content={WellController} />
|
<Route path={'cluster'} element={<ClusterController />} />
|
||||||
<PrivateMenu.Link content={UserController} />
|
<Route path={'well'} element={<WellController />} />
|
||||||
<PrivateMenu.Link content={CompanyController} />
|
<Route path={'user'} element={<UserController />} />
|
||||||
<PrivateMenu.Link content={CompanyTypeController} />
|
<Route path={'company'} element={<CompanyController />} />
|
||||||
<PrivateMenu.Link content={RoleController} />
|
<Route path={'company_type'} element={<CompanyTypeController />} />
|
||||||
<PrivateMenu.Link content={PermissionController} />
|
<Route path={'role'} element={<RoleController />} />
|
||||||
<PrivateMenu.Link content={Telemetry} />
|
<Route path={'permission'} element={<PermissionController />} />
|
||||||
<PrivateMenu.Link content={VisitLog} />
|
<Route path={'telemetry'} element={<Telemetry />}>
|
||||||
</PrivateMenu>
|
<Route index element={<Navigate to={'viewer'} replace />} />
|
||||||
|
<Route path={'*'} element={<NoAccessComponent />} />
|
||||||
<Layout>
|
<Route path={'viewer'} element={<TelemetryViewer />} />
|
||||||
<Layout.Content className={'site-layout-background'}>
|
<Route path={'merger'} element={<TelemetryMerger />} />
|
||||||
<Routes>
|
</Route>
|
||||||
<Route index element={<Navigate to={VisitLog.route} replace />} />
|
<Route path={'visit_log'} element={<VisitLog />} />
|
||||||
<Route path={'*'} element={<NoAccessComponent />} />
|
</Routes>
|
||||||
<Route path={DepositController.route} element={<DepositController />} />
|
|
||||||
<Route path={ClusterController.route} element={<ClusterController />} />
|
|
||||||
<Route path={WellController.route} element={<WellController />} />
|
|
||||||
<Route path={UserController.route} element={<UserController />} />
|
|
||||||
<Route path={CompanyController.route} element={<CompanyController />} />
|
|
||||||
<Route path={CompanyTypeController.route} element={<CompanyTypeController />} />
|
|
||||||
<Route path={RoleController.route} element={<RoleController />} />
|
|
||||||
<Route path={PermissionController.route} element={<PermissionController />} />
|
|
||||||
<Route path={Telemetry.route} element={<Telemetry />} />
|
|
||||||
<Route path={VisitLog.route} element={<VisitLog />} />
|
|
||||||
</Routes>
|
|
||||||
</Layout.Content>
|
|
||||||
</Layout>
|
|
||||||
</AdminLayoutPortal>
|
|
||||||
</RootPathContext.Provider>
|
</RootPathContext.Provider>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
import { memo, useMemo } from 'react'
|
|
||||||
import { Navigate, Route, Routes } from 'react-router-dom'
|
|
||||||
import { Layout } from 'antd'
|
|
||||||
|
|
||||||
import { RootPathContext, useRootPath } from '@asb/context'
|
|
||||||
import { PrivateMenu } from '@components/Private'
|
|
||||||
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
|
|
||||||
|
|
||||||
import Statistics from './Statistics'
|
|
||||||
import WellCompositeEditor from './WellCompositeEditor'
|
|
||||||
|
|
||||||
const Analytics = memo(() => {
|
|
||||||
const root = useRootPath()
|
|
||||||
const rootPath = useMemo(() => `${root}/analytics`, [root])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RootPathContext.Provider value={rootPath}>
|
|
||||||
<Layout>
|
|
||||||
<PrivateMenu className={'well_menu'}>
|
|
||||||
<PrivateMenu.Link content={WellCompositeEditor} />
|
|
||||||
<PrivateMenu.Link key={'statistics'} title={'Оценка по ЦБ'} content={Statistics} />
|
|
||||||
</PrivateMenu>
|
|
||||||
<Layout>
|
|
||||||
<Layout.Content className={'site-layout-background'}>
|
|
||||||
<Routes>
|
|
||||||
<Route index element={<Navigate to={WellCompositeEditor.getKey()} replace />} />
|
|
||||||
<Route path={'*'} element={<NoAccessComponent />} />
|
|
||||||
|
|
||||||
<Route path={WellCompositeEditor.route} element={<WellCompositeEditor />} />
|
|
||||||
<Route path={Statistics.route} element={<Statistics />} />
|
|
||||||
</Routes>
|
|
||||||
</Layout.Content>
|
|
||||||
</Layout>
|
|
||||||
</Layout>
|
|
||||||
</RootPathContext.Provider>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default wrapPrivateComponent(Analytics, {
|
|
||||||
requirements: [],
|
|
||||||
title: 'Аналитика',
|
|
||||||
route: 'analytics/*',
|
|
||||||
key: 'analytics',
|
|
||||||
})
|
|
@ -1,6 +1,6 @@
|
|||||||
import { Link, useLocation } from 'react-router-dom'
|
import { Link, useLocation } from 'react-router-dom'
|
||||||
import { useState, useEffect, memo, useMemo } from 'react'
|
import { useState, useEffect, memo, useMemo, lazy, Suspense } from 'react'
|
||||||
import { Tag, Button, Modal } from 'antd'
|
import { Button, Modal } from 'antd'
|
||||||
import { LineChartOutlined, ProfileOutlined, TeamOutlined } from '@ant-design/icons'
|
import { LineChartOutlined, ProfileOutlined, TeamOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -13,9 +13,9 @@ import {
|
|||||||
makeNumericRender,
|
makeNumericRender,
|
||||||
makeNumericColumn,
|
makeNumericColumn,
|
||||||
} from '@components/Table'
|
} from '@components/Table'
|
||||||
import { CompanyView } from '@components/views'
|
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
import PointerIcon from '@components/icons/PointerIcon'
|
import PointerIcon from '@components/icons/PointerIcon'
|
||||||
|
import SuspenseFallback from '@components/SuspenseFallback'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import {
|
import {
|
||||||
getOperations,
|
getOperations,
|
||||||
@ -25,9 +25,10 @@ import {
|
|||||||
wrapPrivateComponent
|
wrapPrivateComponent
|
||||||
} from '@utils'
|
} from '@utils'
|
||||||
|
|
||||||
import Tvd from '@pages/WellOperations/Tvd'
|
const Tvd = lazy(() => import('@pages/Well/WellOperations/Tvd'))
|
||||||
|
|
||||||
|
import CompaniesTable from './CompaniesTable'
|
||||||
import WellOperationsTable from './WellOperationsTable'
|
import WellOperationsTable from './WellOperationsTable'
|
||||||
import CompaniesTable from '@pages/Cluster/CompaniesTable'
|
|
||||||
|
|
||||||
const filtersMinMax = [
|
const filtersMinMax = [
|
||||||
{ text: 'min', value: 'min' },
|
{ text: 'min', value: 'min' },
|
||||||
@ -173,23 +174,26 @@ const ClusterWells = memo(({ statsWells }) => {
|
|||||||
pagination={false}
|
pagination={false}
|
||||||
rowKey={(record) => record.caption}
|
rowKey={(record) => record.caption}
|
||||||
tableName={'cluster'}
|
tableName={'cluster'}
|
||||||
|
scroll={{ x: true }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={'TVD'}
|
title={'TVD'}
|
||||||
centered
|
centered
|
||||||
visible={isTVDModalVisible}
|
open={isTVDModalVisible}
|
||||||
onCancel={() => setIsTVDModalVisible(false)}
|
onCancel={() => setIsTVDModalVisible(false)}
|
||||||
width={1500}
|
width={1500}
|
||||||
footer={null}
|
footer={null}
|
||||||
>
|
>
|
||||||
<Tvd style={{ minHeight: '600px' }} well={selectedWell} />
|
<Suspense fallback={<SuspenseFallback style={{ minHeight: '100%' }} />}>
|
||||||
|
<Tvd style={{ minHeight: '600px' }} well={selectedWell} />
|
||||||
|
</Suspense>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={'Операции'}
|
title={'Операции'}
|
||||||
centered
|
centered
|
||||||
visible={isOpsModalVisible}
|
open={isOpsModalVisible}
|
||||||
onCancel={() => setIsOpsModalVisible(false)}
|
onCancel={() => setIsOpsModalVisible(false)}
|
||||||
width={1500}
|
width={1500}
|
||||||
footer={null}
|
footer={null}
|
||||||
@ -202,7 +206,7 @@ const ClusterWells = memo(({ statsWells }) => {
|
|||||||
<Modal
|
<Modal
|
||||||
title={'Участники'}
|
title={'Участники'}
|
||||||
centered
|
centered
|
||||||
visible={isCompaniesModalVisible}
|
open={isCompaniesModalVisible}
|
||||||
onCancel={() => setIsCompaniesModalVisible(false)}
|
onCancel={() => setIsCompaniesModalVisible(false)}
|
||||||
width={1500}
|
width={1500}
|
||||||
footer={null}
|
footer={null}
|
||||||
|
@ -1,18 +1,22 @@
|
|||||||
import React, { memo, useMemo } from 'react'
|
import { memo, useMemo } from 'react'
|
||||||
import { BankOutlined } from '@ant-design/icons'
|
import { BankOutlined } from '@ant-design/icons'
|
||||||
|
|
||||||
import { makeTextColumn, Table } from '@components/Table'
|
import { makeColumn, makeTextColumn, Table } from '@components/Table'
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
makeTextColumn('', 'logo'),
|
makeColumn('', 'logo'),
|
||||||
makeTextColumn('Название компании', 'caption'),
|
makeTextColumn('Название компании', 'caption'),
|
||||||
makeTextColumn('Тип компании', 'companyTypeCaption'),
|
makeTextColumn('Тип компании', 'companyTypeCaption'),
|
||||||
]
|
]
|
||||||
|
|
||||||
const CompaniesTable = memo(({companies}) => {
|
const CompaniesTable = memo(({ companies }) => {
|
||||||
const dataCompanies = useMemo(() => companies?.map((company) => ({
|
const dataCompanies = useMemo(() => companies?.map((company) => ({
|
||||||
key: company.id,
|
key: company.id,
|
||||||
logo: company?.logo ? <img src={company.logo}/> : <BankOutlined/>,
|
logo: (
|
||||||
|
<div className={'centered'}>
|
||||||
|
{company?.logo ? <img src={company.logo}/> : <BankOutlined/>}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
caption: company.caption,
|
caption: company.caption,
|
||||||
companyTypeCaption: company.companyTypeCaption,
|
companyTypeCaption: company.companyTypeCaption,
|
||||||
})), [companies])
|
})), [companies])
|
||||||
@ -30,4 +34,4 @@ const CompaniesTable = memo(({companies}) => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default CompaniesTable
|
export default CompaniesTable
|
||||||
|
@ -7,8 +7,8 @@ import { getPrecision } from '@utils/functions'
|
|||||||
const columns = [
|
const columns = [
|
||||||
makeTextColumn('Конструкция секции', 'sectionType'),
|
makeTextColumn('Конструкция секции', 'sectionType'),
|
||||||
makeTextColumn('Операция', 'operationName'),
|
makeTextColumn('Операция', 'operationName'),
|
||||||
makeNumericColumnPlanFact('Глубина забоя', 'depth', null, null, getPrecision),
|
makeNumericColumnPlanFact('Глубина забоя', 'depth', null, null, (number) => getPrecision(number)),
|
||||||
makeNumericColumnPlanFact('Часы', 'durationHours', null, null, getPrecision),
|
makeNumericColumnPlanFact('Часы', 'durationHours', null, null, (number) => getPrecision(number)),
|
||||||
makeNumericColumnPlanFact('Комментарий', 'comment', null, null, (text) => text ?? '-')
|
makeNumericColumnPlanFact('Комментарий', 'comment', null, null, (text) => text ?? '-')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1,19 +1,25 @@
|
|||||||
import { useState, useEffect, memo } from 'react'
|
import { useState, useEffect, memo } from 'react'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
|
||||||
import { LayoutPortal } from '@components/Layout'
|
import { useLayoutProps } from '@asb/context'
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
|
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
|
||||||
import { OperationStatService } from '@api'
|
import { OperationStatService } from '@api'
|
||||||
|
|
||||||
import ClusterWells from './ClusterWells'
|
import ClusterWells from './ClusterWells'
|
||||||
import { useParams } from 'react-router-dom'
|
|
||||||
|
const layoutProps = {
|
||||||
|
title: 'Анализ скважин куста'
|
||||||
|
}
|
||||||
|
|
||||||
const Cluster = memo(() => {
|
const Cluster = memo(() => {
|
||||||
const { idCluster } = useParams()
|
const { idCluster } = useParams()
|
||||||
const [data, setData] = useState([])
|
const [data, setData] = useState([])
|
||||||
const [showLoader, setShowLoader] = useState(false)
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
|
|
||||||
|
useLayoutProps(layoutProps)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
invokeWebApiWrapperAsync(
|
invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
@ -27,11 +33,9 @@ const Cluster = memo(() => {
|
|||||||
}, [idCluster])
|
}, [idCluster])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutPortal title={'Анализ скважин куста'}>
|
<LoaderPortal show={showLoader}>
|
||||||
<LoaderPortal show={showLoader}>
|
<ClusterWells statsWells={data} />
|
||||||
<ClusterWells statsWells={data} />
|
</LoaderPortal>
|
||||||
</LoaderPortal>
|
|
||||||
</LayoutPortal>
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { Map, Overlay } from 'pigeon-maps'
|
import { Map, Overlay } from 'pigeon-maps'
|
||||||
import { useState, useEffect, memo } from 'react'
|
import { useState, useEffect, memo, useMemo } from 'react'
|
||||||
import { Link, useLocation } from 'react-router-dom'
|
import { Link, useLocation } from 'react-router-dom'
|
||||||
import { Popover, Badge } from 'antd'
|
import { Popover, Badge } from 'antd'
|
||||||
|
|
||||||
|
import { useLayoutProps } from '@asb/context'
|
||||||
import { PointerIcon } from '@components/icons'
|
import { PointerIcon } from '@components/icons'
|
||||||
import { LayoutPortal } from '@components/Layout'
|
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { arrayOrDefault, limitValue, wrapPrivateComponent } from '@utils'
|
import { arrayOrDefault, limitValue, wrapPrivateComponent } from '@utils'
|
||||||
@ -47,8 +47,26 @@ const Deposit = memo(() => {
|
|||||||
const [showLoader, setShowLoader] = useState(false)
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
const [viewParams, setViewParams] = useState(defaultViewParams)
|
const [viewParams, setViewParams] = useState(defaultViewParams)
|
||||||
|
|
||||||
|
const setLayoutProps = useLayoutProps()
|
||||||
|
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
||||||
|
const selectorProps = useMemo(() => {
|
||||||
|
const hasId = location.pathname.length > '/deposit/'.length
|
||||||
|
|
||||||
|
return {
|
||||||
|
expand: hasId ? [location.pathname] : true,
|
||||||
|
current: hasId ? location.pathname : undefined,
|
||||||
|
}
|
||||||
|
}, [location.pathname])
|
||||||
|
|
||||||
|
useEffect(() => setLayoutProps({
|
||||||
|
sheet: false,
|
||||||
|
showSelector: true,
|
||||||
|
selectorProps,
|
||||||
|
title: 'Месторождение',
|
||||||
|
}), [setLayoutProps, selectorProps])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
invokeWebApiWrapperAsync(
|
invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
@ -63,37 +81,35 @@ const Deposit = memo(() => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutPortal noSheet showSelector title={'Месторождение'}>
|
<LoaderPortal show={showLoader}>
|
||||||
<LoaderPortal show={showLoader}>
|
<div className={'h-100vh'} style={{ overflow: 'hidden' }}>
|
||||||
<div className={'h-100vh'}>
|
<Map {...viewParams}>
|
||||||
<Map {...viewParams}>
|
{depositsData.map(deposit => (
|
||||||
{depositsData.map(deposit => (
|
<Overlay
|
||||||
<Overlay
|
width={32}
|
||||||
width={32}
|
anchor={[deposit.latitude, deposit.longitude]}
|
||||||
anchor={[deposit.latitude, deposit.longitude]}
|
key={`${deposit.latitude} ${deposit.longitude}`}
|
||||||
key={`${deposit.latitude} ${deposit.longitude}`}
|
>
|
||||||
>
|
<Popover content={
|
||||||
<Popover content={
|
<div>
|
||||||
<div>
|
{deposit.clusters.map(cluster => (
|
||||||
{deposit.clusters.map(cluster => (
|
<Link key={cluster.id} to={{ pathname: `/cluster/${cluster.id}`, state: { from: location.pathname }}}>
|
||||||
<Link key={cluster.id} to={{ pathname: `/cluster/${cluster.id}`, state: { from: location.pathname }}}>
|
<div>{cluster.caption}</div>
|
||||||
<div>{cluster.caption}</div>
|
</Link>
|
||||||
</Link>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
} trigger={['click']} title={deposit.caption}>
|
||||||
} trigger={['click']} title={deposit.caption}>
|
<div style={{cursor: 'pointer'}}>
|
||||||
<div style={{cursor: 'pointer'}}>
|
<Badge count={deposit.clusters.length}>
|
||||||
<Badge count={deposit.clusters.length}>
|
<PointerIcon state={'active'} width={48} height={59} />
|
||||||
<PointerIcon state={'active'} width={48} height={59} />
|
</Badge>
|
||||||
</Badge>
|
</div>
|
||||||
</div>
|
</Popover>
|
||||||
</Popover>
|
</Overlay>
|
||||||
</Overlay>
|
))}
|
||||||
))}
|
</Map>
|
||||||
</Map>
|
</div>
|
||||||
</div>
|
</LoaderPortal>
|
||||||
</LoaderPortal>
|
|
||||||
</LayoutPortal>
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -7,8 +7,6 @@ import { downloadFile, invokeWebApiWrapperAsync } from '@components/factory'
|
|||||||
import { wrapPrivateComponent } from '@utils'
|
import { wrapPrivateComponent } from '@utils'
|
||||||
import { FileService, WellService } from '@api'
|
import { FileService, WellService } from '@api'
|
||||||
|
|
||||||
import AccessDenied from './AccessDenied'
|
|
||||||
|
|
||||||
const { Paragraph, Text } = Typography
|
const { Paragraph, Text } = Typography
|
||||||
|
|
||||||
export const getLinkToFile = (fileInfo) => `/file_download/${fileInfo.idWell}/${fileInfo.id}`
|
export const getLinkToFile = (fileInfo) => `/file_download/${fileInfo.idWell}/${fileInfo.id}`
|
||||||
@ -104,4 +102,4 @@ FileDownload.displayName = 'FileDownloadMemo'
|
|||||||
export default wrapPrivateComponent(FileDownload, {
|
export default wrapPrivateComponent(FileDownload, {
|
||||||
requirements: ['File.get'],
|
requirements: ['File.get'],
|
||||||
route: 'file_download/:idWell/:idFile/*',
|
route: 'file_download/:idWell/:idFile/*',
|
||||||
}, <AccessDenied />)
|
})
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
import { memo, Fragment } from 'react'
|
|
||||||
import { Empty, Form } from 'antd'
|
|
||||||
|
|
||||||
import { Grid, GridItem } from '@components/Grid'
|
|
||||||
|
|
||||||
import '@styles/index.css'
|
|
||||||
import '@styles/measure.css'
|
|
||||||
|
|
||||||
const colsCount = 3
|
|
||||||
|
|
||||||
export const View = memo(({ columns, item }) => !item || !columns?.length ? (
|
|
||||||
<Empty key={'empty'} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
||||||
) : (
|
|
||||||
<Grid>
|
|
||||||
{columns.map((column, i) => (
|
|
||||||
<Fragment key={i}>
|
|
||||||
<GridItem
|
|
||||||
key={column.dataIndex}
|
|
||||||
row={Math.floor(i / colsCount) + 1}
|
|
||||||
col={(i % colsCount) * 2 + 1}
|
|
||||||
className={'measure-column-header'}
|
|
||||||
>
|
|
||||||
{column.title}
|
|
||||||
</GridItem>
|
|
||||||
|
|
||||||
<GridItem
|
|
||||||
key={column.title}
|
|
||||||
row={Math.floor(i / colsCount) + 1}
|
|
||||||
col={(i % colsCount) * 2 + 2}
|
|
||||||
className={'measure-column-value'}
|
|
||||||
style={{ padding: 0 }}
|
|
||||||
>
|
|
||||||
{column.render ? (
|
|
||||||
<Form.Item
|
|
||||||
key={column.dataIndex}
|
|
||||||
name={column.dataIndex}
|
|
||||||
style={{ margin: 0 }}
|
|
||||||
>
|
|
||||||
{column.render(item[column.dataIndex])}
|
|
||||||
</Form.Item>
|
|
||||||
) : (
|
|
||||||
<p key={column.title} className={'m-5px'}>
|
|
||||||
{item[column.dataIndex]}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</GridItem>
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
))
|
|
@ -1,71 +0,0 @@
|
|||||||
import { makeColumn } from '@components/Table'
|
|
||||||
|
|
||||||
import { numericColumnOptions, textColumnOptions } from './columnsCommon'
|
|
||||||
|
|
||||||
export const columnsDrillingFluid = [
|
|
||||||
makeColumn('Наименование', 'name', textColumnOptions),
|
|
||||||
makeColumn('Температура, °C', 'temperature', numericColumnOptions),
|
|
||||||
makeColumn('Плотность, г/см³', 'density', numericColumnOptions),
|
|
||||||
makeColumn('Усл. вязкость, сек', 'conditionalViscosity', numericColumnOptions),
|
|
||||||
makeColumn('R300', 'r300', numericColumnOptions),
|
|
||||||
makeColumn('R600', 'r600', numericColumnOptions),
|
|
||||||
makeColumn('R3/R6', 'r3r6', numericColumnOptions),
|
|
||||||
makeColumn('ДНС, дПа', 'dnsDpa', numericColumnOptions),
|
|
||||||
makeColumn('Пластич. вязкость, сПз', 'plasticViscocity', numericColumnOptions),
|
|
||||||
makeColumn('СНС, дПа', 'snsDpa', numericColumnOptions),
|
|
||||||
makeColumn('R3/R6 49С', 'r3r649С', numericColumnOptions),
|
|
||||||
makeColumn('ДНС 49С, дПа', 'dns49Cdpa', numericColumnOptions),
|
|
||||||
makeColumn('Пластич. вязкость 49С, сПз', 'plasticViscocity49c', numericColumnOptions),
|
|
||||||
makeColumn('СНС 49С, дПа', 'sns49Cdpa', numericColumnOptions),
|
|
||||||
makeColumn('МВТ, кг/м³', 'mbt', numericColumnOptions),
|
|
||||||
makeColumn('Песок, %', 'sand', numericColumnOptions),
|
|
||||||
makeColumn('Фильтрация, см³/30мин', 'filtering', numericColumnOptions),
|
|
||||||
makeColumn('Корка, мм', 'crust', numericColumnOptions),
|
|
||||||
makeColumn('KTK', 'ktk', numericColumnOptions),
|
|
||||||
makeColumn('pH', 'ph', numericColumnOptions),
|
|
||||||
makeColumn('Жесткость, мг/л', 'hardness', numericColumnOptions),
|
|
||||||
makeColumn('Хлориды, мг/л', 'chlorides', numericColumnOptions),
|
|
||||||
makeColumn('PF', 'pf', numericColumnOptions),
|
|
||||||
makeColumn('Mf', 'mf', numericColumnOptions),
|
|
||||||
makeColumn('Pm', 'pm', numericColumnOptions),
|
|
||||||
makeColumn('Твердая фаза раствора, %', 'fluidSolidPhase', numericColumnOptions),
|
|
||||||
makeColumn('Смазка, %', 'grease', numericColumnOptions),
|
|
||||||
makeColumn('Карбонат кальция, кг/м³', 'calciumCarbonate', numericColumnOptions),
|
|
||||||
]
|
|
||||||
|
|
||||||
export const drillingFluidDefaultData = {
|
|
||||||
idWell: 0,
|
|
||||||
key: 'drillingFluidDefaultData',
|
|
||||||
idCategory: 0,
|
|
||||||
isDefaultData: true,
|
|
||||||
data: {
|
|
||||||
name: 0,
|
|
||||||
temperature: 0,
|
|
||||||
density: 0,
|
|
||||||
conditionalViscosity: 0,
|
|
||||||
r300: 0,
|
|
||||||
r600: 0,
|
|
||||||
r3r6: 0,
|
|
||||||
dnsDpa: 0,
|
|
||||||
plasticViscocity: 0,
|
|
||||||
snsDpa: 0,
|
|
||||||
r3r649С: 0,
|
|
||||||
dns49Cdpa: 0,
|
|
||||||
plasticViscocity49c: 0,
|
|
||||||
sns49Cdpa: 0,
|
|
||||||
mbt: 0,
|
|
||||||
sand: 0,
|
|
||||||
filtering: 0,
|
|
||||||
crust: 0,
|
|
||||||
ktk: 0,
|
|
||||||
ph: 0,
|
|
||||||
hardness: 0,
|
|
||||||
chlorides: 0,
|
|
||||||
pf: 0,
|
|
||||||
mf: 0,
|
|
||||||
pm: 0,
|
|
||||||
fluidSolidPhase: 0,
|
|
||||||
grease: 0,
|
|
||||||
calciumCarbonate: 0
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
import { makeColumn } from '@components/Table'
|
|
||||||
|
|
||||||
import { numericColumnOptions, textColumnOptions } from './columnsCommon'
|
|
||||||
|
|
||||||
export const columnsMudDiagram = [
|
|
||||||
makeColumn('N пробы', 'probeNumber', numericColumnOptions),
|
|
||||||
makeColumn('Глубина отбора пробы', 'probeExtractionDepth', numericColumnOptions),
|
|
||||||
makeColumn('Песчаник (%)', 'sandstone', numericColumnOptions),
|
|
||||||
makeColumn('Алевролит (%)', 'siltstone', numericColumnOptions),
|
|
||||||
makeColumn('Аргиллит (%)', 'argillit', numericColumnOptions),
|
|
||||||
makeColumn('Аргиллит бит. (%)', 'brokenArgillit', numericColumnOptions),
|
|
||||||
makeColumn('Уголь (%)', 'coal', numericColumnOptions),
|
|
||||||
makeColumn('Песок (%)', 'sand', numericColumnOptions),
|
|
||||||
makeColumn('Глина (%)', 'clay', numericColumnOptions),
|
|
||||||
makeColumn('Известняк (%)', 'camstone', numericColumnOptions),
|
|
||||||
makeColumn('Цемент (%)', 'cement', numericColumnOptions),
|
|
||||||
makeColumn('Краткое описание', 'summary', textColumnOptions),
|
|
||||||
makeColumn('ЛБА бурового раствора', 'drillingMud', numericColumnOptions),
|
|
||||||
makeColumn('ЛБА (шлама)', 'sludge', numericColumnOptions),
|
|
||||||
makeColumn('Сумма УВ мах. (абс%)', 'maxSum', numericColumnOptions),
|
|
||||||
makeColumn('С1 метан (отн%)', 'methane', numericColumnOptions),
|
|
||||||
makeColumn('С2 этан (отн%)', 'ethane', numericColumnOptions),
|
|
||||||
makeColumn('С3 пропан (отн%)', 'propane', numericColumnOptions),
|
|
||||||
makeColumn('С4 бутан (отн%)', 'butane', numericColumnOptions),
|
|
||||||
makeColumn('С5 пентан (отн%)', 'pentane', numericColumnOptions),
|
|
||||||
makeColumn('Мех. скорость', 'mechanicalSpeed', numericColumnOptions),
|
|
||||||
makeColumn('Предварительное заключение о насыщении по ГК', 'preliminaryConclusion', textColumnOptions),
|
|
||||||
]
|
|
||||||
|
|
||||||
export const mudDiagramDefaultData = {
|
|
||||||
idWell: 0,
|
|
||||||
key: 'mudDiagramDefaultData',
|
|
||||||
idCategory: 0,
|
|
||||||
isDefaultData: true,
|
|
||||||
data: {
|
|
||||||
probeNumber: 0,
|
|
||||||
probeExtractionDepth: 0,
|
|
||||||
sandstone: 0,
|
|
||||||
siltstone: 0,
|
|
||||||
argillit: 0,
|
|
||||||
brokenArgillit: 0,
|
|
||||||
coal: 0,
|
|
||||||
sand: 0,
|
|
||||||
clay: 0,
|
|
||||||
camstone: 0,
|
|
||||||
cement: 0,
|
|
||||||
summary: '-',
|
|
||||||
drillingMud: 0,
|
|
||||||
sludge: 0,
|
|
||||||
maxSum: 0,
|
|
||||||
methane: 0,
|
|
||||||
ethane: 0,
|
|
||||||
propane: 0,
|
|
||||||
butane: 0,
|
|
||||||
pentane: 0,
|
|
||||||
mechanicalSpeed: 0,
|
|
||||||
preliminaryConclusion: '-'
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
import { makeColumn } from '@components/Table'
|
|
||||||
import { numericColumnOptions, textColumnOptions } from './columnsCommon'
|
|
||||||
|
|
||||||
export const columnsNnb = [
|
|
||||||
makeColumn('Глубина по стволу, м', 'depth', textColumnOptions),
|
|
||||||
makeColumn('Зенитный угол, град', 'zenithAngle', numericColumnOptions),
|
|
||||||
makeColumn('Азимут магнитный, град', 'magneticAzimuth', numericColumnOptions),
|
|
||||||
makeColumn('Азимут истинный, град', 'trueAzimuth', numericColumnOptions),
|
|
||||||
makeColumn('Азимут дирекц., град', 'directAzimuth', numericColumnOptions),
|
|
||||||
makeColumn('Глубина по вертикали, м', 'verticalDepth', numericColumnOptions),
|
|
||||||
makeColumn('Абсолютная отметка, м', 'absoluteMark', numericColumnOptions),
|
|
||||||
makeColumn('Лок. смещение к северу, м', 'localNorthOffset', numericColumnOptions),
|
|
||||||
makeColumn('Лок. смещение к востоку, м', 'localEastOffset', numericColumnOptions),
|
|
||||||
makeColumn('Отклонение от устья, м', 'outFallOffset', numericColumnOptions),
|
|
||||||
makeColumn('Азимут смещения, град', 'offsetAzimuth', numericColumnOptions),
|
|
||||||
makeColumn('Пространст.\nинтенсивность, град/10 м', 'areaIntensity', numericColumnOptions),
|
|
||||||
makeColumn('Угол установки отклон., град', 'offsetStopAngle', numericColumnOptions),
|
|
||||||
makeColumn('Интенсив. по зениту, град/10 м', 'zenithIntensity', numericColumnOptions),
|
|
||||||
makeColumn('Комментарий', 'comment', numericColumnOptions),
|
|
||||||
makeColumn('Разница вертикальных глубин\nмежду планом и фактом', 'depthPlanFactDifference', numericColumnOptions),
|
|
||||||
makeColumn('Расстояние в пространстве\nмежду планом и фактом', 'distancePlanFactDifference', numericColumnOptions),
|
|
||||||
]
|
|
||||||
|
|
||||||
export const nnbDefaultData = {
|
|
||||||
idWell: 0,
|
|
||||||
key: 'nnbDefaultData',
|
|
||||||
idCategory: 0,
|
|
||||||
isDefaultData: true,
|
|
||||||
data: {
|
|
||||||
depth: 0,
|
|
||||||
zenithAngle: 0,
|
|
||||||
magneticAzimuth: 0,
|
|
||||||
trueAzimuth: 0,
|
|
||||||
directAzimuth: 0,
|
|
||||||
verticalDepth: 0,
|
|
||||||
absoluteMark: 0,
|
|
||||||
localNorthOffset: 0,
|
|
||||||
localEastOffset: 0,
|
|
||||||
outFallOffset: 0,
|
|
||||||
offsetAzimuth: 0,
|
|
||||||
areaIntensity: '-',
|
|
||||||
offsetStopAngle: 0,
|
|
||||||
zenithIntensity: 0,
|
|
||||||
comment: '-',
|
|
||||||
depthPlanFactDifference: 0,
|
|
||||||
distancePlanFactDifference: 0
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
import { Navigate, Route, Routes } from 'react-router-dom'
|
|
||||||
import { memo, useMemo } from 'react'
|
|
||||||
import { FilePdfOutlined } from '@ant-design/icons'
|
|
||||||
import { Layout } from 'antd'
|
|
||||||
|
|
||||||
import { RootPathContext, useRootPath } from '@asb/context'
|
|
||||||
import { PrivateMenu } from '@components/Private'
|
|
||||||
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
|
|
||||||
|
|
||||||
import DailyReport from './DailyReport'
|
|
||||||
import DiagramReport from './DiagramReport'
|
|
||||||
|
|
||||||
const { Content } = Layout
|
|
||||||
|
|
||||||
const Reports = memo(() => {
|
|
||||||
const root = useRootPath()
|
|
||||||
const rootPath = useMemo(() => `${root}/reports`, [root])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RootPathContext.Provider value={rootPath}>
|
|
||||||
<Layout>
|
|
||||||
<PrivateMenu className={'well_menu'}>
|
|
||||||
<PrivateMenu.Link content={DiagramReport} icon={<FilePdfOutlined />} />
|
|
||||||
<PrivateMenu.Link content={DailyReport} />
|
|
||||||
</PrivateMenu>
|
|
||||||
|
|
||||||
<Layout>
|
|
||||||
<Content className={'site-layout-background'}>
|
|
||||||
<Routes>
|
|
||||||
<Route index element={<Navigate to={'diagram_report'} replace />} />
|
|
||||||
<Route path={'*'} element={<NoAccessComponent />} />
|
|
||||||
|
|
||||||
<Route path={DiagramReport.route} element={<DiagramReport />} />
|
|
||||||
<Route path={DailyReport.route} element={<DailyReport />} />
|
|
||||||
</Routes>
|
|
||||||
</Content>
|
|
||||||
</Layout>
|
|
||||||
</Layout>
|
|
||||||
</RootPathContext.Provider>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default wrapPrivateComponent(Reports, {
|
|
||||||
requirements: [],
|
|
||||||
title: 'Рапорта',
|
|
||||||
route: 'reports/*',
|
|
||||||
key: 'reports',
|
|
||||||
})
|
|
@ -1,60 +0,0 @@
|
|||||||
import { Navigate, Route, Routes } from 'react-router-dom'
|
|
||||||
import { memo, useMemo } from 'react'
|
|
||||||
import { Layout } from 'antd'
|
|
||||||
import { AlertOutlined, FundViewOutlined, DatabaseOutlined } from '@ant-design/icons'
|
|
||||||
|
|
||||||
import { RootPathContext, useRootPath } from '@asb/context'
|
|
||||||
import { PrivateMenu } from '@components/Private'
|
|
||||||
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
|
|
||||||
|
|
||||||
import Archive from './Archive'
|
|
||||||
import Messages from './Messages'
|
|
||||||
import Operations from './Operations'
|
|
||||||
import DashboardNNB from './DashboardNNB'
|
|
||||||
import TelemetryView from './TelemetryView'
|
|
||||||
|
|
||||||
import '@styles/index.css'
|
|
||||||
|
|
||||||
const { Content } = Layout
|
|
||||||
|
|
||||||
const Telemetry = memo(() => {
|
|
||||||
const root = useRootPath()
|
|
||||||
const rootPath = useMemo(() => `${root}/telemetry`, [root])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RootPathContext.Provider value={rootPath}>
|
|
||||||
<Layout>
|
|
||||||
<PrivateMenu className={'well_menu'}>
|
|
||||||
<PrivateMenu.Link content={TelemetryView} icon={<FundViewOutlined />} />
|
|
||||||
<PrivateMenu.Link content={Messages} icon={<AlertOutlined/>} />
|
|
||||||
<PrivateMenu.Link content={Archive} icon={<DatabaseOutlined />} />
|
|
||||||
<PrivateMenu.Link content={DashboardNNB} />
|
|
||||||
<PrivateMenu.Link content={Operations} />
|
|
||||||
</PrivateMenu>
|
|
||||||
|
|
||||||
<Layout>
|
|
||||||
<Content className={'site-layout-background'}>
|
|
||||||
<Routes>
|
|
||||||
<Route index element={<Navigate to={TelemetryView.route} replace />} />
|
|
||||||
<Route path={'*'} element={<NoAccessComponent />} />
|
|
||||||
|
|
||||||
<Route path={TelemetryView.route} element={<TelemetryView />} />
|
|
||||||
<Route path={Messages.route} element={<Messages />} />
|
|
||||||
<Route path={Archive.route} element={<Archive />} />
|
|
||||||
<Route path={DashboardNNB.route} element={<DashboardNNB />} />
|
|
||||||
<Route path={Operations.route} element={<Operations />} />
|
|
||||||
</Routes>
|
|
||||||
</Content>
|
|
||||||
</Layout>
|
|
||||||
</Layout>
|
|
||||||
</RootPathContext.Provider>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default wrapPrivateComponent(Telemetry, {
|
|
||||||
requirements: [],
|
|
||||||
icon: <FundViewOutlined />,
|
|
||||||
title: 'Телеметрия',
|
|
||||||
route: 'telemetry/*',
|
|
||||||
key: 'telemetry',
|
|
||||||
})
|
|
@ -1,103 +0,0 @@
|
|||||||
import {
|
|
||||||
FolderOutlined,
|
|
||||||
FilePdfOutlined,
|
|
||||||
ExperimentOutlined,
|
|
||||||
DeploymentUnitOutlined,
|
|
||||||
} from '@ant-design/icons'
|
|
||||||
import { Layout } from 'antd'
|
|
||||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
|
||||||
import { Navigate, Route, Routes, useParams } from 'react-router-dom'
|
|
||||||
|
|
||||||
import { WellContext, RootPathContext, useRootPath } from '@asb/context'
|
|
||||||
import { LayoutPortal } from '@components/Layout'
|
|
||||||
import { PrivateMenu } from '@components/Private'
|
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
|
||||||
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
|
|
||||||
import { WellService } from '@api'
|
|
||||||
|
|
||||||
import Measure from './Measure'
|
|
||||||
import Reports from './Reports'
|
|
||||||
import Analytics from './Analytics'
|
|
||||||
import Documents from './Documents'
|
|
||||||
import Telemetry from './Telemetry'
|
|
||||||
import WellOperations from './WellOperations'
|
|
||||||
import DrillingProgram from './DrillingProgram'
|
|
||||||
|
|
||||||
import '@styles/index.css'
|
|
||||||
|
|
||||||
const { Content } = Layout
|
|
||||||
|
|
||||||
const Well = memo(() => {
|
|
||||||
const { idWell } = useParams()
|
|
||||||
|
|
||||||
const [well, setWell] = useState({ id: idWell })
|
|
||||||
|
|
||||||
const root = useRootPath()
|
|
||||||
const rootPath = useMemo(() => `${root}/well/${idWell}`, [root, idWell])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
invokeWebApiWrapperAsync(
|
|
||||||
async () => {
|
|
||||||
const well = await WellService.get(idWell)
|
|
||||||
setWell(well ?? { id: idWell })
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
'Не удалось получить данные по скважине'
|
|
||||||
)
|
|
||||||
}, [idWell])
|
|
||||||
|
|
||||||
const updateWell = useCallback((data) => invokeWebApiWrapperAsync(
|
|
||||||
async () => {
|
|
||||||
const newWell = { ...well, ...data }
|
|
||||||
await WellService.updateWell(newWell)
|
|
||||||
setWell(newWell)
|
|
||||||
},
|
|
||||||
undefined,
|
|
||||||
`Не удалось изменить данные скважины`,
|
|
||||||
{ actionName: 'Изменение данных скважины', well }
|
|
||||||
), [well])
|
|
||||||
|
|
||||||
const wellContext = useMemo(() => [well, updateWell], [well, updateWell])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LayoutPortal>
|
|
||||||
<RootPathContext.Provider value={rootPath}>
|
|
||||||
<PrivateMenu className={'well_menu'}>
|
|
||||||
<PrivateMenu.Link content={Telemetry} />
|
|
||||||
<PrivateMenu.Link content={Reports} icon={<FilePdfOutlined />} />
|
|
||||||
<PrivateMenu.Link content={Analytics} icon={<DeploymentUnitOutlined />} />
|
|
||||||
<PrivateMenu.Link content={WellOperations} icon={<FolderOutlined />} />
|
|
||||||
<PrivateMenu.Link content={Documents} icon={<FolderOutlined />} />
|
|
||||||
<PrivateMenu.Link content={Measure} icon={<ExperimentOutlined />} />
|
|
||||||
<PrivateMenu.Link content={DrillingProgram} icon={<FolderOutlined />} />
|
|
||||||
</PrivateMenu>
|
|
||||||
|
|
||||||
<WellContext.Provider value={wellContext}>
|
|
||||||
<Layout>
|
|
||||||
<Content className={'site-layout-background'}>
|
|
||||||
<Routes>
|
|
||||||
<Route index element={<Navigate to={Telemetry.getKey()} replace />} />
|
|
||||||
<Route path={'*'} element={<NoAccessComponent />} />
|
|
||||||
|
|
||||||
<Route path={Telemetry.route} element={<Telemetry />} />
|
|
||||||
<Route path={Reports.route} element={<Reports />} />
|
|
||||||
<Route path={Analytics.route} element={<Analytics />} />
|
|
||||||
<Route path={WellOperations.route} element={<WellOperations />} />
|
|
||||||
<Route path={Documents.route} element={<Documents />} />
|
|
||||||
<Route path={Measure.route} element={<Measure />} />
|
|
||||||
<Route path={DrillingProgram.route} element={<DrillingProgram />} />
|
|
||||||
</Routes>
|
|
||||||
</Content>
|
|
||||||
</Layout>
|
|
||||||
</WellContext.Provider>
|
|
||||||
</RootPathContext.Provider>
|
|
||||||
</LayoutPortal>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default wrapPrivateComponent(Well, {
|
|
||||||
requirements: [],
|
|
||||||
title: 'Скважина',
|
|
||||||
route: 'well/:idWell/*',
|
|
||||||
key: 'well',
|
|
||||||
})
|
|
0
src/pages/Analytics/Statistics.jsx → src/pages/Well/Analytics/Statistics.jsx
Executable file → Normal file
0
src/pages/Analytics/Statistics.jsx → src/pages/Well/Analytics/Statistics.jsx
Executable file → Normal file
71
src/pages/Analytics/WellCompositeEditor/NewParamsTable.jsx → src/pages/Well/Analytics/WellCompositeEditor/NewParamsTable.jsx
Executable file → Normal file
71
src/pages/Analytics/WellCompositeEditor/NewParamsTable.jsx → src/pages/Well/Analytics/WellCompositeEditor/NewParamsTable.jsx
Executable file → Normal file
@ -2,12 +2,75 @@ import { memo, useCallback, useEffect, useState } from 'react'
|
|||||||
import { Button, Modal, Popconfirm } from 'antd'
|
import { Button, Modal, Popconfirm } from 'antd'
|
||||||
|
|
||||||
import { useWell } from '@asb/context'
|
import { useWell } from '@asb/context'
|
||||||
import { Table } from '@components/Table'
|
import { makeColumn, makeGroupColumn, makeNumericRender, makeSelectColumn, Table } from '@components/Table'
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { DrillParamsService } from '@api'
|
import { DrillParamsService, WellOperationService } from '@api'
|
||||||
|
|
||||||
import { getColumns } from '@pages/WellOperations/WellDrillParams'
|
const getDeepValue = (data, key) => {
|
||||||
|
if (!key || key.trim() === '') return null
|
||||||
|
const keys = key.split('.')
|
||||||
|
let out = data
|
||||||
|
while (keys.length > 0) {
|
||||||
|
if (!(keys[0] in out)) return null
|
||||||
|
out = out[keys[0]]
|
||||||
|
keys.splice(0, 1)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeNumericSorter = (keys) => (a, b) => getDeepValue(a, keys) - getDeepValue(b, keys)
|
||||||
|
|
||||||
|
const numericRender = makeNumericRender(1)
|
||||||
|
const makeNumericColumn = (title, dataIndex, render, other) => makeColumn(title, dataIndex, {
|
||||||
|
sorter: makeNumericSorter(dataIndex),
|
||||||
|
render: (_, record, index) => {
|
||||||
|
const func = render ?? ((value) => <>{value}</>)
|
||||||
|
const item = getDeepValue(record, dataIndex)
|
||||||
|
return func(item, record, index)
|
||||||
|
},
|
||||||
|
align: 'right',
|
||||||
|
...other,
|
||||||
|
})
|
||||||
|
|
||||||
|
const makeAvgRender = (dataIndex) => (avg, record) => {
|
||||||
|
const max = record[dataIndex]?.max
|
||||||
|
const fillW = (max - avg) / max * 100
|
||||||
|
return (
|
||||||
|
<div className={'avg-column'}>
|
||||||
|
<div className={'avg-fill'} style={{ width: `${fillW}%` }} />
|
||||||
|
<div className={'avg-value'}>
|
||||||
|
{numericRender(avg)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeNumericAvgRange = (title, dataIndex, defaultRender = false) => makeGroupColumn(title, [
|
||||||
|
makeNumericColumn('мин', `${dataIndex}.min`),
|
||||||
|
makeNumericColumn('сред', `${dataIndex}.avg`, defaultRender ? undefined : makeAvgRender(dataIndex)),
|
||||||
|
makeNumericColumn('макс', `${dataIndex}.max`),
|
||||||
|
])
|
||||||
|
|
||||||
|
export const getColumns = async (idWell) => {
|
||||||
|
let sectionTypes = await WellOperationService.getSectionTypes(idWell)
|
||||||
|
sectionTypes = Object.entries(sectionTypes).map(([id, value]) => ({
|
||||||
|
label: value,
|
||||||
|
value: id,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return [
|
||||||
|
makeSelectColumn('Конструкция секции','idWellSectionType', sectionTypes, null, {
|
||||||
|
width: 160,
|
||||||
|
sorter: makeNumericSorter('idWellSectionType'),
|
||||||
|
}),
|
||||||
|
makeNumericAvgRange('Нагрузка, т', 'axialLoad'),
|
||||||
|
makeNumericAvgRange('Давление, атм', 'pressure'),
|
||||||
|
makeNumericAvgRange('Момент на ВСП, кН·м', 'rotorTorque', true),
|
||||||
|
makeNumericAvgRange('Обороты на ВСП, об/мин', 'rotorSpeed'),
|
||||||
|
makeNumericAvgRange('Расход, л/с', 'flow'),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
export const NewParamsTable = memo(({ selectedWellsKeys }) => {
|
export const NewParamsTable = memo(({ selectedWellsKeys }) => {
|
||||||
const [params, setParams] = useState([])
|
const [params, setParams] = useState([])
|
||||||
@ -54,7 +117,7 @@ export const NewParamsTable = memo(({ selectedWellsKeys }) => {
|
|||||||
<Modal
|
<Modal
|
||||||
title={'Заполнить режимы текущей скважины'}
|
title={'Заполнить режимы текущей скважины'}
|
||||||
centered
|
centered
|
||||||
visible={isParamsModalVisible}
|
open={isParamsModalVisible}
|
||||||
onCancel={() => setIsParamsModalVisible(false)}
|
onCancel={() => setIsParamsModalVisible(false)}
|
||||||
width={1700}
|
width={1700}
|
||||||
footer={(
|
footer={(
|
@ -1,7 +1,7 @@
|
|||||||
import { Link, useLocation } from 'react-router-dom'
|
import { Link, useLocation } from 'react-router-dom'
|
||||||
import { useState, useEffect, memo, useMemo } from 'react'
|
import { useState, useEffect, memo, useMemo, lazy, Suspense } from 'react'
|
||||||
import { LineChartOutlined, ProfileOutlined, TeamOutlined } from '@ant-design/icons'
|
import { LineChartOutlined, ProfileOutlined, TeamOutlined } from '@ant-design/icons'
|
||||||
import { Table, Tag, Button, Badge, Divider, Modal, Row, Col } from 'antd'
|
import { Table, Button, Badge, Divider, Modal, Row, Col } from 'antd'
|
||||||
|
|
||||||
import { useWell } from '@asb/context'
|
import { useWell } from '@asb/context'
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
@ -16,10 +16,12 @@ import {
|
|||||||
getOperations
|
getOperations
|
||||||
} from '@utils'
|
} from '@utils'
|
||||||
|
|
||||||
import Tvd from '@pages/WellOperations/Tvd'
|
|
||||||
import WellOperationsTable from '@pages/Cluster/WellOperationsTable'
|
|
||||||
import NewParamsTable from './NewParamsTable'
|
import NewParamsTable from './NewParamsTable'
|
||||||
import CompaniesTable from '@pages/Cluster/CompaniesTable'
|
import SuspenseFallback from '@asb/components/SuspenseFallback'
|
||||||
|
|
||||||
|
const Tvd = lazy(() => import('@pages/Well/WellOperations/Tvd'))
|
||||||
|
const CompaniesTable = lazy(() => import('@pages/Cluster/CompaniesTable'))
|
||||||
|
const WellOperationsTable = lazy(() => import('@pages/Cluster/WellOperationsTable'))
|
||||||
|
|
||||||
const filtersMinMax = [
|
const filtersMinMax = [
|
||||||
{ text: 'min', value: 'min' },
|
{ text: 'min', value: 'min' },
|
||||||
@ -222,38 +224,44 @@ const WellCompositeSections = memo(({ statsWells, selectedSections }) => {
|
|||||||
<Modal
|
<Modal
|
||||||
title={'TVD'}
|
title={'TVD'}
|
||||||
centered
|
centered
|
||||||
visible={isTVDModalVisible}
|
open={isTVDModalVisible}
|
||||||
onCancel={() => setIsTVDModalVisible(false)}
|
onCancel={() => setIsTVDModalVisible(false)}
|
||||||
width={1500}
|
width={1500}
|
||||||
footer={null}
|
footer={null}
|
||||||
>
|
>
|
||||||
<Tvd well={selectedWell} style={{ height: '80vh' }} />
|
<Suspense fallback={<SuspenseFallback style={{ minHeight: '100%' }} />}>
|
||||||
|
<Tvd well={selectedWell} style={{ height: '80vh' }} />
|
||||||
|
</Suspense>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={'Операции'}
|
title={'Операции'}
|
||||||
centered
|
centered
|
||||||
visible={isOpsModalVisible}
|
open={isOpsModalVisible}
|
||||||
onCancel={() => setIsOpsModalVisible(false)}
|
onCancel={() => setIsOpsModalVisible(false)}
|
||||||
width={1500}
|
width={1500}
|
||||||
footer={null}
|
footer={null}
|
||||||
>
|
>
|
||||||
<LoaderPortal show={showLoader}>
|
<Suspense fallback={<SuspenseFallback style={{ minHeight: '100%' }} />}>
|
||||||
<WellOperationsTable wellOperations={wellOperations} />
|
<LoaderPortal show={showLoader}>
|
||||||
</LoaderPortal>
|
<WellOperationsTable wellOperations={wellOperations} />
|
||||||
|
</LoaderPortal>
|
||||||
|
</Suspense>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={'Участники'}
|
title={'Участники'}
|
||||||
centered
|
centered
|
||||||
visible={isCompaniesModalVisible}
|
open={isCompaniesModalVisible}
|
||||||
onCancel={() => setIsCompaniesModalVisible(false)}
|
onCancel={() => setIsCompaniesModalVisible(false)}
|
||||||
width={1500}
|
width={1500}
|
||||||
footer={null}
|
footer={null}
|
||||||
>
|
>
|
||||||
<LoaderPortal show={showLoader}>
|
<Suspense fallback={<SuspenseFallback style={{ minHeight: '100%' }} />}>
|
||||||
<CompaniesTable companies={companies} />
|
<LoaderPortal show={showLoader}>
|
||||||
</LoaderPortal>
|
<CompaniesTable companies={companies} />
|
||||||
|
</LoaderPortal>
|
||||||
|
</Suspense>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
57
src/pages/Analytics/WellCompositeEditor/index.jsx → src/pages/Well/Analytics/WellCompositeEditor/index.jsx
Executable file → Normal file
57
src/pages/Analytics/WellCompositeEditor/index.jsx → src/pages/Well/Analytics/WellCompositeEditor/index.jsx
Executable file → Normal file
@ -1,35 +1,26 @@
|
|||||||
import { useState, useEffect, memo, useMemo } from 'react'
|
|
||||||
import { Navigate, Route, Routes } from 'react-router-dom'
|
import { Navigate, Route, Routes } from 'react-router-dom'
|
||||||
import { Col, Layout, Row } from 'antd'
|
import { useState, useEffect, memo, Suspense, lazy } from 'react'
|
||||||
|
import { Row } from 'antd'
|
||||||
|
|
||||||
import { useWell, useRootPath } from '@asb/context'
|
import { useWell } from '@asb/context'
|
||||||
import { PrivateMenu } from '@components/Private'
|
|
||||||
import LoaderPortal from '@components/LoaderPortal'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
|
import SuspenseFallback from '@components/SuspenseFallback'
|
||||||
import WellSelector from '@components/selectors/WellSelector'
|
import WellSelector from '@components/selectors/WellSelector'
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { arrayOrDefault, NoAccessComponent, wrapPrivateComponent } from '@utils'
|
import { arrayOrDefault, NoAccessComponent, wrapPrivateComponent } from '@utils'
|
||||||
import { OperationStatService, WellCompositeService } from '@api'
|
import { OperationStatService, WellCompositeService } from '@api'
|
||||||
|
|
||||||
import ClusterWells from '@pages/Cluster/ClusterWells'
|
|
||||||
import WellCompositeSections from './WellCompositeSections'
|
import WellCompositeSections from './WellCompositeSections'
|
||||||
|
|
||||||
const { Content } = Layout
|
import '@styles/well_composite.less'
|
||||||
|
|
||||||
const properties = {
|
const ClusterWells = lazy(() => import('@pages/Cluster/ClusterWells'))
|
||||||
requirements: ['OperationStat.get', 'WellComposite.get'],
|
|
||||||
title: 'Композитная скважина',
|
|
||||||
route: 'composite/*',
|
|
||||||
key: 'composite',
|
|
||||||
}
|
|
||||||
|
|
||||||
const WellCompositeEditor = memo(() => {
|
const WellCompositeEditor = memo(() => {
|
||||||
const [well] = useWell()
|
const [well] = useWell()
|
||||||
const root = useRootPath()
|
|
||||||
const rootPath = useMemo(() => `${root}/${properties.key}`, [root])
|
|
||||||
|
|
||||||
const [statsWells, setStatsWells] = useState([])
|
const [statsWells, setStatsWells] = useState([])
|
||||||
const [showLoader, setShowLoader] = useState(false)
|
const [showLoader, setShowLoader] = useState(false)
|
||||||
const [showTabLoader, setShowTabLoader] = useState(false)
|
|
||||||
const [selectedIdWells, setSelectedIdWells] = useState([])
|
const [selectedIdWells, setSelectedIdWells] = useState([])
|
||||||
const [selectedSections, setSelectedSections] = useState([])
|
const [selectedSections, setSelectedSections] = useState([])
|
||||||
|
|
||||||
@ -61,7 +52,7 @@ const WellCompositeEditor = memo(() => {
|
|||||||
const stats = arrayOrDefault(await OperationStatService.getWellsStat(selectedIdWells))
|
const stats = arrayOrDefault(await OperationStatService.getWellsStat(selectedIdWells))
|
||||||
setStatsWells(stats)
|
setStatsWells(stats)
|
||||||
},
|
},
|
||||||
setShowTabLoader,
|
setShowLoader,
|
||||||
'Не удалось загрузить статистику по скважинам/секциям',
|
'Не удалось загрузить статистику по скважинам/секциям',
|
||||||
{ actionName: 'Получение статистики по скважинам/секциям' }
|
{ actionName: 'Получение статистики по скважинам/секциям' }
|
||||||
)
|
)
|
||||||
@ -69,32 +60,20 @@ const WellCompositeEditor = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<LoaderPortal show={showLoader}>
|
<LoaderPortal show={showLoader}>
|
||||||
<Row align={'middle'} justify={'space-between'} wrap={false} style={{ backgroundColor: 'white' }}>
|
<Row align={'middle'} justify={'space-between'} wrap={false} style={{ backgroundColor: 'white', marginBottom: '15px' }}>
|
||||||
<Col span={18}>
|
<WellSelector onChange={setSelectedIdWells} value={selectedIdWells} />
|
||||||
<WellSelector onChange={setSelectedIdWells} value={selectedIdWells} />
|
|
||||||
</Col>
|
|
||||||
<Col span={6}>
|
|
||||||
<PrivateMenu root={rootPath} className={'well_menu'}>
|
|
||||||
<PrivateMenu.Link content={ClusterWells} />
|
|
||||||
<PrivateMenu.Link content={WellCompositeSections} />
|
|
||||||
</PrivateMenu>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
</Row>
|
||||||
<Layout>
|
<Suspense fallback={<SuspenseFallback style={{ minHeight: '100%' }} />}>
|
||||||
<Content className={'site-layout-background'}>
|
<Routes>
|
||||||
<LoaderPortal show={showTabLoader}>
|
<Route index element={<Navigate to={'wells'} replace/>} />
|
||||||
<Routes>
|
<Route path={'*'} element={<NoAccessComponent />} />
|
||||||
<Route index element={<Navigate to={ClusterWells.route} replace/>} />
|
|
||||||
<Route path={'*'} element={<NoAccessComponent />} />
|
|
||||||
|
|
||||||
<Route path={ClusterWells.route} element={<ClusterWells statsWells={statsWells} />} />
|
<Route path={'wells'} element={<ClusterWells statsWells={statsWells} />} />
|
||||||
<Route path={WellCompositeSections.route} element={<WellCompositeSections statsWells={statsWells} selectedSections={selectedSections} />} />
|
<Route path={'sections'} element={<WellCompositeSections statsWells={statsWells} selectedSections={selectedSections} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</LoaderPortal>
|
</Suspense>
|
||||||
</Content>
|
|
||||||
</Layout>
|
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default wrapPrivateComponent(WellCompositeEditor, properties)
|
export default wrapPrivateComponent(WellCompositeEditor, { requirements: ['OperationStat.get', 'WellComposite.get'] })
|
23
src/pages/Well/Analytics/index.jsx
Normal file
23
src/pages/Well/Analytics/index.jsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { memo, useMemo } from 'react'
|
||||||
|
import { Outlet } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { RootPathContext, useRootPath } from '@asb/context'
|
||||||
|
import { wrapPrivateComponent } from '@utils'
|
||||||
|
|
||||||
|
const Analytics = memo(() => {
|
||||||
|
const root = useRootPath()
|
||||||
|
const rootPath = useMemo(() => `${root}/analytics`, [root])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RootPathContext.Provider value={rootPath}>
|
||||||
|
<Outlet />
|
||||||
|
</RootPathContext.Provider>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default wrapPrivateComponent(Analytics, {
|
||||||
|
requirements: [],
|
||||||
|
title: 'Аналитика',
|
||||||
|
route: 'analytics/*',
|
||||||
|
key: 'analytics',
|
||||||
|
})
|
13
src/pages/Documents/DocumentsTemplate.jsx → src/pages/Well/Documents/DocumentsTemplate.jsx
Executable file → Normal file
13
src/pages/Documents/DocumentsTemplate.jsx → src/pages/Well/Documents/DocumentsTemplate.jsx
Executable file → Normal file
@ -40,7 +40,7 @@ export const DocumentsTemplate = ({ idCategory, well: givenWell, mimeTypes, head
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
makeDateColumn('Дата загрузки', 'uploadDate'),
|
makeDateColumn('Дата загрузки', 'uploadDate'),
|
||||||
makeNumericColumn('Размер', 'size', null, null, formatBytes),
|
makeNumericColumn('Размер', 'size', null, null, (value) => formatBytes(value)),
|
||||||
makeColumn('Автор', 'author', { render: item => <UserView user={item}/> }),
|
makeColumn('Автор', 'author', { render: item => <UserView user={item}/> }),
|
||||||
makeColumn('Компания', 'company', { render: (_, record) => <CompanyView company={record?.author?.company}/> }),
|
makeColumn('Компания', 'company', { render: (_, record) => <CompanyView company={record?.author?.company}/> }),
|
||||||
...(customColumns ?? [])
|
...(customColumns ?? [])
|
||||||
@ -49,12 +49,8 @@ export const DocumentsTemplate = ({ idCategory, well: givenWell, mimeTypes, head
|
|||||||
const filenames = useMemo(() => files.map(file => file.name).filter(Boolean).filter(unique), [files])
|
const filenames = useMemo(() => files.map(file => file.name).filter(Boolean).filter(unique), [files])
|
||||||
|
|
||||||
const update = useCallback(() => {
|
const update = useCallback(() => {
|
||||||
let begin = null
|
const begin = filterDataRange?.length > 1 ? filterDataRange[0].toISOString() : null
|
||||||
let end = null
|
const end = filterDataRange?.length > 1 ? filterDataRange[1].toISOString() : null
|
||||||
if (filterDataRange?.length > 1) {
|
|
||||||
begin = filterDataRange[0].toISOString()
|
|
||||||
end = filterDataRange[1].toISOString()
|
|
||||||
}
|
|
||||||
|
|
||||||
invokeWebApiWrapperAsync(
|
invokeWebApiWrapperAsync(
|
||||||
async () => {
|
async () => {
|
||||||
@ -65,6 +61,7 @@ export const DocumentsTemplate = ({ idCategory, well: givenWell, mimeTypes, head
|
|||||||
filterFileName,
|
filterFileName,
|
||||||
begin,
|
begin,
|
||||||
end,
|
end,
|
||||||
|
false,
|
||||||
(page - 1) * pageSize,
|
(page - 1) * pageSize,
|
||||||
pageSize,
|
pageSize,
|
||||||
)
|
)
|
||||||
@ -137,6 +134,7 @@ export const DocumentsTemplate = ({ idCategory, well: givenWell, mimeTypes, head
|
|||||||
<div>
|
<div>
|
||||||
<span>Загрузка</span>
|
<span>Загрузка</span>
|
||||||
<UploadForm
|
<UploadForm
|
||||||
|
multiple
|
||||||
url={uploadUrl}
|
url={uploadUrl}
|
||||||
mimeTypes={mimeTypes}
|
mimeTypes={mimeTypes}
|
||||||
onUploadStart={() => setShowLoader(true)}
|
onUploadStart={() => setShowLoader(true)}
|
||||||
@ -160,6 +158,7 @@ export const DocumentsTemplate = ({ idCategory, well: givenWell, mimeTypes, head
|
|||||||
onRowDelete={handleFileDelete}
|
onRowDelete={handleFileDelete}
|
||||||
rowKey={(record) => record.id}
|
rowKey={(record) => record.id}
|
||||||
tableName={tableName ?? `file_${idCategory}`}
|
tableName={tableName ?? `file_${idCategory}`}
|
||||||
|
scroll={{ x: true }}
|
||||||
/>
|
/>
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
)
|
)
|
49
src/pages/Documents/index.jsx → src/pages/Well/Documents/index.jsx
Executable file → Normal file
49
src/pages/Documents/index.jsx → src/pages/Well/Documents/index.jsx
Executable file → Normal file
@ -1,16 +1,11 @@
|
|||||||
import { Navigate, Route, Routes } from 'react-router-dom'
|
import { Navigate, Route, Routes } from 'react-router-dom'
|
||||||
import { memo, useMemo } from 'react'
|
import { memo, useMemo } from 'react'
|
||||||
import { FolderOutlined } from '@ant-design/icons'
|
|
||||||
import { Layout } from 'antd'
|
|
||||||
|
|
||||||
import { RootPathContext, useRootPath } from '@asb/context'
|
import { RootPathContext, useRootPath } from '@asb/context'
|
||||||
import { PrivateMenu } from '@components/Private'
|
import { wrapPrivateComponent, NoAccessComponent, hasPermission } from '@utils'
|
||||||
import { getTabname, wrapPrivateComponent, NoAccessComponent, hasPermission } from '@utils'
|
|
||||||
|
|
||||||
import DocumentsTemplate from './DocumentsTemplate'
|
import DocumentsTemplate from './DocumentsTemplate'
|
||||||
|
|
||||||
const { Content } = Layout
|
|
||||||
|
|
||||||
const makeDocCat = (id, key, title, permissions = ['File.get']) => ({ id, key, title, permissions })
|
const makeDocCat = (id, key, title, permissions = ['File.get']) => ({ id, key, title, permissions })
|
||||||
|
|
||||||
export const documentCategories = [
|
export const documentCategories = [
|
||||||
@ -27,7 +22,6 @@ export const documentCategories = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const MenuDocuments = memo(() => {
|
const MenuDocuments = memo(() => {
|
||||||
const category = getTabname()
|
|
||||||
const root = useRootPath()
|
const root = useRootPath()
|
||||||
const rootPath = useMemo(() => `${root}/document`, [root])
|
const rootPath = useMemo(() => `${root}/document`, [root])
|
||||||
|
|
||||||
@ -35,34 +29,21 @@ const MenuDocuments = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RootPathContext.Provider value={rootPath}>
|
<RootPathContext.Provider value={rootPath}>
|
||||||
<PrivateMenu mode={'horizontal'} selectable={true} className={'well_menu'} selectedKeys={[category]}>
|
<Routes>
|
||||||
{categories.map(category => (
|
{categories.length > 0 && (
|
||||||
<PrivateMenu.Link
|
<Route index element={<Navigate to={categories[0].key} replace />} />
|
||||||
key={`${category.key}`}
|
)}
|
||||||
icon={<FolderOutlined/>}
|
<Route path={'*'} element={<NoAccessComponent />} />
|
||||||
title={category.title}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</PrivateMenu>
|
|
||||||
<Layout>
|
|
||||||
<Content className={'site-layout-background'}>
|
|
||||||
<Routes>
|
|
||||||
{categories.length > 0 && (
|
|
||||||
<Route index element={<Navigate to={categories[0].key} replace />} />
|
|
||||||
)}
|
|
||||||
<Route path={'*'} element={<NoAccessComponent />} />
|
|
||||||
|
|
||||||
{categories.map(category => (
|
{categories.map(category => (
|
||||||
<Route key={category.key} path={category.key} element={(
|
<Route key={category.key} path={category.key} element={(
|
||||||
<DocumentsTemplate
|
<DocumentsTemplate
|
||||||
idCategory={category.id}
|
idCategory={category.id}
|
||||||
tableName={`documents_${category.key}`}
|
tableName={`documents_${category.key}`}
|
||||||
/>
|
/>
|
||||||
)} />
|
)} />
|
||||||
))}
|
))}
|
||||||
</Routes>
|
</Routes>
|
||||||
</Content>
|
|
||||||
</Layout>
|
|
||||||
</RootPathContext.Provider>
|
</RootPathContext.Provider>
|
||||||
)
|
)
|
||||||
})
|
})
|
0
src/pages/DrillingProgram/CategoryAdder.jsx → src/pages/Well/DrillingProgram/CategoryAdder.jsx
Executable file → Normal file
0
src/pages/DrillingProgram/CategoryAdder.jsx → src/pages/Well/DrillingProgram/CategoryAdder.jsx
Executable file → Normal file
2
src/pages/DrillingProgram/CategoryEditor.jsx → src/pages/Well/DrillingProgram/CategoryEditor.jsx
Executable file → Normal file
2
src/pages/DrillingProgram/CategoryEditor.jsx → src/pages/Well/DrillingProgram/CategoryEditor.jsx
Executable file → Normal file
@ -159,7 +159,7 @@ export const CategoryEditor = memo(({ visible, category, onClosed }) => {
|
|||||||
<Modal
|
<Modal
|
||||||
centered
|
centered
|
||||||
width={1000}
|
width={1000}
|
||||||
visible={visible}
|
open={visible}
|
||||||
footer={null}
|
footer={null}
|
||||||
onCancel={onModalClosed}
|
onCancel={onModalClosed}
|
||||||
title={`Редактирование пользователей категории ${title}`}
|
title={`Редактирование пользователей категории ${title}`}
|
4
src/pages/DrillingProgram/CategoryHistory.jsx → src/pages/Well/DrillingProgram/CategoryHistory.jsx
Executable file → Normal file
4
src/pages/DrillingProgram/CategoryHistory.jsx → src/pages/Well/DrillingProgram/CategoryHistory.jsx
Executable file → Normal file
@ -74,7 +74,7 @@ export const CategoryHistory = ({ idCategory, visible, onClose }) => {
|
|||||||
const [begin, end] = range?.length > 1 ? [range[0].toISOString(), range[1].toISOString()] : [null, null]
|
const [begin, end] = range?.length > 1 ? [range[0].toISOString(), range[1].toISOString()] : [null, null]
|
||||||
const skip = (page - 1) * pageSize
|
const skip = (page - 1) * pageSize
|
||||||
|
|
||||||
const paginatedHistory = await FileService.getFilesInfo(well.caption, idCategory, companyName, fileName, begin, end, skip, pageSize)
|
const paginatedHistory = await FileService.getFilesInfo(well.caption, idCategory, companyName, fileName, begin, end, false, skip, pageSize)
|
||||||
setTotal(paginatedHistory?.count ?? 0)
|
setTotal(paginatedHistory?.count ?? 0)
|
||||||
setData(arrayOrDefault(paginatedHistory?.items))
|
setData(arrayOrDefault(paginatedHistory?.items))
|
||||||
},
|
},
|
||||||
@ -94,7 +94,7 @@ export const CategoryHistory = ({ idCategory, visible, onClose }) => {
|
|||||||
title={'История категории'}
|
title={'История категории'}
|
||||||
width={1200}
|
width={1200}
|
||||||
centered
|
centered
|
||||||
visible={!!visible}
|
open={!!visible}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
footer={(
|
footer={(
|
||||||
<Button onClick={onClose}>Закрыть</Button>
|
<Button onClick={onClose}>Закрыть</Button>
|
1
src/pages/DrillingProgram/CategoryRender.jsx → src/pages/Well/DrillingProgram/CategoryRender.jsx
Executable file → Normal file
1
src/pages/DrillingProgram/CategoryRender.jsx → src/pages/Well/DrillingProgram/CategoryRender.jsx
Executable file → Normal file
@ -123,6 +123,7 @@ export const CategoryRender = memo(({ partData, onUpdate, onEdit, onHistory, set
|
|||||||
<div className={'file_actions'}>
|
<div className={'file_actions'}>
|
||||||
{permissionToUpload && (
|
{permissionToUpload && (
|
||||||
<UploadForm
|
<UploadForm
|
||||||
|
multiple
|
||||||
url={uploadUrl}
|
url={uploadUrl}
|
||||||
mimeTypes={MimeTypes.XLSX}
|
mimeTypes={MimeTypes.XLSX}
|
||||||
style={{ margin: '5px 0 10px 0' }}
|
style={{ margin: '5px 0 10px 0' }}
|
0
src/pages/DrillingProgram/MarksCard.jsx → src/pages/Well/DrillingProgram/MarksCard.jsx
Executable file → Normal file
0
src/pages/DrillingProgram/MarksCard.jsx → src/pages/Well/DrillingProgram/MarksCard.jsx
Executable file → Normal file
0
src/pages/DrillingProgram/index.jsx → src/pages/Well/DrillingProgram/index.jsx
Executable file → Normal file
0
src/pages/DrillingProgram/index.jsx → src/pages/Well/DrillingProgram/index.jsx
Executable file → Normal file
2
src/pages/Measure/InclinometryTable.jsx → src/pages/Well/Measure/InclinometryTable.jsx
Executable file → Normal file
2
src/pages/Measure/InclinometryTable.jsx → src/pages/Well/Measure/InclinometryTable.jsx
Executable file → Normal file
@ -35,7 +35,7 @@ export const InclinometryTable = memo(({ group, visible, onClose }) => {
|
|||||||
<Modal
|
<Modal
|
||||||
title={group?.title}
|
title={group?.title}
|
||||||
centered
|
centered
|
||||||
visible={visible}
|
open={visible}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
width={1900}
|
width={1900}
|
||||||
footer={null}
|
footer={null}
|
4
src/pages/Measure/MeasureTable.jsx → src/pages/Well/Measure/MeasureTable.jsx
Executable file → Normal file
4
src/pages/Measure/MeasureTable.jsx → src/pages/Well/Measure/MeasureTable.jsx
Executable file → Normal file
@ -21,7 +21,7 @@ import '@styles/index.css'
|
|||||||
import '@styles/measure.css'
|
import '@styles/measure.css'
|
||||||
|
|
||||||
const createEditingColumns = (cols, renderDelegate) =>
|
const createEditingColumns = (cols, renderDelegate) =>
|
||||||
cols.map(col => ({ render: renderDelegate, ...col }))
|
cols.map(col => col.map(col => ({ render: renderDelegate, ...col })))
|
||||||
|
|
||||||
const disabled = !hasPermission('Measure.edit')
|
const disabled = !hasPermission('Measure.edit')
|
||||||
|
|
||||||
@ -134,7 +134,7 @@ export const MeasureTable = memo(({ group, updateMeasuresFunc, additionalButtons
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'measure-dates mt-20px'}>
|
<div className={'measure-dates mt-20px p-10'}>
|
||||||
<Timeline className={'mt-12px ml-10px'}>
|
<Timeline className={'mt-12px ml-10px'}>
|
||||||
{data.map((item, index) => (
|
{data.map((item, index) => (
|
||||||
<Timeline.Item
|
<Timeline.Item
|
51
src/pages/Well/Measure/View.jsx
Normal file
51
src/pages/Well/Measure/View.jsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { memo, Fragment } from 'react'
|
||||||
|
import { Empty, Form } from 'antd'
|
||||||
|
|
||||||
|
import { Grid, GridItem } from '@components/Grid'
|
||||||
|
|
||||||
|
import '@styles/index.css'
|
||||||
|
import '@styles/measure.css'
|
||||||
|
|
||||||
|
export const View = memo(({ columns, item }) => !item || !columns?.length ? (
|
||||||
|
<Empty key={'empty'} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
) : (
|
||||||
|
<Grid>
|
||||||
|
{columns.map((cols, i) => {
|
||||||
|
const columnPosition = 1 + i * 2
|
||||||
|
return cols.map((column, j) => (
|
||||||
|
<Fragment key={column.key}>
|
||||||
|
<GridItem
|
||||||
|
key={column.dataIndex}
|
||||||
|
row={j + 1}
|
||||||
|
col={columnPosition}
|
||||||
|
className={'measure-column-header'}
|
||||||
|
>
|
||||||
|
{column.title}
|
||||||
|
</GridItem>
|
||||||
|
|
||||||
|
<GridItem
|
||||||
|
key={column.title}
|
||||||
|
row={j + 1}
|
||||||
|
col={columnPosition + 1}
|
||||||
|
className={'measure-column-value'}
|
||||||
|
style={{ padding: 0 }}
|
||||||
|
>
|
||||||
|
{column.render ? (
|
||||||
|
<Form.Item
|
||||||
|
key={column.dataIndex}
|
||||||
|
name={column.dataIndex}
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
>
|
||||||
|
{column.render(item[column.dataIndex])}
|
||||||
|
</Form.Item>
|
||||||
|
) : (
|
||||||
|
<p key={column.title} className={'m-5px'}>
|
||||||
|
{item[column.dataIndex]}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</GridItem>
|
||||||
|
</Fragment>
|
||||||
|
))
|
||||||
|
}).flat()}
|
||||||
|
</Grid>
|
||||||
|
))
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user