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 a07a7dad3..c3b9029ea 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -70,6 +70,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(); @@ -1078,8 +1079,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 @@ -1087,7 +1088,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 0686812e1..6d8fd58e0 100644 --- a/src/Explorer/Panes/UploadItemsPane/UploadItemsPane.tsx +++ b/src/Explorer/Panes/UploadItemsPane/UploadItemsPane.tsx @@ -14,7 +14,11 @@ import { getErrorMessage } from "../../Tables/Utilities"; import { useSelectedNode } from "../../useSelectedNode"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; -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(""); @@ -37,6 +41,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 58b976d44..74b43579c 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"; @@ -302,7 +301,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; @@ -313,26 +311,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 @@ -345,7 +324,6 @@ export const UPLOAD_BUTTON_ID = "uploadItemBtn"; // Export to expose in unit tests export const getTabsButtons = ({ - _collection, selectedRows, editorState, isPreferredApiMongoDB, @@ -356,6 +334,7 @@ export const getTabsButtons = ({ onSaveExistingDocumentClick, onRevertExistingDocumentClick, onDeleteExistingDocumentsClick, + onUploadDocumentsClick, }: ButtonsDependencies): CommandButtonComponentProps[] => { if (isFabric() && userContext.fabricContext?.isReadOnly) { // All the following buttons require write access @@ -467,7 +446,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; @@ -870,7 +862,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, @@ -1299,11 +1327,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 { @@ -1149,18 +1146,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(