refactor: use @tanstack/react-virtual for virtualization (#39992)

* refactor(infinite list): use @tanstack/react-virtual for virtualization

Swapping virtualization libraries from `react-window` to
`@tanstack/react-virtual`.

Motivation: we need a completely headless library for maximum
flexibility. `react-window` injects extra DOM elements which makes it
hard to customize styling and placement on elements that are very picky
about their DOM structure, like tables.

* refactor(table editor menu): use new infinite list

* refactor(notifications): use new infinite list

* cleanup(infinite list): remove old infinite list

* refactor(storage menu): use new infinite list & remove react-window deps
This commit is contained in:
Charis authored and GitHub committed 2025-10-30 14:27:53 -04:00
1 parent c3fb31263e
commit cfa59dabc8
14 files changed
+528 -372

No files matched your search

@@ -2,11 +2,11 @@ import { Transition } from '@headlessui/react'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { get, noop, sum } from 'lodash'
import { Upload } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useContextMenu } from 'react-contexify'
import { toast } from 'sonner'
import InfiniteList from 'components/ui/InfiniteList'
import { InfiniteListDefault, LoaderForIconMenuItems } from 'components/ui/InfiniteList'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { BASE_PATH } from 'lib/constants'
@@ -146,6 +146,23 @@ export const FileExplorerColumn = ({
/>
)
const getItemKey = useCallback(
(index: number) => {
const item = columnItems[index]
return item?.id || `file-explorer-item-${index}`
},
[columnItems]
)
const itemProps = useMemo(
() => ({
view: snap.view,
columnIndex: index,
selectedItems,
}),
[snap.view, index, selectedItems]
)
return (
<div
ref={fileExplorerColumnRef}
@@ -213,19 +230,20 @@ export const FileExplorerColumn = ({
)}
{/* Column Interface */}
<InfiniteList
items={columnItems}
itemProps={{
view: snap.view,
columnIndex: index,
selectedItems,
}}
ItemComponent={FileExplorerRow}
getItemSize={(index) => (index !== 0 && index === columnItems.length ? 85 : 37)}
hasNextPage={column.status !== STORAGE_ROW_STATUS.LOADING && column.hasMoreItems}
isLoadingNextPage={column.isLoadingMoreItems}
onLoadNextPage={() => onColumnLoadMore(index, column)}
/>
{columnItems.length > 0 && (
<InfiniteListDefault
className="h-full"
items={columnItems}
itemProps={itemProps}
getItemKey={getItemKey}
getItemSize={(index) => (index !== 0 && index === columnItems.length ? 85 : 37)}
ItemComponent={FileExplorerRow}
LoaderComponent={LoaderForIconMenuItems}
hasNextPage={column.status !== STORAGE_ROW_STATUS.LOADING && column.hasMoreItems}
isLoadingNextPage={column.isLoadingMoreItems}
onLoadNextPage={() => onColumnLoadMore(index, column)}
/>
)}
{/* Drag drop upload CTA for when column is empty */}
{!(snap.isSearching && itemSearchString.length > 0) &&
@@ -18,10 +18,10 @@ import { useContextMenu } from 'react-contexify'
import SVG from 'react-inlinesvg'
import { useParams } from 'common'
import type { ItemRenderer } from 'components/ui/InfiniteList'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { BASE_PATH } from 'lib/constants'
import { formatBytes } from 'lib/helpers'
import type { CSSProperties } from 'react'
import { useStorageExplorerStateSnapshot } from 'state/storage-explorer'
import {
Checkbox,
@@ -46,7 +46,7 @@ import {
STORAGE_VIEWS,
URL_EXPIRY_DURATION,
} from '../Storage.constants'
import { StorageItem, StorageItemWithColumn } from '../Storage.types'
import { StorageItemWithColumn, type StorageItem } from '../Storage.types'
import { FileExplorerRowEditing } from './FileExplorerRowEditing'
import { copyPathToFolder, downloadFile } from './StorageExplorer.utils'
import { useCopyUrl } from './useCopyUrl'
@@ -99,18 +99,22 @@ export const RowIcon = ({
}
interface FileExplorerRowProps {
index: number
item: StorageItem
view: STORAGE_VIEWS
columnIndex: number
selectedItems: StorageItemWithColumn[]
style?: CSSProperties
}
export const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> = ({
export const FileExplorerRow = ({
index: itemIndex,
item,
view = STORAGE_VIEWS.COLUMNS,
columnIndex = 0,
selectedItems = [],
}) => {
style,
}: FileExplorerRowProps) => {
const { ref: projectRef, bucketId } = useParams()
const {
@@ -141,7 +145,7 @@ export const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> =
const isPreviewed = !isEmpty(selectedFilePreview) && isEqual(selectedFilePreview?.id, item.id)
const { can: canUpdateFiles } = useAsyncCheckPermissions(PermissionAction.STORAGE_WRITE, '*')
const onSelectFile = async (columnIndex: number, file: StorageItem) => {
const onSelectFile = async (columnIndex: number) => {
popColumnAtIndex(columnIndex)
popOpenedFoldersAtIndex(columnIndex - 1)
setSelectedFilePreview(itemWithColumnIndex)
@@ -299,11 +303,14 @@ export const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> =
: '100%'
if (item.status === STORAGE_ROW_STATUS.EDITING) {
return <FileExplorerRowEditing view={view} item={item} columnIndex={columnIndex} />
return (
<FileExplorerRowEditing style={style} view={view} item={item} columnIndex={columnIndex} />
)
}
return (
<div
style={style}
className="h-full border-b border-default"
onContextMenu={(event) => {
event.stopPropagation()
@@ -326,7 +333,7 @@ export const FileExplorerRow: ItemRenderer<StorageItem, FileExplorerRowProps> =
if (item.status !== STORAGE_ROW_STATUS.LOADING && !isOpened && !isPreviewed) {
item.type === STORAGE_ROW_TYPES.FOLDER || item.type === STORAGE_ROW_TYPES.BUCKET
? openFolder(columnIndex, item)
: onSelectFile(columnIndex, item)
: onSelectFile(columnIndex)
}
}}
>
@@ -1,5 +1,5 @@
import { has } from 'lodash'
import { useEffect, useRef, useState } from 'react'
import { useEffect, useRef, useState, type CSSProperties } from 'react'
import { useStorageExplorerStateSnapshot } from 'state/storage-explorer'
import { STORAGE_ROW_STATUS, STORAGE_ROW_TYPES, STORAGE_VIEWS } from '../Storage.constants'
@@ -10,12 +10,14 @@ export interface FileExplorerRowEditingProps {
item: StorageItem
view: STORAGE_VIEWS
columnIndex: number
style?: CSSProperties
}
export const FileExplorerRowEditing = ({
item,
view,
columnIndex,
style,
}: FileExplorerRowEditingProps) => {
const { renameFile, renameFolder, addNewFolder, updateRowStatus } =
useStorageExplorerStateSnapshot()
@@ -86,7 +88,10 @@ export const FileExplorerRowEditing = ({
}, [])
return (
<div className="storage-row flex items-center justify-between rounded bg-gray-500">
<div
style={style}
className="storage-row flex items-center justify-between rounded bg-gray-500"
>
<div className="flex h-full flex-grow items-center px-2.5">
<div>
<RowIcon
@@ -1,97 +1,74 @@
import type { CSSProperties } from 'react'
import { memo, useLayoutEffect, useMemo, useRef, useState } from 'react'
import type { ListChildComponentProps } from 'react-window'
import { FixedSizeList as List, areEqual } from 'react-window'
import { memo, useCallback, useMemo } from 'react'
import { InfiniteListDefault } from 'components/ui/InfiniteList'
import type { Bucket } from 'data/storage/buckets-query'
import { cn } from 'ui'
import { BucketRow } from './BucketRow'
type BucketListProps = {
buckets: Bucket[]
selectedBucketId?: string
type VirtualizedBucketRowProps = {
item: Bucket
projectRef?: string
selectedBucketId?: string
style?: CSSProperties
}
const BUCKET_ROW_HEIGHT = 'h-7'
const VirtualizedBucketRow = memo(
({ index, style, data }: ListChildComponentProps<BucketListProps>) => {
const bucket = data.buckets[index]
const isSelected = data.selectedBucketId === bucket.id
({ item, projectRef, selectedBucketId, style }: VirtualizedBucketRowProps) => {
const isSelected = selectedBucketId === item.id
return (
<BucketRow
bucket={bucket}
bucket={item}
isSelected={isSelected}
projectRef={data.projectRef}
projectRef={projectRef}
style={style as CSSProperties}
className={cn(BUCKET_ROW_HEIGHT)}
/>
)
},
(prev, next) => {
if (!areEqual(prev, next)) return false
const prevBucket = prev.data.buckets[prev.index]
const nextBucket = next.data.buckets[next.index]
if (prevBucket !== nextBucket) return false
const wasSelected = prev.data.selectedBucketId === prevBucket.id
const isSelected = next.data.selectedBucketId === nextBucket.id
return wasSelected === isSelected
}
)
VirtualizedBucketRow.displayName = 'VirtualizedBucketRow'
const BucketListVirtualized = ({ buckets, selectedBucketId, projectRef = '' }: BucketListProps) => {
const [listHeight, setListHeight] = useState(500)
const sizerRef = useRef<HTMLDivElement>(null)
useLayoutEffect(() => {
if (sizerRef.current) {
const resizeObserver = new ResizeObserver(([entry]) => {
const { height } = entry.contentRect
setListHeight(height)
})
resizeObserver.observe(sizerRef.current)
setListHeight(sizerRef.current.getBoundingClientRect().height)
return () => {
resizeObserver.disconnect()
}
}
}, [])
const itemData = useMemo<BucketListProps>(
const itemData = useMemo(
() => ({
buckets,
projectRef,
selectedBucketId,
}),
[buckets, projectRef, selectedBucketId]
[projectRef, selectedBucketId]
)
const getItemKey = useCallback(
(index: number) => {
const item = buckets[index]
return item?.id || `bucket-${index}`
},
[buckets]
)
return (
<div ref={sizerRef} className="flex-grow">
<List
itemCount={buckets.length}
itemData={itemData}
itemKey={(index) => buckets[index].id}
height={listHeight}
// itemSize should match the height of BucketRow + any gap/margin
itemSize={28}
width="100%"
>
{VirtualizedBucketRow}
</List>
</div>
<InfiniteListDefault
items={buckets}
itemProps={itemData}
getItemKey={getItemKey}
// Keep in tandem with BUCKET_ROW_HEIGHT
getItemSize={() => 28}
ItemComponent={VirtualizedBucketRow}
// There is no loader because all buckets load from backend at once
LoaderComponent={() => null}
/>
)
}
type BucketListProps = {
buckets: Bucket[]
selectedBucketId?: string
projectRef?: string
}
export const BucketList = ({ buckets, selectedBucketId, projectRef = '' }: BucketListProps) => {
const numBuckets = buckets.length
@@ -74,7 +74,11 @@ export const StorageMenu = () => {
return (
<>
<Menu type="pills" className="h-full flex flex-col" ulClassName="flex flex-col flex-grow">
<Menu
type="pills"
className="h-full flex flex-col"
ulClassName="flex flex-col flex-grow min-h-0"
>
<div className="flex flex-col gap-y-1.5 px-5 pb-6 pt-6 sticky top-0 bg-sidebar">
<CreateBucketModal />
@@ -107,12 +111,12 @@ export const StorageMenu = () => {
</InnerSideBarFilters>
</div>
<div className={cn('flex flex-col', isVirtualized && 'flex-grow')}>
<div className={cn('flex flex-col', isVirtualized && 'flex-grow min-h-0')}>
<div
className={cn(
'flex flex-col mx-3',
isVirtualized
? 'flex-grow'
? 'flex-grow min-h-0'
: isSuccess && filteredBuckets.length > 0
? 'mb-3'
: 'mb-5'
@@ -1,37 +1,36 @@
import dayjs from 'dayjs'
import { Archive, ArchiveRestoreIcon, ExternalLink } from 'lucide-react'
import Link from 'next/link'
import { useEffect, useRef } from 'react'
import { useEffect } from 'react'
import { useInView } from 'react-intersection-observer'
import { Button, cn } from 'ui'
import { Markdown } from 'components/interfaces/Markdown'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import type { ItemRenderer } from 'components/ui/InfiniteList'
import { Notification, NotificationData } from 'data/notifications/notifications-v2-query'
import { useVirtualizerContext } from 'components/ui/InfiniteList'
import { NotificationData, type NotificationsData } from 'data/notifications/notifications-v2-query'
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
import type { Organization } from 'types'
import { CriticalIcon, WarningIcon } from 'ui'
interface NotificationRowProps {
setRowHeight: (idx: number, height: number) => void
index: number
item: NotificationsData[number]
getOrganizationById: (id: number) => Organization
getOrganizationBySlug: (slug: string) => Organization
onUpdateNotificationStatus: (id: string, status: 'archived' | 'seen') => void
queueMarkRead: (id: string) => void
}
const NotificationRow: ItemRenderer<Notification, NotificationRowProps> = ({
const NotificationRow = ({
index,
listRef,
item: notification,
setRowHeight,
getOrganizationById,
getOrganizationBySlug,
onUpdateNotificationStatus,
queueMarkRead,
}) => {
const ref = useRef<HTMLDivElement>(null)
}: NotificationRowProps) => {
const { virtualizer } = useVirtualizerContext()
const { ref: viewRef, inView } = useInView()
const { status, priority } = notification
@@ -55,13 +54,6 @@ const NotificationRow: ItemRenderer<Notification, NotificationRowProps> = ({
console.log('Action', type)
}
useEffect(() => {
if (ref.current) {
listRef?.current?.resetAfterIndex(0)
setRowHeight(index, ref.current.clientHeight)
}
}, [ref])
useEffect(() => {
if (inView && notification.status === 'new') {
queueMarkRead(notification.id)
@@ -70,7 +62,7 @@ const NotificationRow: ItemRenderer<Notification, NotificationRowProps> = ({
return (
<div
ref={ref}
ref={virtualizer.measureElement}
className={cn(
`p-4 flex justify-between gap-x-3 group`,
index !== 0 ? 'border-t border-overlay' : '',
@@ -4,7 +4,7 @@ import { toast } from 'sonner'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import InfiniteList from 'components/ui/InfiniteList'
import { InfiniteListDefault } from 'components/ui/InfiniteList'
import ShimmeringLoader, { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useNotificationsArchiveAllMutation } from 'data/notifications/notifications-v2-archive-all-mutation'
import { useNotificationsV2Query } from 'data/notifications/notifications-v2-query'
@@ -34,11 +34,6 @@ export const NotificationsPopoverV2 = () => {
// Storing in ref as no re-rendering required
const markedRead = useRef<string[]>([])
// [Joshen] Just FYI this variable row heights logic should ideally live in InfiniteList
// but I ran into some infinite loops issues when I was trying to implement it there
// so opting to simplify and implement it here for now
const rowHeights = useRef<{ [key: number]: number }>({})
const { data: organizations } = useOrganizationsQuery({ enabled: open })
const {
data,
@@ -197,20 +192,16 @@ export const NotificationsPopoverV2 = () => {
<div className="flex flex-1 h-[400px] bg-background">
{notifications.length > 0 &&
!(activeTab === 'archived' && snap.filterStatuses.includes('unread')) ? (
<InfiniteList
<InfiniteListDefault
className="w-full"
items={notifications}
ItemComponent={NotificationRow}
LoaderComponent={
<div className="p-4">
LoaderComponent={({ style }) => (
<div style={style} className="p-4">
<ShimmeringLoader />
</div>
}
)}
itemProps={{
setRowHeight: (idx: number, height: number) => {
if (rowHeights.current) {
rowHeights.current = { ...rowHeights.current, [idx]: height }
}
},
getOrganizationById: (id: number) =>
organizations?.find((org) => org.id === id)!,
getOrganizationBySlug: (slug: string) =>
@@ -224,7 +215,7 @@ export const NotificationsPopoverV2 = () => {
}
},
}}
getItemSize={(idx: number) => rowHeights?.current?.[idx] ?? 56}
getItemSize={() => 56}
hasNextPage={hasNextPage}
isLoadingNextPage={isFetchingNextPage}
onLoadNextPage={() => fetchNextPage()}
@@ -15,7 +15,6 @@ import {
getEntityLintDetails,
} from 'components/interfaces/TableGridEditor/TableEntity.utils'
import { EntityTypeIcon } from 'components/ui/EntityTypeIcon'
import type { ItemRenderer } from 'components/ui/InfiniteList'
import { InlineLink } from 'components/ui/InlineLink'
import { getTableDefinition } from 'data/database/table-definition-query'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
@@ -28,6 +27,7 @@ import { fetchAllTableRows } from 'data/table-rows/table-rows-query'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { formatSql } from 'lib/formatSql'
import type { CSSProperties } from 'react'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { createTabId, useTabsStateSnapshot } from 'state/tabs'
import {
@@ -52,8 +52,10 @@ import {
export interface EntityListItemProps {
id: number | string
projectRef: string
item: Entity
isLocked: boolean
isActive?: boolean
style?: CSSProperties
onExportCLI: () => void
}
@@ -62,14 +64,15 @@ function isTableLikeEntityListItem(entity: { type?: string }) {
return entity?.type === ENTITY_TYPE.TABLE || entity?.type === ENTITY_TYPE.PARTITIONED_TABLE
}
const EntityListItem: ItemRenderer<Entity, EntityListItemProps> = ({
const EntityListItem = ({
id,
projectRef,
item: entity,
isLocked,
isActive: _isActive,
style,
onExportCLI,
}) => {
}: EntityListItemProps) => {
const { data: project } = useSelectedProjectQuery()
const snap = useTableEditorStateSnapshot()
const { selectedSchema } = useQuerySchemaState()
@@ -236,6 +239,7 @@ const EntityListItem: ItemRenderer<Entity, EntityListItemProps> = ({
return (
<EditorTablePageLink
title={entity.name}
style={style}
id={String(entity.id)}
href={`/project/${projectRef}/editor/${entity.id}?schema=${entity.schema}&${LOAD_TAB_FROM_CACHE_PARAM}=true`}
role="button"
@@ -1,6 +1,6 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Filter, Plus } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useParams } from 'common'
import { useBreakpoint } from 'common/hooks/useBreakpoint'
@@ -11,7 +11,7 @@ import { ProtectedSchemaWarning } from 'components/interfaces/Database/Protected
import EditorMenuListSkeleton from 'components/layouts/TableEditorLayout/EditorMenuListSkeleton'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import InfiniteList from 'components/ui/InfiniteList'
import { InfiniteListDefault, LoaderForIconMenuItems } from 'components/ui/InfiniteList'
import SchemaSelector from 'components/ui/SchemaSelector'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query'
@@ -104,15 +104,36 @@ export const TableEditorMenu = () => {
const tableEditorTabsCleanUp = useTableEditorTabsCleanUp()
const onSelectExportCLI = async (id: number) => {
const table = await getTableEditor({
id: id,
projectRef,
connectionString: project?.connectionString,
})
const supaTable = table && parseSupaTable(table)
setTableToExport(supaTable)
}
const onSelectExportCLI = useCallback(
async (id: number) => {
const table = await getTableEditor({
id: id,
projectRef,
connectionString: project?.connectionString,
})
const supaTable = table && parseSupaTable(table)
setTableToExport(supaTable)
},
[project?.connectionString, projectRef]
)
const getItemKey = useCallback(
(index: number) => {
const item = entityTypes?.[index]
return item?.id ? String(item.id) : `table-editor-entity-${index}`
},
[entityTypes]
)
const entityProps = useMemo(
() => ({
projectRef: project?.ref!,
id: Number(id),
isLocked: isSchemaLocked,
onExportCLI: () => onSelectExportCLI(Number(id)),
}),
[project?.ref, id, isSchemaLocked, onSelectExportCLI]
)
useEffect(() => {
// Clean up tabs + recent items for any tables that might have been removed outside of the dashboard session
@@ -164,7 +185,7 @@ export const TableEditorMenu = () => {
)}
</div>
</div>
<div className="flex flex-auto flex-col gap-2 pb-4">
<div className="grow min-h-0 flex flex-col gap-2 pb-4">
<InnerSideBarFilters className="mx-2">
<InnerSideBarFilterSearchInput
autoFocus={!isMobile}
@@ -261,21 +282,20 @@ export const TableEditorMenu = () => {
/>
)}
{(entityTypes?.length ?? 0) > 0 && (
<div className="flex flex-1 flex-grow" data-testid="tables-list">
<InfiniteList
items={entityTypes}
// @ts-expect-error
<div className="flex flex-1 min-h-0 w-full" data-testid="tables-list">
<InfiniteListDefault
className="h-full w-full"
items={entityTypes!}
ItemComponent={EntityListItem}
itemProps={{
projectRef: project?.ref!,
id: Number(id),
isSchemaLocked,
onExportCLI: () => onSelectExportCLI(Number(id)),
}}
getItemSize={() => 28}
LoaderComponent={LoaderForIconMenuItems}
itemProps={entityProps}
getItemKey={getItemKey}
getItemSize={(index) =>
index !== 0 && index === entityTypes!.length ? 85 : 28
}
hasNextPage={hasNextPage}
isLoadingNextPage={isFetchingNextPage}
onLoadNextPage={() => fetchNextPage()}
onLoadNextPage={fetchNextPage}
/>
</div>
)}
+349 -133
View File
@@ -1,157 +1,373 @@
import { propsAreEqual } from 'lib/helpers'
import memoize from 'memoize-one'
import { CSSProperties, ComponentType, MutableRefObject, ReactNode, memo, useRef } from 'react'
import AutoSizer from 'react-virtualized-auto-sizer'
import { VariableSizeList, areEqual } from 'react-window'
import InfiniteLoader from 'react-window-infinite-loader'
import { Skeleton } from 'ui'
import { Virtualizer, useVirtualizer } from '@tanstack/react-virtual'
import {
CSSProperties,
ComponentPropsWithRef,
ComponentType,
ElementType,
ReactNode,
Ref,
createContext,
createElement,
memo,
useContext,
useEffect,
useMemo,
useRef,
type ComponentProps,
type PropsWithChildren,
} from 'react'
/**
* Note that the loading more logic of this component works best with a cursor-based
* pagination API such that each payload response from the API returns a structure like
* { cursor, items, hasNext, hasPrevious }
*/
import { Skeleton, cn } from 'ui'
const createItemData = memoize((items, itemProps) => ({ items, ...itemProps }))
// Regular memo erases generics, so this helper adds them back
const typedMemo = <Component extends (props: any) => JSX.Element | null>(
component: Component,
propsAreEqual?: (
prevProps: Readonly<Parameters<Component>[0]>,
nextProps: Readonly<Parameters<Component>[0]>
) => boolean
) => memo(component, propsAreEqual) as unknown as Component & { displayName?: string }
export type ItemRenderer<T, P> = ComponentType<
{
item: T
listRef: MutableRefObject<VariableSizeList<any> | null | undefined>
index: number
} & P
const createStyleObject = ({ size, start }: { size: number; start: number }): CSSProperties => ({
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${size}px`,
transform: `translateY(${start}px)`,
})
type VirtualizerInstance = Virtualizer<Element, Element>
type VirtualItems = ReturnType<VirtualizerInstance['getVirtualItems']>
type VirtualizerContextValue = {
virtualizer: VirtualizerInstance
virtualItems: VirtualItems
}
const VirtualizerContext = createContext<VirtualizerContextValue | null>(null)
export const VirtualizerProvider = ({
children,
value,
}: PropsWithChildren<{ value: VirtualizerContextValue }>) => {
return <VirtualizerContext.Provider value={value}>{children}</VirtualizerContext.Provider>
}
export const useVirtualizerContext = () => {
const context = useContext(VirtualizerContext)
if (!context) {
throw new Error('useVirtualizerContext must be used within a VirtualizerProvider')
}
return context
}
type ExtractRefType<Elem extends ElementType> =
ComponentPropsWithRef<Elem> extends { ref?: Ref<infer RefType> } ? RefType : never
type ExtractScrollElementFromRefComponent<RefComponent extends ElementType> = Extract<
ExtractRefType<RefComponent>,
Element
>
export interface ItemProps<T, P> {
data: {
items: T[]
itemProps: P
ItemComponent: ItemRenderer<T, P>
listRef: MutableRefObject<VariableSizeList<any> | null | undefined>
LoaderComponent?: ReactNode
}
index: number
style: CSSProperties
}
type ScrollWrapperComponentConstraints<Component extends ElementType> =
ComponentPropsWithRef<Component> extends { className?: string }
? ComponentPropsWithRef<Component> extends { children?: ReactNode | ReactNode[] }
? ExtractRefType<Component> extends never
? { ERROR_WRAPPER_COMPONENT_REQUIRES_REF_SUPPORT: never }
: ExtractRefType<Component> extends Element
? {}
: { ERROR_WRAPPER_COMPONENT_REF_MUST_EXTEND_ELEMENT: never }
: { ERROR_WRAPPER_COMPONENT_REQUIRES_CHILDREN: never }
: { ERROR_WRAPPER_COMPONENT_REQUIRES_CLASSNAME: never }
export interface InfiniteListProps<T, P> {
items?: T[]
itemProps?: P
type InfiniteListWrapperProps<Item, Component extends ElementType = 'div'> = {
className?: string
items: Item[]
getItemKey?: (index: number) => string
getItemSize: (index: number) => number
hasNextPage?: boolean
isLoadingNextPage?: boolean
getItemSize?: (index: number) => number
onLoadNextPage?: () => void
ItemComponent?: ItemRenderer<T, P>
LoaderComponent?: ReactNode
}
Component?: Component
} & ScrollWrapperComponentConstraints<Component>
const Item = memo(<T, P>({ data, index, style }: ItemProps<T, P>) => {
const { items, itemProps, ItemComponent, listRef, LoaderComponent } = data
const item = index < items.length ? items[index] : undefined
return item ? (
<div style={style}>
<ItemComponent index={index} item={item} listRef={listRef} {...itemProps} />
</div>
) : LoaderComponent !== undefined ? (
<div style={style}>{LoaderComponent}</div>
) : (
<div className="space-y-1 my-1" style={style}>
<div className="flex flex-col gap-y-1">
<div className="flex flex-row h-6 px-4 items-center gap-2">
<Skeleton className="h-4 w-5" />
<Skeleton className="w-40 h-4" />
</div>
<div className="flex flex-row h-6 px-4 items-center gap-2">
<Skeleton className="h-4 w-5" />
<Skeleton className="w-32 h-4" />
</div>
<div className="flex flex-row h-6 px-4 items-center gap-2 opacity-75">
<Skeleton className="h-4 w-5" />
<Skeleton className="w-20 h-4" />
</div>
<div className="flex flex-row h-6 px-4 items-center gap-2 opacity-50">
<Skeleton className="h-4 w-5" />
<Skeleton className="w-40 h-4" />
</div>
<div className="flex flex-row h-6 px-4 items-center gap-2 opacity-25">
<Skeleton className="h-4 w-5" />
<Skeleton className="w-20 h-4" />
</div>
</div>
</div>
)
}, areEqual)
Item.displayName = 'Item'
function InfiniteList<T, P>({
items = [],
itemProps,
export const InfiniteListScrollWrapper = <Item, Wrapper extends ElementType = 'div'>({
children,
items,
getItemKey,
getItemSize,
hasNextPage = false,
isLoadingNextPage = false,
getItemSize = () => 40,
onLoadNextPage = () => {},
ItemComponent = () => null,
LoaderComponent,
}: InfiniteListProps<T, P>) {
const listRef = useRef<VariableSizeList<any> | null>()
className,
Component,
}: PropsWithChildren<InfiniteListWrapperProps<Item, Wrapper>>) => {
const scrollRef = useRef<ExtractScrollElementFromRefComponent<Wrapper> | null>(null)
// Only load 1 page of items at a time
// Pass an empty callback to InfiniteLoader in case it asks to load more than once
const loadMoreItems = isLoadingNextPage ? () => {} : onLoadNextPage
const rowVirtualizer = useVirtualizer<ExtractScrollElementFromRefComponent<Wrapper>, Element>({
count: hasNextPage ? items.length + 1 : items.length,
getScrollElement: () => scrollRef.current,
getItemKey,
estimateSize: getItemSize,
overscan: 5,
})
// Every row is loaded except for our loading indicator row
const isItemLoaded = (index: number) => {
return !hasNextPage || index < items.length
const virtualItems = rowVirtualizer.getVirtualItems()
const virtualizerContextValue = useMemo(
() => ({
virtualizer: rowVirtualizer as unknown as Virtualizer<Element, Element>,
virtualItems,
}),
[rowVirtualizer, virtualItems]
)
useEffect(() => {
const lastItem = virtualItems[virtualItems.length - 1]
if (!lastItem) return
if (lastItem.index >= items.length - 1 && hasNextPage && !isLoadingNextPage) {
onLoadNextPage()
}
}, [virtualItems, items.length, hasNextPage, isLoadingNextPage, onLoadNextPage])
const WrapperToRender: Wrapper = Component ?? ('div' as Wrapper)
const wrapperProps = {
ref: (node: ExtractScrollElementFromRefComponent<Wrapper> | null) => {
scrollRef.current = node
},
className: cn('overflow-auto', className),
children,
} as ComponentPropsWithRef<Wrapper>
return (
<VirtualizerProvider value={virtualizerContextValue}>
<WrapperToRender {...wrapperProps} />
</VirtualizerProvider>
)
}
type ComponentWithStylePropConstraint<Component extends ElementType> =
ComponentProps<Component> extends { style?: CSSProperties }
? {}
: { ERROR_SIZER_COMPONENT_MUST_TAKE_STYLE_PROP: never }
type InfiniteListSizerProps = {
Component?: ElementType
} & ComponentWithStylePropConstraint<ElementType>
export const InfiniteListSizer = ({
children,
Component = 'div',
}: PropsWithChildren<InfiniteListSizerProps>) => {
const { virtualizer } = useVirtualizerContext()
return (
<Component
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{children}
</Component>
)
}
type RowComponentBaseProps<Item> = {
index: number
item: Item
style?: CSSProperties
}
type InfiniteListItemProps<
Item,
ExtraProps extends object = Record<string, never>,
RowComponent extends ComponentType<RowComponentBaseProps<Item> & ExtraProps> = ComponentType<
RowComponentBaseProps<Item> & ExtraProps
>,
> = {
index: number
start: number
size: number
item: Item
itemProps?: ExtraProps
ItemComponent: RowComponent
}
const MemoizedInfiniteListItem = typedMemo(
<
Item,
ExtraProps extends object = Record<string, never>,
RowComponent extends ComponentType<RowComponentBaseProps<Item> & ExtraProps> = ComponentType<
RowComponentBaseProps<Item> & ExtraProps
>,
>({
index,
start,
size,
item,
itemProps,
ItemComponent,
}: InfiniteListItemProps<Item, ExtraProps, RowComponent>) => {
const styleObject = useMemo<CSSProperties>(
() => createStyleObject({ size, start }),
[size, start]
)
const baseProps = useMemo<RowComponentBaseProps<Item>>(
() => ({
index,
item,
style: styleObject,
}),
[index, item, styleObject]
)
const combinedProps = useMemo(
() =>
({
...baseProps,
...(itemProps ?? ({} as ExtraProps)),
}) as RowComponentBaseProps<Item> & ExtraProps,
[baseProps, itemProps]
)
// Not JSX to avoid type error with generic function component
return createElement(ItemComponent, combinedProps)
}
)
MemoizedInfiniteListItem.displayName = 'MemoizedInfiniteListItem'
const itemCount = hasNextPage ? items.length + 1 : items.length
const itemData = createItemData(items, { itemProps, ItemComponent, LoaderComponent, listRef })
type InfiniteListItemsProps<
Item,
ExtraProps extends object = Record<string, never>,
RowComponent extends ComponentType<RowComponentBaseProps<Item> & ExtraProps> = ComponentType<
RowComponentBaseProps<Item> & ExtraProps
>,
> = {
items: Item[]
itemProps?: ExtraProps
ItemComponent: RowComponent
LoaderComponent: ComponentType<{ style?: CSSProperties }>
}
export const InfiniteListItems = <
Item,
ExtraProps extends object = Record<string, never>,
RowComponent extends ComponentType<RowComponentBaseProps<Item> & ExtraProps> = ComponentType<
RowComponentBaseProps<Item> & ExtraProps
>,
>({
items,
itemProps,
ItemComponent,
LoaderComponent,
}: InfiniteListItemsProps<Item, ExtraProps, RowComponent>) => {
const { virtualItems } = useVirtualizerContext()
return (
<>
<div className="flex-grow">
<AutoSizer>
{({ height, width }: { height: number; width: number }) => (
<InfiniteLoader
itemCount={itemCount}
isItemLoaded={isItemLoaded}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<VariableSizeList
ref={(refy) => {
ref(refy)
listRef.current = refy
}}
height={height ?? 0}
width={width ?? 0}
itemCount={itemCount}
itemData={itemData}
itemSize={getItemSize}
onItemsRendered={onItemsRendered}
>
{Item}
</VariableSizeList>
)}
</InfiniteLoader>
)}
</AutoSizer>
</div>
<div
style={{
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
pointerEvents: 'none', //https://github.com/bvaughn/react-window/issues/455
}}
/>
{virtualItems.map((virtualRow) => {
const isLoaderRow = virtualRow.index > items.length - 1
const item = items[virtualRow.index]
return isLoaderRow ? (
<LoaderComponent
key={virtualRow.index}
style={createStyleObject({ size: virtualRow.size, start: virtualRow.start })}
/>
) : (
// Not JSX so we can pass type arguments to the generic function component
createElement(MemoizedInfiniteListItem<Item, ExtraProps>, {
key: virtualRow.index,
index: virtualRow.index,
start: virtualRow.start,
size: virtualRow.size,
item,
itemProps,
ItemComponent,
})
)
})}
</>
)
}
// memo erases generics so this magic is needed
export default memo(InfiniteList, propsAreEqual) as typeof InfiniteList
type InfiniteListDefaultProps<Item, ItemComponentProps extends object = Record<string, never>> = {
className?: string
items: Item[]
itemProps?: ItemComponentProps
getItemKey?: (index: number) => string
getItemSize: (index: number) => number
hasNextPage?: boolean
isLoadingNextPage?: boolean
onLoadNextPage?: () => void
ItemComponent: ComponentType<RowComponentBaseProps<Item> & ItemComponentProps>
LoaderComponent: ComponentType<{ style?: CSSProperties }>
}
export const InfiniteListDefault = <
Item,
ItemComponentProps extends object = Record<string, never>,
>({
className,
items,
itemProps,
getItemKey,
getItemSize,
hasNextPage = false,
isLoadingNextPage = false,
onLoadNextPage = () => {},
ItemComponent,
LoaderComponent,
}: InfiniteListDefaultProps<Item, ItemComponentProps>) => {
return (
<InfiniteListScrollWrapper
className={className}
items={items}
getItemKey={getItemKey}
getItemSize={getItemSize}
hasNextPage={hasNextPage}
isLoadingNextPage={isLoadingNextPage}
onLoadNextPage={onLoadNextPage}
>
<InfiniteListSizer>
<InfiniteListItems
items={items}
itemProps={itemProps}
ItemComponent={ItemComponent}
LoaderComponent={LoaderComponent}
/>
</InfiniteListSizer>
</InfiniteListScrollWrapper>
)
}
export const LoaderForIconMenuItems = ({ style }: { style?: CSSProperties }) => (
<div className="space-y-1 my-1" style={style}>
<div className="flex flex-col gap-y-1">
<div className="flex flex-row h-6 px-4 items-center gap-2">
<Skeleton className="h-4 w-5" />
<Skeleton className="w-40 h-4" />
</div>
<div className="flex flex-row h-6 px-4 items-center gap-2">
<Skeleton className="h-4 w-5" />
<Skeleton className="w-32 h-4" />
</div>
<div className="flex flex-row h-6 px-4 items-center gap-2 opacity-75">
<Skeleton className="h-4 w-5" />
<Skeleton className="w-20 h-4" />
</div>
<div className="flex flex-row h-6 px-4 items-center gap-2 opacity-50">
<Skeleton className="h-4 w-5" />
<Skeleton className="w-40 h-4" />
</div>
<div className="flex flex-row h-6 px-4 items-center gap-2 opacity-25">
<Skeleton className="h-4 w-5" />
<Skeleton className="w-20 h-4" />
</div>
</div>
</div>
)
+1
View File
@@ -0,0 +1 @@
export type PlainObject<Value = unknown> = Record<string | number | symbol, Value>
+1 -6
View File
@@ -67,6 +67,7 @@
"@tanstack/react-query": "4.35.7",
"@tanstack/react-query-devtools": "4.35.7",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
"@uidotdev/usehooks": "^2.4.1",
"@vercel/functions": "^2.1.0",
"@vitejs/plugin-react": "^4.3.4",
@@ -125,9 +126,6 @@
"react-resizable": "3.0.5",
"react-simple-maps": "4.0.0-beta.6",
"react-use": "^17.5.0",
"react-virtualized-auto-sizer": "^1.0.20",
"react-window": "^1.8.6",
"react-window-infinite-loader": "^1.0.7",
"reactflow": "^11.10.1",
"recharts": "^2.8.0",
"remark-gfm": "^3.0.1",
@@ -175,9 +173,6 @@
"@types/react-dom": "catalog:",
"@types/react-grid-layout": "^1.3.0",
"@types/react-simple-maps": "^3.0.1",
"@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5",
"@types/react-window-infinite-loader": "^1.0.5",
"@types/recharts": "^1.8.23",
"@types/sqlstring": "^2.3.0",
"@types/uuid": "^8.3.4",
@@ -21,7 +21,7 @@ const PageLayout: NextPageWithLayout = () => {
if (!project || !projectRef) return null
return (
<div className="storage-container flex flex-grow p-4">
<div className="storage-container flex flex-grow min-h-0 p-4">
{isError && <StorageBucketsError error={error as any} />}
{isSuccess ? (
+12 -86
View File
@@ -855,6 +855,9 @@ importers:
'@tanstack/react-table':
specifier: ^8.21.3
version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tanstack/react-virtual':
specifier: ^3.13.12
version: 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@uidotdev/usehooks':
specifier: ^2.4.1
version: 2.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -1029,15 +1032,6 @@ importers:
react-use:
specifier: ^17.5.0
version: 17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-virtualized-auto-sizer:
specifier: ^1.0.20
version: 1.0.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-window:
specifier: ^1.8.6
version: 1.8.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-window-infinite-loader:
specifier: ^1.0.7
version: 1.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
reactflow:
specifier: ^11.10.1
version: 11.10.1(@types/react@18.3.3)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -1174,15 +1168,6 @@ importers:
'@types/react-simple-maps':
specifier: ^3.0.1
version: 3.0.4
'@types/react-virtualized-auto-sizer':
specifier: ^1.0.1
version: 1.0.1
'@types/react-window':
specifier: ^1.8.5
version: 1.8.6
'@types/react-window-infinite-loader':
specifier: ^1.0.5
version: 1.0.7
'@types/recharts':
specifier: ^1.8.23
version: 1.8.25
@@ -9073,8 +9058,8 @@ packages:
react: '>=16.8'
react-dom: '>=16.8'
'@tanstack/react-virtual@3.13.6':
resolution: {integrity: sha512-WT7nWs8ximoQ0CDx/ngoFP7HbQF9Q2wQe4nh2NB+u2486eX3nZRE40P9g6ccCVq7ZfTSH5gFOuCoVH5DLNS/aA==}
'@tanstack/react-virtual@3.13.12':
resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
@@ -9160,8 +9145,8 @@ packages:
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
engines: {node: '>=12'}
'@tanstack/virtual-core@3.13.6':
resolution: {integrity: sha512-cnQUeWnhNP8tJ4WsGcYiX24Gjkc9ALstLbHcBj1t3E7EimN6n6kHH+DPV4PpDnuw00NApQp+ViojMj1GRdwYQg==}
'@tanstack/virtual-core@3.13.12':
resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==}
'@tanstack/virtual-file-routes@1.114.12':
resolution: {integrity: sha512-aR13V1kSE/kUkP4a8snmqvj82OUlR5Q/rzxICmObLCsERGfzikUc4wquOy1d/RzJgsLb8o+FiOjSWynt4T7Jhg==}
@@ -9601,15 +9586,6 @@ packages:
peerDependencies:
'@types/react': '*'
'@types/react-virtualized-auto-sizer@1.0.1':
resolution: {integrity: sha512-GH8sAnBEM5GV9LTeiz56r4ZhMOUSrP43tAQNSRVxNexDjcNKLCEtnxusAItg1owFUFE6k0NslV26gqVClVvong==}
'@types/react-window-infinite-loader@1.0.7':
resolution: {integrity: sha512-+CG6szhzP7akjbZ5v85yyZqS78XsJ/VVjccNFPR0bGFQf8jzL238aoNJYBP7qZDbcROIx6tiFJcT9PHzw8LAVg==}
'@types/react-window@1.8.6':
resolution: {integrity: sha512-AVJr3A5rIO9dQQu5TwTN0lP2c1RtuqyyZGCt7PGP8e5gUpn1PuQRMJb/u3UpdbwTHh4wbEi33UMW5NI0IXt1Mg==}
'@types/react@18.3.3':
resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==}
@@ -17111,26 +17087,6 @@ packages:
react: '*'
react-dom: '*'
react-virtualized-auto-sizer@1.0.20:
resolution: {integrity: sha512-OdIyHwj4S4wyhbKHOKM1wLSj/UDXm839Z3Cvfg2a9j+He6yDa6i5p0qQvEiCnyQlGO/HyfSnigQwuxvYalaAXA==}
peerDependencies:
react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc
react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc
react-window-infinite-loader@1.0.9:
resolution: {integrity: sha512-5Hg89IdU4Vrp0RT8kZYKeTIxWZYhNkVXeI1HbKo01Vm/Z7qztDvXljwx16sMzsa9yapRJQW3ODZfMUw38SOWHw==}
engines: {node: '>8.0.0'}
peerDependencies:
react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0
react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0
react-window@1.8.9:
resolution: {integrity: sha512-+Eqx/fj1Aa5WnhRfj9dJg4VYATGwIUP2ItwItiJ6zboKWA6EX3lYDAXfGF2hyNqplEprhbtjbipiADEcwQ823Q==}
engines: {node: '>8.0.0'}
peerDependencies:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
react-wrap-balancer@1.1.0:
resolution: {integrity: sha512-EhF3jOZm5Fjx+Cx41e423qOv2c2aOvXAtym2OHqrGeMUnwERIyNsRBgnfT3plB170JmuYvts8K2KSPEIerKr5A==}
peerDependencies:
@@ -22705,7 +22661,7 @@ snapshots:
'@graphql-tools/executor-legacy-ws@1.1.17(graphql@16.11.0)':
dependencies:
'@graphql-tools/utils': 10.8.6(graphql@16.11.0)
'@types/ws': 8.5.10
'@types/ws': 8.18.1
graphql: 16.11.0
isomorphic-ws: 5.0.0(ws@8.18.3)
tslib: 2.8.1
@@ -22964,7 +22920,7 @@ snapshots:
'@floating-ui/react': 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-aria/focus': 3.20.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-aria/interactions': 3.25.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tanstack/react-virtual': 3.13.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tanstack/react-virtual': 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
use-sync-external-store: 1.5.0(react@18.3.1)
@@ -28968,9 +28924,9 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@tanstack/react-virtual@3.13.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@tanstack/react-virtual@3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@tanstack/virtual-core': 3.13.6
'@tanstack/virtual-core': 3.13.12
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
@@ -29192,7 +29148,7 @@ snapshots:
'@tanstack/table-core@8.21.3': {}
'@tanstack/virtual-core@3.13.6': {}
'@tanstack/virtual-core@3.13.12': {}
'@tanstack/virtual-file-routes@1.114.12': {}
@@ -29723,19 +29679,6 @@ snapshots:
dependencies:
'@types/react': 18.3.3
'@types/react-virtualized-auto-sizer@1.0.1':
dependencies:
'@types/react': 18.3.3
'@types/react-window-infinite-loader@1.0.7':
dependencies:
'@types/react': 18.3.3
'@types/react-window': 1.8.6
'@types/react-window@1.8.6':
dependencies:
'@types/react': 18.3.3
'@types/react@18.3.3':
dependencies:
'@types/prop-types': 15.7.8
@@ -39021,23 +38964,6 @@ snapshots:
ts-easing: 0.2.0
tslib: 2.6.2
react-virtualized-auto-sizer@1.0.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-window-infinite-loader@1.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-window@1.8.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.26.10
memoize-one: 5.2.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-wrap-balancer@1.1.0(react@18.3.1):
dependencies:
react: 18.3.1