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

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

View File

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

View File

@ -1,49 +1,62 @@
import { BrowserRouter as Router, Navigate, Route, Routes } from 'react-router-dom'
import { memo } from 'react'
import { ConfigProvider } from 'antd'
import { lazy, memo, Suspense } from 'react'
import locale from 'antd/lib/locale/ru_RU'
import { ConfigProvider } from 'antd'
import { RootPathContext } from '@asb/context'
import { getUserToken, NoAccessComponent } from '@utils'
import { UserOutlet } from '@components/outlets'
import LayoutPortal from '@components/LayoutPortal'
import SuspenseFallback from '@components/SuspenseFallback'
import { getUser, NoAccessComponent } from '@utils'
import { OpenAPI } from '@api'
import AdminPanel from '@pages/AdminPanel'
import Well from '@pages/Well'
import Login from '@pages/Login'
import Cluster from '@pages/Cluster'
import Deposit from '@pages/Deposit'
import Register from '@pages/Register'
import FileDownload from '@pages/FileDownload'
import '@styles/App.less'
import '@styles/include/antd_theme.less'
import '@styles/App.less'
//OpenAPI.BASE = 'http://localhost:3000'
OpenAPI.TOKEN = async () => getUserToken() ?? ''
OpenAPI.HEADERS = {'Content-Type': 'application/json'}
const Login = lazy(() => import('@pages/public/Login'))
const Register = lazy(() => import('@pages/public/Register'))
const FileDownload = lazy(() => import('@pages/FileDownload'))
const AdminPanel = lazy(() => import('@pages/AdminPanel'))
const Deposit = lazy(() => import('@pages/Deposit'))
const Cluster = lazy(() => import('@pages/Cluster'))
const Well = lazy(() => import('@pages/Well'))
// OpenAPI.BASE = 'http://localhost:3000'
// TODO: Удалить взятие из 'token' в следующем релизе, вставлено для совместимости
OpenAPI.TOKEN = async () => getUser().token || localStorage.getItem('token') || ''
OpenAPI.HEADERS = { 'Content-Type': 'application/json' }
export const App = memo(() => (
<ConfigProvider locale={locale}>
<RootPathContext.Provider value={''}>
<Suspense fallback={<SuspenseFallback style={{ minHeight: '100vh' }} />}>
<Router>
<Routes>
<Route index element={<Navigate to={Deposit.getKey()} replace />} />
<Route index element={<Navigate to={'deposit'} replace />} />
<Route path={'*'} element={<NoAccessComponent />} />
{/* Public pages */}
<Route path={Login.route} element={<Login />} />
<Route path={Register.route} element={<Register />} />
{/* Admin pages */}
<Route path={AdminPanel.route} element={<AdminPanel />} />
<Route path={'/login'} element={<Login />} />
<Route path={'/register'} element={<Register />} />
{/* User pages */}
<Route path={Deposit.route} element={<Deposit />} />
<Route path={Cluster.route} element={<Cluster />} />
<Route path={Well.route} element={<Well />} />
<Route path={FileDownload.route} element={<FileDownload />} />
<Route element={<UserOutlet />}>
<Route path={'/file_download/:idWell/:idFile/*'} element={<FileDownload />} />
<Route element={<LayoutPortal />}>
{/* Admin pages */}
<Route path={'/admin/*'} element={<AdminPanel />} />
{/* Client pages */}
<Route path={'/deposit/*'} element={<Deposit />} />
<Route path={'/cluster/:idCluster'} element={<Cluster />} />
<Route path={'/well/:idWell/*'} element={<Well />} />
</Route>
</Route>
</Routes>
</Router>
</Suspense>
</RootPathContext.Provider>
</ConfigProvider>
))

View File

@ -2,8 +2,8 @@ import { memo, useCallback, useMemo, useState } from 'react'
import { Rule } from 'antd/lib/form'
import { Form, Input, Modal, FormProps } from 'antd'
import { useUser } from '@asb/context'
import { AuthService, UserDto } from '@api'
import { getUserId, getUserLogin } from '@utils'
import { passwordRules, createPasswordRules } from '@utils/validationRules'
import LoaderPortal from './LoaderPortal'
@ -31,7 +31,8 @@ export const ChangePassword = memo<ChangePasswordProps>(({ user, visible, onCanc
const [showLoader, setShowLoader] = useState<boolean>(false)
const [isDisabled, setIsDisabled] = useState(true)
const userData = useMemo(() => user ?? { id: getUserId(), login: getUserLogin() } as UserDto, [user])
const userContext = useUser()
const userData = useMemo(() => user ?? userContext, [user])
const [form] = Form.useForm()
@ -63,7 +64,7 @@ export const ChangePassword = memo<ChangePasswordProps>(({ user, visible, onCanc
{user && <>&nbsp;(<UserView user={user} />)</>}
</>
)}
visible={visible}
open={visible}
onCancel={onModalCancel}
onOk={() => form.submit()}
okText={'Сохранить'}

View File

@ -106,7 +106,7 @@ export const ColorPicker = memo<ColorPickerProps>(({ value = '#AA33BB', onChange
return (
<Popover
trigger={'click'}
onVisibleChange={onClose}
onOpenChange={onClose}
content={(
<div className={'asb-color-picker-content'}>
<div className={'asb-color-picker-sliders'}>

View File

@ -1,28 +0,0 @@
import { memo, ReactNode } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { Button, Layout, LayoutProps } from 'antd'
import PageHeader from '@components/PageHeader'
export type AdminLayoutPortalProps = LayoutProps & {
title?: ReactNode
}
export const AdminLayoutPortal = memo<AdminLayoutPortalProps>(({ title, ...props }) => {
const location = useLocation()
return (
<Layout.Content>
<PageHeader isAdmin title={title} style={{ backgroundColor: '#900' }}>
<Button size={'large'}>
<Link to={'/'}>Вернуться на сайт</Link>
</Button>
</PageHeader>
<Layout>
<Layout.Content className={'site-layout-background sheet'} {...props}/>
</Layout>
</Layout.Content>
)
})
export default AdminLayoutPortal

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +0,0 @@
import { memo, ReactElement } from 'react'
import { isURLAvailable } from '@utils'
export type PrivateContentProps = {
absolutePath: string
children?: ReactElement<any, any>
}
export const PrivateContent = memo<PrivateContentProps>(({ absolutePath, children = null }) =>
isURLAvailable(absolutePath) ? children : null
)
export default PrivateContent

View File

@ -1,19 +0,0 @@
import { memo } from 'react'
import { Navigate, Route, RouteProps } from 'react-router-dom'
import { isURLAvailable } from '@utils'
import { getDefaultRedirectPath } from './PrivateRoutes'
export type PrivateDefaultRouteProps = RouteProps & {
urls: string[]
elseRedirect?: string
}
export const PrivateDefaultRoute = memo<PrivateDefaultRouteProps>(({ elseRedirect, urls, ...other }) => (
<Route {...other} path={'/'} element={(
<Navigate replace to={urls.find((url) => isURLAvailable(url)) ?? elseRedirect ?? getDefaultRedirectPath()} />
)} />
))
export default PrivateDefaultRoute

View File

@ -1,75 +0,0 @@
import { join } from 'path'
import { Menu, MenuProps } from 'antd'
import { ItemType } from 'antd/lib/menu/hooks/useItems'
import { Link, LinkProps } from 'react-router-dom'
import { Children, isValidElement, memo, ReactNode, RefAttributes, useMemo } from 'react'
import { useRootPath } from '@asb/context'
import { getTabname, hasPermission, PrivateComponent, PrivateProps } from '@utils'
export type PrivateMenuProps = MenuProps & { root?: string }
export type PrivateMenuLinkProps = Partial<ItemType> & Omit<LinkProps, 'to'> & RefAttributes<HTMLAnchorElement> & {
icon?: ReactNode
danger?: boolean
title?: ReactNode
content?: PrivateComponent<any>
path?: string
visible?: boolean
permissions?: string[]
}
export const PrivateMenuLink = memo<PrivateMenuLinkProps>(({ content, path = '', title, ...other }) => (
<Link to={path} {...other}>{title ?? content?.title}</Link>
))
const PrivateMenuMain = memo<PrivateMenuProps>(({ selectable, mode, selectedKeys, root, children, ...other }) => {
const rootContext = useRootPath()
const rootPath = useMemo(() => root ?? rootContext ?? '', [root, rootContext])
const tab = getTabname()
const keys = useMemo(() => selectedKeys ?? (tab ? [tab] : []), [selectedKeys, tab])
const items = useMemo(() => Children.map(children, (child) => {
if (!child || !isValidElement<PrivateMenuLinkProps>(child))
return null
const content: PrivateProps | undefined = child.props.content
const visible: boolean | undefined = child.props.visible
if (visible === false) return null
let key
if (content?.key)
key = content.key
else if (content?.route)
key = content.route
else if (child.key) {
key = child.key?.toString()
key = key.slice(key.lastIndexOf('$') + 1)
} else return null
const permissions = child.props.permissions ?? content?.requirements
const path = child.props.path ?? join(rootPath, key)
if (visible || hasPermission(permissions))
return {
...child.props,
key,
label: <PrivateMenuLink {...child.props} path={path} />,
}
return null
})?.filter((v) => v) ?? [], [children, rootPath])
return (
<Menu
selectable={selectable ?? true}
mode={mode ?? 'horizontal'}
selectedKeys={keys}
items={items as ItemType[]}
{...other}
/>
)
})
export const PrivateMenu = Object.assign(PrivateMenuMain, { Link: PrivateMenuLink })
export default PrivateMenu

View File

@ -1,37 +0,0 @@
import { join } from 'path'
import { Menu, MenuItemProps } from 'antd'
import { memo, NamedExoticComponent } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { isURLAvailable } from '@utils'
export type PrivateMenuItemProps = MenuItemProps & {
root: string
path: string
}
export type PrivateMenuItemLinkProps = MenuItemProps & {
root?: string
path: string
title: string
}
export const PrivateMenuItemLink = memo<PrivateMenuItemLinkProps>(({ root = '', path, title, ...other }) => {
const location = useLocation()
return (
<PrivateMenuItem key={path} root={root} path={path} {...other}>
<Link to={join(root, path)}>{title}</Link>
</PrivateMenuItem>
)
})
export const PrivateMenuItem: NamedExoticComponent<PrivateMenuItemProps> & {
Link: NamedExoticComponent<PrivateMenuItemLinkProps>
} = Object.assign(memo<PrivateMenuItemProps>(({ root, path, ...other }) =>
<Menu.Item key={path} hidden={!isURLAvailable(join(root, path))} {...other} />
), {
Link: PrivateMenuItemLink
})
export default PrivateMenuItem

View File

@ -1,30 +0,0 @@
import { join } from 'path'
import { memo, ReactNode } from 'react'
import { Navigate, Route, RouteProps } from 'react-router-dom'
import { getUserId, isURLAvailable } from '@utils'
export type PrivateRouteProps = RouteProps & {
root?: string
path: string
children?: ReactNode
redirect?: ReactNode
}
export const defaultRedirect = (
<Navigate to={getUserId() ? '/access_denied' : '/login'} />
)
export const PrivateRoute = memo<PrivateRouteProps>(({ root = '', path, children, redirect = defaultRedirect, ...other }) => {
const available = isURLAvailable(join(root, path))
return (
<Route
{...other}
path={path}
element={available ? children : redirect}
/>
)
})
export default PrivateRoute

View File

@ -1,67 +0,0 @@
import { join } from 'path'
import { Navigate, Route, Routes, RoutesProps } from 'react-router-dom'
import { Children, cloneElement, memo, ReactElement, ReactNode, useCallback, useMemo } from 'react'
import { useRootPath } from '@asb/context'
import { getUserId, isURLAvailable } from '@utils'
export type PrivateRoutesProps = RoutesProps & {
root?: string
redirect?: ReactNode
elseRedirect?: string | string[]
}
export const getDefaultRedirectPath = () => getUserId() ? '/access_denied' : '/login'
export const defaultRedirect = (
<Navigate to={getDefaultRedirectPath()} />
)
export const PrivateRoutes = memo<PrivateRoutesProps>(({ root, elseRedirect, redirect = defaultRedirect, children }) => {
const rootContext = useRootPath()
const rootPath = useMemo(() => root ?? rootContext ?? '', [root, rootContext])
const toAbsolute = useCallback((path: string) => path.startsWith('/') ? path : join(rootPath, path), [rootPath])
const items = useMemo(() => Children.map(children, (child) => {
const element = child as ReactElement
let key = element.key?.toString()
if (!key) return <></>
key = key.slice(key.lastIndexOf('$') + 1).replaceAll('=2', ':')
// Ключ автоматический преобразуется в "(.+)\$ключ"
// Все ":" в ключе заменяются на "=2"
// TODO: улучшить метод нормализации ключа
const path = toAbsolute(key)
return (
<Route
key={key}
path={path}
element={isURLAvailable(path) ? cloneElement(element) : redirect}
/>
)
}) ?? [], [children, redirect, toAbsolute])
const defaultRoute = useMemo(() => {
const routes: string[] = []
if (Array.isArray(elseRedirect))
routes.push(...elseRedirect)
else if(elseRedirect)
routes.push(elseRedirect)
routes.push(...items.map((elm) => elm?.props?.path))
const firstAvailableRoute = routes.find((path) => path && isURLAvailable(path))
return firstAvailableRoute ? toAbsolute(firstAvailableRoute) : getDefaultRedirectPath()
}, [items, elseRedirect, toAbsolute])
return (
<Routes>
{items}
<Route path={'/'} element={(
<Navigate to={defaultRoute} />
)}/>
</Routes>
)
})
export default PrivateRoutes

View File

@ -1,13 +0,0 @@
export { PrivateRoute, defaultRedirect } from './PrivateRoute'
export { PrivateContent } from './PrivateContent' // TODO: Remove
export { PrivateMenuItem, PrivateMenuItemLink } from './PrivateMenuItem' // TODO: Remove
export { PrivateDefaultRoute } from './PrivateDefaultRoute'
export { PrivateMenu, PrivateMenuLink } from './PrivateMenu'
export { PrivateRoutes } from './PrivateRoutes'
export type { PrivateRouteProps } from './PrivateRoute'
export type { PrivateContentProps } from './PrivateContent' // TODO: Remove
export type { PrivateMenuItemProps, PrivateMenuItemLinkProps } from './PrivateMenuItem' // TODO: Remove
export type { PrivateDefaultRouteProps } from './PrivateDefaultRoute'
export type { PrivateMenuProps, PrivateMenuLinkProps } from './PrivateMenu'
export type { PrivateRoutesProps } from './PrivateRoutes'

View File

@ -0,0 +1,95 @@
import { ItemType } from 'antd/lib/menu/hooks/useItems'
import { memo, ReactNode, useMemo } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { join } from 'path'
import { Menu, MenuProps } from 'antd'
import { hasPermission, Permission } from '@utils'
export type PrivateWellMenuItem = {
title: string
route: string
permissions: Permission | Permission[]
icon?: ReactNode
visible?: boolean
children?: PrivateWellMenuItem[]
}
const makeItems = (items: PrivateWellMenuItem[], parentRoute: string, pathParser?: (path: string, parent: string) => string): ItemType[] => {
return items.map((item) => {
if (item.visible === false || !(item.visible === true || hasPermission(item.permissions))) return null
let route = item.route
if (pathParser)
route = pathParser(item.route, parentRoute)
else if (!item.route.startsWith('/') && parentRoute)
route = join(parentRoute, item.route)
const out: ItemType = {
key: route,
icon: item.icon,
title: item.title,
label: <Link to={route}>{item.title}</Link>,
}
if (item.children && item.children.length > 0) {
return {
...out,
children: makeItems(item.children, route, pathParser)
}
}
return out
}).filter(Boolean)
}
const makeItemList = (items: PrivateWellMenuItem[], rootPath: string, wellId?: number): ItemType[] => {
const parser = (path: string, parent: string) => {
if (!path.startsWith('/'))
path = join(parent, path)
return path.replace(/\{wellId\}/, String(wellId))
}
return makeItems(items, rootPath, parser)
}
export const makeItem = (
title: string,
route: string,
permissions: Permission | Permission[],
icon?: ReactNode,
children?: PrivateWellMenuItem[],
visible?: boolean
): PrivateWellMenuItem => ({
title,
route,
icon,
permissions,
children,
visible,
})
export type PrivateWellMenuProps = Omit<MenuProps, 'items'> & {
idWell?: number
items: PrivateWellMenuItem[]
rootPath?: string
}
export const PrivateWellMenu = memo<PrivateWellMenuProps>(({ idWell, items, rootPath = '/', ...other }) => {
const location = useLocation()
const menuItems = useMemo(() => makeItemList(items, rootPath, idWell), [items, rootPath, idWell])
const tabKeys = useMemo(() => {
const out = []
const rx = RegExp(/(?<!^)\//g)
let input = location.pathname
if (!input.endsWith('/')) input += '/'
let match: RegExpExecArray | null
while((match = rx.exec(input)) !== null)
out.push(input.slice(0, match.index))
return out
}, [location.pathname])
return <Menu items={menuItems} selectedKeys={tabKeys} {...other} />
})

View File

View File

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

View File

@ -1,56 +1,110 @@
import { memo, MouseEventHandler, useCallback, useState } from 'react'
import { memo, ReactNode, useCallback, useState } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { Button, Dropdown, DropDownProps } from 'antd'
import { UserOutlined } from '@ant-design/icons'
import { Button, Collapse, Drawer, DrawerProps, Form, FormRule, Input, Popconfirm } from 'antd'
import { useForm } from 'antd/lib/form/Form'
import { getUserLogin, removeUser } from '@utils'
import { ChangePassword } from './ChangePassword'
import { PrivateMenu } from './Private'
import { useUser } from '@asb/context'
import { Grid, GridItem } from '@components/Grid'
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 }) => {
const [isModalVisible, setIsModalVisible] = useState<boolean>(false)
type ChangePasswordForm = {
'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 location = useLocation()
const user = useUser()
const onChangePasswordClick: MouseEventHandler = useCallback((e) => {
setIsModalVisible(true)
e.preventDefault()
}, [])
const navigateTo = useCallback((to: string) => navigate(to, { state: { from: location.pathname }}), [navigate, location.pathname])
const onChangePasswordOk = useCallback(() => {
setIsModalVisible(false)
navigate('/login', { state: { from: location.pathname }})
}, [navigate, location])
const onChangePasswordOk = useCallback(() => invokeWebApiWrapperAsync(
async (values: any) => {
await AuthService.changePassword(user.id ?? -1, `${values['new-password']}`)
removeUser()
navigateTo('/login')
},
setShowLoader,
`Не удалось сменить пароль пользователя ${user.login}`,
{ actionName: 'Смена пароля пользователя' },
), [navigateTo])
const logout = useCallback(() => {
removeUser()
navigateTo('/login')
}, [navigateTo])
return (
<>
<Dropdown
<Drawer
closable
placement={'left'}
className={'user-menu'}
title={'Профиль пользователя'}
{...other}
placement={'bottomRight'}
overlay={(
<PrivateMenu mode={'vertical'} style={{ textAlign: 'right' }}>
{isAdmin ? (
<PrivateMenu.Link visible key={'/'} path={'/'} title={'Вернуться на сайт'} />
) : (
<PrivateMenu.Link path={'/admin'} content={AdminPanel} />
)}
<PrivateMenu.Link visible key={'change_password'} onClick={onChangePasswordClick} title={'Сменить пароль'} />
<PrivateMenu.Link visible key={'login'} path={'/login'} onClick={removeUser} title={'Выход'} />
</PrivateMenu>
)}
>
<Button icon={<UserOutlined/>}>{getUserLogin()}</Button>
</Dropdown>
<ChangePassword
visible={isModalVisible}
onOk={onChangePasswordOk}
onCancel={() => setIsModalVisible(false)}
/>
</>
<div className={'profile-links'}>
{isAdmin ? (
<Button onClick={() => navigateTo('/')}>Вернуться на сайт</Button>
) : isURLAvailable('/admin') && (
<Button onClick={() => navigateTo('/admin')}>Панель администратора</Button>
)}
<Button type={'ghost'} onClick={logout}>Выход</Button>
</div>
<Collapse>
<Collapse.Panel header={'Данные'} key={'summary'}>
<Grid>
<GridItem row={1} col={1}>Логин:</GridItem>
<GridItem row={1} col={2}>{user.login}</GridItem>
<GridItem row={2} col={1}>Фамилия:</GridItem>
<GridItem row={2} col={2}>{user.surname}</GridItem>
<GridItem row={3} col={1}>Имя:</GridItem>
<GridItem row={3} col={2}>{user.name}</GridItem>
<GridItem row={4} col={1}>Отчество:</GridItem>
<GridItem row={4} col={2}>{user.patronymic}</GridItem>
<GridItem row={5} col={1}>E-mail:</GridItem>
<GridItem row={5} col={2}>{user.email}</GridItem>
</Grid>
</Collapse.Panel>
<Collapse.Panel header={'Смена пароля'} key={'change-password'}>
<LoaderPortal show={showLoader}>
<Form name={'change-password'} form={changeLoginForm} autoComplete={'off'} onFinish={onChangePasswordOk}>
<Form.Item name={'new-password'} label={'Новый пароль'} rules={newPasswordRules}>
<Input.Password placeholder={'Впишите новый пароль'} />
</Form.Item>
<Form.Item required name={'confirm-password'} rules={confirmPasswordRules} label={'Подтверждение пароля'}>
<Input.Password />
</Form.Item>
<Form.Item>
<Popconfirm title={'Вы уверены что хотите сменить пароль?'} onConfirm={changeLoginForm.submit} placement={'topRight'}>
<Button type={'primary'}>Сменить</Button>
</Popconfirm>
</Form.Item>
</Form>
</LoaderPortal>
</Collapse.Panel>
{additional}
</Collapse>
</Drawer>
)
})

View File

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

View File

@ -3,8 +3,7 @@ import { ArgsProps } from 'antd/lib/notification'
import { Dispatch, ReactNode, SetStateAction } from 'react'
import { WellView } from '@components/views'
import { getUserToken } from '@utils'
import { FunctionalValue, getFunctionalValue, isDev } from '@utils'
import { FunctionalValue, getFunctionalValue, getUser, isDev } from '@utils'
import { ApiError, FileInfoDto, WellDto } from '@api'
export type NotifyType = 'error' | 'warning' | 'info'
@ -97,7 +96,7 @@ export const invokeWebApiWrapperAsync = async (
export const download = async (url: string, fileName?: string) => {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${getUserToken()}`
Authorization: `Bearer ${getUser().token}`
},
method: 'Get'
})
@ -125,7 +124,7 @@ export const download = async (url: string, fileName?: string) => {
export const upload = async (url: string, formData: FormData) => {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${getUserToken()}`
Authorization: `Bearer ${getUser().token}`
},
method: 'Post',
body: formData,

View File

@ -0,0 +1,30 @@
import { memo, useEffect, useState } from 'react'
import { Outlet } from 'react-router-dom'
import { UserContext } from '@asb/context'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { getUser, setUser as setStorageUser } from '@utils'
import { AuthService, UserTokenDto } from '@api'
export const UserOutlet = memo(() => {
const [user, setUser] = useState<UserTokenDto>({})
useEffect(() => {
invokeWebApiWrapperAsync(async () => {
let user = getUser()
if (!user.id) {
user = await AuthService.refresh()
setStorageUser(user)
}
setUser(user)
})
}, [])
return (
<UserContext.Provider value={user}>
<Outlet />
</UserContext.Provider>
)
})
export default UserOutlet

View File

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

View File

@ -39,8 +39,8 @@ export const Poprompt = memo<PopromptProps>(({ formProps, buttonProps, footer, c
)}
trigger={'click'}
{...other}
visible={visible}
onVisibleChange={(visible) => setVisible(visible)}
open={visible}
onOpenChange={(visible) => setVisible(visible)}
>
<Button {...buttonProps}>{text}</Button>
</Popover>

View File

@ -74,6 +74,9 @@ export type WellTreeSelectorProps = TreeProps<TreeDataNode> & {
show?: boolean
expand?: boolean | Key[]
current?: Key
onClose?: () => void
onChange?: (value: string | undefined) => void
open?: boolean
}
const getExpandKeys = (treeData: TreeDataNode[], depositKeys?: Key[] | boolean): Key[] => {
@ -88,13 +91,11 @@ const getExpandKeys = (treeData: TreeDataNode[], depositKeys?: Key[] | boolean):
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 [showLoader, setShowLoader] = useState<boolean>(false)
const [visible, setVisible] = useState<boolean>(false)
const [expanded, setExpanded] = useState<Key[]>([])
const [selected, setSelected] = useState<Key[]>([])
const [value, setValue] = useState<string>()
const navigate = useNavigate()
const location = useLocation()
@ -103,8 +104,6 @@ export const WellTreeSelector = memo<WellTreeSelectorProps>(({ show, expand, cur
if (current) setSelected([current])
}, [current])
useEffect(() => setVisible((prev) => show ?? prev), [show])
useEffect(() => {
setExpanded((prev) => expand ? getExpandKeys(wellsTree, expand) : prev)
}, [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]
setSelected(key ? [key] : [])
setValue(getLabel(wellsTree, value))
onChange?.(getLabel(wellsTree, value))
}, [wellsTree])
const onSelect = useCallback((value: Key[]): void => {
@ -170,13 +169,10 @@ export const WellTreeSelector = memo<WellTreeSelectorProps>(({ show, expand, cur
navigate(newPath, { state: { from: location.pathname }})
}, [navigate, location])
useEffect(() => onChange(location.pathname), [onChange, location])
useEffect(() => onValueChange(location.pathname), [onValueChange, location])
return (
<>
<Button loading={showLoader} onClick={() => setVisible(true)}>{value ?? 'Выберите месторождение'}</Button>
<Drawer visible={visible} mask={false} onClose={() => setVisible(false)}>
<Typography.Title level={3}>Список скважин</Typography.Title>
<Drawer open={open} mask={false} onClose={onClose} title={'Список скважин'}>
<Skeleton active loading={showLoader}>
<Tree
{...other}
@ -189,7 +185,6 @@ export const WellTreeSelector = memo<WellTreeSelectorProps>(({ show, expand, cur
/>
</Skeleton>
</Drawer>
</>
)
})

View File

@ -1,8 +1,9 @@
import { Fragment, memo } from 'react'
import { Tooltip } from 'antd'
import { TelemetryDto, TelemetryInfoDto } from '@api'
import { Grid, GridItem } from '@components/Grid'
import { formatDate } from '@utils'
import { TelemetryDto, TelemetryInfoDto } from '@api'
export const lables: Record<string, string> = {
timeZoneId: 'Временная зона',
@ -30,12 +31,17 @@ export const TelemetryView = memo<TelemetryViewProps>(({ telemetry }) => telemet
overlayInnerStyle={{ width: '400px' }}
title={
<Grid>
{(Object.keys(telemetry.info) as Array<keyof TelemetryInfoDto>).map((key, i) => (
{(Object.keys(telemetry.info) as Array<keyof TelemetryInfoDto>).map((key, i) => {
let value = telemetry.info?.[key]
value = key === 'drillingStartDate' ? formatDate(value) : value
return (
<Fragment key={i}>
<GridItem row={i+1} col={1}>{lables[key] ?? key}:</GridItem>
<GridItem row={i+1} col={2}>{telemetry.info?.[key]}</GridItem>
<GridItem row={i+1} col={2}>{value}</GridItem>
</Fragment>
))}
)
})}
</Grid>
}
>

View File

@ -20,7 +20,7 @@ export const WidgetSettingsWindow = memo<WidgetSettingsWindowProps>(({ settings,
return (
<Modal
{...other}
visible={!!settings}
open={!!settings}
title={(
<>
Настройка виджета {settings?.label ? `"${settings?.label}"` : ''}

View File

@ -1,22 +1,53 @@
import { createContext, useContext } from 'react'
import { createContext, useContext, useEffect } from 'react'
import { WellDto } from '@api'
import { LayoutPortalProps } from '@components/LayoutPortal'
import { UserTokenDto, WellDto } from '@api'
/** Контекст текущей скважины */
export const WellContext = createContext<[WellDto, (well: WellDto) => void]>([{}, () => {}])
/** Контекст текущего корневого пути */
export const RootPathContext = createContext<string>('')
export const RootPathContext = createContext<string>('/')
/** Контекст текущего пользователя */
export const UserContext = createContext<UserTokenDto>({})
/** Контекст метода редактирования параметров заголовка и меню */
export const LayoutPropsContext = createContext<(props: LayoutPortalProps) => void>(() => {})
/** Контекст для блока справа от крошек на страницах скважин и админки */
export const TopRightBlockContext = createContext<(block: JSX.Element) => void>(() => {})
/**
* Получение текущей скважины
* Получить текущую скважину
*
* @returns Текущая скважина, либо `null`
*/
export const useWell = () => useContext(WellContext)
/**
* Получает текущий корневой путь
* Получить текущий корневой путь
*
* @returns Текущий корневой путь
*/
export const useRootPath = () => useContext(RootPathContext)
/**
* Получить текущего пользователя
*
* @returns Текущий пользователь, либо `null`
*/
export const useUser = () => useContext(UserContext)
export const useTopRightBlock = () => useContext(TopRightBlockContext)
/**
* Получить метод задания параметров заголовка и меню
*
* @returns Получить метод задания параметров заголовка и меню
*/
export const useLayoutProps = (props?: LayoutPortalProps) => {
const setLayoutProps = useContext(LayoutPropsContext)
useEffect(() => {
if (props) setLayoutProps(props)
}, [setLayoutProps, props])
return setLayoutProps
}

View File

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

View File

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

View File

@ -11,11 +11,9 @@ import {
} from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminClusterService, AdminDepositService } from '@api'
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import { arrayOrDefault, coordsFormat, wrapPrivateComponent } from '@utils'
import { min1 } from '@utils/validationRules'
import { coordsFixed } from './DepositController'
const ClusterController = memo(() => {
const [deposits, setDeposits] = useState([])
const [clusters, setClusters] = useState([])
@ -41,8 +39,8 @@ const ClusterController = memo(() => {
sorter: makeStringSorter('caption'),
formItemRules: min1,
}),
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFixed }),
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFixed }),
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFormat }),
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFormat }),
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
], [deposits])
@ -108,6 +106,7 @@ const ClusterController = memo(() => {
onRowEdit={tableHandlers.edit}
onRowDelete={tableHandlers.delete}
tableName={'admin_cluster_controller'}
scroll={{ x: true }}
/>
</>
)

View File

@ -97,6 +97,7 @@ const CompanyController = memo(() => {
onRowEdit={tableHandlers.edit}
onRowDelete={tableHandlers.delete}
tableName={'admin_company_controller'}
scroll={{ x: true }}
/>
</>
)

View File

@ -75,6 +75,7 @@ const CompanyTypeController = memo(() => {
onRowEdit={tableHandlers.edit}
onRowDelete={tableHandlers.delete}
tableName={'admin_company_type_controller'}
scroll={{ x: true }}
/>
</>
)

View File

@ -3,16 +3,14 @@ import { Input } from 'antd'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { EditableTable, makeColumn, defaultPagination, makeTimezoneColumn } from '@components/Table'
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import { arrayOrDefault, coordsFormat, wrapPrivateComponent } from '@utils'
import { min1 } from '@utils/validationRules'
import { AdminDepositService } from '@api'
export const coordsFixed = (coords) => coords && isFinite(coords) ? (+coords).toPrecision(10) : '-'
const depositColumns = [
makeColumn('Название', 'caption', { width: 200, editable: true, formItemRules: min1 }),
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFixed }),
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFixed }),
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFormat }),
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFormat }),
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
]
@ -77,6 +75,7 @@ const DepositController = memo(() => {
onRowEdit={tableHandlers.edit}
onRowDelete={tableHandlers.delete}
tableName={'admin_deposit_controller'}
scroll={{ x: true }}
/>
</>
)

View File

@ -82,6 +82,7 @@ const PermissionController = memo(() => {
onRowEdit={tableHandlers.edit}
onRowDelete={tableHandlers.delete}
tableName={'admin_permission_controller'}
scroll={{ x: true }}
/>
</>
)

View File

@ -89,6 +89,7 @@ const RoleController = memo(() => {
onRowEdit={tableHandlers.edit}
onRowDelete={tableHandlers.delete}
tableName={'admin_role_controller'}
scroll={{ x: true }}
/>
</>
)

View File

@ -6,7 +6,7 @@ import { Button, Input } from 'antd'
import {
defaultPagination,
makeColumn,
makeDateSorter,
makeDateColumn,
makeNumericColumn,
makeNumericRender,
makeTextColumn,
@ -53,7 +53,7 @@ const TelemetryController = memo(() => {
makeNumericColumn('ID', 'id', null, null, makeNumericRender(0)),
makeTextColumn('UID', 'remoteUid'),
makeTextColumn('Назначена на скважину', 'realWell'),
makeTextColumn('Дата начала бурения', 'drillingStartDate', null, makeDateSorter('drillingStartDate')),
makeDateColumn('Дата начала бурения', 'drillingStartDate'),
makeTextColumn('Часовой пояс', 'timeZoneId'),
makeTextColumn('Скважина', 'well'),
makeTextColumn('Куст', 'cluster'),
@ -115,6 +115,7 @@ const TelemetryController = memo(() => {
pagination={defaultPagination}
dataSource={filteredTelemetryData}
tableName={'admin_telemetry_controller'}
scroll={{ x: true }}
/>
</>
)

View File

@ -1,13 +1,8 @@
import { Layout } from 'antd'
import { memo, useMemo } from 'react'
import { Navigate, Route, Routes } from 'react-router-dom'
import { Outlet } from 'react-router-dom'
import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import TelemetryViewer from './TelemetryViewer'
import TelemetryMerger from './TelemetryMerger'
import { wrapPrivateComponent } from '@utils'
const Telemetry = memo(() => {
const root = useRootPath()
@ -15,23 +10,7 @@ const Telemetry = memo(() => {
return (
<RootPathContext.Provider value={rootPath}>
<Layout>
<PrivateMenu>
<PrivateMenu.Link content={TelemetryViewer} />
<PrivateMenu.Link content={TelemetryMerger} />
</PrivateMenu>
<Layout>
<Layout.Content className={'site-layout-background'}>
<Routes>
<Route index element={<Navigate to={TelemetryViewer.route} replace />} />
<Route path={'*'} element={<NoAccessComponent />} />
<Route path={TelemetryViewer.route} element={<TelemetryViewer />} />
<Route path={TelemetryMerger.route} element={<TelemetryMerger />} />
</Routes>
</Layout.Content>
</Layout>
</Layout>
<Outlet />
</RootPathContext.Provider>
)
})

View File

@ -214,6 +214,7 @@ const UserController = memo(() => {
buttonsWidth={120}
pagination={defaultPagination}
tableName={'admin_user_controller'}
scroll={{ x: true }}
/>
</LoaderPortal>
<ChangePassword

View File

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

View File

@ -21,9 +21,7 @@ import {
import { invokeWebApiWrapperAsync } from '@components/factory'
import { TelemetryView, CompanyView } from '@components/views'
import TelemetrySelect from '@components/selectors/TelemetrySelect'
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import { coordsFixed } from '../DepositController'
import { arrayOrDefault, coordsFormat, wrapPrivateComponent } from '@utils'
const wellTypes = [
{ value: 1, label: 'Наклонно-направленная' },
@ -98,8 +96,8 @@ const WellController = memo(() => {
editable: true,
sorter: makeNumericSorter('idWellType'),
}),
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFixed }),
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFixed }),
makeColumn('Широта', 'latitude', { width: 150, editable: true, render: coordsFormat }),
makeColumn('Долгота', 'longitude', { width: 150, editable: true, render: coordsFormat }),
makeColumn('Телеметрия', 'telemetry', {
editable: true,
render: (telemetry) => <TelemetryView telemetry={telemetry} />,

View File

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

View File

@ -1,44 +0,0 @@
import { memo, useMemo } from 'react'
import { Navigate, Route, Routes } from 'react-router-dom'
import { Layout } from 'antd'
import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import Statistics from './Statistics'
import WellCompositeEditor from './WellCompositeEditor'
const Analytics = memo(() => {
const root = useRootPath()
const rootPath = useMemo(() => `${root}/analytics`, [root])
return (
<RootPathContext.Provider value={rootPath}>
<Layout>
<PrivateMenu className={'well_menu'}>
<PrivateMenu.Link content={WellCompositeEditor} />
<PrivateMenu.Link key={'statistics'} title={'Оценка по ЦБ'} content={Statistics} />
</PrivateMenu>
<Layout>
<Layout.Content className={'site-layout-background'}>
<Routes>
<Route index element={<Navigate to={WellCompositeEditor.getKey()} replace />} />
<Route path={'*'} element={<NoAccessComponent />} />
<Route path={WellCompositeEditor.route} element={<WellCompositeEditor />} />
<Route path={Statistics.route} element={<Statistics />} />
</Routes>
</Layout.Content>
</Layout>
</Layout>
</RootPathContext.Provider>
)
})
export default wrapPrivateComponent(Analytics, {
requirements: [],
title: 'Аналитика',
route: 'analytics/*',
key: 'analytics',
})

View File

@ -1,6 +1,6 @@
import { Link, useLocation } from 'react-router-dom'
import { useState, useEffect, memo, useMemo } from 'react'
import { Tag, Button, Modal } from 'antd'
import { useState, useEffect, memo, useMemo, lazy, Suspense } from 'react'
import { Button, Modal } from 'antd'
import { LineChartOutlined, ProfileOutlined, TeamOutlined } from '@ant-design/icons'
import {
@ -13,9 +13,9 @@ import {
makeNumericRender,
makeNumericColumn,
} from '@components/Table'
import { CompanyView } from '@components/views'
import LoaderPortal from '@components/LoaderPortal'
import PointerIcon from '@components/icons/PointerIcon'
import SuspenseFallback from '@components/SuspenseFallback'
import { invokeWebApiWrapperAsync } from '@components/factory'
import {
getOperations,
@ -25,9 +25,10 @@ import {
wrapPrivateComponent
} from '@utils'
import Tvd from '@pages/WellOperations/Tvd'
const Tvd = lazy(() => import('@pages/Well/WellOperations/Tvd'))
import CompaniesTable from './CompaniesTable'
import WellOperationsTable from './WellOperationsTable'
import CompaniesTable from '@pages/Cluster/CompaniesTable'
const filtersMinMax = [
{ text: 'min', value: 'min' },
@ -173,23 +174,26 @@ const ClusterWells = memo(({ statsWells }) => {
pagination={false}
rowKey={(record) => record.caption}
tableName={'cluster'}
scroll={{ x: true }}
/>
<Modal
title={'TVD'}
centered
visible={isTVDModalVisible}
open={isTVDModalVisible}
onCancel={() => setIsTVDModalVisible(false)}
width={1500}
footer={null}
>
<Suspense fallback={<SuspenseFallback style={{ minHeight: '100%' }} />}>
<Tvd style={{ minHeight: '600px' }} well={selectedWell} />
</Suspense>
</Modal>
<Modal
title={'Операции'}
centered
visible={isOpsModalVisible}
open={isOpsModalVisible}
onCancel={() => setIsOpsModalVisible(false)}
width={1500}
footer={null}
@ -202,7 +206,7 @@ const ClusterWells = memo(({ statsWells }) => {
<Modal
title={'Участники'}
centered
visible={isCompaniesModalVisible}
open={isCompaniesModalVisible}
onCancel={() => setIsCompaniesModalVisible(false)}
width={1500}
footer={null}

View File

@ -1,18 +1,22 @@
import React, { memo, useMemo } from 'react'
import { memo, useMemo } from 'react'
import { BankOutlined } from '@ant-design/icons'
import { makeTextColumn, Table } from '@components/Table'
import { makeColumn, makeTextColumn, Table } from '@components/Table'
const columns = [
makeTextColumn('', 'logo'),
makeColumn('', 'logo'),
makeTextColumn('Название компании', 'caption'),
makeTextColumn('Тип компании', 'companyTypeCaption'),
]
const CompaniesTable = memo(({companies}) => {
const CompaniesTable = memo(({ companies }) => {
const dataCompanies = useMemo(() => companies?.map((company) => ({
key: company.id,
logo: company?.logo ? <img src={company.logo}/> : <BankOutlined/>,
logo: (
<div className={'centered'}>
{company?.logo ? <img src={company.logo}/> : <BankOutlined/>}
</div>
),
caption: company.caption,
companyTypeCaption: company.companyTypeCaption,
})), [companies])

View File

@ -7,8 +7,8 @@ import { getPrecision } from '@utils/functions'
const columns = [
makeTextColumn('Конструкция секции', 'sectionType'),
makeTextColumn('Операция', 'operationName'),
makeNumericColumnPlanFact('Глубина забоя', 'depth', null, null, getPrecision),
makeNumericColumnPlanFact('Часы', 'durationHours', null, null, getPrecision),
makeNumericColumnPlanFact('Глубина забоя', 'depth', null, null, (number) => getPrecision(number)),
makeNumericColumnPlanFact('Часы', 'durationHours', null, null, (number) => getPrecision(number)),
makeNumericColumnPlanFact('Комментарий', 'comment', null, null, (text) => text ?? '-')
]

View File

@ -1,19 +1,26 @@
import { useState, useEffect, memo } from 'react'
import { useParams } from 'react-router-dom'
import { LayoutPortal } from '@components/Layout'
import { useLayoutProps } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import { OperationStatService } from '@api'
import ClusterWells from './ClusterWells'
import { useParams } from 'react-router-dom'
const layoutProps = {
title: 'Анализ скважин куста',
breadcrumb: true,
}
const Cluster = memo(() => {
const { idCluster } = useParams()
const [data, setData] = useState([])
const [showLoader, setShowLoader] = useState(false)
useLayoutProps(layoutProps)
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
@ -27,11 +34,9 @@ const Cluster = memo(() => {
}, [idCluster])
return (
<LayoutPortal title={'Анализ скважин куста'}>
<LoaderPortal show={showLoader}>
<ClusterWells statsWells={data} />
</LoaderPortal>
</LayoutPortal>
)
})

View File

@ -3,8 +3,8 @@ import { useState, useEffect, memo, useMemo } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { Popover, Badge } from 'antd'
import { useLayoutProps } from '@asb/context'
import { PointerIcon } from '@components/icons'
import { LayoutPortal } from '@components/Layout'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, limitValue, wrapPrivateComponent } from '@utils'
@ -47,18 +47,26 @@ const Deposit = memo(() => {
const [showLoader, setShowLoader] = useState(false)
const [viewParams, setViewParams] = useState(defaultViewParams)
const setLayoutProps = useLayoutProps()
const location = useLocation()
const selectorProps = useMemo(() => {
const hasId = location.pathname.length > '/deposit/'.length
return {
show: true,
expand: hasId ? [location.pathname] : true,
current: hasId ? location.pathname : undefined,
}
}, [location.pathname])
useEffect(() => setLayoutProps({
sheet: false,
showSelector: true,
selectorProps,
title: 'Месторождение',
}), [setLayoutProps, selectorProps])
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
@ -73,9 +81,8 @@ const Deposit = memo(() => {
}, [])
return (
<LayoutPortal noSheet selector={selectorProps} title={'Месторождение'}>
<LoaderPortal show={showLoader}>
<div className={'h-100vh'}>
<div className={'h-100vh'} style={{ overflow: 'hidden' }}>
<Map {...viewParams}>
{depositsData.map(deposit => (
<Overlay
@ -103,7 +110,6 @@ const Deposit = memo(() => {
</Map>
</div>
</LoaderPortal>
</LayoutPortal>
)
})

View File

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

View File

@ -1,48 +0,0 @@
import { Navigate, Route, Routes } from 'react-router-dom'
import { memo, useMemo } from 'react'
import { FilePdfOutlined } from '@ant-design/icons'
import { Layout } from 'antd'
import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import DailyReport from './DailyReport'
import DiagramReport from './DiagramReport'
const { Content } = Layout
const Reports = memo(() => {
const root = useRootPath()
const rootPath = useMemo(() => `${root}/reports`, [root])
return (
<RootPathContext.Provider value={rootPath}>
<Layout>
<PrivateMenu className={'well_menu'}>
<PrivateMenu.Link content={DiagramReport} icon={<FilePdfOutlined />} />
<PrivateMenu.Link content={DailyReport} />
</PrivateMenu>
<Layout>
<Content className={'site-layout-background'}>
<Routes>
<Route index element={<Navigate to={'diagram_report'} replace />} />
<Route path={'*'} element={<NoAccessComponent />} />
<Route path={DiagramReport.route} element={<DiagramReport />} />
<Route path={DailyReport.route} element={<DailyReport />} />
</Routes>
</Content>
</Layout>
</Layout>
</RootPathContext.Provider>
)
})
export default wrapPrivateComponent(Reports, {
requirements: [],
title: 'Рапорта',
route: 'reports/*',
key: 'reports',
})

View File

@ -1,63 +0,0 @@
import { Navigate, Route, Routes } from 'react-router-dom'
import { memo, useMemo } from 'react'
import { Layout } from 'antd'
import { AlertOutlined, FundViewOutlined, DatabaseOutlined } from '@ant-design/icons'
import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import Archive from './Archive'
import Messages from './Messages'
import Operations from './Operations'
import DashboardNNB from './DashboardNNB'
import TelemetryView from './TelemetryView'
import OperationTime from './OperationTime'
import '@styles/index.css'
const { Content } = Layout
const Telemetry = memo(() => {
const root = useRootPath()
const rootPath = useMemo(() => `${root}/telemetry`, [root])
return (
<RootPathContext.Provider value={rootPath}>
<Layout>
<PrivateMenu className={'well_menu'}>
<PrivateMenu.Link content={TelemetryView} icon={<FundViewOutlined />} />
<PrivateMenu.Link content={Messages} icon={<AlertOutlined/>} />
<PrivateMenu.Link content={Archive} icon={<DatabaseOutlined />} />
<PrivateMenu.Link content={DashboardNNB} />
<PrivateMenu.Link content={Operations} />
<PrivateMenu.Link content={OperationTime} />
</PrivateMenu>
<Layout>
<Content className={'site-layout-background'}>
<Routes>
<Route index element={<Navigate to={TelemetryView.route} replace />} />
<Route path={'*'} element={<NoAccessComponent />} />
<Route path={TelemetryView.route} element={<TelemetryView />} />
<Route path={Messages.route} element={<Messages />} />
<Route path={Archive.route} element={<Archive />} />
<Route path={DashboardNNB.route} element={<DashboardNNB />} />
<Route path={Operations.route} element={<Operations />} />
<Route path={OperationTime.route} element={<OperationTime />} />
</Routes>
</Content>
</Layout>
</Layout>
</RootPathContext.Provider>
)
})
export default wrapPrivateComponent(Telemetry, {
requirements: [],
icon: <FundViewOutlined />,
title: 'Телеметрия',
route: 'telemetry/*',
key: 'telemetry',
})

View File

@ -1,104 +0,0 @@
import {
FolderOutlined,
FilePdfOutlined,
ExperimentOutlined,
DeploymentUnitOutlined,
} from '@ant-design/icons'
import { Layout } from 'antd'
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Navigate, Route, Routes, useParams } from 'react-router-dom'
import { WellContext, RootPathContext, useRootPath } from '@asb/context'
import { LayoutPortal } from '@components/Layout'
import { PrivateMenu } from '@components/Private'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import { WellService } from '@api'
import Measure from './Measure'
import Reports from './Reports'
import WellCase from './WellCase'
import Analytics from './Analytics'
import Documents from './Documents'
import Telemetry from './Telemetry'
import WellOperations from './WellOperations'
import DrillingProgram from './DrillingProgram'
import '@styles/index.css'
const { Content } = Layout
const Well = memo(() => {
const { idWell } = useParams()
const [well, setWell] = useState({ id: idWell })
const root = useRootPath()
const rootPath = useMemo(() => `${root}/well/${idWell}`, [root, idWell])
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const well = await WellService.get(idWell)
setWell(well ?? { id: idWell })
},
undefined,
'Не удалось получить данные по скважине'
)
}, [idWell])
const updateWell = useCallback((data) => invokeWebApiWrapperAsync(
async () => {
const newWell = { ...well, ...data }
await WellService.updateWell(newWell)
setWell(newWell)
},
undefined,
`Не удалось изменить данные скважины`,
{ actionName: 'Изменение данных скважины', well }
), [well])
return (
<LayoutPortal>
<RootPathContext.Provider value={rootPath}>
<PrivateMenu className={'well_menu'}>
<PrivateMenu.Link content={Telemetry} />
<PrivateMenu.Link content={Reports} icon={<FilePdfOutlined />} />
<PrivateMenu.Link content={Analytics} icon={<DeploymentUnitOutlined />} />
<PrivateMenu.Link content={WellOperations} icon={<FolderOutlined />} />
<PrivateMenu.Link content={Documents} icon={<FolderOutlined />} />
<PrivateMenu.Link content={Measure} icon={<ExperimentOutlined />} />
<PrivateMenu.Link content={DrillingProgram} icon={<FolderOutlined />} />
<PrivateMenu.Link content={WellCase} icon={<FolderOutlined />} />
</PrivateMenu>
<WellContext.Provider value={[well, updateWell]}>
<Layout>
<Content className={'site-layout-background'}>
<Routes>
<Route index element={<Navigate to={Telemetry.getKey()} replace />} />
<Route path={'*'} element={<NoAccessComponent />} />
<Route path={Telemetry.route} element={<Telemetry />} />
<Route path={Reports.route} element={<Reports />} />
<Route path={Analytics.route} element={<Analytics />} />
<Route path={WellOperations.route} element={<WellOperations />} />
<Route path={Documents.route} element={<Documents />} />
<Route path={Measure.route} element={<Measure />} />
<Route path={DrillingProgram.route} element={<DrillingProgram />} />
<Route path={WellCase.route} element={<WellCase />} />
</Routes>
</Content>
</Layout>
</WellContext.Provider>
</RootPathContext.Provider>
</LayoutPortal>
)
})
export default wrapPrivateComponent(Well, {
requirements: [],
title: 'Скважина',
route: 'well/:idWell/*',
key: 'well',
})

View File

@ -2,12 +2,75 @@ import { memo, useCallback, useEffect, useState } from 'react'
import { Button, Modal, Popconfirm } from 'antd'
import { useWell } from '@asb/context'
import { Table } from '@components/Table'
import { makeColumn, makeGroupColumn, makeNumericRender, makeSelectColumn, Table } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { DrillParamsService } from '@api'
import { DrillParamsService, WellOperationService } from '@api'
import { getColumns } from '@pages/WellOperations/WellDrillParams'
const getDeepValue = (data, key) => {
if (!key || key.trim() === '') return null
const keys = key.split('.')
let out = data
while (keys.length > 0) {
if (!(keys[0] in out)) return null
out = out[keys[0]]
keys.splice(0, 1)
}
return out
}
const makeNumericSorter = (keys) => (a, b) => getDeepValue(a, keys) - getDeepValue(b, keys)
const numericRender = makeNumericRender(1)
const makeNumericColumn = (title, dataIndex, render, other) => makeColumn(title, dataIndex, {
sorter: makeNumericSorter(dataIndex),
render: (_, record, index) => {
const func = render ?? ((value) => <>{value}</>)
const item = getDeepValue(record, dataIndex)
return func(item, record, index)
},
align: 'right',
...other,
})
const makeAvgRender = (dataIndex) => (avg, record) => {
const max = record[dataIndex]?.max
const fillW = (max - avg) / max * 100
return (
<div className={'avg-column'}>
<div className={'avg-fill'} style={{ width: `${fillW}%` }} />
<div className={'avg-value'}>
{numericRender(avg)}
</div>
</div>
)
}
const makeNumericAvgRange = (title, dataIndex, defaultRender = false) => makeGroupColumn(title, [
makeNumericColumn('мин', `${dataIndex}.min`),
makeNumericColumn('сред', `${dataIndex}.avg`, defaultRender ? undefined : makeAvgRender(dataIndex)),
makeNumericColumn('макс', `${dataIndex}.max`),
])
export const getColumns = async (idWell) => {
let sectionTypes = await WellOperationService.getSectionTypes(idWell)
sectionTypes = Object.entries(sectionTypes).map(([id, value]) => ({
label: value,
value: id,
}))
return [
makeSelectColumn('Конструкция секции','idWellSectionType', sectionTypes, null, {
width: 160,
sorter: makeNumericSorter('idWellSectionType'),
}),
makeNumericAvgRange('Нагрузка, т', 'axialLoad'),
makeNumericAvgRange('Давление, атм', 'pressure'),
makeNumericAvgRange('Момент на ВСП, кН·м', 'rotorTorque', true),
makeNumericAvgRange('Обороты на ВСП, об/мин', 'rotorSpeed'),
makeNumericAvgRange('Расход, л/с', 'flow'),
]
}
export const NewParamsTable = memo(({ selectedWellsKeys }) => {
const [params, setParams] = useState([])
@ -54,7 +117,7 @@ export const NewParamsTable = memo(({ selectedWellsKeys }) => {
<Modal
title={'Заполнить режимы текущей скважины'}
centered
visible={isParamsModalVisible}
open={isParamsModalVisible}
onCancel={() => setIsParamsModalVisible(false)}
width={1700}
footer={(

View File

@ -1,7 +1,7 @@
import { Link, useLocation } from 'react-router-dom'
import { useState, useEffect, memo, useMemo } from 'react'
import { useState, useEffect, memo, useMemo, lazy, Suspense } from 'react'
import { LineChartOutlined, ProfileOutlined, TeamOutlined } from '@ant-design/icons'
import { Table, Tag, Button, Badge, Divider, Modal, Row, Col } from 'antd'
import { Table, Button, Badge, Divider, Modal, Row, Col } from 'antd'
import { useWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal'
@ -16,10 +16,12 @@ import {
getOperations
} from '@utils'
import Tvd from '@pages/WellOperations/Tvd'
import WellOperationsTable from '@pages/Cluster/WellOperationsTable'
import NewParamsTable from './NewParamsTable'
import CompaniesTable from '@pages/Cluster/CompaniesTable'
import SuspenseFallback from '@asb/components/SuspenseFallback'
const Tvd = lazy(() => import('@pages/Well/WellOperations/Tvd'))
const CompaniesTable = lazy(() => import('@pages/Cluster/CompaniesTable'))
const WellOperationsTable = lazy(() => import('@pages/Cluster/WellOperationsTable'))
const filtersMinMax = [
{ text: 'min', value: 'min' },
@ -222,38 +224,44 @@ const WellCompositeSections = memo(({ statsWells, selectedSections }) => {
<Modal
title={'TVD'}
centered
visible={isTVDModalVisible}
open={isTVDModalVisible}
onCancel={() => setIsTVDModalVisible(false)}
width={1500}
footer={null}
>
<Suspense fallback={<SuspenseFallback style={{ minHeight: '100%' }} />}>
<Tvd well={selectedWell} style={{ height: '80vh' }} />
</Suspense>
</Modal>
<Modal
title={'Операции'}
centered
visible={isOpsModalVisible}
open={isOpsModalVisible}
onCancel={() => setIsOpsModalVisible(false)}
width={1500}
footer={null}
>
<Suspense fallback={<SuspenseFallback style={{ minHeight: '100%' }} />}>
<LoaderPortal show={showLoader}>
<WellOperationsTable wellOperations={wellOperations} />
</LoaderPortal>
</Suspense>
</Modal>
<Modal
title={'Участники'}
centered
visible={isCompaniesModalVisible}
open={isCompaniesModalVisible}
onCancel={() => setIsCompaniesModalVisible(false)}
width={1500}
footer={null}
>
<Suspense fallback={<SuspenseFallback style={{ minHeight: '100%' }} />}>
<LoaderPortal show={showLoader}>
<CompaniesTable companies={companies} />
</LoaderPortal>
</Suspense>
</Modal>
</>
)

View File

@ -1,35 +1,26 @@
import { useState, useEffect, memo, useMemo } from 'react'
import { Navigate, Route, Routes } from 'react-router-dom'
import { Col, Layout, Row } from 'antd'
import { useState, useEffect, memo, Suspense, lazy } from 'react'
import { Row } from 'antd'
import { useWell, useRootPath } from '@asb/context'
import { PrivateMenu } from '@components/Private'
import { useWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal'
import SuspenseFallback from '@components/SuspenseFallback'
import WellSelector from '@components/selectors/WellSelector'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, NoAccessComponent, wrapPrivateComponent } from '@utils'
import { OperationStatService, WellCompositeService } from '@api'
import ClusterWells from '@pages/Cluster/ClusterWells'
import WellCompositeSections from './WellCompositeSections'
const { Content } = Layout
import '@styles/well_composite.less'
const properties = {
requirements: ['OperationStat.get', 'WellComposite.get'],
title: 'Композитная скважина',
route: 'composite/*',
key: 'composite',
}
const ClusterWells = lazy(() => import('@pages/Cluster/ClusterWells'))
const WellCompositeEditor = memo(() => {
const [well] = useWell()
const root = useRootPath()
const rootPath = useMemo(() => `${root}/${properties.key}`, [root])
const [statsWells, setStatsWells] = useState([])
const [showLoader, setShowLoader] = useState(false)
const [showTabLoader, setShowTabLoader] = useState(false)
const [selectedIdWells, setSelectedIdWells] = useState([])
const [selectedSections, setSelectedSections] = useState([])
@ -61,7 +52,7 @@ const WellCompositeEditor = memo(() => {
const stats = arrayOrDefault(await OperationStatService.getWellsStat(selectedIdWells))
setStatsWells(stats)
},
setShowTabLoader,
setShowLoader,
'Не удалось загрузить статистику по скважинам/секциям',
{ actionName: 'Получение статистики по скважинам/секциям' }
)
@ -69,32 +60,20 @@ const WellCompositeEditor = memo(() => {
return (
<LoaderPortal show={showLoader}>
<Row align={'middle'} justify={'space-between'} wrap={false} style={{ backgroundColor: 'white' }}>
<Col span={18}>
<Row align={'middle'} justify={'space-between'} wrap={false} style={{ backgroundColor: 'white', marginBottom: '15px' }}>
<WellSelector onChange={setSelectedIdWells} value={selectedIdWells} />
</Col>
<Col span={6}>
<PrivateMenu root={rootPath} className={'well_menu'}>
<PrivateMenu.Link content={ClusterWells} />
<PrivateMenu.Link content={WellCompositeSections} />
</PrivateMenu>
</Col>
</Row>
<Layout>
<Content className={'site-layout-background'}>
<LoaderPortal show={showTabLoader}>
<Suspense fallback={<SuspenseFallback style={{ minHeight: '100%' }} />}>
<Routes>
<Route index element={<Navigate to={ClusterWells.route} replace/>} />
<Route index element={<Navigate to={'wells'} replace/>} />
<Route path={'*'} element={<NoAccessComponent />} />
<Route path={ClusterWells.route} element={<ClusterWells statsWells={statsWells} />} />
<Route path={WellCompositeSections.route} element={<WellCompositeSections statsWells={statsWells} selectedSections={selectedSections} />} />
<Route path={'wells'} element={<ClusterWells statsWells={statsWells} />} />
<Route path={'sections'} element={<WellCompositeSections statsWells={statsWells} selectedSections={selectedSections} />} />
</Routes>
</LoaderPortal>
</Content>
</Layout>
</Suspense>
</LoaderPortal>
)
})
export default wrapPrivateComponent(WellCompositeEditor, properties)
export default wrapPrivateComponent(WellCompositeEditor, { requirements: ['OperationStat.get', 'WellComposite.get'] })

View File

@ -0,0 +1,23 @@
import { memo, useMemo } from 'react'
import { Outlet } from 'react-router-dom'
import { RootPathContext, useRootPath } from '@asb/context'
import { wrapPrivateComponent } from '@utils'
const Analytics = memo(() => {
const root = useRootPath()
const rootPath = useMemo(() => `${root}/analytics`, [root])
return (
<RootPathContext.Provider value={rootPath}>
<Outlet />
</RootPathContext.Provider>
)
})
export default wrapPrivateComponent(Analytics, {
requirements: [],
title: 'Аналитика',
route: 'analytics/*',
key: 'analytics',
})

View File

@ -94,7 +94,7 @@ export const DocumentsTemplate = ({ idCategory, well: givenWell, mimeTypes, head
return (
<LoaderPortal show={showLoader}>
<div style={{ margin: 16, display: 'flex' }}>
<div style={{ margin: 16, marginTop: 0, display: 'flex' }}>
<div>
<span>Фильтр по дате</span>
<div>
@ -158,6 +158,7 @@ export const DocumentsTemplate = ({ idCategory, well: givenWell, mimeTypes, head
onRowDelete={handleFileDelete}
rowKey={(record) => record.id}
tableName={tableName ?? `file_${idCategory}`}
scroll={{ x: true }}
/>
</LoaderPortal>
)

View File

@ -1,16 +1,11 @@
import { Navigate, Route, Routes } from 'react-router-dom'
import { memo, useMemo } from 'react'
import { FolderOutlined } from '@ant-design/icons'
import { Layout } from 'antd'
import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu } from '@components/Private'
import { getTabname, wrapPrivateComponent, NoAccessComponent, hasPermission } from '@utils'
import { wrapPrivateComponent, NoAccessComponent, hasPermission } from '@utils'
import DocumentsTemplate from './DocumentsTemplate'
const { Content } = Layout
const makeDocCat = (id, key, title, permissions = ['File.get']) => ({ id, key, title, permissions })
export const documentCategories = [
@ -27,7 +22,6 @@ export const documentCategories = [
]
const MenuDocuments = memo(() => {
const category = getTabname()
const root = useRootPath()
const rootPath = useMemo(() => `${root}/document`, [root])
@ -35,17 +29,6 @@ const MenuDocuments = memo(() => {
return (
<RootPathContext.Provider value={rootPath}>
<PrivateMenu mode={'horizontal'} selectable={true} className={'well_menu'} selectedKeys={[category]}>
{categories.map(category => (
<PrivateMenu.Link
key={`${category.key}`}
icon={<FolderOutlined/>}
title={category.title}
/>
))}
</PrivateMenu>
<Layout>
<Content className={'site-layout-background'}>
<Routes>
{categories.length > 0 && (
<Route index element={<Navigate to={categories[0].key} replace />} />
@ -61,8 +44,6 @@ const MenuDocuments = memo(() => {
)} />
))}
</Routes>
</Content>
</Layout>
</RootPathContext.Provider>
)
})

View File

@ -159,7 +159,7 @@ export const CategoryEditor = memo(({ visible, category, onClosed }) => {
<Modal
centered
width={1000}
visible={visible}
open={visible}
footer={null}
onCancel={onModalClosed}
title={`Редактирование пользователей категории ${title}`}

View File

@ -94,7 +94,7 @@ export const CategoryHistory = ({ idCategory, visible, onClose }) => {
title={'История категории'}
width={1200}
centered
visible={!!visible}
open={!!visible}
onCancel={onClose}
footer={(
<Button onClick={onClose}>Закрыть</Button>

View File

@ -35,7 +35,7 @@ export const InclinometryTable = memo(({ group, visible, onClose }) => {
<Modal
title={group?.title}
centered
visible={visible}
open={visible}
onCancel={onClose}
width={1900}
footer={null}

View File

View File

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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