* Получение списка месторождений вынесено в контекст для сокращения колличества запросов

* Обёртки для страниц вынесены в ленивую загрузку
* Страница месторождений и селекторы скважин переписаны с использованием контекста
* Улучшена мемоизация и оптимизированы расчёты
This commit is contained in:
Александр Сироткин 2022-11-11 00:06:11 +05:00
parent ac9b4d6c0d
commit cc1c6a0661
8 changed files with 223 additions and 185 deletions

View File

@ -4,8 +4,6 @@ import locale from 'antd/lib/locale/ru_RU'
import { ConfigProvider } from 'antd' 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 { getUser, NoAccessComponent } from '@utils'
import { OpenAPI } from '@api' import { OpenAPI } from '@api'
@ -13,6 +11,10 @@ import { OpenAPI } from '@api'
import '@styles/include/antd_theme.less' 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'))
@ -44,14 +46,16 @@ export const App = memo(() => (
<Route element={<UserOutlet />}> <Route element={<UserOutlet />}>
<Route path={'/file_download/:idFile/*'} element={<FileDownload />} /> <Route path={'/file_download/:idFile/*'} element={<FileDownload />} />
<Route element={<LayoutPortal />}> <Route element={<DepositsOutlet />}>
{/* Admin pages */} <Route element={<LayoutPortal />}>
<Route path={'/admin/*'} element={<AdminPanel />} /> {/* Admin pages */}
<Route path={'/admin/*'} element={<AdminPanel />} />
{/* Client pages */} {/* Client pages */}
<Route path={'/deposit/*'} element={<Deposit />} /> <Route path={'/deposit/*'} element={<Deposit />} />
<Route path={'/cluster/:idCluster'} element={<Cluster />} /> <Route path={'/cluster/:idCluster'} element={<Cluster />} />
<Route path={'/well/:idWell/*'} element={<Well />} /> <Route path={'/well/:idWell/*'} element={<Well />} />
</Route>
</Route> </Route>
</Route> </Route>
</Routes> </Routes>

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

@ -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 {
@ -157,4 +157,13 @@ code {
.asb-grid-item { .asb-grid-item {
padding: 4px; padding: 4px;
} }
.pointer {
cursor: pointer;
}
.deposit-page {
height: 100vh;
overflow: hidden;
}