Compare commits

..

9 Commits

Author SHA1 Message Date
Justin Kolasa (from Dev Box)
f1484a72c8 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-15 16:56:07 -04:00
Justin Kolasa (from Dev Box)
4444f66e91 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-14 10:02:56 -04:00
Justin Kolasa (from Dev Box)
2180eba0a0 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-14 09:47:45 -04:00
Justin Kolasa (from Dev Box)
ea1f0e9b0c Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-13 11:37:09 -04:00
Justin Kolasa (from Dev Box)
f66b78aaf8 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-13 09:28:03 -04:00
Justin Kolasa (from Dev Box)
30182ed503 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-08 08:55:42 -04:00
Justin Kolasa (from Dev Box)
9bf8cffd29 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-05 11:29:18 -04:00
Justin Kolasa (from Dev Box)
cb92bdb003 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-02 11:22:49 -04:00
Justin Kolasa (from Dev Box)
5f4ea0e614 master pull 2025-04-22 08:50:29 -04:00
11 changed files with 63 additions and 130 deletions

4
.npmrc
View File

@@ -1,4 +1,6 @@
save-exact=true
registry=https://msdata.pkgs.visualstudio.com/_packaging/cosmosdbportal/npm/registry/
always-auth=true
allow-same-version=true
# Ignore peer dependency conflicts
force=true # TODO: Remove this when we update to React 17 or higher!

View File

@@ -24,5 +24,6 @@
"source.organizeImports": "explicit"
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "esbenp.prettier-vscode",
"sarif-viewer.connectToGithubCodeScanning": "off"
}

View File

@@ -2869,7 +2869,6 @@ a:link {
z-index: 1000;
overflow-y: auto;
overflow-x: clip;
min-height: fit-content;
}
.uniqueIndexesContainer {

View File

@@ -42,7 +42,6 @@ export interface IBulkDeleteResult {
export const deleteDocuments = async (
collection: CollectionBase,
documentIds: DocumentId[],
abortSignal: AbortSignal,
): Promise<IBulkDeleteResult[]> => {
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
try {
@@ -66,16 +65,12 @@ export const deleteDocuments = async (
operationType: BulkOperationType.Delete,
}));
const promise = v2Container.items
.bulk(operations, undefined, {
abortSignal,
})
.then((bulkResults) => {
return bulkResults.map((bulkResult, index) => {
const documentId = documentIdsChunk[index];
return { ...bulkResult, documentId };
});
const promise = v2Container.items.bulk(operations).then((bulkResults) => {
return bulkResults.map((bulkResult, index) => {
const documentId = documentIdsChunk[index];
return { ...bulkResult, documentId };
});
});
promiseArray.push(promise);
}

View File

@@ -1,5 +1,4 @@
import { FabricMessageTypes } from "./FabricMessageTypes";
import { MessageTypes } from "./MessageTypes";
// This is the current version of these messages
export const DATA_EXPLORER_RPC_VERSION = "3";
@@ -20,22 +19,9 @@ export type DataExploreMessageV3 =
type: FabricMessageTypes.GetAllResourceTokens;
id: string;
}
| {
type: FabricMessageTypes.GetAccessToken;
id: string;
}
| {
type: MessageTypes.TelemetryInfo;
data: {
action: string;
actionModifier: string;
data: unknown;
timestamp: number;
};
}
| {
type: FabricMessageTypes.OpenSettings;
params: [{ settingsId?: "About" | "Connection" }];
settingsId: string;
};
export interface GetCosmosTokenMessageOptions {
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";

View File

@@ -1,5 +1,4 @@
import {
ItemDefinition,
JSONObject,
QueryMetrics,
Resource,
@@ -31,11 +30,8 @@ export interface UploadDetailsRecord {
numFailed: number;
numThrottled: number;
errors: string[];
resources?: ItemDefinition[];
}
export type BulkInsertResult = Omit<UploadDetailsRecord, "fileName">;
export interface QueryResultsMetadata {
hasMoreResults: boolean;
firstItemIndex: number;

View File

@@ -71,7 +71,6 @@ 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();
@@ -1116,8 +1115,8 @@ export default class Explorer {
}
}
public openUploadItemsPane(onUpload?: (data: UploadDetailsRecord[]) => void): void {
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane onUpload={onUpload} />);
public openUploadItemsPane(): void {
useSidePanel.getState().openSidePanel("Upload " + getUploadName(), <UploadItemsPane />);
}
public openExecuteSprocParamsPanel(storedProcedure: StoredProcedure): void {
useSidePanel
@@ -1125,7 +1124,7 @@ export default class Explorer {
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
}
public getDownloadModalContent(fileName: string): JSX.Element {
public getDownloadModalConent(fileName: string): JSX.Element {
if (useNotebook.getState().isPhoenixNotebooks) {
return (
<>

View File

@@ -48,11 +48,7 @@ const classNames = mergeStyleSets({
warning: [{ color: theme.semanticColors.warningIcon }, iconClass],
});
export type UploadItemsPaneProps = {
onUpload?: (data: UploadDetailsRecord[]) => void;
};
export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpload }) => {
export const UploadItemsPane: FunctionComponent = () => {
const [files, setFiles] = useState<FileList>();
const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]);
const [formError, setFormError] = useState<string>("");
@@ -75,8 +71,6 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ onUpl
(uploadDetails) => {
setUploadFileData(uploadDetails.data);
setFiles(undefined);
// Emit the upload details to the parent component
onUpload && onUpload(uploadDetails.data);
},
(error: Error) => {
const errorMessage = getErrorMessage(error);

View File

@@ -26,6 +26,7 @@ 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 {
@@ -63,7 +64,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, UploadDetailsRecord } from "../../../Contracts/ViewModels";
import { CollectionBase } from "../../../Contracts/ViewModels";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as QueryUtils from "../../../Utils/QueryUtils";
import { defaultQueryFields, extractPartitionKeyValues } from "../../../Utils/QueryUtils";
@@ -308,6 +309,7 @@ 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;
@@ -318,7 +320,26 @@ export type ButtonsDependencies = {
onSaveExistingDocumentClick: UiKeyboardEvent;
onRevertExistingDocumentClick: 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
@@ -331,6 +352,7 @@ export const UPLOAD_BUTTON_ID = "uploadItemBtn";
// Export to expose in unit tests
export const getTabsButtons = ({
_collection,
selectedRows,
editorState,
isPreferredApiMongoDB,
@@ -341,7 +363,6 @@ export const getTabsButtons = ({
onSaveExistingDocumentClick,
onRevertExistingDocumentClick,
onDeleteExistingDocumentsClick,
onUploadDocumentsClick,
}: ButtonsDependencies): CommandButtonComponentProps[] => {
if (isFabric() && userContext.fabricContext?.isReadOnly) {
// All the following buttons require write access
@@ -453,20 +474,7 @@ export const getTabsButtons = ({
}
if (!isPreferredApiMongoDB) {
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(),
});
buttons.push(createUploadButton(_collection.container));
}
return buttons;
@@ -671,7 +679,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
collection: CollectionBase;
}>(undefined);
const [bulkDeleteMode, setBulkDeleteMode] = useState<"inProgress" | "completed" | "aborting" | "aborted">(undefined);
const [abortController, setAbortController] = useState<AbortController | undefined>(undefined);
const setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
@@ -699,19 +706,13 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
return;
}
if (bulkDeleteProcess.pendingIds.length === 0 && bulkDeleteProcess.throttledIds.length === 0) {
// Successfully deleted all documents
if (
(bulkDeleteProcess.pendingIds.length === 0 && bulkDeleteProcess.throttledIds.length === 0) ||
bulkDeleteMode === "aborting"
) {
// Successfully deleted all documents or operation was aborted
bulkDeleteOperation.onCompleted(bulkDeleteProcess.successfulIds);
setBulkDeleteMode("completed");
return;
}
if (bulkDeleteMode === "aborting") {
// Operation was aborted
abortController?.abort();
bulkDeleteOperation.onCompleted(bulkDeleteProcess.successfulIds);
setBulkDeleteMode("aborted");
setAbortController(undefined);
setBulkDeleteMode(bulkDeleteMode === "aborting" ? "aborted" : "completed");
return;
}
@@ -719,10 +720,8 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
const newPendingIds = bulkDeleteProcess.pendingIds.concat(bulkDeleteProcess.throttledIds);
const timeout = bulkDeleteProcess.beforeExecuteMs || 0;
const ac = new AbortController();
setAbortController(ac);
setTimeout(() => {
deleteNoSqlDocuments(bulkDeleteOperation.collection, [...newPendingIds], ac.signal)
deleteNoSqlDocuments(bulkDeleteOperation.collection, [...newPendingIds])
.then((deleteResult) => {
let retryAfterMilliseconds = 0;
const newSuccessful: DocumentId[] = [];
@@ -878,6 +877,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
}
updateNavbarWithTabsButtons(isTabActive, {
_collection,
selectedRows,
editorState,
isPreferredApiMongoDB,
@@ -888,7 +888,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
onSaveExistingDocumentClick,
onRevertExistingDocumentClick,
onDeleteExistingDocumentsClick,
onUploadDocumentsClick,
});
}, []);
@@ -1294,47 +1293,11 @@ 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,
@@ -1343,11 +1306,11 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
onSaveNewDocumentClick,
onRevertNewDocumentClick,
onSaveExistingDocumentClick,
onRevertExistingDocumentClick,
onDeleteExistingDocumentsClick,
onUploadDocumentsClick,
onRevertExistingDocumentClick: onRevertExistingDocumentClick,
onDeleteExistingDocumentsClick: onDeleteExistingDocumentsClick,
}),
[
_collection,
selectedRows,
editorState,
isPreferredApiMongoDB,
@@ -1358,7 +1321,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
onSaveExistingDocumentClick,
onRevertExistingDocumentClick,
onDeleteExistingDocumentsClick,
onUploadDocumentsClick,
isTabActive,
],
);

View File

@@ -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 { BulkInsertResult, UploadDetailsRecord } from "../../Contracts/ViewModels";
import { UploadDetailsRecord } from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
@@ -1092,13 +1092,17 @@ export default class Collection implements ViewModels.Collection {
});
}
public async bulkInsertDocuments(documents: JSONObject[]): Promise<BulkInsertResult> {
const stats: BulkInsertResult = {
public async bulkInsertDocuments(documents: JSONObject[]): Promise<{
numSucceeded: number;
numFailed: number;
numThrottled: number;
errors: string[];
}> {
const stats = {
numSucceeded: 0,
numFailed: 0,
numThrottled: 0,
errors: [] as string[],
resources: [],
};
const chunkSize = 100; // 100 is the max # of bulk operations the SDK currently accepts
@@ -1116,7 +1120,6 @@ 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) {
@@ -1149,22 +1152,18 @@ 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, resources } =
await this.bulkInsertDocuments(parsedContent);
const { numSucceeded, numFailed, numThrottled, errors } = await this.bulkInsertDocuments(parsedContent);
record.numSucceeded = numSucceeded;
record.numFailed = numFailed;
record.numThrottled = numThrottled;
record.errors = errors;
record.resources = record.resources.concat(resources);
} else {
const resource = await createDocument(this, parsedContent);
record.resources.push(resource);
await createDocument(this, parsedContent);
record.numSucceeded++;
}

View File

@@ -245,7 +245,7 @@ export function downloadItem(
},
"Cancel",
undefined,
container.getDownloadModalContent(name),
container.getDownloadModalConent(name),
);
}
export async function downloadNotebookItem(