Merge branch 'dev' into fix/file-download-page-fix

# Conflicts:
#	src/pages/FileDownload.jsx
This commit is contained in:
ts_salikhov 2022-10-27 10:28:59 +04:00
commit bea165d76e
151 changed files with 1724 additions and 1441 deletions

View File

@ -1,6 +1,6 @@
{ {
"short_name": "React App", "short_name": "ЕЦП",
"name": "Create React App Sample", "name": "Единая Цифровая Платформа",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",

View File

@ -1,49 +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 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/include/antd_theme.less' import '@styles/include/antd_theme.less'
import '@styles/App.less'
//OpenAPI.BASE = 'http://localhost:3000' const Login = lazy(() => import('@pages/public/Login'))
OpenAPI.TOKEN = async () => getUserToken() ?? '' const Register = lazy(() => import('@pages/public/Register'))
OpenAPI.HEADERS = {'Content-Type': 'application/json'} 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('@pages/Well'))
// OpenAPI.BASE = 'http://localhost:3000'
// TODO: Удалить взятие из 'token' в следующем релизе, вставлено для совместимости
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={''}>
<Suspense fallback={<SuspenseFallback style={{ minHeight: '100vh' }} />}>
<Router> <Router>
<Routes> <Routes>
<Route index element={<Navigate to={Deposit.getKey()} replace />} /> <Route index element={<Navigate to={'deposit'} replace />} />
<Route path={'*'} element={<NoAccessComponent />} /> <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 */}
<Route path={AdminPanel.route} element={<AdminPanel />} />
{/* User pages */} {/* User pages */}
<Route path={Deposit.route} element={<Deposit />} /> <Route element={<UserOutlet />}>
<Route path={Cluster.route} element={<Cluster />} /> <Route path={'/file_download/:idWell/:idFile/*'} element={<FileDownload />} />
<Route path={Well.route} element={<Well />} />
<Route path={FileDownload.route} element={<FileDownload />} /> <Route element={<LayoutPortal />}>
{/* Admin pages */}
<Route path={'/admin/*'} element={<AdminPanel />} />
{/* Client pages */}
<Route path={'/deposit/*'} element={<Deposit />} />
<Route path={'/cluster/:idCluster'} element={<Cluster />} />
<Route path={'/well/:idWell/*'} element={<Well />} />
</Route>
</Route>
</Routes> </Routes>
</Router> </Router>
</Suspense>
</RootPathContext.Provider> </RootPathContext.Provider>
</ConfigProvider> </ConfigProvider>
)) ))

View File

@ -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 && <>&nbsp;(<UserView user={user} />)</>} {user && <>&nbsp;(<UserView user={user} />)</>}
</> </>
)} )}
visible={visible} open={visible}
onCancel={onModalCancel} onCancel={onModalCancel}
onOk={() => form.submit()} onOk={() => form.submit()}
okText={'Сохранить'} okText={'Сохранить'}

View File

@ -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'}>

View File

@ -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

View File

@ -1,31 +0,0 @@
import { Key, memo, ReactNode } from 'react'
import { Layout, LayoutProps } from 'antd'
import PageHeader from '@components/PageHeader'
import { WellTreeSelector, WellTreeSelectorProps } from '@components/selectors/WellTreeSelector'
import { wrapPrivateComponent } from '@utils'
export type LayoutPortalProps = LayoutProps & {
title?: ReactNode
noSheet?: boolean
selector?: WellTreeSelectorProps
}
const _LayoutPortal = memo<LayoutPortalProps>(({ title, noSheet, selector, ...props }) => (
<Layout.Content>
<PageHeader title={title}>
<WellTreeSelector {...selector} />
</PageHeader>
<Layout>
{noSheet ? props.children : (
<Layout.Content className={'site-layout-background sheet'} {...props}/>
)}
</Layout>
</Layout.Content>
))
export const LayoutPortal = wrapPrivateComponent(_LayoutPortal, {
requirements: ['Deposit.get'],
})
export default LayoutPortal

View File

@ -1,5 +0,0 @@
export * from './AdminLayoutPortal'
export * from './LayoutPortal'
export type { AdminLayoutPortalProps } from './AdminLayoutPortal'
export type { LayoutPortalProps } from './LayoutPortal'

View File

@ -0,0 +1,136 @@
import { Breadcrumb, Layout, LayoutProps, Menu, SiderProps } from 'antd'
import { Key, memo, ReactNode, Suspense, useCallback, useEffect, useMemo, useState } from 'react'
import { ItemType } from 'antd/lib/menu/hooks/useItems'
import { Link, Outlet } from 'react-router-dom'
import {
ApartmentOutlined,
CodeOutlined,
HomeOutlined,
UserOutlined,
} from '@ant-design/icons'
import { LayoutPropsContext } from '@asb/context'
import { UserMenu, UserMenuProps } from '@components/UserMenu'
import { WellTreeSelector, WellTreeSelectorProps } from '@components/selectors/WellTreeSelector'
import { isURLAvailable, wrapPrivateComponent } from '@utils'
import SuspenseFallback from './SuspenseFallback'
import Logo from '@images/Logo'
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
breadcrumb?: boolean | JSX.Element
topRightBlock?: JSX.Element
}
const defaultProps: LayoutPortalProps = {
title: 'Единая цифровая платформа',
sider: true,
sheet: true,
}
const makeItem = (title: string, key: Key, icon: JSX.Element, label?: ReactNode, onClick?: () => void) => ({ icon, key, title, label: label ?? title, onClick })
const _LayoutPortal = memo(() => {
const [menuCollapsed, setMenuCollapsed] = useState<boolean>(true)
const [wellsTreeOpen, setWellsTreeOpen] = useState<boolean>(false)
const [userMenuOpen, setUserMenuOpen] = useState<boolean>(false)
const [currentWell, setCurrentWell] = useState<string>('')
const [props, setProps] = useState<LayoutPortalProps>(defaultProps)
const { isAdmin, title, sheet, showSelector, selectorProps, sider, siderProps, fallback, breadcrumb, topRightBlock, ...other } = useMemo(() => props, [props])
const setLayoutProps = useCallback((props: LayoutPortalProps) => setProps({ ...defaultProps, ...props}), [])
useEffect(() => {
if (typeof showSelector === 'boolean')
setWellsTreeOpen(showSelector)
}, [showSelector])
const menuItems = useMemo(() => [
!isAdmin && makeItem(currentWell, 'well', <ApartmentOutlined/>, null, () => setWellsTreeOpen((prev) => !prev)),
isAdmin && makeItem('Вернуться на сайт', 'go_back', <HomeOutlined />, <Link to={'/'}>Домой</Link>),
!isAdmin && isURLAvailable('/admin') && makeItem('Панель администратора', 'admin_panel', <CodeOutlined />, <Link to={'/admin'}>Панель администратора</Link>),
makeItem('Профиль', 'profile', <UserOutlined/>, null, () => setUserMenuOpen((prev) => !prev)),
].filter(Boolean) as ItemType[], [isAdmin, currentWell])
return (
<Layout className={`page-layout ${isAdmin ? 'page-layout-admin' : ''}`}>
{(sider || siderProps) && (
<Sider {...siderProps} collapsedWidth={50} collapsed={menuCollapsed} trigger={null} collapsible className={`menu-sider ${siderProps?.className || ''}`}>
<div className={'sider-content'}>
<button className={'sider-toogle'} onClick={() => setMenuCollapsed((prev) => !prev)}>
<Logo onlyIcon={menuCollapsed} />
</button>
<div className={'scrollable hide-slider'}>
{sider}
</div>
<Menu
mode={'inline'}
items={menuItems}
theme={'dark'}
selectable={false}
/>
<UserMenu
open={userMenuOpen}
onClose={() => setUserMenuOpen(false)}
isAdmin={isAdmin}
{...siderProps?.userMenuProps}
/>
</div>
</Sider>
)}
{!isAdmin && (
<WellTreeSelector
open={wellsTreeOpen}
onClose={() => setWellsTreeOpen(false)}
{...selectorProps}
onChange={(well) => setCurrentWell(well ?? 'Выберите месторождение')}
/>
)}
<Layout className={'page-content'}>
<Content {...other} className={`${sheet ? 'site-layout-background sheet' : ''} ${other.className ?? ''}`}>
{(breadcrumb || topRightBlock) && (
<div className={'breadcrumb-block'}>
{breadcrumb && (
<Breadcrumb>
<Breadcrumb.Item href={'/'}>
<HomeOutlined />
</Breadcrumb.Item>
{!isAdmin && (
<Breadcrumb.Item>
<a style={{ userSelect: 'none' }} onClick={() => setWellsTreeOpen((prev) => !prev)}>{currentWell}</a>
</Breadcrumb.Item>
)}
{breadcrumb}
</Breadcrumb>
)}
{topRightBlock}
</div>
)}
<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

View File

@ -0,0 +1,56 @@
import { Breadcrumb, BreadcrumbItemProps } from 'antd'
import { Link, useLocation } from 'react-router-dom'
import { memo, useMemo } from 'react'
import { join } from 'path'
import { PrivateWellMenuItem } from '@components/PrivateWellMenu'
import { FunctionalValue, useFunctionalValue } from '@utils'
export const makeBreadcrumbItems = (items: PrivateWellMenuItem[], pathParts: string[], root: string = '/') => {
const out = []
const parts = [...pathParts]
let route = root
let arr: PrivateWellMenuItem[] | undefined = items
while (arr && parts.length > 0) {
const child: PrivateWellMenuItem | undefined = arr.find(elm => elm.route.toLowerCase() === parts[0].toLowerCase())
if (!child) break
route = join(route, child.route)
out.push({ ...child, route })
parts.splice(0, 1)
arr = child.children
}
return out
}
export type MenuBreadcrumbItemsProps = {
menuItems: PrivateWellMenuItem[]
pathRoot?: RegExp
itemsProps?: FunctionalValue<(item: PrivateWellMenuItem) => BreadcrumbItemProps>
itemRender?: (item: PrivateWellMenuItem) => JSX.Element
}
export const MenuBreadcrumbItems = memo<MenuBreadcrumbItemsProps>(({ menuItems, pathRoot = /^\//, itemsProps, itemRender }) => {
const location = useLocation()
const getItemProps = useFunctionalValue(itemsProps)
const items = useMemo(() => {
const path = location.pathname
const rootPart = pathRoot.exec(path)
if (!rootPart || rootPart.length <= 0) return []
const root = rootPart[0]
const parts = path.trim().slice(root.length).split('/')
return makeBreadcrumbItems(menuItems, parts, root)
}, [location, menuItems, pathRoot])
return (
<>
{items.map((item) => (
<Breadcrumb.Item key={item.route} {...getItemProps(item)}>
{itemRender ? itemRender(item) : (
<Link to={item.route}>{item.title}</Link>
)}
</Breadcrumb.Item>
))}
</>
)
})

View File

@ -1,30 +0,0 @@
import { memo } from 'react'
import { Layout } from 'antd'
import { Link } from 'react-router-dom'
import { BasicProps } from 'antd/lib/layout/layout'
import { headerHeight } from '@utils'
import { UserMenu } from './UserMenu'
import Logo from '@images/Logo'
export type PageHeaderProps = BasicProps & {
title?: string
isAdmin?: boolean
children?: React.ReactNode
}
export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Мониторинг', isAdmin, children, ...other }) => (
<Layout>
<Layout.Header className={'header'} {...other}>
<Link to={'/'} style={{ height: headerHeight }}>
<Logo />
</Link>
<h1 className={'title'}>{title}</h1>
{children}
<UserMenu isAdmin={isAdmin} />
</Layout.Header>
</Layout>
))
export default PageHeader

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View 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'
export 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 = Omit<MenuProps, 'items'> & {
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} />
})

View File

View File

@ -84,7 +84,7 @@ const _TableSettingsChanger = <T extends object>({ title, columns, settings, onC
<> <>
<Modal <Modal
centered centered
visible={visible} open={visible}
onCancel={onModalCancel} onCancel={onModalCancel}
onOk={onModalOk} onOk={onModalOk}
title={title ?? 'Настройка отображения таблицы'} title={title ?? 'Настройка отображения таблицы'}

View File

@ -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
placement={'left'}
className={'user-menu'}
title={'Профиль пользователя'}
{...other} {...other}
placement={'bottomRight'}
overlay={(
<PrivateMenu mode={'vertical'} style={{ textAlign: 'right' }}>
{isAdmin ? (
<PrivateMenu.Link visible key={'/'} path={'/'} title={'Вернуться на сайт'} />
) : (
<PrivateMenu.Link path={'/admin'} content={AdminPanel} />
)}
<PrivateMenu.Link visible key={'change_password'} onClick={onChangePasswordClick} title={'Сменить пароль'} />
<PrivateMenu.Link visible key={'login'} path={'/login'} onClick={removeUser} title={'Выход'} />
</PrivateMenu>
)}
> >
<Button icon={<UserOutlined/>}>{getUserLogin()}</Button> <div className={'profile-links'}>
</Dropdown> {isAdmin ? (
<ChangePassword <Button onClick={() => navigateTo('/')}>Вернуться на сайт</Button>
visible={isModalVisible} ) : isURLAvailable('/admin') && (
onOk={onChangePasswordOk} <Button onClick={() => navigateTo('/admin')}>Панель администратора</Button>
onCancel={() => setIsModalVisible(false)} )}
/> <Button type={'ghost'} onClick={logout}>Выход</Button>
</> </div>
<Collapse>
<Collapse.Panel header={'Данные'} key={'summary'}>
<Grid>
<GridItem row={1} col={1}>Логин:</GridItem>
<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>
) )
}) })

View File

@ -135,7 +135,7 @@ const _D3MonitoringEditor = <DataType extends BaseDataType>({
<Modal <Modal
centered centered
width={800} width={800}
visible={visible} open={visible}
title={'Настройка групп графиков'} title={'Настройка групп графиков'}
onCancel={onCancel} onCancel={onCancel}
footer={( footer={(

View File

@ -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'
@ -97,7 +96,7 @@ export const invokeWebApiWrapperAsync = async (
export const download = async (url: string, fileName?: string) => { export const download = async (url: string, fileName?: string) => {
const response = await fetch(url, { const response = await fetch(url, {
headers: { headers: {
Authorization: `Bearer ${getUserToken()}` Authorization: `Bearer ${getUser().token}`
}, },
method: 'Get' method: 'Get'
}) })
@ -125,7 +124,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,

View 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

View File

@ -0,0 +1 @@
export * from './UserOutlet'

View File

@ -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>

View File

@ -74,6 +74,9 @@ export type WellTreeSelectorProps = TreeProps<TreeDataNode> & {
show?: boolean show?: boolean
expand?: boolean | Key[] expand?: boolean | Key[]
current?: Key current?: Key
onClose?: () => void
onChange?: (value: string | undefined) => void
open?: boolean
} }
const getExpandKeys = (treeData: TreeDataNode[], depositKeys?: Key[] | boolean): Key[] => { const getExpandKeys = (treeData: TreeDataNode[], depositKeys?: Key[] | boolean): Key[] => {
@ -88,13 +91,11 @@ const getExpandKeys = (treeData: TreeDataNode[], depositKeys?: Key[] | boolean):
return out return out
} }
export const WellTreeSelector = memo<WellTreeSelectorProps>(({ show, expand, current, ...other }) => { export const WellTreeSelector = memo<WellTreeSelectorProps>(({ expand, current, onChange, onClose, open, ...other }) => {
const [wellsTree, setWellsTree] = useState<TreeDataNode[]>([]) 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()
@ -103,8 +104,6 @@ export const WellTreeSelector = memo<WellTreeSelectorProps>(({ show, expand, cur
if (current) setSelected([current]) if (current) setSelected([current])
}, [current]) }, [current])
useEffect(() => setVisible((prev) => show ?? prev), [show])
useEffect(() => { useEffect(() => {
setExpanded((prev) => expand ? getExpandKeys(wellsTree, expand) : prev) setExpanded((prev) => expand ? getExpandKeys(wellsTree, expand) : prev)
}, [wellsTree, expand]) }, [wellsTree, expand])
@ -149,10 +148,10 @@ export const WellTreeSelector = memo<WellTreeSelectorProps>(({ show, expand, cur
) )
}, []) }, [])
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 => {
@ -170,13 +169,10 @@ export const WellTreeSelector = memo<WellTreeSelectorProps>(({ show, expand, cur
navigate(newPath, { state: { from: location.pathname }}) 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>
<Drawer visible={visible} mask={false} onClose={() => setVisible(false)}>
<Typography.Title level={3}>Список скважин</Typography.Title>
<Skeleton active loading={showLoader}> <Skeleton active loading={showLoader}>
<Tree <Tree
{...other} {...other}
@ -189,7 +185,6 @@ export const WellTreeSelector = memo<WellTreeSelectorProps>(({ show, expand, cur
/> />
</Skeleton> </Skeleton>
</Drawer> </Drawer>
</>
) )
}) })

View File

@ -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) => {
let value = telemetry.info?.[key]
value = key === 'drillingStartDate' ? formatDate(value) : value
return (
<Fragment key={i}> <Fragment key={i}>
<GridItem row={i+1} col={1}>{lables[key] ?? key}:</GridItem> <GridItem row={i+1} col={1}>{lables[key] ?? key}:</GridItem>
<GridItem row={i+1} col={2}>{telemetry.info?.[key]}</GridItem> <GridItem row={i+1} col={2}>{value}</GridItem>
</Fragment> </Fragment>
))} )
})}
</Grid> </Grid>
} }
> >

View File

@ -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}"` : ''}

View File

@ -1,22 +1,53 @@
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>(() => {})
/** Контекст для блока справа от крошек на страницах скважин и админки */
export const TopRightBlockContext = createContext<(block: JSX.Element) => 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)
export const useTopRightBlock = () => useContext(TopRightBlockContext)
/**
* Получить метод задания параметров заголовка и меню
*
* @returns Получить метод задания параметров заголовка и меню
*/
export const useLayoutProps = (props?: LayoutPortalProps) => {
const setLayoutProps = useContext(LayoutPropsContext)
useEffect(() => {
if (props) setLayoutProps(props)
}, [setLayoutProps, props])
return setLayoutProps
}

View File

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

View File

@ -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 '@utils'
export const menuItems = [
makeItem('Месторождения', 'deposit', [], <FolderOutlined />),
makeItem('Кусты', 'cluster', [], <FolderOutlined />),
makeItem('Скважины', 'well', [], <FolderOutlined />),
makeItem('Пользователи', 'user', [], <UserOutlined />),
makeItem('Компании', 'company', [], <BankOutlined />),
makeItem('Типы компаний', 'company_type', [], <BankOutlined />),
makeItem('Роли', 'role', [], <TeamOutlined />),
makeItem('Разрешения', 'permission', [], <IdcardOutlined />),
makeItem('Телеметрия', 'telemetry', [], <DashboardOutlined />, [
makeItem('Просмотр', 'viewer', [], <MonitorOutlined />),
makeItem('Объединение', 'merger', [], <BranchesOutlined />),
]),
makeItem('Журнал посещений', 'visit_log', [], <FileSearchOutlined />),
isDev() && makeItem('API', '/swagger/index.html', [], <ApiOutlined />),
].filter(Boolean)
export const AdminNavigationMenu = memo((props) => (
<PrivateWellMenu
{...props}
items={menuItems}
rootPath={'/admin'}
selectable={false}
mode={'inline'}
theme={'dark'}
/>
))
export default AdminNavigationMenu

View File

@ -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 }}
/> />
</> </>
) )

View File

@ -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 }}
/> />
</> </>
) )

View File

@ -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 }}
/> />
</> </>
) )

View File

@ -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 }}
/> />
</> </>
) )

View File

@ -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 }}
/> />
</> </>
) )

View File

@ -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 }}
/> />
</> </>
) )

View File

@ -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 }}
/> />
</> </>
) )

View File

@ -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>
) )
}) })

View File

@ -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

View File

@ -59,6 +59,7 @@ const VisitLog = memo(() => {
dataSource={filteredLogData} dataSource={filteredLogData}
pagination={defaultPagination} pagination={defaultPagination}
tableName={'visit_log'} tableName={'visit_log'}
scroll={{ x: true }}
/> />
</> </>
) )

View File

@ -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} />,

View File

@ -1,69 +1,61 @@
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 { MenuBreadcrumbItems } from '@components/MenuBreadcrumb'
import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils' import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import ClusterController from './ClusterController' import { AdminNavigationMenu, menuItems } 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,
breadcrumb: <MenuBreadcrumbItems menuItems={menuItems} pathRoot={/^\/admin\//} />,
}
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={'Администраторская панель'}>
<PrivateMenu>
<PrivateMenu.Link content={DepositController} />
<PrivateMenu.Link content={ClusterController} />
<PrivateMenu.Link content={WellController} />
<PrivateMenu.Link content={UserController} />
<PrivateMenu.Link content={CompanyController} />
<PrivateMenu.Link content={CompanyTypeController} />
<PrivateMenu.Link content={RoleController} />
<PrivateMenu.Link content={PermissionController} />
<PrivateMenu.Link content={Telemetry} />
<PrivateMenu.Link content={VisitLog} />
</PrivateMenu>
<Layout>
<Layout.Content className={'site-layout-background'}>
<Routes> <Routes>
<Route index element={<Navigate to={VisitLog.route} replace />} /> <Route index element={<Navigate to={'visit_log'} replace />} />
<Route path={'*'} element={<NoAccessComponent />} /> <Route path={'*'} element={<NoAccessComponent />} />
<Route path={DepositController.route} element={<DepositController />} /> <Route path={'deposit'} element={<DepositController />} />
<Route path={ClusterController.route} element={<ClusterController />} /> <Route path={'cluster'} element={<ClusterController />} />
<Route path={WellController.route} element={<WellController />} /> <Route path={'well'} element={<WellController />} />
<Route path={UserController.route} element={<UserController />} /> <Route path={'user'} element={<UserController />} />
<Route path={CompanyController.route} element={<CompanyController />} /> <Route path={'company'} element={<CompanyController />} />
<Route path={CompanyTypeController.route} element={<CompanyTypeController />} /> <Route path={'company_type'} element={<CompanyTypeController />} />
<Route path={RoleController.route} element={<RoleController />} /> <Route path={'role'} element={<RoleController />} />
<Route path={PermissionController.route} element={<PermissionController />} /> <Route path={'permission'} element={<PermissionController />} />
<Route path={Telemetry.route} element={<Telemetry />} /> <Route path={'telemetry'} element={<Telemetry />}>
<Route path={VisitLog.route} element={<VisitLog />} /> <Route index element={<Navigate to={'viewer'} replace />} />
<Route path={'*'} element={<NoAccessComponent />} />
<Route path={'viewer'} element={<TelemetryViewer />} />
<Route path={'merger'} element={<TelemetryMerger />} />
</Route>
<Route path={'visit_log'} element={<VisitLog />} />
</Routes> </Routes>
</Layout.Content>
</Layout>
</AdminLayoutPortal>
</RootPathContext.Provider> </RootPathContext.Provider>
) )
}) })
export default wrapPrivateComponent(AdminPanel, { export default wrapPrivateComponent(AdminPanel, { requirements: ['RequestTracker.get'] })
requirements: ['RequestTracker.get'],
title: 'Панель администратора',
route: 'admin/*',
key: 'admin',
})

View File

@ -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',
})

View File

@ -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}
> >
<Suspense fallback={<SuspenseFallback style={{ minHeight: '100%' }} />}>
<Tvd style={{ minHeight: '600px' }} well={selectedWell} /> <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}

View File

@ -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])

View File

@ -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 ?? '-')
] ]

View File

@ -1,19 +1,26 @@
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: 'Анализ скважин куста',
breadcrumb: true,
}
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 +34,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>
) )
}) })

View File

@ -3,8 +3,8 @@ 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,18 +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 selectorProps = useMemo(() => {
const hasId = location.pathname.length > '/deposit/'.length const hasId = location.pathname.length > '/deposit/'.length
return { return {
show: true,
expand: hasId ? [location.pathname] : true, expand: hasId ? [location.pathname] : true,
current: hasId ? location.pathname : undefined, current: hasId ? location.pathname : undefined,
} }
}, [location.pathname]) }, [location.pathname])
useEffect(() => setLayoutProps({
sheet: false,
showSelector: true,
selectorProps,
title: 'Месторождение',
}), [setLayoutProps, selectorProps])
useEffect(() => { useEffect(() => {
invokeWebApiWrapperAsync( invokeWebApiWrapperAsync(
async () => { async () => {
@ -73,9 +81,8 @@ const Deposit = memo(() => {
}, []) }, [])
return ( return (
<LayoutPortal noSheet selector={selectorProps} title={'Месторождение'}>
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<div className={'h-100vh'}> <div className={'h-100vh'} style={{ overflow: 'hidden' }}>
<Map {...viewParams}> <Map {...viewParams}>
{depositsData.map(deposit => ( {depositsData.map(deposit => (
<Overlay <Overlay
@ -103,7 +110,6 @@ const Deposit = memo(() => {
</Map> </Map>
</div> </div>
</LoaderPortal> </LoaderPortal>
</LayoutPortal>
) )
}) })

View File

@ -7,8 +7,6 @@ import { downloadFile, invokeWebApiWrapperAsync } from '@components/factory'
import { wrapPrivateComponent } from '@utils' import { wrapPrivateComponent } from '@utils'
import { FileService } from '@api' import { FileService } from '@api'
import AccessDenied from './AccessDenied'
const { Paragraph, Text } = Typography const { Paragraph, Text } = Typography
export const getLinkToFile = (fileInfo) => `/file_download/${fileInfo.id}` export const getLinkToFile = (fileInfo) => `/file_download/${fileInfo.id}`
@ -97,4 +95,4 @@ FileDownload.displayName = 'FileDownloadMemo'
export default wrapPrivateComponent(FileDownload, { export default wrapPrivateComponent(FileDownload, {
requirements: ['File.get'], requirements: ['File.get'],
route: 'file_download/:idFile/*', route: 'file_download/:idFile/*',
}, <AccessDenied />) })

View File

@ -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',
})

View File

@ -1,63 +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 OperationTime from './OperationTime'
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.Link content={OperationTime} />
</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 />} />
<Route path={OperationTime.route} element={<OperationTime />} />
</Routes>
</Content>
</Layout>
</Layout>
</RootPathContext.Provider>
)
})
export default wrapPrivateComponent(Telemetry, {
requirements: [],
icon: <FundViewOutlined />,
title: 'Телеметрия',
route: 'telemetry/*',
key: 'telemetry',
})

View File

@ -1,104 +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 WellCase from './WellCase'
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])
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.Link content={WellCase} icon={<FolderOutlined />} />
</PrivateMenu>
<WellContext.Provider value={[well, updateWell]}>
<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 />} />
<Route path={WellCase.route} element={<WellCase />} />
</Routes>
</Content>
</Layout>
</WellContext.Provider>
</RootPathContext.Provider>
</LayoutPortal>
)
})
export default wrapPrivateComponent(Well, {
requirements: [],
title: 'Скважина',
route: 'well/:idWell/*',
key: 'well',
})

View 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={(

View File

@ -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}
> >
<Suspense fallback={<SuspenseFallback style={{ minHeight: '100%' }} />}>
<Tvd well={selectedWell} style={{ height: '80vh' }} /> <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}
> >
<Suspense fallback={<SuspenseFallback style={{ minHeight: '100%' }} />}>
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<WellOperationsTable wellOperations={wellOperations} /> <WellOperationsTable wellOperations={wellOperations} />
</LoaderPortal> </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}
> >
<Suspense fallback={<SuspenseFallback style={{ minHeight: '100%' }} />}>
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<CompaniesTable companies={companies} /> <CompaniesTable companies={companies} />
</LoaderPortal> </LoaderPortal>
</Suspense>
</Modal> </Modal>
</> </>
) )

View 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'}>
<LoaderPortal show={showTabLoader}>
<Routes> <Routes>
<Route index element={<Navigate to={ClusterWells.route} replace/>} /> <Route index element={<Navigate to={'wells'} replace/>} />
<Route path={'*'} element={<NoAccessComponent />} /> <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'] })

View 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',
})

View File

@ -94,7 +94,7 @@ export const DocumentsTemplate = ({ idCategory, well: givenWell, mimeTypes, head
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<div style={{ margin: 16, display: 'flex' }}> <div style={{ margin: 16, marginTop: 0, display: 'flex' }}>
<div> <div>
<span>Фильтр по дате</span> <span>Фильтр по дате</span>
<div> <div>
@ -158,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>
) )

View 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,17 +29,6 @@ const MenuDocuments = memo(() => {
return ( return (
<RootPathContext.Provider value={rootPath}> <RootPathContext.Provider value={rootPath}>
<PrivateMenu mode={'horizontal'} selectable={true} className={'well_menu'} selectedKeys={[category]}>
{categories.map(category => (
<PrivateMenu.Link
key={`${category.key}`}
icon={<FolderOutlined/>}
title={category.title}
/>
))}
</PrivateMenu>
<Layout>
<Content className={'site-layout-background'}>
<Routes> <Routes>
{categories.length > 0 && ( {categories.length > 0 && (
<Route index element={<Navigate to={categories[0].key} replace />} /> <Route index element={<Navigate to={categories[0].key} replace />} />
@ -61,8 +44,6 @@ const MenuDocuments = memo(() => {
)} /> )} />
))} ))}
</Routes> </Routes>
</Content>
</Layout>
</RootPathContext.Provider> </RootPathContext.Provider>
) )
}) })

View 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}`}

View File

@ -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>

View 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}

View File

View File

View File

View File

@ -0,0 +1,75 @@
import { memo } from 'react'
import {
AlertOutlined,
BarChartOutlined,
BuildOutlined,
ControlOutlined,
DatabaseOutlined,
DeploymentUnitOutlined,
ExperimentOutlined,
FilePdfOutlined,
FolderOutlined,
FundViewOutlined,
LineChartOutlined,
TableOutlined,
} from '@ant-design/icons'
import { makeItem, PrivateWellMenu } from '@components/PrivateWellMenu'
export const menuItems = [
makeItem('Телеметрия', 'telemetry', [], <FundViewOutlined />, [
makeItem('Мониторинг', 'telemetry', [], <FundViewOutlined />),
makeItem('Сообщения', 'messages', [], <AlertOutlined />),
makeItem('Архив', 'archive', [], <DatabaseOutlined />),
makeItem('ННБ', 'dashboard_nnb', [], <FolderOutlined />),
makeItem('Операции', 'operations', [], <FolderOutlined />),
makeItem('Наработка', 'operation_time', [], <FolderOutlined />),
]),
makeItem('Рапорта', 'reports', [], <FilePdfOutlined />, [
makeItem('Диаграмма', 'diagram_report', [], <FilePdfOutlined />),
makeItem('Суточный рапорт', 'daily_report', [], <FolderOutlined />),
]),
makeItem('Аналитика', 'analytics', [], <DeploymentUnitOutlined />, [
makeItem('Композитная скважина', 'composite', [], <FolderOutlined />, [
makeItem('Статистика по скважинам', 'wells', [], <FolderOutlined />),
makeItem('Статистика по секциям', 'sections', [], <FolderOutlined />),
]),
makeItem('Оценка по ЦБ', 'statistics', [], <FolderOutlined />),
]),
makeItem('Операции по скважине', 'operations', [], <FolderOutlined />, [
makeItem('TVD', 'tvd', [], <LineChartOutlined />),
makeItem('Секции', 'sections', [], <BuildOutlined />),
makeItem('План', 'plan', [], <TableOutlined />),
makeItem('Факт', 'fact', [], <TableOutlined />),
makeItem('РТК', 'drillProcessFlow', [], <BarChartOutlined />),
makeItem('Режимы', 'params', [], <ControlOutlined />),
]),
makeItem('Документы', 'document', [], <FolderOutlined />, [
makeItem('Растворный сервис', 'fluidService', [], <FolderOutlined />),
makeItem('Цементирование', 'cementing', [], <FolderOutlined />),
makeItem('ННБ', 'nnb', [], <FolderOutlined />),
makeItem('ГТИ', 'gti', [], <FolderOutlined />),
makeItem('Документы по скважине', 'documentsForWell', [], <FolderOutlined />),
makeItem('Супервайзер', 'supervisor', [], <FolderOutlined />),
makeItem('Мастер', 'master', [], <FolderOutlined />),
makeItem('Долотный сервис', 'toolService', [], <FolderOutlined />),
makeItem('Буровой подрядчик', 'drillService', [], <FolderOutlined />),
makeItem('Сервис по заканчиванию скважины', 'closingService', [], <FolderOutlined />),
]),
makeItem('Измерения', 'measure', [], <ExperimentOutlined />),
makeItem('Программа бурения', 'drillingProgram', [], <FolderOutlined />),
makeItem('Дело скважины', 'well_case', [], <FolderOutlined />),
]
export const NavigationMenu = memo((props) => (
<PrivateWellMenu
{...props}
items={menuItems}
rootPath={'/well/{wellId}'}
mode={'inline'}
theme={'dark'}
style={{ backgroundColor: 'transparent' }}
/>
))
export default NavigationMenu

View File

@ -374,7 +374,7 @@ export const ReportEditor = memo(({ visible, data, onDone, onCancel, checkIsDate
<Modal <Modal
centered centered
width={1200} width={1200}
visible={visible} open={visible}
onCancel={onCancel} onCancel={onCancel}
okText={'Сохранить'} okText={'Сохранить'}
title={data ? ( title={data ? (

View File

@ -4,9 +4,9 @@ import { FilePdfOutlined, FileTextOutlined } from '@ant-design/icons'
import { useWell } from '@asb/context' import { useWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { Table, makeDateSorter, makeNumericSorter } from '@components/Table' import { Table, makeNumericSorter, makeColumn, makeDateColumn } from '@components/Table'
import { invokeWebApiWrapperAsync, downloadFile } from '@components/factory' import { invokeWebApiWrapperAsync, downloadFile } from '@components/factory'
import { formatDate, periodToString } from '@utils' import { periodToString } from '@utils'
import { ReportService } from '@api' import { ReportService } from '@api'
const imgPaths = { const imgPaths = {
@ -15,10 +15,7 @@ const imgPaths = {
} }
const columns = [ const columns = [
{ makeColumn('Название', 'name', {
title: 'Название',
dataIndex: 'name',
key: 'name',
render: (name, report) => ( render: (name, report) => (
<Button <Button
type={'link'} type={'link'}
@ -29,31 +26,14 @@ const columns = [
{name} {name}
</Button> </Button>
), ),
}, { }),
title: <Tooltip title={'Дата формирования'}>Сформирован</Tooltip>, makeDateColumn(<Tooltip title={'Дата формирования'}>Сформирован</Tooltip>, 'date'),
dataIndex: 'date', makeDateColumn(<Tooltip title={'Дата начала периода рапорта'}>С</Tooltip>, 'begin'),
key: 'date', makeDateColumn(<Tooltip title={'Дата окончания периода рапорта'}>По</Tooltip>, 'end'),
sorter: makeDateSorter('date'), makeColumn(<Tooltip title={'шаг сетки графиков'}>шаг, сек</Tooltip>, 'step', {
render: (date) => formatDate(date),
}, {
title: <Tooltip title={'Дата начала периода рапорта'}>С</Tooltip>,
dataIndex: 'begin',
key: 'begin',
sorter: makeDateSorter('begin'),
render: (date) => formatDate(date),
}, {
title: <Tooltip title={'Дата окончания периода рапорта'}>По</Tooltip>,
dataIndex: 'end',
key: 'end',
sorter: makeDateSorter('end'),
render: (date) => formatDate(date),
}, {
title: <Tooltip title={'шаг сетки графиков'}>шаг, сек</Tooltip>,
dataIndex: 'step',
key: 'step',
sorter: makeNumericSorter('step'), sorter: makeNumericSorter('step'),
render: step => periodToString(step), render: step => periodToString(step),
}, }),
] ]
export const Reports = memo(() => { export const Reports = memo(() => {
@ -84,6 +64,7 @@ export const Reports = memo(() => {
dataSource={reports} dataSource={reports}
pagination={{ pageSize: 13 }} pagination={{ pageSize: 13 }}
tableName={'reports'} tableName={'reports'}
scroll={{ x: true }}
/> />
</LoaderPortal> </LoaderPortal>
) )

View File

@ -0,0 +1,23 @@
import { Outlet } from 'react-router-dom'
import { memo, useMemo } from 'react'
import { RootPathContext, useRootPath } from '@asb/context'
import { wrapPrivateComponent } from '@utils'
const Reports = memo(() => {
const root = useRootPath()
const rootPath = useMemo(() => `${root}/reports`, [root])
return (
<RootPathContext.Provider value={rootPath}>
<Outlet />
</RootPathContext.Provider>
)
})
export default wrapPrivateComponent(Reports, {
requirements: [],
title: 'Рапорта',
route: 'reports/*',
key: 'reports',
})

View File

@ -33,7 +33,8 @@ export const makeMessageColumns = (idWell) => [
<Tooltip title={'Нажмите для перехода в архив'}> <Tooltip title={'Нажмите для перехода в архив'}>
<Link <Link
style={{ color: 'inherit'}} style={{ color: 'inherit'}}
to={`/well/${idWell}/telemetry/archive?range=1800&start=${moment(item?.date).subtract(3, 'minute').local().toISOString()}`}> to={`/well/${idWell}/telemetry/archive?range=1800&start=${moment(item?.date).subtract(3, 'minute').local().toISOString()}`}
>
<LinkOutlined /> <LinkOutlined />
&nbsp; &nbsp;
{depth.toFixed(2)} {depth.toFixed(2)}
@ -139,6 +140,7 @@ const Messages = memo(() => {
}} }}
rowKey={(record) => record.id} rowKey={(record) => record.id}
tableName={'messages'} tableName={'messages'}
scroll={{ x: true }}
/> />
</LoaderPortal> </LoaderPortal>
</> </>

View File

@ -53,7 +53,7 @@ export const DrillerList = memo(({ loading, drillers, onChange }) => {
centered centered
width={500} width={500}
footer={null} footer={null}
visible={showModal} open={showModal}
onCancel={onModalCancel} onCancel={onModalCancel}
title={'Список бурильщиков'} title={'Список бурильщиков'}
> >

View File

@ -100,7 +100,7 @@ export const DrillerSchedule = memo(({ drillers, loading, onChange }) => {
centered centered
width={1600} width={1600}
footer={null} footer={null}
visible={modalVisible} open={modalVisible}
onCancel={onModalCancel} onCancel={onModalCancel}
title={'Настройка бурильщиков и расписаний'} title={'Настройка бурильщиков и расписаний'}
> >

View File

@ -42,7 +42,7 @@ export const OperationsTable = memo(({ data, height, ...other }) => (
columns={columns} columns={columns}
dataSource={data} dataSource={data}
tableName={'well_telemetry_detected_operations'} tableName={'well_telemetry_detected_operations'}
scroll={{ y: height ?? '70vh', scrollToFirstRowOnChange: true }} scroll={{ x: true, y: height ?? '70vh', scrollToFirstRowOnChange: true }}
/> />
</div> </div>
)) ))

View File

@ -96,7 +96,7 @@ export const TargetEditor = memo(({ loading, onChange }) => {
centered centered
width={1000} width={1000}
footer={null} footer={null}
visible={showModal} open={showModal}
onCancel={onModalCancel} onCancel={onModalCancel}
title={'Цели бурения'} title={'Цели бурения'}
> >

View File

@ -1,11 +1,12 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Empty, InputNumber, Select } from 'antd' import { InputNumber, Select } from 'antd'
import moment from 'moment' import moment from 'moment'
import { useWell } from '@asb/context' import { useWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { DateRangeWrapper } from '@components/Table' import { DateRangeWrapper } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { unique } from '@utils/filters'
import { getPermissions, arrayOrDefault, range, wrapPrivateComponent, pretify } from '@utils' import { getPermissions, arrayOrDefault, range, wrapPrivateComponent, pretify } from '@utils'
import { DetectedOperationService, DrillerService, TelemetryDataSaubService } from '@api' import { DetectedOperationService, DrillerService, TelemetryDataSaubService } from '@api'
@ -16,7 +17,6 @@ import OperationsChart from './OperationsChart'
import OperationsTable from './OperationsTable' import OperationsTable from './OperationsTable'
import '@styles/detected_operations.less' import '@styles/detected_operations.less'
import { unique } from '@asb/utils/filters'
const Operations = memo(() => { const Operations = memo(() => {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
@ -161,11 +161,4 @@ const Operations = memo(() => {
) )
}) })
export default wrapPrivateComponent(Operations, { export default wrapPrivateComponent(Operations, { requirements: ['DetectedOperation.get', 'TelemetryDataSaub.get'] })
requirements: [
'DetectedOperation.get',
'TelemetryDataSaub.get',
],
title: 'Операции',
route: 'operations',
})

View File

@ -80,7 +80,7 @@ export const SetpointSender = memo(({ onClose, visible, setpointNames }) => {
<Modal <Modal
width={800} width={800}
title={'Рекомендовать уставки'} title={'Рекомендовать уставки'}
visible={visible} open={visible}
onCancel={onClose} onCancel={onClose}
onOk={onModalOk} onOk={onModalOk}
okText={'Отправить'} okText={'Отправить'}

View File

@ -38,7 +38,7 @@ export const SetpointViewer = memo(({ setpoint, visible, onClose, setpointNames
width={800} width={800}
title={`Уставка от ${date}`} title={`Уставка от ${date}`}
onCancel={onClose} onCancel={onClose}
visible={visible} open={visible}
footer={null} footer={null}
> >
<Grid> <Grid>

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