From 8f5479923d9d36f3aa4d738f9dd58f32a7b3b484 Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Wed, 20 Mar 2024 13:56:09 +0100 Subject: [PATCH] Dynamic columns for pk --- .../Tabs/DocumentsTabV2/DocumentsTabV2.tsx | 405 ++++++++++-------- .../DocumentsTableComponent.tsx | 197 +++++---- 2 files changed, 334 insertions(+), 268 deletions(-) diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index 29cc224f5..dab0f7bc1 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -1,20 +1,20 @@ -import { ItemDefinition, QueryIterator, Resource } from '@azure/cosmos'; -import { FluentProvider } from '@fluentui/react-components'; -import Split from '@uiw/react-split'; +import { ItemDefinition, QueryIterator, Resource } from "@azure/cosmos"; +import { FluentProvider } from "@fluentui/react-components"; +import Split from "@uiw/react-split"; import { KeyCodes, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants"; -import { getErrorMessage, getErrorStack } from 'Common/ErrorHandlingUtils'; -import { queryDocuments } from 'Common/dataAccess/queryDocuments'; -import { readDocument } from 'Common/dataAccess/readDocument'; -import { useDialog } from 'Explorer/Controls/Dialog'; -import { EditorReact } from 'Explorer/Controls/Editor/EditorReact'; -import { querySampleDocuments, readSampleDocument } from 'Explorer/QueryCopilot/QueryCopilotUtilities'; -import DocumentsTab from 'Explorer/Tabs/DocumentsTab'; -import { dataExplorerLightTheme } from 'Explorer/Theme/ThemeUtil'; -import { QueryConstants } from 'Shared/Constants'; -import { LocalStorageUtility, StorageKey } from 'Shared/StorageUtility'; -import { Action } from 'Shared/Telemetry/TelemetryConstants'; +import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; +import { queryDocuments } from "Common/dataAccess/queryDocuments"; +import { readDocument } from "Common/dataAccess/readDocument"; +import { useDialog } from "Explorer/Controls/Dialog"; +import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; +import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities"; +import DocumentsTab from "Explorer/Tabs/DocumentsTab"; +import { dataExplorerLightTheme } from "Explorer/Theme/ThemeUtil"; +import { QueryConstants } from "Shared/Constants"; +import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; +import { Action } from "Shared/Telemetry/TelemetryConstants"; import { userContext } from "UserContext"; -import { logConsoleError } from 'Utils/NotificationConsoleUtils'; +import { logConsoleError } from "Utils/NotificationConsoleUtils"; import React, { KeyboardEventHandler, useEffect, useMemo, useRef, useState } from "react"; import { format } from "react-string-format"; import CloseIcon from "../../../../images/close-black.svg"; @@ -42,15 +42,17 @@ export class DocumentsTabV2 extends TabsBase { } public render(): JSX.Element { - return ; + return ( + + ); } public onActivate(): void { @@ -84,8 +86,8 @@ const DocumentsTabComponent: React.FunctionComponent<{ // Query const [documentsIterator, setDocumentsIterator] = useState<{ - iterator: QueryIterator, - applyFilterButtonPressed: boolean + iterator: QueryIterator; + applyFilterButtonPressed: boolean; }>(undefined); const [queryAbortController, setQueryAbortController] = useState(undefined); const [resourceTokenPartitionKey, setResourceTokenPartitionKey] = useState(undefined); @@ -95,7 +97,6 @@ const DocumentsTabComponent: React.FunctionComponent<{ const [isExecutionError, setIsExecutionError] = useState(false); const [onLoadStartKey, setOnLoadStartKey] = useState(props.onLoadStartKey); - const [currentDocument, setCurrentDocument] = useState(undefined); // TODO remove this? @@ -106,14 +107,17 @@ const DocumentsTabComponent: React.FunctionComponent<{ const documentContentsContainerId = `documentContentsContainer${props.tabId}`; const documentContentsGridId = `documentContentsGrid${props.tabId}`; - const partitionKey: DataModels.PartitionKey = props.partitionKey || (props.collection && props.collection.partitionKey); + const partitionKey: DataModels.PartitionKey = + props.partitionKey || (props.collection && props.collection.partitionKey); const partitionKeyPropertyHeaders: string[] = props.collection?.partitionKeyPropertyHeaders || partitionKey?.paths; const partitionKeyProperties = partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) => partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""), ); - const isPreferredApiMongoDB = useMemo(() => userContext.apiType === "Mongo" || props.isPreferredApiMongoDB, - [props.isPreferredApiMongoDB]); + const isPreferredApiMongoDB = useMemo( + () => userContext.apiType === "Mongo" || props.isPreferredApiMongoDB, + [props.isPreferredApiMongoDB], + ); useEffect(() => { setDocumentIds(props.documentIds); @@ -125,12 +129,10 @@ const DocumentsTabComponent: React.FunctionComponent<{ try { refreshDocumentsGrid(); - // // Select first document and load content // if (documentIds.length > 0) { // documentIds[0].click(); // } - } catch (error) { if (onLoadStartKey !== null && onLoadStartKey !== undefined) { TelemetryProcessor.traceFailure( @@ -190,7 +192,7 @@ const DocumentsTabComponent: React.FunctionComponent<{ return isQueryCopilotSampleContainer ? querySampleDocuments(query, options) : queryDocuments(props.collection.databaseId, props.collection.id(), query, options); - } + }; /** * Query first page of documents @@ -224,7 +226,6 @@ const DocumentsTabComponent: React.FunctionComponent<{ applyFilterButtonPressed, }); - // collapse filter setAppliedFilter(filterContent); setIsFilterExpanded(false); @@ -331,11 +332,15 @@ const DocumentsTabComponent: React.FunctionComponent<{ partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""), ); - return new DocumentId({ - partitionKey, - partitionKeyPropertyHeaders, - partitionKeyProperties - } as DocumentsTab, rawDocument, partitionKeyValue); + return new DocumentId( + { + partitionKey, + partitionKeyPropertyHeaders, + partitionKeyProperties, + } as DocumentsTab, + rawDocument, + partitionKeyValue, + ); }); const merged = currentDocuments.concat(nextDocumentIds); @@ -423,181 +428,229 @@ const DocumentsTabComponent: React.FunctionComponent<{ props.collection?.databaseId === QueryCopilotSampleDatabaseId && props.collection?.id() === QueryCopilotSampleContainerId; - // Table config here - const tableItems: DocumentsTableComponentItem[] = documentIds.map((documentId) => ({ - id: documentId.id(), - // TODO: for now, merge all the pk values into a single string/column - type: documentId.partitionKeyProperties ? documentId.stringPartitionKeyValues.join(",") : undefined, - })); + const tableItems: DocumentsTableComponentItem[] = documentIds.map((documentId) => { + const item: Record = { + id: documentId.id(), + }; + if (partitionKeyPropertyHeaders && documentId.partitionKeyProperties) { + for (let i = 0; i < partitionKeyPropertyHeaders.length; i++) { + item[partitionKeyPropertyHeaders[i]] = documentId.stringPartitionKeyValues[i]; + } + } + + + return item; + // TODO: for now, merge all the pk values into a single string/column + // type: documentId.partitionKeyProperties ? documentId.stringPartitionKeyValues.join(",") : undefined, + }); const onSelectedDocument = (index: number) => readSingleDocument(documentIds[index]); // TODO: replicate logic of selectedDocument.click(); // TODO: Check if editor is dirty - const readSingleDocument = (documentId: DocumentId) => (_isQueryCopilotSampleContainer - ? readSampleDocument(documentId) - : readDocument(props.collection, documentId)).then((content) => { - // this.initDocumentEditor(documentId, content); - setCurrentDocument(content); - }); + const readSingleDocument = (documentId: DocumentId) => + (_isQueryCopilotSampleContainer ? readSampleDocument(documentId) : readDocument(props.collection, documentId)).then( + (content) => { + // this.initDocumentEditor(documentId, content); + setCurrentDocument(content); + }, + ); const tableContainerRef = useRef(null); - const [tableContainerSizePx, setTableContainerSizePx] = useState<{ height: number, width: number }>(undefined); + const [tableContainerSizePx, setTableContainerSizePx] = useState<{ height: number; width: number }>(undefined); useEffect(() => { if (!tableContainerRef.current) { return undefined; } - const resizeObserver = new ResizeObserver(() => setTableContainerSizePx({ - height: tableContainerRef.current.offsetHeight, - width: tableContainerRef.current.offsetWidth, - })); + const resizeObserver = new ResizeObserver(() => + setTableContainerSizePx({ + height: tableContainerRef.current.offsetHeight, + width: tableContainerRef.current.offsetWidth, + }), + ); resizeObserver.observe(tableContainerRef.current); return () => resizeObserver.disconnect(); // clean up }, []); const columnHeaders = { idHeader: isPreferredApiMongoDB ? "_id" : "id", - pkeyHeaders: partitionKeyPropertyHeaders + partitionKeyHeaders: partitionKeyPropertyHeaders || [], }; - return -
+
- {/* */} - {isFilterCreated && -
- {/* */} - {!isFilterExpanded && !isPreferredApiMongoDB && -
- SELECT * FROM c - {appliedFilter} - -
- } - {!isFilterExpanded && isPreferredApiMongoDB && -
- {appliedFilter.length > 0 && - Filter : - } - {!(appliedFilter.length > 0) && - No filter applied - } - {appliedFilter} - -
- } - {/* */} + role="tabpanel" + style={{ display: "flex" }} + > + {/* */} + {isFilterCreated && ( +
+ {/* */} + {!isFilterExpanded && !isPreferredApiMongoDB && ( +
+ SELECT * FROM c + {appliedFilter} + +
+ )} + {!isFilterExpanded && isPreferredApiMongoDB && ( +
+ {appliedFilter.length > 0 && ( + Filter : + )} + {!(appliedFilter.length > 0) && ( + + No filter applied + + )} + {appliedFilter} + +
+ )} + {/* */} - {/* */} - {isFilterExpanded && -
-
-
- {!isPreferredApiMongoDB && - SELECT * FROM c - } - setFilterContent(e.target.value)} - /* - data-bind=" - W attr:{ - placeholder:isPreferredApiMongoDB?'Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents.':'Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents.' - }, - css: { placeholderVisible: filterContent().length === 0 }, - textInput: filterContent" - */ - /> - - - {lastFilterContents.map((filter) => -
+ )} + {/* */} + + {/* doesn't like to be a flex child */} +
+ + - } - {/* */} +
+ { }} + /> +
+
- } - {/* */} - - {/* doesn't like to be a flex child */} -
- - -
- { }} - /> -
-
-
- ; -} - + + ); +}; diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx index 013af9de6..70c63baf3 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTableComponent.tsx @@ -1,54 +1,48 @@ -import { Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, TableRowData as RowStateBase, Table, TableBody, TableCell, TableCellLayout, TableColumnDefinition, TableColumnSizingOptions, TableHeader, TableHeaderCell, TableRow, TableRowId, TableSelectionCell, createTableColumn, useArrowNavigationGroup, useFluent, useScrollbarWidth, useTableColumnSizing_unstable, useTableFeatures, useTableSelection } from '@fluentui/react-components'; -import React, { useEffect } from 'react'; +import { + Menu, + MenuItem, + MenuList, + MenuPopover, + MenuTrigger, + TableRowData as RowStateBase, + Table, + TableBody, + TableCell, + TableCellLayout, + TableColumnDefinition, + TableColumnSizingOptions, + TableHeader, + TableHeaderCell, + TableRow, + TableRowId, + TableSelectionCell, + createTableColumn, + useArrowNavigationGroup, + useFluent, + useScrollbarWidth, + useTableColumnSizing_unstable, + useTableFeatures, + useTableSelection, +} from "@fluentui/react-components"; +import React, { useEffect, useMemo } from "react"; import { FixedSizeList as List, ListChildComponentProps } from "react-window"; export type DocumentsTableComponentItem = { id: string; - type: string; -}; +} & Record; +export type ColumnHeaders = { + idHeader: string; + partitionKeyHeaders: string[]; +}; export interface IDocumentsTableComponentProps { items: DocumentsTableComponentItem[]; onSelectedItem: (index: number) => void; size: { height: number; width: number }; - columnHeaders: { - idHeader: string; - partitionKeyHeader: string; - }; + columnHeaders: ColumnHeaders; style?: React.CSSProperties; } -const columns: TableColumnDefinition[] = [ - createTableColumn({ - columnId: "id", - compare: (a, b) => { - return a.id.localeCompare(b.id); - }, - renderHeaderCell: () => { - return "id"; - }, - renderCell: (item) => { - return ( - {item.id} - ); - }, - }), - createTableColumn({ - columnId: "type", - compare: (a, b) => { - return a.type.localeCompare(b.type); - }, - renderHeaderCell: () => { - return "/type"; - }, - renderCell: (item) => { - return ( - {item.type} - ); - }, - }), -]; - interface TableRowData extends RowStateBase { onClick: (e: React.MouseEvent) => void; onKeyDown: (e: React.KeyboardEvent) => void; @@ -60,7 +54,11 @@ interface ReactWindowRenderFnProps extends ListChildComponentProps { } export const DocumentsTableComponent: React.FC = ({ - items, onSelectedItem, style, size, + items, + onSelectedItem, + style, + size, + columnHeaders, }: IDocumentsTableComponentProps) => { const { targetDocument } = useFluent(); const scrollbarWidth = useScrollbarWidth({ targetDocument }); @@ -72,11 +70,12 @@ export const DocumentsTableComponent: React.FC = idealWidth: 280, // minWidth: 273, }, - type: { - defaultWidth: 100 - // minWidth: 110, - // defaultWidth: 120, - }, + // TODO FIX THIS + // type: { + // defaultWidth: 100, + // // minWidth: 110, + // // defaultWidth: 120, + // }, }); const onColumnResize = React.useCallback((_, { columnId, width }) => { @@ -89,49 +88,72 @@ export const DocumentsTableComponent: React.FC = })); }, []); - const [selectedRows, setSelectedRows] = React.useState>( - () => new Set([0]) + const [selectedRows, setSelectedRows] = React.useState>(() => new Set([0])); + + // Columns must be a static object and cannot change on re-renders otherwise React will complain about too many refreshes + const columns: TableColumnDefinition[] = useMemo( + () => + [ + createTableColumn({ + columnId: "id", + compare: (a, b) => { + return a.id.localeCompare(b.id); + }, + renderHeaderCell: () => { + return "id"; + }, + renderCell: (item) => { + return {item.id}; + }, + }), + ].concat( + columnHeaders.partitionKeyHeaders.map((pkHeader) => + createTableColumn({ + columnId: pkHeader, + compare: (a, b) => { + return a[pkHeader].localeCompare(b[pkHeader]); + }, + renderHeaderCell: () => { + return `/${pkHeader}`; + }, + renderCell: (item) => { + return {item[pkHeader]}; + }, + }), + ), + ), + [columnHeaders], ); const RenderRow = ({ index, style, data }: ReactWindowRenderFnProps) => { const { item, selected, appearance, onClick, onKeyDown } = data[index]; - return - - {columns.map((column) => ( - setSelectedRows(new Set([index]))} + return ( + + - {column.renderCell(item)} - - ))} - ; + /> + {columns.map((column) => ( + setSelectedRows(new Set([index]))} + onKeyDown={onKeyDown} + {...columnSizing.getTableCellProps(column.columnId)} + > + {column.renderCell(item)} + + ))} + + ); }; const { getRows, columnSizing_unstable: columnSizing, tableRef, - selection: { - allRowsSelected, - someRowsSelected, - toggleAllRows, - toggleRow, - isRowSelected, - }, + selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected }, } = useTableFeatures( { columns, @@ -144,7 +166,7 @@ export const DocumentsTableComponent: React.FC = selectedItems: selectedRows, onSelectionChange: (e, data) => setSelectedRows(data.selectedItems), }), - ] + ], ); const rows: TableRowData[] = getRows((row) => { @@ -170,7 +192,7 @@ export const DocumentsTableComponent: React.FC = e.preventDefault(); } }, - [toggleAllRows] + [toggleAllRows], ); // Load document depending on selection @@ -202,30 +224,21 @@ export const DocumentsTableComponent: React.FC = - {columns.map((column, /* index */) => ( + {columns.map((column /* index */) => ( - + {column.renderHeaderCell()} - + Keyboard Column Resizing