* Переработан LayoutPortal

* Переработан профиль пользователя
* Переработана система организации ссылок меню
* Новый LayoutPortal добавлен на все страницы
* Изменён редирект со страницы загрузки файла
This commit is contained in:
goodmice 2022-10-13 14:31:11 +05:00
parent 60118f9327
commit bd8962df26
No known key found for this signature in database
GPG Key ID: 63EA771203189CF1
22 changed files with 503 additions and 488 deletions

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

View File

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

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

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
{...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>
) )
}) })

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

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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