in src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx [862:1393]
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
onLoadStartKey,
);
setOnLoadStartKey(undefined);
}
}
}
updateNavbarWithTabsButtons(isTabActive, {
_collection,
selectedRows,
editorState,
isPreferredApiMongoDB,
clientWriteEnabled,
onNewDocumentClick,
onSaveNewDocumentClick,
onRevertNewDocumentClick,
onSaveExistingDocumentClick,
onRevertExistingDocumentClick,
onDeleteExistingDocumentsClick,
});
}, []);
const isEditorDirty = useCallback((): boolean => {
switch (editorState) {
case ViewModels.DocumentExplorerState.noDocumentSelected:
case ViewModels.DocumentExplorerState.existingDocumentNoEdits:
return false;
case ViewModels.DocumentExplorerState.newDocumentValid:
case ViewModels.DocumentExplorerState.newDocumentInvalid:
case ViewModels.DocumentExplorerState.existingDocumentDirtyInvalid:
return true;
case ViewModels.DocumentExplorerState.existingDocumentDirtyValid:
return true;
default:
return false;
}
}, [editorState]);
const confirmDiscardingChange = useCallback(
(onDiscard: () => void, onCancelDiscard?: () => void): void => {
if (isEditorDirty()) {
useDialog
.getState()
.showOkCancelModalDialog(
"Unsaved changes",
"Your unsaved changes will be lost. Do you want to continue?",
"OK",
onDiscard,
"Cancel",
onCancelDiscard,
);
} else {
onDiscard();
}
},
[isEditorDirty],
);
// Update parent (tab) if isExecuting has changed
useEffect(() => {
onIsExecutingChange(isExecuting);
}, [onIsExecutingChange, isExecuting]);
const onNewDocumentClick = useCallback(
(): void => confirmDiscardingChange(() => initializeNewDocument()),
[confirmDiscardingChange],
);
const initializeNewDocument = (): void => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const newDocument: any = {
id: "replace_with_new_document_id",
};
partitionKeyProperties.forEach((partitionKeyProperty) => {
let target = newDocument;
const keySegments = partitionKeyProperty.split(".");
const finalSegment = keySegments.pop();
// Initialize nested objects as needed
keySegments.forEach((segment) => {
target = target[segment] = target[segment] || {};
});
target[finalSegment] = "replace_with_new_partition_key_value";
});
const defaultDocument: string = renderObjectForEditor(newDocument, null, 4);
setInitialDocumentContent(defaultDocument);
setSelectedDocumentContent(defaultDocument);
setSelectedDocumentContentBaseline(defaultDocument);
setSelectedRows(new Set());
setClickedRowIndex(undefined);
setEditorState(ViewModels.DocumentExplorerState.newDocumentValid);
};
let onSaveNewDocumentClick = useCallback((): Promise<unknown> => {
onExecutionErrorChange(false);
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle,
});
const sanitizedContent = selectedDocumentContent.replace("\n", "");
const document = JSON.parse(sanitizedContent);
setIsExecuting(true);
return createDocument(_collection, document)
.then(
(savedDocument: DataModels.DocumentId) => {
// TODO: Reuse initDocumentEditor() to remove code duplication
const value: string = renderObjectForEditor(savedDocument || {}, null, 4);
setSelectedDocumentContentBaseline(value);
setSelectedDocumentContent(value);
setInitialDocumentContent(value);
const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues(
savedDocument,
partitionKey as PartitionKeyDefinition,
);
const id = newDocumentId(savedDocument, partitionKeyProperties, partitionKeyValueArray as string[]);
const ids = documentIds;
ids.push(id);
setDocumentIds(ids);
setEditorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits);
// Update column choices
setColumnDefinitionsFromDocument(savedDocument);
TelemetryProcessor.traceSuccess(
Action.CreateDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle,
},
startKey,
);
},
(error) => {
onExecutionErrorChange(true);
const errorMessage = getErrorMessage(error);
useDialog.getState().showOkModalDialog("Create document failed", errorMessage);
TelemetryProcessor.traceFailure(
Action.CreateDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle,
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey,
);
},
)
.then(() => {
setSelectedRows(new Set([documentIds.length - 1]));
setClickedRowIndex(documentIds.length - 1);
})
.finally(() => setIsExecuting(false));
}, [
onExecutionErrorChange,
tabTitle,
selectedDocumentContent,
_collection,
partitionKey,
newDocumentId,
partitionKeyProperties,
documentIds,
]);
const onRevertNewDocumentClick = useCallback((): void => {
setInitialDocumentContent("");
setSelectedDocumentContent("");
setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected);
}, [setInitialDocumentContent, setSelectedDocumentContent, setEditorState]);
let onSaveExistingDocumentClick = useCallback((): Promise<void> => {
const documentContent = JSON.parse(selectedDocumentContent);
const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues(
documentContent,
partitionKey as PartitionKeyDefinition,
);
const selectedDocumentId = documentIds[clickedRowIndex as number];
const originalPartitionKeyValue = selectedDocumentId.partitionKeyValue;
selectedDocumentId.partitionKeyValue = partitionKeyValueArray;
onExecutionErrorChange(false);
const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle,
});
setIsExecuting(true);
return updateDocument(_collection, selectedDocumentId, documentContent)
.then(
(updatedDocument: Item & { _rid: string }) => {
const value: string = renderObjectForEditor(updatedDocument || {}, null, 4);
setSelectedDocumentContentBaseline(value);
setInitialDocumentContent(value);
setSelectedDocumentContent(value);
documentIds.forEach((documentId: DocumentId) => {
if (documentId.rid === updatedDocument._rid) {
documentId.id(updatedDocument.id);
}
});
setEditorState(ViewModels.DocumentExplorerState.existingDocumentNoEdits);
TelemetryProcessor.traceSuccess(
Action.UpdateDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle,
},
startKey,
);
// Update column choices
selectedDocumentId.tableFields = { ...documentContent };
setColumnDefinitionsFromDocument(documentContent);
},
(error) => {
// in case of any kind of failures of accidently changing partition key, restore the original
// so that when user navigates away from current document and comes back,
// it doesnt fail to load due to using the invalid partition keys
selectedDocumentId.partitionKeyValue = originalPartitionKeyValue;
onExecutionErrorChange(true);
const errorMessage = getErrorMessage(error);
useDialog.getState().showOkModalDialog("Update document failed", errorMessage);
TelemetryProcessor.traceFailure(
Action.UpdateDocument,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle,
error: errorMessage,
errorStack: getErrorStack(error),
},
startKey,
);
},
)
.finally(() => setIsExecuting(false));
}, [
onExecutionErrorChange,
tabTitle,
selectedDocumentContent,
_collection,
partitionKey,
documentIds,
clickedRowIndex,
]);
const onRevertExistingDocumentClick = useCallback((): void => {
setSelectedDocumentContentBaseline(initialDocumentContent);
setSelectedDocumentContent(selectedDocumentContentBaseline);
}, [initialDocumentContent, selectedDocumentContentBaseline, setSelectedDocumentContent]);
/**
* Trigger a useEffect() to bulk delete noSql documents
* @param collection
* @param documentIds
* @returns
*/
const _bulkDeleteNoSqlDocuments = (collection: CollectionBase, documentIds: DocumentId[]): Promise<DocumentId[]> =>
new Promise<DocumentId[]>((resolve, reject) => {
setBulkDeleteOperation({
onCompleted: resolve,
onFailed: reject,
count: documentIds.length,
collection,
});
setBulkDeleteProcess({
pendingIds: [...documentIds],
throttledIds: [],
successfulIds: [],
failedIds: [],
beforeExecuteMs: 0,
hasBeenThrottled: false,
});
setIsBulkDeleteDialogOpen(true);
setBulkDeleteMode("inProgress");
});
/**
* Implementation using bulk delete NoSQL API
* @param list of document ids to delete
* @returns Promise of list of deleted document ids
*/
const _deleteDocuments = useCallback(
async (toDeleteDocumentIds: DocumentId[]): Promise<DocumentId[]> => {
onExecutionErrorChange(false);
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocuments, {
dataExplorerArea: Constants.Areas.Tab,
tabTitle,
});
setIsExecuting(true);
let deletePromise;
if (!isPreferredApiMongoDB) {
if (partitionKey.systemKey) {
// ----------------------------------------------------------------------------------------------------
// TODO: Once JS SDK Bug fix for bulk deleting legacy containers (whose systemKey==1) is released:
// Remove the check for systemKey, remove call to deleteNoSqlDocument(). deleteNoSqlDocuments() should
// always be called for NoSQL.
deletePromise = deleteNoSqlDocument(_collection, toDeleteDocumentIds[0]).then(() => {
useDialog.getState().showOkModalDialog("Delete document", "Document successfully deleted.");
return [toDeleteDocumentIds[0]];
});
// ----------------------------------------------------------------------------------------------------
} else {
deletePromise = _bulkDeleteNoSqlDocuments(_collection, toDeleteDocumentIds);
}
} else {
deletePromise = MongoProxyClient.deleteDocuments(
_collection.databaseId,
_collection as ViewModels.Collection,
toDeleteDocumentIds,
).then(({ deletedCount, isAcknowledged }) => {
if (deletedCount === toDeleteDocumentIds.length && isAcknowledged) {
return toDeleteDocumentIds;
}
throw new Error(`Delete failed with deletedCount: ${deletedCount} and isAcknowledged: ${isAcknowledged}`);
});
}
return deletePromise
.then(
(deletedIds) => {
TelemetryProcessor.traceSuccess(
Action.DeleteDocuments,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle,
},
startKey,
);
return deletedIds;
},
(error) => {
onExecutionErrorChange(true);
console.error(error);
TelemetryProcessor.traceFailure(
Action.DeleteDocuments,
{
dataExplorerArea: Constants.Areas.Tab,
tabTitle,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey,
);
throw error;
},
)
.finally(() => {
setIsExecuting(false);
});
},
[_collection, isPreferredApiMongoDB, onExecutionErrorChange, tabTitle, partitionKey.systemKey],
);
const deleteDocuments = useCallback(
(toDeleteDocumentIds: DocumentId[]): void => {
onExecutionErrorChange(false);
setIsExecuting(true);
_deleteDocuments(toDeleteDocumentIds)
.then(
(deletedIds: DocumentId[]) => {
const deletedIdsSet = new Set(deletedIds.map((documentId) => documentId.id));
const newDocumentIds = [...documentIds.filter((documentId) => !deletedIdsSet.has(documentId.id))];
setDocumentIds(newDocumentIds);
setSelectedDocumentContent(undefined);
setClickedRowIndex(undefined);
setSelectedRows(new Set());
setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected);
},
(error: Error) => {
if (error instanceof MongoProxyClient.ThrottlingError) {
useDialog
.getState()
.showOkModalDialog(
"Delete documents",
`Some documents failed to delete due to a rate limiting error. Please try again later. To prevent this in the future, consider increasing the throughput on your container or database.`,
{
linkText: "Learn More",
linkUrl: MONGO_THROTTLING_DOC_URL,
},
);
} else {
useDialog
.getState()
.showOkModalDialog("Delete documents", `Deleting document(s) failed (${error.message})`);
}
},
)
.finally(() => setIsExecuting(false));
},
[onExecutionErrorChange, _deleteDocuments, documentIds],
);
const onDeleteExistingDocumentsClick = useCallback(async (): Promise<void> => {
// TODO: Rework this for localization
const isPlural = selectedRows.size > 1;
const documentName = !isPreferredApiMongoDB
? isPlural
? `the selected ${selectedRows.size} items`
: "the selected item"
: isPlural
? `the selected ${selectedRows.size} documents`
: "the selected document";
const msg = `Are you sure you want to delete ${documentName}?`;
useDialog
.getState()
.showOkCancelModalDialog(
"Confirm delete",
msg,
"Delete",
() => deleteDocuments(Array.from(selectedRows).map((index) => documentIds[index as number])),
"Cancel",
undefined,
);
}, [deleteDocuments, documentIds, isPreferredApiMongoDB, selectedRows]);
// 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,
clientWriteEnabled,
onNewDocumentClick,
onSaveNewDocumentClick,
onRevertNewDocumentClick,
onSaveExistingDocumentClick,
onRevertExistingDocumentClick,
onDeleteExistingDocumentsClick,
isTabActive,
],
);
const queryTimeoutEnabled = useCallback(
(): boolean => !isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled),
[isPreferredApiMongoDB],
);
const createIterator = useCallback((): QueryIterator<ItemDefinition & Resource> => {
const _queryAbortController = new AbortController();
setQueryAbortController(_queryAbortController);
const filter: string = filterContent.trim();
const query: string = buildQuery(
isPreferredApiMongoDB,
filter,
partitionKeyProperties,
partitionKey,
selectedColumnIds,
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const options: any = {};
// TODO: Property 'enableCrossPartitionQuery' does not exist on type 'FeedOptions'.
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
if (resourceTokenPartitionKey) {
options.partitionKey = resourceTokenPartitionKey;
}
// Fixes compile error error TS2741: Property 'throwIfAborted' is missing in type 'AbortSignal' but required in type 'import("/home/runner/work/cosmos-explorer/cosmos-explorer/node_modules/node-abort-controller/index").AbortSignal'.
options.abortSignal = _queryAbortController.signal;
return isQueryCopilotSampleContainer
? querySampleDocuments(query, options)
: queryDocuments(_collection.databaseId, _collection.id(), query, options);
}, [
filterContent,
isPreferredApiMongoDB,
partitionKeyProperties,
partitionKey,
resourceTokenPartitionKey,
isQueryCopilotSampleContainer,
_collection,
selectedColumnIds,
]);
const updateDocumentIds = (newDocumentsIds: DocumentId[]): void => {
setDocumentIds(newDocumentsIds);
if (onLoadStartKey !== null && onLoadStartKey !== undefined) {
TelemetryProcessor.traceSuccess(
Action.Tab,
{
databaseName: _collection.databaseId,
collectionName: _collection.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle,
},
onLoadStartKey,
);
setOnLoadStartKey(undefined);
}
};
let loadNextPage = useCallback(
(iterator: QueryIterator<ItemDefinition & Resource>, applyFilterButtonClicked: boolean): Promise<unknown> => {
setIsExecuting(true);
onExecutionErrorChange(false);
let automaticallyCancelQueryAfterTimeout: boolean;
if (applyFilterButtonClicked && queryTimeoutEnabled()) {
const queryTimeout: number = LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout);
automaticallyCancelQueryAfterTimeout = LocalStorageUtility.getEntryBoolean(
StorageKey.AutomaticallyCancelQueryAfterTimeout,
);
const cancelQueryTimeoutID: NodeJS.Timeout = setTimeout(() => {
if (isExecuting) {
if (automaticallyCancelQueryAfterTimeout) {