diff --git a/packages/compass-components/src/components/leafygreen.tsx b/packages/compass-components/src/components/leafygreen.tsx
index 96f8df17870..7cbf9d75001 100644
--- a/packages/compass-components/src/components/leafygreen.tsx
+++ b/packages/compass-components/src/components/leafygreen.tsx
@@ -53,8 +53,11 @@ import {
TableBody,
flexRender,
useLeafyGreenTable,
+ useLeafyGreenVirtualTable,
+ type LeafyGreenVirtualItem,
getExpandedRowModel,
getFilteredRowModel,
+ type TableProps,
} from '@leafygreen-ui/table';
import type { Row as LgTableRowType } from '@tanstack/table-core'; // TODO(COMPASS-8437): import from LG
@@ -197,6 +200,9 @@ export {
InfoSprinkle,
flexRender,
useLeafyGreenTable,
+ useLeafyGreenVirtualTable,
+ type LeafyGreenVirtualItem,
+ type TableProps,
getExpandedRowModel,
getFilteredRowModel,
type LgTableRowType,
diff --git a/packages/databases-collections-list/src/collections.tsx b/packages/databases-collections-list/src/collections.tsx
index 0519d6d26e1..405ca92e8f3 100644
--- a/packages/databases-collections-list/src/collections.tsx
+++ b/packages/databases-collections-list/src/collections.tsx
@@ -1,20 +1,76 @@
-import React from 'react';
-import { css, spacing } from '@mongodb-js/compass-components';
+import React, { useCallback } from 'react';
+import {
+ Badge,
+ type BadgeVariant,
+ cx,
+ css,
+ type GlyphName,
+ Icon,
+ spacing,
+ type LGColumnDef,
+ Tooltip,
+} from '@mongodb-js/compass-components';
import { compactBytes, compactNumber } from './format';
-import type { BadgeProp } from './namespace-card';
-import { NamespaceItemCard } from './namespace-card';
-import { ItemsGrid } from './items-grid';
+import { ItemsTable } from './items-table';
import type { CollectionProps } from 'mongodb-collection-model';
import { usePreference } from 'compass-preferences-model/provider';
-const COLLECTION_CARD_WIDTH = spacing[1600] * 4;
+type BadgeProp = {
+ id: string;
+ name: string;
+ variant?: BadgeVariant;
+ icon?: GlyphName;
+ hint?: React.ReactNode;
+};
-const COLLECTION_CARD_HEIGHT = 238;
-const COLLECTION_CARD_WITHOUT_STATS_HEIGHT = COLLECTION_CARD_HEIGHT - 150;
+const cardBadgesStyles = css({
+ display: 'flex',
+ gap: spacing[200],
+ // Preserving space for when cards with and without badges are mixed in a
+ // single row
+ minHeight: 20,
+});
-const COLLECTION_CARD_LIST_HEIGHT = 118;
-const COLLECTION_CARD_LIST_WITHOUT_STATS_HEIGHT =
- COLLECTION_CARD_LIST_HEIGHT - 50;
+const CardBadges: React.FunctionComponent = ({ children }) => {
+ return
{children}
;
+};
+
+const cardBadgeStyles = css({
+ gap: spacing[100],
+});
+
+const CardBadge: React.FunctionComponent = ({
+ id,
+ name,
+ icon,
+ variant,
+ hint,
+}) => {
+ const badge = useCallback(
+ ({ className, children, ...props } = {}) => {
+ return (
+
+ {icon && }
+ {name}
+ {/* Tooltip will be rendered here */}
+ {children}
+
+ );
+ },
+ [id, icon, name, variant]
+ );
+
+ if (hint) {
+ return {hint};
+ }
+
+ return badge();
+};
function collectionPropertyToBadge({
id,
@@ -62,18 +118,114 @@ function collectionPropertyToBadge({
}
}
-const pageContainerStyles = css({
- height: 'auto',
- width: '100%',
+const collectionNameStyles = css({
display: 'flex',
- flexDirection: 'column',
+ gap: spacing[100],
+ flexWrap: 'wrap',
+ alignItems: 'anchor-center',
});
+function collectionColumns(
+ enableDbAndCollStats: boolean
+): LGColumnDef[] {
+ return [
+ {
+ accessorKey: 'name',
+ header: 'Collection name',
+ enableSorting: true,
+ size: 300,
+ cell: (info) => {
+ const name = info.getValue() as string;
+
+ const badges = info.row.original.properties
+ .filter((prop) => prop.id !== 'read-only')
+ .map((prop) => {
+ return collectionPropertyToBadge(prop);
+ });
+
+ return (
+
+ {name}
+
+ {badges.map((badge) => {
+ return ;
+ })}
+
+
+ );
+ },
+ },
+ {
+ accessorKey: 'calculated_storage_size',
+ header: 'Storage size',
+ enableSorting: true,
+ cell: (info) => {
+ const type = info.row.original.type as string;
+ if (type === 'view') {
+ return '-';
+ }
+ const size = info.getValue() as number | undefined;
+ return enableDbAndCollStats && size !== undefined
+ ? compactBytes(size)
+ : '-';
+ },
+ },
+ {
+ accessorKey: 'avg_document_size',
+ header: 'Avg. document size',
+ enableSorting: true,
+ cell: (info) => {
+ const type = info.row.original.type as string;
+ if (type === 'view' || type === 'timeseries') {
+ return '-';
+ }
+
+ const size = info.getValue() as number | undefined;
+ return enableDbAndCollStats && size !== undefined
+ ? compactBytes(size)
+ : '-';
+ },
+ },
+ {
+ accessorKey: 'Indexes',
+ header: 'Indexes',
+ enableSorting: true,
+ cell: (info) => {
+ const type = info.row.original.type as string;
+ if (type === 'view' || type === 'timeseries') {
+ return '-';
+ }
+
+ const index_count = info.getValue() as number | undefined;
+ return enableDbAndCollStats && index_count !== undefined
+ ? compactNumber(index_count)
+ : '-';
+ },
+ },
+ {
+ accessorKey: 'index_size',
+ header: 'Total index size',
+ enableSorting: true,
+ cell: (info) => {
+ const type = info.row.original.type as string;
+ if (type === 'view' || type === 'timeseries') {
+ return '-';
+ }
+
+ const size = info.getValue() as number | undefined;
+ return enableDbAndCollStats && size !== undefined
+ ? compactBytes(size)
+ : '-';
+ },
+ },
+ ];
+}
+
+// TODO: we removed delete click functionality, we removed the header hint functionality
const CollectionsList: React.FunctionComponent<{
namespace: string;
collections: CollectionProps[];
onCollectionClick: (id: string) => void;
- onDeleteCollectionClick?: (id: string) => void;
onCreateCollectionClick?: () => void;
onRefreshClick?: () => void;
}> = ({
@@ -81,148 +233,23 @@ const CollectionsList: React.FunctionComponent<{
collections,
onCollectionClick,
onCreateCollectionClick,
- onDeleteCollectionClick,
onRefreshClick,
}) => {
const enableDbAndCollStats = usePreference('enableDbAndCollStats');
+ const columns = React.useMemo(
+ () => collectionColumns(enableDbAndCollStats),
+ [enableDbAndCollStats]
+ );
return (
-
- {
- const data =
- coll.type === 'view'
- ? [{ label: 'View on', value: coll.source?.name }]
- : coll.type === 'timeseries'
- ? [
- {
- label: 'Storage',
- value:
- coll.calculated_storage_size !== undefined
- ? compactBytes(coll.calculated_storage_size)
- : 'N/A',
- hint:
- coll.calculated_storage_size !== undefined &&
- coll.storage_size !== undefined &&
- coll.free_storage_size !== undefined &&
- 'Storage Data: Disk space allocated to this collection for document storage.\n' +
- `Total storage: ${compactBytes(coll.storage_size)}\n` +
- `Free storage: ${compactBytes(coll.free_storage_size)}`,
- },
- {
- label: 'Uncompressed data',
- value:
- coll.document_size !== undefined
- ? compactBytes(coll.document_size)
- : 'N/A',
- hint:
- coll.document_size !== undefined &&
- 'Uncompressed Data Size: Total size of the uncompressed data held in this collection.',
- },
- ]
- : [
- {
- label: 'Storage',
- value:
- coll.calculated_storage_size !== undefined
- ? compactBytes(coll.calculated_storage_size)
- : 'N/A',
- hint:
- coll.calculated_storage_size !== undefined &&
- 'Storage Data: Disk space allocated to this collection for document storage.',
- },
- {
- label: 'Uncompressed data',
- value:
- coll.document_size !== undefined
- ? compactBytes(coll.document_size)
- : 'N/A',
- hint:
- coll.document_size !== undefined &&
- 'Uncompressed Data Size: Total size of the uncompressed data held in this collection.',
- },
- {
- label: 'Documents',
- value:
- coll.document_count !== undefined
- ? compactNumber(coll.document_count)
- : 'N/A',
- },
- {
- label: 'Avg. document size',
- value:
- coll.avg_document_size !== undefined
- ? compactBytes(coll.avg_document_size)
- : 'N/A',
- },
- {
- label: 'Indexes',
- value:
- coll.index_count !== undefined
- ? compactNumber(coll.index_count)
- : 'N/A',
- },
- {
- label: 'Total index size',
- value:
- coll.index_size !== undefined
- ? compactBytes(coll.index_size)
- : 'N/A',
- },
- ];
-
- const badges = coll.properties.map((prop) => {
- return collectionPropertyToBadge(prop);
- });
-
- return (
-
- );
- }}
- >
-
+
);
};
diff --git a/packages/databases-collections-list/src/databases.tsx b/packages/databases-collections-list/src/databases.tsx
index ec4e1556f51..b1c2c1228b9 100644
--- a/packages/databases-collections-list/src/databases.tsx
+++ b/packages/databases-collections-list/src/databases.tsx
@@ -1,24 +1,62 @@
/* eslint-disable react/prop-types */
import React from 'react';
-import { PerformanceSignals, spacing } from '@mongodb-js/compass-components';
+// TODO: don't forget about performance insights?
+//import { PerformanceSignals, spacing } from '@mongodb-js/compass-components';
import { compactBytes, compactNumber } from './format';
-import { NamespaceItemCard } from './namespace-card';
-import { ItemsGrid } from './items-grid';
+import { ItemsTable } from './items-table';
import type { DatabaseProps } from 'mongodb-database-model';
import { usePreference } from 'compass-preferences-model/provider';
+import type { LGColumnDef } from '@mongodb-js/compass-components';
-const DATABASE_CARD_WIDTH = spacing[1600] * 4;
-
-const DATABASE_CARD_HEIGHT = 154;
-const DATABASE_CARD_WITHOUT_STATS_HEIGHT = DATABASE_CARD_HEIGHT - 85;
-
-const DATABASE_CARD_LIST_HEIGHT = 118;
-const DATABASE_CARD_LIST_WITHOUT_STATS_HEIGHT = DATABASE_CARD_LIST_HEIGHT - 50;
+function databaseColumns(
+ enableDbAndCollStats: boolean
+): LGColumnDef[] {
+ return [
+ {
+ accessorKey: 'name',
+ header: 'Database name',
+ enableSorting: true,
+ },
+ {
+ accessorKey: 'calculated_storage_size',
+ header: 'Storage size',
+ enableSorting: true,
+ cell: (info) => {
+ // TODO: shouldn't this just have the right type rather than unknown?
+ const size = info.getValue() as number | undefined;
+ return enableDbAndCollStats && size !== undefined
+ ? compactBytes(size)
+ : '-';
+ },
+ },
+ {
+ accessorKey: 'collectionsLength',
+ header: 'Collections',
+ enableSorting: true,
+ cell: (info) => {
+ return enableDbAndCollStats
+ ? compactNumber(info.getValue() as number)
+ : '-';
+ },
+ },
+ {
+ accessorKey: 'index_count',
+ header: 'Indexes',
+ enableSorting: true,
+ cell: (info) => {
+ const index_count = info.getValue() as number | undefined;
+ return enableDbAndCollStats && index_count !== undefined
+ ? compactNumber(index_count)
+ : '-';
+ },
+ },
+ ];
+}
+// TODO: we removed delete click functionality, we removed the header hint functionality
const DatabasesList: React.FunctionComponent<{
databases: DatabaseProps[];
onDatabaseClick: (id: string) => void;
- onDeleteDatabaseClick?: (id: string) => void;
onCreateDatabaseClick?: () => void;
onRefreshClick?: () => void;
renderLoadSampleDataBanner?: () => React.ReactNode;
@@ -26,104 +64,24 @@ const DatabasesList: React.FunctionComponent<{
databases,
onDatabaseClick,
onCreateDatabaseClick,
- onDeleteDatabaseClick,
onRefreshClick,
renderLoadSampleDataBanner,
}) => {
const enableDbAndCollStats = usePreference('enableDbAndCollStats');
+ const columns = React.useMemo(
+ () => databaseColumns(enableDbAndCollStats),
+ [enableDbAndCollStats]
+ );
return (
- {
- return (
- = 10_000
- ? PerformanceSignals.get('too-many-collections')
- : undefined,
- },
- {
- label: 'Indexes',
- value:
- enableDbAndCollStats && db.index_count !== undefined
- ? compactNumber(db.index_count)
- : 'N/A',
- },
- ]}
- onItemClick={onItemClick}
- onItemDeleteClick={onDeleteItemClick}
- {...props}
- >
- );
- }}
renderLoadSampleDataBanner={renderLoadSampleDataBanner}
- >
+ >
);
};
diff --git a/packages/databases-collections-list/src/items-table.tsx b/packages/databases-collections-list/src/items-table.tsx
new file mode 100644
index 00000000000..62d0263895d
--- /dev/null
+++ b/packages/databases-collections-list/src/items-table.tsx
@@ -0,0 +1,342 @@
+import React, { Fragment, useMemo } from 'react';
+import {
+ css,
+ cx,
+ spacing,
+ WorkspaceContainer,
+ Button,
+ Icon,
+ Breadcrumbs,
+ Table,
+ TableHead,
+ TableBody,
+ useLeafyGreenVirtualTable,
+ type LGColumnDef,
+ type HeaderGroup,
+ HeaderRow,
+ HeaderCell,
+ flexRender,
+ ExpandedContent,
+ Row,
+ Cell,
+ //type LeafyGreenTableRow,
+ type LeafyGreenVirtualItem,
+} from '@mongodb-js/compass-components';
+import { useTelemetry } from '@mongodb-js/compass-telemetry/provider';
+import { useConnectionInfo } from '@mongodb-js/compass-connections/provider';
+import toNS from 'mongodb-ns';
+import { getConnectionTitle } from '@mongodb-js/connection-info';
+import { useOpenWorkspace } from '@mongodb-js/compass-workspaces/provider';
+import { usePreferences } from 'compass-preferences-model/provider';
+
+type Item = { _id: string } & Record;
+
+export const createButtonStyles = css({
+ whiteSpace: 'nowrap',
+});
+
+type ItemsGridProps = {
+ namespace?: string;
+ itemType: 'collection' | 'database';
+ columns: LGColumnDef[];
+ items: T[];
+ onItemClick: (id: string) => void;
+ onCreateItemClick?: () => void;
+ onRefreshClick?: () => void;
+ renderLoadSampleDataBanner?: () => React.ReactNode;
+};
+
+const controlsContainerStyles = css({
+ paddingTop: spacing[200],
+ paddingRight: spacing[400],
+ paddingBottom: spacing[400],
+ paddingLeft: spacing[400],
+
+ display: 'grid',
+ gridTemplate: '1fr / 100%',
+ gap: spacing[200],
+});
+
+const controlRowStyles = css({
+ display: 'flex',
+ alignItems: 'center',
+ gap: spacing[200],
+});
+
+const controlStyles = css({
+ flex: 'none',
+});
+
+const breadcrumbContainerStyles = css({
+ display: 'flex',
+ minWidth: 0,
+ paddingTop: spacing[200],
+ paddingBottom: spacing[200],
+});
+
+const pushRightStyles = css({
+ marginLeft: 'auto',
+});
+
+const bannerRowStyles = css({
+ paddingTop: spacing[200],
+});
+
+function buildChartsUrl(
+ groupId: string,
+ clusterName: string,
+ namespace?: string
+) {
+ const { database } = toNS(namespace ?? '');
+ const url = new URL(`/charts/${groupId}`, window.location.origin);
+ url.searchParams.set('sourceType', 'cluster');
+ url.searchParams.set('name', clusterName);
+ if (database) {
+ url.searchParams.set('database', database);
+ }
+ return url.toString();
+}
+
+const TableControls: React.FunctionComponent<{
+ namespace?: string;
+ itemType: string;
+ onCreateItemClick?: () => void;
+ onRefreshClick?: () => void;
+ renderLoadSampleDataBanner?: () => React.ReactNode;
+}> = ({
+ namespace,
+ itemType,
+ onCreateItemClick,
+ onRefreshClick,
+ renderLoadSampleDataBanner,
+}) => {
+ const connectionInfo = useConnectionInfo();
+ const connectionTitle = getConnectionTitle(connectionInfo);
+ const {
+ openDatabasesWorkspace,
+ openCollectionsWorkspace,
+ openShellWorkspace,
+ } = useOpenWorkspace();
+ const track = useTelemetry();
+ const { enableShell: showOpenShellButton } = usePreferences(['enableShell']);
+
+ const breadcrumbs = useMemo(() => {
+ const { database } = toNS(namespace ?? '');
+ const items = [
+ {
+ name: connectionTitle,
+ onClick: () => {
+ openDatabasesWorkspace(connectionInfo.id);
+ },
+ },
+ ];
+
+ if (database) {
+ items.push({
+ name: database,
+ onClick: () => {
+ openCollectionsWorkspace(connectionInfo.id, database);
+ },
+ });
+ }
+
+ return items;
+ }, [
+ connectionInfo.id,
+ connectionTitle,
+ namespace,
+ openCollectionsWorkspace,
+ openDatabasesWorkspace,
+ ]);
+
+ const banner = renderLoadSampleDataBanner?.();
+
+ return (
+
+
+
+
+
+
+
+ {showOpenShellButton && (
+
+ )}
+
+ {connectionInfo.atlasMetadata && (
+
}
+ >
+ Visualize your data
+
+ )}
+
+ {onCreateItemClick && (
+
+ }
+ onClick={onCreateItemClick}
+ className={createButtonStyles}
+ size="small"
+ >
+ Create {itemType}
+
+
+ )}
+
+ {onRefreshClick && (
+
+ }
+ onClick={onRefreshClick}
+ size="small"
+ >
+ Refresh
+
+
+ )}
+
+
+ {banner &&
{banner}
}
+
+ );
+};
+
+const itemsGridContainerStyles = css({
+ width: '100%',
+ height: '100%',
+});
+
+const virtualScrollingContainerHeight = css({
+ height: 'calc(100vh - 100px)',
+ padding: `0 ${spacing[400]}px`,
+});
+
+export const ItemsTable = ({
+ namespace,
+ itemType,
+ columns,
+ items,
+ onItemClick,
+ onCreateItemClick,
+ onRefreshClick,
+ renderLoadSampleDataBanner,
+}: ItemsGridProps): React.ReactElement => {
+ const tableContainerRef = React.useRef(null);
+
+ const table = useLeafyGreenVirtualTable({
+ containerRef: tableContainerRef,
+ data: items,
+ columns,
+ virtualizerOptions: {
+ estimateSize: () => 50,
+ overscan: 10,
+ },
+ });
+
+ return (
+
+
+ }
+ >
+
+
+ {table.getHeaderGroups().map((headerGroup: HeaderGroup) => (
+
+ {headerGroup.headers.map((header) => {
+ return (
+
+ {flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+
+ );
+ })}
+
+ ))}
+
+
+ {table.virtual.getVirtualItems() &&
+ table.virtual
+ .getVirtualItems()
+ .map((virtualRow: LeafyGreenVirtualItem) => {
+ const row = virtualRow.row;
+ const isExpandedContent = row.isExpandedContent ?? false;
+
+ return (
+
+ {!isExpandedContent && (
+ // row is required
+
+ onItemClick(row.original._id as string)
+ }
+ >
+ {row.getVisibleCells().map((cell: any) => {
+ return (
+ // cell is required
+
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext()
+ )}
+ |
+ );
+ })}
+
+ )}
+ {isExpandedContent && }
+
+ );
+ })}
+
+
+
+
+ );
+};