From 45513e5e1be94f6b28b3c878e3bd826d442e8f6d Mon Sep 17 00:00:00 2001 From: Dmitry Shilov Date: Mon, 19 May 2025 11:35:04 +0200 Subject: [PATCH] fix: Update grid after uploading documents (#2131) - Refactor UploadItemsPane to support onUpload callback - Enhance bulk insert result type - Insert uploaded documents to the grid --- src/Contracts/ViewModels.ts | 4 + src/Explorer/Explorer.tsx | 7 +- .../Panes/UploadItemsPane/UploadItemsPane.tsx | 8 +- .../Tabs/DocumentsTabV2/DocumentsTabV2.tsx | 89 ++++++++++++------- src/Explorer/Tree/Collection.ts | 21 ++--- src/Utils/GalleryUtils.ts | 2 +- 6 files changed, 86 insertions(+), 45 deletions(-) diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index 5aa393cbf..403eb9e79 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -1,4 +1,5 @@ import { + ItemDefinition, JSONObject, QueryMetrics, Resource, @@ -30,8 +31,11 @@ export interface UploadDetailsRecord { numFailed: number; numThrottled: number; errors: string[]; + resources?: ItemDefinition[]; } +export type BulkInsertResult = Omit; + export interface QueryResultsMetadata { hasMoreResults: boolean; firstItemIndex: number; diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 5bf42e9aa..23ff72edc 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -71,6 +71,7 @@ import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; import StoredProcedure from "./Tree/StoredProcedure"; import { useDatabases } from "./useDatabases"; import { useSelectedNode } from "./useSelectedNode"; +import { UploadDetailsRecord } from "../Contracts/ViewModels"; BindingHandlersRegisterer.registerBindingHandlers(); @@ -1115,8 +1116,8 @@ export default class Explorer { } } - public openUploadItemsPane(): void { - useSidePanel.getState().openSidePanel("Upload " + getUploadName(), ); + public openUploadItemsPane(onUpload?: (data: UploadDetailsRecord[]) => void): void { + useSidePanel.getState().openSidePanel("Upload " + getUploadName(), ); } public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void { useSidePanel @@ -1124,7 +1125,7 @@ export default class Explorer { .openSidePanel("Input parameters", ); } - public getDownloadModalConent(fileName: string): JSX.Element { + public getDownloadModalContent(fileName: string): JSX.Element { if (useNotebook.getState().isPhoenixNotebooks) { return ( <> diff --git a/src/Explorer/Panes/UploadItemsPane/UploadItemsPane.tsx b/src/Explorer/Panes/UploadItemsPane/UploadItemsPane.tsx index 6915104f9..344dc13c7 100644 --- a/src/Explorer/Panes/UploadItemsPane/UploadItemsPane.tsx +++ b/src/Explorer/Panes/UploadItemsPane/UploadItemsPane.tsx @@ -48,7 +48,11 @@ const classNames = mergeStyleSets({ warning: [{ color: theme.semanticColors.warningIcon }, iconClass], }); -export const UploadItemsPane: FunctionComponent = () => { +export type UploadItemsPaneProps = { + onUpload?: (data: UploadDetailsRecord[]) => void; +}; + +export const UploadItemsPane: FunctionComponent = ({ onUpload }) => { const [files, setFiles] = useState(); const [uploadFileData, setUploadFileData] = useState([]); const [formError, setFormError] = useState(""); @@ -71,6 +75,8 @@ export const UploadItemsPane: FunctionComponent = () => { (uploadDetails) => { setUploadFileData(uploadDetails.data); setFiles(undefined); + // Emit the upload details to the parent component + onUpload && onUpload(uploadDetails.data); }, (error: Error) => { const errorMessage = getErrorMessage(error); diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index c84fcb945..53850dc85 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -26,7 +26,6 @@ import { useDialog } from "Explorer/Controls/Dialog"; import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; import { InputDataList, InputDatalistDropdownOptionSection } from "Explorer/Controls/InputDataList/InputDataList"; import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog"; -import Explorer from "Explorer/Explorer"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities"; import { @@ -64,7 +63,7 @@ import * as Logger from "../../../Common/Logger"; import * as MongoProxyClient from "../../../Common/MongoProxyClient"; import * as DataModels from "../../../Contracts/DataModels"; import * as ViewModels from "../../../Contracts/ViewModels"; -import { CollectionBase } from "../../../Contracts/ViewModels"; +import { CollectionBase, UploadDetailsRecord } from "../../../Contracts/ViewModels"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as QueryUtils from "../../../Utils/QueryUtils"; import { defaultQueryFields, extractPartitionKeyValues } from "../../../Utils/QueryUtils"; @@ -309,7 +308,6 @@ type UiKeyboardEvent = (e: KeyboardEvent | React.SyntheticEvent) // Export to expose to unit tests export type ButtonsDependencies = { - _collection: ViewModels.CollectionBase; selectedRows: Set; editorState: ViewModels.DocumentExplorerState; isPreferredApiMongoDB: boolean; @@ -320,26 +318,7 @@ export type ButtonsDependencies = { onSaveExistingDocumentClick: UiKeyboardEvent; onRevertExistingDocumentClick: UiKeyboardEvent; onDeleteExistingDocumentsClick: UiKeyboardEvent; -}; - -const createUploadButton = (container: Explorer): CommandButtonComponentProps => { - const label = "Upload Item"; - return { - id: UPLOAD_BUTTON_ID, - iconSrc: UploadIcon, - iconAlt: label, - onCommandClick: () => { - const selectedCollection: ViewModels.Collection = useSelectedNode.getState().findSelectedCollection(); - selectedCollection && container.openUploadItemsPane(); - }, - commandButtonLabel: label, - ariaLabel: label, - hasPopup: true, - disabled: - useSelectedNode.getState().isDatabaseNodeOrNoneSelected() || - !useClientWriteEnabled.getState().clientWriteEnabled || - useSelectedNode.getState().isQueryCopilotCollectionSelected(), - }; + onUploadDocumentsClick: UiKeyboardEvent; }; // Export to expose to unit tests @@ -352,7 +331,6 @@ export const UPLOAD_BUTTON_ID = "uploadItemBtn"; // Export to expose in unit tests export const getTabsButtons = ({ - _collection, selectedRows, editorState, isPreferredApiMongoDB, @@ -363,6 +341,7 @@ export const getTabsButtons = ({ onSaveExistingDocumentClick, onRevertExistingDocumentClick, onDeleteExistingDocumentsClick, + onUploadDocumentsClick, }: ButtonsDependencies): CommandButtonComponentProps[] => { if (isFabric() && userContext.fabricContext?.isReadOnly) { // All the following buttons require write access @@ -474,7 +453,20 @@ export const getTabsButtons = ({ } if (!isPreferredApiMongoDB) { - buttons.push(createUploadButton(_collection.container)); + const label = "Upload Item"; + buttons.push({ + id: UPLOAD_BUTTON_ID, + iconSrc: UploadIcon, + iconAlt: label, + onCommandClick: onUploadDocumentsClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: true, + disabled: + useSelectedNode.getState().isDatabaseNodeOrNoneSelected() || + !useClientWriteEnabled.getState().clientWriteEnabled || + useSelectedNode.getState().isQueryCopilotCollectionSelected(), + }); } return buttons; @@ -886,7 +878,6 @@ export const DocumentsTabComponent: React.FunctionComponent { + if (!isPreferredApiMongoDB) { + const onSuccessUpload = (data: UploadDetailsRecord[]) => { + const addedIdsSet = new Set( + data + .reduce( + (result: ItemDefinition[], record) => + result.concat(record.resources && record.resources.length ? record.resources : []), + [], + ) + .map((document) => { + const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues( + document, + partitionKey as PartitionKeyDefinition, + ); + return newDocumentId( + document as ItemDefinition & Resource, + partitionKeyProperties, + partitionKeyValueArray as string[], + ); + }), + ); + + const documents = new Set(documentIds); + addedIdsSet.forEach((item) => documents.add(item)); + setDocumentIds(Array.from(documents)); + + setSelectedDocumentContent(undefined); + setClickedRowIndex(undefined); + setSelectedRows(new Set()); + setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected); + }; + + _collection.container.openUploadItemsPane(onSuccessUpload); + } + }, [_collection.container, documentIds, isPreferredApiMongoDB, newDocumentId, partitionKey, partitionKeyProperties]); + // If editor state changes, update the nav useEffect( () => updateNavbarWithTabsButtons(isTabActive, { - _collection, selectedRows, editorState, isPreferredApiMongoDB, @@ -1315,11 +1343,11 @@ export const DocumentsTabComponent: React.FunctionComponent { - const stats = { + public async bulkInsertDocuments(documents: JSONObject[]): Promise { + const stats: BulkInsertResult = { numSucceeded: 0, numFailed: 0, numThrottled: 0, errors: [] as string[], + resources: [], }; const chunkSize = 100; // 100 is the max # of bulk operations the SDK currently accepts @@ -1120,6 +1116,7 @@ export default class Collection implements ViewModels.Collection { responses.forEach((response, index) => { if (response.statusCode === 201) { stats.numSucceeded++; + stats.resources.push(response.resourceBody); } else if (response.statusCode === 429) { documentsToAttempt.push(attemptedDocuments[index]); } else if (response.statusCode === 409) { @@ -1152,18 +1149,22 @@ export default class Collection implements ViewModels.Collection { numFailed: 0, numThrottled: 0, errors: [], + resources: [], }; try { const parsedContent = JSON.parse(documentContent); if (Array.isArray(parsedContent)) { - const { numSucceeded, numFailed, numThrottled, errors } = await this.bulkInsertDocuments(parsedContent); + const { numSucceeded, numFailed, numThrottled, errors, resources } = + await this.bulkInsertDocuments(parsedContent); record.numSucceeded = numSucceeded; record.numFailed = numFailed; record.numThrottled = numThrottled; record.errors = errors; + record.resources = record.resources.concat(resources); } else { - await createDocument(this, parsedContent); + const resource = await createDocument(this, parsedContent); + record.resources.push(resource); record.numSucceeded++; } diff --git a/src/Utils/GalleryUtils.ts b/src/Utils/GalleryUtils.ts index f11b6df5c..4d934ef08 100644 --- a/src/Utils/GalleryUtils.ts +++ b/src/Utils/GalleryUtils.ts @@ -245,7 +245,7 @@ export function downloadItem( }, "Cancel", undefined, - container.getDownloadModalConent(name), + container.getDownloadModalContent(name), ); } export async function downloadNotebookItem(