mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-05-11 02:44:21 +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 {
|
import {
|
||||||
|
ItemDefinition,
|
||||||
JSONObject,
|
JSONObject,
|
||||||
QueryMetrics,
|
QueryMetrics,
|
||||||
Resource,
|
Resource,
|
||||||
@ -30,8 +31,11 @@ export interface UploadDetailsRecord {
|
|||||||
numFailed: number;
|
numFailed: number;
|
||||||
numThrottled: number;
|
numThrottled: number;
|
||||||
errors: string[];
|
errors: string[];
|
||||||
|
resources?: ItemDefinition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type BulkInsertResult = Omit<UploadDetailsRecord, "fileName">;
|
||||||
|
|
||||||
export interface QueryResultsMetadata {
|
export interface QueryResultsMetadata {
|
||||||
hasMoreResults: boolean;
|
hasMoreResults: boolean;
|
||||||
firstItemIndex: number;
|
firstItemIndex: number;
|
||||||
|
@ -70,6 +70,7 @@ import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter";
|
|||||||
import StoredProcedure from "./Tree/StoredProcedure";
|
import StoredProcedure from "./Tree/StoredProcedure";
|
||||||
import { useDatabases } from "./useDatabases";
|
import { useDatabases } from "./useDatabases";
|
||||||
import { useSelectedNode } from "./useSelectedNode";
|
import { useSelectedNode } from "./useSelectedNode";
|
||||||
|
import { UploadDetailsRecord } from "../Contracts/ViewModels";
|
||||||
|
|
||||||
BindingHandlersRegisterer.registerBindingHandlers();
|
BindingHandlersRegisterer.registerBindingHandlers();
|
||||||
|
|
||||||
@ -1078,8 +1079,8 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public openUploadItemsPane(): void {
|
public openUploadItemsPane(onUpload?: (data: UploadDetailsRecord[]) => void): void {
|
||||||
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
|
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane onUpload={onUpload} />);
|
||||||
}
|
}
|
||||||
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
|
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
|
||||||
useSidePanel
|
useSidePanel
|
||||||
@ -1087,7 +1088,7 @@ export default class Explorer {
|
|||||||
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
|
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDownloadModalConent(fileName: string): JSX.Element {
|
public getDownloadModalContent(fileName: string): JSX.Element {
|
||||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -14,7 +14,11 @@ import { getErrorMessage } from "../../Tables/Utilities";
|
|||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
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 [files, setFiles] = useState<FileList>();
|
||||||
const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]);
|
const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]);
|
||||||
const [formError, setFormError] = useState<string>("");
|
const [formError, setFormError] = useState<string>("");
|
||||||
@ -37,6 +41,8 @@ export const UploadItemsPane: FunctionComponent = () => {
|
|||||||
(uploadDetails) => {
|
(uploadDetails) => {
|
||||||
setUploadFileData(uploadDetails.data);
|
setUploadFileData(uploadDetails.data);
|
||||||
setFiles(undefined);
|
setFiles(undefined);
|
||||||
|
// Emit the upload details to the parent component
|
||||||
|
onUpload && onUpload(uploadDetails.data);
|
||||||
},
|
},
|
||||||
(error: Error) => {
|
(error: Error) => {
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
|
@ -26,7 +26,6 @@ import { useDialog } from "Explorer/Controls/Dialog";
|
|||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { InputDataList, InputDatalistDropdownOptionSection } from "Explorer/Controls/InputDataList/InputDataList";
|
import { InputDataList, InputDatalistDropdownOptionSection } from "Explorer/Controls/InputDataList/InputDataList";
|
||||||
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
import { ProgressModalDialog } from "Explorer/Controls/ProgressModalDialog";
|
||||||
import Explorer from "Explorer/Explorer";
|
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import {
|
import {
|
||||||
@ -64,7 +63,7 @@ import * as Logger from "../../../Common/Logger";
|
|||||||
import * as MongoProxyClient from "../../../Common/MongoProxyClient";
|
import * as MongoProxyClient from "../../../Common/MongoProxyClient";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
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 TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import * as QueryUtils from "../../../Utils/QueryUtils";
|
import * as QueryUtils from "../../../Utils/QueryUtils";
|
||||||
import { defaultQueryFields, extractPartitionKeyValues } 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 to expose to unit tests
|
||||||
export type ButtonsDependencies = {
|
export type ButtonsDependencies = {
|
||||||
_collection: ViewModels.CollectionBase;
|
|
||||||
selectedRows: Set<TableRowId>;
|
selectedRows: Set<TableRowId>;
|
||||||
editorState: ViewModels.DocumentExplorerState;
|
editorState: ViewModels.DocumentExplorerState;
|
||||||
isPreferredApiMongoDB: boolean;
|
isPreferredApiMongoDB: boolean;
|
||||||
@ -313,26 +311,7 @@ export type ButtonsDependencies = {
|
|||||||
onSaveExistingDocumentClick: UiKeyboardEvent;
|
onSaveExistingDocumentClick: UiKeyboardEvent;
|
||||||
onRevertExistingDocumentClick: UiKeyboardEvent;
|
onRevertExistingDocumentClick: UiKeyboardEvent;
|
||||||
onDeleteExistingDocumentsClick: UiKeyboardEvent;
|
onDeleteExistingDocumentsClick: UiKeyboardEvent;
|
||||||
};
|
onUploadDocumentsClick: 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(),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export to expose to unit tests
|
// Export to expose to unit tests
|
||||||
@ -345,7 +324,6 @@ export const UPLOAD_BUTTON_ID = "uploadItemBtn";
|
|||||||
|
|
||||||
// Export to expose in unit tests
|
// Export to expose in unit tests
|
||||||
export const getTabsButtons = ({
|
export const getTabsButtons = ({
|
||||||
_collection,
|
|
||||||
selectedRows,
|
selectedRows,
|
||||||
editorState,
|
editorState,
|
||||||
isPreferredApiMongoDB,
|
isPreferredApiMongoDB,
|
||||||
@ -356,6 +334,7 @@ export const getTabsButtons = ({
|
|||||||
onSaveExistingDocumentClick,
|
onSaveExistingDocumentClick,
|
||||||
onRevertExistingDocumentClick,
|
onRevertExistingDocumentClick,
|
||||||
onDeleteExistingDocumentsClick,
|
onDeleteExistingDocumentsClick,
|
||||||
|
onUploadDocumentsClick,
|
||||||
}: ButtonsDependencies): CommandButtonComponentProps[] => {
|
}: ButtonsDependencies): CommandButtonComponentProps[] => {
|
||||||
if (isFabric() && userContext.fabricContext?.isReadOnly) {
|
if (isFabric() && userContext.fabricContext?.isReadOnly) {
|
||||||
// All the following buttons require write access
|
// All the following buttons require write access
|
||||||
@ -467,7 +446,20 @@ export const getTabsButtons = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isPreferredApiMongoDB) {
|
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;
|
return buttons;
|
||||||
@ -870,7 +862,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateNavbarWithTabsButtons(isTabActive, {
|
updateNavbarWithTabsButtons(isTabActive, {
|
||||||
_collection,
|
|
||||||
selectedRows,
|
selectedRows,
|
||||||
editorState,
|
editorState,
|
||||||
isPreferredApiMongoDB,
|
isPreferredApiMongoDB,
|
||||||
@ -881,6 +872,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
onSaveExistingDocumentClick,
|
onSaveExistingDocumentClick,
|
||||||
onRevertExistingDocumentClick,
|
onRevertExistingDocumentClick,
|
||||||
onDeleteExistingDocumentsClick,
|
onDeleteExistingDocumentsClick,
|
||||||
|
onUploadDocumentsClick,
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -1286,24 +1278,47 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
);
|
);
|
||||||
}, [deleteDocuments, documentIds, isPreferredApiMongoDB, selectedRows]);
|
}, [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
|
// If editor state changes, update the nav
|
||||||
useEffect(
|
useEffect(
|
||||||
() =>
|
() =>
|
||||||
updateNavbarWithTabsButtons(isTabActive, {
|
updateNavbarWithTabsButtons(isTabActive, {
|
||||||
_collection,
|
|
||||||
selectedRows,
|
|
||||||
editorState,
|
|
||||||
isPreferredApiMongoDB,
|
|
||||||
clientWriteEnabled,
|
|
||||||
onNewDocumentClick,
|
|
||||||
onSaveNewDocumentClick,
|
|
||||||
onRevertNewDocumentClick,
|
|
||||||
onSaveExistingDocumentClick,
|
|
||||||
onRevertExistingDocumentClick: onRevertExistingDocumentClick,
|
|
||||||
onDeleteExistingDocumentsClick: onDeleteExistingDocumentsClick,
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
_collection,
|
|
||||||
selectedRows,
|
selectedRows,
|
||||||
editorState,
|
editorState,
|
||||||
isPreferredApiMongoDB,
|
isPreferredApiMongoDB,
|
||||||
@ -1314,6 +1329,20 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
|||||||
onSaveExistingDocumentClick,
|
onSaveExistingDocumentClick,
|
||||||
onRevertExistingDocumentClick,
|
onRevertExistingDocumentClick,
|
||||||
onDeleteExistingDocumentsClick,
|
onDeleteExistingDocumentsClick,
|
||||||
|
onUploadDocumentsClick,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
selectedRows,
|
||||||
|
editorState,
|
||||||
|
isPreferredApiMongoDB,
|
||||||
|
clientWriteEnabled,
|
||||||
|
onNewDocumentClick,
|
||||||
|
onSaveNewDocumentClick,
|
||||||
|
onRevertNewDocumentClick,
|
||||||
|
onSaveExistingDocumentClick,
|
||||||
|
onRevertExistingDocumentClick,
|
||||||
|
onDeleteExistingDocumentsClick,
|
||||||
|
onUploadDocumentsClick,
|
||||||
isTabActive,
|
isTabActive,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -21,7 +21,7 @@ import { readTriggers } from "../../Common/dataAccess/readTriggers";
|
|||||||
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
|
import { readUserDefinedFunctions } from "../../Common/dataAccess/readUserDefinedFunctions";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
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 { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
@ -1092,17 +1092,13 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async bulkInsertDocuments(documents: JSONObject[]): Promise<{
|
public async bulkInsertDocuments(documents: JSONObject[]): Promise<BulkInsertResult> {
|
||||||
numSucceeded: number;
|
const stats: BulkInsertResult = {
|
||||||
numFailed: number;
|
|
||||||
numThrottled: number;
|
|
||||||
errors: string[];
|
|
||||||
}> {
|
|
||||||
const stats = {
|
|
||||||
numSucceeded: 0,
|
numSucceeded: 0,
|
||||||
numFailed: 0,
|
numFailed: 0,
|
||||||
numThrottled: 0,
|
numThrottled: 0,
|
||||||
errors: [] as string[],
|
errors: [] as string[],
|
||||||
|
resources: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const chunkSize = 100; // 100 is the max # of bulk operations the SDK currently accepts
|
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) => {
|
responses.forEach((response, index) => {
|
||||||
if (response.statusCode === 201) {
|
if (response.statusCode === 201) {
|
||||||
stats.numSucceeded++;
|
stats.numSucceeded++;
|
||||||
|
stats.resources.push(response.resourceBody);
|
||||||
} else if (response.statusCode === 429) {
|
} else if (response.statusCode === 429) {
|
||||||
documentsToAttempt.push(attemptedDocuments[index]);
|
documentsToAttempt.push(attemptedDocuments[index]);
|
||||||
} else {
|
} else {
|
||||||
@ -1149,18 +1146,22 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
numFailed: 0,
|
numFailed: 0,
|
||||||
numThrottled: 0,
|
numThrottled: 0,
|
||||||
errors: [],
|
errors: [],
|
||||||
|
resources: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsedContent = JSON.parse(documentContent);
|
const parsedContent = JSON.parse(documentContent);
|
||||||
if (Array.isArray(parsedContent)) {
|
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.numSucceeded = numSucceeded;
|
||||||
record.numFailed = numFailed;
|
record.numFailed = numFailed;
|
||||||
record.numThrottled = numThrottled;
|
record.numThrottled = numThrottled;
|
||||||
record.errors = errors;
|
record.errors = errors;
|
||||||
|
record.resources = record.resources.concat(resources);
|
||||||
} else {
|
} else {
|
||||||
await createDocument(this, parsedContent);
|
const resource = await createDocument(this, parsedContent);
|
||||||
|
record.resources.push(resource);
|
||||||
record.numSucceeded++;
|
record.numSucceeded++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,7 +245,7 @@ export function downloadItem(
|
|||||||
},
|
},
|
||||||
"Cancel",
|
"Cancel",
|
||||||
undefined,
|
undefined,
|
||||||
container.getDownloadModalConent(name),
|
container.getDownloadModalContent(name),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export async function downloadNotebookItem(
|
export async function downloadNotebookItem(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user