* блок 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",
"private": true,
"dependencies": {
"@craco/craco": "^6.1.2",
"@microsoft/signalr": "^6.0.4",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^12.8.3",
"@types/react-dom": "^18.0.3",
"antd": "^4.15.0",
"chart.js": "^3.6.0",
"@microsoft/signalr": "^6.0.5",
"@svgr/webpack": "^6.2.1",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/user-event": "^14.2.0",
"@types/react-dom": "^18.0.5",
"antd": "^4.20.7",
"chart.js": "^3.8.0",
"chartjs-adapter-moment": "^1.0.0",
"chartjs-plugin-datalabels": "^2.0.0-rc.1",
"chartjs-plugin-zoom": "^1.1.1",
"craco-less": "^1.17.1",
"chartjs-plugin-datalabels": "^2.0.0",
"chartjs-plugin-zoom": "^1.2.1",
"d3": "^7.4.4",
"moment": "^2.29.1",
"pigeon-maps": "^0.19.7",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"rxjs": "^7.5.4",
"typescript": "^4.2.3",
"web-vitals": "^1.1.1"
"less": "^4.1.2",
"less-loader": "^11.0.0",
"moment": "^2.29.3",
"pigeon-maps": "^0.21.0",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-router-dom": "^6.3.0",
"rxjs": "^7.5.5",
"typescript": "^4.7.2",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"start": "webpack-dev-server --mode=development --open --hot",
"build": "webpack --mode=production",
"test": "jest",
"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",
"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",
"react_start": "react-scripts start",
"react_build": "react-scripts build",
"react_test": "react-scripts test",
"eject": "react-scripts eject"
"oug_dev": "npx openapi -i http://46.146.209.148:89/swagger/v1/swagger.json -o src/services/api"
},
"proxy": "http://46.146.209.148:89",
"eslintConfig": {
@ -58,12 +53,57 @@
"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": {
"@types/d3": "^7.1.0",
"@types/react": "^17.0.3",
"@types/react-router-dom": "^5.3.2",
"craco-alias": "^3.0.1",
"openapi-typescript": "^3.4.1",
"openapi-typescript-codegen": "^0.21.0"
"@babel/core": "^7.18.2",
"@babel/preset-env": "^7.18.2",
"@babel/preset-react": "^7.17.12",
"@babel/preset-typescript": "^7.17.12",
"@testing-library/react": "^13.3.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">
<head>
<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="theme-color" 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="description"
content="Онлайн мониторинг процесса бурения в реальном времени в офисе заказчика"
/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<meta name="description" content="Онлайн мониторинг процесса бурения в реальном времени в офисе заказчика" />
<title>АСБ Vision</title>
</head>
<body>

View File

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

View File

@ -3,7 +3,7 @@ import { Rule } from 'antd/lib/form'
import { Form, Input, Modal, FormProps } from 'antd'
import { AuthService, UserDto } from '@api'
import { getUserId, getUserLogin } from '@utils/storage'
import { getUserId, getUserLogin } from '@utils'
import { passwordRules, createPasswordRules } from '@utils/validationRules'
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>
<PageHeader isAdmin title={title} style={{ backgroundColor: '#900' }}>
<Button size={'large'}>
<Link to={{ pathname: '/', state: { from: location.pathname }}}>Вернуться на сайт</Link>
<Link to={'/'}>Вернуться на сайт</Link>
</Button>
</PageHeader>
<Layout>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import { Menu, MenuItemProps } from 'antd'
import { memo, NamedExoticComponent } from 'react'
import { Link, useLocation } from 'react-router-dom'
import { isURLAvailable } from '@utils/permissions'
import { isURLAvailable } from '@utils'
export type PrivateMenuItemProps = MenuItemProps & {
root: string
@ -20,7 +20,7 @@ export const PrivateMenuItemLink = memo<PrivateMenuItemLinkProps>(({ root = '',
const location = useLocation()
return (
<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>
)
})

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

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 { PrivateDefaultRoute } from './PrivateDefaultRoute'
export { PrivateMenu, PrivateMenuLink } from './PrivateMenu'
export { PrivateSwitch } from './PrivateSwitch'
export { PrivateRoutes } from './PrivateRoutes'
export type { PrivateRouteProps } from './PrivateRoute'
export type { PrivateContentProps } from './PrivateContent' // TODO: Remove
export type { PrivateMenuItemProps, PrivateMenuItemLinkProps } from './PrivateMenuItem' // TODO: Remove
export type { PrivateDefaultRouteProps } from './PrivateDefaultRoute'
export type { PrivateMenuProps, PrivateMenuLinkProps } from './PrivateMenu'
export type { PrivateSwitchProps } from './PrivateSwitch'
export type { PrivateRoutesProps } from './PrivateRoutes'

View File

@ -1,8 +1,8 @@
import { memo, ReactNode, useCallback, useEffect, useState } from 'react'
import { Select, SelectProps, Tag } from 'antd'
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 '.'

View File

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

View File

@ -1,8 +1,8 @@
import { memo, ReactNode, useCallback, useEffect, useState } from 'react'
import { Select, SelectProps } from 'antd'
import { OmitExtends } from '@utils'
import { findTimezoneId, rawTimezones, TimezoneId } from '@utils/datetime'
import { findTimezoneId, rawTimezones, TimezoneId } from '@utils'
import type { OmitExtends } from '@utils/types'
import { SimpleTimezoneDto } from '@api'
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 { NamePath, Rule } from 'rc-field-form/lib/interface'
type EditableCellProps = React.DetailedHTMLProps<React.TdHTMLAttributes<HTMLTableDataCellElement>, HTMLTableDataCellElement> & {
editing?: boolean
dataIndex?: NamePath
input?: React.Component
input?: ReactNode
isRequired?: boolean
title: string
formItemClass?: string
formItemRules?: Rule[]
children: React.ReactNode
children: ReactNode
initialValue: any
}

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { Moment } from 'moment'
import { TimePicker, TimePickerProps } from 'antd'
import { memo, useCallback, useMemo } from 'react'
import { defaultTimeFormat, momentToTime, timeToMoment } from '@utils/datetime'
import { defaultTimeFormat, momentToTime, timeToMoment } from '@utils'
import { TimeDto } from '@api'
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 { TimeDto } from '@api'

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,9 @@
import { memo } from 'react'
import logo from '@images/logo_32.png'
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

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 ReactDOM from 'react-dom'
import App from './App'
import { createRoot } from 'react-dom/client'
import reportWebVitals from './reportWebVitals'
import App from './App'
import '@styles/index.css'
ReactDOM.render((
const container = document.getElementById('root') ?? document.body
const root = createRoot(container)
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
), document.getElementById('root'))
)
// If you want to start measuring performance in your app, pass a function
// 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 { Link, useHistory } from 'react-router-dom'
import { Link, useNavigate } from 'react-router-dom'
import { CloseCircleOutlined } from '@ant-design/icons'
const { Paragraph, Text } = Typography
export const AccessDenied = memo(() => {
const history = useHistory()
const navigate = useNavigate()
return (
<Result
@ -28,7 +28,7 @@ export const AccessDenied = memo(() => {
<Paragraph>
<CloseCircleOutlined style={{ color: 'red' }} />
&nbsp;Страницы не существует.&nbsp;
<Link to={'#'} onClick={history.goBack}>Вернуться назад &gt;</Link>
<Link to={'#'} onClick={navigate(-1)}>Вернуться назад &gt;</Link>
</Paragraph>
<Paragraph>
<CloseCircleOutlined style={{ color: 'red' }} />

View File

@ -12,13 +12,12 @@ import {
} from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminClusterService, AdminDepositService } from '@api'
import { arrayOrDefault } from '@utils'
import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { min1 } from '@utils/validationRules'
import { hasPermission } from '@utils/permissions'
import { coordsFixed } from './DepositController'
export const ClusterController = memo(() => {
const ClusterController = memo(() => {
const [deposits, setDeposits] = useState([])
const [clusters, setClusters] = useState([])
const [showLoader, setShowLoader] = useState(false)
@ -58,7 +57,8 @@ export const ClusterController = memo(() => {
'Получение списка кустов'
), [])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
let deposits = arrayOrDefault(await AdminDepositService.getAll())
deposits = deposits.map((deposit) => ({ value: deposit.id, label: deposit.caption }))
@ -67,9 +67,12 @@ export const ClusterController = memo(() => {
setShowLoader,
`Не удалось загрузить список месторождений`,
'Получение списка месторождений'
), [])
)
}, [])
useEffect(updateTable, [updateTable])
useEffect(() => {
updateTable()
}, [updateTable])
const handlerProps = useMemo(() => ({
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'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminCompanyService, AdminCompanyTypeService } from '@api'
import { arrayOrDefault } from '@utils'
import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { min1 } from '@utils/validationRules'
import { hasPermission } from '@utils/permissions'
export const CompanyController = memo(() => {
const CompanyController = memo(() => {
const [columns, setColumns] = useState([])
const [companies, setCompanies] = useState([])
const [showLoader, setShowLoader] = useState(false)
@ -31,7 +29,8 @@ export const CompanyController = memo(() => {
setCompanies(arrayOrDefault(companies))
}, [])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async() => {
const companyTypes = arrayOrDefault(await AdminCompanyTypeService.getAll()).map((companyType) => ({
value: companyType.id,
@ -56,7 +55,8 @@ export const CompanyController = memo(() => {
setShowLoader,
`Не удалось загрузить список типов компаний`,
'Получение списка типов команд'
), [updateTable])
)
}, [updateTable])
const handlerProps = useMemo(() => ({
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
} from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminCompanyTypeService } from '@api'
import { arrayOrDefault } from '@utils'
import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { min1 } from '@utils/validationRules'
import { hasPermission } from '@utils/permissions'
import { AdminCompanyTypeService } from '@api'
const columns = [
makeColumn('Название', 'caption', {
@ -23,7 +22,7 @@ const columns = [
}),
]
export const CompanyTypeController = memo(() => {
const CompanyTypeController = memo(() => {
const [companyTypes, setCompanyTypes] = useState([])
const [showLoader, setShowLoader] = useState(false)
const [searchValue, setSearchValue] = useState('')
@ -42,7 +41,9 @@ export const CompanyTypeController = memo(() => {
'Получение списка типов компаний'
), [])
useEffect(updateTable, [updateTable])
useEffect(() => {
updateTable()
}, [updateTable])
const handlerProps = useMemo(() => ({
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 { 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 { arrayOrDefault } from '@utils'
import { AdminDepositService } from '@api'
export const coordsFixed = (coords) => coords && isFinite(coords) ? (+coords).toPrecision(10) : '-'
@ -17,7 +16,7 @@ const depositColumns = [
makeTimezoneColumn('Зона', 'timezone', null, true, { width: 150 }),
]
export const DepositController = memo(() => {
const DepositController = memo(() => {
const [deposits, setDeposits] = useState([])
const [showLoader, setShowLoader] = useState(false)
const [searchValue, setSearchValue] = useState('')
@ -39,7 +38,9 @@ export const DepositController = memo(() => {
'Получение списка месторождений'
), [])
useEffect(updateTable, [updateTable])
useEffect(() => {
updateTable()
}, [updateTable])
const handlerProps = useMemo(() => ({
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
} from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { AdminPermissionService } from '@api'
import { arrayOrDefault } from '@utils'
import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { min1 } from '@utils/validationRules'
import { hasPermission } from '@utils/permissions'
import { AdminPermissionService } from '@api'
const columns = [
makeColumn('Название', 'name', {
@ -26,7 +25,7 @@ const columns = [
}),
]
export const PermissionController = memo(() => {
const PermissionController = memo(() => {
const [permissions, setPermissions] = useState([])
const [showLoader, setShowLoader] = useState(false)
const [searchValue, setSearchValue] = useState('')
@ -47,7 +46,9 @@ export const PermissionController = memo(() => {
'Получение списка прав'
), [])
useEffect(() => updateTable(), [updateTable])
useEffect(() => {
updateTable()
}, [updateTable])
const handlerProps = useMemo(() => ({
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 { EditableTable, makeActionHandler, makeTagColumn, makeTextColumn } from '@components/Table'
import { AdminPermissionService, AdminUserRoleService } from '@api'
import { arrayOrDefault } from '@utils'
import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { min1 } from '@utils/validationRules'
import { hasPermission } from '@utils/permissions'
export const RoleController = memo(() => {
const RoleController = memo(() => {
const [permissions, setPermissions] = useState([])
const [roles, setRoles] = useState([])
const [showLoader, setShowLoader] = useState(false)
@ -38,7 +37,8 @@ export const RoleController = memo(() => {
setRoles(arrayOrDefault(roles))
}, [])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const permissions = await AdminPermissionService.getAll()
setPermissions(arrayOrDefault(permissions))
@ -47,7 +47,8 @@ export const RoleController = memo(() => {
setShowLoader,
`Не удалось загрузить список ролей`,
'Получение списка ролей'
), [loadRoles])
)
}, [loadRoles])
const handlerProps = useMemo(() => ({
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 { invokeWebApiWrapperAsync } from '@components/factory'
import TelemetrySelect from '@components/selectors/TelemetrySelect'
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import { AdminTelemetryService } from '@api'
import { arrayOrDefault } from '@utils'
const { Item } = Descriptions
@ -32,7 +32,7 @@ export const TelemetryInfo = memo(({ info, danger, ...other }) => (
</Descriptions>
))
export const TelemetryMerger = memo(() => {
const TelemetryMerger = memo(() => {
const [primary, setPrimary] = useState(null)
const [secondary, setSecondary] = useState(null)
const [telemetry, setTelemetry] = useState(null)
@ -68,7 +68,9 @@ export const TelemetryMerger = memo(() => {
'Объединение телеметрий',
), [updateTelemetry, secondary, primary])
useEffect(updateTelemetry, [updateTelemetry])
useEffect(() => {
updateTelemetry()
}, [updateTelemetry])
useEffect(() => {
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 { useNavigate } from 'react-router-dom'
import { PullRequestOutlined } from '@ant-design/icons'
import { Button, Input } from 'antd'
@ -13,18 +14,17 @@ import {
} from '@components/Table'
import Poprompt from '@components/selectors/Poprompt'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
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 [showLoader, setShowLoader] = useState(false)
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) => (
<Poprompt
@ -81,7 +81,8 @@ export const TelemetryController = memo(() => {
].join(' ').toLowerCase().includes(searchValue.toLowerCase()))
), [telemetryData, searchValue])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const telemetryData = arrayOrDefault(await AdminTelemetryService.getAll())
setTelemetryData(telemetryData.map((telemetry) => ({
@ -94,7 +95,8 @@ export const TelemetryController = memo(() => {
setShowLoader,
`Не удалось загрузить список телеметрии скважин`,
'Полученик списка телеметрии скважин'
), [])
)
}, [])
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 { lazy, memo, Suspense, useContext, useMemo } from 'react'
import { useParams } from 'react-router-dom'
import { memo, useMemo } from 'react'
import { Navigate, Route, Routes } from 'react-router-dom'
import { RootPathContext } from '@asb/context'
import { PrivateMenu, PrivateSwitch } from '@components/Private'
import { RootPathContext, useRootPath } from '@asb/context'
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 TelemetryMerger = lazy(() => import('./TelemetryMerger'))
export const Telemetry = memo(() => {
const { tab } = useParams()
const root = useContext(RootPathContext)
const Telemetry = memo(() => {
const root = useRootPath()
const rootPath = useMemo(() => `${root}/telemetry`, [root])
return (
<RootPathContext.Provider value={rootPath}>
<Layout>
<PrivateMenu mode={'horizontal'} selectable={true} selectedKeys={[tab]}>
<PrivateMenu.Link key={'viewer'} title={'Просмотр'} />
<PrivateMenu.Link key={'merger'} title={'Объединение'} />
<PrivateMenu>
<PrivateMenu.Link content={TelemetryViewer} />
<PrivateMenu.Link content={TelemetryMerger} />
</PrivateMenu>
<Layout>
<Layout.Content className={'site-layout-background'}>
<Suspense fallback={<SuspenseFallback />}>
<PrivateSwitch elseRedirect={['viewer', 'merger']}>
<TelemetryViewer key={'viewer'} />
<TelemetryMerger key={'merger'} />
</PrivateSwitch>
</Suspense>
<Routes>
<Route index element={<Navigate to={TelemetryViewer.route} replace />} />
<Route path={'*'} element={<NoAccessComponent />} />
<Route path={TelemetryViewer.route} element={<TelemetryViewer />} />
<Route path={TelemetryMerger.route} element={<TelemetryMerger />} />
</Routes>
</Layout.Content>
</Layout>
</Layout>
@ -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 { AdminCompanyService, AdminUserRoleService, AdminUserService } from '@api'
import { createLoginRules, nameRules, phoneRules, emailRules } from '@utils/validationRules'
import { makeTextOnFilter, makeTextFilters, makeArrayOnFilter } from '@utils/table'
import { hasPermission } from '@utils/permissions'
import { arrayOrDefault } from '@utils'
import { makeTextOnFilter, makeTextFilters, makeArrayOnFilter } from '@utils/filters'
import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import RoleTag from './RoleTag'
const SEARCH_TIMEOUT = 400
export const UserController = memo(() => {
const UserController = memo(() => {
const [users, setUsers] = useState([])
const [filteredUsers, setFilteredUsers] = useState([])
const [searchValue, setSearchValue] = useState('')
@ -35,7 +34,8 @@ export const UserController = memo(() => {
const [selectedUser, setSelectedUser] = useState(null)
const [subject, setSubject] = useState(null)
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const filteredUsers = users.filter((user) => user && (!searchValue || [
user.login ?? '',
@ -51,7 +51,8 @@ export const UserController = memo(() => {
},
setIsSearching,
`Не удалось произвести поиск пользователей`
), [users, searchValue])
)
}, [users, searchValue])
useEffect(() => {
if (!subject) {
@ -91,7 +92,8 @@ export const UserController = memo(() => {
'Получение списка пользователей'
), [])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const roles = arrayOrDefault(await AdminUserRoleService.getAll())
const companies = arrayOrDefault(await AdminCompanyService.getAll()).map((company) => ({
@ -170,7 +172,8 @@ export const UserController = memo(() => {
setShowLoader,
`Не удалось загрузить список компаний`,
'Получение списка компаний'
), [])
)
}, [])
const handlerProps = useMemo(() => ({
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 { defaultPagination, makeColumn, makeDateSorter, makeStringSorter, Table } from '@components/Table'
import { arrayOrDefault, formatDate, wrapPrivateComponent } from '@utils'
import { RequestTrackerService } from '@api'
import { arrayOrDefault, formatDate } from '@utils'
const logRecordCount = 1000
@ -17,7 +17,7 @@ const columns = [
}),
]
export const VisitLog = memo(() => {
const VisitLog = memo(() => {
const [logData, setLogData] = useState([])
const [showLoader, setShowLoader] = useState(false)
const [searchValue, setSearchValue] = useState('')
@ -29,7 +29,8 @@ export const VisitLog = memo(() => {
].join(' ').toLowerCase().includes(searchValue.toLowerCase()))
), [logData, searchValue])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const logData = arrayOrDefault(await RequestTrackerService.getUsersStat(logRecordCount))
logData.forEach((log) => log.key = `${log.login}${log.ip}`)
@ -38,7 +39,8 @@ export const VisitLog = memo(() => {
setShowLoader,
`Не удалось загрузить список последних посещений пользователей`,
'Получение списка последних посещений'
), [])
)
}, [])
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 { TelemetryView, CompanyView } from '@components/views'
import TelemetrySelect from '@components/selectors/TelemetrySelect'
import { hasPermission } from '@utils/permissions'
import { arrayOrDefault } from '@utils'
import { arrayOrDefault, hasPermission, wrapPrivateComponent } from '@utils'
import { coordsFixed } from '../DepositController'
@ -37,7 +36,7 @@ const recordParser = (record) => ({
idTelemetry: record.telemetry?.id,
})
export const WellController = memo(() => {
const WellController = memo(() => {
const [columns, setColumns] = useState([])
const [wells, setWells] = useState([])
const [showLoader, setShowLoader] = useState(false)
@ -74,7 +73,8 @@ export const WellController = memo(() => {
/>
))
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const companies = arrayOrDefault(await AdminCompanyService.getAll())
const telemetry = arrayOrDefault(await AdminTelemetryService.getAll())
@ -118,7 +118,8 @@ export const WellController = memo(() => {
setShowLoader,
`Не удалось загрузить список кустов`,
'Получение списка кустов'
), [updateTable])
)
}, [updateTable])
const handlerProps = useMemo(() => ({
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 { lazy, memo, Suspense, useContext, useMemo } from 'react'
import { useParams } from 'react-router-dom'
import { RootPathContext } from '@asb/context'
import { PrivateMenu, PrivateSwitch } from '@components/Private'
import { RootPathContext, useRootPath } from '@asb/context'
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 CompanyController = lazy(() => import( './CompanyController'))
const DepositController = lazy(() => import( './DepositController'))
const UserController = lazy(() => import( './UserController'))
const WellController = lazy(() => import( './WellController'))
const RoleController = lazy(() => import( './RoleController'))
const CompanyTypeController = lazy(() => import('./CompanyTypeController'))
const PermissionController = lazy(() => import( './PermissionController'))
const TelemetrySection = lazy(() => import( './Telemetry'))
const VisitLog = lazy(() => import( './VisitLog'))
export const AdminPanel = memo(() => {
const { tab } = useParams()
const root = useContext(RootPathContext)
const AdminPanel = memo(() => {
const root = useRootPath()
const rootPath = useMemo(() => `${root}/admin`, [root])
return (
<RootPathContext.Provider value={rootPath}>
<Layout>
<PrivateMenu mode={'horizontal'} selectable={true} selectedKeys={[tab]}>
<PrivateMenu.Link key={'deposit' } title={'Месторождения' } />
<PrivateMenu.Link key={'cluster' } title={'Кусты' } />
<PrivateMenu.Link key={'well' } title={'Скважины' } />
<PrivateMenu.Link key={'user' } title={'Пользователи' } />
<PrivateMenu.Link key={'company' } title={'Компании' } />
<PrivateMenu.Link key={'company_type'} title={'Типы компаний' } />
<PrivateMenu.Link key={'role' } title={'Роли' } />
<PrivateMenu.Link key={'permission' } title={'Разрешения' } />
<PrivateMenu.Link key={'telemetry' } title={'Телеметрия' } />
<PrivateMenu.Link key={'visit_log' } title={'Журнал посещений'} />
<AdminLayoutPortal title={'Администраторская панель'}>
<PrivateMenu>
<PrivateMenu.Link content={DepositController} />
<PrivateMenu.Link content={ClusterController} />
<PrivateMenu.Link content={WellController} />
<PrivateMenu.Link content={UserController} />
<PrivateMenu.Link content={CompanyController} />
<PrivateMenu.Link content={CompanyTypeController} />
<PrivateMenu.Link content={RoleController} />
<PrivateMenu.Link content={PermissionController} />
<PrivateMenu.Link content={Telemetry} />
<PrivateMenu.Link content={VisitLog} />
</PrivateMenu>
<Layout>
<Layout.Content className={'site-layout-background'}>
<Suspense fallback={<SuspenseFallback />}>
<PrivateSwitch elseRedirect={['deposit', 'cluster', 'well', 'user', 'company', 'company_type', 'role', 'permission', 'telemetry', 'visit_log']}>
<DepositController key={'deposit'} />
<ClusterController key={'cluster'} />
<WellController key={'well'} />
<UserController key={'user'} />
<CompanyController key={'company'} />
<CompanyTypeController key={'company_type'} />
<RoleController key={'role'} />
<PermissionController key={'permission'} />
<TelemetrySection key={'telemetry/:tab?'} />
<VisitLog key={'visit_log'} />
</PrivateSwitch>
</Suspense>
<Routes>
<Route index element={<Navigate to={VisitLog.route} replace />} />
<Route path={'*'} element={<NoAccessComponent />} />
<Route path={DepositController.route} element={<DepositController />} />
<Route path={ClusterController.route} element={<ClusterController />} />
<Route path={WellController.route} element={<WellController />} />
<Route path={UserController.route} element={<UserController />} />
<Route path={CompanyController.route} element={<CompanyController />} />
<Route path={CompanyTypeController.route} element={<CompanyTypeController />} />
<Route path={RoleController.route} element={<RoleController />} />
<Route path={PermissionController.route} element={<PermissionController />} />
<Route path={Telemetry.route} element={<Telemetry />} />
<Route path={VisitLog.route} element={<VisitLog />} />
</Routes>
</Layout.Content>
</Layout>
</Layout>
</AdminLayoutPortal>
</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 { 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 { invokeWebApiWrapperAsync } from '@components/factory'
import { WellSelector } from '@components/selectors/WellSelector'
import { makeGroupColumn, makeNumericColumn, makeNumericRender, makeTextColumn, Table } from '@components/Table'
import { OperationStatService, WellOperationService } from '@api'
import { arrayOrDefault } from '@utils'
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import '@styles/index.css'
import '@styles/statistics.less'
@ -64,7 +63,7 @@ const getWellData = async (wellsList) => {
return wellData
}
export const Statistics = memo(() => {
const Statistics = memo(() => {
const [sectionTypes, setSectionTypes] = useState([])
const [avgColumns, setAvgColumns] = useState(defaultColumns)
const [cmpColumns, setCmpColumns] = useState(defaultColumns)
@ -77,7 +76,7 @@ export const Statistics = memo(() => {
const [cmpData, setCmpData] = useState([])
const [avgRow, setAvgRow] = useState({})
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
const cmpSpeedRender = useCallback((key) => (section) => {
let spanClass = ''
@ -97,7 +96,8 @@ export const Statistics = memo(() => {
)
}, [avgRow])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const types = await WellOperationService.getSectionTypes(idWell)
setSectionTypes(Object.entries(types))
@ -105,9 +105,11 @@ export const Statistics = memo(() => {
setIsPageLoading,
`Не удалось получить типы секции`,
`Получение списка возможных секций`,
), [idWell])
)
}, [idWell])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
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,
'Не удалось установить необходимые столбцы'
), [sectionTypes, avgData, cmpSpeedRender])
)
}, [sectionTypes, avgData, cmpSpeedRender])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const avgData = await getWellData(avgWells)
setAvgData(avgData)
@ -148,16 +152,19 @@ export const Statistics = memo(() => {
},
setIsAvgTableLoading,
'Не удалось загрузить данные для расчёта средних значений',
), [avgWells])
)
}, [avgWells])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const cmpData = await getWellData(cmpWells)
setCmpData(cmpData)
},
setIsCmpTableLoading,
'Не удалось получить скважины для сравнения',
), [cmpWells])
)
}, [cmpWells])
const getStatisticsAvgSummary = useCallback((data) => (
<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 { Button, Modal, Popconfirm } from 'antd'
import { memo, useCallback, useEffect, useState } from 'react'
import { Button, Modal, Popconfirm, Tooltip } from 'antd'
import { IdWellContext } from '@asb/context'
import { useIdWell } from '@asb/context'
import { Table } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
@ -15,13 +15,11 @@ export const NewParamsTable = memo(({ selectedWellsKeys }) => {
const [showParamsLoader, setShowParamsLoader] = useState(false)
const [isParamsModalVisible, setIsParamsModalVisible] = useState(false)
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync(
async () => setParamsColumns(await getColumns(idWell))
), [idWell])
useEffect(() => console.log(paramsColumns), [paramsColumns])
useEffect(() => {
invokeWebApiWrapperAsync(async () => setParamsColumns(await getColumns(idWell)))
}, [idWell])
const onParamButtonClick = useCallback(() => invokeWebApiWrapperAsync(
async () => {
@ -61,7 +59,10 @@ export const NewParamsTable = memo(({ selectedWellsKeys }) => {
width={1700}
footer={(
<Popconfirm title={'Заменить существующие режимы выбранными?'} onConfirm={onParamsAddClick}>
<Button size={'large'} disabled={params.length <= 0}>Сохранить</Button>
<Button
size={'large'}
disabled={params.length <= 0}
>Сохранить</Button>
</Popconfirm>
)}
>

View File

@ -1,22 +1,23 @@
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 { 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 LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { makeTextColumn, makeNumericColumnPlanFact, makeNumericColumn } from '@components/Table'
import { WellCompositeService } from '@api'
import { hasPermission } from '@utils/permissions'
import {
hasPermission,
wrapPrivateComponent,
calcAndUpdateStatsBySections,
makeFilterMinMaxFunction,
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 NewParamsTable from './NewParamsTable'
@ -30,7 +31,7 @@ const sortBySectionId = (a, b) => a?.sectionId - b?.sectionId
const filtersSectionsType = []
const DAY_IN_MS = 1000 * 60 * 60 * 24
export const WellCompositeSections = memo(({ statsWells, selectedSections }) => {
const WellCompositeSections = memo(({ statsWells, selectedSections }) => {
const [selectedWells, setSelectedWells] = useState([])
const [wellOperations, setWellOperations] = useState([])
const [selectedWellsKeys, setSelectedWellsKeys] = useState([])
@ -39,7 +40,7 @@ export const WellCompositeSections = memo(({ statsWells, selectedSections }) =>
const [isTVDModalVisible, setIsTVDModalVisible] = useState(false)
const [isOpsModalVisible, setIsOpsModalVisible] = useState(false)
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
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 { useParams } from 'react-router-dom'
import { useState, useEffect, memo, useMemo } from 'react'
import { Navigate, Route, Routes } from 'react-router-dom'
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 WellSelector from '@components/selectors/WellSelector'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { PrivateMenu, PrivateSwitch } from '@components/Private'
import { arrayOrDefault } from '@utils'
import { arrayOrDefault, NoAccessComponent, wrapPrivateComponent } from '@utils'
import { OperationStatService, WellCompositeService } from '@api'
import ClusterWells from '@pages/Cluster/ClusterWells'
import { WellCompositeSections } from './WellCompositeSections'
import WellCompositeSections from './WellCompositeSections'
const { Content } = Layout
export const WellCompositeEditor = memo(({ rootPath }) => {
const { tab } = useParams()
const idWell = useContext(IdWellContext)
const properties = {
requirements: ['OperationStat.get', 'WellComposite.get'],
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 [showLoader, setShowLoader] = useState(false)
@ -25,19 +33,21 @@ export const WellCompositeEditor = memo(({ rootPath }) => {
const [selectedIdWells, setSelectedIdWells] = useState([])
const [selectedSections, setSelectedSections] = useState([])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
try {
const selected = await WellCompositeService.get(idWell)
setSelectedSections(arrayOrDefault(selected))
setSelectedSections(arrayOrDefault(await WellCompositeService.get(idWell)))
} catch(e) {
setSelectedSections([])
throw e
}
},
setShowLoader,
'Не удалось загрузить список скважин',
'Получение списка скважин'
), [idWell])
)
}, [idWell])
useEffect(() => {
const wellIds = selectedSections.map((value) => value.idWellSrc)
@ -45,7 +55,8 @@ export const WellCompositeEditor = memo(({ rootPath }) => {
setSelectedIdWells(wellIds)
}, [selectedSections])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const stats = arrayOrDefault(await OperationStatService.getWellsStat(selectedIdWells))
setStatsWells(stats)
@ -53,7 +64,8 @@ export const WellCompositeEditor = memo(({ rootPath }) => {
setShowTabLoader,
'Не удалось загрузить статистику по скважинам/секциям',
'Получение статистики по скважинам/секциям'
), [selectedIdWells])
)
}, [selectedIdWells])
return (
<LoaderPortal show={showLoader}>
@ -66,19 +78,22 @@ export const WellCompositeEditor = memo(({ rootPath }) => {
/>
</Col>
<Col span={6}>
<PrivateMenu root={rootPath} mode={'horizontal'} selectable={true} className={'well_menu'} selectedKeys={[tab]}>
<PrivateMenu.Link key={'wells'} title={'Статистика по скважинам'} />
<PrivateMenu.Link key={'sections'} title={'Статистика по секциям'} />
<PrivateMenu root={rootPath} className={'well_menu'}>
<PrivateMenu.Link content={ClusterWells} />
<PrivateMenu.Link content={WellCompositeSections} />
</PrivateMenu>
</Col>
</Row>
<Layout>
<Content className={'site-layout-background'}>
<LoaderPortal show={showTabLoader}>
<PrivateSwitch root={rootPath} elseRedirect={['wells', 'sections']}>
<ClusterWells key={'wells'} statsWells={statsWells} />
<WellCompositeSections key={'sections'} statsWells={statsWells} selectedSections={selectedSections} />
</PrivateSwitch>
<Routes>
<Route index element={<Navigate to={ClusterWells.route} replace/>} />
<Route path={'*'} element={<NoAccessComponent />} />
<Route path={ClusterWells.route} element={<ClusterWells statsWells={statsWells} />} />
<Route path={WellCompositeSections.route} element={<WellCompositeSections statsWells={statsWells} selectedSections={selectedSections} />} />
</Routes>
</LoaderPortal>
</Content>
</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 { useParams } from 'react-router-dom'
import { memo, useMemo } from 'react'
import { Navigate, Route, Routes } from 'react-router-dom'
import { Layout } from 'antd'
import { RootPathContext } from '@asb/context'
import { PrivateMenu, PrivateSwitch } from '@components/Private'
import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import Statistics from './Statistics'
import WellCompositeEditor from './WellCompositeEditor'
export const Analytics = memo(() => {
const { tab } = useParams()
const root = useContext(RootPathContext)
const Analytics = memo(() => {
const root = useRootPath()
const rootPath = useMemo(() => `${root}/analytics`, [root])
return (
<RootPathContext.Provider value={rootPath}>
<Layout>
<PrivateMenu mode={'horizontal'} selectable={true} className={'well_menu'} selectedKeys={[tab]}>
<PrivateMenu.Link key={'composite'} title={'Композитная скважина'} />
<PrivateMenu.Link key={'statistics'} title={'Оценка по ЦБ'} />
<PrivateMenu className={'well_menu'}>
<PrivateMenu.Link content={WellCompositeEditor} />
<PrivateMenu.Link key={'statistics'} title={'Оценка по ЦБ'} content={Statistics} />
</PrivateMenu>
<Layout>
<Layout.Content>
<PrivateSwitch elseRedirect={'composite'}>
<WellCompositeEditor key={'composite/:tab?'} rootPath={`${rootPath}/composite`} />
<Statistics key={'statistics'} />
</PrivateSwitch>
<Layout.Content className={'site-layout-background'}>
<Routes>
<Route index element={<Navigate to={WellCompositeEditor.getKey()} replace />} />
<Route path={'*'} element={<NoAccessComponent />} />
<Route path={WellCompositeEditor.route} element={<WellCompositeEditor />} />
<Route path={Statistics.route} element={<Statistics />} />
</Routes>
</Layout.Content>
</Layout>
</Layout>
@ -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 {
getOperations,
calcAndUpdateStatsBySections,
makeFilterMinMaxFunction
} from '@utils/functions'
import { isRawDate } from '@utils'
isRawDate,
makeFilterMinMaxFunction,
wrapPrivateComponent
} from '@utils'
import { Tvd } from '@pages/WellOperations/Tvd'
import Tvd from '@pages/WellOperations/Tvd'
import WellOperationsTable from './WellOperationsTable'
const filtersMinMax = [
@ -39,7 +40,7 @@ const ONLINE_DEADTIME = 600_000
const getDate = (str) => isRawDate(str) ? new Date(str).toLocaleString() : '-'
const numericRender = makeNumericRender(1)
export const ClusterWells = memo(({ statsWells }) => {
const ClusterWells = memo(({ statsWells }) => {
const [selectedWellId, setSelectedWellId] = useState(0)
const [isTVDModalVisible, setIsTVDModalVisible] = 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 { useParams } from 'react-router-dom'
import { arrayOrDefault } from '@utils'
import { OperationStatService } from '@api'
import { LayoutPortal } from '@components/Layout'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import { OperationStatService } from '@api'
import ClusterWells from './ClusterWells'
import { useParams } from 'react-router-dom'
export const Cluster = memo(() => {
const Cluster = memo(() => {
const { idCluster } = useParams()
const [data, setData] = useState([])
const [showLoader, setShowLoader] = useState(false)
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const clusterData = await OperationStatService.getStatCluster(idCluster)
setData(arrayOrDefault(clusterData?.statsWells))
@ -21,13 +23,21 @@ export const Cluster = memo(() => {
setShowLoader,
`Не удалось загрузить данные по кусту "${idCluster}"`,
'Получение данных по кусту'
), [idCluster])
)
}, [idCluster])
return (
<LayoutPortal title={'Анализ скважин куста'}>
<LoaderPortal show={showLoader}>
<ClusterWells statsWells={data} />
</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 { Link, useLocation } from 'react-router-dom'
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 { LayoutPortal } from '@components/Layout'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, limitValue, wrapPrivateComponent } from '@utils'
import { ClusterService } from '@api'
import '@styles/index.css'
const defaultViewParams = { center: [60.81226, 70.0562], zoom: 5 }
const zoomLimit = limitValue(5, 15)
const calcViewParams = (clusters) => {
if ((clusters?.length ?? 0) <= 0)
return defaultViewParams
@ -33,19 +36,20 @@ const calcViewParams = (clusters) => {
// zoom min = 1 (mega far)
// 4 - full Russia (161.6 deg)
// 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 }
}
export const Deposit = memo(() => {
const Deposit = memo(() => {
const [clustersData, setClustersData] = useState([])
const [showLoader, setShowLoader] = useState(false)
const [viewParams, setViewParams] = useState(defaultViewParams)
const location = useLocation()
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const data = await ClusterService.getClusters()
setClustersData(arrayOrDefault(data))
@ -54,9 +58,11 @@ export const Deposit = memo(() => {
setShowLoader,
`Не удалось загрузить список кустов`,
'Получить список кустов'
), [])
)
}, [])
return (
<LayoutPortal noSheet title={'Месторождение'}>
<LoaderPortal show={showLoader}>
<div className={'h-100vh'}>
<Map {...viewParams}>
@ -75,7 +81,13 @@ export const Deposit = memo(() => {
</Map>
</div>
</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 { IdWellContext } from '@asb/context'
import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal'
import { UploadForm } from '@components/UploadForm'
import { CompanyView, UserView } from '@components/views'
import { invokeWebApiWrapperAsync, downloadFile, formatBytes } from '@components/factory'
import { EditableTable, makeColumn, makeDateColumn, makeNumericColumn, makePaginationObject } from '@components/Table'
import { hasPermission } from '@utils/permissions'
import { hasPermission } from '@utils'
import { FileService } from '@api'
const pageSize = 12
@ -40,7 +40,7 @@ export const DocumentsTemplate = ({ idCategory, idWell: wellId, mimeTypes, heade
const [files, setFiles] = useState([])
const [showLoader, setShowLoader] = useState(false)
const idwellContext = useContext(IdWellContext)
const idwellContext = useIdWell()
const idWell = useMemo(() => wellId ?? idwellContext, [wellId, idwellContext])
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])
useEffect(update, [update])
useEffect(() => onChange?.(files), [files, onChange])
useEffect(() => {
update()
}, [update])
useEffect(() => {
onChange?.(files)
}, [files, onChange])
const handleFileDelete = useMemo(() => hasPermission(`File.edit${idCategory}`) && (async (file) => {
await FileService.delete(idWell, file.id)

View File

@ -1,10 +1,11 @@
import { useParams } from 'react-router-dom'
import { memo, useContext, useMemo } from 'react'
import { Navigate, Route, Routes } from 'react-router-dom'
import { memo, useMemo } from 'react'
import { FolderOutlined } from '@ant-design/icons'
import { Layout } from 'antd'
import { RootPathContext } from '@asb/context'
import { PrivateMenu, PrivateSwitch } from '@components/Private'
import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu } from '@components/Private'
import { getTabname, wrapPrivateComponent, NoAccessComponent } from '@utils'
import DocumentsTemplate from './DocumentsTemplate'
@ -23,9 +24,9 @@ export const documentCategories = [
{ id: 9, key: 'closingService', title: 'Сервис по заканчиванию скважины' },
]
export const MenuDocuments = memo(() => {
const { category } = useParams()
const root = useContext(RootPathContext)
const MenuDocuments = memo(() => {
const category = getTabname()
const root = useRootPath()
const rootPath = useMemo(() => `${root}/document`, [root])
return (
@ -42,19 +43,30 @@ export const MenuDocuments = memo(() => {
</PrivateMenu>
<Layout>
<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 => (
<Route key={category.key} path={category.key} element={(
<DocumentsTemplate
key={category.key}
idCategory={category.id}
tableName={`documents_${category.key}`}
/>
)} />
))}
</PrivateSwitch>
</Routes>
</Content>
</Layout>
</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 { 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 { invokeWebApiWrapperAsync } from '@components/factory'
import { DrillingProgramService } from '@api'
@ -21,9 +21,10 @@ export const CategoryAdder = memo(({ categories, onUpdate, className, ...other }
const [showLoader, setShowLoader] = useState(false)
const [showCatLoader, setShowCatLoader] = useState(false)
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
setOptions(categories.map((category) => ({
label: category.name ?? category.shortName,
@ -32,7 +33,8 @@ export const CategoryAdder = memo(({ categories, onUpdate, className, ...other }
},
setShowCatLoader,
`Не удалось установить список доступных категорий для добавления`
), [categories])
)
}, [categories])
const onFinish = useCallback(({ categories }) => invokeWebApiWrapperAsync(
async () => {

View File

@ -1,8 +1,8 @@
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 { IdWellContext } from '@asb/context'
import { useIdWell } from '@asb/context'
import { UserView } from '@components/views'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
@ -30,11 +30,15 @@ export const CategoryEditor = memo(({ visible, category, onClosed }) => {
const [subject, setSubject] = useState(null)
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 () => {
const filteredUsers = users.filter(({ user }) => user && [
user.login ?? '',
@ -50,7 +54,8 @@ export const CategoryEditor = memo(({ visible, category, onClosed }) => {
},
setIsSearching,
`Не удалось произвести поиск пользователей`
), [users, searchValue])
)
}, [users, searchValue])
useEffect(() => {
if (!subject) {
@ -71,14 +76,16 @@ export const CategoryEditor = memo(({ visible, category, onClosed }) => {
}
}, [subject])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const allUsers = arrayOrDefault(await DrillingProgramService.getAvailableUsers(idWell))
setAllUsers(allUsers)
},
setShowLoader,
`Не удалось загрузить список доступных пользователей скважины "${idWell}"`
), [idWell])
)
}, [idWell])
const calcUsers = useCallback(() => {
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 { IdWellContext } from '@asb/context'
import { useIdWell } from '@asb/context'
import { CompanyView } from '@components/views'
import DownloadLink from '@components/DownloadLink'
import LoaderPortal from '@components/LoaderPortal'
@ -65,9 +65,10 @@ export const CategoryHistory = ({ idCategory, visible, onClose }) => {
const [isLoading, setIsLoading] = useState(false)
const [companyName, setCompanyName] = useState('')
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
if (!visible) return
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,
`Не удалось загрузить историю категорий "${idCategory}" скважины "${idWell}"`
), [idWell, idCategory, visible, range, companyName, fileName, page, pageSize])
)
}, [idWell, idCategory, visible, range, companyName, fileName, page, pageSize])
const onPaginationChange = useCallback((page, pageSize) => {
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 {
DeleteOutlined,
@ -6,7 +6,7 @@ import {
TableOutlined,
} from '@ant-design/icons'
import { IdWellContext } from '@asb/context'
import { useIdWell } from '@asb/context'
import { UserView } from '@components/views'
import UploadForm from '@components/UploadForm'
import DownloadLink from '@components/DownloadLink'
@ -45,7 +45,7 @@ export const CategoryRender = memo(({ partData, onUpdate, onEdit, onHistory, set
file // Информация о файле
} = partData ?? {}
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
const uploadUrl = useMemo(() => `/api/well/${idWell}/drillingProgram/part/${idFileCategory}`, [idWell, idFileCategory])
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 LoaderPortal from '@components/LoaderPortal'
import { downloadFile, formatBytes, invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, formatDate } from '@utils'
import { arrayOrDefault, formatDate, wrapPrivateComponent } from '@utils'
import { DrillingProgramService } from '@api'
import CategoryAdder from './CategoryAdder'
@ -41,7 +41,7 @@ const STATE_STRING = {
[ID_STATE.Unknown]: { icon: WarningOutlined, text: 'Неизвестно' },
}
export const DrillingProgram = memo(() => {
const DrillingProgram = memo(() => {
const [selectedCategory, setSelectedCategory] = useState()
const [historyVisible, setHistoryVisible] = useState(false)
const [editorVisible, setEditorVisible] = useState(false)
@ -79,7 +79,9 @@ export const DrillingProgram = memo(() => {
`Не удалось загрузить название скважины "${idWell}"`
), [idWell])
useEffect(() => updateData(), [updateData])
useEffect(() => {
updateData()
}, [updateData])
const onCategoryEdit = useCallback((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 { Link, useHistory, useLocation } from 'react-router-dom'
import { Card, Form, Input, Button } from 'antd'
import { Link, useNavigate, useLocation } from 'react-router-dom'
import { UserOutlined, LockOutlined } from '@ant-design/icons'
import { Card, Form, Input, Button } from 'antd'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { loginRules, passwordRules } from '@utils/validationRules'
import { setUser } from '@utils/storage'
import { setUser, wrapPrivateComponent } from '@utils'
import { AuthService } from '@api'
import '@styles/index.css'
import Logo from '@images/Logo'
export const Login = memo(() => {
const Login = memo(() => {
const [showLoader, setShowLoader] = useState(false)
const history = useHistory()
const navigate = useNavigate()
const location = useLocation()
const handleLogin = useCallback((formData) => invokeWebApiWrapperAsync(
@ -23,20 +23,19 @@ export const Login = memo(() => {
const user = await AuthService.login(formData)
if (!user) throw Error('Неправильный логин или пароль')
setUser(user)
console.log(location.state?.from)
history.push(location.state?.from ?? 'well')
navigate(location.state?.from ?? '/deposit')
},
setShowLoader,
(ex) => ex?.message ?? 'Ошибка входа',
'Вход в систему'
), [history, location])
), [navigate, location])
return (
<div className={'login_page shadow'}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Logo style={{ marginBottom: '10px' }} />
<LoaderPortal show={showLoader}>
<Card title={'Система мониторинга'} className={'shadow'} bordered={true} style={{ width: 350 }}>
<Card bordered title={'Система мониторинга'} className={'shadow'} style={{ width: 350 }}>
<Form onFinish={handleLogin}>
<Form.Item name={'login'} rules={loginRules}>
<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 {
CheckSquareOutlined,
@ -9,11 +9,10 @@ import {
DeleteOutlined
} from '@ant-design/icons'
import { IdWellContext } from '@asb/context'
import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { hasPermission } from '@utils/permissions'
import { formatDate } from '@utils'
import { hasPermission, formatDate } from '@utils'
import { MeasureService } from '@api'
import { View } from './View'
@ -33,7 +32,7 @@ export const MeasureTable = memo(({ group, updateMeasuresFunc, additionalButtons
const [isTableEditing, setIsTableEditing] = useState(false)
const [editingActionName, setEditingActionName] = useState('')
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
const [measuresForm] = Form.useForm()

View File

@ -1,10 +1,11 @@
import { Button } from 'antd'
import { useState, useEffect, memo, useContext } from 'react'
import { useState, useEffect, memo } from 'react'
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 { invokeWebApiWrapperAsync } from '@components/factory'
import { wrapPrivateComponent } from '@utils'
import { MeasureService } from '@api'
import { MeasureTable } from './MeasureTable'
@ -42,15 +43,16 @@ const defaultData = [
}
]
export const Measure = memo(() => {
const Measure = memo(() => {
const [showLoader, setShowLoader] = useState(false)
const [isMeasuresUpdating, setIsMeasuresUpdating] = useState(true)
const [data, setData] = useState(defaultData)
const [tableIdx, setTableIdx] = useState(-1)
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
if (!isMeasuresUpdating) return
const measures = await MeasureService.getHisory(idWell)
@ -69,7 +71,8 @@ export const Measure = memo(() => {
setShowLoader,
`Не удалось загрузить последние данные по скважине ${idWell}`,
'Получение последних данных телеметрий'
), [idWell, isMeasuresUpdating])
)
}, [idWell, isMeasuresUpdating])
return (
<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 { Link, useHistory } from 'react-router-dom'
import { Link, useNavigate } from 'react-router-dom'
import { Card, Form, Input, Button } from 'antd'
import {
UserOutlined,
@ -20,6 +20,7 @@ import {
passwordRules,
phoneRules
} from '@utils/validationRules'
import { wrapPrivateComponent } from '@utils'
import Logo from '@images/Logo'
@ -52,17 +53,17 @@ const createInput = (name, placeholder, rules, isPassword, dependencies) => (
export const Register = memo(() => {
const [showLoader, setShowLoader] = useState(false)
const history = useHistory()
const navigate = useNavigate()
const handleRegister = useCallback((formData) => invokeWebApiWrapperAsync(
async () => {
await AuthService.register(formData)
history.push('/login')
navigate('/login')
},
setShowLoader,
`Ошибка отправки заявки на регистрацию`,
'Отправка заявки на регистрацию'
), [history])
), [navigate])
return (
<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 { 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 { invokeWebApiWrapperAsync } from '@components/factory'
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 [isLoading, setIsLoading] = useState(false)
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
const setFields = useCallback((data) => form.setFieldsValue(data ? {
...data,

View File

@ -1,25 +1,25 @@
import moment from 'moment'
import { Button } from 'antd'
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
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 { DateRangeWrapper, Table, makeDateColumn, makeColumn } from '@components/Table'
import { download, invokeWebApiWrapperAsync } from '@components/factory'
import { arrayOrDefault, wrapPrivateComponent } from '@utils'
import { DailyReportService } from '@api'
import { arrayOrDefault } from '@utils'
import ReportEditor from './ReportEditor'
export const DailyReport = memo(() => {
const DailyReport = memo(() => {
const [data, setData] = useState([])
const [isLoading, setIsLoading] = useState(false)
const [searchDate, setSearchDate] = useState([moment().subtract(1, 'week'), moment()])
const [selectedReport, setSelectedReport] = useState(null)
const [isEditorVisible, setIsEditorVisible] = useState(false)
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
const updateTable = useCallback(() => invokeWebApiWrapperAsync(
async () => {
@ -31,7 +31,9 @@ export const DailyReport = memo(() => {
'Получение списка суточных рапортов',
), [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])
@ -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 { useState, useEffect, memo, useContext } from 'react'
import { useState, useEffect, memo } from 'react'
import { FilePdfOutlined, FileTextOutlined } from '@ant-design/icons'
import { IdWellContext } from '@asb/context'
import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal'
import { Table, makeDateSorter, makeNumericSorter } from '@components/Table'
import { invokeWebApiWrapperAsync, downloadFile } from '@components/factory'
@ -60,9 +60,10 @@ export const Reports = memo(() => {
const [reports, setReports] = useState([])
const [showLoader, setShowLoader] = useState(false)
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const reportsResponse = await ReportService.getAllReportsNamesByWell(idWell)
const reports = reportsResponse.map(r => ({ ...r, key: r.id ?? r.name ?? r.date }))
@ -71,7 +72,8 @@ export const Reports = memo(() => {
setShowLoader,
`Не удалось загрузить список рапортов по скважине "${idWell}"`,
'Получение списка рапортов'
), [idWell])
)
}, [idWell])
return (
<LoaderPortal show={showLoader}>

View File

@ -1,12 +1,13 @@
import 'moment/locale/ru'
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 { IdWellContext } from '@asb/context'
import { useIdWell } from '@asb/context'
import { DateRangeWrapper } from 'components/Table'
import { LoaderPortal } from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { wrapPrivateComponent } from '@utils'
import { Subscribe } from '@services/signalr'
import { ReportService } from '@api'
@ -33,7 +34,7 @@ const reportFormats = [
{ value: 1, label: 'LAS' },
]
export const DiagramReport = memo(() => {
const DiagramReport = memo(() => {
const [aviableDateRange, setAviableDateRange] = useState([moment(), moment()])
const [filterDateRange, setFilterDateRange] = useState([
moment().subtract(1, 'days').startOf('day'),
@ -44,7 +45,7 @@ export const DiagramReport = memo(() => {
const [pagesCount, setPagesCount] = useState(0)
const [showLoader, setShowLoader] = useState(false)
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
const handleReportCreation = useCallback(async () => await invokeWebApiWrapperAsync(
async () => {
@ -88,7 +89,8 @@ export const DiagramReport = memo(() => {
!current.isBetween(aviableDateRange[0], aviableDateRange[1], 'seconds', '[]')
, [aviableDateRange])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const datesRangeResponse = await ReportService.getReportsDateRange(idWell)
if (!datesRangeResponse?.from || !datesRangeResponse.to)
@ -110,9 +112,11 @@ export const DiagramReport = memo(() => {
setShowLoader,
`Не удалось получить диапозон дат рапортов для скважины "${idWell}"`,
'Получение диапозона дат рапортов'
), [idWell])
)
}, [idWell])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
if (filterDateRange?.length !== 2) return
const pagesCount = await ReportService.getReportSize(
@ -129,7 +133,8 @@ export const DiagramReport = memo(() => {
${filterDateRange[0].format(dateTimeFormat)} по
${filterDateRange[1].format(dateTimeFormat)}`,
'Получение размера рапортов'
), [filterDateRange, step, format, idWell])
)
}, [filterDateRange, step, format, idWell])
return (
<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 { memo, useContext, useMemo } from 'react'
import { Navigate, Route, Routes } from 'react-router-dom'
import { memo, useMemo } from 'react'
import { FilePdfOutlined } from '@ant-design/icons'
import { Layout } from 'antd'
import { RootPathContext } from '@asb/context'
import { PrivateMenu, PrivateSwitch } from '@components/Private'
import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import DailyReport from './DailyReport'
import DiagramReport from './DiagramReport'
const { Content } = Layout
export const Reports = memo(() => {
const { tab } = useParams()
const root = useContext(RootPathContext)
const Reports = memo(() => {
const root = useRootPath()
const rootPath = useMemo(() => `${root}/reports`, [root])
return (
<RootPathContext.Provider value={rootPath}>
<Layout>
<PrivateMenu mode={'horizontal'} selectable={true} selectedKeys={[tab]} className={'well_menu'}>
<PrivateMenu.Link key={'diagram_report'} icon={<FilePdfOutlined />} title={'Диаграмма'}/>
<PrivateMenu.Link key={'daily_report'} title={'Суточный рапорт'} />
<PrivateMenu className={'well_menu'}>
<PrivateMenu.Link content={DiagramReport} icon={<FilePdfOutlined />} />
<PrivateMenu.Link content={DailyReport} />
</PrivateMenu>
<Layout>
<Content className={'site-layout-background'}>
<PrivateSwitch elseRedirect={['diagram_report', 'daily_report']}>
<DiagramReport key={'diagram_report'} />
<DailyReport key={'daily_report'} />
</PrivateSwitch>
<Routes>
<Route index element={<Navigate to={'diagram_report'} replace />} />
<Route path={'*'} element={<NoAccessComponent />} />
<Route path={DiagramReport.route} element={<DiagramReport />} />
<Route path={DailyReport.route} element={<DailyReport />} />
</Routes>
</Content>
</Layout>
</Layout>
@ -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 */
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 { CopyUrlButton } from '@components/CopyUrl'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { DatePickerWrapper, makeDateSorter } from '@components/Table'
import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
import { range, wrapPrivateComponent } from '@utils'
import { TelemetryDataSaubService } from '@api'
import { range } from '@utils'
import { normalizeData } from '../TelemetryView'
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 [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 [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) => {
if (loaded && dateLimit.from && dateLimit.to) {
@ -102,7 +109,17 @@ export const Archive = memo(() => {
})
}), [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 () => {
let dates = await TelemetryDataSaubService.getDataDatesRange(idWell)
dates = {
@ -110,17 +127,16 @@ export const Archive = memo(() => {
to: new Date(dates?.to ?? 0)
}
setDateLimit(dates)
setStartDate(new Date(Math.max(dates.from, +dates.to - chartInterval)))
},
setShowLoader,
`Не удалось загрузить диапозон телеметрии для скважины "${idWell}"`,
'Загрузка диапозона телеметрии'
), [])
)
}, [])
useEffect(() => {
setStartDate((startDate) => new Date(Math.min(Date.now() - chartInterval, startDate)))
setDataSaub([])
}, [chartInterval])
setStartDate((prev) => new Date(Math.max(dateLimit.from, Math.min(+prev, +dateLimit.to - chartInterval))))
}, [chartInterval, dateLimit])
useEffect(() => {
if (showLoader) return
@ -150,6 +166,11 @@ export const Archive = memo(() => {
)
}, [idWell, chartInterval, loaded, startDate])
const onRangeChange = useCallback((value) => {
setChartInterval(value * 1000)
setDataSaub([])
}, [])
return (
<>
<Flex style={{margin: '8px 8px 0'}}>
@ -164,8 +185,9 @@ export const Archive = memo(() => {
</div>
<div style={{ marginLeft: '1rem' }}>
Период:&nbsp;
<PeriodPicker onChange={(val) => setChartInterval(val * 1000)} />
<PeriodPicker value={chartInterval / 1000} onChange={onRangeChange} />
</div>
<CopyUrlButton style={{ marginLeft: '1rem' }} />
</Flex>
<LoaderPortal show={showLoader}>
<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 { useHistory, useParams } from 'react-router-dom'
import { memo, useCallback, useEffect, useMemo, useReducer, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { CloseOutlined } from '@ant-design/icons'
import { Button, Menu, Popconfirm } from 'antd'
import { IdWellContext, RootPathContext } from '@asb/context'
import { useIdWell, useRootPath } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { BaseWidget, WidgetSettingsWindow } from '@components/widgets'
import { getJSON, setJSON } from '@utils/storage'
import { arrayOrDefault } from '@utils'
import { arrayOrDefault, wrapPrivateComponent, getJSON, setJSON, getTabname } from '@utils'
import Subscribe from '@services/signalr'
import {
WitsInfoService,
@ -88,7 +87,7 @@ const groupsReducer = (groups, action) => {
break
case 'add_widget':
if (groupIdx >= 0)
if (groupIdx >= 0 && widgetIdx < 0)
newGroups[groupIdx].widgets.push(value)
break
case 'edit_widget':
@ -106,21 +105,21 @@ const groupsReducer = (groups, action) => {
return newGroups
}
export const DashboardNNB = memo(() => {
const DashboardNNB = memo(({ enableEditing = false }) => {
const [groups, dispatchGroups] = useReducer(groupsReducer, [])
const [witsInfo, setWitsInfo] = useState([])
const [isLoading, setIsLoading] = useState(false)
const [selectedSettings, setSelectedSettings] = useState(null)
const [values, setValues] = useState({})
const idWell = useContext(IdWellContext)
const root = useContext(RootPathContext)
const idWell = useIdWell()
const root = useRootPath()
const rootPath = useMemo(() => `${root}/dashboard_nnb`, [root])
const history = useHistory()
const { tab: selectedGroup } = useParams()
const navigate = useNavigate()
const selectedGroup = getTabname()
if (!selectedGroup && groups?.length > 0)
history.push(`${rootPath}/${groups[0].id}`)
navigate(`${rootPath}/${groups[0].id}`)
const group = useMemo(() => ({
@ -128,7 +127,8 @@ export const DashboardNNB = memo(() => {
...groups.find(({ id }) => `${id}` === selectedGroup),
}), [groups, selectedGroup])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const info = await getWitsInfo()
setWitsInfo(info)
@ -137,7 +137,8 @@ export const DashboardNNB = memo(() => {
setIsLoading,
'Не удалось загрузить информацию о параметрах ННБ',
'Получение информации о параметрах ННБ'
), [])
)
}, [])
const handleData = useCallback((data, recordId) => {
const mergedData = data.reduce((out, record) => ({ ...out, ...record }), {})
@ -175,8 +176,8 @@ export const DashboardNNB = memo(() => {
const removeGroup = useCallback((id) => {
dispatchGroups({ type: 'remove_group', groupId: `${id}` })
if (id === selectedGroup) history.push(`${rootPath}`)
}, [rootPath, history, selectedGroup])
if (id === selectedGroup) navigate(`${rootPath}`)
}, [rootPath, navigate, selectedGroup])
const addWidget = useCallback((settings) => dispatchGroups({
type: 'add_widget',
@ -209,6 +210,8 @@ export const DashboardNNB = memo(() => {
selectable={true}
selectedKeys={[selectedGroup]}
>
{enableEditing && (
<>
<Menu.Item key={'add_group'}>
<AddGroupWindow addGroup={addGroup} />
</Menu.Item>
@ -217,9 +220,11 @@ export const DashboardNNB = memo(() => {
<AddWidgetWindow witsInfo={witsInfo} onAdded={addWidget} />
</Menu.Item>
)}
</>
)}
{groups.map(({ id, name, editable }) => (
<Menu.Item key={id}>
{editable && (
{enableEditing && editable && (
<Popconfirm
title={'Вы уверены, что хотите удалить группу, это действие невозможно отменить?'}
onConfirm={() => removeGroup(id)}
@ -229,7 +234,7 @@ export const DashboardNNB = memo(() => {
<Button type={'link'} icon={<CloseOutlined />} />
</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>
@ -238,7 +243,7 @@ export const DashboardNNB = memo(() => {
<BaseWidget
key={widget.id}
// onEdit={group.editable && setSelectedSettings} // TODO: Доделать редактирование
onRemove={group.editable && removeWidget}
onRemove={ enableEditing && group.editable && removeWidget}
{...widget}
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 { 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 { invokeWebApiWrapperAsync } from '@components/factory'
import { makeColumn, makeDateColumn, makeNumericSorter } from '@components/Table'
import { wrapPrivateComponent } from '@utils'
import { MessageService } from '@api'
@ -47,7 +50,7 @@ const filterOptions = [
const children = filterOptions.map((line) => <Option key={line.value}>{line.label}</Option>)
// Данные для таблицы
export const Messages = memo(() => {
const Messages = memo(() => {
const [messages, setMessages] = useState([])
const [pagination, setPagination] = useState(null)
const [page, setPage] = useState(1)
@ -56,11 +59,13 @@ export const Messages = memo(() => {
const [searchString, setSearchString] = useState('')
const [showLoader, setShowLoader] = useState(false)
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
const navigate = useNavigate()
const onChangeSearchString = useCallback((message) => setSearchString(message.length > 2 ? message : ''), [])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const [begin, end] = range?.length > 1 ? [range[0].toISOString(), range[1].toISOString()] : [null, null]
const skip = (page - 1) * pageSize
@ -81,7 +86,12 @@ export const Messages = memo(() => {
setShowLoader,
`Не удалось загрузить сообщения по скважине "${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 (
<>
@ -118,10 +128,15 @@ export const Messages = memo(() => {
}}
rowKey={(record) => record.id}
tableName={'messages'}
onRow={onMessageRow}
/>
</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 { EditableTable, makeActionHandler, makeTextColumn } from '@components/Table'
import { getPermissions } from '@utils/permissions'
import { getPermissions } from '@utils'
import { DrillerService } from '@api'
const reqRule = [{ message: 'Обязательное поле!', required: true }]

View File

@ -11,7 +11,7 @@ import {
makeSelectColumn,
} from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { getPermissions } from '@utils/permissions'
import { getPermissions } from '@utils'
import { ScheduleService } from '@api'
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]) // Получаем массив координат линий
useEffect(() => d3.select(axisX.current).call(d3.axisBottom(x)), [axisX, x]) // Рисуем ось X
useEffect(() => d3.select(axisY.current).call(d3.axisLeft(y)), [axisY, y]) // Рисуем ось Y
useEffect(() => {
d3.select(axisX.current).call(d3.axisBottom(x))
}, [axisX, x]) // Рисуем ось X
useEffect(() => {
d3.select(axisY.current).call(d3.axisLeft(y))
}, [axisY, y]) // Рисуем ось Y
return (
<div className={'page-left'} ref={setRef}>

View File

@ -7,8 +7,7 @@ import LoaderPortal from '@components/LoaderPortal'
import { DateRangeWrapper } from '@components/Table'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { DetectedOperationService, DrillerService, TelemetryDataSaubService } from '@api'
import { getPermissions } from '@utils/permissions'
import { arrayOrDefault, range } from '@utils'
import { getPermissions, arrayOrDefault, range, wrapPrivateComponent } from '@utils'
import DrillerList from './DrillerList'
import DrillerSchedule from './DrillerSchedule'
@ -17,7 +16,7 @@ import OperationsTable from './OperationsTable'
import '@styles/detected_operations.less'
export const Operations = memo(() => {
const Operations = memo(() => {
const [isLoading, setIsLoading] = useState(false)
const [dateRange, setDateRange] = useState([])
const [yDomain, setYDomain] = useState(20)
@ -53,7 +52,8 @@ export const Operations = memo(() => {
updateDrillers()
}, [updateDrillers, permissions])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const dates = await TelemetryDataSaubService.getDataDatesRange(idWell)
if (dates) {
@ -65,9 +65,11 @@ export const Operations = memo(() => {
setIsLoading,
'Не удалось загрузить диапазон доступных дат',
'Получение дапазона доступних дат',
), [idWell])
)
}, [idWell])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
if (!dates) return
const data = await DetectedOperationService.get(idWell, undefined, dates[0].toISOString(), dates[1].toISOString())
@ -76,7 +78,8 @@ export const Operations = memo(() => {
setIsLoading,
'Не удалось загрузить список определённых операций',
'Получение списка определённых операций',
), [idWell, dates])
)
}, [idWell, dates])
return (
<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 { 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 { invokeWebApiWrapperAsync } from '@components/factory'
import { Subscribe } from '@services/signalr'
@ -15,7 +15,7 @@ export const ActiveMessagesOnline = memo(() => {
const [messages, setMessages] = useState([])
const [loader, setLoader] = useState(false)
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
const handleReceiveMessages = useCallback((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 { IdWellContext } from '@asb/context'
import { useIdWell } from '@asb/context'
import { Grid, GridItem } from '@components/Grid'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
@ -16,7 +16,7 @@ export const SetpointSender = memo(({ onClose, visible, setpointNames }) => {
const [setpoints, setSetpoints] = useState([])
const [isLoading, setIsLoading] = useState(false)
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
const addingColumns = useMemo(() => [
{

View File

@ -1,14 +1,12 @@
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 { UserView } from '@components/views'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { hasPermission } from '@utils/permissions'
import { makeStringCutter } from '@utils/string'
import { formatDate } from '@utils'
import { hasPermission, makeStringCutter, formatDate } from '@utils'
import { SetpointsService } from '@api'
import SetpointSender from './SetpointSender'
@ -23,9 +21,10 @@ export const Setpoints = memo(({ ...other }) => {
const [selected, setSelected] = useState(null)
const [setpointNames, setSetpointNames] = useState([])
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const names = await SetpointsService.getSetpointsNamesByIdWell(idWell)
if (!names) throw Error('Setpoints not found')
@ -38,7 +37,8 @@ export const Setpoints = memo(({ ...other }) => {
setIsLoading,
`Не удалось загрузить список имён уставок по скважине "${idWell}"`,
'Получение списка имён уставок'
), [idWell])
)
}, [idWell])
const showMore = useCallback((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 { invokeWebApiWrapperAsync } from '@components/factory'
import { TelemetryWirelineRunOutService } from '@api'
@ -10,7 +10,7 @@ export const WirelineRunOut = memo(() => {
const [twro, setTwro] = useState({})
const [isLoading, setIsLoading] = useState(false)
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
const update = useCallback(() => invokeWebApiWrapperAsync(
async () => {
@ -25,7 +25,9 @@ export const WirelineRunOut = memo(() => {
if (visible) update()
}, [update])
useEffect(update, [update])
useEffect(() => {
update()
}, [update])
return (
<WirelineView

View File

@ -1,6 +1,14 @@
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 {
DrillFlowChartService,
OperationStatService,
@ -8,14 +16,6 @@ import {
TelemetryDataSpinService,
WellService
} 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 { CustomColumn } from './CustomColumn'
@ -304,7 +304,7 @@ export const normalizeData = (data) => data?.map(item => ({
blockSpeed: Math.abs(item.blockSpeed)
})) ?? []
export default function TelemetryView() {
const TelemetryView = memo(() => {
const [dataSaub, setDataSaub] = useState([])
const [dataSpin, setDataSpin] = useState([])
const [chartInterval, setChartInterval] = useState(defaultPeriod)
@ -313,7 +313,7 @@ export default function TelemetryView() {
const [flowChartData, setFlowChartData] = useState([])
const [rop, setRop] = useState(null)
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
const handleDataSaub = useCallback((data) => {
if (data) {
@ -347,7 +347,8 @@ export default function TelemetryView() {
return unsubscribe
}, [idWell, chartInterval, handleDataSpin, handleDataSaub])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const well = await WellService.get(idWell)
const rop = await OperationStatService.getClusterRopStatByIdWell(idWell)
@ -357,7 +358,8 @@ export default function TelemetryView() {
setShowLoader,
`Не удалось загрузить данные по скважине "${idWell}"`,
'Получение данных по скважине'
), [idWell])
)
}, [idWell])
const onStatusChanged = useCallback((value) => invokeWebApiWrapperAsync(
async () => {
@ -428,4 +430,16 @@ export default function TelemetryView() {
</Grid>
</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 { memo, useContext, useMemo } from 'react'
import { Navigate, Route, Routes } from 'react-router-dom'
import { memo, useMemo } from 'react'
import { Layout } from 'antd'
import { AlertOutlined, FundViewOutlined, DatabaseOutlined } from '@ant-design/icons'
import { RootPathContext } from '@asb/context'
import { PrivateSwitch, PrivateMenu } from '@components/Private'
import { RootPathContext, useRootPath } from '@asb/context'
import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import Archive from './Archive'
import Messages from './Messages'
import Operations from './Operations'
import DashboardNNB from './DashboardNNB'
import TelemetryView from './TelemetryView'
import '@styles/index.css'
import Operations from './Operations'
const { Content } = Layout
export const Telemetry = memo(() => {
const { tab } = useParams()
const root = useContext(RootPathContext)
const Telemetry = memo(() => {
const root = useRootPath()
const rootPath = useMemo(() => `${root}/telemetry`, [root])
return (
<RootPathContext.Provider value={rootPath}>
<Layout>
<PrivateMenu mode={'horizontal'} selectable={true} selectedKeys={[tab]} className={'well_menu'}>
<PrivateMenu.Link key={'monitoring'} icon={<FundViewOutlined />} title={'Мониторинг'}/>
<PrivateMenu.Link key={'messages'} icon={<AlertOutlined/>} title={'Сообщения'} />
<PrivateMenu.Link key={'archive'} icon={<DatabaseOutlined />} title={'Архив'} />
<PrivateMenu.Link key={'dashboard_nnb'} title={'ННБ'} />
<PrivateMenu.Link key={'operations'} title={'Операции'} />
<PrivateMenu className={'well_menu'}>
<PrivateMenu.Link content={TelemetryView} icon={<FundViewOutlined />} />
<PrivateMenu.Link content={Messages} icon={<AlertOutlined/>} />
<PrivateMenu.Link content={Archive} icon={<DatabaseOutlined />} />
<PrivateMenu.Link content={DashboardNNB} />
<PrivateMenu.Link content={Operations} />
</PrivateMenu>
<Layout>
<Content className={'site-layout-background'}>
<PrivateSwitch elseRedirect={['monitoring', 'messages', 'archive', 'dashboard_nnb']}>
<TelemetryView key={'monitoring'} />
<Messages key={'messages'} />
<Archive key={'archive'} />
<DashboardNNB key={'dashboard_nnb/:tab?'} />
<Operations key={'operations'}/>
</PrivateSwitch>
<Routes>
<Route index element={<Navigate to={TelemetryView.route} replace />} />
<Route path={'*'} element={<NoAccessComponent />} />
<Route path={TelemetryView.route} element={<TelemetryView />} />
<Route path={Messages.route} element={<Messages />} />
<Route path={Archive.route} element={<Archive />} />
<Route path={DashboardNNB.route} element={<DashboardNNB />} />
<Route path={Operations.route} element={<Operations />} />
</Routes>
</Content>
</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 {
FolderOutlined,
FundViewOutlined,
FilePdfOutlined,
ExperimentOutlined,
DeploymentUnitOutlined,
} from '@ant-design/icons'
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 { PrivateMenu, PrivateSwitch } from '@components/Private'
import { IdWellContext, RootPathContext, useRootPath } from '@asb/context'
import { LayoutPortal } from '@components/Layout'
import { PrivateMenu } from '@components/Private'
import { NoAccessComponent, wrapPrivateComponent } from '@utils'
import Measure from './Measure'
import Reports from './Reports'
@ -24,42 +25,50 @@ import '@styles/index.css'
const { Content } = Layout
export const Well = memo(() => {
const { idWell, tab } = useParams()
const root = useContext(RootPathContext)
const Well = memo(() => {
const { idWell } = useParams()
const root = useRootPath()
const rootPath = useMemo(() => `${root}/well/${idWell}`, [root, idWell])
return (
<LayoutPortal>
<RootPathContext.Provider value={rootPath}>
<Layout>
<PrivateMenu mode={'horizontal'} selectable={true} selectedKeys={[tab]} className={'well_menu'}>
<PrivateMenu.Link key={'telemetry'} icon={<FundViewOutlined />} title={'Телеметрия'}/>
<PrivateMenu.Link key={'reports'} icon={<FilePdfOutlined />} title={'Рапорта'} />
<PrivateMenu.Link key={'analytics'} icon={<DeploymentUnitOutlined />} title={'Аналитика'} />
<PrivateMenu.Link key={'operations'} icon={<FolderOutlined />} title={'Операции по скважине'} />
<PrivateMenu.Link key={'document'} icon={<FolderOutlined />} title={'Документы'} />
<PrivateMenu.Link key={'measure'} icon={<ExperimentOutlined />} title={'Измерения'} />
<PrivateMenu.Link key={'drillingProgram'} icon={<FolderOutlined />} title={'Программа бурения'} />
<PrivateMenu className={'well_menu'}>
<PrivateMenu.Link content={Telemetry} />
<PrivateMenu.Link content={Reports} icon={<FilePdfOutlined />} />
<PrivateMenu.Link content={Analytics} icon={<DeploymentUnitOutlined />} />
<PrivateMenu.Link content={WellOperations} icon={<FolderOutlined />} />
<PrivateMenu.Link content={Documents} icon={<FolderOutlined />} />
<PrivateMenu.Link content={Measure} icon={<ExperimentOutlined />} />
<PrivateMenu.Link content={DrillingProgram} icon={<FolderOutlined />} />
</PrivateMenu>
<IdWellContext.Provider value={idWell}>
<Layout>
<Content className={'site-layout-background'}>
<PrivateSwitch elseRedirect={['telemetry', 'reports', 'analytics', 'operations', 'telemetryAnalysis', 'document', 'measure', 'drillingProgram']}>
<Telemetry key={'telemetry/:tab?'} />
<Reports key={'reports/:tab?'} />
<Analytics key={'analytics/:tab?'} />
<WellOperations key={'operations/:tab?'} />
<Documents key={'document/:category?'} />
<Measure key={'measure'} />
<DrillingProgram key={'drillingProgram'} />
</PrivateSwitch>
<Routes>
<Route index element={<Navigate to={Telemetry.getKey()} replace />} />
<Route path={'*'} element={<NoAccessComponent />} />
<Route path={Telemetry.route} element={<Telemetry />} />
<Route path={Reports.route} element={<Reports />} />
<Route path={Analytics.route} element={<Analytics />} />
<Route path={WellOperations.route} element={<WellOperations />} />
<Route path={Documents.route} element={<Documents />} />
<Route path={Measure.route} element={<Measure />} />
<Route path={DrillingProgram.route} element={<DrillingProgram />} />
</Routes>
</Content>
</Layout>
</IdWellContext.Provider>
</Layout>
</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 {
EditableTable,
makeNumericMinMax,
@ -8,8 +8,7 @@ import {
} from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { hasPermission } from '@utils/permissions'
import { arrayOrDefault } from '@utils'
import { hasPermission, arrayOrDefault } from '@utils'
import { DrillFlowChartService } from '@api'
@ -26,7 +25,7 @@ export const DrillProcessFlow = memo(() => {
const [flows, setFlows] = useState([])
const [showLoader, setShowLoader] = useState(false)
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
const updateFlows = () => invokeWebApiWrapperAsync(
async () => {
@ -38,7 +37,9 @@ export const DrillProcessFlow = memo(() => {
'Получение режимно-технологической карты скважины'
)
useEffect(updateFlows, [idWell])
useEffect(() => {
updateFlows()
}, [idWell])
const onAdd = async (flow) => {
flow.idWell = idWell
@ -62,8 +63,8 @@ export const DrillProcessFlow = memo(() => {
return (
<LoaderPortal show={showLoader}>
<EditableTable
size={'small'}
bordered
size={'small'}
columns={columns}
dataSource={flows}
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 { FileOutlined, ImportOutlined, ExportOutlined } from '@ant-design/icons'
import { useIdWell } from '@asb/context'
import { download } from '@components/factory'
import { hasPermission } from '@utils/permissions'
import { hasPermission } from '@utils'
import { ImportOperations } from './ImportOperations'
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 idWellContext = useIdWell()
const idWell = useMemo(() => wellId ?? idWellContext, [idWellContext])
const downloadTemplate = async () => await download(`/api/well/${idWell}/wellOperations/template`)
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 { invokeWebApiWrapperAsync } from '@components/factory'
import { formatDate, fractionalSum } from '@utils/datetime'
import { formatDate, fractionalSum } from '@utils'
import '@styles/tvd.less'
@ -26,7 +26,8 @@ export const AdditionalTables = memo(({ operations, xLabel, setIsLoading }) => {
.reduce((out, row) => out + (row?.durationHours ?? 0), 0)
, [operations.fact])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const [factStartDate, factEndDate] = calcEndDate(operations.fact)
const [planStartDate, planEndDate] = calcEndDate(operations.plan)
@ -46,7 +47,8 @@ export const AdditionalTables = memo(({ operations, xLabel, setIsLoading }) => {
},
setIsLoading,
'Не удалось высчитать дополнительные данные'
), [operations, setIsLoading])
)
}, [operations, setIsLoading])
return (
<>

View File

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

View File

@ -1,5 +1,5 @@
import { useHistory } from 'react-router-dom'
import { memo, useState, useRef, useEffect, useCallback, useContext, useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
import { memo, useState, useRef, useEffect, useCallback, useMemo } from 'react'
import { DoubleLeftOutlined, DoubleRightOutlined } from '@ant-design/icons'
import { Switch, Button } from 'antd'
@ -16,11 +16,10 @@ import 'chartjs-adapter-moment'
import zoomPlugin from 'chartjs-plugin-zoom'
import ChartDataLabels from 'chartjs-plugin-datalabels'
import { IdWellContext } from '@asb/context'
import { useIdWell } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { formatDate, fractionalSum } from '@utils/datetime'
import { getOperations } from '@utils/functions'
import { formatDate, fractionalSum, wrapPrivateComponent, getOperations } from '@utils'
import NptTable from './NptTable'
import NetGraphExport from './NetGraphExport'
@ -115,18 +114,18 @@ const makeDataset = (data, label, color, borderWidth = 1.5, borderDash) => ({
borderDash,
})
export const Tvd = memo(({ idWell: wellId, title, ...other }) => {
const Tvd = memo(({ idWell: wellId, title, ...other }) => {
const [chart, setChart] = useState()
const [xLabel, setXLabel] = useState('day')
const [operations, setOperations] = useState({})
const [tableVisible, setTableVisible] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const idWellContext = useContext(IdWellContext)
const idWellContext = useIdWell()
const idWell = useMemo(() => wellId ?? idWellContext, [wellId, idWellContext])
const chartRef = useRef(null)
const history = useHistory()
const navigate = useNavigate()
const onPointClick = useCallback((e) => {
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 ids = points.filter((p) => p?.raw?.id).map((p) => p.raw.id).join(',')
history.push(`/well/${idWell}/operations/${datasetName}/?selectedId=${ids}`)
}, [idWell, history])
navigate(`/well/${idWell}/operations/${datasetName}/?selectedId=${ids}`)
}, [idWell, navigate])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => setOperations(await getOperations(idWell)),
setIsLoading,
`Не удалось загрузить операции по скважине "${idWell}"`,
'Получение списка опервций по скважине'
), [idWell])
)
}, [idWell])
useEffect(() => {
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 {
EditableTable,
makeSelectColumn,
@ -11,15 +11,14 @@ import {
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { DrillParamsService, WellOperationService } from '@api'
import { hasPermission } from '@utils/permissions'
import { arrayOrDefault } from '@utils'
import { hasPermission, arrayOrDefault } from '@utils'
export const getColumns = async (idWell) => {
let sectionTypes = await WellOperationService.getSectionTypes(idWell)
sectionTypes = Object.entries(sectionTypes).map(([id, value]) => ({
label: value,
value: parseInt(id),
value: id,
}))
return [
@ -41,7 +40,7 @@ export const WellDrillParams = memo(() => {
const [showLoader, setShowLoader] = useState(false)
const [columns, setColumns] = useState([])
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
const updateParams = useCallback(async () => await invokeWebApiWrapperAsync(
async () => {
@ -55,10 +54,12 @@ export const WellDrillParams = memo(() => {
'Получение списка режимов бурения скважины'
), [idWell])
useEffect(() => (async () => {
useEffect(() => {
(async () => {
setColumns(await getColumns(idWell))
await updateParams()
})(), [idWell, updateParams])
})()
}, [idWell, updateParams])
const handlerProps = useMemo(() => ({
service: DrillParamsService,
@ -73,8 +74,8 @@ export const WellDrillParams = memo(() => {
return (
<LoaderPortal show={showLoader}>
<EditableTable
size={'small'}
bordered
size={'small'}
columns={columns}
dataSource={params}
tableName={'well_drill_params'}

View File

@ -1,9 +1,9 @@
import moment from 'moment'
import { Input } from 'antd'
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 {
EditableTable,
makeColumn,
@ -18,11 +18,9 @@ import {
} from '@components/Table'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { hasPermission } from '@utils/permissions'
import { arrayOrDefault } from '@utils'
import { arrayOrDefault, wrapPrivateComponent, hasPermission } from '@utils'
import { WellOperationService } from '@api'
const { TextArea } = Input
const basePageSize = 160
@ -54,13 +52,13 @@ const generateColumns = (showNpt = false, categories = [], sectionTypes = []) =>
makeTextColumn('Комментарий', 'comment', null, null, null, { editable: true, input: <TextArea/> }),
].filter(Boolean)
export const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => {
const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => {
const [pageNumAndPageSize, setPageNumAndPageSize] = useState({ current: 1, pageSize: basePageSize })
const [paginationTotal, setPaginationTotal] = useState(0)
const [operations, setOperations] = useState([])
const [showLoader, setShowLoader] = useState(false)
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
const [categories, setCategories] = useState([])
const [sectionTypes, setSectionTypes] = useState([])
@ -73,7 +71,8 @@ export const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => {
return arrayOrDefault(query.get('selectedId')?.split(',')?.map(parseInt))
}, [location])
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const categories = arrayOrDefault(await WellOperationService.getCategories(idWell))
setCategories(categories.map((item) => ({ value: item.id, label: item.name })))
@ -84,7 +83,8 @@ export const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => {
setShowLoader,
'Не удалось загрузить список операций по скважине',
'Получение списка операций по скважине'
), [idWell])
)
}, [idWell])
const updateOperations = useCallback(() => invokeWebApiWrapperAsync(
async () => {
@ -103,7 +103,9 @@ export const WellOperationsEditor = memo(({ idType, showNpt, ...other }) => {
'Получение списка операций по скважине'
), [idWell, idType, pageNumAndPageSize])
useEffect(updateOperations, [updateOperations])
useEffect(() => {
updateOperations()
}, [updateOperations])
const handlerProps = useMemo(() => ({
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 { invokeWebApiWrapperAsync } from '@components/factory'
import { Table, makeColumn, makeColumnsPlanFact, makeNumericRender } from '@components/Table'
import { calcDuration } from '@utils/datetime'
import { calcDuration } from '@utils'
import { OperationStatService } from '@api'
@ -25,9 +25,10 @@ export const WellSectionsStat = memo(() => {
const [sections, setSections] = useState([])
const [showLoader, setShowLoader] = useState(false)
const idWell = useContext(IdWellContext)
const idWell = useIdWell()
useEffect(() => invokeWebApiWrapperAsync(
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const sectionsResponse = await OperationStatService.getStatWell(idWell)
@ -58,7 +59,8 @@ export const WellSectionsStat = memo(() => {
setShowLoader,
`Не удалось получить статистику по секциям скважины "${idWell}"`,
'Получение статистики по секциям скважины'
), [idWell])
)
}, [idWell])
return (
<LoaderPortal show={showLoader}>

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