Compare commits
2 Commits
7ca78280b3
...
edce328880
| Author | SHA1 | Date | |
|---|---|---|---|
| edce328880 | |||
| cd30be7677 |
@ -1,13 +1,5 @@
|
||||
import { useEffect, useState, useContext, useMemo } from 'react'
|
||||
import {
|
||||
Card,
|
||||
Segmented,
|
||||
Flex,
|
||||
Popover,
|
||||
DatePicker,
|
||||
Button,
|
||||
Space
|
||||
} from 'antd'
|
||||
import { Card, Segmented, Flex, Popover, DatePicker, Button, Space } from 'antd'
|
||||
import {
|
||||
ResponsiveContainer,
|
||||
BarChart,
|
||||
@ -25,9 +17,9 @@ import { ApiServerContext } from '../context/ApiServerContext'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import dayjs from 'dayjs'
|
||||
import { useThemeContext } from '../context/ThemeContext'
|
||||
import LoadingPlaceholder from './LoadingPlaceholder'
|
||||
import MissingPlaceholder from './MissingPlaceholder'
|
||||
import CheckIcon from '../../Icons/CheckIcon'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
|
||||
const HistoryDisplay = ({
|
||||
objectType,
|
||||
@ -291,15 +283,17 @@ const HistoryDisplay = ({
|
||||
})
|
||||
|
||||
const chartRows = Array.from(
|
||||
chartData.reduce((acc, item) => {
|
||||
const existing = acc.get(item.date) || {
|
||||
date: item.date,
|
||||
dateFormatted: item.dateFormatted
|
||||
}
|
||||
existing[item.category] = item.value
|
||||
acc.set(item.date, existing)
|
||||
return acc
|
||||
}, new Map()).values()
|
||||
chartData
|
||||
.reduce((acc, item) => {
|
||||
const existing = acc.get(item.date) || {
|
||||
date: item.date,
|
||||
dateFormatted: item.dateFormatted
|
||||
}
|
||||
existing[item.category] = item.value
|
||||
acc.set(item.date, existing)
|
||||
return acc
|
||||
}, new Map())
|
||||
.values()
|
||||
).sort((a, b) => new Date(a.date) - new Date(b.date))
|
||||
|
||||
const customTimeRangeContent = (
|
||||
@ -334,89 +328,99 @@ const HistoryDisplay = ({
|
||||
style={{ width: '100%' }}
|
||||
styles={{ body: { padding: '12px', ...styles } }}
|
||||
>
|
||||
{!startDate && !endDate && (
|
||||
<Flex justify='space-between'>
|
||||
<Flex align='center' gap='5px'>
|
||||
<Popover
|
||||
content={customTimeRangeContent}
|
||||
trigger='hover'
|
||||
arrow={false}
|
||||
placement='bottomLeft'
|
||||
styles={{ body: { borderRadius: '22.5px' } }}
|
||||
>
|
||||
<Flex gap='small' vertical>
|
||||
{!startDate && !endDate && (
|
||||
<Flex justify='space-between'>
|
||||
<Flex align='center' gap='5px'>
|
||||
<Popover
|
||||
content={customTimeRangeContent}
|
||||
trigger='hover'
|
||||
arrow={false}
|
||||
placement='bottomLeft'
|
||||
styles={{ body: { borderRadius: '22.5px' } }}
|
||||
>
|
||||
<Segmented
|
||||
size='small'
|
||||
options={[{ label: 'Custom', value: 'custom' }]}
|
||||
value={timeRange}
|
||||
onChange={setTimeRange}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Popover>
|
||||
</Flex>
|
||||
|
||||
<Flex gap='middle'>
|
||||
{loading == true && <LoadingOutlined />}
|
||||
<Segmented
|
||||
size='small'
|
||||
options={[{ label: 'Custom', value: 'custom' }]}
|
||||
options={[
|
||||
{ label: '24hr', value: '24hrs' },
|
||||
{ label: '12hr', value: '12hrs' },
|
||||
{ label: '8hr', value: '8hrs' },
|
||||
{ label: '4hr', value: '4hrs' },
|
||||
{ label: '1hr', value: '1hrs' }
|
||||
]}
|
||||
value={timeRange}
|
||||
onChange={setTimeRange}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Popover>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Segmented
|
||||
size='small'
|
||||
options={[
|
||||
{ label: '24hr', value: '24hrs' },
|
||||
{ label: '12hr', value: '12hrs' },
|
||||
{ label: '8hr', value: '8hrs' },
|
||||
{ label: '4hr', value: '4hrs' },
|
||||
{ label: '1hr', value: '1hrs' }
|
||||
]}
|
||||
value={timeRange}
|
||||
onChange={setTimeRange}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
{loading == true && (
|
||||
<Flex justify='center' align='center' style={{ height: `${height}px` }}>
|
||||
<LoadingPlaceholder message='Loading history data...' />
|
||||
</Flex>
|
||||
)}
|
||||
{chartRows.length > 0 && (
|
||||
<div style={{ width: '100%', height: `${height}px` }}>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<BarChart data={chartRows}>
|
||||
<CartesianGrid
|
||||
strokeDasharray='3 3'
|
||||
stroke={isDarkMode ? '#303030' : '#f0f0f0'}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey='dateFormatted'
|
||||
tick={{ fill: isDarkMode ? '#d9d9d9' : '#595959', fontSize: 12 }}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fill: isDarkMode ? '#d9d9d9' : '#595959', fontSize: 12 }}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: isDarkMode ? '#1f1f1f' : '#ffffff',
|
||||
border: `1px solid ${isDarkMode ? '#303030' : '#f0f0f0'}`,
|
||||
borderRadius: 8
|
||||
}}
|
||||
/>
|
||||
<Legend />
|
||||
{modelStats.map((statDef, index) => {
|
||||
const category = statDef.label || statDef.name
|
||||
return (
|
||||
<Bar
|
||||
key={category}
|
||||
dataKey={category}
|
||||
stackId='history'
|
||||
fill={colorRange[index] || colors.default}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
{loading == false && chartRows.length == 0 && (
|
||||
<Flex justify='center' align='center' style={{ height: `${height}px` }}>
|
||||
<MissingPlaceholder message='No data available.' />
|
||||
</Flex>
|
||||
)}
|
||||
)}
|
||||
{chartRows.length > 0 && (
|
||||
<div style={{ width: '100%', height: `${height}px` }}>
|
||||
<ResponsiveContainer width='100%' height='100%'>
|
||||
<BarChart data={chartRows}>
|
||||
<CartesianGrid
|
||||
strokeDasharray='3 3'
|
||||
stroke={isDarkMode ? '#303030' : '#f0f0f0'}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey='dateFormatted'
|
||||
tick={{
|
||||
fill: isDarkMode ? '#d9d9d9' : '#595959',
|
||||
fontSize: 12
|
||||
}}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{
|
||||
fill: isDarkMode ? '#d9d9d9' : '#595959',
|
||||
fontSize: 12
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: isDarkMode ? '#1f1f1f' : '#ffffff',
|
||||
border: `1px solid ${isDarkMode ? '#303030' : '#f0f0f0'}`,
|
||||
borderRadius: 8
|
||||
}}
|
||||
/>
|
||||
<Legend />
|
||||
{modelStats.map((statDef, index) => {
|
||||
const category = statDef.label || statDef.name
|
||||
return (
|
||||
<Bar
|
||||
key={category}
|
||||
dataKey={category}
|
||||
stackId='history'
|
||||
fill={colorRange[index] || colors.default}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
{loading == false && chartRows.length == 0 && (
|
||||
<Flex
|
||||
justify='center'
|
||||
align='center'
|
||||
style={{ height: `${height}px` }}
|
||||
>
|
||||
<MissingPlaceholder message='No data available.' />
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
useMemo,
|
||||
@ -49,6 +50,7 @@ const logger = loglevel.getLogger('DasboardTable')
|
||||
logger.setLevel(config.logLevel)
|
||||
|
||||
const SCROLL_THRESHOLD = 50
|
||||
const SKELETON_HEIGHT = 49
|
||||
|
||||
const RowForm = ({ record, isEditing, onRegister, children }) => {
|
||||
const [form] = Form.useForm()
|
||||
@ -152,10 +154,12 @@ const ObjectTable = forwardRef(
|
||||
// Table state
|
||||
const [pages, setPages] = useState([])
|
||||
const pagesRef = useRef(pages)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const tableData = useMemo(
|
||||
() => pages.flatMap((page) => page.items),
|
||||
[pages]
|
||||
)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [lazyLoading, setLazyLoading] = useState(false)
|
||||
const [tableData, setTableData] = useState([])
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editLoading, setEditLoading] = useState(false)
|
||||
@ -182,37 +186,26 @@ const ObjectTable = forwardRef(
|
||||
const rowActions =
|
||||
model.actions?.filter((action) => action.row == true) || []
|
||||
|
||||
const createSkeletonData = useCallback(() => {
|
||||
const createSkeletonData = useCallback((pageNum) => {
|
||||
return Array(pageSize)
|
||||
.fill(null)
|
||||
.map(() => ({
|
||||
_id: `skeleton-${Math.random().toString(36).substring(2, 15)}`,
|
||||
.map((_, index) => ({
|
||||
_id: `skeleton-${pageNum}-${index}`,
|
||||
isSkeleton: true
|
||||
}))
|
||||
}, [pageSize])
|
||||
|
||||
const scrollOperationTimingOut = useRef(false)
|
||||
const scrollOperationTimeout = useRef(null)
|
||||
const createSkeletonPage = useCallback(
|
||||
(pageNum) => ({
|
||||
pageNum,
|
||||
items: createSkeletonData(pageNum),
|
||||
isSkeletonPage: true
|
||||
}),
|
||||
[createSkeletonData]
|
||||
)
|
||||
|
||||
const runScrollOperation = useCallback((operation) => {
|
||||
if (scrollOperationTimingOut.current) return
|
||||
|
||||
scrollOperationTimingOut.current = true
|
||||
operation()
|
||||
|
||||
scrollOperationTimeout.current = setTimeout(() => {
|
||||
scrollOperationTimingOut.current = false
|
||||
scrollOperationTimeout.current = null
|
||||
}, 150)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (scrollOperationTimeout.current) {
|
||||
clearTimeout(scrollOperationTimeout.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
const loadingPagesRef = useRef(new Set())
|
||||
const pendingScrollAnchorRef = useRef(null)
|
||||
|
||||
const renderActions = (objectData) => {
|
||||
return (
|
||||
@ -255,7 +248,7 @@ const ObjectTable = forwardRef(
|
||||
)
|
||||
}
|
||||
|
||||
const fetchData = useCallback(
|
||||
const fetchPage = useCallback(
|
||||
async (pageNum = 1, filter = null, sorter = null) => {
|
||||
if (filter == null) {
|
||||
filter = activeFilterRef.current
|
||||
@ -270,148 +263,266 @@ const ObjectTable = forwardRef(
|
||||
order: sorter.order
|
||||
}
|
||||
}
|
||||
try {
|
||||
const result = await fetchObjects(type, {
|
||||
page: pageNum,
|
||||
limit: pageSize,
|
||||
filter: { ...filter, ...masterFilter },
|
||||
sorter,
|
||||
onDataChange
|
||||
})
|
||||
|
||||
setHasMore(result.hasMore)
|
||||
|
||||
setPages((prev) => {
|
||||
const existingPageIndex = prev.findIndex(
|
||||
(p) => p.pageNum === pageNum
|
||||
)
|
||||
if (existingPageIndex !== -1) {
|
||||
// Update existing page
|
||||
const newPages = [...prev]
|
||||
newPages[existingPageIndex] = { pageNum, items: result.data }
|
||||
return newPages
|
||||
}
|
||||
// If page doesn't exist, return unchanged
|
||||
return prev
|
||||
})
|
||||
|
||||
setLoading(false)
|
||||
setLazyLoading(false)
|
||||
return result.data || []
|
||||
} catch (error) {
|
||||
setPages((prev) =>
|
||||
prev.map((page) => ({
|
||||
...page,
|
||||
items: page.items.filter((item) => !item.isSkeleton)
|
||||
}))
|
||||
)
|
||||
setLoading(false)
|
||||
setLazyLoading(false)
|
||||
throw error
|
||||
}
|
||||
return fetchObjects(type, {
|
||||
page: pageNum,
|
||||
limit: pageSize,
|
||||
filter: { ...filter, ...masterFilter },
|
||||
sorter,
|
||||
onDataChange
|
||||
})
|
||||
},
|
||||
[type, masterFilter, pageSize, onDataChange, fetchObjects]
|
||||
)
|
||||
|
||||
const loadNextPage = useCallback(() => {
|
||||
const highestPage = Math.max(...pages.map((p) => p.pageNum))
|
||||
const nextPage = highestPage + 1
|
||||
if (hasMore && lazyLoading == false) {
|
||||
setPages((prev) => {
|
||||
const filteredPages = prev.map((page) => ({
|
||||
...page,
|
||||
items: page.items.filter((item) => !item.isSkeleton)
|
||||
}))
|
||||
const minPage = Math.min(...filteredPages.map((p) => p.pageNum))
|
||||
const relevantPages = filteredPages.filter(
|
||||
(p) => p.pageNum !== minPage
|
||||
)
|
||||
return [
|
||||
...relevantPages,
|
||||
{ pageNum: nextPage, items: createSkeletonData() }
|
||||
]
|
||||
})
|
||||
fetchData(nextPage)
|
||||
}
|
||||
}, [pages, createSkeletonData, fetchData, hasMore, lazyLoading])
|
||||
const skeletonsHeight = useMemo(() => {
|
||||
return SKELETON_HEIGHT * pageSize
|
||||
}, [pageSize])
|
||||
|
||||
const loadPreviousPage = useCallback(() => {
|
||||
const lowestPage = Math.min(...pages.map((p) => p.pageNum))
|
||||
const prevPage = lowestPage - 1
|
||||
const createPageWindow = useCallback(
|
||||
(loadedPages, direction) => {
|
||||
const sortedPages = [...loadedPages].sort(
|
||||
(a, b) => a.pageNum - b.pageNum
|
||||
)
|
||||
const visiblePages =
|
||||
direction === 'previous'
|
||||
? sortedPages.slice(0, 2)
|
||||
: sortedPages.slice(-2)
|
||||
const firstPage = visiblePages[0]
|
||||
const lastPage = visiblePages[visiblePages.length - 1]
|
||||
const nextPages = []
|
||||
|
||||
if (prevPage > 0 && lazyLoading == false) {
|
||||
setPages((prev) => {
|
||||
const filteredPages = prev.map((page) => ({
|
||||
...page,
|
||||
items: page.items.filter((item) => !item.isSkeleton)
|
||||
}))
|
||||
const maxPage = Math.max(...filteredPages.map((p) => p.pageNum))
|
||||
const relevantPages = filteredPages.filter(
|
||||
(p) => p.pageNum !== maxPage
|
||||
)
|
||||
return [
|
||||
{ pageNum: prevPage, items: createSkeletonData() },
|
||||
...relevantPages
|
||||
]
|
||||
})
|
||||
fetchData(prevPage)
|
||||
}
|
||||
}, [pages, createSkeletonData, fetchData, lazyLoading])
|
||||
if (firstPage?.pageNum > 1) {
|
||||
nextPages.push(createSkeletonPage(firstPage.pageNum - 1))
|
||||
}
|
||||
nextPages.push(...visiblePages)
|
||||
if (lastPage?.hasMore) {
|
||||
nextPages.push(createSkeletonPage(lastPage.pageNum + 1))
|
||||
}
|
||||
|
||||
const scrollDown = useCallback(
|
||||
(target, scrollHeight) => {
|
||||
setTimeout(() => {
|
||||
target.scrollTop = scrollHeight / 2 - target.clientHeight
|
||||
}, 0)
|
||||
setLazyLoading(true)
|
||||
logger.debug('Loading next page...')
|
||||
loadNextPage()
|
||||
return nextPages
|
||||
},
|
||||
[loadNextPage]
|
||||
[createSkeletonPage]
|
||||
)
|
||||
|
||||
const scrollUp = useCallback(
|
||||
(target, scrollHeight) => {
|
||||
setTimeout(() => {
|
||||
target.scrollTop = scrollHeight / 2
|
||||
}, 0)
|
||||
setLazyLoading(true)
|
||||
logger.debug('Loading previous page...')
|
||||
loadPreviousPage()
|
||||
const fetchData = useCallback(
|
||||
async (pageNum = 1, filter = null, sorter = null) => {
|
||||
try {
|
||||
const result = await fetchPage(pageNum, filter, sorter)
|
||||
const loadedPage = {
|
||||
pageNum,
|
||||
items: result.data || [],
|
||||
hasMore: result.hasMore
|
||||
}
|
||||
setPages((prev) =>
|
||||
prev.map((page) =>
|
||||
page.pageNum === pageNum ? loadedPage : page
|
||||
)
|
||||
)
|
||||
setLoading(false)
|
||||
return result.data || []
|
||||
} catch (error) {
|
||||
setLoading(false)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
[loadPreviousPage]
|
||||
[fetchPage]
|
||||
)
|
||||
|
||||
const findRenderedRow = useCallback((scrollTarget, id) => {
|
||||
if (!scrollTarget || id == null) return null
|
||||
return Array.from(
|
||||
scrollTarget.querySelectorAll('tr[data-row-key]')
|
||||
).find((row) => row.getAttribute('data-row-key') === String(id))
|
||||
}, [])
|
||||
|
||||
const captureScrollAnchor = useCallback(
|
||||
(scrollTarget, placeholderPage, direction, currentPages) => {
|
||||
if (!scrollTarget) return null
|
||||
|
||||
const targetTop = scrollTarget.getBoundingClientRect().top
|
||||
const closestRow = (items) => {
|
||||
return items
|
||||
.map((item, index) => {
|
||||
const element = findRenderedRow(scrollTarget, item._id)
|
||||
return element
|
||||
? {
|
||||
index,
|
||||
id: item._id,
|
||||
top: element.getBoundingClientRect().top
|
||||
}
|
||||
: null
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Math.abs(a.top - targetTop) - Math.abs(b.top - targetTop)
|
||||
)[0]
|
||||
}
|
||||
|
||||
const placeholderAnchor = closestRow(placeholderPage.items)
|
||||
const loadedPages = currentPages.filter(
|
||||
(page) => !page.isSkeletonPage
|
||||
)
|
||||
const retainedPage =
|
||||
direction === 'previous'
|
||||
? loadedPages[0]
|
||||
: loadedPages[loadedPages.length - 1]
|
||||
const fallbackAnchor = retainedPage
|
||||
? closestRow(retainedPage.items)
|
||||
: null
|
||||
|
||||
return { placeholderAnchor, fallbackAnchor }
|
||||
},
|
||||
[findRenderedRow]
|
||||
)
|
||||
|
||||
const restoreScrollAnchor = useCallback(
|
||||
(scrollTarget, anchor, loadedPage) => {
|
||||
if (!scrollTarget || !anchor) return
|
||||
|
||||
const loadedItem =
|
||||
loadedPage.items[anchor.placeholderAnchor?.index]
|
||||
const anchorId = loadedItem?._id ?? anchor.fallbackAnchor?.id
|
||||
const previousTop = loadedItem
|
||||
? anchor.placeholderAnchor?.top
|
||||
: anchor.fallbackAnchor?.top
|
||||
if (anchorId == null || previousTop == null) return
|
||||
|
||||
const element = findRenderedRow(scrollTarget, anchorId)
|
||||
if (!element) return
|
||||
const renderedTop = element.getBoundingClientRect().top
|
||||
scrollTarget.scrollTop += renderedTop - previousTop
|
||||
},
|
||||
[findRenderedRow]
|
||||
)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const pendingAnchor = pendingScrollAnchorRef.current
|
||||
if (!pendingAnchor) return
|
||||
pendingScrollAnchorRef.current = null
|
||||
restoreScrollAnchor(
|
||||
pendingAnchor.scrollTarget,
|
||||
pendingAnchor.anchor,
|
||||
pendingAnchor.loadedPage
|
||||
)
|
||||
}, [pages, restoreScrollAnchor])
|
||||
|
||||
const loadBoundaryPage = useCallback(
|
||||
async (pageNum, direction, scrollTarget) => {
|
||||
if (loadingPagesRef.current.size > 0) return
|
||||
|
||||
const currentPages = pagesRef.current
|
||||
const placeholderPage = currentPages.find(
|
||||
(page) => page.pageNum === pageNum && page.isSkeletonPage
|
||||
)
|
||||
if (!placeholderPage) return
|
||||
const scrollAnchor = captureScrollAnchor(
|
||||
scrollTarget,
|
||||
placeholderPage,
|
||||
direction,
|
||||
currentPages
|
||||
)
|
||||
|
||||
loadingPagesRef.current.add(pageNum)
|
||||
setLazyLoading(true)
|
||||
logger.debug(`Loading ${direction} page...`)
|
||||
|
||||
try {
|
||||
const result = await fetchPage(pageNum)
|
||||
const loadedPage = {
|
||||
pageNum,
|
||||
items: result.data || [],
|
||||
hasMore: result.hasMore
|
||||
}
|
||||
|
||||
pendingScrollAnchorRef.current = {
|
||||
scrollTarget,
|
||||
anchor: scrollAnchor,
|
||||
loadedPage
|
||||
}
|
||||
setPages((prev) => {
|
||||
if (
|
||||
!prev.some(
|
||||
(page) => page.pageNum === pageNum && page.isSkeletonPage
|
||||
)
|
||||
) {
|
||||
return prev
|
||||
}
|
||||
|
||||
const loadedPages = prev
|
||||
.filter((page) => !page.isSkeletonPage)
|
||||
.concat(loadedPage)
|
||||
return createPageWindow(loadedPages, direction)
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`Error loading page ${pageNum}:`, error)
|
||||
} finally {
|
||||
loadingPagesRef.current.delete(pageNum)
|
||||
if (loadingPagesRef.current.size === 0) {
|
||||
setLazyLoading(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
captureScrollAnchor,
|
||||
createPageWindow,
|
||||
fetchPage
|
||||
]
|
||||
)
|
||||
|
||||
const loadNextPage = useCallback(
|
||||
(scrollTarget) => {
|
||||
const nextPage = pagesRef.current[pagesRef.current.length - 1]
|
||||
if (nextPage?.isSkeletonPage) {
|
||||
loadBoundaryPage(nextPage.pageNum, 'next', scrollTarget)
|
||||
}
|
||||
},
|
||||
[loadBoundaryPage]
|
||||
)
|
||||
|
||||
const loadPreviousPage = useCallback(
|
||||
(scrollTarget) => {
|
||||
const previousPage = pagesRef.current[0]
|
||||
if (previousPage?.isSkeletonPage) {
|
||||
loadBoundaryPage(previousPage.pageNum, 'previous', scrollTarget)
|
||||
}
|
||||
},
|
||||
[loadBoundaryPage]
|
||||
)
|
||||
|
||||
const handleScroll = useCallback(
|
||||
(e) => {
|
||||
const { target } = e
|
||||
const scrollHeight = target.scrollHeight
|
||||
const scrollTop = target.scrollTop
|
||||
const clientHeight = target.clientHeight
|
||||
const lowestPage = Math.min(...pages.map((p) => p.pageNum))
|
||||
const prevPage = lowestPage - 1
|
||||
const firstPage = pagesRef.current[0]
|
||||
const lastPage = pagesRef.current[pagesRef.current.length - 1]
|
||||
|
||||
// Load more data when scrolling down
|
||||
if (
|
||||
scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD &&
|
||||
hasMore
|
||||
lastPage?.isSkeletonPage &&
|
||||
target.scrollHeight - target.scrollTop - target.clientHeight <=
|
||||
skeletonsHeight + SCROLL_THRESHOLD
|
||||
) {
|
||||
runScrollOperation(() => scrollDown(target, scrollHeight))
|
||||
}
|
||||
|
||||
// Load previous data when scrolling up
|
||||
if (scrollTop < SCROLL_THRESHOLD && prevPage > 0) {
|
||||
runScrollOperation(() => scrollUp(target, scrollHeight))
|
||||
loadNextPage(target)
|
||||
} else if (
|
||||
firstPage?.isSkeletonPage &&
|
||||
target.scrollTop <= skeletonsHeight + SCROLL_THRESHOLD
|
||||
) {
|
||||
loadPreviousPage(target)
|
||||
}
|
||||
},
|
||||
[hasMore, pages, runScrollOperation, scrollDown, scrollUp]
|
||||
[loadNextPage, loadPreviousPage, skeletonsHeight]
|
||||
)
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
setLazyLoading(true)
|
||||
for (let i = 0; i < pagesRef.current.length; i++) {
|
||||
const page = pagesRef.current[i]
|
||||
await fetchData(page.pageNum)
|
||||
try {
|
||||
const loadedPages = pagesRef.current.filter(
|
||||
(page) => !page.isSkeletonPage
|
||||
)
|
||||
for (let i = 0; i < loadedPages.length; i++) {
|
||||
await fetchData(loadedPages[i].pageNum)
|
||||
}
|
||||
} finally {
|
||||
setLazyLoading(false)
|
||||
}
|
||||
}, [fetchData])
|
||||
|
||||
@ -601,24 +712,41 @@ const ObjectTable = forwardRef(
|
||||
|
||||
const loadPage = useCallback(
|
||||
async (pageNum, filter = null, sorter = null) => {
|
||||
// Create initial page with skeletons
|
||||
setPages([{ pageNum: pageNum, items: createSkeletonData() }])
|
||||
setPages([createSkeletonPage(pageNum)])
|
||||
setLoading(true)
|
||||
|
||||
const items = await fetchData(pageNum, filter, sorter)
|
||||
try {
|
||||
const firstResult = await fetchPage(pageNum, filter, sorter)
|
||||
const loadedPages = [
|
||||
{
|
||||
pageNum,
|
||||
items: firstResult.data || [],
|
||||
hasMore: firstResult.hasMore
|
||||
}
|
||||
]
|
||||
|
||||
if (items.length >= 25) {
|
||||
setPages((prev) => {
|
||||
// Remove any existing page with the same pageNum
|
||||
const filtered = prev.filter((p) => p.pageNum !== pageNum + 1)
|
||||
return [
|
||||
...filtered,
|
||||
{ pageNum: pageNum + 1, items: createSkeletonData() }
|
||||
]
|
||||
})
|
||||
await fetchData(pageNum + 1, filter, sorter)
|
||||
if (firstResult.hasMore) {
|
||||
setPages([
|
||||
...loadedPages,
|
||||
createSkeletonPage(pageNum + 1)
|
||||
])
|
||||
const secondResult = await fetchPage(pageNum + 1)
|
||||
loadedPages.push({
|
||||
pageNum: pageNum + 1,
|
||||
items: secondResult.data || [],
|
||||
hasMore: secondResult.hasMore
|
||||
})
|
||||
}
|
||||
|
||||
setPages(createPageWindow(loadedPages, 'next'))
|
||||
} catch (error) {
|
||||
logger.error(`Error loading page ${pageNum}:`, error)
|
||||
setPages([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
},
|
||||
[createSkeletonData, fetchData]
|
||||
[createPageWindow, createSkeletonPage, fetchPage]
|
||||
)
|
||||
|
||||
const loadInitialPage = useCallback(async () => {
|
||||
@ -667,7 +795,6 @@ const ObjectTable = forwardRef(
|
||||
setInitialized(false)
|
||||
setLoading(true)
|
||||
setLazyLoading(false)
|
||||
setHasMore(true)
|
||||
prevValuesRef.current = { type, masterFilter }
|
||||
}
|
||||
}, [type, masterFilter])
|
||||
@ -727,8 +854,6 @@ const ObjectTable = forwardRef(
|
||||
}
|
||||
})
|
||||
|
||||
console.log('filters--', filters)
|
||||
|
||||
setSidebarFilter(next)
|
||||
setPages([])
|
||||
setLoading(true)
|
||||
@ -766,11 +891,6 @@ const ObjectTable = forwardRef(
|
||||
pagesRef.current = pages
|
||||
}, [pages])
|
||||
|
||||
// Flatten pages array for table display
|
||||
useEffect(() => {
|
||||
setTableData(pages.flatMap((page) => page.items))
|
||||
}, [pages])
|
||||
|
||||
useEffect(() => {
|
||||
if (!expandHeight || cards) return
|
||||
|
||||
@ -911,35 +1031,27 @@ const ObjectTable = forwardRef(
|
||||
|
||||
const handleCardsScroll = (e) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target
|
||||
const lowestPage = Math.min(...pages.map((p) => p.pageNum))
|
||||
const prevPage = lowestPage - 1
|
||||
const firstPage = pagesRef.current[0]
|
||||
const lastPage = pagesRef.current[pagesRef.current.length - 1]
|
||||
|
||||
// Load more data when scrolling down
|
||||
if (
|
||||
lastPage?.isSkeletonPage &&
|
||||
scrollHeight - scrollTop - clientHeight < 100 &&
|
||||
hasMore &&
|
||||
!lazyLoading
|
||||
) {
|
||||
setTimeout(() => {
|
||||
e.target.scrollTop = scrollHeight / 2
|
||||
}, 0)
|
||||
setLazyLoading(true)
|
||||
loadNextPage()
|
||||
}
|
||||
|
||||
// Load previous data when scrolling up
|
||||
if (scrollTop < 100 && prevPage > 0 && !lazyLoading) {
|
||||
setTimeout(() => {
|
||||
e.target.scrollTop = scrollHeight / 2
|
||||
}, 0)
|
||||
setLazyLoading(true)
|
||||
} else if (
|
||||
firstPage?.isSkeletonPage &&
|
||||
scrollTop < 100 &&
|
||||
!lazyLoading
|
||||
) {
|
||||
loadPreviousPage()
|
||||
}
|
||||
}
|
||||
|
||||
container.addEventListener('scroll', handleCardsScroll)
|
||||
return () => container.removeEventListener('scroll', handleCardsScroll)
|
||||
}, [cards, pages, hasMore, lazyLoading, loadNextPage, loadPreviousPage])
|
||||
}, [cards, pages, lazyLoading, loadNextPage, loadPreviousPage])
|
||||
|
||||
const renderCards = () => {
|
||||
return (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user