Compare commits
No commits in common. "edce32888094b2002b62a3f36d3551de1c10895d" and "7ca78280b348fe54bc8dc01a10121e9a94e489fe" have entirely different histories.
edce328880
...
7ca78280b3
@ -1,5 +1,13 @@
|
|||||||
import { useEffect, useState, useContext, useMemo } from 'react'
|
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 {
|
import {
|
||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
BarChart,
|
BarChart,
|
||||||
@ -17,9 +25,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,
|
||||||
@ -283,17 +291,15 @@ const HistoryDisplay = ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const chartRows = Array.from(
|
const chartRows = Array.from(
|
||||||
chartData
|
chartData.reduce((acc, item) => {
|
||||||
.reduce((acc, item) => {
|
const existing = acc.get(item.date) || {
|
||||||
const existing = acc.get(item.date) || {
|
date: item.date,
|
||||||
date: item.date,
|
dateFormatted: item.dateFormatted
|
||||||
dateFormatted: item.dateFormatted
|
}
|
||||||
}
|
existing[item.category] = item.value
|
||||||
existing[item.category] = item.value
|
acc.set(item.date, existing)
|
||||||
acc.set(item.date, existing)
|
return acc
|
||||||
return acc
|
}, new Map()).values()
|
||||||
}, 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 = (
|
||||||
@ -328,99 +334,89 @@ const HistoryDisplay = ({
|
|||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
styles={{ body: { padding: '12px', ...styles } }}
|
styles={{ body: { padding: '12px', ...styles } }}
|
||||||
>
|
>
|
||||||
<Flex gap='small' vertical>
|
{!startDate && !endDate && (
|
||||||
{!startDate && !endDate && (
|
<Flex justify='space-between'>
|
||||||
<Flex justify='space-between'>
|
<Flex align='center' gap='5px'>
|
||||||
<Flex align='center' gap='5px'>
|
<Popover
|
||||||
<Popover
|
content={customTimeRangeContent}
|
||||||
content={customTimeRangeContent}
|
trigger='hover'
|
||||||
trigger='hover'
|
arrow={false}
|
||||||
arrow={false}
|
placement='bottomLeft'
|
||||||
placement='bottomLeft'
|
styles={{ body: { borderRadius: '22.5px' } }}
|
||||||
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={[
|
options={[{ label: 'Custom', value: 'custom' }]}
|
||||||
{ 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}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Popover>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
|
||||||
{chartRows.length > 0 && (
|
<Segmented
|
||||||
<div style={{ width: '100%', height: `${height}px` }}>
|
size='small'
|
||||||
<ResponsiveContainer width='100%' height='100%'>
|
options={[
|
||||||
<BarChart data={chartRows}>
|
{ label: '24hr', value: '24hrs' },
|
||||||
<CartesianGrid
|
{ label: '12hr', value: '12hrs' },
|
||||||
strokeDasharray='3 3'
|
{ label: '8hr', value: '8hrs' },
|
||||||
stroke={isDarkMode ? '#303030' : '#f0f0f0'}
|
{ label: '4hr', value: '4hrs' },
|
||||||
/>
|
{ label: '1hr', value: '1hrs' }
|
||||||
<XAxis
|
]}
|
||||||
dataKey='dateFormatted'
|
value={timeRange}
|
||||||
tick={{
|
onChange={setTimeRange}
|
||||||
fill: isDarkMode ? '#d9d9d9' : '#595959',
|
disabled={loading}
|
||||||
fontSize: 12
|
/>
|
||||||
}}
|
</Flex>
|
||||||
/>
|
)}
|
||||||
<YAxis
|
{loading == true && (
|
||||||
tick={{
|
<Flex justify='center' align='center' style={{ height: `${height}px` }}>
|
||||||
fill: isDarkMode ? '#d9d9d9' : '#595959',
|
<LoadingPlaceholder message='Loading history data...' />
|
||||||
fontSize: 12
|
</Flex>
|
||||||
}}
|
)}
|
||||||
/>
|
{chartRows.length > 0 && (
|
||||||
<Tooltip
|
<div style={{ width: '100%', height: `${height}px` }}>
|
||||||
contentStyle={{
|
<ResponsiveContainer width='100%' height='100%'>
|
||||||
backgroundColor: isDarkMode ? '#1f1f1f' : '#ffffff',
|
<BarChart data={chartRows}>
|
||||||
border: `1px solid ${isDarkMode ? '#303030' : '#f0f0f0'}`,
|
<CartesianGrid
|
||||||
borderRadius: 8
|
strokeDasharray='3 3'
|
||||||
}}
|
stroke={isDarkMode ? '#303030' : '#f0f0f0'}
|
||||||
/>
|
/>
|
||||||
<Legend />
|
<XAxis
|
||||||
{modelStats.map((statDef, index) => {
|
dataKey='dateFormatted'
|
||||||
const category = statDef.label || statDef.name
|
tick={{ fill: isDarkMode ? '#d9d9d9' : '#595959', fontSize: 12 }}
|
||||||
return (
|
/>
|
||||||
<Bar
|
<YAxis
|
||||||
key={category}
|
tick={{ fill: isDarkMode ? '#d9d9d9' : '#595959', fontSize: 12 }}
|
||||||
dataKey={category}
|
/>
|
||||||
stackId='history'
|
<Tooltip
|
||||||
fill={colorRange[index] || colors.default}
|
contentStyle={{
|
||||||
/>
|
backgroundColor: isDarkMode ? '#1f1f1f' : '#ffffff',
|
||||||
)
|
border: `1px solid ${isDarkMode ? '#303030' : '#f0f0f0'}`,
|
||||||
})}
|
borderRadius: 8
|
||||||
</BarChart>
|
}}
|
||||||
</ResponsiveContainer>
|
/>
|
||||||
</div>
|
<Legend />
|
||||||
)}
|
{modelStats.map((statDef, index) => {
|
||||||
{loading == false && chartRows.length == 0 && (
|
const category = statDef.label || statDef.name
|
||||||
<Flex
|
return (
|
||||||
justify='center'
|
<Bar
|
||||||
align='center'
|
key={category}
|
||||||
style={{ height: `${height}px` }}
|
dataKey={category}
|
||||||
>
|
stackId='history'
|
||||||
<MissingPlaceholder message='No data available.' />
|
fill={colorRange[index] || colors.default}
|
||||||
</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,7 +3,6 @@ import {
|
|||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useRef,
|
useRef,
|
||||||
useEffect,
|
useEffect,
|
||||||
useLayoutEffect,
|
|
||||||
useState,
|
useState,
|
||||||
useCallback,
|
useCallback,
|
||||||
useMemo,
|
useMemo,
|
||||||
@ -50,7 +49,6 @@ 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()
|
||||||
@ -154,12 +152,10 @@ const ObjectTable = forwardRef(
|
|||||||
// Table state
|
// Table state
|
||||||
const [pages, setPages] = useState([])
|
const [pages, setPages] = useState([])
|
||||||
const pagesRef = useRef(pages)
|
const pagesRef = useRef(pages)
|
||||||
const tableData = useMemo(
|
const [hasMore, setHasMore] = useState(true)
|
||||||
() => 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)
|
||||||
@ -186,26 +182,37 @@ const ObjectTable = forwardRef(
|
|||||||
const rowActions =
|
const rowActions =
|
||||||
model.actions?.filter((action) => action.row == true) || []
|
model.actions?.filter((action) => action.row == true) || []
|
||||||
|
|
||||||
const createSkeletonData = useCallback((pageNum) => {
|
const createSkeletonData = useCallback(() => {
|
||||||
return Array(pageSize)
|
return Array(pageSize)
|
||||||
.fill(null)
|
.fill(null)
|
||||||
.map((_, index) => ({
|
.map(() => ({
|
||||||
_id: `skeleton-${pageNum}-${index}`,
|
_id: `skeleton-${Math.random().toString(36).substring(2, 15)}`,
|
||||||
isSkeleton: true
|
isSkeleton: true
|
||||||
}))
|
}))
|
||||||
}, [pageSize])
|
}, [pageSize])
|
||||||
|
|
||||||
const createSkeletonPage = useCallback(
|
const scrollOperationTimingOut = useRef(false)
|
||||||
(pageNum) => ({
|
const scrollOperationTimeout = useRef(null)
|
||||||
pageNum,
|
|
||||||
items: createSkeletonData(pageNum),
|
|
||||||
isSkeletonPage: true
|
|
||||||
}),
|
|
||||||
[createSkeletonData]
|
|
||||||
)
|
|
||||||
|
|
||||||
const loadingPagesRef = useRef(new Set())
|
const runScrollOperation = useCallback((operation) => {
|
||||||
const pendingScrollAnchorRef = useRef(null)
|
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 renderActions = (objectData) => {
|
const renderActions = (objectData) => {
|
||||||
return (
|
return (
|
||||||
@ -248,7 +255,7 @@ const ObjectTable = forwardRef(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchPage = useCallback(
|
const fetchData = 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
|
||||||
@ -263,266 +270,148 @@ const ObjectTable = forwardRef(
|
|||||||
order: sorter.order
|
order: sorter.order
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fetchObjects(type, {
|
try {
|
||||||
page: pageNum,
|
const result = await fetchObjects(type, {
|
||||||
limit: pageSize,
|
page: pageNum,
|
||||||
filter: { ...filter, ...masterFilter },
|
limit: pageSize,
|
||||||
sorter,
|
filter: { ...filter, ...masterFilter },
|
||||||
onDataChange
|
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
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[type, masterFilter, pageSize, onDataChange, fetchObjects]
|
[type, masterFilter, pageSize, onDataChange, fetchObjects]
|
||||||
)
|
)
|
||||||
|
|
||||||
const skeletonsHeight = useMemo(() => {
|
const loadNextPage = useCallback(() => {
|
||||||
return SKELETON_HEIGHT * pageSize
|
const highestPage = Math.max(...pages.map((p) => p.pageNum))
|
||||||
}, [pageSize])
|
const nextPage = highestPage + 1
|
||||||
|
if (hasMore && lazyLoading == false) {
|
||||||
const createPageWindow = useCallback(
|
setPages((prev) => {
|
||||||
(loadedPages, direction) => {
|
const filteredPages = prev.map((page) => ({
|
||||||
const sortedPages = [...loadedPages].sort(
|
...page,
|
||||||
(a, b) => a.pageNum - b.pageNum
|
items: page.items.filter((item) => !item.isSkeleton)
|
||||||
)
|
}))
|
||||||
const visiblePages =
|
const minPage = Math.min(...filteredPages.map((p) => p.pageNum))
|
||||||
direction === 'previous'
|
const relevantPages = filteredPages.filter(
|
||||||
? sortedPages.slice(0, 2)
|
(p) => p.pageNum !== minPage
|
||||||
: sortedPages.slice(-2)
|
|
||||||
const firstPage = visiblePages[0]
|
|
||||||
const lastPage = visiblePages[visiblePages.length - 1]
|
|
||||||
const nextPages = []
|
|
||||||
|
|
||||||
if (firstPage?.pageNum > 1) {
|
|
||||||
nextPages.push(createSkeletonPage(firstPage.pageNum - 1))
|
|
||||||
}
|
|
||||||
nextPages.push(...visiblePages)
|
|
||||||
if (lastPage?.hasMore) {
|
|
||||||
nextPages.push(createSkeletonPage(lastPage.pageNum + 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextPages
|
|
||||||
},
|
|
||||||
[createSkeletonPage]
|
|
||||||
)
|
|
||||||
|
|
||||||
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 [
|
||||||
return result.data || []
|
...relevantPages,
|
||||||
} catch (error) {
|
{ pageNum: nextPage, items: createSkeletonData() }
|
||||||
setLoading(false)
|
]
|
||||||
throw error
|
})
|
||||||
}
|
fetchData(nextPage)
|
||||||
},
|
}
|
||||||
[fetchPage]
|
}, [pages, createSkeletonData, fetchData, hasMore, lazyLoading])
|
||||||
)
|
|
||||||
|
|
||||||
const findRenderedRow = useCallback((scrollTarget, id) => {
|
const loadPreviousPage = useCallback(() => {
|
||||||
if (!scrollTarget || id == null) return null
|
const lowestPage = Math.min(...pages.map((p) => p.pageNum))
|
||||||
return Array.from(
|
const prevPage = lowestPage - 1
|
||||||
scrollTarget.querySelectorAll('tr[data-row-key]')
|
|
||||||
).find((row) => row.getAttribute('data-row-key') === String(id))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const captureScrollAnchor = useCallback(
|
if (prevPage > 0 && lazyLoading == false) {
|
||||||
(scrollTarget, placeholderPage, direction, currentPages) => {
|
setPages((prev) => {
|
||||||
if (!scrollTarget) return null
|
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])
|
||||||
|
|
||||||
const targetTop = scrollTarget.getBoundingClientRect().top
|
const scrollDown = useCallback(
|
||||||
const closestRow = (items) => {
|
(target, scrollHeight) => {
|
||||||
return items
|
setTimeout(() => {
|
||||||
.map((item, index) => {
|
target.scrollTop = scrollHeight / 2 - target.clientHeight
|
||||||
const element = findRenderedRow(scrollTarget, item._id)
|
}, 0)
|
||||||
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)
|
setLazyLoading(true)
|
||||||
logger.debug(`Loading ${direction} page...`)
|
logger.debug('Loading next page...')
|
||||||
|
loadNextPage()
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[
|
[loadNextPage]
|
||||||
captureScrollAnchor,
|
|
||||||
createPageWindow,
|
|
||||||
fetchPage
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const loadNextPage = useCallback(
|
const scrollUp = useCallback(
|
||||||
(scrollTarget) => {
|
(target, scrollHeight) => {
|
||||||
const nextPage = pagesRef.current[pagesRef.current.length - 1]
|
setTimeout(() => {
|
||||||
if (nextPage?.isSkeletonPage) {
|
target.scrollTop = scrollHeight / 2
|
||||||
loadBoundaryPage(nextPage.pageNum, 'next', scrollTarget)
|
}, 0)
|
||||||
}
|
setLazyLoading(true)
|
||||||
|
logger.debug('Loading previous page...')
|
||||||
|
loadPreviousPage()
|
||||||
},
|
},
|
||||||
[loadBoundaryPage]
|
[loadPreviousPage]
|
||||||
)
|
|
||||||
|
|
||||||
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 firstPage = pagesRef.current[0]
|
const scrollHeight = target.scrollHeight
|
||||||
const lastPage = pagesRef.current[pagesRef.current.length - 1]
|
const scrollTop = target.scrollTop
|
||||||
|
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 (
|
||||||
lastPage?.isSkeletonPage &&
|
scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD &&
|
||||||
target.scrollHeight - target.scrollTop - target.clientHeight <=
|
hasMore
|
||||||
skeletonsHeight + SCROLL_THRESHOLD
|
|
||||||
) {
|
) {
|
||||||
loadNextPage(target)
|
runScrollOperation(() => scrollDown(target, scrollHeight))
|
||||||
} else if (
|
}
|
||||||
firstPage?.isSkeletonPage &&
|
|
||||||
target.scrollTop <= skeletonsHeight + SCROLL_THRESHOLD
|
// Load previous data when scrolling up
|
||||||
) {
|
if (scrollTop < SCROLL_THRESHOLD && prevPage > 0) {
|
||||||
loadPreviousPage(target)
|
runScrollOperation(() => scrollUp(target, scrollHeight))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[loadNextPage, loadPreviousPage, skeletonsHeight]
|
[hasMore, pages, runScrollOperation, scrollDown, scrollUp]
|
||||||
)
|
)
|
||||||
|
|
||||||
const reload = useCallback(async () => {
|
const reload = useCallback(async () => {
|
||||||
setLazyLoading(true)
|
setLazyLoading(true)
|
||||||
try {
|
for (let i = 0; i < pagesRef.current.length; i++) {
|
||||||
const loadedPages = pagesRef.current.filter(
|
const page = pagesRef.current[i]
|
||||||
(page) => !page.isSkeletonPage
|
await fetchData(page.pageNum)
|
||||||
)
|
|
||||||
for (let i = 0; i < loadedPages.length; i++) {
|
|
||||||
await fetchData(loadedPages[i].pageNum)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setLazyLoading(false)
|
|
||||||
}
|
}
|
||||||
}, [fetchData])
|
}, [fetchData])
|
||||||
|
|
||||||
@ -712,41 +601,24 @@ const ObjectTable = forwardRef(
|
|||||||
|
|
||||||
const loadPage = useCallback(
|
const loadPage = useCallback(
|
||||||
async (pageNum, filter = null, sorter = null) => {
|
async (pageNum, filter = null, sorter = null) => {
|
||||||
setPages([createSkeletonPage(pageNum)])
|
// Create initial page with skeletons
|
||||||
setLoading(true)
|
setPages([{ pageNum: pageNum, items: createSkeletonData() }])
|
||||||
|
|
||||||
try {
|
const items = await fetchData(pageNum, filter, sorter)
|
||||||
const firstResult = await fetchPage(pageNum, filter, sorter)
|
|
||||||
const loadedPages = [
|
|
||||||
{
|
|
||||||
pageNum,
|
|
||||||
items: firstResult.data || [],
|
|
||||||
hasMore: firstResult.hasMore
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
if (firstResult.hasMore) {
|
if (items.length >= 25) {
|
||||||
setPages([
|
setPages((prev) => {
|
||||||
...loadedPages,
|
// Remove any existing page with the same pageNum
|
||||||
createSkeletonPage(pageNum + 1)
|
const filtered = prev.filter((p) => p.pageNum !== pageNum + 1)
|
||||||
])
|
return [
|
||||||
const secondResult = await fetchPage(pageNum + 1)
|
...filtered,
|
||||||
loadedPages.push({
|
{ pageNum: pageNum + 1, items: createSkeletonData() }
|
||||||
pageNum: pageNum + 1,
|
]
|
||||||
items: secondResult.data || [],
|
})
|
||||||
hasMore: secondResult.hasMore
|
await fetchData(pageNum + 1, filter, sorter)
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setPages(createPageWindow(loadedPages, 'next'))
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error loading page ${pageNum}:`, error)
|
|
||||||
setPages([])
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[createPageWindow, createSkeletonPage, fetchPage]
|
[createSkeletonData, fetchData]
|
||||||
)
|
)
|
||||||
|
|
||||||
const loadInitialPage = useCallback(async () => {
|
const loadInitialPage = useCallback(async () => {
|
||||||
@ -795,6 +667,7 @@ 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])
|
||||||
@ -854,6 +727,8 @@ const ObjectTable = forwardRef(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('filters--', filters)
|
||||||
|
|
||||||
setSidebarFilter(next)
|
setSidebarFilter(next)
|
||||||
setPages([])
|
setPages([])
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@ -891,6 +766,11 @@ 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
|
||||||
|
|
||||||
@ -1031,27 +911,35 @@ const ObjectTable = forwardRef(
|
|||||||
|
|
||||||
const handleCardsScroll = (e) => {
|
const handleCardsScroll = (e) => {
|
||||||
const { scrollTop, scrollHeight, clientHeight } = e.target
|
const { scrollTop, scrollHeight, clientHeight } = e.target
|
||||||
const firstPage = pagesRef.current[0]
|
const lowestPage = Math.min(...pages.map((p) => p.pageNum))
|
||||||
const lastPage = pagesRef.current[pagesRef.current.length - 1]
|
const prevPage = lowestPage - 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 &&
|
|
||||||
scrollTop < 100 &&
|
// Load previous data when scrolling up
|
||||||
!lazyLoading
|
if (scrollTop < 100 && prevPage > 0 && !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, lazyLoading, loadNextPage, loadPreviousPage])
|
}, [cards, pages, hasMore, lazyLoading, loadNextPage, loadPreviousPage])
|
||||||
|
|
||||||
const renderCards = () => {
|
const renderCards = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user