/* eslint-disable react/prop-types */ import React, { useCallback, useMemo, memo, useRef, useEffect } from 'react'; import { FixedSizeList as List, areEqual } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FadeInPlaceholder, css, useId, VisuallyHidden, } from '@mongodb-js/compass-components'; import { PlaceholderItem } from './placeholder-item'; import { MAX_COLLECTION_PLACEHOLDER_ITEMS, DATABASE_ROW_HEIGHT, } from './constants'; import { DatabaseItem } from './database-item'; import { CollectionItem } from './collection-item'; import type { Actions } from './constants'; import { useVirtualNavigationTree } from './use-virtual-navigation-tree'; import type { NavigationTreeData } from './use-virtual-navigation-tree'; import { DatabasesPlaceholder } from './databases-placeholder'; type Collection = { _id: string; name: string; type: string; }; type Database = { _id: string; name: string; collectionsStatus: string; collectionsLength: number; collections: Collection[]; }; type PlaceholderTreeItem = { key: string; type: 'placeholder'; id?: string; }; type DatabaseTreeItem = { key: string; type: 'database'; level: 1; id: string; name: string; isExpanded: boolean; setSize: number; posInSet: number; }; type CollectionTreeItem = { key: string; type: 'collection' | 'view' | 'timeseries'; level: 2; id: string; name: string; setSize: number; posInSet: number; }; type TreeItem = PlaceholderTreeItem | DatabaseTreeItem | CollectionTreeItem; type ListItemData = { items: TreeItem[]; isReadOnly: boolean; activeNamespace?: string; currentTabbable?: string; onDatabaseExpand(this: void, id: string, isExpanded: boolean): void; onNamespaceAction(this: void, namespace: string, action: Actions): void; }; const collectionItemContainer = css({ position: 'relative', }); const NavigationItem = memo<{ index: number; style: React.CSSProperties; data: ListItemData; }>(function NavigationItem({ index, style, data }) { const { items, isReadOnly, activeNamespace, currentTabbable, onDatabaseExpand, onNamespaceAction, } = data; const itemData = items[index]; if (itemData.type === 'database') { return ( ); } return (
{ return ( itemData.type !== 'placeholder' && ( ) ); }} fallback={() => } >
); }, areEqual); const navigationTree = css({ flex: '1 0 auto', }); const DatabasesNavigationTree: React.FunctionComponent<{ databases: Database[]; expanded?: Record; onDatabaseExpand(id: string, isExpanded: boolean): void; onNamespaceAction(namespace: string, action: Actions): void; activeNamespace?: string; isReadOnly?: boolean; }> = ({ databases, expanded = {}, activeNamespace = '', onDatabaseExpand, onNamespaceAction, isReadOnly = false, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error ignoring test props so they are not part of the interface __TEST_REACT_AUTOSIZER_DEFAULT_WIDTH = null, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error ignoring test props so they are not part of the interface __TEST_REACT_AUTOSIZER_DEFAULT_HEIGHT = null, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error ignoring test props so they are not part of the interface __TEST_REACT_WINDOW_OVERSCAN = null, }) => { const listRef = useRef(null); const id = useId(); useEffect(() => { if (activeNamespace) { onDatabaseExpand(activeNamespace, true); } }, [activeNamespace, onDatabaseExpand]); const items: TreeItem[] = useMemo(() => { return databases .map( ( { _id: id, name, collections, collectionsLength, collectionsStatus }, dbIndex ) => { const isExpanded = expanded[id]; const database: DatabaseTreeItem = { key: String(dbIndex), level: 1, id, name, type: 'database' as const, isExpanded, setSize: databases.length, posInSet: dbIndex + 1, }; if (!isExpanded) { return [database] as TreeItem[]; } const areCollectionsReady = ['ready', 'refreshing', 'error'].includes( collectionsStatus ); const placeholdersLength = Math.min( collectionsLength, MAX_COLLECTION_PLACEHOLDER_ITEMS ); return ([database] as TreeItem[]).concat( areCollectionsReady ? collections.map(({ _id: id, name, type }, index) => ({ key: `${dbIndex}-${index}`, level: 2, id, name, type: type as 'collection' | 'view' | 'timeseries', setSize: collections.length, posInSet: index + 1, })) : Array.from({ length: placeholdersLength }, (_, index) => ({ key: `${dbIndex}-${index}`, type: 'placeholder' as const, })) ); } ) .flat(); }, [databases, expanded]); const onExpandedChange = useCallback( (item, isExpanded) => { onDatabaseExpand(item.id, isExpanded); }, [onDatabaseExpand] ); const onFocusMove = useCallback( (item) => { const idx = items.indexOf(item); if (idx >= 0) { // It is possible that the item we are trying to move the focus to is // not rendered currently. Scroll it into view so that it's rendered and // can be focused listRef.current?.scrollToItem(idx); } }, [items] ); const [rootProps, currentTabbable] = useVirtualNavigationTree( { items: items as NavigationTreeData, activeItemId: activeNamespace, onExpandedChange, onFocusMove, } ); const itemData: ListItemData = useMemo(() => { return { items, isReadOnly, activeNamespace, currentTabbable, onNamespaceAction, onDatabaseExpand, }; }, [ items, isReadOnly, activeNamespace, currentTabbable, onNamespaceAction, onDatabaseExpand, ]); const getItemKey = useCallback((index: number, data: typeof itemData) => { return data.items[index].key; }, []); return ( <> Databases and Collections
{({ width = __TEST_REACT_AUTOSIZER_DEFAULT_WIDTH, height = __TEST_REACT_AUTOSIZER_DEFAULT_HEIGHT, }) => ( {NavigationItem} )}
); }; const container = css({ display: 'flex', flex: '1 0 auto', }); const contentContainer = css({ display: 'flex', flex: '1 0 auto', }); const NavigationWithPlaceholder: React.FunctionComponent< { isReady: boolean } & React.ComponentProps > = ({ isReady, ...props }) => { return ( { return ; }} fallback={() => { return ; }} > ); }; export { NavigationWithPlaceholder, DatabasesNavigationTree };