Merge branch 'dev' into feature/merging-telemetry-view-and-archive

This commit is contained in:
Александр Сироткин 2022-11-16 11:29:06 +05:00
commit ec2513b4a0
25 changed files with 383 additions and 266 deletions

View File

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

View File

@ -22,7 +22,7 @@ export const Grid = memo<ComponentProps>(({ children, style, ...other }) => (
</div> </div>
)) ))
export const GridItem = memo<GridItemProps>(({ children, row, col, rowSpan, colSpan, style, ...other }) => { export const GridItem = memo<GridItemProps>(({ children, row, col, rowSpan, colSpan, style, className, ...other }) => {
const localRow = +row const localRow = +row
const localCol = +col const localCol = +col
const localColSpan = colSpan ? colSpan - 1 : 0 const localColSpan = colSpan ? colSpan - 1 : 0
@ -32,12 +32,11 @@ export const GridItem = memo<GridItemProps>(({ children, row, col, rowSpan, colS
gridColumnEnd: localCol + localColSpan, gridColumnEnd: localCol + localColSpan,
gridRowStart: localRow, gridRowStart: localRow,
gridRowEnd: localRow + localRowSpan, gridRowEnd: localRow + localRowSpan,
padding: '4px',
...style, ...style,
} }
return ( return (
<div style={gridItemStyle} {...other}> <div className={`asb-grid-item ${className || ''}`} style={gridItemStyle} {...other}>
{children} {children}
</div> </div>
) )

View File

@ -6,11 +6,12 @@ type LoaderPortalProps = HTMLAttributes<HTMLDivElement> & {
show?: boolean, show?: boolean,
fade?: boolean, fade?: boolean,
spinnerProps?: HTMLAttributes<HTMLDivElement>, spinnerProps?: HTMLAttributes<HTMLDivElement>,
fillContent?: boolean
} }
export const LoaderPortal: React.FC<LoaderPortalProps> = ({ className = '', show, fade = true, children, spinnerProps, ...other }) => ( export const LoaderPortal: React.FC<LoaderPortalProps> = ({ className = '', show, fade = true, children, spinnerProps, fillContent, ...other }) => (
<div className={`loader-container ${className}`} {...other}> <div className={`loader-container ${className}`} {...other}>
<div className={'loader-content'}>{children}</div> <div className={`loader-content${fillContent ? ' loader-content-fill' : ''}`}>{children}</div>
{show && fade && <div className={'loader-fade'}/>} {show && fade && <div className={'loader-fade'}/>}
{show && <div className={'loader-overlay'}><Loader {...spinnerProps} /></div>} {show && <div className={'loader-overlay'}><Loader {...spinnerProps} /></div>}
</div> </div>

View File

@ -221,9 +221,7 @@ export const EditableTable = memo(({
const mergedColumns = useMemo(() => [...columns.map(handleColumn), operationColumn], [columns, handleColumn, operationColumn]) const mergedColumns = useMemo(() => [...columns.map(handleColumn), operationColumn], [columns, handleColumn, operationColumn])
useEffect(() => { useEffect(() => setData(tryAddKeys(dataSource)), [dataSource])
setData(tryAddKeys(dataSource))
}, [dataSource])
return ( return (
<Form form={form}> <Form form={form}>

View File

@ -193,6 +193,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
methods, methods,
className = '', className = '',
style,
...other ...other
}: D3MonitoringChartsProps<DataType>) => { }: D3MonitoringChartsProps<DataType>) => {
const [datasets, setDatasets, resetDatasets] = useUserSettings(chartName, datasetGroups) const [datasets, setDatasets, resetDatasets] = useUserSettings(chartName, datasetGroups)
@ -351,10 +352,10 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
x: getByAccessor(dataset.xAxis?.accessor), x: getByAccessor(dataset.xAxis?.accessor),
} }
) )
if (newChart.type === 'line') if (newChart.type === 'line')
newChart.optimization = false newChart.optimization = false
// Если у графика нет группы создаём её // Если у графика нет группы создаём её
if (newChart().empty()) if (newChart().empty())
group().append('g') group().append('g')
@ -496,12 +497,12 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
default: default:
break break
} }
if (chart.point) if (chart.point)
renderPoint<DataType>(xAxis, yAxis, chart, chartData, true) renderPoint<DataType>(xAxis, yAxis, chart, chartData, true)
if (dash) chart().attr('stroke-dasharray', dash) if (dash) chart().attr('stroke-dasharray', dash)
chart.afterDraw?.(chart) chart.afterDraw?.(chart)
}) })
}) })
@ -513,6 +514,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
style={{ style={{
width: givenWidth, width: givenWidth,
height: givenHeight, height: givenHeight,
...style,
}} }}
> >
<div <div

View File

@ -0,0 +1,35 @@
import { memo, useEffect, useState } from 'react'
import { Outlet } from 'react-router-dom'
import { DepositsContext } from '@asb/context'
import LoaderPortal from '@components/LoaderPortal'
import { invokeWebApiWrapperAsync } from '@components/factory'
import { DepositDto, DepositService } from '@api'
import { arrayOrDefault } from '@utils'
export const DepositsOutlet = memo(() => {
const [deposits, setDeposits] = useState<DepositDto[]>([])
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const deposits = await DepositService.getDeposits()
setDeposits(arrayOrDefault(deposits))
},
setIsLoading,
`Не удалось загрузить список кустов`,
{ actionName: 'Получить список кустов' }
)
}, [])
return (
<DepositsContext.Provider value={deposits}>
<LoaderPortal show={isLoading}>
<Outlet />
</LoaderPortal>
</DepositsContext.Provider>
)
})
export default DepositsOutlet

View File

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

View File

@ -1,12 +1,11 @@
import { Tag, TreeSelect } from 'antd' import { Tag, TreeSelect } from 'antd'
import { memo, useEffect, useState } from 'react' import { memo, useEffect, useState } from 'react'
import { useDeposits } from '@asb/context'
import { invokeWebApiWrapperAsync } from '@components/factory' import { invokeWebApiWrapperAsync } from '@components/factory'
import { hasPermission } from '@utils' import { hasPermission } from '@utils'
import { DepositService } from '@api'
export const getTreeData = async () => { export const getTreeData = async (deposits) => {
const deposits = await DepositService.getDeposits()
const wellsTree = deposits.map((deposit, dIdx) => ({ const wellsTree = deposits.map((deposit, dIdx) => ({
title: deposit.caption, title: deposit.caption,
key: `0-${dIdx}`, key: `0-${dIdx}`,
@ -40,10 +39,12 @@ export const WellSelector = memo(({ value, onChange, treeData, treeLabels, ...ot
const [wellsTree, setWellsTree] = useState([]) const [wellsTree, setWellsTree] = useState([])
const [wellLabels, setWellLabels] = useState([]) const [wellLabels, setWellLabels] = useState([])
const deposits = useDeposits()
useEffect(() => { useEffect(() => {
invokeWebApiWrapperAsync( invokeWebApiWrapperAsync(
async () => { async () => {
const wellsTree = treeData ?? await getTreeData() const wellsTree = treeData ?? await getTreeData(deposits)
const labels = treeLabels ?? getTreeLabels(wellsTree) const labels = treeLabels ?? getTreeLabels(wellsTree)
setWellsTree(wellsTree) setWellsTree(wellsTree)
setWellLabels(labels) setWellLabels(labels)
@ -52,7 +53,7 @@ export const WellSelector = memo(({ value, onChange, treeData, treeLabels, ...ot
'Не удалось загрузить список скважин', 'Не удалось загрузить список скважин',
{ actionName: 'Получение списка скважин' } { actionName: 'Получение списка скважин' }
) )
}, [treeData, treeLabels]) }, [deposits, treeData, treeLabels])
return ( return (
<TreeSelect <TreeSelect

View File

@ -1,10 +1,10 @@
import { Button, Drawer, Skeleton, Tree, TreeDataNode, TreeProps, Typography } from 'antd' import { Drawer, Tree, TreeDataNode, TreeProps } from 'antd'
import { useState, useEffect, useCallback, memo, Key } from 'react' import { useState, useEffect, useCallback, memo, Key, useMemo } from 'react'
import { useNavigate, useLocation } from 'react-router-dom' import { useNavigate, useLocation } from 'react-router-dom'
import { useDeposits } from '@asb/context'
import { WellIcon, WellIconState } from '@components/icons' import { WellIcon, WellIconState } from '@components/icons'
import { invokeWebApiWrapperAsync } from '@components/factory' import { DepositDto, WellDto } from '@api'
import { DepositService, DepositDto, WellDto } from '@api'
import { isRawDate } from '@utils' import { isRawDate } from '@utils'
import { ReactComponent as DepositIcon } from '@images/DepositIcon.svg' import { ReactComponent as DepositIcon } from '@images/DepositIcon.svg'
@ -91,62 +91,44 @@ const getExpandKeys = (treeData: TreeDataNode[], depositKeys?: Key[] | boolean):
return out return out
} }
const makeWellsTreeData = (deposits: DepositDto[]): TreeDataNode[] => deposits.map(deposit =>({
title: deposit.caption,
key: `/deposit/${deposit.id}`,
value: `/deposit/${deposit.id}`,
icon: <DepositIcon width={24} height={24}/>,
children: deposit.clusters?.map(cluster => {
const wells = cluster.wells ? cluster.wells.slice() : []
wells.sort(sortWellsByActive)
return {
title: cluster.caption,
key: `/cluster/${cluster.id}`,
value: `/cluster/${cluster.id}`,
icon: <ClusterIcon width={24} height={24}/>,
children: wells.map(well => ({
title: well.caption,
key: `/well/${well.id}`,
value: `/well/${well.id}`,
icon: <WellIcon
width={24}
height={24}
state={getWellState(well.idState)}
online={checkIsWellOnline(well.lastTelemetryDate)}
/>
})),
}
}),
}))
export const WellTreeSelector = memo<WellTreeSelectorProps>(({ expand, current, onChange, onClose, open, ...other }) => { export const WellTreeSelector = memo<WellTreeSelectorProps>(({ expand, current, onChange, onClose, open, ...other }) => {
const [wellsTree, setWellsTree] = useState<TreeDataNode[]>([])
const [showLoader, setShowLoader] = useState<boolean>(false)
const [expanded, setExpanded] = useState<Key[]>([]) const [expanded, setExpanded] = useState<Key[]>([])
const [selected, setSelected] = useState<Key[]>([]) const [selected, setSelected] = useState<Key[]>([])
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const deposits = useDeposits()
useEffect(() => { const wellsTree = useMemo(() => makeWellsTreeData(deposits), [deposits])
if (current) setSelected([current])
}, [current])
useEffect(() => {
setExpanded((prev) => expand ? getExpandKeys(wellsTree, expand) : prev)
}, [wellsTree, expand])
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const deposits: Array<DepositDto> = await DepositService.getDeposits()
const wellsTree: TreeDataNode[] = deposits.map(deposit =>({
title: deposit.caption,
key: `/deposit/${deposit.id}`,
value: `/deposit/${deposit.id}`,
icon: <DepositIcon width={24} height={24}/>,
children: deposit.clusters?.map(cluster => {
const wells = cluster.wells ? cluster.wells.slice() : []
wells.sort(sortWellsByActive)
return {
title: cluster.caption,
key: `/cluster/${cluster.id}`,
value: `/cluster/${cluster.id}`,
icon: <ClusterIcon width={24} height={24}/>,
children: wells.map(well => ({
title: well.caption,
key: `/well/${well.id}`,
value: `/well/${well.id}`,
icon: <WellIcon
width={24}
height={24}
state={getWellState(well.idState)}
online={checkIsWellOnline(well.lastTelemetryDate)}
/>
})),
}
}),
}))
setWellsTree(wellsTree)
},
setShowLoader,
`Не удалось загрузить список скважин`,
{ actionName: 'Получить список скважин' }
)
}, [])
const onValueChange = useCallback((value?: string): void => { const onValueChange = useCallback((value?: string): void => {
const key = getKeyByUrl(value)[0] const key = getKeyByUrl(value)[0]
@ -169,21 +151,27 @@ export const WellTreeSelector = memo<WellTreeSelectorProps>(({ expand, current,
navigate(newPath, { state: { from: location.pathname }}) navigate(newPath, { state: { from: location.pathname }})
}, [navigate, location]) }, [navigate, location])
useEffect(() => onValueChange(location.pathname), [onValueChange, location]) useEffect(() => {
if (current) setSelected([current])
}, [current])
useEffect(() => {
setExpanded((prev) => expand ? getExpandKeys(wellsTree, expand) : prev)
}, [wellsTree, expand])
useEffect(() => onValueChange(location.pathname), [onValueChange, location.pathname])
return ( return (
<Drawer open={open} mask={false} onClose={onClose} title={'Список скважин'}> <Drawer open={open} mask={false} onClose={onClose} title={'Список скважин'}>
<Skeleton active loading={showLoader}> <Tree
<Tree {...other}
{...other} showIcon
showIcon selectedKeys={selected}
selectedKeys={selected} treeData={wellsTree}
treeData={wellsTree} onSelect={onSelect}
onSelect={onSelect} onExpand={setExpanded}
onExpand={setExpanded} expandedKeys={expanded}
expandedKeys={expanded} />
/>
</Skeleton>
</Drawer> </Drawer>
) )
}) })

View File

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

View File

@ -1,15 +1,28 @@
import React from 'react' import locale from 'antd/lib/locale/ru_RU'
import { ConfigProvider } from 'antd'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import React from 'react'
import { getUser } from '@utils'
import { OpenAPI } from '@api'
import App from './App' import App from './App'
import '@styles/include/antd_theme.less'
import '@styles/index.css' import '@styles/index.css'
// OpenAPI.BASE = 'http://localhost:3000'
// TODO: Удалить взятие из 'token' в следующем релизе, вставлено для совместимости
OpenAPI.TOKEN = async () => getUser().token || localStorage.getItem('token') || ''
OpenAPI.HEADERS = { 'Content-Type': 'application/json' }
const container = document.getElementById('root') ?? document.body const container = document.getElementById('root') ?? document.body
const root = createRoot(container) const root = createRoot(container)
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<App /> <ConfigProvider locale={locale}>
<App />
</ConfigProvider>
</React.StrictMode> </React.StrictMode>
) )

View File

@ -7,11 +7,11 @@ import {
makeTextColumn, makeTextColumn,
makeGroupColumn, makeGroupColumn,
makeColumn, makeColumn,
makeDateSorter,
makeNumericColumnPlanFact, makeNumericColumnPlanFact,
Table, Table,
makeNumericRender, makeNumericRender,
makeNumericColumn, makeNumericColumn,
makeDateColumn,
} from '@components/Table' } from '@components/Table'
import LoaderPortal from '@components/LoaderPortal' import LoaderPortal from '@components/LoaderPortal'
import PointerIcon from '@components/icons/PointerIcon' import PointerIcon from '@components/icons/PointerIcon'
@ -39,7 +39,6 @@ const filtersWellsType = []
const DAY_IN_MS = 86_400_000 const DAY_IN_MS = 86_400_000
const ONLINE_DEADTIME = 600_000 const ONLINE_DEADTIME = 600_000
const getDate = (str) => isRawDate(str) ? new Date(str).toLocaleString() : '-'
const numericRender = makeNumericRender(1) const numericRender = makeNumericRender(1)
const ClusterWells = memo(({ statsWells }) => { const ClusterWells = memo(({ statsWells }) => {
@ -131,8 +130,8 @@ const ClusterWells = memo(({ statsWells }) => {
), ),
makeTextColumn('Тип скв.', 'wellType', filtersWellsType, null, (text) => text ?? '-'), makeTextColumn('Тип скв.', 'wellType', filtersWellsType, null, (text) => text ?? '-'),
makeGroupColumn('Фактические сроки', [ makeGroupColumn('Фактические сроки', [
makeColumn('начало', 'factStart', { sorter: makeDateSorter('factStart'), render: getDate }), makeDateColumn('начало', 'factStart'),
makeColumn('окончание', 'factEnd', { sorter: makeDateSorter('factEnd'), render: getDate }), makeDateColumn('окончание', 'factEnd'),
]), ]),
makeNumericColumnPlanFact('Продолжительность, сут', 'period', filtersMinMax, makeFilterMinMaxFunction, numericRender), makeNumericColumnPlanFact('Продолжительность, сут', 'period', filtersMinMax, makeFilterMinMaxFunction, numericRender),
makeNumericColumnPlanFact('МСП, м/ч', 'rateOfPenetration', filtersMinMax, makeFilterMinMaxFunction, numericRender), makeNumericColumnPlanFact('МСП, м/ч', 'rateOfPenetration', filtersMinMax, makeFilterMinMaxFunction, numericRender),

View File

@ -1,120 +1,106 @@
import { useState, useEffect, memo, useMemo } from 'react' import { useEffect, memo, useMemo, useCallback } from 'react'
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import { Map, Overlay } from 'pigeon-maps' import { Map, Overlay } from 'pigeon-maps'
import { Popover, Badge } from 'antd' import { Popover, Badge } from 'antd'
import { useLayoutProps } from '@asb/context' import { useDeposits, useLayoutProps } from '@asb/context'
import { PointerIcon } from '@components/icons' import { PointerIcon } from '@components/icons'
import LoaderPortal from '@components/LoaderPortal'
import { FastRunMenu } from '@components/FastRunMenu' import { FastRunMenu } from '@components/FastRunMenu'
import { invokeWebApiWrapperAsync } from '@components/factory' import { limitValue, withPermissions } from '@utils'
import { arrayOrDefault, limitValue, withPermissions } from '@utils'
import { DepositService } from '@api'
import '@styles/index.css' import '@styles/index.css'
const defaultViewParams = { center: [60.81226, 70.0562], zoom: 5 }
const zoomLimit = limitValue(5, 15) const zoomLimit = limitValue(5, 15)
const calcViewParams = (clusters) => { const calcViewParams = (clusters) => {
if ((clusters?.length ?? 0) <= 0) if ((clusters?.length ?? 0) <= 0)
return defaultViewParams return { center: [60.81226, 70.0562], zoom: 5 }
const center = clusters.reduce((sum, cluster) => { const center = clusters.reduce((sum, cluster) => {
sum[0] += (cluster.latitude / clusters.length) sum[0] += cluster.latitude
sum[1] += (cluster.longitude / clusters.length) sum[1] += cluster.longitude
return sum return sum
}, [0, 0]) }, [0, 0]).map((elm) => elm / clusters.length)
const maxDeg = clusters.reduce((max, cluster) => { const maxDeg = clusters.reduce((max, cluster) => {
const dLatitude = Math.abs(center[0] - cluster.latitude) const dLatitude = Math.abs(center[0] - cluster.latitude)
const dLongitude = Math.abs(center[1] - cluster.longitude) const dLongitude = Math.abs(center[1] - cluster.longitude)
const d = dLatitude > dLongitude ? dLatitude : dLongitude return Math.max(Math.max(dLatitude, dLongitude), max)
return d > max ? d : max }, 0)
}, 0)
// zoom max = 20 (too close) // zoom max = 20 (too close)
// zoom min = 1 (mega far) // zoom min = 1 (mega far)
// 4 - full Russia (161.6 deg) // 4 - full Russia (161.6 deg)
// 13.5 - Khanty-Mansiysk // 13.5 - Khanty-Mansiysk
const zoom = zoomLimit(5 + 5 / (maxDeg + 0.5)) const zoom = zoomLimit(5 + 5 / (maxDeg + 0.5))
return { center, zoom } return { center, zoom }
} }
const Deposit = memo(() => { const Deposit = memo(() => {
const [depositsData, setDepositsData] = useState([]) const deposits = useDeposits()
const [showLoader, setShowLoader] = useState(false) const setLayoutProps = useLayoutProps()
const [viewParams, setViewParams] = useState(defaultViewParams) const location = useLocation()
const setLayoutProps = useLayoutProps() const makeDepositLinks = useCallback((clusters) => (
<div>
const location = useLocation() {clusters.map(cluster => (
<Link
const selectorProps = useMemo(() => { key={cluster.id}
const hasId = location.pathname.length > '/deposit/'.length to={{
pathname: `/cluster/${cluster.id}`,
return { state: { from: location.pathname }
expand: hasId ? [location.pathname] : true, }}
current: hasId ? location.pathname : undefined, >
} <div>{cluster.caption}</div>
}, [location.pathname]) </Link>
useEffect(() => setLayoutProps({
sheet: false,
showSelector: true,
selectorProps,
title: 'Месторождение',
}), [setLayoutProps, selectorProps])
useEffect(() => {
invokeWebApiWrapperAsync(
async () => {
const deposits = await DepositService.getDeposits()
setDepositsData(arrayOrDefault(deposits))
setViewParams(calcViewParams(deposits))
},
setShowLoader,
`Не удалось загрузить список кустов`,
{ actionName: 'Получить список кустов' }
)
}, [])
return (
<>
<FastRunMenu />
<LoaderPortal show={showLoader}>
<div className={'h-100vh'} style={{ overflow: 'hidden' }}>
<Map {...viewParams}>
{depositsData.map(deposit => (
<Overlay
width={32}
anchor={[deposit.latitude, deposit.longitude]}
key={`${deposit.latitude} ${deposit.longitude}`}
>
<Popover content={
<div>
{deposit.clusters.map(cluster => (
<Link key={cluster.id} to={{ pathname: `/cluster/${cluster.id}`, state: { from: location.pathname }}}>
<div>{cluster.caption}</div>
</Link>
))}
</div>
} trigger={['click']} title={deposit.caption}>
<div style={{cursor: 'pointer'}}>
<Badge count={deposit.clusters.length}>
<PointerIcon state={'active'} width={48} height={59} />
</Badge>
</div>
</Popover>
</Overlay>
))} ))}
</Map>
</div> </div>
</LoaderPortal> ), [location.pathname])
</>
) const viewParams = useMemo(() => calcViewParams(deposits), [deposits])
useEffect(() => {
const hasId = location.pathname.length > '/deposit/'.length
const selectorProps = {
expand: hasId ? [location.pathname] : true,
current: hasId ? location.pathname : undefined,
}
setLayoutProps({
sheet: false,
showSelector: true,
selectorProps,
title: 'Месторождение',
})
}, [setLayoutProps, location.pathname])
return (
<>
<FastRunMenu />
<div className={'deposit-page'}>
<Map {...viewParams}>
{deposits.map(deposit => {
const anchor = [deposit.latitude, deposit.longitude]
const links = makeDepositLinks(deposit.clusters)
return (
<Overlay width={32} anchor={anchor} key={anchor.join(' ')}>
<Popover content={links} trigger={['click']} title={deposit.caption}>
<div className={'pointer'}>
<Badge count={deposit.clusters.length}>
<PointerIcon state={'active'} width={48} height={59} />
</Badge>
</div>
</Popover>
</Overlay>
)
})}
</Map>
</div>
</>
)
}) })
export default withPermissions(Deposit, ['Cluster.get']) export default withPermissions(Deposit, ['Cluster.get'])

View File

@ -15,7 +15,7 @@ import { formatDate, range, withPermissions } from '@utils'
import { TelemetryDataSaubService } from '@api' import { TelemetryDataSaubService } from '@api'
import { normalizeData } from '../TelemetryView' import { normalizeData } from '../TelemetryView'
import cursorRender from '../TelemetryView/cursorRender' import { cursorRender } from '../TelemetryView/cursorRender'
import { makeChartGroups, yAxis } from '../TelemetryView/dataset' import { makeChartGroups, yAxis } from '../TelemetryView/dataset'
const DATA_COUNT = 2048 // Колличество точек на подгрузку графика const DATA_COUNT = 2048 // Колличество точек на подгрузку графика
@ -223,7 +223,7 @@ const Archive = memo(() => {
const chartData = useMemo(() => cutData(dataSaub, domain.min, domain.max), [dataSaub, domain]) const chartData = useMemo(() => cutData(dataSaub, domain.min, domain.max), [dataSaub, domain])
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader} style={{ flexGrow: 1 }}>
<Flex style={{margin: '8px 8px 0'}}> <Flex style={{margin: '8px 8px 0'}}>
<div> <div>
Начальная дата:&nbsp; Начальная дата:&nbsp;
@ -259,6 +259,7 @@ const Archive = memo(() => {
render: cursorRender, render: cursorRender,
} }
}} }}
style={{ flexGrow: 1 }}
height={'76vh'} height={'76vh'}
onWheel={onGraphWheel} onWheel={onGraphWheel}
/> />

View File

@ -28,7 +28,7 @@ const categoryDictionary = {
// Конфигурация таблицы // Конфигурация таблицы
export const makeMessageColumns = (idWell) => [ export const makeMessageColumns = (idWell) => [
makeDateColumn('Дата', 'date', undefined, undefined, { width: '10rem' }), makeDateColumn('Дата', 'date', undefined, undefined, { width: '120px' }),
makeNumericColumn('Глубина, м', 'wellDepth', null, null, (depth, item) => ( makeNumericColumn('Глубина, м', 'wellDepth', null, null, (depth, item) => (
<Tooltip title={'Нажмите для перехода в архив'}> <Tooltip title={'Нажмите для перехода в архив'}>
<Link <Link
@ -50,7 +50,7 @@ export const makeMessageColumns = (idWell) => [
ellipsis: true, ellipsis: true,
}), }),
makeColumn('Сообщение', 'message', { onFilter: (value, record) => record.name.indexOf(value) === 0 }), makeColumn('Сообщение', 'message', { onFilter: (value, record) => record.name.indexOf(value) === 0 }),
makeTextColumn('Пользователь', 'user', null, null, null, { width: '10rem' }), makeTextColumn('Пользователь', 'user', null, null, null, { width: '120px' }),
] ]
const filterOptions = [ const filterOptions = [

View File

@ -24,15 +24,15 @@ import WirelineRunOut from './WirelineRunOut'
import { CustomColumn } from './CustomColumn' import { CustomColumn } from './CustomColumn'
import { ModeDisplay } from './ModeDisplay' import { ModeDisplay } from './ModeDisplay'
import { UserOfWell } from './UserOfWells' import { UserOfWell } from './UserOfWells'
import cursorRender from './cursorRender'
import { Setpoints } from './Setpoints' import { Setpoints } from './Setpoints'
import { cursorRender } from './cursorRender'
import MomentStabPicEnabled from '@images/DempherOn.png' import MomentStabPicEnabled from '@images/DempherOn.png'
import MomentStabPicDisabled from '@images/DempherOff.png' import MomentStabPicDisabled from '@images/DempherOff.png'
import SpinPicEnabled from '@images/SpinEnabled.png' import SpinPicEnabled from '@images/SpinEnabled.png'
import SpinPicDisabled from '@images/SpinDisabled.png' import SpinPicDisabled from '@images/SpinDisabled.png'
import '@styles/monitoring.less' import '@styles/telemetry_view.less'
import '@styles/message.less' import '@styles/message.less'
const { Option } = Select const { Option } = Select
@ -64,6 +64,7 @@ const makeSubjectSubsription = (subject$, handler) => {
} }
const TelemetryView = memo(() => { const TelemetryView = memo(() => {
const [currentWellId, setCurrentWellId] = useState(null)
const [dataSaub, setDataSaub] = useState([]) const [dataSaub, setDataSaub] = useState([])
const [dataSpin, setDataSpin] = useState([]) const [dataSpin, setDataSpin] = useState([])
const [chartInterval, setChartInterval] = useState(defaultPeriod) const [chartInterval, setChartInterval] = useState(defaultPeriod)
@ -78,12 +79,12 @@ const TelemetryView = memo(() => {
const saubSubject$ = useMemo(() => new BehaviorSubject(), []) const saubSubject$ = useMemo(() => new BehaviorSubject(), [])
const spinSubject$ = useMemo(() => new BehaviorSubject(), []) const spinSubject$ = useMemo(() => new BehaviorSubject(), [])
const handleDataSaub = useCallback((data) => { const handleDataSaub = useCallback((data, replace = false) => {
if (!data) return
const dataSaub = normalizeData(data)
setDataSaub((prev) => { setDataSaub((prev) => {
const out = [...prev, ...dataSaub] if (!data)
return replace ? [] : prev
const dataSaub = normalizeData(data)
const out = replace ? [...dataSaub] : [...prev, ...dataSaub]
out.sort(dateSorter) out.sort(dateSorter)
return out return out
}) })
@ -115,6 +116,8 @@ const TelemetryView = memo(() => {
useEffect(() => makeSubjectSubsription(spinSubject$, handleDataSpin), [spinSubject$, handleDataSpin]) useEffect(() => makeSubjectSubsription(spinSubject$, handleDataSpin), [spinSubject$, handleDataSpin])
useEffect(() => { useEffect(() => {
if (currentWellId == well.id) return
setCurrentWellId(well.id)
invokeWebApiWrapperAsync( invokeWebApiWrapperAsync(
async () => { async () => {
const flowChart = await DrillFlowChartService.getByIdWell(well.id) const flowChart = await DrillFlowChartService.getByIdWell(well.id)
@ -128,7 +131,7 @@ const TelemetryView = memo(() => {
`Не удалось получить данные`, `Не удалось получить данные`,
{ actionName: 'Получение данных по скважине', well } { actionName: 'Получение данных по скважине', well }
) )
}, [well, chartInterval, handleDataSpin, handleDataSaub]) }, [well, chartInterval, currentWellId, handleDataSaub])
useEffect(() => { useEffect(() => {
const unsubscribe = Subscribe( const unsubscribe = Subscribe(
@ -163,7 +166,7 @@ const TelemetryView = memo(() => {
return ( return (
<LoaderPortal show={showLoader}> <LoaderPortal show={showLoader}>
<Grid style={{ gridTemplateColumns: 'auto repeat(6, 1fr)' }}> <Grid className={'telemetry-view-page'} style={{ gridTemplateColumns: 'auto repeat(6, 1fr)' }}>
<GridItem col={'1'} row={'1'} colSpan={'8'} style={{ marginBottom: '0.5rem' }}> <GridItem col={'1'} row={'1'} colSpan={'8'} style={{ marginBottom: '0.5rem' }}>
<div className={'page-top'}> <div className={'page-top'}>
<ModeDisplay data={dataSaub} /> <ModeDisplay data={dataSaub} />

View File

@ -10,7 +10,7 @@ import { arrayOrDefault } from '@utils'
const columns = [ const columns = [
makeNumericStartEnd('Глубина, м', 'depth'), makeNumericStartEnd('Глубина, м', 'depth'),
makeNumericMinMax('Нагрузка, т', 'axialLoad'), makeNumericMinMax('Нагрузка, т', 'axialLoad'),
makeNumericMinMax('Давление, атм', 'pressure'), makeNumericMinMax('Перепад давления, атм', 'pressure'),
makeNumericMinMax('Момент на ВСП, кН·м', 'rotorTorque'), makeNumericMinMax('Момент на ВСП, кН·м', 'rotorTorque'),
makeNumericMinMax('Обороты на ВСП, об/мин', 'rotorSpeed'), makeNumericMinMax('Обороты на ВСП, об/мин', 'rotorSpeed'),
makeNumericMinMax('Расход, л/с', 'flow') makeNumericMinMax('Расход, л/с', 'flow')

View File

@ -26,7 +26,7 @@ export const getColumns = async (idWell) => {
sorter: makeNumericSorter('idWellSectionType'), sorter: makeNumericSorter('idWellSectionType'),
}), }),
makeNumericAvgRange('Нагрузка, т', 'axialLoad', 1), makeNumericAvgRange('Нагрузка, т', 'axialLoad', 1),
makeNumericAvgRange('Давление, атм', 'pressure', 1), makeNumericAvgRange('Перепад давления, атм', 'pressure', 1),
makeNumericAvgRange('Момент на ВСП, кН·м', 'rotorTorque', 1), makeNumericAvgRange('Момент на ВСП, кН·м', 'rotorTorque', 1),
makeNumericAvgRange('Обороты на ВСП, об/мин', 'rotorSpeed', 1), makeNumericAvgRange('Обороты на ВСП, об/мин', 'rotorSpeed', 1),
makeNumericAvgRange('Расход, л/с', 'flow', 1), makeNumericAvgRange('Расход, л/с', 'flow', 1),

View File

@ -7,8 +7,5 @@
// Переменные для темы тут: // Переменные для темы тут:
// https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less // https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less
//@primary-color: rgba(124, 124, 124, 0.3); @primary-color: #C32828; // rgb(195, 40, 40)
@primary-color: rgb(195, 40,40); @layout-header-background:#413F3D; // rgb(65, 63, 61)
//@primary-color:rgb(65, 63, 61);
//@layout-header-background: rgb(195, 40,40);
@layout-header-background: rgb(65, 63, 61);

View File

@ -135,7 +135,7 @@ code {
-moz-user-select: none; /* Old versions of Firefox */ -moz-user-select: none; /* Old versions of Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */ -ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently user-select: none; /* Non-prefixed version, currently
supported by Chrome, Edge, Opera and Firefox */ supported by Chrome, Edge, Opera and Firefox */
} }
.download-link { .download-link {
@ -153,4 +153,17 @@ code {
.color-pale-green { .color-pale-green {
background-color: #98fb98; background-color: #98fb98;
} }
.asb-grid-item {
padding: 4px;
}
.pointer {
cursor: pointer;
}
.deposit-page {
height: 100vh;
overflow: hidden;
}

View File

@ -19,6 +19,10 @@
} }
.page-layout { .page-layout {
--sheet-padding: 15px;
@sheet-padding: var(--sheet-padding);
height: 100vh; height: 100vh;
& .menu-sider { & .menu-sider {
@ -62,7 +66,7 @@
background-color: transparent; background-color: transparent;
cursor: pointer; cursor: pointer;
overflow: hidden; overflow: hidden;
&:hover { &:hover {
color: white; color: white;
} }
@ -88,8 +92,9 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 15px; gap: @sheet-padding;
padding: 0 15px 15px 15px; padding: @sheet-padding;
padding-top: 0;
& .ant-breadcrumb-link, .ant-breadcrumb-separator { & .ant-breadcrumb-link, .ant-breadcrumb-separator {
.no-select; .no-select;
@ -99,7 +104,7 @@
} }
& .sheet{ & .sheet{
padding: 15px; padding: @sheet-padding;
// min-height: calc(@layout-min-height - 30px); // 280px; // min-height: calc(@layout-min-height - 30px); // 280px;
overflow: visible; overflow: visible;
// margin: 15px; // margin: 15px;
@ -126,3 +131,15 @@
.site-layout-background { .site-layout-background {
background: #fff; background: #fff;
} }
@media only screen and (max-width: 1280px) {
.page-layout {
--sheet-padding: 10px;
}
}
@media only screen and (max-width: 1024px) {
.page-layout {
--sheet-padding: 5px;
}
}

View File

@ -39,11 +39,18 @@
} }
.loader-content{ .loader-content{
display: flex;
flex-direction: column;
grid-column-start: 1; grid-column-start: 1;
grid-column-end: span 3; grid-column-end: span 3;
grid-row-start: 1; grid-row-start: 1;
} }
.loader-content.loader-content-fill {
height: 100%;
width: 100%;
}
.loader-overlay{ .loader-overlay{
grid-column-start: 1; grid-column-start: 1;
grid-column-end: span 3; grid-column-end: span 3;

View File

@ -1,17 +0,0 @@
.page-top {
display: flex;
flex-wrap: wrap;
margin: -5px;
& > * {
margin: 5px;
}
& > .icons {
display: flex;
& > * {
margin-left: 15px;
}
}
}

View File

@ -0,0 +1,66 @@
.telemetry-view-page {
--page-gap: 10px;
@page-gap: var(--page-gap);
.flex-container {
display: flex;
align-items: stretch;
justify-content: space-between;
gap: @page-gap;
}
width: 100%;
height: 100%;
flex-direction: column;
.flex-container;
& .page-top {
.flex-container;
flex-wrap: wrap;
justify-content: flex-start;
& .icons {
display: flex;
& > * {
margin-left: 15px;
}
}
}
& .page-main {
flex-grow: 1;
.flex-container;
& .page-left {
.flex-container;
justify-content: flex-start;
flex-direction: column;
& .modes, & .current-values {
.flex-container;
flex-direction: column;
gap: 0;
}
}
& .page-right {
flex-grow: 1;
.flex-container;
flex-direction: column;
}
}
}
@media only screen and (max-width: 1280px) {
.telemetry-view-page {
--page-gap: 7.5px;
}
}
@media only screen and (max-width: 1024px) {
.telemetry-view-page {
--page-gap: 5px;
}
}

View File

@ -4,7 +4,7 @@
right: 0; right: 0;
top: 0; top: 0;
height: 100%; height: 100%;
background-color: red; background-color: #FFF2F0;
} }
& .avg-value { & .avg-value {