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