mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-05-10 18:33:52 +01:00
fix: Update grid after uploading documents
- Refactor UploadItemsPane to support onUpload callback - Enhance bulk insert result type - Insert uploaded documents to the grid
This commit is contained in:
parent
10cda21401
commit
82e7179846
@ -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<UploadDetailsRecord, "fileName">;
|
||||
|
||||
export interface QueryResultsMetadata {
|
||||
hasMoreResults: boolean;
|
||||
firstItemIndex: number;
|
||||
|
@ -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(), <UploadItemsPane />);
|
||||
public openUploadItemsPane(onUpload?: (data: UploadDetailsRecord[]) => void): void {
|
||||
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane onUpload={onUpload} />);
|
||||
}
|
||||
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
|
||||
useSidePanel
|
||||
@ -1087,7 +1088,7 @@ export default class Explorer {
|
||||
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
|
||||
}
|
||||
|
||||
public getDownloadModalConent(fileName: string): JSX.Element {
|
||||
public getDownloadModalContent(fileName: string): JSX.Element {
|
||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||
return (
|
||||
<>
|
||||
|
@ -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<UploadItemsPaneProps> = ({onUpload}) => {
|
||||
const [files, setFiles] = useState<FileList>();
|
||||
const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]);
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
@ -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);
|
||||
|
@ -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<Element, Event>)
|
||||
|
||||
// Export to expose to unit tests
|
||||
export type ButtonsDependencies = {
|
||||
_collection: ViewModels.CollectionBase;
|
||||
selectedRows: Set<TableRowId>;
|
||||
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<IDocumentsTabCompone
|
||||
}
|
||||
|
||||
updateNavbarWithTabsButtons(isTabActive, {
|
||||
_collection,
|
||||
selectedRows,
|
||||
editorState,
|
||||
isPreferredApiMongoDB,
|
||||
@ -881,6 +872,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
onSaveExistingDocumentClick,
|
||||
onRevertExistingDocumentClick,
|
||||
onDeleteExistingDocumentsClick,
|
||||
onUploadDocumentsClick,
|
||||
});
|
||||
}, []);
|
||||
|
||||
@ -1286,24 +1278,47 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
);
|
||||
}, [deleteDocuments, documentIds, isPreferredApiMongoDB, selectedRows]);
|
||||
|
||||
const onUploadDocumentsClick = useCallback((): void => {
|
||||
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,
|
||||
clientWriteEnabled,
|
||||
onNewDocumentClick,
|
||||
onSaveNewDocumentClick,
|
||||
onRevertNewDocumentClick,
|
||||
onSaveExistingDocumentClick,
|
||||
onRevertExistingDocumentClick: onRevertExistingDocumentClick,
|
||||
onDeleteExistingDocumentsClick: onDeleteExistingDocumentsClick,
|
||||
}),
|
||||
[
|
||||
_collection,
|
||||
selectedRows,
|
||||
editorState,
|
||||
isPreferredApiMongoDB,
|
||||
@ -1314,6 +1329,20 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||
onSaveExistingDocumentClick,
|
||||
onRevertExistingDocumentClick,
|
||||
onDeleteExistingDocumentsClick,
|
||||
onUploadDocumentsClick,
|
||||
}),
|
||||
[
|
||||
selectedRows,
|
||||
editorState,
|
||||
isPreferredApiMongoDB,
|
||||
clientWriteEnabled,
|
||||
onNewDocumentClick,
|
||||
onSaveNewDocumentClick,
|
||||
onRevertNewDocumentClick,
|
||||
onSaveExistingDocumentClick,
|
||||
onRevertExistingDocumentClick,
|
||||
onDeleteExistingDocumentsClick,
|
||||
onUploadDocumentsClick,
|
||||
isTabActive,
|
||||
],
|
||||
);
|
||||
|
@ -21,7 +21,7 @@ import { readTriggers } from "../../Common/dataAccess/readTriggers";
|
||||
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { UploadDetailsRecord } from "../../Contracts/ViewModels";
|
||||
import { BulkInsertResult, UploadDetailsRecord } from "../../Contracts/ViewModels";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
@ -1092,17 +1092,13 @@ export default class Collection implements ViewModels.Collection {
|
||||
});
|
||||
}
|
||||
|
||||
public async bulkInsertDocuments(documents: JSONObject[]): Promise<{
|
||||
numSucceeded: number;
|
||||
numFailed: number;
|
||||
numThrottled: number;
|
||||
errors: string[];
|
||||
}> {
|
||||
const stats = {
|
||||
public async bulkInsertDocuments(documents: JSONObject[]): Promise<BulkInsertResult> {
|
||||
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++;
|
||||
}
|
||||
|
||||
|
@ -245,7 +245,7 @@ export function downloadItem(
|
||||
},
|
||||
"Cancel",
|
||||
undefined,
|
||||
container.getDownloadModalConent(name),
|
||||
container.getDownloadModalContent(name),
|
||||
);
|
||||
}
|
||||
export async function downloadNotebookItem(
|
||||
|
Loading…
x
Reference in New Issue
Block a user