error: getErrorMessage()

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) {