forked from ddrilling/asb_cloud_front
* Переработан LayoutPortal
* Переработан профиль пользователя * Переработана система организации ссылок меню * Новый LayoutPortal добавлен на все страницы * Изменён редирект со страницы загрузки файла
This commit is contained in:
parent
60118f9327
commit
bd8962df26
@ -1,28 +0,0 @@
|
|||||||
import { memo, ReactNode } from 'react'
|
|
||||||
import { Link, useLocation } from 'react-router-dom'
|
|
||||||
import { Button, Layout, LayoutProps } from 'antd'
|
|
||||||
|
|
||||||
import PageHeader from '@components/PageHeader'
|
|
||||||
|
|
||||||
export type AdminLayoutPortalProps = LayoutProps & {
|
|
||||||
title?: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AdminLayoutPortal = memo<AdminLayoutPortalProps>(({ title, ...props }) => {
|
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout.Content>
|
|
||||||
<PageHeader isAdmin title={title} style={{ backgroundColor: '#900' }}>
|
|
||||||
<Button size={'large'}>
|
|
||||||
<Link to={'/'}>Вернуться на сайт</Link>
|
|
||||||
</Button>
|
|
||||||
</PageHeader>
|
|
||||||
<Layout>
|
|
||||||
<Layout.Content className={'site-layout-background sheet'} {...props}/>
|
|
||||||
</Layout>
|
|
||||||
</Layout.Content>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default AdminLayoutPortal
|
|
@ -1,31 +0,0 @@
|
|||||||
import { 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
|
|
@ -1,5 +0,0 @@
|
|||||||
export * from './AdminLayoutPortal'
|
|
||||||
export * from './LayoutPortal'
|
|
||||||
|
|
||||||
export type { AdminLayoutPortalProps } from './AdminLayoutPortal'
|
|
||||||
export type { LayoutPortalProps } from './LayoutPortal'
|
|
110
src/components/LayoutPortal.tsx
Normal file
110
src/components/LayoutPortal.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { Button, Layout, LayoutProps, Menu, SiderProps } from 'antd'
|
||||||
|
import { HTMLProps, Key, memo, ReactNode, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { ItemType } from 'antd/lib/menu/hooks/useItems'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import {
|
||||||
|
ApartmentOutlined,
|
||||||
|
CodeOutlined,
|
||||||
|
HomeOutlined,
|
||||||
|
MenuFoldOutlined,
|
||||||
|
MenuUnfoldOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
|
||||||
|
import PageHeader from '@components/PageHeader'
|
||||||
|
import { UserMenu, UserMenuProps } from '@components/UserMenu'
|
||||||
|
import { WellTreeSelector, WellTreeSelectorProps } from '@components/selectors/WellTreeSelector'
|
||||||
|
import { isURLAvailable, wrapPrivateComponent } from '@utils'
|
||||||
|
|
||||||
|
import '@styles/layout.less'
|
||||||
|
|
||||||
|
const { Content, Sider } = Layout
|
||||||
|
|
||||||
|
export type LayoutPortalProps = HTMLProps<HTMLDivElement> & {
|
||||||
|
title?: ReactNode
|
||||||
|
noSheet?: boolean
|
||||||
|
showSelector?: boolean
|
||||||
|
selectorProps?: WellTreeSelectorProps
|
||||||
|
sider?: boolean | JSX.Element
|
||||||
|
siderProps?: SiderProps & { userMenuProps?: UserMenuProps }
|
||||||
|
isAdmin?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeItem = (title: string, key: Key, icon: JSX.Element, label?: ReactNode, onClick?: () => void) => ({ icon, key, title, label: label ?? title, onClick })
|
||||||
|
|
||||||
|
const _LayoutPortal = memo<LayoutPortalProps>(({ isAdmin, title, noSheet, showSelector, selectorProps, sider, siderProps, ...props }) => {
|
||||||
|
const [menuCollapsed, setMenuCollapsed] = useState<boolean>(true)
|
||||||
|
const [wellsTreeOpen, setWellsTreeOpen] = useState<boolean>(false)
|
||||||
|
const [userMenuOpen, setUserMenuOpen] = useState<boolean>(false)
|
||||||
|
const [currentWell, setCurrentWell] = useState<string>('')
|
||||||
|
|
||||||
|
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} collapsed={menuCollapsed} trigger={null} collapsible className={`menu-sider ${siderProps?.className || ''}`}>
|
||||||
|
<div className={'sider-content'}>
|
||||||
|
<button className={'sider-toogle'} onClick={() => setMenuCollapsed((prev) => !prev)}>
|
||||||
|
{menuCollapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||||
|
</button>
|
||||||
|
<div className={'scrollable hide-slider'}>
|
||||||
|
{sider}
|
||||||
|
</div>
|
||||||
|
<Menu
|
||||||
|
mode={'inline'}
|
||||||
|
items={menuItems}
|
||||||
|
inlineCollapsed
|
||||||
|
theme={'dark'}
|
||||||
|
selectable={false}
|
||||||
|
/>
|
||||||
|
<UserMenu
|
||||||
|
open={userMenuOpen}
|
||||||
|
onClose={() => setUserMenuOpen(false)}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
{...siderProps?.userMenuProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Sider>
|
||||||
|
)}
|
||||||
|
<Layout className={'page-content'}>
|
||||||
|
<PageHeader title={title}>
|
||||||
|
{isAdmin ? (
|
||||||
|
<Button size={'large'}>
|
||||||
|
<Link to={'/'}>Вернуться на сайт</Link>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<WellTreeSelector
|
||||||
|
open={wellsTreeOpen}
|
||||||
|
onClose={() => setWellsTreeOpen(false)}
|
||||||
|
{...selectorProps}
|
||||||
|
onChange={(well) => setCurrentWell(well ?? 'Выберите месторождение')}
|
||||||
|
/>
|
||||||
|
<Button onClick={() => setWellsTreeOpen((prev) => !prev)}>{currentWell}</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</PageHeader>
|
||||||
|
<Content>
|
||||||
|
<div {...props} className={`${noSheet ? '' : 'site-layout-background sheet'} ${props.className}`} />
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const LayoutPortal = wrapPrivateComponent(_LayoutPortal, {
|
||||||
|
requirements: ['Deposit.get'],
|
||||||
|
})
|
||||||
|
|
||||||
|
export default LayoutPortal
|
@ -4,27 +4,24 @@ import { Link } from 'react-router-dom'
|
|||||||
import { BasicProps } from 'antd/lib/layout/layout'
|
import { BasicProps } from 'antd/lib/layout/layout'
|
||||||
|
|
||||||
import { headerHeight } from '@utils'
|
import { headerHeight } from '@utils'
|
||||||
import { UserMenu } from './UserMenu'
|
|
||||||
|
|
||||||
import Logo from '@images/Logo'
|
import Logo from '@images/Logo'
|
||||||
|
|
||||||
|
import '@styles/layout.less'
|
||||||
|
|
||||||
export type PageHeaderProps = BasicProps & {
|
export type PageHeaderProps = BasicProps & {
|
||||||
title?: string
|
title?: string
|
||||||
isAdmin?: boolean
|
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Мониторинг', isAdmin, children, ...other }) => (
|
export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Мониторинг', children, ...other }) => (
|
||||||
<Layout>
|
<Layout.Header className={'header'} {...other}>
|
||||||
<Layout.Header className={'header'} {...other}>
|
<Link to={'/'} style={{ height: headerHeight }}>
|
||||||
<Link to={'/'} style={{ height: headerHeight }}>
|
<Logo />
|
||||||
<Logo />
|
</Link>
|
||||||
</Link>
|
<h1 className={'title'}>{title}</h1>
|
||||||
<h1 className={'title'}>{title}</h1>
|
{children}
|
||||||
{children}
|
</Layout.Header>
|
||||||
<UserMenu isAdmin={isAdmin} />
|
|
||||||
</Layout.Header>
|
|
||||||
</Layout>
|
|
||||||
))
|
))
|
||||||
|
|
||||||
export default PageHeader
|
export default PageHeader
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
import { memo, ReactElement } from 'react'
|
|
||||||
|
|
||||||
import { isURLAvailable } from '@utils'
|
|
||||||
|
|
||||||
export type PrivateContentProps = {
|
|
||||||
absolutePath: string
|
|
||||||
children?: ReactElement<any, any>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PrivateContent = memo<PrivateContentProps>(({ absolutePath, children = null }) =>
|
|
||||||
isURLAvailable(absolutePath) ? children : null
|
|
||||||
)
|
|
||||||
|
|
||||||
export default PrivateContent
|
|
@ -1,19 +0,0 @@
|
|||||||
import { memo } from 'react'
|
|
||||||
import { Navigate, Route, RouteProps } from 'react-router-dom'
|
|
||||||
|
|
||||||
import { isURLAvailable } from '@utils'
|
|
||||||
|
|
||||||
import { getDefaultRedirectPath } from './PrivateRoutes'
|
|
||||||
|
|
||||||
export type PrivateDefaultRouteProps = RouteProps & {
|
|
||||||
urls: string[]
|
|
||||||
elseRedirect?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PrivateDefaultRoute = memo<PrivateDefaultRouteProps>(({ elseRedirect, urls, ...other }) => (
|
|
||||||
<Route {...other} path={'/'} element={(
|
|
||||||
<Navigate replace to={urls.find((url) => isURLAvailable(url)) ?? elseRedirect ?? getDefaultRedirectPath()} />
|
|
||||||
)} />
|
|
||||||
))
|
|
||||||
|
|
||||||
export default PrivateDefaultRoute
|
|
@ -1,75 +0,0 @@
|
|||||||
import { join } from 'path'
|
|
||||||
import { Menu, MenuProps } from 'antd'
|
|
||||||
import { ItemType } from 'antd/lib/menu/hooks/useItems'
|
|
||||||
import { Link, LinkProps } from 'react-router-dom'
|
|
||||||
import { Children, isValidElement, memo, ReactNode, RefAttributes, useMemo } from 'react'
|
|
||||||
|
|
||||||
import { useRootPath } from '@asb/context'
|
|
||||||
import { getTabname, hasPermission, PrivateComponent, PrivateProps } from '@utils'
|
|
||||||
|
|
||||||
export type PrivateMenuProps = MenuProps & { root?: string }
|
|
||||||
|
|
||||||
export type PrivateMenuLinkProps = Partial<ItemType> & Omit<LinkProps, 'to'> & RefAttributes<HTMLAnchorElement> & {
|
|
||||||
icon?: ReactNode
|
|
||||||
danger?: boolean
|
|
||||||
title?: ReactNode
|
|
||||||
content?: PrivateComponent<any>
|
|
||||||
path?: string
|
|
||||||
visible?: boolean
|
|
||||||
permissions?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PrivateMenuLink = memo<PrivateMenuLinkProps>(({ content, path = '', title, ...other }) => (
|
|
||||||
<Link to={path} {...other}>{title ?? content?.title}</Link>
|
|
||||||
))
|
|
||||||
|
|
||||||
const PrivateMenuMain = memo<PrivateMenuProps>(({ selectable, mode, selectedKeys, root, children, ...other }) => {
|
|
||||||
const rootContext = useRootPath()
|
|
||||||
const rootPath = useMemo(() => root ?? rootContext ?? '', [root, rootContext])
|
|
||||||
|
|
||||||
const tab = getTabname()
|
|
||||||
const keys = useMemo(() => selectedKeys ?? (tab ? [tab] : []), [selectedKeys, tab])
|
|
||||||
|
|
||||||
const items = useMemo(() => Children.map(children, (child) => {
|
|
||||||
if (!child || !isValidElement<PrivateMenuLinkProps>(child))
|
|
||||||
return null
|
|
||||||
const content: PrivateProps | undefined = child.props.content
|
|
||||||
const visible: boolean | undefined = child.props.visible
|
|
||||||
|
|
||||||
if (visible === false) return null
|
|
||||||
let key
|
|
||||||
if (content?.key)
|
|
||||||
key = content.key
|
|
||||||
else if (content?.route)
|
|
||||||
key = content.route
|
|
||||||
else if (child.key) {
|
|
||||||
key = child.key?.toString()
|
|
||||||
key = key.slice(key.lastIndexOf('$') + 1)
|
|
||||||
} else return null
|
|
||||||
|
|
||||||
const permissions = child.props.permissions ?? content?.requirements
|
|
||||||
const path = child.props.path ?? join(rootPath, key)
|
|
||||||
|
|
||||||
if (visible || hasPermission(permissions))
|
|
||||||
return {
|
|
||||||
...child.props,
|
|
||||||
key,
|
|
||||||
label: <PrivateMenuLink {...child.props} path={path} />,
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})?.filter((v) => v) ?? [], [children, rootPath])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu
|
|
||||||
selectable={selectable ?? true}
|
|
||||||
mode={mode ?? 'horizontal'}
|
|
||||||
selectedKeys={keys}
|
|
||||||
items={items as ItemType[]}
|
|
||||||
{...other}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const PrivateMenu = Object.assign(PrivateMenuMain, { Link: PrivateMenuLink })
|
|
||||||
|
|
||||||
export default PrivateMenu
|
|
@ -1,37 +0,0 @@
|
|||||||
import { join } from 'path'
|
|
||||||
import { Menu, MenuItemProps } from 'antd'
|
|
||||||
import { memo, NamedExoticComponent } from 'react'
|
|
||||||
import { Link, useLocation } from 'react-router-dom'
|
|
||||||
|
|
||||||
import { isURLAvailable } from '@utils'
|
|
||||||
|
|
||||||
export type PrivateMenuItemProps = MenuItemProps & {
|
|
||||||
root: string
|
|
||||||
path: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PrivateMenuItemLinkProps = MenuItemProps & {
|
|
||||||
root?: string
|
|
||||||
path: string
|
|
||||||
title: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PrivateMenuItemLink = memo<PrivateMenuItemLinkProps>(({ root = '', path, title, ...other }) => {
|
|
||||||
const location = useLocation()
|
|
||||||
return (
|
|
||||||
<PrivateMenuItem key={path} root={root} path={path} {...other}>
|
|
||||||
<Link to={join(root, path)}>{title}</Link>
|
|
||||||
</PrivateMenuItem>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const PrivateMenuItem: NamedExoticComponent<PrivateMenuItemProps> & {
|
|
||||||
Link: NamedExoticComponent<PrivateMenuItemLinkProps>
|
|
||||||
} = Object.assign(memo<PrivateMenuItemProps>(({ root, path, ...other }) =>
|
|
||||||
<Menu.Item key={path} hidden={!isURLAvailable(join(root, path))} {...other} />
|
|
||||||
), {
|
|
||||||
Link: PrivateMenuItemLink
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export default PrivateMenuItem
|
|
@ -1,30 +0,0 @@
|
|||||||
import { join } from 'path'
|
|
||||||
import { memo, ReactNode } from 'react'
|
|
||||||
import { Navigate, Route, RouteProps } from 'react-router-dom'
|
|
||||||
|
|
||||||
import { getUserId, isURLAvailable } from '@utils'
|
|
||||||
|
|
||||||
export type PrivateRouteProps = RouteProps & {
|
|
||||||
root?: string
|
|
||||||
path: string
|
|
||||||
children?: ReactNode
|
|
||||||
redirect?: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultRedirect = (
|
|
||||||
<Navigate to={getUserId() ? '/access_denied' : '/login'} />
|
|
||||||
)
|
|
||||||
|
|
||||||
export const PrivateRoute = memo<PrivateRouteProps>(({ root = '', path, children, redirect = defaultRedirect, ...other }) => {
|
|
||||||
const available = isURLAvailable(join(root, path))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Route
|
|
||||||
{...other}
|
|
||||||
path={path}
|
|
||||||
element={available ? children : redirect}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default PrivateRoute
|
|
@ -1,67 +0,0 @@
|
|||||||
import { join } from 'path'
|
|
||||||
import { Navigate, Route, Routes, RoutesProps } from 'react-router-dom'
|
|
||||||
import { Children, cloneElement, memo, ReactElement, ReactNode, useCallback, useMemo } from 'react'
|
|
||||||
|
|
||||||
import { useRootPath } from '@asb/context'
|
|
||||||
import { getUserId, isURLAvailable } from '@utils'
|
|
||||||
|
|
||||||
export type PrivateRoutesProps = RoutesProps & {
|
|
||||||
root?: string
|
|
||||||
redirect?: ReactNode
|
|
||||||
elseRedirect?: string | string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getDefaultRedirectPath = () => getUserId() ? '/access_denied' : '/login'
|
|
||||||
|
|
||||||
export const defaultRedirect = (
|
|
||||||
<Navigate to={getDefaultRedirectPath()} />
|
|
||||||
)
|
|
||||||
|
|
||||||
export const PrivateRoutes = memo<PrivateRoutesProps>(({ root, elseRedirect, redirect = defaultRedirect, children }) => {
|
|
||||||
const rootContext = useRootPath()
|
|
||||||
const rootPath = useMemo(() => root ?? rootContext ?? '', [root, rootContext])
|
|
||||||
|
|
||||||
const toAbsolute = useCallback((path: string) => path.startsWith('/') ? path : join(rootPath, path), [rootPath])
|
|
||||||
|
|
||||||
const items = useMemo(() => Children.map(children, (child) => {
|
|
||||||
const element = child as ReactElement
|
|
||||||
let key = element.key?.toString()
|
|
||||||
if (!key) return <></>
|
|
||||||
key = key.slice(key.lastIndexOf('$') + 1).replaceAll('=2', ':')
|
|
||||||
// Ключ автоматический преобразуется в "(.+)\$ключ"
|
|
||||||
// Все ":" в ключе заменяются на "=2"
|
|
||||||
// TODO: улучшить метод нормализации ключа
|
|
||||||
const path = toAbsolute(key)
|
|
||||||
return (
|
|
||||||
<Route
|
|
||||||
key={key}
|
|
||||||
path={path}
|
|
||||||
element={isURLAvailable(path) ? cloneElement(element) : redirect}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}) ?? [], [children, redirect, toAbsolute])
|
|
||||||
|
|
||||||
const defaultRoute = useMemo(() => {
|
|
||||||
const routes: string[] = []
|
|
||||||
if (Array.isArray(elseRedirect))
|
|
||||||
routes.push(...elseRedirect)
|
|
||||||
else if(elseRedirect)
|
|
||||||
routes.push(elseRedirect)
|
|
||||||
|
|
||||||
routes.push(...items.map((elm) => elm?.props?.path))
|
|
||||||
|
|
||||||
const firstAvailableRoute = routes.find((path) => path && isURLAvailable(path))
|
|
||||||
return firstAvailableRoute ? toAbsolute(firstAvailableRoute) : getDefaultRedirectPath()
|
|
||||||
}, [items, elseRedirect, toAbsolute])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Routes>
|
|
||||||
{items}
|
|
||||||
<Route path={'/'} element={(
|
|
||||||
<Navigate to={defaultRoute} />
|
|
||||||
)}/>
|
|
||||||
</Routes>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export default PrivateRoutes
|
|
@ -1,13 +0,0 @@
|
|||||||
export { PrivateRoute, defaultRedirect } from './PrivateRoute'
|
|
||||||
export { PrivateContent } from './PrivateContent' // TODO: Remove
|
|
||||||
export { PrivateMenuItem, PrivateMenuItemLink } from './PrivateMenuItem' // TODO: Remove
|
|
||||||
export { PrivateDefaultRoute } from './PrivateDefaultRoute'
|
|
||||||
export { PrivateMenu, PrivateMenuLink } from './PrivateMenu'
|
|
||||||
export { PrivateRoutes } from './PrivateRoutes'
|
|
||||||
|
|
||||||
export type { PrivateRouteProps } from './PrivateRoute'
|
|
||||||
export type { PrivateContentProps } from './PrivateContent' // TODO: Remove
|
|
||||||
export type { PrivateMenuItemProps, PrivateMenuItemLinkProps } from './PrivateMenuItem' // TODO: Remove
|
|
||||||
export type { PrivateDefaultRouteProps } from './PrivateDefaultRoute'
|
|
||||||
export type { PrivateMenuProps, PrivateMenuLinkProps } from './PrivateMenu'
|
|
||||||
export type { PrivateRoutesProps } from './PrivateRoutes'
|
|
97
src/components/PrivateWellMenu.tsx
Normal file
97
src/components/PrivateWellMenu.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
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 { useWell } from '@asb/context'
|
||||||
|
import { hasPermission, Permission } from '@utils'
|
||||||
|
|
||||||
|
type PrivateWellMenuItem = {
|
||||||
|
title: string
|
||||||
|
route: string
|
||||||
|
permissions: Permission | Permission[]
|
||||||
|
icon?: ReactNode
|
||||||
|
visible?: boolean
|
||||||
|
children?: PrivateWellMenuItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeItems = (items: PrivateWellMenuItem[], parentRoute: string, pathParser?: (path: string, parent: string) => string): ItemType[] => {
|
||||||
|
return items.map((item) => {
|
||||||
|
if (item.visible === false || !(item.visible === true || hasPermission(item.permissions))) return null
|
||||||
|
|
||||||
|
let route = item.route
|
||||||
|
if (pathParser)
|
||||||
|
route = pathParser(item.route, parentRoute)
|
||||||
|
else if (!item.route.startsWith('/') && parentRoute)
|
||||||
|
route = join(parentRoute, item.route)
|
||||||
|
|
||||||
|
const out: ItemType = {
|
||||||
|
key: route,
|
||||||
|
icon: item.icon,
|
||||||
|
title: item.title,
|
||||||
|
label: <Link to={route}>{item.title}</Link>,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
return {
|
||||||
|
...out,
|
||||||
|
children: makeItems(item.children, route, pathParser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeItemList = (items: PrivateWellMenuItem[], rootPath: string, wellId?: number): ItemType[] => {
|
||||||
|
const parser = (path: string, parent: string) => {
|
||||||
|
if (!path.startsWith('/'))
|
||||||
|
path = join(parent, path)
|
||||||
|
return path.replace(/\{wellId\}/, String(wellId))
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeItems(items, rootPath, parser)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeItem = (
|
||||||
|
title: string,
|
||||||
|
route: string,
|
||||||
|
permissions: Permission | Permission[],
|
||||||
|
icon?: ReactNode,
|
||||||
|
children?: PrivateWellMenuItem[],
|
||||||
|
visible?: boolean
|
||||||
|
): PrivateWellMenuItem => ({
|
||||||
|
title,
|
||||||
|
route,
|
||||||
|
icon,
|
||||||
|
permissions,
|
||||||
|
children,
|
||||||
|
visible,
|
||||||
|
})
|
||||||
|
|
||||||
|
export type PrivateWellMenuProps = MenuProps & {
|
||||||
|
items: PrivateWellMenuItem[]
|
||||||
|
rootPath?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrivateWellMenu = memo<PrivateWellMenuProps>(({ items, rootPath = '/', ...other }) => {
|
||||||
|
const [well] = useWell()
|
||||||
|
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
const menuItems = useMemo(() => makeItemList(items, rootPath, well.id), [items, rootPath, well.id])
|
||||||
|
|
||||||
|
const tabKeys = useMemo(() => {
|
||||||
|
const out = []
|
||||||
|
const rx = RegExp(/(?<!^)\//g)
|
||||||
|
let input = location.pathname
|
||||||
|
if (!input.endsWith('/')) input += '/'
|
||||||
|
let match: RegExpExecArray | null
|
||||||
|
while((match = rx.exec(input)) !== null)
|
||||||
|
out.push(input.slice(0, match.index))
|
||||||
|
return out
|
||||||
|
}, [location.pathname])
|
||||||
|
|
||||||
|
return <Menu items={menuItems} selectedKeys={tabKeys} {...other} />
|
||||||
|
})
|
@ -1,56 +1,110 @@
|
|||||||
import { memo, MouseEventHandler, useCallback, useState } from 'react'
|
import { memo, ReactNode, useCallback, useState } from 'react'
|
||||||
import { useNavigate, useLocation } from 'react-router-dom'
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
import { Button, Dropdown, DropDownProps } from 'antd'
|
import { Button, Collapse, Drawer, DrawerProps, Form, FormRule, Input, Popconfirm } from 'antd'
|
||||||
import { UserOutlined } from '@ant-design/icons'
|
import { useForm } from 'antd/lib/form/Form'
|
||||||
|
|
||||||
import { getUserLogin, removeUser } from '@utils'
|
import { useUser } from '@asb/context'
|
||||||
import { ChangePassword } from './ChangePassword'
|
import { Grid, GridItem } from '@components/Grid'
|
||||||
import { PrivateMenu } from './Private'
|
import LoaderPortal from '@components/LoaderPortal'
|
||||||
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
|
import { isURLAvailable, removeUser } from '@utils'
|
||||||
|
import { AuthService } from '@api'
|
||||||
|
|
||||||
import AdminPanel from '@pages/AdminPanel'
|
import '@styles/user_menu.less'
|
||||||
|
|
||||||
type UserMenuProps = Omit<DropDownProps, 'overlay'> & { isAdmin?: boolean }
|
export type UserMenuProps = DrawerProps & {
|
||||||
|
isAdmin?: boolean
|
||||||
|
additional?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
export const UserMenu = memo<UserMenuProps>(({ isAdmin, ...other }) => {
|
type ChangePasswordForm = {
|
||||||
const [isModalVisible, setIsModalVisible] = useState<boolean>(false)
|
'new-password': string
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPasswordRules: FormRule[] = [{ required: true, message: 'Пожалуйста, введите новый пароль!' }]
|
||||||
|
|
||||||
|
const confirmPasswordRules: FormRule[] = [({ getFieldValue }) => ({ validator(_, value: string) {
|
||||||
|
if (value !== getFieldValue('new-password'))
|
||||||
|
return Promise.reject('Пароли не совпадают!')
|
||||||
|
return Promise.resolve()
|
||||||
|
}})]
|
||||||
|
|
||||||
|
export const UserMenu = memo<UserMenuProps>(({ isAdmin, additional, ...other }) => {
|
||||||
|
const [showLoader, setShowLoader] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const [changeLoginForm] = useForm<ChangePasswordForm>()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
const onChangePasswordClick: MouseEventHandler = useCallback((e) => {
|
const navigateTo = useCallback((to: string) => navigate(to, { state: { from: location.pathname }}), [navigate, location.pathname])
|
||||||
setIsModalVisible(true)
|
|
||||||
e.preventDefault()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onChangePasswordOk = useCallback(() => {
|
const onChangePasswordOk = useCallback(() => invokeWebApiWrapperAsync(
|
||||||
setIsModalVisible(false)
|
async (values: any) => {
|
||||||
navigate('/login', { state: { from: location.pathname }})
|
await AuthService.changePassword(user.id ?? -1, `${values['new-password']}`)
|
||||||
}, [navigate, location])
|
removeUser()
|
||||||
|
navigateTo('/login')
|
||||||
|
},
|
||||||
|
setShowLoader,
|
||||||
|
`Не удалось сменить пароль пользователя ${user.login}`,
|
||||||
|
{ actionName: 'Смена пароля пользователя' },
|
||||||
|
), [navigateTo])
|
||||||
|
|
||||||
|
const logout = useCallback(() => {
|
||||||
|
removeUser()
|
||||||
|
navigateTo('/login')
|
||||||
|
}, [navigateTo])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Drawer
|
||||||
<Dropdown
|
closable
|
||||||
{...other}
|
placement={'left'}
|
||||||
placement={'bottomRight'}
|
className={'user-menu'}
|
||||||
overlay={(
|
title={'Профиль пользователя'}
|
||||||
<PrivateMenu mode={'vertical'} style={{ textAlign: 'right' }}>
|
{...other}
|
||||||
{isAdmin ? (
|
>
|
||||||
<PrivateMenu.Link visible key={'/'} path={'/'} title={'Вернуться на сайт'} />
|
<div className={'profile-links'}>
|
||||||
) : (
|
{isAdmin ? (
|
||||||
<PrivateMenu.Link path={'/admin'} content={AdminPanel} />
|
<Button onClick={() => navigateTo('/')}>Вернуться на сайт</Button>
|
||||||
)}
|
) : isURLAvailable('/admin') && (
|
||||||
<PrivateMenu.Link visible key={'change_password'} onClick={onChangePasswordClick} title={'Сменить пароль'} />
|
<Button onClick={() => navigateTo('/admin')}>Панель администратора</Button>
|
||||||
<PrivateMenu.Link visible key={'login'} path={'/login'} onClick={removeUser} title={'Выход'} />
|
|
||||||
</PrivateMenu>
|
|
||||||
)}
|
)}
|
||||||
>
|
<Button type={'ghost'} onClick={logout}>Выход</Button>
|
||||||
<Button icon={<UserOutlined/>}>{getUserLogin()}</Button>
|
</div>
|
||||||
</Dropdown>
|
<Collapse>
|
||||||
<ChangePassword
|
<Collapse.Panel header={'Данные'} key={'summary'}>
|
||||||
visible={isModalVisible}
|
<Grid>
|
||||||
onOk={onChangePasswordOk}
|
<GridItem row={1} col={1}>Логин:</GridItem>
|
||||||
onCancel={() => setIsModalVisible(false)}
|
<GridItem row={1} col={2}>{user.login}</GridItem>
|
||||||
/>
|
<GridItem row={2} col={1}>Фамилия:</GridItem>
|
||||||
</>
|
<GridItem row={2} col={2}>{user.surname}</GridItem>
|
||||||
|
<GridItem row={3} col={1}>Имя:</GridItem>
|
||||||
|
<GridItem row={3} col={2}>{user.name}</GridItem>
|
||||||
|
<GridItem row={4} col={1}>Отчество:</GridItem>
|
||||||
|
<GridItem row={4} col={2}>{user.patronymic}</GridItem>
|
||||||
|
<GridItem row={5} col={1}>E-mail:</GridItem>
|
||||||
|
<GridItem row={5} col={2}>{user.email}</GridItem>
|
||||||
|
</Grid>
|
||||||
|
</Collapse.Panel>
|
||||||
|
<Collapse.Panel header={'Смена пароля'} key={'change-password'}>
|
||||||
|
<LoaderPortal show={showLoader}>
|
||||||
|
<Form name={'change-password'} form={changeLoginForm} autoComplete={'off'} onFinish={onChangePasswordOk}>
|
||||||
|
<Form.Item name={'new-password'} label={'Новый пароль'} rules={newPasswordRules}>
|
||||||
|
<Input.Password placeholder={'Впишите новый пароль'} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item required name={'confirm-password'} rules={confirmPasswordRules} label={'Подтверждение пароля'}>
|
||||||
|
<Input.Password />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Popconfirm title={'Вы уверены что хотите сменить пароль?'} onConfirm={changeLoginForm.submit} placement={'topRight'}>
|
||||||
|
<Button type={'primary'}>Сменить</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</LoaderPortal>
|
||||||
|
</Collapse.Panel>
|
||||||
|
{additional}
|
||||||
|
</Collapse>
|
||||||
|
</Drawer>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -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,26 +169,22 @@ 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>
|
<Skeleton active loading={showLoader}>
|
||||||
<Drawer visible={visible} mask={false} onClose={() => setVisible(false)}>
|
<Tree
|
||||||
<Typography.Title level={3}>Список скважин</Typography.Title>
|
{...other}
|
||||||
<Skeleton active loading={showLoader}>
|
showIcon
|
||||||
<Tree
|
selectedKeys={selected}
|
||||||
{...other}
|
treeData={wellsTree}
|
||||||
showIcon
|
onSelect={onSelect}
|
||||||
selectedKeys={selected}
|
onExpand={setExpanded}
|
||||||
treeData={wellsTree}
|
expandedKeys={expanded}
|
||||||
onSelect={onSelect}
|
/>
|
||||||
onExpand={setExpanded}
|
</Skeleton>
|
||||||
expandedKeys={expanded}
|
</Drawer>
|
||||||
/>
|
|
||||||
</Skeleton>
|
|
||||||
</Drawer>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
46
src/pages/AdminPanel/AdminNavigationMenu.jsx
Normal file
46
src/pages/AdminPanel/AdminNavigationMenu.jsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
import {
|
||||||
|
ApiOutlined,
|
||||||
|
BankOutlined,
|
||||||
|
BranchesOutlined,
|
||||||
|
DashboardOutlined,
|
||||||
|
FileSearchOutlined,
|
||||||
|
FolderOutlined,
|
||||||
|
IdcardOutlined,
|
||||||
|
MonitorOutlined,
|
||||||
|
TeamOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
|
||||||
|
import { makeItem, PrivateWellMenu } from '@components/PrivateWellMenu'
|
||||||
|
|
||||||
|
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 />),
|
||||||
|
makeItem('API', '/swagger/index.html', [], <ApiOutlined />),
|
||||||
|
]
|
||||||
|
|
||||||
|
export const AdminNavigationMenu = memo((props) => (
|
||||||
|
<PrivateWellMenu
|
||||||
|
{...props}
|
||||||
|
items={menuItems}
|
||||||
|
rootPath={'/admin'}
|
||||||
|
inlineCollapsed={true}
|
||||||
|
selectable={false}
|
||||||
|
mode={'inline'}
|
||||||
|
theme={'dark'}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
export default AdminNavigationMenu
|
@ -1,12 +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 { Layout } from 'antd'
|
|
||||||
|
|
||||||
import { RootPathContext, useRootPath } from '@asb/context'
|
import { RootPathContext, useRootPath } from '@asb/context'
|
||||||
import { AdminLayoutPortal } from '@components/Layout'
|
import { LayoutPortal } from '@components/LayoutPortal'
|
||||||
import { PrivateMenu } from '@components/Private'
|
|
||||||
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
|
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
|
||||||
|
|
||||||
|
import AdminNavigationMenu from './AdminNavigationMenu'
|
||||||
import ClusterController from './ClusterController'
|
import ClusterController from './ClusterController'
|
||||||
import CompanyController from './CompanyController'
|
import CompanyController from './CompanyController'
|
||||||
import DepositController from './DepositController'
|
import DepositController from './DepositController'
|
||||||
@ -24,39 +23,22 @@ const AdminPanel = memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RootPathContext.Provider value={rootPath}>
|
<RootPathContext.Provider value={rootPath}>
|
||||||
<AdminLayoutPortal title={'Администраторская панель'}>
|
<LayoutPortal isAdmin title={'Администраторская панель'} sider={<AdminNavigationMenu />}>
|
||||||
<PrivateMenu>
|
<Routes>
|
||||||
<PrivateMenu.Link content={DepositController} />
|
<Route index element={<Navigate to={VisitLog.route} replace />} />
|
||||||
<PrivateMenu.Link content={ClusterController} />
|
<Route path={'*'} element={<NoAccessComponent />} />
|
||||||
<PrivateMenu.Link content={WellController} />
|
<Route path={DepositController.route} element={<DepositController />} />
|
||||||
<PrivateMenu.Link content={UserController} />
|
<Route path={ClusterController.route} element={<ClusterController />} />
|
||||||
<PrivateMenu.Link content={CompanyController} />
|
<Route path={WellController.route} element={<WellController />} />
|
||||||
<PrivateMenu.Link content={CompanyTypeController} />
|
<Route path={UserController.route} element={<UserController />} />
|
||||||
<PrivateMenu.Link content={RoleController} />
|
<Route path={CompanyController.route} element={<CompanyController />} />
|
||||||
<PrivateMenu.Link content={PermissionController} />
|
<Route path={CompanyTypeController.route} element={<CompanyTypeController />} />
|
||||||
<PrivateMenu.Link content={Telemetry} />
|
<Route path={RoleController.route} element={<RoleController />} />
|
||||||
<PrivateMenu.Link content={VisitLog} />
|
<Route path={PermissionController.route} element={<PermissionController />} />
|
||||||
</PrivateMenu>
|
<Route path={Telemetry.route} element={<Telemetry />} />
|
||||||
|
<Route path={VisitLog.route} element={<VisitLog />} />
|
||||||
<Layout>
|
</Routes>
|
||||||
<Layout.Content className={'site-layout-background'}>
|
</LayoutPortal>
|
||||||
<Routes>
|
|
||||||
<Route index element={<Navigate to={VisitLog.route} replace />} />
|
|
||||||
<Route path={'*'} element={<NoAccessComponent />} />
|
|
||||||
<Route path={DepositController.route} element={<DepositController />} />
|
|
||||||
<Route path={ClusterController.route} element={<ClusterController />} />
|
|
||||||
<Route path={WellController.route} element={<WellController />} />
|
|
||||||
<Route path={UserController.route} element={<UserController />} />
|
|
||||||
<Route path={CompanyController.route} element={<CompanyController />} />
|
|
||||||
<Route path={CompanyTypeController.route} element={<CompanyTypeController />} />
|
|
||||||
<Route path={RoleController.route} element={<RoleController />} />
|
|
||||||
<Route path={PermissionController.route} element={<PermissionController />} />
|
|
||||||
<Route path={Telemetry.route} element={<Telemetry />} />
|
|
||||||
<Route path={VisitLog.route} element={<VisitLog />} />
|
|
||||||
</Routes>
|
|
||||||
</Layout.Content>
|
|
||||||
</Layout>
|
|
||||||
</AdminLayoutPortal>
|
|
||||||
</RootPathContext.Provider>
|
</RootPathContext.Provider>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { useState, useEffect, memo } from 'react'
|
import { useState, useEffect, memo } from 'react'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
|
||||||
import { LayoutPortal } from '@components/Layout'
|
import { LayoutPortal } from '@components/LayoutPortal'
|
||||||
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 Cluster = memo(() => {
|
const Cluster = memo(() => {
|
||||||
const { idCluster } = useParams()
|
const { idCluster } = useParams()
|
||||||
@ -27,7 +27,7 @@ const Cluster = memo(() => {
|
|||||||
}, [idCluster])
|
}, [idCluster])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutPortal title={'Анализ скважин куста'}>
|
<LayoutPortal title={'Анализ скважин куста'} sider={true}>
|
||||||
<LoaderPortal show={showLoader}>
|
<LoaderPortal show={showLoader}>
|
||||||
<ClusterWells statsWells={data} />
|
<ClusterWells statsWells={data} />
|
||||||
</LoaderPortal>
|
</LoaderPortal>
|
||||||
|
@ -4,7 +4,7 @@ import { Link, useLocation } from 'react-router-dom'
|
|||||||
import { Popover, Badge } from 'antd'
|
import { Popover, Badge } from 'antd'
|
||||||
|
|
||||||
import { PointerIcon } from '@components/icons'
|
import { PointerIcon } from '@components/icons'
|
||||||
import { LayoutPortal } from '@components/Layout'
|
import { LayoutPortal } from '@components/LayoutPortal'
|
||||||
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'
|
||||||
@ -53,7 +53,6 @@ const Deposit = memo(() => {
|
|||||||
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,
|
||||||
}
|
}
|
||||||
@ -73,9 +72,9 @@ const Deposit = memo(() => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutPortal noSheet selector={selectorProps} title={'Месторождение'}>
|
<LayoutPortal noSheet showSelector selectorProps={selectorProps} title={'Месторождение'} sider={true}>
|
||||||
<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
|
||||||
|
@ -7,8 +7,6 @@ import { downloadFile, invokeWebApiWrapperAsync } from '@components/factory'
|
|||||||
import { wrapPrivateComponent } from '@utils'
|
import { wrapPrivateComponent } from '@utils'
|
||||||
import { FileService, WellService } from '@api'
|
import { FileService, WellService } from '@api'
|
||||||
|
|
||||||
import AccessDenied from './AccessDenied'
|
|
||||||
|
|
||||||
const { Paragraph, Text } = Typography
|
const { Paragraph, Text } = Typography
|
||||||
|
|
||||||
export const getLinkToFile = (fileInfo) => `/file_download/${fileInfo.idWell}/${fileInfo.id}`
|
export const getLinkToFile = (fileInfo) => `/file_download/${fileInfo.idWell}/${fileInfo.id}`
|
||||||
@ -104,4 +102,4 @@ FileDownload.displayName = 'FileDownloadMemo'
|
|||||||
export default wrapPrivateComponent(FileDownload, {
|
export default wrapPrivateComponent(FileDownload, {
|
||||||
requirements: ['File.get'],
|
requirements: ['File.get'],
|
||||||
route: 'file_download/:idWell/:idFile/*',
|
route: 'file_download/:idWell/:idFile/*',
|
||||||
}, <AccessDenied />)
|
})
|
||||||
|
78
src/pages/NavigationMenu.jsx
Normal file
78
src/pages/NavigationMenu.jsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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}'}
|
||||||
|
inlineCollapsed={true}
|
||||||
|
mode={'inline'}
|
||||||
|
theme={'dark'}
|
||||||
|
style={{ backgroundColor: 'transparent' }}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
export default NavigationMenu
|
@ -1,16 +1,8 @@
|
|||||||
import {
|
|
||||||
FolderOutlined,
|
|
||||||
FilePdfOutlined,
|
|
||||||
ExperimentOutlined,
|
|
||||||
DeploymentUnitOutlined,
|
|
||||||
} from '@ant-design/icons'
|
|
||||||
import { Layout } from 'antd'
|
|
||||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { Navigate, Route, Routes, useParams } from 'react-router-dom'
|
import { Navigate, Outlet, Route, Routes, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
import { WellContext, RootPathContext, useRootPath } from '@asb/context'
|
import { WellContext, RootPathContext, useRootPath } from '@asb/context'
|
||||||
import { LayoutPortal } from '@components/Layout'
|
import { LayoutPortal } from '@components/LayoutPortal'
|
||||||
import { PrivateMenu } from '@components/Private'
|
|
||||||
import { invokeWebApiWrapperAsync } from '@components/factory'
|
import { invokeWebApiWrapperAsync } from '@components/factory'
|
||||||
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
|
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
|
||||||
import { WellService } from '@api'
|
import { WellService } from '@api'
|
||||||
@ -23,10 +15,10 @@ import Documents from './Documents'
|
|||||||
import Telemetry from './Telemetry'
|
import Telemetry from './Telemetry'
|
||||||
import WellOperations from './WellOperations'
|
import WellOperations from './WellOperations'
|
||||||
import DrillingProgram from './DrillingProgram'
|
import DrillingProgram from './DrillingProgram'
|
||||||
|
import NavigationMenu from './NavigationMenu'
|
||||||
|
|
||||||
import '@styles/index.css'
|
import '@styles/index.css'
|
||||||
|
|
||||||
const { Content } = Layout
|
|
||||||
|
|
||||||
const Well = memo(() => {
|
const Well = memo(() => {
|
||||||
const { idWell } = useParams()
|
const { idWell } = useParams()
|
||||||
@ -59,40 +51,26 @@ const Well = memo(() => {
|
|||||||
), [well])
|
), [well])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LayoutPortal>
|
<RootPathContext.Provider value={rootPath}>
|
||||||
<RootPathContext.Provider value={rootPath}>
|
<WellContext.Provider value={[well, updateWell]}>
|
||||||
<PrivateMenu className={'well_menu'}>
|
<LayoutPortal sider={<NavigationMenu />}>
|
||||||
<PrivateMenu.Link content={Telemetry} />
|
<Routes>
|
||||||
<PrivateMenu.Link content={Reports} icon={<FilePdfOutlined />} />
|
<Route index element={<Navigate to={Telemetry.getKey()} replace />} />
|
||||||
<PrivateMenu.Link content={Analytics} icon={<DeploymentUnitOutlined />} />
|
<Route path={'*'} element={<NoAccessComponent />} />
|
||||||
<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]}>
|
<Route path={Telemetry.route} element={<Telemetry />} />
|
||||||
<Layout>
|
<Route path={Reports.route} element={<Reports />} />
|
||||||
<Content className={'site-layout-background'}>
|
<Route path={Analytics.route} element={<Analytics />} />
|
||||||
<Routes>
|
<Route path={WellOperations.route} element={<WellOperations />} />
|
||||||
<Route index element={<Navigate to={Telemetry.getKey()} replace />} />
|
<Route path={Documents.route} element={<Documents />} />
|
||||||
<Route path={'*'} element={<NoAccessComponent />} />
|
<Route path={Measure.route} element={<Measure />} />
|
||||||
|
<Route path={DrillingProgram.route} element={<DrillingProgram />} />
|
||||||
<Route path={Telemetry.route} element={<Telemetry />} />
|
<Route path={WellCase.route} element={<WellCase />} />
|
||||||
<Route path={Reports.route} element={<Reports />} />
|
</Routes>
|
||||||
<Route path={Analytics.route} element={<Analytics />} />
|
<Outlet />
|
||||||
<Route path={WellOperations.route} element={<WellOperations />} />
|
</LayoutPortal>
|
||||||
<Route path={Documents.route} element={<Documents />} />
|
</WellContext.Provider>
|
||||||
<Route path={Measure.route} element={<Measure />} />
|
</RootPathContext.Provider>
|
||||||
<Route path={DrillingProgram.route} element={<DrillingProgram />} />
|
|
||||||
<Route path={WellCase.route} element={<WellCase />} />
|
|
||||||
</Routes>
|
|
||||||
</Content>
|
|
||||||
</Layout>
|
|
||||||
</WellContext.Provider>
|
|
||||||
</RootPathContext.Provider>
|
|
||||||
</LayoutPortal>
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user