mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
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)
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
+7
-2
@@ -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'
|
||||
|
||||
+9
-17
@@ -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' : '',
|
||||
|
||||
+7
-16
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
export type PlainObject<Value = unknown> = Record<string | number | symbol, Value>
|
||||
@@ -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 ? (
|
||||
|
||||
Generated
+12
-86
@@ -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
|
||||
|
||||
Reference in new issue
Block a user