* блок utils подразделён на functions, hooks, types и filters

* добавлен хук useFunctionalValue
* добавлен хук useCachedFetch
* удалён RCA
* добавлен конфиг babel
* добавлен конфиг webpack
* обновлены все пакеты
* добавлены базовые моки
* добавлены конфиги для тестов
* добавлена кнопка копирования url
* роутер переписан
* в Messages добавлен переход в Архив при клике на сообщение
This commit is contained in:
goodmice 2022-06-09 17:51:41 +05:00
parent b97066af6e
commit d5e827532d
125 changed files with 9242 additions and 28633 deletions

1
__mocks__/fileMock.js Normal file
View File

@ -0,0 +1 @@
module.exports = 'test-file-stub'

1
__mocks__/styleMock.js Normal file
View File

@ -0,0 +1 @@
module.exports = {}

7
babel.config.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
presets: [
'@babel/preset-env',
['@babel/preset-react', {runtime: 'automatic'}],
'@babel/preset-typescript',
],
}

View File

@ -1,25 +0,0 @@
const CracoLessPlugin = require('craco-less')
const CracoAlias = require('craco-alias')
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
//modifyVars: { '@primary-color': '#E20000' },
javascriptEnabled: true,
},
},
},
}, {
plugin: CracoAlias,
options: {
source: 'tsconfig',
baseUrl: './src',
tsConfigPath: './tsconfig.paths.json'
}
}
],
}

30
custom.d.ts vendored Normal file
View File

@ -0,0 +1,30 @@
declare module '*.gif' {
const src: string;
export default src;
}
declare module '*.jpg' {
const src: string;
export default src;
}
declare module '*.jpeg' {
const src: string;
export default src;
}
declare module '*.png' {
const src: string;
export default src;
}
declare module '*.svg' {
import * as React from 'react'
export const ReactComponent: React.FunctionComponent<
React.SVGProps<SVGSVGElement> & { title?: string }
>
const src: string
export default src
}

33638
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,41 +3,36 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@craco/craco": "^6.1.2", "@microsoft/signalr": "^6.0.5",
"@microsoft/signalr": "^6.0.4", "@svgr/webpack": "^6.2.1",
"@testing-library/jest-dom": "^5.11.10", "@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^11.2.6", "@testing-library/user-event": "^14.2.0",
"@testing-library/user-event": "^12.8.3", "@types/react-dom": "^18.0.5",
"@types/react-dom": "^18.0.3", "antd": "^4.20.7",
"antd": "^4.15.0", "chart.js": "^3.8.0",
"chart.js": "^3.6.0",
"chartjs-adapter-moment": "^1.0.0", "chartjs-adapter-moment": "^1.0.0",
"chartjs-plugin-datalabels": "^2.0.0-rc.1", "chartjs-plugin-datalabels": "^2.0.0",
"chartjs-plugin-zoom": "^1.1.1", "chartjs-plugin-zoom": "^1.2.1",
"craco-less": "^1.17.1",
"d3": "^7.4.4", "d3": "^7.4.4",
"moment": "^2.29.1", "less": "^4.1.2",
"pigeon-maps": "^0.19.7", "less-loader": "^11.0.0",
"react": "^17.0.2", "moment": "^2.29.3",
"react-dom": "^17.0.2", "pigeon-maps": "^0.21.0",
"react-router-dom": "^5.2.0", "react": "^18.1.0",
"react-scripts": "4.0.3", "react-dom": "^18.1.0",
"rxjs": "^7.5.4", "react-router-dom": "^6.3.0",
"typescript": "^4.2.3", "rxjs": "^7.5.5",
"web-vitals": "^1.1.1" "typescript": "^4.7.2",
"web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {
"start": "craco start", "start": "webpack-dev-server --mode=development --open --hot",
"build": "craco build", "build": "webpack --mode=production",
"test": "craco test", "test": "jest",
"oul": "npx openapi -i http://127.0.0.1:5000/swagger/v1/swagger.json -o src/services/api", "oul": "npx openapi -i http://127.0.0.1:5000/swagger/v1/swagger.json -o src/services/api",
"oud": "npx openapi -i http://192.168.1.70:5000/swagger/v1/swagger.json -o src/services/api", "oud": "npx openapi -i http://192.168.1.70:5000/swagger/v1/swagger.json -o src/services/api",
"oug": "npx openapi -i http://46.146.209.148/swagger/v1/swagger.json -o src/services/api", "oug": "npx openapi -i http://46.146.209.148/swagger/v1/swagger.json -o src/services/api",
"oug_dev": "npx openapi -i http://46.146.209.148:89/swagger/v1/swagger.json -o src/services/api", "oug_dev": "npx openapi -i http://46.146.209.148:89/swagger/v1/swagger.json -o src/services/api"
"react_start": "react-scripts start",
"react_build": "react-scripts build",
"react_test": "react-scripts test",
"eject": "react-scripts eject"
}, },
"proxy": "http://46.146.209.148:89", "proxy": "http://46.146.209.148:89",
"eslintConfig": { "eslintConfig": {
@ -58,12 +53,57 @@
"last 1 safari version" "last 1 safari version"
] ]
}, },
"jest": {
"moduleFileExtensions": [
"js",
"jsx",
"ts",
"tsx"
],
"moduleDirectories": [
"node_modules",
"src"
],
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js",
"^@asb(.*)$": "<rootDir>/src$1",
"^@api(.*)$": "<rootDir>/src/services/api$1",
"^@components(.*)$": "<rootDir>/src/components$1",
"^@services(.*)$": "<rootDir>/src/services$1",
"^@pages(.*)$": "<rootDir>/src/pages$1",
"^@utils(.*)$": "<rootDir>/src/utils$1",
"^@images(.*)$": "<rootDir>/src/images$1",
"^@styles(.*)$": "<rootDir>/src/styles$1"
}
},
"devDependencies": { "devDependencies": {
"@types/d3": "^7.1.0", "@babel/core": "^7.18.2",
"@types/react": "^17.0.3", "@babel/preset-env": "^7.18.2",
"@types/react-router-dom": "^5.3.2", "@babel/preset-react": "^7.17.12",
"craco-alias": "^3.0.1", "@babel/preset-typescript": "^7.17.12",
"openapi-typescript": "^3.4.1", "@testing-library/react": "^13.3.0",
"openapi-typescript-codegen": "^0.21.0" "@types/d3": "^7.4.0",
"@types/jest": "^28.1.0",
"@types/react": "^18.0.10",
"@types/react-router-dom": "^5.3.3",
"babel-jest": "^28.1.0",
"babel-loader": "^8.2.5",
"css-loader": "^6.7.1",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.0",
"interpolate-html-plugin": "^4.0.0",
"jest": "^28.1.0",
"openapi-typescript": "^5.4.0",
"openapi-typescript-codegen": "^0.23.0",
"path-browserify": "^1.0.1",
"react-test-renderer": "^18.1.0",
"source-map-loader": "^3.0.1",
"style-loader": "^3.3.1",
"ts-loader": "^9.3.0",
"url-loader": "^4.1.1",
"webpack": "^5.73.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.9.1"
} }
} }

View File

@ -2,16 +2,12 @@
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="white" /> <meta name="theme-color" content="white" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white" /> <meta name="theme-color" media="(prefers-color-scheme: light)" content="white" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" /> <meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" />
<meta <meta name="description" content="Онлайн мониторинг процесса бурения в реальном времени в офисе заказчика" />
name="description"
content="Онлайн мониторинг процесса бурения в реальном времени в офисе заказчика"
/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>АСБ Vision</title> <title>АСБ Vision</title>
</head> </head>
<body> <body>

View File

@ -1,18 +1,17 @@
import { import { BrowserRouter as Router, Navigate, Route, Routes } from 'react-router-dom'
BrowserRouter as Router,
Switch,
Route
} from 'react-router-dom'
import { memo } from 'react' import { memo } from 'react'
import { ConfigProvider } from 'antd' import { ConfigProvider } from 'antd'
import locale from 'antd/lib/locale/ru_RU' import locale from 'antd/lib/locale/ru_RU'
import { PrivateRoute } from '@components/Private' import { RootPathContext } from '@asb/context'
import { getUserToken } from '@utils/storage' import { getUserToken, NoAccessComponent } from '@utils'
import { OpenAPI } from '@api' import { OpenAPI } from '@api'
import Main from '@pages/Main' import AdminPanel from '@pages/AdminPanel'
import Well from '@pages/Well'
import Login from '@pages/Login' import Login from '@pages/Login'
import Cluster from '@pages/Cluster'
import Deposit from '@pages/Deposit'
import Register from '@pages/Register' import Register from '@pages/Register'
import '@styles/App.less' import '@styles/App.less'
@ -23,19 +22,26 @@ OpenAPI.HEADERS = {'Content-Type': 'application/json'}
export const App = memo(() => ( export const App = memo(() => (
<ConfigProvider locale={locale}> <ConfigProvider locale={locale}>
<RootPathContext.Provider value={''}>
<Router> <Router>
<Switch> <Routes>
<Route path={'/login'}> <Route index element={<Navigate to={Deposit.getKey()} replace />} />
<Login /> <Route path={'*'} element={<NoAccessComponent />} />
</Route>
<Route path={'/register'}> {/* Public pages */}
<Register /> <Route path={Login.route} element={<Login />} />
</Route> <Route path={Register.route} element={<Register />} />
<PrivateRoute path={'/'}>
<Main /> {/* Admin pages */}
</PrivateRoute> <Route path={AdminPanel.route} element={<AdminPanel />} />
</Switch>
{/* User pages */}
<Route path={Deposit.route} element={<Deposit />} />
<Route path={Cluster.route} element={<Cluster />} />
<Route path={Well.route} element={<Well />} />
</Routes>
</Router> </Router>
</RootPathContext.Provider>
</ConfigProvider> </ConfigProvider>
)) ))

View File

@ -3,7 +3,7 @@ import { Rule } from 'antd/lib/form'
import { Form, Input, Modal, FormProps } from 'antd' import { Form, Input, Modal, FormProps } from 'antd'
import { AuthService, UserDto } from '@api' import { AuthService, UserDto } from '@api'
import { getUserId, getUserLogin } from '@utils/storage' import { getUserId, getUserLogin } from '@utils'
import { passwordRules, createPasswordRules } from '@utils/validationRules' import { passwordRules, createPasswordRules } from '@utils/validationRules'
import LoaderPortal from './LoaderPortal' import LoaderPortal from './LoaderPortal'

View File

@ -0,0 +1,55 @@
import { cloneElement, memo, useCallback, useMemo, useState } from 'react'
import { Button, ButtonProps } from 'antd'
import { CopyOutlined } from '@ant-design/icons'
import { invokeWebApiWrapperAsync, notify } from './factory'
export type CopyUrlProps = {
sendLoading?: boolean
hideUnsupported?: boolean
onCopy?: () => (void | Promise<void>)
children: JSX.Element
}
export const CopyUrl = memo<CopyUrlProps>(({ children, onCopy, sendLoading, hideUnsupported = true }) => {
const props = useMemo(() => children.props, [children])
const [loading, setLoading] = useState(false)
const supported = !!navigator?.clipboard?.writeText // Проверка поддержки
const onClick = useCallback((event: MouseEvent) => {
if (supported) {
invokeWebApiWrapperAsync(
async () => {
await navigator.clipboard.writeText(window.location.href)
await onCopy?.()
notify('URL успешно скопирован', 'info')
},
setLoading,
`Не удалось скопировать URL в буфер обмена`
)
}
props.onClick?.(event) // Запуск onClick по-умолчанию
}, [props])
if (hideUnsupported && !supported) return null
return cloneElement(children, { onClick, loading: sendLoading ? loading : props.loading })
})
export type CopyUrlButtonProps = Omit<CopyUrlProps, 'children'> & ButtonProps
export const CopyUrlButton = memo<CopyUrlButtonProps>(({ sendLoading, hideUnsupported, onCopy, ...other }) => {
return (
<CopyUrl sendLoading={sendLoading} hideUnsupported={hideUnsupported} onCopy={onCopy}>
<Button
icon={<CopyOutlined />}
title={'Скопировать URL в буфер обмена'}
{...other}
/>
</CopyUrl>
)
})
export default CopyUrl

View File

@ -15,7 +15,7 @@ export const AdminLayoutPortal = memo<AdminLayoutPortalProps>(({ title, ...props
<Layout.Content> <Layout.Content>
<PageHeader isAdmin title={title} style={{ backgroundColor: '#900' }}> <PageHeader isAdmin title={title} style={{ backgroundColor: '#900' }}>
<Button size={'large'}> <Button size={'large'}>
<Link to={{ pathname: '/', state: { from: location.pathname }}}>Вернуться на сайт</Link> <Link to={'/'}>Вернуться на сайт</Link>
</Button> </Button>
</PageHeader> </PageHeader>
<Layout> <Layout>

View File

@ -3,13 +3,14 @@ import { Layout, LayoutProps } from 'antd'
import PageHeader from '@components/PageHeader' import PageHeader from '@components/PageHeader'
import WellTreeSelector from '@components/selectors/WellTreeSelector' import WellTreeSelector from '@components/selectors/WellTreeSelector'
import { wrapPrivateComponent } from '@utils'
export type LayoutPortalProps = LayoutProps & { export type LayoutPortalProps = LayoutProps & {
title?: ReactNode title?: ReactNode
noSheet?: boolean noSheet?: boolean
} }
export const LayoutPortal = memo<LayoutPortalProps>(({ title, noSheet, ...props }) => ( const _LayoutPortal = memo<LayoutPortalProps>(({ title, noSheet, ...props }) => (
<Layout.Content> <Layout.Content>
<PageHeader title={title}> <PageHeader title={title}>
<WellTreeSelector /> <WellTreeSelector />
@ -22,4 +23,8 @@ export const LayoutPortal = memo<LayoutPortalProps>(({ title, noSheet, ...props
</Layout.Content> </Layout.Content>
)) ))
export const LayoutPortal = wrapPrivateComponent(_LayoutPortal, {
requirements: ['Deposit.get'],
})
export default LayoutPortal export default LayoutPortal

View File

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

View File

@ -20,7 +20,7 @@ export const PageHeader: React.FC<PageHeaderProps> = memo(({ title = 'Монит
return ( return (
<Layout> <Layout>
<Layout.Header className={'header'} {...other}> <Layout.Header className={'header'} {...other}>
<Link to={{ pathname: '/', state: { from: location.pathname }}} style={{ height: headerHeight }}> <Link to={'/'} style={{ height: headerHeight }}>
<Logo /> <Logo />
</Link> </Link>
{children} {children}

View File

@ -1,6 +1,6 @@
import { memo, ReactElement } from 'react' import { memo, ReactElement } from 'react'
import { isURLAvailable } from '@utils/permissions' import { isURLAvailable } from '@utils'
export type PrivateContentProps = { export type PrivateContentProps = {
absolutePath: string absolutePath: string

View File

@ -1,25 +1,19 @@
import { memo } from 'react' import { memo } from 'react'
import { Redirect, Route, RouteProps, useLocation } from 'react-router-dom' import { Navigate, Route, RouteProps } from 'react-router-dom'
import { getUserId } from '@utils/storage' import { isURLAvailable } from '@utils'
import { isURLAvailable } from '@utils/permissions'
import { getDefaultRedirectPath } from './PrivateRoutes'
export type PrivateDefaultRouteProps = RouteProps & { export type PrivateDefaultRouteProps = RouteProps & {
urls: string[] urls: string[]
elseRedirect?: string elseRedirect?: string
} }
export const PrivateDefaultRoute = memo<PrivateDefaultRouteProps>(({ elseRedirect, urls, ...other }) => { export const PrivateDefaultRoute = memo<PrivateDefaultRouteProps>(({ elseRedirect, urls, ...other }) => (
const location = useLocation() <Route {...other} path={'/'} element={(
<Navigate replace to={urls.find((url) => isURLAvailable(url)) ?? elseRedirect ?? getDefaultRedirectPath()} />
return ( )} />
<Route {...other} path={'/'}> ))
<Redirect to={{
pathname: urls.find((url) => isURLAvailable(url)) ?? elseRedirect ?? (getUserId() ? '/access_denied' : '/login'),
state: { from: location.pathname },
}} />
</Route>
)
})
export default PrivateDefaultRoute export default PrivateDefaultRoute

View File

@ -1,47 +1,76 @@
import { join } from 'path' import { join } from 'path'
import { Menu, MenuItemProps, MenuProps } from 'antd' import { Menu, MenuProps } from 'antd'
import { Children, cloneElement, memo, ReactElement, useContext, useMemo } from 'react' import { ItemType } from 'antd/lib/menu/hooks/useItems'
import { Link, useLocation } from 'react-router-dom' import { Link, LinkProps } from 'react-router-dom'
import { Children, isValidElement, memo, ReactNode, RefAttributes, useMemo } from 'react'
import { RootPathContext } from '@asb/context' import { useRootPath } from '@asb/context'
import { isURLAvailable } from '@utils/permissions' import { getTabname, hasPermission, PrivateComponent, PrivateProps } from '@utils'
export type PrivateMenuProps = MenuProps & { root?: string } export type PrivateMenuProps = MenuProps & { root?: string }
export type PrivateMenuLinkProps = MenuItemProps & { export type PrivateMenuLinkProps = Partial<ItemType> & Omit<LinkProps, 'to'> & RefAttributes<HTMLAnchorElement> & {
tabName?: string icon?: ReactNode
danger?: boolean
title?: ReactNode
content?: PrivateComponent<any>
path?: string path?: string
title: string
visible?: boolean visible?: boolean
permissions?: string[]
} }
export const PrivateMenuLink = memo<PrivateMenuLinkProps>(({ tabName = '', path = '', title, ...other }) => { export const PrivateMenuLink = memo<PrivateMenuLinkProps>(({ content, danger, icon, path = '', title, ...other }) => (
const location = useLocation() <Menu.Item icon={icon ?? content?.icon} danger={danger}>
return ( <Link to={path} {...other}>{title ?? content?.title}</Link>
<Menu.Item key={tabName} {...other}>
<Link to={{ pathname: path, state: { from: location.pathname }}}>{title}</Link>
</Menu.Item> </Menu.Item>
) ))
})
const PrivateMenuMain = memo<PrivateMenuProps>(({ root, children, ...other }) => { const PrivateMenuMain = memo<PrivateMenuProps>(({ selectable, mode, selectedKeys, root, children, ...other }) => {
const rootContext = useContext(RootPathContext) const rootContext = useRootPath()
const rootPath = useMemo(() => root ?? rootContext ?? '', [root, rootContext]) const rootPath = useMemo(() => root ?? rootContext ?? '', [root, rootContext])
const items = useMemo(() => Children.toArray(children).map((child) => { const tab = getTabname()
const element = child as ReactElement const keys = useMemo(() => selectedKeys ?? (tab ? [tab] : []), [selectedKeys, tab])
let key = element.key?.toString()
const visible: boolean | undefined = element.props.visible const items = useMemo(() => Children.map(children, (child) => {
if (key && visible !== false) { if (!child || !isValidElement<PrivateMenuLinkProps>(child))
key = key.slice(key.lastIndexOf('$') + 1) // Ключ автоматический преобразуется в "(.+)\$ключ" return null
const path = join(rootPath, key) const content: PrivateProps | undefined = child.props.content
if (visible || isURLAvailable(path)) const visible: boolean | undefined = child.props.visible
return cloneElement(element, { key, path, tabName: key })
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,
icon: null,
key,
label: <PrivateMenuLink {...child.props} path={path} />,
} }
return null return null
}), [children, rootPath]) })?.filter((v) => v) ?? [], [children, rootPath])
return <Menu children={items} {...other} /> return (
<Menu
selectable={selectable ?? true}
mode={mode ?? 'horizontal'}
selectedKeys={keys}
items={items as ItemType[]}
{...other}
/>
)
}) })
export const PrivateMenu = Object.assign(PrivateMenuMain, { Link: PrivateMenuLink }) export const PrivateMenu = Object.assign(PrivateMenuMain, { Link: PrivateMenuLink })

View File

@ -3,7 +3,7 @@ import { Menu, MenuItemProps } from 'antd'
import { memo, NamedExoticComponent } from 'react' import { memo, NamedExoticComponent } from 'react'
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { isURLAvailable } from '@utils/permissions' import { isURLAvailable } from '@utils'
export type PrivateMenuItemProps = MenuItemProps & { export type PrivateMenuItemProps = MenuItemProps & {
root: string root: string
@ -20,7 +20,7 @@ export const PrivateMenuItemLink = memo<PrivateMenuItemLinkProps>(({ root = '',
const location = useLocation() const location = useLocation()
return ( return (
<PrivateMenuItem key={path} root={root} path={path} {...other}> <PrivateMenuItem key={path} root={root} path={path} {...other}>
<Link to={{ pathname: join(root, path), state: { from: location.pathname }}}>{title}</Link> <Link to={join(root, path)}>{title}</Link>
</PrivateMenuItem> </PrivateMenuItem>
) )
}) })

View File

@ -1,31 +1,28 @@
import { Location } from 'history'
import { memo, ReactNode } from 'react'
import { Redirect, Route, RouteProps } from 'react-router-dom'
import { join } from 'path' import { join } from 'path'
import { memo, ReactNode } from 'react'
import { Navigate, Route, RouteProps } from 'react-router-dom'
import { getUserId } from '@utils/storage' import { getUserId, isURLAvailable } from '@utils'
import { isURLAvailable } from '@utils/permissions'
export type PrivateRouteProps = RouteProps & { export type PrivateRouteProps = RouteProps & {
root?: string root?: string
path: string path: string
children?: ReactNode children?: ReactNode
redirect?: (location?: Location<unknown>) => ReactNode redirect?: ReactNode
} }
export const defaultRedirect = (location?: Location<unknown>) => ( export const defaultRedirect = (
<Redirect to={{ pathname: getUserId() ? '/access_denied' : '/login', state: { from: location?.pathname } }} /> <Navigate to={getUserId() ? '/access_denied' : '/login'} />
) )
export const PrivateRoute = memo<PrivateRouteProps>(({ root = '', path, component, children, redirect = defaultRedirect, ...other }) => { export const PrivateRoute = memo<PrivateRouteProps>(({ root = '', path, children, redirect = defaultRedirect, ...other }) => {
const available = isURLAvailable(join(root, path)) const available = isURLAvailable(join(root, path))
return ( return (
<Route <Route
{...other} {...other}
path={path} path={path}
component={available ? component : undefined} element={available ? children : redirect}
render={({ location }) => available ? children : redirect(location)}
/> />
) )
}) })

View File

@ -0,0 +1,67 @@
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,75 +0,0 @@
import { join } from 'path'
import { Location } from 'history'
import { Children, cloneElement, memo, ReactElement, ReactNode, useCallback, useContext, useMemo } from 'react'
import { Redirect, Route, Switch, SwitchProps, useLocation } from 'react-router-dom'
import { RootPathContext } from '@asb/context'
import { isURLAvailable } from '@utils/permissions'
import { getUserId } from '@utils/storage'
export type PrivateSwitchProps = SwitchProps & {
root?: string
redirect?: (location?: Location<unknown>) => ReactNode
elseRedirect?: string | string[]
}
const getDefaultRedirectPath = () => getUserId() ? '/access_denied' : '/login'
export const defaultRedirect = (location?: Location<unknown>) => (
<Redirect to={{ pathname: getDefaultRedirectPath(), state: { from: location?.pathname } }} />
)
export const PrivateSwitch = memo<PrivateSwitchProps>(({ root, elseRedirect, redirect = defaultRedirect, children }) => {
const rootContext = useContext(RootPathContext)
const rootPath = useMemo(() => root ?? rootContext ?? '', [root, rootContext])
const location = useLocation()
const toAbsolute = useCallback((path: string) => path.startsWith('/') ? path : join(rootPath, path), [rootPath])
const items = useMemo(() => Children.toArray(children).map((child) => {
const element = child as ReactElement
let key = element.key?.toString()
if (!key) return null
key = key.slice(key.lastIndexOf('$') + 1).replaceAll('=2', ':')
// Ключ автоматический преобразуется в "(.+)\$ключ"
// Все ":" в ключе заменяются на "=2"
// TODO: улучшить метод нормализации ключа
const path = toAbsolute(key)
return (
<Route
key={key}
path={path}
render={({ location }) => isURLAvailable(path) ? cloneElement(element) : redirect(location)}
/>
)
}), [children, redirect, toAbsolute])
const defaultRoute = useMemo(() => {
if (!elseRedirect) {
const path = items.map((elm) => elm?.props.path).find((path) => path && isURLAvailable(path))
if (path) return path
} else if (Array.isArray(elseRedirect)) {
const path = elseRedirect.find((path) => {
if (!path) return false
return isURLAvailable(toAbsolute(path))
})
if (path) return toAbsolute(path)
} else if(elseRedirect && isURLAvailable(toAbsolute(elseRedirect))) {
return toAbsolute(elseRedirect)
}
return getDefaultRedirectPath()
}, [items, elseRedirect, toAbsolute])
return (
<Switch>
{items}
<Route path={'/'}>
<Redirect to={{ pathname: defaultRoute, state: { from: location.pathname } }} />
</Route>
</Switch>
)
})
export default PrivateSwitch

View File

@ -3,11 +3,11 @@ export { PrivateContent } from './PrivateContent' // TODO: Remove
export { PrivateMenuItem, PrivateMenuItemLink } from './PrivateMenuItem' // TODO: Remove export { PrivateMenuItem, PrivateMenuItemLink } from './PrivateMenuItem' // TODO: Remove
export { PrivateDefaultRoute } from './PrivateDefaultRoute' export { PrivateDefaultRoute } from './PrivateDefaultRoute'
export { PrivateMenu, PrivateMenuLink } from './PrivateMenu' export { PrivateMenu, PrivateMenuLink } from './PrivateMenu'
export { PrivateSwitch } from './PrivateSwitch' export { PrivateRoutes } from './PrivateRoutes'
export type { PrivateRouteProps } from './PrivateRoute' export type { PrivateRouteProps } from './PrivateRoute'
export type { PrivateContentProps } from './PrivateContent' // TODO: Remove export type { PrivateContentProps } from './PrivateContent' // TODO: Remove
export type { PrivateMenuItemProps, PrivateMenuItemLinkProps } from './PrivateMenuItem' // TODO: Remove export type { PrivateMenuItemProps, PrivateMenuItemLinkProps } from './PrivateMenuItem' // TODO: Remove
export type { PrivateDefaultRouteProps } from './PrivateDefaultRoute' export type { PrivateDefaultRouteProps } from './PrivateDefaultRoute'
export type { PrivateMenuProps, PrivateMenuLinkProps } from './PrivateMenu' export type { PrivateMenuProps, PrivateMenuLinkProps } from './PrivateMenu'
export type { PrivateSwitchProps } from './PrivateSwitch' export type { PrivateRoutesProps } from './PrivateRoutes'

View File

@ -1,8 +1,8 @@
import { memo, ReactNode, useCallback, useEffect, useState } from 'react' import { memo, ReactNode, useCallback, useEffect, useState } from 'react'
import { Select, SelectProps, Tag } from 'antd'
import { DefaultOptionType, SelectValue } from 'antd/lib/select' import { DefaultOptionType, SelectValue } from 'antd/lib/select'
import { Select, SelectProps, Tag } from 'antd'
import { OmitExtends } from '@utils' import type { OmitExtends } from '@utils/types'
import { columnPropsOther, DataType, makeColumn } from '.' import { columnPropsOther, DataType, makeColumn } from '.'

View File

@ -1,6 +1,6 @@
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { formatTime } from '@utils/datetime' import { formatTime } from '@utils'
import { makeColumn, columnPropsOther } from '.' import { makeColumn, columnPropsOther } from '.'
import { makeTimeSorter, TimePickerWrapper, TimePickerWrapperProps } from '..' import { makeTimeSorter, TimePickerWrapper, TimePickerWrapperProps } from '..'

View File

@ -1,8 +1,8 @@
import { memo, ReactNode, useCallback, useEffect, useState } from 'react' import { memo, ReactNode, useCallback, useEffect, useState } from 'react'
import { Select, SelectProps } from 'antd' import { Select, SelectProps } from 'antd'
import { OmitExtends } from '@utils' import { findTimezoneId, rawTimezones, TimezoneId } from '@utils'
import { findTimezoneId, rawTimezones, TimezoneId } from '@utils/datetime' import type { OmitExtends } from '@utils/types'
import { SimpleTimezoneDto } from '@api' import { SimpleTimezoneDto } from '@api'
import { columnPropsOther, makeColumn } from '.' import { columnPropsOther, makeColumn } from '.'

View File

@ -1,16 +1,16 @@
import { memo } from 'react' import { memo, ReactNode } from 'react'
import { Form, Input } from 'antd' import { Form, Input } from 'antd'
import { NamePath, Rule } from 'rc-field-form/lib/interface' import { NamePath, Rule } from 'rc-field-form/lib/interface'
type EditableCellProps = React.DetailedHTMLProps<React.TdHTMLAttributes<HTMLTableDataCellElement>, HTMLTableDataCellElement> & { type EditableCellProps = React.DetailedHTMLProps<React.TdHTMLAttributes<HTMLTableDataCellElement>, HTMLTableDataCellElement> & {
editing?: boolean editing?: boolean
dataIndex?: NamePath dataIndex?: NamePath
input?: React.Component input?: ReactNode
isRequired?: boolean isRequired?: boolean
title: string title: string
formItemClass?: string formItemClass?: string
formItemRules?: Rule[] formItemRules?: Rule[]
children: React.ReactNode children: ReactNode
initialValue: any initialValue: any
} }

View File

@ -2,9 +2,8 @@ import { memo, useCallback, useEffect, useState } from 'react'
import { ColumnGroupType, ColumnType } from 'antd/lib/table' import { ColumnGroupType, ColumnType } from 'antd/lib/table'
import { Table as RawTable, TableProps } from 'antd' import { Table as RawTable, TableProps } from 'antd'
import { OmitExtends } from '@utils' import type { OmitExtends } from '@utils/types'
import { getTableSettings, setTableSettings } from '@utils/storage' import { applyTableSettings, getTableSettings, setTableSettings, TableColumnSettings, TableSettings } from '@utils'
import { applySettings, ColumnSettings, TableSettings } from '@utils/table_settings'
import TableSettingsChanger from './TableSettingsChanger' import TableSettingsChanger from './TableSettingsChanger'
import { tryAddKeys } from './EditableTable' import { tryAddKeys } from './EditableTable'
@ -12,7 +11,7 @@ import { tryAddKeys } from './EditableTable'
import '@styles/index.css' import '@styles/index.css'
export type BaseTableColumn<T = any> = ColumnGroupType<T> | ColumnType<T> export type BaseTableColumn<T = any> = ColumnGroupType<T> | ColumnType<T>
export type TableColumns<T = any> = OmitExtends<BaseTableColumn<T>, ColumnSettings>[] export type TableColumns<T = any> = OmitExtends<BaseTableColumn<T>, TableColumnSettings>[]
export type TableContainer = TableProps<any> & { export type TableContainer = TableProps<any> & {
columns: TableColumns columns: TableColumns
@ -33,7 +32,7 @@ export const Table = memo<TableContainer>(({ columns, dataSource, tableName, sho
useEffect(() => setSettings(tableName ? getTableSettings(tableName) : {}), [tableName]) useEffect(() => setSettings(tableName ? getTableSettings(tableName) : {}), [tableName])
useEffect(() => setNewColumns(() => { useEffect(() => setNewColumns(() => {
const newColumns = applySettings(columns, settings) const newColumns = applyTableSettings(columns, settings)
if (tableName && showSettingsChanger) { if (tableName && showSettingsChanger) {
const oldTitle = newColumns[0].title const oldTitle = newColumns[0].title
newColumns[0].title = (props) => ( newColumns[0].title = (props) => (

View File

@ -3,16 +3,16 @@ import { ColumnsType } from 'antd/lib/table'
import { Button, Modal, Switch, Table } from 'antd' import { Button, Modal, Switch, Table } from 'antd'
import { SettingOutlined } from '@ant-design/icons' import { SettingOutlined } from '@ant-design/icons'
import { ColumnSettings, makeSettings, mergeSettings, TableSettings } from '@utils/table_settings' import { TableColumnSettings, makeTableSettings, mergeTableSettings, TableSettings } from '@utils'
import { TableColumns } from './Table' import { TableColumns } from './Table'
import { makeColumn } from '.' import { makeColumn } from '.'
const parseSettings = (columns?: TableColumns, settings?: TableSettings | null): ColumnSettings[] => { const parseSettings = (columns?: TableColumns, settings?: TableSettings | null): TableColumnSettings[] => {
const newSettings = mergeSettings(makeSettings(columns ?? []), settings ?? {}) const newSettings = mergeTableSettings(makeTableSettings(columns ?? []), settings ?? {})
return Object.values(newSettings).map((set, i) => ({ ...set, key: i })) return Object.values(newSettings).map((set, i) => ({ ...set, key: i }))
} }
const unparseSettings = (columns: ColumnSettings[]): TableSettings => const unparseSettings = (columns: TableColumnSettings[]): TableSettings =>
Object.fromEntries(columns.map((column) => [column.columnName, column])) Object.fromEntries(columns.map((column) => [column.columnName, column]))
export type TableSettingsChangerProps = { export type TableSettingsChangerProps = {
@ -24,8 +24,8 @@ export type TableSettingsChangerProps = {
export const TableSettingsChanger = memo<TableSettingsChangerProps>(({ title, columns, settings, onChange }) => { export const TableSettingsChanger = memo<TableSettingsChangerProps>(({ title, columns, settings, onChange }) => {
const [visible, setVisible] = useState<boolean>(false) const [visible, setVisible] = useState<boolean>(false)
const [newSettings, setNewSettings] = useState<ColumnSettings[]>(parseSettings(columns, settings)) const [newSettings, setNewSettings] = useState<TableColumnSettings[]>(parseSettings(columns, settings))
const [tableColumns, setTableColumns] = useState<ColumnsType<ColumnSettings>>([]) const [tableColumns, setTableColumns] = useState<ColumnsType<TableColumnSettings>>([])
const onVisibilityChange = useCallback((index: number, visible: boolean) => { const onVisibilityChange = useCallback((index: number, visible: boolean) => {
setNewSettings((oldSettings) => { setNewSettings((oldSettings) => {
@ -52,7 +52,7 @@ export const TableSettingsChanger = memo<TableSettingsChangerProps>(({ title, co
<Button type={'link'} onClick={() => toogleAll(true)}>Показать все</Button> <Button type={'link'} onClick={() => toogleAll(true)}>Показать все</Button>
</> </>
), ),
render: (visible: boolean, _?: ColumnSettings, index: number = NaN) => ( render: (visible: boolean, _?: TableColumnSettings, index: number = NaN) => (
<Switch <Switch
checked={visible} checked={visible}
checkedChildren={'Отображён'} checkedChildren={'Отображён'}

View File

@ -2,7 +2,7 @@ import { Moment } from 'moment'
import { TimePicker, TimePickerProps } from 'antd' import { TimePicker, TimePickerProps } from 'antd'
import { memo, useCallback, useMemo } from 'react' import { memo, useCallback, useMemo } from 'react'
import { defaultTimeFormat, momentToTime, timeToMoment } from '@utils/datetime' import { defaultTimeFormat, momentToTime, timeToMoment } from '@utils'
import { TimeDto } from '@api' import { TimeDto } from '@api'
export type TimePickerWrapperProps = Omit<Omit<TimePickerProps, 'value'>, 'onChange'> & { export type TimePickerWrapperProps = Omit<Omit<TimePickerProps, 'value'>, 'onChange'> & {

View File

@ -1,4 +1,4 @@
import { timeToMoment } from '@utils/datetime' import { timeToMoment } from '@utils'
import { isRawDate } from '@utils' import { isRawDate } from '@utils'
import { TimeDto } from '@api' import { TimeDto } from '@api'

View File

@ -1,19 +1,20 @@
import { memo, MouseEventHandler, useCallback, useState } from 'react' import { memo, MouseEventHandler, useCallback, useState } from 'react'
import { Link, useHistory, useLocation } from 'react-router-dom' import { useNavigate, useLocation } from 'react-router-dom'
import { Button, Dropdown, DropDownProps, Menu } from 'antd' import { Button, Dropdown, DropDownProps } from 'antd'
import { UserOutlined } from '@ant-design/icons' import { UserOutlined } from '@ant-design/icons'
import { getUserLogin, removeUser } from '@utils/storage' import { getUserLogin, removeUser } from '@utils'
import { ChangePassword } from './ChangePassword' import { ChangePassword } from './ChangePassword'
import { PrivateMenuItemLink } from './Private/PrivateMenuItem' import { PrivateMenu } from './Private'
import AdminPanel from '@pages/AdminPanel'
type UserMenuProps = Omit<DropDownProps, 'overlay'> & { isAdmin?: boolean } type UserMenuProps = Omit<DropDownProps, 'overlay'> & { isAdmin?: boolean }
export const UserMenu = memo<UserMenuProps>(({ isAdmin, ...other }) => { export const UserMenu = memo<UserMenuProps>(({ isAdmin, ...other }) => {
const [isModalVisible, setIsModalVisible] = useState<boolean>(false) const [isModalVisible, setIsModalVisible] = useState<boolean>(false)
const history = useHistory() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const onChangePasswordClick: MouseEventHandler = useCallback((e) => { const onChangePasswordClick: MouseEventHandler = useCallback((e) => {
@ -23,8 +24,8 @@ export const UserMenu = memo<UserMenuProps>(({ isAdmin, ...other }) => {
const onChangePasswordOk = useCallback(() => { const onChangePasswordOk = useCallback(() => {
setIsModalVisible(false) setIsModalVisible(false)
history.push({ pathname: '/login', state: { from: location.pathname }}) navigate('/login', { state: { from: location.pathname }})
}, [history, location]) }, [navigate, location])
return ( return (
<> <>
@ -32,19 +33,15 @@ export const UserMenu = memo<UserMenuProps>(({ isAdmin, ...other }) => {
{...other} {...other}
placement={'bottomRight'} placement={'bottomRight'}
overlay={( overlay={(
<Menu style={{ textAlign: 'right' }}> <PrivateMenu style={{ textAlign: 'right' }}>
{isAdmin ? ( {isAdmin ? (
<PrivateMenuItemLink key={''} path={'/'} title={'Вернуться на сайт'}/> <PrivateMenu.Link visible key={'/'} path={'/'} title={'Вернуться на сайт'}/>
) : ( ) : (
<PrivateMenuItemLink key={'admin'} path={'/admin'} title={'Панель администратора'}/> <PrivateMenu.Link key={'admin'} path={'/admin'} title={'Панель администратора'} content={AdminPanel}/>
)} )}
<Menu.Item> <PrivateMenu.Link visible onClick={onChangePasswordClick} title={'Сменить пароль'} />
<Link to={'/'} onClick={onChangePasswordClick}>Сменить пароль</Link> <PrivateMenu.Link visible path={'/login'} onClick={removeUser} title={'Выход'} />
</Menu.Item> </PrivateMenu>
<Menu.Item>
<Link to={{ pathname: '/login', state: { from: location.pathname }}} onClick={removeUser}>Выход</Link>
</Menu.Item>
</Menu>
)} )}
> >
<Button icon={<UserOutlined/>}>{getUserLogin()}</Button> <Button icon={<UserOutlined/>}>{getUserLogin()}</Button>

View File

@ -2,7 +2,7 @@ import { notification } from 'antd'
import { Dispatch, ReactNode, SetStateAction } from 'react' import { Dispatch, ReactNode, SetStateAction } from 'react'
import { isDev } from '@utils' import { isDev } from '@utils'
import { getUserToken } from '@utils/storage' import { getUserToken } from '@utils'
import { ApiError, FileInfoDto } from '@api' import { ApiError, FileInfoDto } from '@api'
const notificationTypeDictionary = new Map([ const notificationTypeDictionary = new Map([

View File

@ -2,7 +2,7 @@ import { Tag, TreeSelect } from 'antd'
import { memo, useEffect, useState } from 'react' import { memo, useEffect, useState } from 'react'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { hasPermission } from '@utils/permissions' import { hasPermission } from '@utils'
import { DepositService } from '@api' import { DepositService } from '@api'
export const getTreeData = async () => { export const getTreeData = async () => {
@ -40,7 +40,8 @@ export const WellSelector = memo(({ idWell, value, onChange, treeData, treeLabel
const [wellsTree, setWellsTree] = useState([]) const [wellsTree, setWellsTree] = useState([])
const [wellLabels, setWellLabels] = useState([]) const [wellLabels, setWellLabels] = useState([])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const wellsTree = treeData ?? await getTreeData() const wellsTree = treeData ?? await getTreeData()
const labels = treeLabels ?? getTreeLabels(wellsTree) const labels = treeLabels ?? getTreeLabels(wellsTree)
@ -50,7 +51,8 @@ export const WellSelector = memo(({ idWell, value, onChange, treeData, treeLabel
null, null,
'Не удалось загрузить список скважин', 'Не удалось загрузить список скважин',
'Получение списка скважин' 'Получение списка скважин'
), [idWell, treeData, treeLabels]) )
}, [idWell, treeData, treeLabels])
return ( return (
<TreeSelect <TreeSelect

View File

@ -3,7 +3,7 @@ import { LabelInValueType } from 'rc-select/lib/Select'
import { RawValueType } from 'rc-tree-select/lib/TreeSelect' import { RawValueType } from 'rc-tree-select/lib/TreeSelect'
import { DefaultValueType } from 'rc-tree-select/lib/interface' import { DefaultValueType } from 'rc-tree-select/lib/interface'
import { useState, useEffect, ReactNode, useCallback, memo } from 'react' import { useState, useEffect, ReactNode, useCallback, memo } from 'react'
import { useHistory, useLocation, useRouteMatch } from 'react-router-dom' import { useNavigate, useLocation } from 'react-router-dom'
import { isRawDate } from '@utils' import { isRawDate } from '@utils'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
@ -29,40 +29,47 @@ export type TreeNodeData = {
} }
const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined => { const getLabel = (wellsTree: TreeNodeData[], value?: string): string | undefined => {
if (!value) return value const result = value?.match(/^\/([^\/]+)\/([^\/?]+)/) // pattern "/:type/:id"
const type = value.replaceAll('/', ' ').trim().split(' ')[0] if (wellsTree.length <= 0 || !result) return
const [url, type] = result
let deposit: TreeNodeData | undefined let deposit: TreeNodeData | undefined
let cluster: TreeNodeData | undefined let cluster: TreeNodeData | undefined
let well: TreeNodeData | undefined let well: TreeNodeData | undefined
switch (type) { switch (type) {
case 'deposit':
deposit = wellsTree.find((deposit) => deposit.key === url)
if (deposit)
return `${deposit.title}`
return 'Ошибка! Месторождение не найдено!'
case 'cluster': case 'cluster':
deposit = wellsTree.find((deposit) => ( deposit = wellsTree.find((deposit) => (
cluster = deposit.children?.find((cluster: TreeNodeData) => cluster.key === value) cluster = deposit.children?.find((cluster: TreeNodeData) => cluster.key === url)
)) ))
if (deposit && cluster) if (deposit && cluster)
return `${deposit.title} / ${cluster.title}` return `${deposit.title} / ${cluster.title}`
break return 'Ошибка! Куст не найден!'
case 'well': case 'well':
deposit = wellsTree.find((deposit) => ( deposit = wellsTree.find((deposit) => (
cluster = deposit.children?.find((cluster: TreeNodeData) => ( cluster = deposit.children?.find((cluster: TreeNodeData) => (
well = cluster.children?.find((well: TreeNodeData) => well.key === value) well = cluster.children?.find((well: TreeNodeData) => well.key === url)
)) ))
)) ))
if (deposit && cluster && well) if (deposit && cluster && well)
return `${deposit.title} / ${cluster.title} / ${well.title}` return `${deposit.title} / ${cluster.title} / ${well.title}`
break return 'Ошибка! Скважина не найдена!'
default: break default: break
} }
return 'Ошибка! Скважина не найдена!'
} }
export const WellTreeSelector = memo(({ ...other }) => { export const WellTreeSelector = memo(({ ...other }) => {
const [wellsTree, setWellsTree] = useState<TreeNodeData[]>([]) const [wellsTree, setWellsTree] = useState<TreeNodeData[]>([])
const [showLoader, setShowLoader] = useState<boolean>(false) const [showLoader, setShowLoader] = useState<boolean>(false)
const [value, setValue] = useState<string>() const [value, setValue] = useState<string>()
const history = useHistory() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const routeMatch = useRouteMatch('/:route/:id')
useEffect(() => { useEffect(() => {
invokeWebApiWrapperAsync( invokeWebApiWrapperAsync(
@ -99,14 +106,14 @@ export const WellTreeSelector = memo(({ ...other }) => {
) )
}, []) }, [])
useEffect(() => setValue(getLabel(wellsTree, routeMatch?.url)), [wellsTree, routeMatch]) useEffect(() => setValue(getLabel(wellsTree, location.pathname)), [wellsTree, location])
const onChange = useCallback((value?: string): void => setValue(getLabel(wellsTree, value)), [wellsTree]) const onChange = useCallback((value?: string): void => setValue(getLabel(wellsTree, value)), [wellsTree])
const onSelect = useCallback((value: RawValueType | LabelInValueType): void => { const onSelect = useCallback((value: RawValueType | LabelInValueType): void => {
if (['number', 'string'].includes(typeof value)) if (['number', 'string'].includes(typeof value))
history.push({ pathname: String(value), state: { from: location.pathname }}) navigate(String(value), { state: { from: location.pathname }})
}, [history, location]) }, [navigate, location])
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>

View File

@ -1,7 +1,9 @@
import { memo } from 'react' import { memo } from 'react'
import logo from '@images/logo_32.png'
export const Logo = memo<React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>>((props) => ( export const Logo = memo<React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>>((props) => (
<img src={'/images/logo_32.png'} alt={'АСБ'} className={'logo'} {...props} /> <img src={logo} alt={'АСБ'} className={'logo'} {...props} />
)) ))
export default Logo export default Logo

0
public/images/logo_32.png → src/images/logo_32.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -1,15 +1,19 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import { createRoot } from 'react-dom/client'
import App from './App'
import reportWebVitals from './reportWebVitals' import reportWebVitals from './reportWebVitals'
import App from './App'
import '@styles/index.css' import '@styles/index.css'
ReactDOM.render(( const container = document.getElementById('root') ?? document.body
const root = createRoot(container)
root.render(
<React.StrictMode> <React.StrictMode>
<App /> <App />
</React.StrictMode> </React.StrictMode>
), document.getElementById('root')) )
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log)) // to log results (for example: reportWebVitals(console.log))

View File

@ -1,12 +1,12 @@
import { Result, Tooltip, Typography } from 'antd' import { Result, Typography } from 'antd'
import { memo } from 'react' import { memo } from 'react'
import { Link, useHistory } from 'react-router-dom' import { Link, useNavigate } from 'react-router-dom'
import { CloseCircleOutlined } from '@ant-design/icons' import { CloseCircleOutlined } from '@ant-design/icons'
const { Paragraph, Text } = Typography const { Paragraph, Text } = Typography
export const AccessDenied = memo(() => { export const AccessDenied = memo(() => {
const history = useHistory() const navigate = useNavigate()
return ( return (
<Result <Result
@ -28,7 +28,7 @@ export const AccessDenied = memo(() => {
<Paragraph> <Paragraph>
<CloseCircleOutlined style={{ color: 'red' }} /> <CloseCircleOutlined style={{ color: 'red' }} />
&nbsp;Страницы не существует.&nbsp; &nbsp;Страницы не существует.&nbsp;
<Link to={'#'} onClick={history.goBack}>Вернуться назад &gt;</Link> <Link to={'#'} onClick={navigate(-1)}>Вернуться назад &gt;</Link>
</Paragraph> </Paragraph>
<Paragraph> <Paragraph>
<CloseCircleOutlined style={{ color: 'red' }} /> <CloseCircleOutlined style={{ color: 'red' }} />

View File

@ -12,13 +12,12 @@ import {
} from '@components/Table' } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminClusterService, AdminDepositService } from '@api' import { AdminClusterService, AdminDepositService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
import { hasPermission } from '@utils/permissions'
import { coordsFixed } from './DepositController' import { coordsFixed } from './DepositController'
export const ClusterController = memo(() => { const ClusterController = memo(() => {
const [deposits, setDeposits] = useState([]) const [deposits, setDeposits] = useState([])
const [clusters, setClusters] = useState([]) const [clusters, setClusters] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
@ -58,7 +57,8 @@ export const ClusterController = memo(() => {
'Получение списка кустов' 'Получение списка кустов'
), []) ), [])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
let deposits = arrayOrDefault(await AdminDepositService.getAll()) let deposits = arrayOrDefault(await AdminDepositService.getAll())
deposits = deposits.map((deposit) => ({ value: deposit.id, label: deposit.caption })) deposits = deposits.map((deposit) => ({ value: deposit.id, label: deposit.caption }))
@ -67,9 +67,12 @@ export const ClusterController = memo(() => {
setShowLoader, setShowLoader,
`Не удалось загрузить список месторождений`, `Не удалось загрузить список месторождений`,
'Получение списка месторождений' 'Получение списка месторождений'
), []) )
}, [])
useEffect(updateTable, [updateTable]) useEffect(() => {
updateTable()
}, [updateTable])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: AdminClusterService, service: AdminClusterService,
@ -103,4 +106,8 @@ export const ClusterController = memo(() => {
) )
}) })
export default ClusterController export default wrapPrivateComponent(ClusterController, {
requirements: ['AdminDeposit.get', 'AdminCluster.get'],
title: 'Кусты',
route: 'cluster',
})

View File

@ -11,12 +11,10 @@ import {
} from '@components/Table' } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminCompanyService, AdminCompanyTypeService } from '@api' import { AdminCompanyService, AdminCompanyTypeService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
import { hasPermission } from '@utils/permissions'
const CompanyController = memo(() => {
export const CompanyController = memo(() => {
const [columns, setColumns] = useState([]) const [columns, setColumns] = useState([])
const [companies, setCompanies] = useState([]) const [companies, setCompanies] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
@ -31,7 +29,8 @@ export const CompanyController = memo(() => {
setCompanies(arrayOrDefault(companies)) setCompanies(arrayOrDefault(companies))
}, []) }, [])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async() => { async() => {
const companyTypes = arrayOrDefault(await AdminCompanyTypeService.getAll()).map((companyType) => ({ const companyTypes = arrayOrDefault(await AdminCompanyTypeService.getAll()).map((companyType) => ({
value: companyType.id, value: companyType.id,
@ -56,7 +55,8 @@ export const CompanyController = memo(() => {
setShowLoader, setShowLoader,
`Не удалось загрузить список типов компаний`, `Не удалось загрузить список типов компаний`,
'Получение списка типов команд' 'Получение списка типов команд'
), [updateTable]) )
}, [updateTable])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: AdminCompanyService, service: AdminCompanyService,
@ -95,4 +95,8 @@ export const CompanyController = memo(() => {
) )
}) })
export default CompanyController export default wrapPrivateComponent(CompanyController, {
requirements: ['AdminCompany.get', 'AdminCompanyType.get'],
title: 'Компании',
route: 'company',
})

View File

@ -9,10 +9,9 @@ import {
defaultPagination defaultPagination
} from '@components/Table' } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminCompanyTypeService } from '@api' import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { arrayOrDefault } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
import { hasPermission } from '@utils/permissions' import { AdminCompanyTypeService } from '@api'
const columns = [ const columns = [
makeColumn('Название', 'caption', { makeColumn('Название', 'caption', {
@ -23,7 +22,7 @@ const columns = [
}), }),
] ]
export const CompanyTypeController = memo(() => { const CompanyTypeController = memo(() => {
const [companyTypes, setCompanyTypes] = useState([]) const [companyTypes, setCompanyTypes] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [searchValue, setSearchValue] = useState('') const [searchValue, setSearchValue] = useState('')
@ -42,7 +41,9 @@ export const CompanyTypeController = memo(() => {
'Получение списка типов компаний' 'Получение списка типов компаний'
), []) ), [])
useEffect(updateTable, [updateTable]) useEffect(() => {
updateTable()
}, [updateTable])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: AdminCompanyTypeService, service: AdminCompanyTypeService,
@ -76,4 +77,8 @@ export const CompanyTypeController = memo(() => {
) )
}) })
export default CompanyTypeController export default wrapPrivateComponent(CompanyTypeController, {
requirements: ['AdminCompanyType.get'],
title: 'Типы компаний',
route: 'company_type',
})

View File

@ -3,9 +3,8 @@ import { Input } from 'antd'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { EditableTable, makeColumn, makeActionHandler, defaultPagination, makeTimezoneColumn } from '@components/Table' import { EditableTable, makeColumn, makeActionHandler, defaultPagination, makeTimezoneColumn } from '@components/Table'
import { hasPermission } from '@utils/permissions' import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
import { arrayOrDefault } from '@utils'
import { AdminDepositService } from '@api' import { AdminDepositService } from '@api'
export const coordsFixed = (coords) => coords && isFinite(coords) ? (+coords).toPrecision(10) : '-' export const coordsFixed = (coords) => coords && isFinite(coords) ? (+coords).toPrecision(10) : '-'
@ -17,7 +16,7 @@ const depositColumns = [
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }), makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
] ]
export const DepositController = memo(() => { const DepositController = memo(() => {
const [deposits, setDeposits] = useState([]) const [deposits, setDeposits] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [searchValue, setSearchValue] = useState('') const [searchValue, setSearchValue] = useState('')
@ -39,7 +38,9 @@ export const DepositController = memo(() => {
'Получение списка месторождений' 'Получение списка месторождений'
), []) ), [])
useEffect(updateTable, [updateTable]) useEffect(() => {
updateTable()
}, [updateTable])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: AdminDepositService, service: AdminDepositService,
@ -73,4 +74,8 @@ export const DepositController = memo(() => {
) )
}) })
export default DepositController export default wrapPrivateComponent(DepositController, {
requirements: ['AdminDeposit.get'],
title: 'Месторождения',
route: 'deposit',
})

View File

@ -8,10 +8,9 @@ import {
makeStringSorter makeStringSorter
} from '@components/Table' } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminPermissionService } from '@api' import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { arrayOrDefault } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
import { hasPermission } from '@utils/permissions' import { AdminPermissionService } from '@api'
const columns = [ const columns = [
makeColumn('Название', 'name', { makeColumn('Название', 'name', {
@ -26,7 +25,7 @@ const columns = [
}), }),
] ]
export const PermissionController = memo(() => { const PermissionController = memo(() => {
const [permissions, setPermissions] = useState([]) const [permissions, setPermissions] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [searchValue, setSearchValue] = useState('') const [searchValue, setSearchValue] = useState('')
@ -47,7 +46,9 @@ export const PermissionController = memo(() => {
'Получение списка прав' 'Получение списка прав'
), []) ), [])
useEffect(() => updateTable(), [updateTable]) useEffect(() => {
updateTable()
}, [updateTable])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: AdminPermissionService, service: AdminPermissionService,
@ -81,4 +82,8 @@ export const PermissionController = memo(() => {
) )
}) })
export default PermissionController export default wrapPrivateComponent(PermissionController, {
requirements: ['AdminPermission.get'],
title: 'Разрешения',
route: 'permission',
})

View File

@ -5,11 +5,10 @@ import { PermissionView, RoleView } from '@components/views'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { EditableTable, makeActionHandler, makeTagColumn, makeTextColumn } from '@components/Table' import { EditableTable, makeActionHandler, makeTagColumn, makeTextColumn } from '@components/Table'
import { AdminPermissionService, AdminUserRoleService } from '@api' import { AdminPermissionService, AdminUserRoleService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { min1 } from '@utils/validationRules' import { min1 } from '@utils/validationRules'
import { hasPermission } from '@utils/permissions'
export const RoleController = memo(() => { const RoleController = memo(() => {
const [permissions, setPermissions] = useState([]) const [permissions, setPermissions] = useState([])
const [roles, setRoles] = useState([]) const [roles, setRoles] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
@ -38,7 +37,8 @@ export const RoleController = memo(() => {
setRoles(arrayOrDefault(roles)) setRoles(arrayOrDefault(roles))
}, []) }, [])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const permissions = await AdminPermissionService.getAll() const permissions = await AdminPermissionService.getAll()
setPermissions(arrayOrDefault(permissions)) setPermissions(arrayOrDefault(permissions))
@ -47,7 +47,8 @@ export const RoleController = memo(() => {
setShowLoader, setShowLoader,
`Не удалось загрузить список ролей`, `Не удалось загрузить список ролей`,
'Получение списка ролей' 'Получение списка ролей'
), [loadRoles]) )
}, [loadRoles])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: AdminUserRoleService, service: AdminUserRoleService,
@ -85,4 +86,8 @@ export const RoleController = memo(() => {
) )
}) })
export default RoleController export default wrapPrivateComponent(RoleController, {
requirements: ['AdminPermission.get', 'AdminUserRole.get'],
title: 'Роли',
route: 'role',
})

View File

@ -8,8 +8,8 @@ import LoaderPortal from '@components/LoaderPortal'
import { lables } from '@components/views/TelemetryView' import { lables } from '@components/views/TelemetryView'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import TelemetrySelect from '@components/selectors/TelemetrySelect' import TelemetrySelect from '@components/selectors/TelemetrySelect'
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import { AdminTelemetryService } from '@api' import { AdminTelemetryService } from '@api'
import { arrayOrDefault } from '@utils'
const { Item } = Descriptions const { Item } = Descriptions
@ -32,7 +32,7 @@ export const TelemetryInfo = memo(({ info, danger, ...other }) => (
</Descriptions> </Descriptions>
)) ))
export const TelemetryMerger = memo(() => { const TelemetryMerger = memo(() => {
const [primary, setPrimary] = useState(null) const [primary, setPrimary] = useState(null)
const [secondary, setSecondary] = useState(null) const [secondary, setSecondary] = useState(null)
const [telemetry, setTelemetry] = useState(null) const [telemetry, setTelemetry] = useState(null)
@ -68,7 +68,9 @@ export const TelemetryMerger = memo(() => {
'Объединение телеметрий', 'Объединение телеметрий',
), [updateTelemetry, secondary, primary]) ), [updateTelemetry, secondary, primary])
useEffect(updateTelemetry, [updateTelemetry]) useEffect(() => {
updateTelemetry()
}, [updateTelemetry])
useEffect(() => { useEffect(() => {
const query = new URLSearchParams(location.search) const query = new URLSearchParams(location.search)
@ -132,4 +134,9 @@ export const TelemetryMerger = memo(() => {
) )
}) })
export default TelemetryMerger export default wrapPrivateComponent(TelemetryMerger, {
requirements: [],
title: 'Объединение',
route: 'merger',
key: 'merger',
})

View File

@ -1,4 +1,5 @@
import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { PullRequestOutlined } from '@ant-design/icons' import { PullRequestOutlined } from '@ant-design/icons'
import { Button, Input } from 'antd' import { Button, Input } from 'antd'
@ -13,18 +14,17 @@ import {
} from '@components/Table' } from '@components/Table'
import Poprompt from '@components/selectors/Poprompt' import Poprompt from '@components/selectors/Poprompt'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import { AdminTelemetryService } from '@api' import { AdminTelemetryService } from '@api'
import { arrayOrDefault } from '@utils'
import { useHistory } from 'react-router-dom'
export const TelemetryController = memo(() => { const TelemetryController = memo(() => {
const [telemetryData, setTelemetryData] = useState([]) const [telemetryData, setTelemetryData] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [searchValue, setSearchValue] = useState('') const [searchValue, setSearchValue] = useState('')
const history = useHistory() const navigate = useNavigate()
const toMerger = useCallback((type, id) => () => history.push(`/admin/telemetry/merger/?${type}=${id}`), [history]) const toMerger = useCallback((type, id) => () => navigate(`/admin/telemetry/merger/?${type}=${id}`), [navigate])
const mergeRender = useCallback((value, record) => ( const mergeRender = useCallback((value, record) => (
<Poprompt <Poprompt
@ -81,7 +81,8 @@ export const TelemetryController = memo(() => {
].join(' ').toLowerCase().includes(searchValue.toLowerCase())) ].join(' ').toLowerCase().includes(searchValue.toLowerCase()))
), [telemetryData, searchValue]) ), [telemetryData, searchValue])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const telemetryData = arrayOrDefault(await AdminTelemetryService.getAll()) const telemetryData = arrayOrDefault(await AdminTelemetryService.getAll())
setTelemetryData(telemetryData.map((telemetry) => ({ setTelemetryData(telemetryData.map((telemetry) => ({
@ -94,7 +95,8 @@ export const TelemetryController = memo(() => {
setShowLoader, setShowLoader,
`Не удалось загрузить список телеметрии скважин`, `Не удалось загрузить список телеметрии скважин`,
'Полученик списка телеметрии скважин' 'Полученик списка телеметрии скважин'
), []) )
}, [])
return ( return (
<> <>
@ -118,4 +120,9 @@ export const TelemetryController = memo(() => {
) )
}) })
export default TelemetryController export default wrapPrivateComponent(TelemetryController, {
requirements: [],
title: 'Просмотр',
route: 'viewer',
key: 'viewer',
})

View File

@ -1,37 +1,34 @@
import { Layout } from 'antd' import { Layout } from 'antd'
import { lazy, memo, Suspense, useContext, useMemo } from 'react' import { memo, useMemo } from 'react'
import { useParams } from 'react-router-dom' import { Navigate, Route, Routes } from 'react-router-dom'
import { RootPathContext } from '@asb/context' import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu, PrivateSwitch } from '@components/Private' import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import { SuspenseFallback } from '@pages/SuspenseFallback' import TelemetryViewer from './TelemetryViewer'
import TelemetryMerger from './TelemetryMerger'
const TelemetryViewer = lazy(() => import('./TelemetryViewer')) const Telemetry = memo(() => {
const TelemetryMerger = lazy(() => import('./TelemetryMerger')) const root = useRootPath()
export const Telemetry = memo(() => {
const { tab } = useParams()
const root = useContext(RootPathContext)
const rootPath = useMemo(() => `${root}/telemetry`, [root]) const rootPath = useMemo(() => `${root}/telemetry`, [root])
return ( return (
<RootPathContext.Provider value={rootPath}> <RootPathContext.Provider value={rootPath}>
<Layout> <Layout>
<PrivateMenu mode={'horizontal'} selectable={true} selectedKeys={[tab]}> <PrivateMenu>
<PrivateMenu.Link key={'viewer'} title={'Просмотр'} /> <PrivateMenu.Link content={TelemetryViewer} />
<PrivateMenu.Link key={'merger'} title={'Объединение'} /> <PrivateMenu.Link content={TelemetryMerger} />
</PrivateMenu> </PrivateMenu>
<Layout> <Layout>
<Layout.Content className={'site-layout-background'}> <Layout.Content className={'site-layout-background'}>
<Suspense fallback={<SuspenseFallback />}> <Routes>
<PrivateSwitch elseRedirect={['viewer', 'merger']}> <Route index element={<Navigate to={TelemetryViewer.route} replace />} />
<TelemetryViewer key={'viewer'} /> <Route path={'*'} element={<NoAccessComponent />} />
<TelemetryMerger key={'merger'} /> <Route path={TelemetryViewer.route} element={<TelemetryViewer />} />
</PrivateSwitch> <Route path={TelemetryMerger.route} element={<TelemetryMerger />} />
</Suspense> </Routes>
</Layout.Content> </Layout.Content>
</Layout> </Layout>
</Layout> </Layout>
@ -39,4 +36,9 @@ export const Telemetry = memo(() => {
) )
}) })
export default Telemetry export default wrapPrivateComponent(Telemetry, {
requirements: ['AdminTelemetry.get'],
title: 'Телеметрия',
key: 'telemetry',
route: 'telemetry/*',
})

View File

@ -17,15 +17,14 @@ import { ChangePassword } from '@components/ChangePassword'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminCompanyService, AdminUserRoleService, AdminUserService } from '@api' import { AdminCompanyService, AdminUserRoleService, AdminUserService } from '@api'
import { createLoginRules, nameRules, phoneRules, emailRules } from '@utils/validationRules' import { createLoginRules, nameRules, phoneRules, emailRules } from '@utils/validationRules'
import { makeTextOnFilter, makeTextFilters, makeArrayOnFilter } from '@utils/table' import { makeTextOnFilter, makeTextFilters, makeArrayOnFilter } from '@utils/filters'
import { hasPermission } from '@utils/permissions' import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { arrayOrDefault } from '@utils'
import RoleTag from './RoleTag' import RoleTag from './RoleTag'
const SEARCH_TIMEOUT = 400 const SEARCH_TIMEOUT = 400
export const UserController = memo(() => { const UserController = memo(() => {
const [users, setUsers] = useState([]) const [users, setUsers] = useState([])
const [filteredUsers, setFilteredUsers] = useState([]) const [filteredUsers, setFilteredUsers] = useState([])
const [searchValue, setSearchValue] = useState('') const [searchValue, setSearchValue] = useState('')
@ -35,7 +34,8 @@ export const UserController = memo(() => {
const [selectedUser, setSelectedUser] = useState(null) const [selectedUser, setSelectedUser] = useState(null)
const [subject, setSubject] = useState(null) const [subject, setSubject] = useState(null)
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const filteredUsers = users.filter((user) => user && (!searchValue || [ const filteredUsers = users.filter((user) => user && (!searchValue || [
user.login ?? '', user.login ?? '',
@ -51,7 +51,8 @@ export const UserController = memo(() => {
}, },
setIsSearching, setIsSearching,
`Не удалось произвести поиск пользователей` `Не удалось произвести поиск пользователей`
), [users, searchValue]) )
}, [users, searchValue])
useEffect(() => { useEffect(() => {
if (!subject) { if (!subject) {
@ -91,7 +92,8 @@ export const UserController = memo(() => {
'Получение списка пользователей' 'Получение списка пользователей'
), []) ), [])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const roles = arrayOrDefault(await AdminUserRoleService.getAll()) const roles = arrayOrDefault(await AdminUserRoleService.getAll())
const companies = arrayOrDefault(await AdminCompanyService.getAll()).map((company) => ({ const companies = arrayOrDefault(await AdminCompanyService.getAll()).map((company) => ({
@ -170,7 +172,8 @@ export const UserController = memo(() => {
setShowLoader, setShowLoader,
`Не удалось загрузить список компаний`, `Не удалось загрузить список компаний`,
'Получение списка компаний' 'Получение списка компаний'
), []) )
}, [])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: AdminUserService, service: AdminUserService,
@ -215,4 +218,8 @@ export const UserController = memo(() => {
) )
}) })
export default UserController export default wrapPrivateComponent(UserController, {
requirements: ['AdminUser.get', 'AdminCompany.get', 'AdminUserRole.get'],
title: 'Пользователи',
route: 'user',
})

View File

@ -3,8 +3,8 @@ import { Input } from 'antd'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { defaultPagination, makeColumn, makeDateSorter, makeStringSorter, Table } from '@components/Table' import { defaultPagination, makeColumn, makeDateSorter, makeStringSorter, Table } from '@components/Table'
import { arrayOrDefault, formatDate, wrapPrivateComponent } from '@utils'
import { RequestTrackerService } from '@api' import { RequestTrackerService } from '@api'
import { arrayOrDefault, formatDate } from '@utils'
const logRecordCount = 1000 const logRecordCount = 1000
@ -17,7 +17,7 @@ const columns = [
}), }),
] ]
export const VisitLog = memo(() => { const VisitLog = memo(() => {
const [logData, setLogData] = useState([]) const [logData, setLogData] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [searchValue, setSearchValue] = useState('') const [searchValue, setSearchValue] = useState('')
@ -29,7 +29,8 @@ export const VisitLog = memo(() => {
].join(' ').toLowerCase().includes(searchValue.toLowerCase())) ].join(' ').toLowerCase().includes(searchValue.toLowerCase()))
), [logData, searchValue]) ), [logData, searchValue])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const logData = arrayOrDefault(await RequestTrackerService.getUsersStat(logRecordCount)) const logData = arrayOrDefault(await RequestTrackerService.getUsersStat(logRecordCount))
logData.forEach((log) => log.key = `${log.login}${log.ip}`) logData.forEach((log) => log.key = `${log.login}${log.ip}`)
@ -38,7 +39,8 @@ export const VisitLog = memo(() => {
setShowLoader, setShowLoader,
`Не удалось загрузить список последних посещений пользователей`, `Не удалось загрузить список последних посещений пользователей`,
'Получение списка последних посещений' 'Получение списка последних посещений'
), []) )
}, [])
return ( return (
<> <>
@ -62,4 +64,8 @@ export const VisitLog = memo(() => {
) )
}) })
export default VisitLog export default wrapPrivateComponent(VisitLog, {
requirements: ['RequestTracker.get'],
title: 'Журнал посещений',
route: 'visit_log',
})

View File

@ -22,8 +22,7 @@ import {
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { TelemetryView, CompanyView } from '@components/views' import { TelemetryView, CompanyView } from '@components/views'
import TelemetrySelect from '@components/selectors/TelemetrySelect' import TelemetrySelect from '@components/selectors/TelemetrySelect'
import { hasPermission } from '@utils/permissions' import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { arrayOrDefault } from '@utils'
import { coordsFixed } from '../DepositController' import { coordsFixed } from '../DepositController'
@ -37,7 +36,7 @@ const recordParser = (record) => ({
idTelemetry: record.telemetry?.id, idTelemetry: record.telemetry?.id,
}) })
export const WellController = memo(() => { const WellController = memo(() => {
const [columns, setColumns] = useState([]) const [columns, setColumns] = useState([])
const [wells, setWells] = useState([]) const [wells, setWells] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
@ -74,7 +73,8 @@ export const WellController = memo(() => {
/> />
)) ))
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const companies = arrayOrDefault(await AdminCompanyService.getAll()) const companies = arrayOrDefault(await AdminCompanyService.getAll())
const telemetry = arrayOrDefault(await AdminTelemetryService.getAll()) const telemetry = arrayOrDefault(await AdminTelemetryService.getAll())
@ -118,7 +118,8 @@ export const WellController = memo(() => {
setShowLoader, setShowLoader,
`Не удалось загрузить список кустов`, `Не удалось загрузить список кустов`,
'Получение списка кустов' 'Получение списка кустов'
), [updateTable]) )
}, [updateTable])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: AdminWellService, service: AdminWellService,
@ -154,4 +155,8 @@ export const WellController = memo(() => {
) )
}) })
export default WellController export default wrapPrivateComponent(WellController, {
requirements: ['AdminCluster.get', 'AdminCompany.get', 'AdminTelemetry.get', 'AdminWell.get'],
title: 'Скважины',
route: 'well',
})

View File

@ -1,66 +1,69 @@
import { Navigate, Route, Routes } from 'react-router-dom'
import { memo, useMemo } from 'react'
import { Layout } from 'antd' import { Layout } from 'antd'
import { lazy, memo, Suspense, useContext, useMemo } from 'react'
import { useParams } from 'react-router-dom'
import { RootPathContext } from '@asb/context' import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu, PrivateSwitch } from '@components/Private' import { AdminLayoutPortal } from '@components/Layout'
import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import { SuspenseFallback } from '@pages/SuspenseFallback' 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'
const ClusterController = lazy(() => import( './ClusterController')) const AdminPanel = memo(() => {
const CompanyController = lazy(() => import( './CompanyController')) const root = useRootPath()
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 TelemetrySection = lazy(() => import( './Telemetry'))
const VisitLog = lazy(() => import( './VisitLog'))
export const AdminPanel = memo(() => {
const { tab } = useParams()
const root = useContext(RootPathContext)
const rootPath = useMemo(() => `${root}/admin`, [root]) const rootPath = useMemo(() => `${root}/admin`, [root])
return ( return (
<RootPathContext.Provider value={rootPath}> <RootPathContext.Provider value={rootPath}>
<Layout> <AdminLayoutPortal title={'Администраторская панель'}>
<PrivateMenu mode={'horizontal'} selectable={true} selectedKeys={[tab]}> <PrivateMenu>
<PrivateMenu.Link key={'deposit' } title={'Месторождения' } /> <PrivateMenu.Link content={DepositController} />
<PrivateMenu.Link key={'cluster' } title={'Кусты' } /> <PrivateMenu.Link content={ClusterController} />
<PrivateMenu.Link key={'well' } title={'Скважины' } /> <PrivateMenu.Link content={WellController} />
<PrivateMenu.Link key={'user' } title={'Пользователи' } /> <PrivateMenu.Link content={UserController} />
<PrivateMenu.Link key={'company' } title={'Компании' } /> <PrivateMenu.Link content={CompanyController} />
<PrivateMenu.Link key={'company_type'} title={'Типы компаний' } /> <PrivateMenu.Link content={CompanyTypeController} />
<PrivateMenu.Link key={'role' } title={'Роли' } /> <PrivateMenu.Link content={RoleController} />
<PrivateMenu.Link key={'permission' } title={'Разрешения' } /> <PrivateMenu.Link content={PermissionController} />
<PrivateMenu.Link key={'telemetry' } title={'Телеметрия' } /> <PrivateMenu.Link content={Telemetry} />
<PrivateMenu.Link key={'visit_log' } title={'Журнал посещений'} /> <PrivateMenu.Link content={VisitLog} />
</PrivateMenu> </PrivateMenu>
<Layout> <Layout>
<Layout.Content className={'site-layout-background'}> <Layout.Content className={'site-layout-background'}>
<Suspense fallback={<SuspenseFallback />}> <Routes>
<PrivateSwitch elseRedirect={['deposit', 'cluster', 'well', 'user', 'company', 'company_type', 'role', 'permission', 'telemetry', 'visit_log']}> <Route index element={<Navigate to={VisitLog.route} replace />} />
<DepositController key={'deposit'} /> <Route path={'*'} element={<NoAccessComponent />} />
<ClusterController key={'cluster'} /> <Route path={DepositController.route} element={<DepositController />} />
<WellController key={'well'} /> <Route path={ClusterController.route} element={<ClusterController />} />
<UserController key={'user'} /> <Route path={WellController.route} element={<WellController />} />
<CompanyController key={'company'} /> <Route path={UserController.route} element={<UserController />} />
<CompanyTypeController key={'company_type'} /> <Route path={CompanyController.route} element={<CompanyController />} />
<RoleController key={'role'} /> <Route path={CompanyTypeController.route} element={<CompanyTypeController />} />
<PermissionController key={'permission'} /> <Route path={RoleController.route} element={<RoleController />} />
<TelemetrySection key={'telemetry/:tab?'} /> <Route path={PermissionController.route} element={<PermissionController />} />
<VisitLog key={'visit_log'} /> <Route path={Telemetry.route} element={<Telemetry />} />
</PrivateSwitch> <Route path={VisitLog.route} element={<VisitLog />} />
</Suspense> </Routes>
</Layout.Content> </Layout.Content>
</Layout> </Layout>
</Layout> </AdminLayoutPortal>
</RootPathContext.Provider> </RootPathContext.Provider>
) )
}) })
export default AdminPanel export default wrapPrivateComponent(AdminPanel, {
requirements: ['RequestTracker.get'],
title: 'Панель администратора',
route: 'admin/*',
key: 'admin',
})

View File

@ -1,14 +1,13 @@
import { Table as RawTable, Typography } from 'antd' import { Table as RawTable, Typography } from 'antd'
import { Fragment, memo, useCallback, useContext, useEffect, useState } from 'react' import { Fragment, memo, useCallback, useEffect, useState } from 'react'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { WellSelector } from '@components/selectors/WellSelector' import { WellSelector } from '@components/selectors/WellSelector'
import { makeGroupColumn, makeNumericColumn, makeNumericRender, makeTextColumn, Table } from '@components/Table' import { makeGroupColumn, makeNumericColumn, makeNumericRender, makeTextColumn, Table } from '@components/Table'
import { OperationStatService, WellOperationService } from '@api' import { OperationStatService, WellOperationService } from '@api'
import { arrayOrDefault } from '@utils' import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import '@styles/index.css' import '@styles/index.css'
import '@styles/statistics.less' import '@styles/statistics.less'
@ -64,7 +63,7 @@ const getWellData = async (wellsList) => {
return wellData return wellData
} }
export const Statistics = memo(() => { const Statistics = memo(() => {
const [sectionTypes, setSectionTypes] = useState([]) const [sectionTypes, setSectionTypes] = useState([])
const [avgColumns, setAvgColumns] = useState(defaultColumns) const [avgColumns, setAvgColumns] = useState(defaultColumns)
const [cmpColumns, setCmpColumns] = useState(defaultColumns) const [cmpColumns, setCmpColumns] = useState(defaultColumns)
@ -77,7 +76,7 @@ export const Statistics = memo(() => {
const [cmpData, setCmpData] = useState([]) const [cmpData, setCmpData] = useState([])
const [avgRow, setAvgRow] = useState({}) const [avgRow, setAvgRow] = useState({})
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const cmpSpeedRender = useCallback((key) => (section) => { const cmpSpeedRender = useCallback((key) => (section) => {
let spanClass = '' let spanClass = ''
@ -97,7 +96,8 @@ export const Statistics = memo(() => {
) )
}, [avgRow]) }, [avgRow])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const types = await WellOperationService.getSectionTypes(idWell) const types = await WellOperationService.getSectionTypes(idWell)
setSectionTypes(Object.entries(types)) setSectionTypes(Object.entries(types))
@ -105,9 +105,11 @@ export const Statistics = memo(() => {
setIsPageLoading, setIsPageLoading,
`Не удалось получить типы секции`, `Не удалось получить типы секции`,
`Получение списка возможных секций`, `Получение списка возможных секций`,
), [idWell]) )
}, [idWell])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const filteredSections = avgData?.length > 0 ? sectionTypes.filter(([id, _]) => avgData.some((row) => `section_${id}` in row)) : sectionTypes const filteredSections = avgData?.length > 0 ? sectionTypes.filter(([id, _]) => avgData.some((row) => `section_${id}` in row)) : sectionTypes
@ -124,9 +126,11 @@ export const Statistics = memo(() => {
}, },
setIsPageLoading, setIsPageLoading,
'Не удалось установить необходимые столбцы' 'Не удалось установить необходимые столбцы'
), [sectionTypes, avgData, cmpSpeedRender]) )
}, [sectionTypes, avgData, cmpSpeedRender])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const avgData = await getWellData(avgWells) const avgData = await getWellData(avgWells)
setAvgData(avgData) setAvgData(avgData)
@ -148,16 +152,19 @@ export const Statistics = memo(() => {
}, },
setIsAvgTableLoading, setIsAvgTableLoading,
'Не удалось загрузить данные для расчёта средних значений', 'Не удалось загрузить данные для расчёта средних значений',
), [avgWells]) )
}, [avgWells])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const cmpData = await getWellData(cmpWells) const cmpData = await getWellData(cmpWells)
setCmpData(cmpData) setCmpData(cmpData)
}, },
setIsCmpTableLoading, setIsCmpTableLoading,
'Не удалось получить скважины для сравнения', 'Не удалось получить скважины для сравнения',
), [cmpWells]) )
}, [cmpWells])
const getStatisticsAvgSummary = useCallback((data) => ( const getStatisticsAvgSummary = useCallback((data) => (
<Summary fixed={'bottom'}> <Summary fixed={'bottom'}>
@ -234,4 +241,8 @@ export const Statistics = memo(() => {
) )
}) })
export default Statistics export default wrapPrivateComponent(Statistics, {
requirements: [],
title: 'Оценка по ЦБ',
route: 'statistics',
})

View File

@ -1,7 +1,7 @@
import { memo, useCallback, useContext, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useState } from 'react'
import { Button, Modal, Popconfirm } from 'antd' import { Button, Modal, Popconfirm, Tooltip } from 'antd'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { Table } from '@components/Table' import { Table } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
@ -15,13 +15,11 @@ export const NewParamsTable = memo(({ selectedWellsKeys }) => {
const [showParamsLoader, setShowParamsLoader] = useState(false) const [showParamsLoader, setShowParamsLoader] = useState(false)
const [isParamsModalVisible, setIsParamsModalVisible] = useState(false) const [isParamsModalVisible, setIsParamsModalVisible] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
async () => setParamsColumns(await getColumns(idWell)) invokeWebApiWrapperAsync(async () => setParamsColumns(await getColumns(idWell)))
), [idWell]) }, [idWell])
useEffect(() => console.log(paramsColumns), [paramsColumns])
const onParamButtonClick = useCallback(() => invokeWebApiWrapperAsync( const onParamButtonClick = useCallback(() => invokeWebApiWrapperAsync(
async () => { async () => {
@ -61,7 +59,10 @@ export const NewParamsTable = memo(({ selectedWellsKeys }) => {
width={1700} width={1700}
footer={( footer={(
<Popconfirm title={'Заменить существующие режимы выбранными?'} onConfirm={onParamsAddClick}> <Popconfirm title={'Заменить существующие режимы выбранными?'} onConfirm={onParamsAddClick}>
<Button size={'large'} disabled={params.length <= 0}>Сохранить</Button> <Button
size={'large'}
disabled={params.length <= 0}
>Сохранить</Button>
</Popconfirm> </Popconfirm>
)} )}
> >

View File

@ -1,22 +1,23 @@
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { useState, useEffect, memo, useMemo, useContext } from 'react' import { useState, useEffect, memo, useMemo } from 'react'
import { LineChartOutlined, ProfileOutlined } from '@ant-design/icons' import { LineChartOutlined, ProfileOutlined } from '@ant-design/icons'
import { Table, Tag, Button, Badge, Divider, Modal, Row, Col } from 'antd' import { Table, Tag, Button, Badge, Divider, Modal, Row, Col } from 'antd'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { CompanyView } from '@components/views' import { CompanyView } from '@components/views'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { makeTextColumn, makeNumericColumnPlanFact, makeNumericColumn } from '@components/Table' import { makeTextColumn, makeNumericColumnPlanFact, makeNumericColumn } from '@components/Table'
import { WellCompositeService } from '@api' import { WellCompositeService } from '@api'
import { hasPermission } from '@utils/permissions'
import { import {
hasPermission,
wrapPrivateComponent,
calcAndUpdateStatsBySections, calcAndUpdateStatsBySections,
makeFilterMinMaxFunction, makeFilterMinMaxFunction,
getOperations getOperations
} from '@utils/functions' } from '@utils'
import { Tvd } from '@pages/WellOperations/Tvd' import Tvd from '@pages/WellOperations/Tvd'
import WellOperationsTable from '@pages/Cluster/WellOperationsTable' import WellOperationsTable from '@pages/Cluster/WellOperationsTable'
import NewParamsTable from './NewParamsTable' import NewParamsTable from './NewParamsTable'
@ -30,7 +31,7 @@ const sortBySectionId = (a, b) => a?.sectionId - b?.sectionId
const filtersSectionsType = [] const filtersSectionsType = []
const DAY_IN_MS = 1000 * 60 * 60 * 24 const DAY_IN_MS = 1000 * 60 * 60 * 24
export const WellCompositeSections = memo(({ statsWells, selectedSections }) => { const WellCompositeSections = memo(({ statsWells, selectedSections }) => {
const [selectedWells, setSelectedWells] = useState([]) const [selectedWells, setSelectedWells] = useState([])
const [wellOperations, setWellOperations] = useState([]) const [wellOperations, setWellOperations] = useState([])
const [selectedWellsKeys, setSelectedWellsKeys] = useState([]) const [selectedWellsKeys, setSelectedWellsKeys] = useState([])
@ -39,7 +40,7 @@ export const WellCompositeSections = memo(({ statsWells, selectedSections }) =>
const [isTVDModalVisible, setIsTVDModalVisible] = useState(false) const [isTVDModalVisible, setIsTVDModalVisible] = useState(false)
const [isOpsModalVisible, setIsOpsModalVisible] = useState(false) const [isOpsModalVisible, setIsOpsModalVisible] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const location = useLocation() const location = useLocation()
@ -239,4 +240,8 @@ export const WellCompositeSections = memo(({ statsWells, selectedSections }) =>
) )
}) })
export default WellCompositeSections export default wrapPrivateComponent(WellCompositeSections, {
requirements: ['WellComposite.get'],
title: 'Статистика по секциям',
route: 'sections',
})

View File

@ -1,23 +1,31 @@
import { useState, useEffect, memo, useContext } from 'react' import { useState, useEffect, memo, useMemo } from 'react'
import { useParams } from 'react-router-dom' import { Navigate, Route, Routes } from 'react-router-dom'
import { Col, Layout, Row } from 'antd' import { Col, Layout, Row } from 'antd'
import { IdWellContext } from '@asb/context' import { useIdWell, useRootPath } from '@asb/context'
import { PrivateMenu } from '@components/Private'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import WellSelector from '@components/selectors/WellSelector' import WellSelector from '@components/selectors/WellSelector'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { PrivateMenu, PrivateSwitch } from '@components/Private' import { arrayOrDefault, NoAccessComponent, wrapPrivateComponent } from '@utils'
import { arrayOrDefault } from '@utils'
import { OperationStatService, WellCompositeService } from '@api' import { OperationStatService, WellCompositeService } from '@api'
import ClusterWells from '@pages/Cluster/ClusterWells' import ClusterWells from '@pages/Cluster/ClusterWells'
import { WellCompositeSections } from './WellCompositeSections' import WellCompositeSections from './WellCompositeSections'
const { Content } = Layout const { Content } = Layout
export const WellCompositeEditor = memo(({ rootPath }) => { const properties = {
const { tab } = useParams() requirements: ['OperationStat.get', 'WellComposite.get'],
const idWell = useContext(IdWellContext) title: 'Композитная скважина',
route: 'composite/*',
key: 'composite',
}
const WellCompositeEditor = memo(() => {
const idWell = useIdWell()
const root = useRootPath()
const rootPath = useMemo(() => `${root}/${properties.key}`, [root])
const [statsWells, setStatsWells] = useState([]) const [statsWells, setStatsWells] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
@ -25,19 +33,21 @@ export const WellCompositeEditor = memo(({ rootPath }) => {
const [selectedIdWells, setSelectedIdWells] = useState([]) const [selectedIdWells, setSelectedIdWells] = useState([])
const [selectedSections, setSelectedSections] = useState([]) const [selectedSections, setSelectedSections] = useState([])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
try { try {
const selected = await WellCompositeService.get(idWell) setSelectedSections(arrayOrDefault(await WellCompositeService.get(idWell)))
setSelectedSections(arrayOrDefault(selected))
} catch(e) { } catch(e) {
setSelectedSections([]) setSelectedSections([])
throw e
} }
}, },
setShowLoader, setShowLoader,
'Не удалось загрузить список скважин', 'Не удалось загрузить список скважин',
'Получение списка скважин' 'Получение списка скважин'
), [idWell]) )
}, [idWell])
useEffect(() => { useEffect(() => {
const wellIds = selectedSections.map((value) => value.idWellSrc) const wellIds = selectedSections.map((value) => value.idWellSrc)
@ -45,7 +55,8 @@ export const WellCompositeEditor = memo(({ rootPath }) => {
setSelectedIdWells(wellIds) setSelectedIdWells(wellIds)
}, [selectedSections]) }, [selectedSections])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const stats = arrayOrDefault(await OperationStatService.getWellsStat(selectedIdWells)) const stats = arrayOrDefault(await OperationStatService.getWellsStat(selectedIdWells))
setStatsWells(stats) setStatsWells(stats)
@ -53,7 +64,8 @@ export const WellCompositeEditor = memo(({ rootPath }) => {
setShowTabLoader, setShowTabLoader,
'Не удалось загрузить статистику по скважинам/секциям', 'Не удалось загрузить статистику по скважинам/секциям',
'Получение статистики по скважинам/секциям' 'Получение статистики по скважинам/секциям'
), [selectedIdWells]) )
}, [selectedIdWells])
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
@ -66,19 +78,22 @@ export const WellCompositeEditor = memo(({ rootPath }) => {
/> />
</Col> </Col>
<Col span={6}> <Col span={6}>
<PrivateMenu root={rootPath} mode={'horizontal'} selectable={true} className={'well_menu'} selectedKeys={[tab]}> <PrivateMenu root={rootPath} className={'well_menu'}>
<PrivateMenu.Link key={'wells'} title={'Статистика по скважинам'} /> <PrivateMenu.Link content={ClusterWells} />
<PrivateMenu.Link key={'sections'} title={'Статистика по секциям'} /> <PrivateMenu.Link content={WellCompositeSections} />
</PrivateMenu> </PrivateMenu>
</Col> </Col>
</Row> </Row>
<Layout> <Layout>
<Content className={'site-layout-background'}> <Content className={'site-layout-background'}>
<LoaderPortal show={showTabLoader}> <LoaderPortal show={showTabLoader}>
<PrivateSwitch root={rootPath} elseRedirect={['wells', 'sections']}> <Routes>
<ClusterWells key={'wells'} statsWells={statsWells} /> <Route index element={<Navigate to={ClusterWells.route} replace/>} />
<WellCompositeSections key={'sections'} statsWells={statsWells} selectedSections={selectedSections} /> <Route path={'*'} element={<NoAccessComponent />} />
</PrivateSwitch>
<Route path={ClusterWells.route} element={<ClusterWells statsWells={statsWells} />} />
<Route path={WellCompositeSections.route} element={<WellCompositeSections statsWells={statsWells} selectedSections={selectedSections} />} />
</Routes>
</LoaderPortal> </LoaderPortal>
</Content> </Content>
</Layout> </Layout>
@ -86,4 +101,4 @@ export const WellCompositeEditor = memo(({ rootPath }) => {
) )
}) })
export default WellCompositeEditor export default wrapPrivateComponent(WellCompositeEditor, properties)

View File

@ -1,31 +1,34 @@
import { memo, useContext, useMemo } from 'react' import { memo, useMemo } from 'react'
import { useParams } from 'react-router-dom' import { Navigate, Route, Routes } from 'react-router-dom'
import { Layout } from 'antd' import { Layout } from 'antd'
import { RootPathContext } from '@asb/context' import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu, PrivateSwitch } from '@components/Private' import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import Statistics from './Statistics' import Statistics from './Statistics'
import WellCompositeEditor from './WellCompositeEditor' import WellCompositeEditor from './WellCompositeEditor'
export const Analytics = memo(() => { const Analytics = memo(() => {
const { tab } = useParams() const root = useRootPath()
const root = useContext(RootPathContext)
const rootPath = useMemo(() => `${root}/analytics`, [root]) const rootPath = useMemo(() => `${root}/analytics`, [root])
return ( return (
<RootPathContext.Provider value={rootPath}> <RootPathContext.Provider value={rootPath}>
<Layout> <Layout>
<PrivateMenu mode={'horizontal'} selectable={true} className={'well_menu'} selectedKeys={[tab]}> <PrivateMenu className={'well_menu'}>
<PrivateMenu.Link key={'composite'} title={'Композитная скважина'} /> <PrivateMenu.Link content={WellCompositeEditor} />
<PrivateMenu.Link key={'statistics'} title={'Оценка по ЦБ'} /> <PrivateMenu.Link key={'statistics'} title={'Оценка по ЦБ'} content={Statistics} />
</PrivateMenu> </PrivateMenu>
<Layout> <Layout>
<Layout.Content> <Layout.Content className={'site-layout-background'}>
<PrivateSwitch elseRedirect={'composite'}> <Routes>
<WellCompositeEditor key={'composite/:tab?'} rootPath={`${rootPath}/composite`} /> <Route index element={<Navigate to={WellCompositeEditor.getKey()} replace />} />
<Statistics key={'statistics'} /> <Route path={'*'} element={<NoAccessComponent />} />
</PrivateSwitch>
<Route path={WellCompositeEditor.route} element={<WellCompositeEditor />} />
<Route path={Statistics.route} element={<Statistics />} />
</Routes>
</Layout.Content> </Layout.Content>
</Layout> </Layout>
</Layout> </Layout>
@ -33,4 +36,9 @@ export const Analytics = memo(() => {
) )
}) })
export default Analytics export default wrapPrivateComponent(Analytics, {
requirements: [],
title: 'Аналитика',
route: 'analytics/*',
key: 'analytics',
})

View File

@ -20,11 +20,12 @@ import { invokeWebApiWrapperAsync } from '@components/factory'
import { import {
getOperations, getOperations,
calcAndUpdateStatsBySections, calcAndUpdateStatsBySections,
makeFilterMinMaxFunction isRawDate,
} from '@utils/functions' makeFilterMinMaxFunction,
import { isRawDate } from '@utils' wrapPrivateComponent
} from '@utils'
import { Tvd } from '@pages/WellOperations/Tvd' import Tvd from '@pages/WellOperations/Tvd'
import WellOperationsTable from './WellOperationsTable' import WellOperationsTable from './WellOperationsTable'
const filtersMinMax = [ const filtersMinMax = [
@ -39,7 +40,7 @@ const ONLINE_DEADTIME = 600_000
const getDate = (str) => isRawDate(str) ? new Date(str).toLocaleString() : '-' const getDate = (str) => isRawDate(str) ? new Date(str).toLocaleString() : '-'
const numericRender = makeNumericRender(1) const numericRender = makeNumericRender(1)
export const ClusterWells = memo(({ statsWells }) => { const ClusterWells = memo(({ statsWells }) => {
const [selectedWellId, setSelectedWellId] = useState(0) const [selectedWellId, setSelectedWellId] = useState(0)
const [isTVDModalVisible, setIsTVDModalVisible] = useState(false) const [isTVDModalVisible, setIsTVDModalVisible] = useState(false)
const [isOpsModalVisible, setIsOpsModalVisible] = useState(false) const [isOpsModalVisible, setIsOpsModalVisible] = useState(false)
@ -187,4 +188,8 @@ export const ClusterWells = memo(({ statsWells }) => {
) )
}) })
export default ClusterWells export default wrapPrivateComponent(ClusterWells, {
requirements: [],
title: 'Статистика по скважинам',
route: 'wells',
})

View File

@ -1,19 +1,21 @@
import { useState, useEffect, memo } from 'react' import { useState, useEffect, memo } from 'react'
import { useParams } from 'react-router-dom'
import { arrayOrDefault } from '@utils' import { LayoutPortal } from '@components/Layout'
import { OperationStatService } from '@api'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import { OperationStatService } from '@api'
import ClusterWells from './ClusterWells' import ClusterWells from './ClusterWells'
import { useParams } from 'react-router-dom'
export const Cluster = memo(() => { const Cluster = memo(() => {
const { idCluster } = useParams() const { idCluster } = useParams()
const [data, setData] = useState([]) const [data, setData] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const clusterData = await OperationStatService.getStatCluster(idCluster) const clusterData = await OperationStatService.getStatCluster(idCluster)
setData(arrayOrDefault(clusterData?.statsWells)) setData(arrayOrDefault(clusterData?.statsWells))
@ -21,13 +23,21 @@ export const Cluster = memo(() => {
setShowLoader, setShowLoader,
`Не удалось загрузить данные по кусту "${idCluster}"`, `Не удалось загрузить данные по кусту "${idCluster}"`,
'Получение данных по кусту' 'Получение данных по кусту'
), [idCluster]) )
}, [idCluster])
return ( return (
<LayoutPortal title={'Анализ скважин куста'}>
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<ClusterWells statsWells={data} /> <ClusterWells statsWells={data} />
</LoaderPortal> </LoaderPortal>
</LayoutPortal>
) )
}) })
export default Cluster export default wrapPrivateComponent(Cluster, {
requirements: ['OperationStat.get'],
title: 'Анализ скважин куста',
route: 'cluster/:idCluster/*',
key: 'cluster',
})

View File

@ -1,17 +1,20 @@
import { Map, Overlay } from 'pigeon-maps' import { Map, Overlay } from 'pigeon-maps'
import { Link, useLocation } from 'react-router-dom'
import { useState, useEffect, memo } from 'react' import { useState, useEffect, memo } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { ClusterService } from '@api'
import { arrayOrDefault } from '@utils'
import { PointerIcon } from '@components/icons' import { PointerIcon } from '@components/icons'
import { LayoutPortal } from '@components/Layout'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, limitValue, wrapPrivateComponent } from '@utils'
import { ClusterService } from '@api'
import '@styles/index.css' import '@styles/index.css'
const defaultViewParams = { center: [60.81226, 70.0562], zoom: 5 } const defaultViewParams = { center: [60.81226, 70.0562], zoom: 5 }
const zoomLimit = limitValue(5, 15)
const calcViewParams = (clusters) => { const calcViewParams = (clusters) => {
if ((clusters?.length ?? 0) <= 0) if ((clusters?.length ?? 0) <= 0)
return defaultViewParams return defaultViewParams
@ -33,19 +36,20 @@ const calcViewParams = (clusters) => {
// zoom min = 1 (mega far) // zoom min = 1 (mega far)
// 4 - full Russia (161.6 deg) // 4 - full Russia (161.6 deg)
// 13.5 - Khanty-Mansiysk // 13.5 - Khanty-Mansiysk
const zoom = Math.min(Math.max(5, 5 + 5 / (maxDeg + 0.5)), 15) const zoom = zoomLimit(5 + 5 / (maxDeg + 0.5))
return { center, zoom } return { center, zoom }
} }
export const Deposit = memo(() => { const Deposit = memo(() => {
const [clustersData, setClustersData] = useState([]) const [clustersData, setClustersData] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [viewParams, setViewParams] = useState(defaultViewParams) const [viewParams, setViewParams] = useState(defaultViewParams)
const location = useLocation() const location = useLocation()
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const data = await ClusterService.getClusters() const data = await ClusterService.getClusters()
setClustersData(arrayOrDefault(data)) setClustersData(arrayOrDefault(data))
@ -54,9 +58,11 @@ export const Deposit = memo(() => {
setShowLoader, setShowLoader,
`Не удалось загрузить список кустов`, `Не удалось загрузить список кустов`,
'Получить список кустов' 'Получить список кустов'
), []) )
}, [])
return ( return (
<LayoutPortal noSheet title={'Месторождение'}>
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<div className={'h-100vh'}> <div className={'h-100vh'}>
<Map {...viewParams}> <Map {...viewParams}>
@ -75,7 +81,13 @@ export const Deposit = memo(() => {
</Map> </Map>
</div> </div>
</LoaderPortal> </LoaderPortal>
</LayoutPortal>
) )
}) })
export default Deposit export default wrapPrivateComponent(Deposit, {
requirements: ['Cluster.get'],
title: 'Месторождение',
route: 'deposit/*',
key: 'deposit',
})

View File

@ -1,13 +1,13 @@
import { useState, useEffect, useMemo, useCallback, useContext } from 'react' import { useState, useEffect, useMemo, useCallback } from 'react'
import { DatePicker, Button, Input } from 'antd' import { DatePicker, Button, Input } from 'antd'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { UploadForm } from '@components/UploadForm' import { UploadForm } from '@components/UploadForm'
import { CompanyView, UserView } from '@components/views' import { CompanyView, UserView } from '@components/views'
import { invokeWebApiWrapperAsync, downloadFile, formatBytes } from '@components/factory' import { invokeWebApiWrapperAsync, downloadFile, formatBytes } from '@components/factory'
import { EditableTable, makeColumn, makeDateColumn, makeNumericColumn, makePaginationObject } from '@components/Table' import { EditableTable, makeColumn, makeDateColumn, makeNumericColumn, makePaginationObject } from '@components/Table'
import { hasPermission } from '@utils/permissions' import { hasPermission } from '@utils'
import { FileService } from '@api' import { FileService } from '@api'
const pageSize = 12 const pageSize = 12
@ -40,7 +40,7 @@ export const DocumentsTemplate = ({ idCategory, idWell: wellId, mimeTypes, heade
const [files, setFiles] = useState([]) const [files, setFiles] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const idwellContext = useContext(IdWellContext) const idwellContext = useIdWell()
const idWell = useMemo(() => wellId ?? idwellContext, [wellId, idwellContext]) const idWell = useMemo(() => wellId ?? idwellContext, [wellId, idwellContext])
const uploadUrl = useMemo(() => `/api/well/${idWell}/files/?idCategory=${idCategory}`, [idWell, idCategory]) const uploadUrl = useMemo(() => `/api/well/${idWell}/files/?idCategory=${idCategory}`, [idWell, idCategory])
@ -83,8 +83,13 @@ export const DocumentsTemplate = ({ idCategory, idWell: wellId, mimeTypes, heade
) )
}, [filterCompanyName, filterDataRange, filterFileName, idCategory, idWell, page]) }, [filterCompanyName, filterDataRange, filterFileName, idCategory, idWell, page])
useEffect(update, [update]) useEffect(() => {
useEffect(() => onChange?.(files), [files, onChange]) update()
}, [update])
useEffect(() => {
onChange?.(files)
}, [files, onChange])
const handleFileDelete = useMemo(() => hasPermission(`File.edit${idCategory}`) && (async (file) => { const handleFileDelete = useMemo(() => hasPermission(`File.edit${idCategory}`) && (async (file) => {
await FileService.delete(idWell, file.id) await FileService.delete(idWell, file.id)

View File

@ -1,10 +1,11 @@
import { useParams } from 'react-router-dom' import { Navigate, Route, Routes } from 'react-router-dom'
import { memo, useContext, useMemo } from 'react' import { memo, useMemo } from 'react'
import { FolderOutlined } from '@ant-design/icons' import { FolderOutlined } from '@ant-design/icons'
import { Layout } from 'antd' import { Layout } from 'antd'
import { RootPathContext } from '@asb/context' import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu, PrivateSwitch } from '@components/Private' import { PrivateMenu } from '@components/Private'
import { getTabname, wrapPrivateComponent, NoAccessComponent } from '@utils'
import DocumentsTemplate from './DocumentsTemplate' import DocumentsTemplate from './DocumentsTemplate'
@ -23,9 +24,9 @@ export const documentCategories = [
{ id: 9, key: 'closingService', title: 'Сервис по заканчиванию скважины' }, { id: 9, key: 'closingService', title: 'Сервис по заканчиванию скважины' },
] ]
export const MenuDocuments = memo(() => { const MenuDocuments = memo(() => {
const { category } = useParams() const category = getTabname()
const root = useContext(RootPathContext) const root = useRootPath()
const rootPath = useMemo(() => `${root}/document`, [root]) const rootPath = useMemo(() => `${root}/document`, [root])
return ( return (
@ -42,19 +43,30 @@ export const MenuDocuments = memo(() => {
</PrivateMenu> </PrivateMenu>
<Layout> <Layout>
<Content className={'site-layout-background'}> <Content className={'site-layout-background'}>
<PrivateSwitch elseRedirect={documentCategories.map((cat) => cat.key)}> <Routes>
{documentCategories.length > 0 && (
<Route index element={<Navigate to={documentCategories[0].key} replace />} />
)}
<Route path={'*'} element={<NoAccessComponent />} />
{documentCategories.map(category => ( {documentCategories.map(category => (
<Route key={category.key} path={category.key} element={(
<DocumentsTemplate <DocumentsTemplate
key={category.key}
idCategory={category.id} idCategory={category.id}
tableName={`documents_${category.key}`} tableName={`documents_${category.key}`}
/> />
)} />
))} ))}
</PrivateSwitch> </Routes>
</Content> </Content>
</Layout> </Layout>
</RootPathContext.Provider> </RootPathContext.Provider>
) )
}) })
export default MenuDocuments export default wrapPrivateComponent(MenuDocuments, {
requirements: [ 'Deposit.get', 'File.get' ],
title: 'Документы',
route: 'document/*',
key: 'document',
})

View File

@ -1,8 +1,8 @@
import { Form, Select } from 'antd' import { Form, Select } from 'antd'
import { FileAddOutlined } from '@ant-design/icons' import { FileAddOutlined } from '@ant-design/icons'
import { memo, useCallback, useContext, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useState } from 'react'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import Poprompt from '@components/selectors/Poprompt' import Poprompt from '@components/selectors/Poprompt'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { DrillingProgramService } from '@api' import { DrillingProgramService } from '@api'
@ -21,9 +21,10 @@ export const CategoryAdder = memo(({ categories, onUpdate, className, ...other }
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [showCatLoader, setShowCatLoader] = useState(false) const [showCatLoader, setShowCatLoader] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
setOptions(categories.map((category) => ({ setOptions(categories.map((category) => ({
label: category.name ?? category.shortName, label: category.name ?? category.shortName,
@ -32,7 +33,8 @@ export const CategoryAdder = memo(({ categories, onUpdate, className, ...other }
}, },
setShowCatLoader, setShowCatLoader,
`Не удалось установить список доступных категорий для добавления` `Не удалось установить список доступных категорий для добавления`
), [categories]) )
}, [categories])
const onFinish = useCallback(({ categories }) => invokeWebApiWrapperAsync( const onFinish = useCallback(({ categories }) => invokeWebApiWrapperAsync(
async () => { async () => {

View File

@ -1,8 +1,8 @@
import { Input, Modal, Radio } from 'antd' import { Input, Modal, Radio } from 'antd'
import { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { BehaviorSubject, debounceTime, distinctUntilChanged, filter, map } from 'rxjs' import { BehaviorSubject, debounceTime, distinctUntilChanged, filter, map } from 'rxjs'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { UserView } from '@components/views' import { UserView } from '@components/views'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
@ -30,11 +30,15 @@ export const CategoryEditor = memo(({ visible, category, onClosed }) => {
const [subject, setSubject] = useState(null) const [subject, setSubject] = useState(null)
const [needUpdate, setNeedUpdate] = useState(false) const [needUpdate, setNeedUpdate] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
useEffect(() => visible && setNeedUpdate(false), [visible]) useEffect(() => {
if (visible)
setNeedUpdate(false)
}, [visible])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const filteredUsers = users.filter(({ user }) => user && [ const filteredUsers = users.filter(({ user }) => user && [
user.login ?? '', user.login ?? '',
@ -50,7 +54,8 @@ export const CategoryEditor = memo(({ visible, category, onClosed }) => {
}, },
setIsSearching, setIsSearching,
`Не удалось произвести поиск пользователей` `Не удалось произвести поиск пользователей`
), [users, searchValue]) )
}, [users, searchValue])
useEffect(() => { useEffect(() => {
if (!subject) { if (!subject) {
@ -71,14 +76,16 @@ export const CategoryEditor = memo(({ visible, category, onClosed }) => {
} }
}, [subject]) }, [subject])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const allUsers = arrayOrDefault(await DrillingProgramService.getAvailableUsers(idWell)) const allUsers = arrayOrDefault(await DrillingProgramService.getAvailableUsers(idWell))
setAllUsers(allUsers) setAllUsers(allUsers)
}, },
setShowLoader, setShowLoader,
`Не удалось загрузить список доступных пользователей скважины "${idWell}"` `Не удалось загрузить список доступных пользователей скважины "${idWell}"`
), [idWell]) )
}, [idWell])
const calcUsers = useCallback(() => { const calcUsers = useCallback(() => {
if (!visible) return if (!visible) return

View File

@ -1,7 +1,7 @@
import { useCallback, useContext, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { Button, DatePicker, Input, Modal } from 'antd' import { Button, DatePicker, Input, Modal } from 'antd'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { CompanyView } from '@components/views' import { CompanyView } from '@components/views'
import DownloadLink from '@components/DownloadLink' import DownloadLink from '@components/DownloadLink'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
@ -65,9 +65,10 @@ export const CategoryHistory = ({ idCategory, visible, onClose }) => {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [companyName, setCompanyName] = useState('') const [companyName, setCompanyName] = useState('')
const idWell = useContext(IdWellContext) const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
if (!visible) return if (!visible) return
const [begin, end] = range?.length > 1 ? [range[0].toISOString(), range[1].toISOString()] : [null, null] const [begin, end] = range?.length > 1 ? [range[0].toISOString(), range[1].toISOString()] : [null, null]
@ -79,7 +80,8 @@ export const CategoryHistory = ({ idCategory, visible, onClose }) => {
}, },
setIsLoading, setIsLoading,
`Не удалось загрузить историю категорий "${idCategory}" скважины "${idWell}"` `Не удалось загрузить историю категорий "${idCategory}" скважины "${idWell}"`
), [idWell, idCategory, visible, range, companyName, fileName, page, pageSize]) )
}, [idWell, idCategory, visible, range, companyName, fileName, page, pageSize])
const onPaginationChange = useCallback((page, pageSize) => { const onPaginationChange = useCallback((page, pageSize) => {
setPage(page) setPage(page)

View File

@ -1,4 +1,4 @@
import { memo, useCallback, useContext, useMemo, useState } from 'react' import { memo, useCallback, useMemo, useState } from 'react'
import { Button, Input, Popconfirm, Form } from 'antd' import { Button, Input, Popconfirm, Form } from 'antd'
import { import {
DeleteOutlined, DeleteOutlined,
@ -6,7 +6,7 @@ import {
TableOutlined, TableOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { UserView } from '@components/views' import { UserView } from '@components/views'
import UploadForm from '@components/UploadForm' import UploadForm from '@components/UploadForm'
import DownloadLink from '@components/DownloadLink' import DownloadLink from '@components/DownloadLink'
@ -45,7 +45,7 @@ export const CategoryRender = memo(({ partData, onUpdate, onEdit, onHistory, set
file // Информация о файле file // Информация о файле
} = partData ?? {} } = partData ?? {}
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const uploadUrl = useMemo(() => `/api/well/${idWell}/drillingProgram/part/${idFileCategory}`, [idWell, idFileCategory]) const uploadUrl = useMemo(() => `/api/well/${idWell}/drillingProgram/part/${idFileCategory}`, [idWell, idFileCategory])
const approvedMarks = useMemo(() => file?.fileMarks?.filter((mark) => mark.idMarkType === 1), [file]) const approvedMarks = useMemo(() => file?.fileMarks?.filter((mark) => mark.idMarkType === 1), [file])

View File

@ -13,7 +13,7 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useIdWell } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { downloadFile, formatBytes, invokeWebApiWrapperAsync } from '@components/factory' import { downloadFile, formatBytes, invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, formatDate } from '@utils' import { arrayOrDefault, formatDate, wrapPrivateComponent } from '@utils'
import { DrillingProgramService } from '@api' import { DrillingProgramService } from '@api'
import CategoryAdder from './CategoryAdder' import CategoryAdder from './CategoryAdder'
@ -41,7 +41,7 @@ const STATE_STRING = {
[ID_STATE.Unknown]: { icon: WarningOutlined, text: 'Неизвестно' }, [ID_STATE.Unknown]: { icon: WarningOutlined, text: 'Неизвестно' },
} }
export const DrillingProgram = memo(() => { const DrillingProgram = memo(() => {
const [selectedCategory, setSelectedCategory] = useState() const [selectedCategory, setSelectedCategory] = useState()
const [historyVisible, setHistoryVisible] = useState(false) const [historyVisible, setHistoryVisible] = useState(false)
const [editorVisible, setEditorVisible] = useState(false) const [editorVisible, setEditorVisible] = useState(false)
@ -79,7 +79,9 @@ export const DrillingProgram = memo(() => {
`Не удалось загрузить название скважины "${idWell}"` `Не удалось загрузить название скважины "${idWell}"`
), [idWell]) ), [idWell])
useEffect(() => updateData(), [updateData]) useEffect(() => {
updateData()
}, [updateData])
const onCategoryEdit = useCallback((catId) => { const onCategoryEdit = useCallback((catId) => {
setSelectedCategory(catId) setSelectedCategory(catId)
@ -172,4 +174,8 @@ export const DrillingProgram = memo(() => {
) )
}) })
export default DrillingProgram export default wrapPrivateComponent(DrillingProgram, {
requirements: [ 'DrillingProgram.get' ],
title: 'Программа бурения',
route: 'drillingProgram',
})

View File

@ -1,21 +1,21 @@
import { memo, useCallback, useState } from 'react' import { memo, useCallback, useState } from 'react'
import { Link, useHistory, useLocation } from 'react-router-dom' import { Link, useNavigate, useLocation } from 'react-router-dom'
import { Card, Form, Input, Button } from 'antd'
import { UserOutlined, LockOutlined } from '@ant-design/icons' import { UserOutlined, LockOutlined } from '@ant-design/icons'
import { Card, Form, Input, Button } from 'antd'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { loginRules, passwordRules } from '@utils/validationRules' import { loginRules, passwordRules } from '@utils/validationRules'
import { setUser } from '@utils/storage' import { setUser, wrapPrivateComponent } from '@utils'
import { AuthService } from '@api' import { AuthService } from '@api'
import '@styles/index.css' import '@styles/index.css'
import Logo from '@images/Logo' import Logo from '@images/Logo'
export const Login = memo(() => { const Login = memo(() => {
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const history = useHistory() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const handleLogin = useCallback((formData) => invokeWebApiWrapperAsync( const handleLogin = useCallback((formData) => invokeWebApiWrapperAsync(
@ -23,20 +23,19 @@ export const Login = memo(() => {
const user = await AuthService.login(formData) const user = await AuthService.login(formData)
if (!user) throw Error('Неправильный логин или пароль') if (!user) throw Error('Неправильный логин или пароль')
setUser(user) setUser(user)
console.log(location.state?.from) navigate(location.state?.from ?? '/deposit')
history.push(location.state?.from ?? 'well')
}, },
setShowLoader, setShowLoader,
(ex) => ex?.message ?? 'Ошибка входа', (ex) => ex?.message ?? 'Ошибка входа',
'Вход в систему' 'Вход в систему'
), [history, location]) ), [navigate, location])
return ( return (
<div className={'login_page shadow'}> <div className={'login_page shadow'}>
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<Logo style={{ marginBottom: '10px' }} /> <Logo style={{ marginBottom: '10px' }} />
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<Card title={'Система мониторинга'} className={'shadow'} bordered={true} style={{ width: 350 }}> <Card bordered title={'Система мониторинга'} className={'shadow'} style={{ width: 350 }}>
<Form onFinish={handleLogin}> <Form onFinish={handleLogin}>
<Form.Item name={'login'} rules={loginRules}> <Form.Item name={'login'} rules={loginRules}>
<Input placeholder={'Пользователь'} prefix={<UserOutlined />} /> <Input placeholder={'Пользователь'} prefix={<UserOutlined />} />
@ -60,4 +59,8 @@ export const Login = memo(() => {
) )
}) })
export default Login export default wrapPrivateComponent(Login, {
requirements: [],
title: 'Вход в систему',
route: 'login',
})

View File

@ -1,45 +0,0 @@
import { memo } from 'react'
import { Route, Switch } from 'react-router-dom'
import { RootPathContext } from '@asb/context'
import { AdminLayoutPortal, LayoutPortal } from '@components/Layout'
import { PrivateDefaultRoute, PrivateRoute } from '@components/Private'
import Well from './Well'
import Cluster from './Cluster'
import Deposit from './Deposit'
import AdminPanel from './AdminPanel'
import AccessDenied from './AccessDenied'
export const Main = memo(() => (
<RootPathContext.Provider value={''}>
<Switch>
<PrivateRoute path={'/admin/:tab?'}>
<AdminLayoutPortal title={'Администраторская панель'}>
<AdminPanel />
</AdminLayoutPortal>
</PrivateRoute>
<PrivateRoute path={'/deposit'}>
<LayoutPortal noSheet title='Месторождение'>
<Deposit />
</LayoutPortal>
</PrivateRoute>
<PrivateRoute path={'/cluster/:idCluster'}>
<LayoutPortal title={'Анализ скважин куста'}>
<Cluster />
</LayoutPortal>
</PrivateRoute>
<PrivateRoute path={'/well/:idWell/:tab?'}>
<LayoutPortal>
<Well />
</LayoutPortal>
</PrivateRoute>
<Route path={'/access_denied'}>
<AccessDenied />
</Route>
<PrivateDefaultRoute urls={['/deposit']} />
</Switch>
</RootPathContext.Provider>
))
export default Main

View File

@ -1,4 +1,4 @@
import { useState, useEffect, memo, useMemo, useCallback, useContext } from 'react' import { useState, useEffect, memo, useMemo, useCallback } from 'react'
import { Button, Form, Input, Popconfirm, Timeline } from 'antd' import { Button, Form, Input, Popconfirm, Timeline } from 'antd'
import { import {
CheckSquareOutlined, CheckSquareOutlined,
@ -9,11 +9,10 @@ import {
DeleteOutlined DeleteOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { hasPermission } from '@utils/permissions' import { hasPermission, formatDate } from '@utils'
import { formatDate } from '@utils'
import { MeasureService } from '@api' import { MeasureService } from '@api'
import { View } from './View' import { View } from './View'
@ -33,7 +32,7 @@ export const MeasureTable = memo(({ group, updateMeasuresFunc, additionalButtons
const [isTableEditing, setIsTableEditing] = useState(false) const [isTableEditing, setIsTableEditing] = useState(false)
const [editingActionName, setEditingActionName] = useState('') const [editingActionName, setEditingActionName] = useState('')
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const [measuresForm] = Form.useForm() const [measuresForm] = Form.useForm()

View File

@ -1,10 +1,11 @@
import { Button } from 'antd' import { useState, useEffect, memo } from 'react'
import { useState, useEffect, memo, useContext } from 'react'
import { TableOutlined } from '@ant-design/icons' import { TableOutlined } from '@ant-design/icons'
import { Button } from 'antd'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { wrapPrivateComponent } from '@utils'
import { MeasureService } from '@api' import { MeasureService } from '@api'
import { MeasureTable } from './MeasureTable' import { MeasureTable } from './MeasureTable'
@ -42,15 +43,16 @@ const defaultData = [
} }
] ]
export const Measure = memo(() => { const Measure = memo(() => {
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [isMeasuresUpdating, setIsMeasuresUpdating] = useState(true) const [isMeasuresUpdating, setIsMeasuresUpdating] = useState(true)
const [data, setData] = useState(defaultData) const [data, setData] = useState(defaultData)
const [tableIdx, setTableIdx] = useState(-1) const [tableIdx, setTableIdx] = useState(-1)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
if (!isMeasuresUpdating) return if (!isMeasuresUpdating) return
const measures = await MeasureService.getHisory(idWell) const measures = await MeasureService.getHisory(idWell)
@ -69,7 +71,8 @@ export const Measure = memo(() => {
setShowLoader, setShowLoader,
`Не удалось загрузить последние данные по скважине ${idWell}`, `Не удалось загрузить последние данные по скважине ${idWell}`,
'Получение последних данных телеметрий' 'Получение последних данных телеметрий'
), [idWell, isMeasuresUpdating]) )
}, [idWell, isMeasuresUpdating])
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
@ -90,4 +93,8 @@ export const Measure = memo(() => {
) )
}) })
export default Measure export default wrapPrivateComponent(Measure, {
requirements: [ 'Measure.get' ],
title: 'Измерения',
route: 'measure',
})

View File

@ -1,5 +1,5 @@
import { memo, useCallback, useState } from 'react' import { memo, useCallback, useState } from 'react'
import { Link, useHistory } from 'react-router-dom' import { Link, useNavigate } from 'react-router-dom'
import { Card, Form, Input, Button } from 'antd' import { Card, Form, Input, Button } from 'antd'
import { import {
UserOutlined, UserOutlined,
@ -20,6 +20,7 @@ import {
passwordRules, passwordRules,
phoneRules phoneRules
} from '@utils/validationRules' } from '@utils/validationRules'
import { wrapPrivateComponent } from '@utils'
import Logo from '@images/Logo' import Logo from '@images/Logo'
@ -52,17 +53,17 @@ const createInput = (name, placeholder, rules, isPassword, dependencies) => (
export const Register = memo(() => { export const Register = memo(() => {
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const history = useHistory() const navigate = useNavigate()
const handleRegister = useCallback((formData) => invokeWebApiWrapperAsync( const handleRegister = useCallback((formData) => invokeWebApiWrapperAsync(
async () => { async () => {
await AuthService.register(formData) await AuthService.register(formData)
history.push('/login') navigate('/login')
}, },
setShowLoader, setShowLoader,
`Ошибка отправки заявки на регистрацию`, `Ошибка отправки заявки на регистрацию`,
'Отправка заявки на регистрацию' 'Отправка заявки на регистрацию'
), [history]) ), [navigate])
return ( return (
<LoaderPortal show={showLoader} className={'loader-container login_page shadow'}> <LoaderPortal show={showLoader} className={'loader-container login_page shadow'}>
@ -91,4 +92,8 @@ export const Register = memo(() => {
) )
}) })
export default Register export default wrapPrivateComponent(Register, {
requirements: [],
title: 'Регистрация',
route: 'register',
})

View File

@ -1,8 +1,8 @@
import moment from 'moment'
import { DatePicker, Descriptions, Divider, Form, Input, InputNumber, Modal, Select, Space, Table } from 'antd' import { DatePicker, Descriptions, Divider, Form, Input, InputNumber, Modal, Select, Space, Table } from 'antd'
import { memo, useCallback, useContext, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useState } from 'react'
import moment from 'moment'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { makeColumn, makeGroupColumn } from '@components/Table' import { makeColumn, makeGroupColumn } from '@components/Table'
@ -133,7 +133,7 @@ export const ReportEditor = memo(({ visible, data, onDone, onCancel, checkIsDate
const [isInvalid, setIsInvalid] = useState(false) const [isInvalid, setIsInvalid] = useState(false)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const setFields = useCallback((data) => form.setFieldsValue(data ? { const setFields = useCallback((data) => form.setFieldsValue(data ? {
...data, ...data,

View File

@ -1,25 +1,25 @@
import moment from 'moment' import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { Button } from 'antd'
import { FileExcelOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons' import { FileExcelOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'
import { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { Button } from 'antd'
import moment from 'moment'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { DateRangeWrapper, Table, makeDateColumn, makeColumn } from '@components/Table' import { DateRangeWrapper, Table, makeDateColumn, makeColumn } from '@components/Table'
import { download, invokeWebApiWrapperAsync } from '@components/factory' import { download, invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import { DailyReportService } from '@api' import { DailyReportService } from '@api'
import { arrayOrDefault } from '@utils'
import ReportEditor from './ReportEditor' import ReportEditor from './ReportEditor'
export const DailyReport = memo(() => { const DailyReport = memo(() => {
const [data, setData] = useState([]) const [data, setData] = useState([])
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [searchDate, setSearchDate] = useState([moment().subtract(1, 'week'), moment()]) const [searchDate, setSearchDate] = useState([moment().subtract(1, 'week'), moment()])
const [selectedReport, setSelectedReport] = useState(null) const [selectedReport, setSelectedReport] = useState(null)
const [isEditorVisible, setIsEditorVisible] = useState(false) const [isEditorVisible, setIsEditorVisible] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const updateTable = useCallback(() => invokeWebApiWrapperAsync( const updateTable = useCallback(() => invokeWebApiWrapperAsync(
async () => { async () => {
@ -31,7 +31,9 @@ export const DailyReport = memo(() => {
'Получение списка суточных рапортов', 'Получение списка суточных рапортов',
), [idWell]) ), [idWell])
useEffect(updateTable, [updateTable]) useEffect(() => {
updateTable()
}, [updateTable])
const checkIsDateBusy = useCallback((current) => current.isAfter(moment(), 'day') || data.some((row) => moment(row.reportDate).isSame(current, 'day')), [data]) const checkIsDateBusy = useCallback((current) => current.isAfter(moment(), 'day') || data.some((row) => moment(row.reportDate).isSame(current, 'day')), [data])
@ -111,4 +113,10 @@ export const DailyReport = memo(() => {
) )
}) })
export default DailyReport export default wrapPrivateComponent(DailyReport, {
requirements: [
// 'DailyReport.get',
],
title: 'Суточный рапорт',
route: 'daily_report',
})

View File

@ -1,8 +1,8 @@
import { Button, Tooltip } from 'antd' import { Button, Tooltip } from 'antd'
import { useState, useEffect, memo, useContext } from 'react' import { useState, useEffect, memo } from 'react'
import { FilePdfOutlined, FileTextOutlined } from '@ant-design/icons' import { FilePdfOutlined, FileTextOutlined } from '@ant-design/icons'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { Table, makeDateSorter, makeNumericSorter } from '@components/Table' import { Table, makeDateSorter, makeNumericSorter } from '@components/Table'
import { invokeWebApiWrapperAsync, downloadFile } from '@components/factory' import { invokeWebApiWrapperAsync, downloadFile } from '@components/factory'
@ -60,9 +60,10 @@ export const Reports = memo(() => {
const [reports, setReports] = useState([]) const [reports, setReports] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const reportsResponse = await ReportService.getAllReportsNamesByWell(idWell) const reportsResponse = await ReportService.getAllReportsNamesByWell(idWell)
const reports = reportsResponse.map(r => ({ ...r, key: r.id ?? r.name ?? r.date })) const reports = reportsResponse.map(r => ({ ...r, key: r.id ?? r.name ?? r.date }))
@ -71,7 +72,8 @@ export const Reports = memo(() => {
setShowLoader, setShowLoader,
`Не удалось загрузить список рапортов по скважине "${idWell}"`, `Не удалось загрузить список рапортов по скважине "${idWell}"`,
'Получение списка рапортов' 'Получение списка рапортов'
), [idWell]) )
}, [idWell])
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>

View File

@ -1,12 +1,13 @@
import 'moment/locale/ru' import 'moment/locale/ru'
import moment from 'moment' import moment from 'moment'
import { useState, useEffect, memo, useCallback, useContext } from 'react' import { useState, useEffect, memo, useCallback } from 'react'
import { Radio, Button, Select, notification } from 'antd' import { Radio, Button, Select, notification } from 'antd'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { DateRangeWrapper } from 'components/Table' import { DateRangeWrapper } from 'components/Table'
import { LoaderPortal } from '@components/LoaderPortal' import { LoaderPortal } from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { wrapPrivateComponent } from '@utils'
import { Subscribe } from '@services/signalr' import { Subscribe } from '@services/signalr'
import { ReportService } from '@api' import { ReportService } from '@api'
@ -33,7 +34,7 @@ const reportFormats = [
{ value: 1, label: 'LAS' }, { value: 1, label: 'LAS' },
] ]
export const DiagramReport = memo(() => { const DiagramReport = memo(() => {
const [aviableDateRange, setAviableDateRange] = useState([moment(), moment()]) const [aviableDateRange, setAviableDateRange] = useState([moment(), moment()])
const [filterDateRange, setFilterDateRange] = useState([ const [filterDateRange, setFilterDateRange] = useState([
moment().subtract(1, 'days').startOf('day'), moment().subtract(1, 'days').startOf('day'),
@ -44,7 +45,7 @@ export const DiagramReport = memo(() => {
const [pagesCount, setPagesCount] = useState(0) const [pagesCount, setPagesCount] = useState(0)
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const handleReportCreation = useCallback(async () => await invokeWebApiWrapperAsync( const handleReportCreation = useCallback(async () => await invokeWebApiWrapperAsync(
async () => { async () => {
@ -88,7 +89,8 @@ export const DiagramReport = memo(() => {
!current.isBetween(aviableDateRange[0], aviableDateRange[1], 'seconds', '[]') !current.isBetween(aviableDateRange[0], aviableDateRange[1], 'seconds', '[]')
, [aviableDateRange]) , [aviableDateRange])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const datesRangeResponse = await ReportService.getReportsDateRange(idWell) const datesRangeResponse = await ReportService.getReportsDateRange(idWell)
if (!datesRangeResponse?.from || !datesRangeResponse.to) if (!datesRangeResponse?.from || !datesRangeResponse.to)
@ -110,9 +112,11 @@ export const DiagramReport = memo(() => {
setShowLoader, setShowLoader,
`Не удалось получить диапозон дат рапортов для скважины "${idWell}"`, `Не удалось получить диапозон дат рапортов для скважины "${idWell}"`,
'Получение диапозона дат рапортов' 'Получение диапозона дат рапортов'
), [idWell]) )
}, [idWell])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
if (filterDateRange?.length !== 2) return if (filterDateRange?.length !== 2) return
const pagesCount = await ReportService.getReportSize( const pagesCount = await ReportService.getReportSize(
@ -129,7 +133,8 @@ export const DiagramReport = memo(() => {
${filterDateRange[0].format(dateTimeFormat)} по ${filterDateRange[0].format(dateTimeFormat)} по
${filterDateRange[1].format(dateTimeFormat)}`, ${filterDateRange[1].format(dateTimeFormat)}`,
'Получение размера рапортов' 'Получение размера рапортов'
), [filterDateRange, step, format, idWell]) )
}, [filterDateRange, step, format, idWell])
return ( return (
<div> <div>
@ -174,4 +179,8 @@ export const DiagramReport = memo(() => {
) )
}) })
export default DiagramReport export default wrapPrivateComponent(DiagramReport, {
requirements: [ 'Report.get' ],
title: 'Диаграмма',
route: 'diagram_report',
})

View File

@ -1,36 +1,38 @@
import { useParams } from 'react-router-dom' import { Navigate, Route, Routes } from 'react-router-dom'
import { memo, useContext, useMemo } from 'react' import { memo, useMemo } from 'react'
import { FilePdfOutlined } from '@ant-design/icons' import { FilePdfOutlined } from '@ant-design/icons'
import { Layout } from 'antd' import { Layout } from 'antd'
import { RootPathContext } from '@asb/context' import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu, PrivateSwitch } from '@components/Private' import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import DailyReport from './DailyReport' import DailyReport from './DailyReport'
import DiagramReport from './DiagramReport' import DiagramReport from './DiagramReport'
const { Content } = Layout const { Content } = Layout
export const Reports = memo(() => { const Reports = memo(() => {
const { tab } = useParams() const root = useRootPath()
const root = useContext(RootPathContext)
const rootPath = useMemo(() => `${root}/reports`, [root]) const rootPath = useMemo(() => `${root}/reports`, [root])
return ( return (
<RootPathContext.Provider value={rootPath}> <RootPathContext.Provider value={rootPath}>
<Layout> <Layout>
<PrivateMenu mode={'horizontal'} selectable={true} selectedKeys={[tab]} className={'well_menu'}> <PrivateMenu className={'well_menu'}>
<PrivateMenu.Link key={'diagram_report'} icon={<FilePdfOutlined />} title={'Диаграмма'}/> <PrivateMenu.Link content={DiagramReport} icon={<FilePdfOutlined />} />
<PrivateMenu.Link key={'daily_report'} title={'Суточный рапорт'} /> <PrivateMenu.Link content={DailyReport} />
</PrivateMenu> </PrivateMenu>
<Layout> <Layout>
<Content className={'site-layout-background'}> <Content className={'site-layout-background'}>
<PrivateSwitch elseRedirect={['diagram_report', 'daily_report']}> <Routes>
<DiagramReport key={'diagram_report'} /> <Route index element={<Navigate to={'diagram_report'} replace />} />
<DailyReport key={'daily_report'} /> <Route path={'*'} element={<NoAccessComponent />} />
</PrivateSwitch>
<Route path={DiagramReport.route} element={<DiagramReport />} />
<Route path={DailyReport.route} element={<DailyReport />} />
</Routes>
</Content> </Content>
</Layout> </Layout>
</Layout> </Layout>
@ -38,4 +40,9 @@ export const Reports = memo(() => {
) )
}) })
export default Reports export default wrapPrivateComponent(Reports, {
requirements: [],
title: 'Рапорта',
route: 'reports/*',
key: 'reports',
})

View File

@ -1,14 +1,16 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { useState, useEffect, memo, useCallback, useContext } from 'react' import { useState, useEffect, memo, useCallback } from 'react'
import { useSearchParams } from 'react-router-dom'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { Flex } from '@components/Grid' import { Flex } from '@components/Grid'
import { CopyUrlButton } from '@components/CopyUrl'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { DatePickerWrapper, makeDateSorter } from '@components/Table' import { DatePickerWrapper, makeDateSorter } from '@components/Table'
import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker' import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
import { range, wrapPrivateComponent } from '@utils'
import { TelemetryDataSaubService } from '@api' import { TelemetryDataSaubService } from '@api'
import { range } from '@utils'
import { normalizeData } from '../TelemetryView' import { normalizeData } from '../TelemetryView'
import { ArchiveDisplay, cutData } from './ArchiveDisplay' import { ArchiveDisplay, cutData } from './ArchiveDisplay'
@ -56,15 +58,20 @@ const getLoadingInterval = (loaded, startDate, interval) => {
} }
} }
export const Archive = memo(() => { const Archive = memo(() => {
const [dataSaub, setDataSaub] = useState([]) const [dataSaub, setDataSaub] = useState([])
const [dateLimit, setDateLimit] = useState({ from: 0, to: new Date() }) const [dateLimit, setDateLimit] = useState({ from: 0, to: new Date() })
const [chartInterval, setChartInterval] = useState(parseInt(defaultPeriod) * 1000)
const [startDate, setStartDate] = useState(new Date(Date.now() - chartInterval))
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [loaded, setLoaded] = useState(null) const [loaded, setLoaded] = useState(null)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const [search, setSearchParams] = useSearchParams()
const getInitialRange = useCallback(() => parseInt(search.get('range') ?? defaultPeriod) * 1000, [search])
const getInitialDate = useCallback(() => new Date(search.get('start') ?? (Date.now() - chartInterval)), [search])
const [chartInterval, setChartInterval] = useState(getInitialRange)
const [startDate, setStartDate] = useState(getInitialDate)
const onGraphWheel = useCallback((e) => { const onGraphWheel = useCallback((e) => {
if (loaded && dateLimit.from && dateLimit.to) { if (loaded && dateLimit.from && dateLimit.to) {
@ -102,7 +109,17 @@ export const Archive = memo(() => {
}) })
}), [dateLimit]) }), [dateLimit])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
const params = {}
if (startDate)
params.start = startDate.toISOString()
if (chartInterval)
params.range = chartInterval / 1000
setSearchParams(params)
}, [startDate, chartInterval])
useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
let dates = await TelemetryDataSaubService.getDataDatesRange(idWell) let dates = await TelemetryDataSaubService.getDataDatesRange(idWell)
dates = { dates = {
@ -110,17 +127,16 @@ export const Archive = memo(() => {
to: new Date(dates?.to ?? 0) to: new Date(dates?.to ?? 0)
} }
setDateLimit(dates) setDateLimit(dates)
setStartDate(new Date(Math.max(dates.from, +dates.to - chartInterval)))
}, },
setShowLoader, setShowLoader,
`Не удалось загрузить диапозон телеметрии для скважины "${idWell}"`, `Не удалось загрузить диапозон телеметрии для скважины "${idWell}"`,
'Загрузка диапозона телеметрии' 'Загрузка диапозона телеметрии'
), []) )
}, [])
useEffect(() => { useEffect(() => {
setStartDate((startDate) => new Date(Math.min(Date.now() - chartInterval, startDate))) setStartDate((prev) => new Date(Math.max(dateLimit.from, Math.min(+prev, +dateLimit.to - chartInterval))))
setDataSaub([]) }, [chartInterval, dateLimit])
}, [chartInterval])
useEffect(() => { useEffect(() => {
if (showLoader) return if (showLoader) return
@ -150,6 +166,11 @@ export const Archive = memo(() => {
) )
}, [idWell, chartInterval, loaded, startDate]) }, [idWell, chartInterval, loaded, startDate])
const onRangeChange = useCallback((value) => {
setChartInterval(value * 1000)
setDataSaub([])
}, [])
return ( return (
<> <>
<Flex style={{margin: '8px 8px 0'}}> <Flex style={{margin: '8px 8px 0'}}>
@ -164,8 +185,9 @@ export const Archive = memo(() => {
</div> </div>
<div style={{ marginLeft: '1rem' }}> <div style={{ marginLeft: '1rem' }}>
Период:&nbsp; Период:&nbsp;
<PeriodPicker onChange={(val) => setChartInterval(val * 1000)} /> <PeriodPicker value={chartInterval / 1000} onChange={onRangeChange} />
</div> </div>
<CopyUrlButton style={{ marginLeft: '1rem' }} />
</Flex> </Flex>
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<ArchiveDisplay <ArchiveDisplay
@ -179,4 +201,8 @@ export const Archive = memo(() => {
) )
}) })
export default Archive export default wrapPrivateComponent(Archive, {
requirements: ['TelemetryDataSaub.get'],
title: 'Архив',
route: 'archive',
})

View File

@ -1,14 +1,13 @@
import { memo, useCallback, useContext, useEffect, useMemo, useReducer, useState } from 'react' import { memo, useCallback, useEffect, useMemo, useReducer, useState } from 'react'
import { useHistory, useParams } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { CloseOutlined } from '@ant-design/icons' import { CloseOutlined } from '@ant-design/icons'
import { Button, Menu, Popconfirm } from 'antd' import { Button, Menu, Popconfirm } from 'antd'
import { IdWellContext, RootPathContext } from '@asb/context' import { useIdWell, useRootPath } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { BaseWidget, WidgetSettingsWindow } from '@components/widgets' import { BaseWidget, WidgetSettingsWindow } from '@components/widgets'
import { getJSON, setJSON } from '@utils/storage' import { arrayOrDefault, wrapPrivateComponent, getJSON, setJSON, getTabname } from '@utils'
import { arrayOrDefault } from '@utils'
import Subscribe from '@services/signalr' import Subscribe from '@services/signalr'
import { import {
WitsInfoService, WitsInfoService,
@ -88,7 +87,7 @@ const groupsReducer = (groups, action) => {
break break
case 'add_widget': case 'add_widget':
if (groupIdx >= 0) if (groupIdx >= 0 && widgetIdx < 0)
newGroups[groupIdx].widgets.push(value) newGroups[groupIdx].widgets.push(value)
break break
case 'edit_widget': case 'edit_widget':
@ -106,21 +105,21 @@ const groupsReducer = (groups, action) => {
return newGroups return newGroups
} }
export const DashboardNNB = memo(() => { const DashboardNNB = memo(({ enableEditing = false }) => {
const [groups, dispatchGroups] = useReducer(groupsReducer, []) const [groups, dispatchGroups] = useReducer(groupsReducer, [])
const [witsInfo, setWitsInfo] = useState([]) const [witsInfo, setWitsInfo] = useState([])
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [selectedSettings, setSelectedSettings] = useState(null) const [selectedSettings, setSelectedSettings] = useState(null)
const [values, setValues] = useState({}) const [values, setValues] = useState({})
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const root = useContext(RootPathContext) const root = useRootPath()
const rootPath = useMemo(() => `${root}/dashboard_nnb`, [root]) const rootPath = useMemo(() => `${root}/dashboard_nnb`, [root])
const history = useHistory() const navigate = useNavigate()
const { tab: selectedGroup } = useParams() const selectedGroup = getTabname()
if (!selectedGroup && groups?.length > 0) if (!selectedGroup && groups?.length > 0)
history.push(`${rootPath}/${groups[0].id}`) navigate(`${rootPath}/${groups[0].id}`)
const group = useMemo(() => ({ const group = useMemo(() => ({
@ -128,7 +127,8 @@ export const DashboardNNB = memo(() => {
...groups.find(({ id }) => `${id}` === selectedGroup), ...groups.find(({ id }) => `${id}` === selectedGroup),
}), [groups, selectedGroup]) }), [groups, selectedGroup])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const info = await getWitsInfo() const info = await getWitsInfo()
setWitsInfo(info) setWitsInfo(info)
@ -137,7 +137,8 @@ export const DashboardNNB = memo(() => {
setIsLoading, setIsLoading,
'Не удалось загрузить информацию о параметрах ННБ', 'Не удалось загрузить информацию о параметрах ННБ',
'Получение информации о параметрах ННБ' 'Получение информации о параметрах ННБ'
), []) )
}, [])
const handleData = useCallback((data, recordId) => { const handleData = useCallback((data, recordId) => {
const mergedData = data.reduce((out, record) => ({ ...out, ...record }), {}) const mergedData = data.reduce((out, record) => ({ ...out, ...record }), {})
@ -175,8 +176,8 @@ export const DashboardNNB = memo(() => {
const removeGroup = useCallback((id) => { const removeGroup = useCallback((id) => {
dispatchGroups({ type: 'remove_group', groupId: `${id}` }) dispatchGroups({ type: 'remove_group', groupId: `${id}` })
if (id === selectedGroup) history.push(`${rootPath}`) if (id === selectedGroup) navigate(`${rootPath}`)
}, [rootPath, history, selectedGroup]) }, [rootPath, navigate, selectedGroup])
const addWidget = useCallback((settings) => dispatchGroups({ const addWidget = useCallback((settings) => dispatchGroups({
type: 'add_widget', type: 'add_widget',
@ -209,6 +210,8 @@ export const DashboardNNB = memo(() => {
selectable={true} selectable={true}
selectedKeys={[selectedGroup]} selectedKeys={[selectedGroup]}
> >
{enableEditing && (
<>
<Menu.Item key={'add_group'}> <Menu.Item key={'add_group'}>
<AddGroupWindow addGroup={addGroup} /> <AddGroupWindow addGroup={addGroup} />
</Menu.Item> </Menu.Item>
@ -217,9 +220,11 @@ export const DashboardNNB = memo(() => {
<AddWidgetWindow witsInfo={witsInfo} onAdded={addWidget} /> <AddWidgetWindow witsInfo={witsInfo} onAdded={addWidget} />
</Menu.Item> </Menu.Item>
)} )}
</>
)}
{groups.map(({ id, name, editable }) => ( {groups.map(({ id, name, editable }) => (
<Menu.Item key={id}> <Menu.Item key={id}>
{editable && ( {enableEditing && editable && (
<Popconfirm <Popconfirm
title={'Вы уверены, что хотите удалить группу, это действие невозможно отменить?'} title={'Вы уверены, что хотите удалить группу, это действие невозможно отменить?'}
onConfirm={() => removeGroup(id)} onConfirm={() => removeGroup(id)}
@ -229,7 +234,7 @@ export const DashboardNNB = memo(() => {
<Button type={'link'} icon={<CloseOutlined />} /> <Button type={'link'} icon={<CloseOutlined />} />
</Popconfirm> </Popconfirm>
)} )}
<Button type={'text'} style={{ paddingLeft: 0 }} onClick={() => history.push(`${rootPath}/${id}`)}>{name}</Button> <Button type={'text'} style={{ paddingLeft: 0 }} onClick={() => navigate(`${rootPath}/${id}`)}>{name}</Button>
</Menu.Item> </Menu.Item>
))} ))}
</Menu> </Menu>
@ -238,7 +243,7 @@ export const DashboardNNB = memo(() => {
<BaseWidget <BaseWidget
key={widget.id} key={widget.id}
// onEdit={group.editable && setSelectedSettings} // TODO: Доделать редактирование // onEdit={group.editable && setSelectedSettings} // TODO: Доделать редактирование
onRemove={group.editable && removeWidget} onRemove={ enableEditing && group.editable && removeWidget}
{...widget} {...widget}
value={values[widget.recordId]?.[widget.witsId]} value={values[widget.recordId]?.[widget.witsId]}
/> />
@ -255,4 +260,17 @@ export const DashboardNNB = memo(() => {
) )
}) })
export default DashboardNNB export default wrapPrivateComponent(DashboardNNB, {
requirements: [
// 'WitsInfo',
// 'WitsRecord1',
// 'WitsRecord7',
// 'WitsRecord8',
// 'WitsRecord50',
// 'WitsRecord60',
// 'WitsRecord61',
],
title: 'ННБ',
route: 'dashboard_nnb/*',
key: 'dashboard_nnb',
})

View File

@ -1,10 +1,13 @@
import { useState, useEffect, memo, useCallback, useContext } from 'react'
import { Table, Select, DatePicker, Input } from 'antd' import { Table, Select, DatePicker, Input } from 'antd'
import { useState, useEffect, memo, useCallback } from 'react'
import { useNavigate } from 'react-router-dom'
import moment from 'moment'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { makeColumn, makeDateColumn, makeNumericSorter } from '@components/Table' import { makeColumn, makeDateColumn, makeNumericSorter } from '@components/Table'
import { wrapPrivateComponent } from '@utils'
import { MessageService } from '@api' import { MessageService } from '@api'
@ -47,7 +50,7 @@ const filterOptions = [
const children = filterOptions.map((line) => <Option key={line.value}>{line.label}</Option>) const children = filterOptions.map((line) => <Option key={line.value}>{line.label}</Option>)
// Данные для таблицы // Данные для таблицы
export const Messages = memo(() => { const Messages = memo(() => {
const [messages, setMessages] = useState([]) const [messages, setMessages] = useState([])
const [pagination, setPagination] = useState(null) const [pagination, setPagination] = useState(null)
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
@ -56,11 +59,13 @@ export const Messages = memo(() => {
const [searchString, setSearchString] = useState('') const [searchString, setSearchString] = useState('')
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const navigate = useNavigate()
const onChangeSearchString = useCallback((message) => setSearchString(message.length > 2 ? message : ''), []) const onChangeSearchString = useCallback((message) => setSearchString(message.length > 2 ? message : ''), [])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const [begin, end] = range?.length > 1 ? [range[0].toISOString(), range[1].toISOString()] : [null, null] const [begin, end] = range?.length > 1 ? [range[0].toISOString(), range[1].toISOString()] : [null, null]
const skip = (page - 1) * pageSize const skip = (page - 1) * pageSize
@ -81,7 +86,12 @@ export const Messages = memo(() => {
setShowLoader, setShowLoader,
`Не удалось загрузить сообщения по скважине "${idWell}"`, `Не удалось загрузить сообщения по скважине "${idWell}"`,
'Полученик списка сообщений' 'Полученик списка сообщений'
), [idWell, page, categories, range, searchString]) )
}, [idWell, page, categories, range, searchString])
const onMessageRow = useCallback((record) => ({
onClick: () => navigate(`/well/${idWell}/telemetry/archive?range=1800&start=${moment(record?.date).subtract(3, 'minute').local().toISOString()}`)
}), [idWell, navigate])
return ( return (
<> <>
@ -118,10 +128,15 @@ export const Messages = memo(() => {
}} }}
rowKey={(record) => record.id} rowKey={(record) => record.id}
tableName={'messages'} tableName={'messages'}
onRow={onMessageRow}
/> />
</LoaderPortal> </LoaderPortal>
</> </>
) )
}) })
export default Messages export default wrapPrivateComponent(Messages, {
requirements: ['Message.get'],
title: 'Сообщения',
route: 'messages',
})

View File

@ -2,7 +2,7 @@ import { memo, useCallback, useMemo, useState } from 'react'
import { Button, Modal } from 'antd' import { Button, Modal } from 'antd'
import { EditableTable, makeActionHandler, makeTextColumn } from '@components/Table' import { EditableTable, makeActionHandler, makeTextColumn } from '@components/Table'
import { getPermissions } from '@utils/permissions' import { getPermissions } from '@utils'
import { DrillerService } from '@api' import { DrillerService } from '@api'
const reqRule = [{ message: 'Обязательное поле!', required: true }] const reqRule = [{ message: 'Обязательное поле!', required: true }]

View File

@ -11,7 +11,7 @@ import {
makeSelectColumn, makeSelectColumn,
} from '@components/Table' } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { getPermissions } from '@utils/permissions' import { getPermissions } from '@utils'
import { ScheduleService } from '@api' import { ScheduleService } from '@api'
const reqRule = [{ message: 'Обязательное поле!', required: true }] const reqRule = [{ message: 'Обязательное поле!', required: true }]

View File

@ -29,8 +29,13 @@ export const OperationsChart = memo(({ data, yDomain, width, height, bottom = 30
const lines = useMemo(() => d.map(d => ({ x: x(d.date), y: y(d.value) })), [d, x, y]) // Получаем массив координат линий const lines = useMemo(() => d.map(d => ({ x: x(d.date), y: y(d.value) })), [d, x, y]) // Получаем массив координат линий
useEffect(() => d3.select(axisX.current).call(d3.axisBottom(x)), [axisX, x]) // Рисуем ось X useEffect(() => {
useEffect(() => d3.select(axisY.current).call(d3.axisLeft(y)), [axisY, y]) // Рисуем ось Y d3.select(axisX.current).call(d3.axisBottom(x))
}, [axisX, x]) // Рисуем ось X
useEffect(() => {
d3.select(axisY.current).call(d3.axisLeft(y))
}, [axisY, y]) // Рисуем ось Y
return ( return (
<div className={'page-left'} ref={setRef}> <div className={'page-left'} ref={setRef}>

View File

@ -7,8 +7,7 @@ import LoaderPortal from '@components/LoaderPortal'
import { DateRangeWrapper } from '@components/Table' import { DateRangeWrapper } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { DetectedOperationService, DrillerService, TelemetryDataSaubService } from '@api' import { DetectedOperationService, DrillerService, TelemetryDataSaubService } from '@api'
import { getPermissions } from '@utils/permissions' import { getPermissions, arrayOrDefault, range, wrapPrivateComponent } from '@utils'
import { arrayOrDefault, range } from '@utils'
import DrillerList from './DrillerList' import DrillerList from './DrillerList'
import DrillerSchedule from './DrillerSchedule' import DrillerSchedule from './DrillerSchedule'
@ -17,7 +16,7 @@ import OperationsTable from './OperationsTable'
import '@styles/detected_operations.less' import '@styles/detected_operations.less'
export const Operations = memo(() => { const Operations = memo(() => {
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [dateRange, setDateRange] = useState([]) const [dateRange, setDateRange] = useState([])
const [yDomain, setYDomain] = useState(20) const [yDomain, setYDomain] = useState(20)
@ -53,7 +52,8 @@ export const Operations = memo(() => {
updateDrillers() updateDrillers()
}, [updateDrillers, permissions]) }, [updateDrillers, permissions])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const dates = await TelemetryDataSaubService.getDataDatesRange(idWell) const dates = await TelemetryDataSaubService.getDataDatesRange(idWell)
if (dates) { if (dates) {
@ -65,9 +65,11 @@ export const Operations = memo(() => {
setIsLoading, setIsLoading,
'Не удалось загрузить диапазон доступных дат', 'Не удалось загрузить диапазон доступных дат',
'Получение дапазона доступних дат', 'Получение дапазона доступних дат',
), [idWell]) )
}, [idWell])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
if (!dates) return if (!dates) return
const data = await DetectedOperationService.get(idWell, undefined, dates[0].toISOString(), dates[1].toISOString()) const data = await DetectedOperationService.get(idWell, undefined, dates[0].toISOString(), dates[1].toISOString())
@ -76,7 +78,8 @@ export const Operations = memo(() => {
setIsLoading, setIsLoading,
'Не удалось загрузить список определённых операций', 'Не удалось загрузить список определённых операций',
'Получение списка определённых операций', 'Получение списка определённых операций',
), [idWell, dates]) )
}, [idWell, dates])
return ( return (
<div className={'container detected-operations-page'}> <div className={'container detected-operations-page'}>
@ -116,4 +119,11 @@ export const Operations = memo(() => {
) )
}) })
export default Operations export default wrapPrivateComponent(Operations, {
requirements: [
// 'DetectedOperation.get',
'TelemetryDataSaub.get',
],
title: 'Операции',
route: 'operations',
})

View File

@ -1,7 +1,7 @@
import { Table } from 'antd' import { Table } from 'antd'
import { useState, useEffect, useCallback, memo, useContext } from 'react' import { useState, useEffect, useCallback, memo } from 'react'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { Subscribe } from '@services/signalr' import { Subscribe } from '@services/signalr'
@ -15,7 +15,7 @@ export const ActiveMessagesOnline = memo(() => {
const [messages, setMessages] = useState([]) const [messages, setMessages] = useState([])
const [loader, setLoader] = useState(false) const [loader, setLoader] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const handleReceiveMessages = useCallback((messages) => { const handleReceiveMessages = useCallback((messages) => {
if (messages) if (messages)

View File

@ -1,7 +1,7 @@
import { memo, useCallback, useContext, useMemo, useState } from 'react' import { memo, useCallback, useMemo, useState } from 'react'
import { Select, Modal, Input, InputNumber } from 'antd' import { Select, Modal, Input, InputNumber } from 'antd'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { Grid, GridItem } from '@components/Grid' import { Grid, GridItem } from '@components/Grid'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
@ -16,7 +16,7 @@ export const SetpointSender = memo(({ onClose, visible, setpointNames }) => {
const [setpoints, setSetpoints] = useState([]) const [setpoints, setSetpoints] = useState([])
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const addingColumns = useMemo(() => [ const addingColumns = useMemo(() => [
{ {

View File

@ -1,14 +1,12 @@
import { Button, Modal } from 'antd' import { Button, Modal } from 'antd'
import { useState, useEffect, memo, useCallback, useMemo, useContext } from 'react' import { useState, useEffect, memo, useCallback, useMemo } from 'react'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { Table } from '@components/Table' import { Table } from '@components/Table'
import { UserView } from '@components/views' import { UserView } from '@components/views'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { hasPermission } from '@utils/permissions' import { hasPermission, makeStringCutter, formatDate } from '@utils'
import { makeStringCutter } from '@utils/string'
import { formatDate } from '@utils'
import { SetpointsService } from '@api' import { SetpointsService } from '@api'
import SetpointSender from './SetpointSender' import SetpointSender from './SetpointSender'
@ -23,9 +21,10 @@ export const Setpoints = memo(({ ...other }) => {
const [selected, setSelected] = useState(null) const [selected, setSelected] = useState(null)
const [setpointNames, setSetpointNames] = useState([]) const [setpointNames, setSetpointNames] = useState([])
const idWell = useContext(IdWellContext) const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const names = await SetpointsService.getSetpointsNamesByIdWell(idWell) const names = await SetpointsService.getSetpointsNamesByIdWell(idWell)
if (!names) throw Error('Setpoints not found') if (!names) throw Error('Setpoints not found')
@ -38,7 +37,8 @@ export const Setpoints = memo(({ ...other }) => {
setIsLoading, setIsLoading,
`Не удалось загрузить список имён уставок по скважине "${idWell}"`, `Не удалось загрузить список имён уставок по скважине "${idWell}"`,
'Получение списка имён уставок' 'Получение списка имён уставок'
), [idWell]) )
}, [idWell])
const showMore = useCallback((id) => { const showMore = useCallback((id) => {
const selected = setpoints.find((sp) => sp.id === id) const selected = setpoints.find((sp) => sp.id === id)

View File

@ -1,6 +1,6 @@
import { memo, useCallback, useContext, useEffect, useState } from 'react' import { memo, useCallback, useEffect, useState } from 'react'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { WirelineView } from '@components/views' import { WirelineView } from '@components/views'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { TelemetryWirelineRunOutService } from '@api' import { TelemetryWirelineRunOutService } from '@api'
@ -10,7 +10,7 @@ export const WirelineRunOut = memo(() => {
const [twro, setTwro] = useState({}) const [twro, setTwro] = useState({})
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const update = useCallback(() => invokeWebApiWrapperAsync( const update = useCallback(() => invokeWebApiWrapperAsync(
async () => { async () => {
@ -25,7 +25,9 @@ export const WirelineRunOut = memo(() => {
if (visible) update() if (visible) update()
}, [update]) }, [update])
useEffect(update, [update]) useEffect(() => {
update()
}, [update])
return ( return (
<WirelineView <WirelineView

View File

@ -1,6 +1,14 @@
import { Select } from 'antd' import { Select } from 'antd'
import { useState, useEffect, useCallback, useContext } from 'react' import { useState, useEffect, useCallback, memo } from 'react'
import { useIdWell } from '@asb/context'
import { makeDateSorter } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { Grid, GridItem, Flex } from '@components/Grid'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
import { hasPermission, wrapPrivateComponent } from '@utils'
import { Subscribe } from '@services/signalr'
import { import {
DrillFlowChartService, DrillFlowChartService,
OperationStatService, OperationStatService,
@ -8,14 +16,6 @@ import {
TelemetryDataSpinService, TelemetryDataSpinService,
WellService WellService
} from '@api' } from '@api'
import { IdWellContext } from '@asb/context'
import { makeDateSorter } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { Grid, GridItem, Flex } from '@components/Grid'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
import { hasPermission } from '@utils/permissions'
import { Subscribe } from '@services/signalr'
import { MonitoringColumn } from './MonitoringColumn' import { MonitoringColumn } from './MonitoringColumn'
import { CustomColumn } from './CustomColumn' import { CustomColumn } from './CustomColumn'
@ -304,7 +304,7 @@ export const normalizeData = (data) => data?.map(item => ({
blockSpeed: Math.abs(item.blockSpeed) blockSpeed: Math.abs(item.blockSpeed)
})) ?? [] })) ?? []
export default function TelemetryView() { const TelemetryView = memo(() => {
const [dataSaub, setDataSaub] = useState([]) const [dataSaub, setDataSaub] = useState([])
const [dataSpin, setDataSpin] = useState([]) const [dataSpin, setDataSpin] = useState([])
const [chartInterval, setChartInterval] = useState(defaultPeriod) const [chartInterval, setChartInterval] = useState(defaultPeriod)
@ -313,7 +313,7 @@ export default function TelemetryView() {
const [flowChartData, setFlowChartData] = useState([]) const [flowChartData, setFlowChartData] = useState([])
const [rop, setRop] = useState(null) const [rop, setRop] = useState(null)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const handleDataSaub = useCallback((data) => { const handleDataSaub = useCallback((data) => {
if (data) { if (data) {
@ -347,7 +347,8 @@ export default function TelemetryView() {
return unsubscribe return unsubscribe
}, [idWell, chartInterval, handleDataSpin, handleDataSaub]) }, [idWell, chartInterval, handleDataSpin, handleDataSaub])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const well = await WellService.get(idWell) const well = await WellService.get(idWell)
const rop = await OperationStatService.getClusterRopStatByIdWell(idWell) const rop = await OperationStatService.getClusterRopStatByIdWell(idWell)
@ -357,7 +358,8 @@ export default function TelemetryView() {
setShowLoader, setShowLoader,
`Не удалось загрузить данные по скважине "${idWell}"`, `Не удалось загрузить данные по скважине "${idWell}"`,
'Получение данных по скважине' 'Получение данных по скважине'
), [idWell]) )
}, [idWell])
const onStatusChanged = useCallback((value) => invokeWebApiWrapperAsync( const onStatusChanged = useCallback((value) => invokeWebApiWrapperAsync(
async () => { async () => {
@ -428,4 +430,16 @@ export default function TelemetryView() {
</Grid> </Grid>
</LoaderPortal> </LoaderPortal>
) )
} })
export default wrapPrivateComponent(TelemetryView, {
requirements: [
'DrillFlowChart.get',
'OperationStat.get',
'TelemetryDataSaub.get',
'TelemetryDataSpin.get',
'Well.get',
],
title: 'Мониторинг',
route: 'telemetry',
})

View File

@ -1,46 +1,49 @@
import { useParams } from 'react-router-dom' import { Navigate, Route, Routes } from 'react-router-dom'
import { memo, useContext, useMemo } from 'react' import { memo, useMemo } from 'react'
import { Layout } from 'antd' import { Layout } from 'antd'
import { AlertOutlined, FundViewOutlined, DatabaseOutlined } from '@ant-design/icons' import { AlertOutlined, FundViewOutlined, DatabaseOutlined } from '@ant-design/icons'
import { RootPathContext } from '@asb/context' import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateSwitch, PrivateMenu } from '@components/Private' import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import Archive from './Archive' import Archive from './Archive'
import Messages from './Messages' import Messages from './Messages'
import Operations from './Operations'
import DashboardNNB from './DashboardNNB' import DashboardNNB from './DashboardNNB'
import TelemetryView from './TelemetryView' import TelemetryView from './TelemetryView'
import '@styles/index.css' import '@styles/index.css'
import Operations from './Operations'
const { Content } = Layout const { Content } = Layout
export const Telemetry = memo(() => { const Telemetry = memo(() => {
const { tab } = useParams() const root = useRootPath()
const root = useContext(RootPathContext)
const rootPath = useMemo(() => `${root}/telemetry`, [root]) const rootPath = useMemo(() => `${root}/telemetry`, [root])
return ( return (
<RootPathContext.Provider value={rootPath}> <RootPathContext.Provider value={rootPath}>
<Layout> <Layout>
<PrivateMenu mode={'horizontal'} selectable={true} selectedKeys={[tab]} className={'well_menu'}> <PrivateMenu className={'well_menu'}>
<PrivateMenu.Link key={'monitoring'} icon={<FundViewOutlined />} title={'Мониторинг'}/> <PrivateMenu.Link content={TelemetryView} icon={<FundViewOutlined />} />
<PrivateMenu.Link key={'messages'} icon={<AlertOutlined/>} title={'Сообщения'} /> <PrivateMenu.Link content={Messages} icon={<AlertOutlined/>} />
<PrivateMenu.Link key={'archive'} icon={<DatabaseOutlined />} title={'Архив'} /> <PrivateMenu.Link content={Archive} icon={<DatabaseOutlined />} />
<PrivateMenu.Link key={'dashboard_nnb'} title={'ННБ'} /> <PrivateMenu.Link content={DashboardNNB} />
<PrivateMenu.Link key={'operations'} title={'Операции'} /> <PrivateMenu.Link content={Operations} />
</PrivateMenu> </PrivateMenu>
<Layout> <Layout>
<Content className={'site-layout-background'}> <Content className={'site-layout-background'}>
<PrivateSwitch elseRedirect={['monitoring', 'messages', 'archive', 'dashboard_nnb']}> <Routes>
<TelemetryView key={'monitoring'} /> <Route index element={<Navigate to={TelemetryView.route} replace />} />
<Messages key={'messages'} /> <Route path={'*'} element={<NoAccessComponent />} />
<Archive key={'archive'} />
<DashboardNNB key={'dashboard_nnb/:tab?'} /> <Route path={TelemetryView.route} element={<TelemetryView />} />
<Operations key={'operations'}/> <Route path={Messages.route} element={<Messages />} />
</PrivateSwitch> <Route path={Archive.route} element={<Archive />} />
<Route path={DashboardNNB.route} element={<DashboardNNB />} />
<Route path={Operations.route} element={<Operations />} />
</Routes>
</Content> </Content>
</Layout> </Layout>
</Layout> </Layout>
@ -48,4 +51,10 @@ export const Telemetry = memo(() => {
) )
}) })
export default Telemetry export default wrapPrivateComponent(Telemetry, {
requirements: [],
icon: <FundViewOutlined />,
title: 'Телеметрия',
route: 'telemetry/*',
key: 'telemetry',
})

View File

@ -1,16 +1,17 @@
import { memo, useContext, useMemo } from 'react'
import { import {
FolderOutlined, FolderOutlined,
FundViewOutlined,
FilePdfOutlined, FilePdfOutlined,
ExperimentOutlined, ExperimentOutlined,
DeploymentUnitOutlined, DeploymentUnitOutlined,
} from '@ant-design/icons' } from '@ant-design/icons'
import { Layout } from 'antd' import { Layout } from 'antd'
import { useParams } from 'react-router-dom' import { memo, useMemo } from 'react'
import { Navigate, Route, Routes, useParams } from 'react-router-dom'
import { IdWellContext, RootPathContext } from '@asb/context' import { IdWellContext, RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu, PrivateSwitch } from '@components/Private' import { LayoutPortal } from '@components/Layout'
import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import Measure from './Measure' import Measure from './Measure'
import Reports from './Reports' import Reports from './Reports'
@ -24,42 +25,50 @@ import '@styles/index.css'
const { Content } = Layout const { Content } = Layout
export const Well = memo(() => { const Well = memo(() => {
const { idWell, tab } = useParams() const { idWell } = useParams()
const root = useContext(RootPathContext) const root = useRootPath()
const rootPath = useMemo(() => `${root}/well/${idWell}`, [root, idWell]) const rootPath = useMemo(() => `${root}/well/${idWell}`, [root, idWell])
return ( return (
<LayoutPortal>
<RootPathContext.Provider value={rootPath}> <RootPathContext.Provider value={rootPath}>
<Layout> <PrivateMenu className={'well_menu'}>
<PrivateMenu mode={'horizontal'} selectable={true} selectedKeys={[tab]} className={'well_menu'}> <PrivateMenu.Link content={Telemetry} />
<PrivateMenu.Link key={'telemetry'} icon={<FundViewOutlined />} title={'Телеметрия'}/> <PrivateMenu.Link content={Reports} icon={<FilePdfOutlined />} />
<PrivateMenu.Link key={'reports'} icon={<FilePdfOutlined />} title={'Рапорта'} /> <PrivateMenu.Link content={Analytics} icon={<DeploymentUnitOutlined />} />
<PrivateMenu.Link key={'analytics'} icon={<DeploymentUnitOutlined />} title={'Аналитика'} /> <PrivateMenu.Link content={WellOperations} icon={<FolderOutlined />} />
<PrivateMenu.Link key={'operations'} icon={<FolderOutlined />} title={'Операции по скважине'} /> <PrivateMenu.Link content={Documents} icon={<FolderOutlined />} />
<PrivateMenu.Link key={'document'} icon={<FolderOutlined />} title={'Документы'} /> <PrivateMenu.Link content={Measure} icon={<ExperimentOutlined />} />
<PrivateMenu.Link key={'measure'} icon={<ExperimentOutlined />} title={'Измерения'} /> <PrivateMenu.Link content={DrillingProgram} icon={<FolderOutlined />} />
<PrivateMenu.Link key={'drillingProgram'} icon={<FolderOutlined />} title={'Программа бурения'} />
</PrivateMenu> </PrivateMenu>
<IdWellContext.Provider value={idWell}> <IdWellContext.Provider value={idWell}>
<Layout> <Layout>
<Content className={'site-layout-background'}> <Content className={'site-layout-background'}>
<PrivateSwitch elseRedirect={['telemetry', 'reports', 'analytics', 'operations', 'telemetryAnalysis', 'document', 'measure', 'drillingProgram']}> <Routes>
<Telemetry key={'telemetry/:tab?'} /> <Route index element={<Navigate to={Telemetry.getKey()} replace />} />
<Reports key={'reports/:tab?'} /> <Route path={'*'} element={<NoAccessComponent />} />
<Analytics key={'analytics/:tab?'} />
<WellOperations key={'operations/:tab?'} /> <Route path={Telemetry.route} element={<Telemetry />} />
<Documents key={'document/:category?'} /> <Route path={Reports.route} element={<Reports />} />
<Measure key={'measure'} /> <Route path={Analytics.route} element={<Analytics />} />
<DrillingProgram key={'drillingProgram'} /> <Route path={WellOperations.route} element={<WellOperations />} />
</PrivateSwitch> <Route path={Documents.route} element={<Documents />} />
<Route path={Measure.route} element={<Measure />} />
<Route path={DrillingProgram.route} element={<DrillingProgram />} />
</Routes>
</Content> </Content>
</Layout> </Layout>
</IdWellContext.Provider> </IdWellContext.Provider>
</Layout>
</RootPathContext.Provider> </RootPathContext.Provider>
</LayoutPortal>
) )
}) })
export default Well export default wrapPrivateComponent(Well, {
requirements: [],
title: 'Скважина',
route: 'well/:idWell/*',
key: 'well',
})

View File

@ -1,6 +1,6 @@
import { useState, useEffect, memo, useContext } from 'react' import { useState, useEffect, memo } from 'react'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { import {
EditableTable, EditableTable,
makeNumericMinMax, makeNumericMinMax,
@ -8,8 +8,7 @@ import {
} from '@components/Table' } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { hasPermission } from '@utils/permissions' import { hasPermission, arrayOrDefault } from '@utils'
import { arrayOrDefault } from '@utils'
import { DrillFlowChartService } from '@api' import { DrillFlowChartService } from '@api'
@ -26,7 +25,7 @@ export const DrillProcessFlow = memo(() => {
const [flows, setFlows] = useState([]) const [flows, setFlows] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const updateFlows = () => invokeWebApiWrapperAsync( const updateFlows = () => invokeWebApiWrapperAsync(
async () => { async () => {
@ -38,7 +37,9 @@ export const DrillProcessFlow = memo(() => {
'Получение режимно-технологической карты скважины' 'Получение режимно-технологической карты скважины'
) )
useEffect(updateFlows, [idWell]) useEffect(() => {
updateFlows()
}, [idWell])
const onAdd = async (flow) => { const onAdd = async (flow) => {
flow.idWell = idWell flow.idWell = idWell
@ -62,8 +63,8 @@ export const DrillProcessFlow = memo(() => {
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<EditableTable <EditableTable
size={'small'}
bordered bordered
size={'small'}
columns={columns} columns={columns}
dataSource={flows} dataSource={flows}
tableName={'well_operations_flow'} tableName={'well_operations_flow'}

View File

@ -1,17 +1,21 @@
import { memo, useState } from 'react' import { memo, useMemo, useState } from 'react'
import { Button, Tooltip, Modal } from 'antd' import { Button, Tooltip, Modal } from 'antd'
import { FileOutlined, ImportOutlined, ExportOutlined } from '@ant-design/icons' import { FileOutlined, ImportOutlined, ExportOutlined } from '@ant-design/icons'
import { useIdWell } from '@asb/context'
import { download } from '@components/factory' import { download } from '@components/factory'
import { hasPermission } from '@utils/permissions' import { hasPermission } from '@utils'
import { ImportOperations } from './ImportOperations' import { ImportOperations } from './ImportOperations'
const style = { margin: 4 } const style = { margin: 4 }
export const ImportExportBar = memo(({ idWell, onImported, disabled }) => { export const ImportExportBar = memo(({ idWell: wellId, onImported, disabled }) => {
const [isImportModalVisible, setIsImportModalVisible] = useState(false) const [isImportModalVisible, setIsImportModalVisible] = useState(false)
const idWellContext = useIdWell()
const idWell = useMemo(() => wellId ?? idWellContext, [idWellContext])
const downloadTemplate = async () => await download(`/api/well/${idWell}/wellOperations/template`) const downloadTemplate = async () => await download(`/api/well/${idWell}/wellOperations/template`)
const downloadExport = async () => await download(`/api/well/${idWell}/wellOperations/export`) const downloadExport = async () => await download(`/api/well/${idWell}/wellOperations/export`)

View File

@ -3,7 +3,7 @@ import { memo, useEffect, useMemo, useState } from 'react'
import { makeNumericRender } from '@components/Table' import { makeNumericRender } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { formatDate, fractionalSum } from '@utils/datetime' import { formatDate, fractionalSum } from '@utils'
import '@styles/tvd.less' import '@styles/tvd.less'
@ -26,7 +26,8 @@ export const AdditionalTables = memo(({ operations, xLabel, setIsLoading }) => {
.reduce((out, row) => out + (row?.durationHours ?? 0), 0) .reduce((out, row) => out + (row?.durationHours ?? 0), 0)
, [operations.fact]) , [operations.fact])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const [factStartDate, factEndDate] = calcEndDate(operations.fact) const [factStartDate, factEndDate] = calcEndDate(operations.fact)
const [planStartDate, planEndDate] = calcEndDate(operations.plan) const [planStartDate, planEndDate] = calcEndDate(operations.plan)
@ -46,7 +47,8 @@ export const AdditionalTables = memo(({ operations, xLabel, setIsLoading }) => {
}, },
setIsLoading, setIsLoading,
'Не удалось высчитать дополнительные данные' 'Не удалось высчитать дополнительные данные'
), [operations, setIsLoading]) )
}, [operations, setIsLoading])
return ( return (
<> <>

View File

@ -23,17 +23,21 @@ export const NptTable = memo(({ operations }) => {
const [filteredNPT, setFilteredNPT] = useState([]) const [filteredNPT, setFilteredNPT] = useState([])
const [isTableLoading, setIsTableLoading] = useState(false) const [isTableLoading, setIsTableLoading] = useState(false)
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => setNPT(operations?.filter((row) => row?.isNPT) ?? []), async () => setNPT(operations?.filter((row) => row?.isNPT) ?? []),
setIsTableLoading, setIsTableLoading,
'Не удалось получить список НПВ' 'Не удалось получить список НПВ'
), [operations]) )
}, [operations])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => setFilteredNPT(npt.filter((row) => !filterValue || row.durationHours >= filterValue)), async () => setFilteredNPT(npt.filter((row) => !filterValue || row.durationHours >= filterValue)),
setIsTableLoading, setIsTableLoading,
'Не удалось отфильтровать НПВ по времени' 'Не удалось отфильтровать НПВ по времени'
), [npt, filterValue]) )
}, [npt, filterValue])
return ( return (
<div className={'tvd-right'}> <div className={'tvd-right'}>

View File

@ -1,5 +1,5 @@
import { useHistory } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { memo, useState, useRef, useEffect, useCallback, useContext, useMemo } from 'react' import { memo, useState, useRef, useEffect, useCallback, useMemo } from 'react'
import { DoubleLeftOutlined, DoubleRightOutlined } from '@ant-design/icons' import { DoubleLeftOutlined, DoubleRightOutlined } from '@ant-design/icons'
import { Switch, Button } from 'antd' import { Switch, Button } from 'antd'
@ -16,11 +16,10 @@ import 'chartjs-adapter-moment'
import zoomPlugin from 'chartjs-plugin-zoom' import zoomPlugin from 'chartjs-plugin-zoom'
import ChartDataLabels from 'chartjs-plugin-datalabels' import ChartDataLabels from 'chartjs-plugin-datalabels'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { formatDate, fractionalSum } from '@utils/datetime' import { formatDate, fractionalSum, wrapPrivateComponent, getOperations } from '@utils'
import { getOperations } from '@utils/functions'
import NptTable from './NptTable' import NptTable from './NptTable'
import NetGraphExport from './NetGraphExport' import NetGraphExport from './NetGraphExport'
@ -115,18 +114,18 @@ const makeDataset = (data, label, color, borderWidth = 1.5, borderDash) => ({
borderDash, borderDash,
}) })
export const Tvd = memo(({ idWell: wellId, title, ...other }) => { const Tvd = memo(({ idWell: wellId, title, ...other }) => {
const [chart, setChart] = useState() const [chart, setChart] = useState()
const [xLabel, setXLabel] = useState('day') const [xLabel, setXLabel] = useState('day')
const [operations, setOperations] = useState({}) const [operations, setOperations] = useState({})
const [tableVisible, setTableVisible] = useState(false) const [tableVisible, setTableVisible] = useState(false)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const idWellContext = useContext(IdWellContext) const idWellContext = useIdWell()
const idWell = useMemo(() => wellId ?? idWellContext, [wellId, idWellContext]) const idWell = useMemo(() => wellId ?? idWellContext, [wellId, idWellContext])
const chartRef = useRef(null) const chartRef = useRef(null)
const history = useHistory() const navigate = useNavigate()
const onPointClick = useCallback((e) => { const onPointClick = useCallback((e) => {
const points = e?.chart?.tooltip?.dataPoints const points = e?.chart?.tooltip?.dataPoints
@ -137,15 +136,17 @@ export const Tvd = memo(({ idWell: wellId, title, ...other }) => {
const datasetName = datasetId === 2 ? 'plan' : 'fact' const datasetName = datasetId === 2 ? 'plan' : 'fact'
const ids = points.filter((p) => p?.raw?.id).map((p) => p.raw.id).join(',') const ids = points.filter((p) => p?.raw?.id).map((p) => p.raw.id).join(',')
history.push(`/well/${idWell}/operations/${datasetName}/?selectedId=${ids}`) navigate(`/well/${idWell}/operations/${datasetName}/?selectedId=${ids}`)
}, [idWell, history]) }, [idWell, navigate])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => setOperations(await getOperations(idWell)), async () => setOperations(await getOperations(idWell)),
setIsLoading, setIsLoading,
`Не удалось загрузить операции по скважине "${idWell}"`, `Не удалось загрузить операции по скважине "${idWell}"`,
'Получение списка опервций по скважине' 'Получение списка опервций по скважине'
), [idWell]) )
}, [idWell])
useEffect(() => { useEffect(() => {
const withoutNpt = [] const withoutNpt = []
@ -227,4 +228,8 @@ export const Tvd = memo(({ idWell: wellId, title, ...other }) => {
) )
}) })
export default Tvd export default wrapPrivateComponent(Tvd, {
requirements: [ 'OperationStat.get' ],
title: 'TVD',
route: 'tvd',
})

View File

@ -1,6 +1,6 @@
import { useState, useEffect, useCallback, memo, useMemo, useContext } from 'react' import { useState, useEffect, useCallback, memo, useMemo } from 'react'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { import {
EditableTable, EditableTable,
makeSelectColumn, makeSelectColumn,
@ -11,15 +11,14 @@ import {
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { DrillParamsService, WellOperationService } from '@api' import { DrillParamsService, WellOperationService } from '@api'
import { hasPermission } from '@utils/permissions' import { hasPermission, arrayOrDefault } from '@utils'
import { arrayOrDefault } from '@utils'
export const getColumns = async (idWell) => { export const getColumns = async (idWell) => {
let sectionTypes = await WellOperationService.getSectionTypes(idWell) let sectionTypes = await WellOperationService.getSectionTypes(idWell)
sectionTypes = Object.entries(sectionTypes).map(([id, value]) => ({ sectionTypes = Object.entries(sectionTypes).map(([id, value]) => ({
label: value, label: value,
value: parseInt(id), value: id,
})) }))
return [ return [
@ -41,7 +40,7 @@ export const WellDrillParams = memo(() => {
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const [columns, setColumns] = useState([]) const [columns, setColumns] = useState([])
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const updateParams = useCallback(async () => await invokeWebApiWrapperAsync( const updateParams = useCallback(async () => await invokeWebApiWrapperAsync(
async () => { async () => {
@ -55,10 +54,12 @@ export const WellDrillParams = memo(() => {
'Получение списка режимов бурения скважины' 'Получение списка режимов бурения скважины'
), [idWell]) ), [idWell])
useEffect(() => (async () => { useEffect(() => {
(async () => {
setColumns(await getColumns(idWell)) setColumns(await getColumns(idWell))
await updateParams() await updateParams()
})(), [idWell, updateParams]) })()
}, [idWell, updateParams])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: DrillParamsService, service: DrillParamsService,
@ -73,8 +74,8 @@ export const WellDrillParams = memo(() => {
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<EditableTable <EditableTable
size={'small'}
bordered bordered
size={'small'}
columns={columns} columns={columns}
dataSource={params} dataSource={params}
tableName={'well_drill_params'} tableName={'well_drill_params'}

View File

@ -1,9 +1,9 @@
import moment from 'moment' import moment from 'moment'
import { Input } from 'antd' import { Input } from 'antd'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
import { useState, useEffect, memo, useMemo, useCallback, useContext } from 'react' import { useState, useEffect, memo, useMemo, useCallback } from 'react'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import { import {
EditableTable, EditableTable,
makeColumn, makeColumn,
@ -18,11 +18,9 @@ import {
} from '@components/Table' } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { hasPermission } from '@utils/permissions' import { arrayOrDefault, wrapPrivateComponent, hasPermission } from '@utils'
import { arrayOrDefault } from '@utils'
import { WellOperationService } from '@api' import { WellOperationService } from '@api'
const { TextArea } = Input const { TextArea } = Input
const basePageSize = 160 const basePageSize = 160
@ -54,13 +52,13 @@ const generateColumns = (showNpt = false, categories = [], sectionTypes = []) =>
makeTextColumn('Комментарий', 'comment', null, null, null, { editable: true, input: <TextArea/> }), makeTextColumn('Комментарий', 'comment', null, null, null, { editable: true, input: <TextArea/> }),
].filter(Boolean) ].filter(Boolean)
export const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => { const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => {
const [pageNumAndPageSize, setPageNumAndPageSize] = useState({ current: 1, pageSize: basePageSize }) const [pageNumAndPageSize, setPageNumAndPageSize] = useState({ current: 1, pageSize: basePageSize })
const [paginationTotal, setPaginationTotal] = useState(0) const [paginationTotal, setPaginationTotal] = useState(0)
const [operations, setOperations] = useState([]) const [operations, setOperations] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
const [categories, setCategories] = useState([]) const [categories, setCategories] = useState([])
const [sectionTypes, setSectionTypes] = useState([]) const [sectionTypes, setSectionTypes] = useState([])
@ -73,7 +71,8 @@ export const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => {
return arrayOrDefault(query.get('selectedId')?.split(',')?.map(parseInt)) return arrayOrDefault(query.get('selectedId')?.split(',')?.map(parseInt))
}, [location]) }, [location])
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const categories = arrayOrDefault(await WellOperationService.getCategories(idWell)) const categories = arrayOrDefault(await WellOperationService.getCategories(idWell))
setCategories(categories.map((item) => ({ value: item.id, label: item.name }))) setCategories(categories.map((item) => ({ value: item.id, label: item.name })))
@ -84,7 +83,8 @@ export const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => {
setShowLoader, setShowLoader,
'Не удалось загрузить список операций по скважине', 'Не удалось загрузить список операций по скважине',
'Получение списка операций по скважине' 'Получение списка операций по скважине'
), [idWell]) )
}, [idWell])
const updateOperations = useCallback(() => invokeWebApiWrapperAsync( const updateOperations = useCallback(() => invokeWebApiWrapperAsync(
async () => { async () => {
@ -103,7 +103,9 @@ export const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => {
'Получение списка операций по скважине' 'Получение списка операций по скважине'
), [idWell, idType, pageNumAndPageSize]) ), [idWell, idType, pageNumAndPageSize])
useEffect(updateOperations, [updateOperations]) useEffect(() => {
updateOperations()
}, [updateOperations])
const handlerProps = useMemo(() => ({ const handlerProps = useMemo(() => ({
service: WellOperationService, service: WellOperationService,
@ -151,4 +153,20 @@ export const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => {
) )
}) })
export default WellOperationsEditor export const WellOperationsEditorPlan = wrapPrivateComponent(
() => <WellOperationsEditor idType={0} tableName={'well_operations_plan'}/>,
{
requirements: [ 'WellOperation.get' ],
title: 'План',
route: 'plan',
}
)
export const WellOperationsEditorFact = wrapPrivateComponent(
() => <WellOperationsEditor idType={1} tableName={'well_operations_fact'}/>,
{
requirements: [ 'WellOperation.get' ],
title: 'Факт',
route: 'fact',
}
)

View File

@ -1,10 +1,10 @@
import { useState, useEffect, memo, useContext } from 'react' import { useState, useEffect, memo } from 'react'
import { IdWellContext } from '@asb/context' import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { Table, makeColumn, makeColumnsPlanFact, makeNumericRender } from '@components/Table' import { Table, makeColumn, makeColumnsPlanFact, makeNumericRender } from '@components/Table'
import { calcDuration } from '@utils/datetime' import { calcDuration } from '@utils'
import { OperationStatService } from '@api' import { OperationStatService } from '@api'
@ -25,9 +25,10 @@ export const WellSectionsStat = memo(() => {
const [sections, setSections] = useState([]) const [sections, setSections] = useState([])
const [showLoader, setShowLoader] = useState(false) const [showLoader, setShowLoader] = useState(false)
const idWell = useContext(IdWellContext) const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync( useEffect(() => {
invokeWebApiWrapperAsync(
async () => { async () => {
const sectionsResponse = await OperationStatService.getStatWell(idWell) const sectionsResponse = await OperationStatService.getStatWell(idWell)
@ -58,7 +59,8 @@ export const WellSectionsStat = memo(() => {
setShowLoader, setShowLoader,
`Не удалось получить статистику по секциям скважины "${idWell}"`, `Не удалось получить статистику по секциям скважины "${idWell}"`,
'Получение статистики по секциям скважины' 'Получение статистики по секциям скважины'
), [idWell]) )
}, [idWell])
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>

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