From 08879984618fc4cb199943790e63f84721781cde Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Mon, 29 Apr 2024 14:27:00 +0200 Subject: [PATCH] Fix typo and format --- .../Tabs/DocumentsTabV2/DocumentsTabV2.tsx | 2534 ++++++++--------- src/Explorer/Theme/ThemeUtil.ts | 2 +- src/Platform/Fabric/FabricTheme.tsx | 34 +- 3 files changed, 1285 insertions(+), 1285 deletions(-) diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index 0bd9d8299..db1651be7 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -160,7 +160,7 @@ const getSaveExistingDocumentButtonState = (editorState: ViewModels.DocumentExpl })(), }); -const getDiscardExisitingDocumentChangesButtonState = (editorState: ViewModels.DocumentExplorerState) => ({ +const getDiscardExistingDocumentChangesButtonState = (editorState: ViewModels.DocumentExplorerState) => ({ enabled: (() => { switch (editorState) { case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: @@ -183,7 +183,7 @@ const getDiscardExisitingDocumentChangesButtonState = (editorState: ViewModels.D })(), }); -const getDeleteExisitingDocumentButtonState = ( +const getDeleteExistingDocumentButtonState = ( editorState: ViewModels.DocumentExplorerState, selectedRows: Set, ) => ({ @@ -220,8 +220,8 @@ type ButtonsDependencies = { onSaveNewDocumentClick: UiKeyboardEvent; onRevertNewDocumentClick: UiKeyboardEvent; onSaveExistingDocumentClick: UiKeyboardEvent; - onRevertExisitingDocumentClick: UiKeyboardEvent; - onDeleteExisitingDocumentsClick: UiKeyboardEvent; + onRevertExistingDocumentClick: UiKeyboardEvent; + onDeleteExistingDocumentsClick: UiKeyboardEvent; }; const getTabsButtons = ({ @@ -233,8 +233,8 @@ const getTabsButtons = ({ onSaveNewDocumentClick, onRevertNewDocumentClick, onSaveExistingDocumentClick, - onRevertExisitingDocumentClick, - onDeleteExisitingDocumentsClick, + onRevertExistingDocumentClick, + onDeleteExistingDocumentsClick, }: ButtonsDependencies): CommandButtonComponentProps[] => { if (configContext.platform === Platform.Fabric && userContext.fabricContext?.isReadOnly) { // All the following buttons require write access @@ -307,34 +307,34 @@ const getTabsButtons = ({ }); } - if (getDiscardExisitingDocumentChangesButtonState(editorState).visible) { + if (getDiscardExistingDocumentChangesButtonState(editorState).visible) { const label = "Discard"; buttons.push({ iconSrc: DiscardIcon, iconAlt: label, keyboardAction: KeyboardAction.CANCEL_OR_DISCARD, - onCommandClick: onRevertExisitingDocumentClick, + onCommandClick: onRevertExistingDocumentClick, commandButtonLabel: label, ariaLabel: label, hasPopup: false, disabled: - !getDiscardExisitingDocumentChangesButtonState(editorState).enabled || + !getDiscardExistingDocumentChangesButtonState(editorState).enabled || useSelectedNode.getState().isQueryCopilotCollectionSelected(), }); } - if (getDeleteExisitingDocumentButtonState(editorState, selectedRows).visible) { + if (getDeleteExistingDocumentButtonState(editorState, selectedRows).visible) { const label = "Delete"; buttons.push({ iconSrc: DeleteDocumentIcon, iconAlt: label, keyboardAction: KeyboardAction.DELETE_ITEM, - onCommandClick: onDeleteExisitingDocumentsClick, + onCommandClick: onDeleteExistingDocumentsClick, commandButtonLabel: label, ariaLabel: label, hasPopup: false, disabled: - !getDeleteExisitingDocumentButtonState(editorState, selectedRows).enabled || + !getDeleteExistingDocumentButtonState(editorState, selectedRows).enabled || useSelectedNode.getState().isQueryCopilotCollectionSelected(), }); } @@ -392,128 +392,453 @@ const DocumentsTabComponent: React.FunctionComponent<{ onExecutionErrorChange, onIsExecutingChange, }) => { - const [isFilterCreated, setIsFilterCreated] = useState(true); - const [isFilterExpanded, setIsFilterExpanded] = useState(false); - const [appliedFilter, setAppliedFilter] = useState(""); - const [filterContent, setFilterContent] = useState(""); - // const [lastFilterContents, setLastFilterContents] = useState([ - // 'WHERE c.id = "foo"', - // "ORDER BY c._ts DESC", - // 'WHERE c.id = "foo" ORDER BY c._ts DESC', - // ]); - const [documentIds, setDocumentIds] = useState([]); - const [isExecuting, setIsExecuting] = useState(false); // TODO isExecuting is a member of TabsBase. We may need to update this field. + const [isFilterCreated, setIsFilterCreated] = useState(true); + const [isFilterExpanded, setIsFilterExpanded] = useState(false); + const [appliedFilter, setAppliedFilter] = useState(""); + const [filterContent, setFilterContent] = useState(""); + // const [lastFilterContents, setLastFilterContents] = useState([ + // 'WHERE c.id = "foo"', + // "ORDER BY c._ts DESC", + // 'WHERE c.id = "foo" ORDER BY c._ts DESC', + // ]); + const [documentIds, setDocumentIds] = useState([]); + const [isExecuting, setIsExecuting] = useState(false); // TODO isExecuting is a member of TabsBase. We may need to update this field. - // Query - const [documentsIterator, setDocumentsIterator] = useState<{ - iterator: QueryIterator; - applyFilterButtonPressed: boolean; - }>(undefined); - const [queryAbortController, setQueryAbortController] = useState(undefined); - const [cancelQueryTimeoutID, setCancelQueryTimeoutID] = useState(undefined); + // Query + const [documentsIterator, setDocumentsIterator] = useState<{ + iterator: QueryIterator; + applyFilterButtonPressed: boolean; + }>(undefined); + const [queryAbortController, setQueryAbortController] = useState(undefined); + const [cancelQueryTimeoutID, setCancelQueryTimeoutID] = useState(undefined); - const [onLoadStartKey, setOnLoadStartKey] = useState(_onLoadStartKey); + const [onLoadStartKey, setOnLoadStartKey] = useState(_onLoadStartKey); - const [initialDocumentContent, setInitialDocumentContent] = useState(undefined); - const [selectedDocumentContent, setSelectedDocumentContent] = useState(undefined); - const [selectedDocumentContentBaseline, setSelectedDocumentContentBaseline] = useState(undefined); + const [initialDocumentContent, setInitialDocumentContent] = useState(undefined); + const [selectedDocumentContent, setSelectedDocumentContent] = useState(undefined); + const [selectedDocumentContentBaseline, setSelectedDocumentContentBaseline] = useState(undefined); - // Table user clicked on this row - const [clickedRow, setClickedRow] = useState(undefined); - // Table multiple selection - const [selectedRows, setSelectedRows] = React.useState>(() => new Set([0])); + // Table user clicked on this row + const [clickedRow, setClickedRow] = useState(undefined); + // Table multiple selection + const [selectedRows, setSelectedRows] = React.useState>(() => new Set([0])); - // Command buttons - const [editorState, setEditorState] = useState( - ViewModels.DocumentExplorerState.noDocumentSelected, + // Command buttons + const [editorState, setEditorState] = useState( + ViewModels.DocumentExplorerState.noDocumentSelected, + ); + + const isQueryCopilotSampleContainer = + _collection?.isSampleCollection && + _collection?.databaseId === QueryCopilotSampleDatabaseId && + _collection?.id() === QueryCopilotSampleContainerId; + + // For Mongo only + const [continuationToken, setContinuationToken] = useState(undefined); + + let lastFilterContents = ['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC']; + + const applyFilterButton = { + enabled: true, + visible: true, + }; + + const partitionKey: DataModels.PartitionKey = useMemo( + () => _partitionKey || (_collection && _collection.partitionKey), + [_collection, _partitionKey], + ); + const partitionKeyPropertyHeaders: string[] = useMemo( + () => _collection?.partitionKeyPropertyHeaders || partitionKey?.paths, + [_collection?.partitionKeyPropertyHeaders, partitionKey?.paths], + ); + let partitionKeyProperties = useMemo( + () => + partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) => + partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""), + ), + [partitionKeyPropertyHeaders], + ); + + // new DocumentId() requires a DocumentTab which we mock with only the required properties + const newDocumentId = useCallback( + (rawDocument: DataModels.DocumentId, partitionKeyProperties: string[], partitionKeyValue: string[]) => + new DocumentId( + { + partitionKey, + partitionKeyProperties, + // Fake unused mocks + isEditorDirty: () => false, + selectDocument: () => Promise.reject(), + }, + rawDocument, + partitionKeyValue, + ), + [partitionKey], + ); + + // const isPreferredApiMongoDB = useMemo( + // () => userContext.apiType === "Mongo" || isPreferredApiMongoDB, + // [isPreferredApiMongoDB], + // ); + + useEffect(() => { + setDocumentIds(_documentIds); + }, [_documentIds]); + + // TODO: this is executed in onActivate() in the original code. + useEffect(() => { + if (!documentsIterator) { + try { + refreshDocumentsGrid(); + // // Select first document and load content + // if (documentIds.length > 0) { + // documentIds[0].click(); + // } + } catch (error) { + if (onLoadStartKey !== null && onLoadStartKey !== undefined) { + TelemetryProcessor.traceFailure( + Action.Tab, + { + databaseName: _collection.databaseId, + collectionName: _collection.id(), + + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + onLoadStartKey, + ); + setOnLoadStartKey(undefined); + } + } + } + + updateNavbarWithTabsButtons({ + _collection, + selectedRows, + editorState, + isPreferredApiMongoDB, + onNewDocumentClick, + onSaveNewDocumentClick, + onRevertNewDocumentClick, + onSaveExistingDocumentClick, + onRevertExistingDocumentClick, + onDeleteExistingDocumentsClick, + }); + }, []); + + const isEditorDirty = useCallback((): boolean => { + switch (editorState) { + case ViewModels.DocumentExplorerState.noDocumentSelected: + case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: + return false; + + case ViewModels.DocumentExplorerState.newDocumentValid: + case ViewModels.DocumentExplorerState.newDocumentInvalid: + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: + return true; + + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + return true; + // return ( + // this.selectedDocumentContent.getEditableOriginalValue() !== + // this.selectedDocumentContent.getEditableCurrentValue() + // ); + + 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 tab if isExecuting has changed + useEffect(() => { + onIsExecutingChange(isExecuting); + }, [onIsExecutingChange, isExecuting]); + + const onNewDocumentClick = useCallback( + (): void => confirmDiscardingChange(() => initializeNewDocument()), + [confirmDiscardingChange], + ); + + const initializeNewDocument = (): void => { + // this.selectedDocumentId(null); + const defaultDocument: string = renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4); + setInitialDocumentContent(defaultDocument); + setSelectedDocumentContent(defaultDocument); + setSelectedDocumentContentBaseline(defaultDocument); + setSelectedRows(new Set()); + setClickedRow(undefined); + setEditorState(ViewModels.DocumentExplorerState.newDocumentValid); + }; + + let onSaveNewDocumentClick = useCallback((): Promise => { + 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) => { + const value: string = renderObjectForEditor(savedDocument || {}, null, 4); + setSelectedDocumentContentBaseline(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); + + // this.selectedDocumentId(id); + setDocumentIds(ids); + setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + 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]))) + .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 => { + // const selectedDocumentId = this.selectedDocumentId(); + + const documentContent = JSON.parse(selectedDocumentContent); + + const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues( + documentContent, + partitionKey as PartitionKeyDefinition, ); - const isQueryCopilotSampleContainer = - _collection?.isSampleCollection && - _collection?.databaseId === QueryCopilotSampleDatabaseId && - _collection?.id() === QueryCopilotSampleContainerId; + const selectedDocumentId = documentIds[clickedRow as number]; + selectedDocumentId.partitionKeyValue = partitionKeyValueArray; - // For Mongo only - const [continuationToken, setContinuationToken] = useState(undefined); + 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.exisitingDocumentNoEdits); + TelemetryProcessor.traceSuccess( + Action.UpdateDocument, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + }, + startKey, + ); + }, + (error) => { + 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, clickedRow]); - let lastFilterContents = ['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC']; + const onRevertExistingDocumentClick = useCallback((): void => { + setSelectedDocumentContentBaseline(initialDocumentContent); + // this.initialDocumentContent.valueHasMutated(); + setSelectedDocumentContent(selectedDocumentContentBaseline); + // setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + }, [ + initialDocumentContent, + selectedDocumentContentBaseline, + setSelectedDocumentContent, + // setEditorState, + ]); - const applyFilterButton = { - enabled: true, - visible: true, - }; + let __deleteDocument = useCallback( + (documentId: DocumentId): Promise => deleteDocument(_collection, documentId), + [_collection], + ); - const partitionKey: DataModels.PartitionKey = useMemo( - () => _partitionKey || (_collection && _collection.partitionKey), - [_collection, _partitionKey], - ); - const partitionKeyPropertyHeaders: string[] = useMemo( - () => _collection?.partitionKeyPropertyHeaders || partitionKey?.paths, - [_collection?.partitionKeyPropertyHeaders, partitionKey?.paths], - ); - let partitionKeyProperties = useMemo( - () => - partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) => - partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""), - ), - [partitionKeyPropertyHeaders], - ); - - // new DocumentId() requires a DocumentTab which we mock with only the required properties - const newDocumentId = useCallback( - (rawDocument: DataModels.DocumentId, partitionKeyProperties: string[], partitionKeyValue: string[]) => - new DocumentId( - { - partitionKey, - partitionKeyProperties, - // Fake unused mocks - isEditorDirty: () => false, - selectDocument: () => Promise.reject(), - }, - rawDocument, - partitionKeyValue, - ), - [partitionKey], - ); - - // const isPreferredApiMongoDB = useMemo( - // () => userContext.apiType === "Mongo" || isPreferredApiMongoDB, - // [isPreferredApiMongoDB], - // ); - - useEffect(() => { - setDocumentIds(_documentIds); - }, [_documentIds]); - - // TODO: this is executed in onActivate() in the original code. - useEffect(() => { - if (!documentsIterator) { - try { - refreshDocumentsGrid(); - // // Select first document and load content - // if (documentIds.length > 0) { - // documentIds[0].click(); - // } - } catch (error) { - if (onLoadStartKey !== null && onLoadStartKey !== undefined) { - TelemetryProcessor.traceFailure( - Action.Tab, + const _deleteDocuments = useCallback( + (documentId: DocumentId): Promise => { + onExecutionErrorChange(false); + const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + }); + setIsExecuting(true); + return __deleteDocument(documentId) + .then( + () => { + TelemetryProcessor.traceSuccess( + Action.DeleteDocument, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + }, + startKey, + ); + return documentId; + }, + (error) => { + onExecutionErrorChange(true); + console.error(error); + TelemetryProcessor.traceFailure( + Action.DeleteDocument, { - databaseName: _collection.databaseId, - collectionName: _collection.id(), - dataExplorerArea: Constants.Areas.Tab, tabTitle, error: getErrorMessage(error), errorStack: getErrorStack(error), }, - onLoadStartKey, + startKey, ); - setOnLoadStartKey(undefined); - } - } - } + return undefined; + }, + ) + .finally(() => setIsExecuting(false)); + }, + [__deleteDocument, onExecutionErrorChange, tabTitle], + ); + const deleteDocuments = useCallback( + (toDeleteDocumentIds: DocumentId[]): void => { + onExecutionErrorChange(false); + setIsExecuting(true); + const promises = toDeleteDocumentIds.map((documentId) => _deleteDocuments(documentId)); + Promise.all(promises) + .then((deletedDocumentIds: DocumentId[]) => { + const newDocumentIds = [...documentIds]; + deletedDocumentIds.forEach((deletedDocumentId) => { + if (deletedDocumentId !== undefined) { + // documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid); + const index = toDeleteDocumentIds.findIndex((documentId) => documentId.rid === deletedDocumentId.rid); + if (index !== -1) { + newDocumentIds.splice(index, 1); + } + } + }); + setDocumentIds(newDocumentIds); + + setSelectedDocumentContent(undefined); + setClickedRow(undefined); + setSelectedRows(new Set()); + setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected); + }) + .finally(() => setIsExecuting(false)); + }, + [onExecutionErrorChange, _deleteDocuments, documentIds], + ); + + const onDeleteExistingDocumentsClick = useCallback(async (): Promise => { + // const selectedDocumentId = this.selectedDocumentId(); + + // 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", + // async () => await _deleteDocuments(selectedDocumentId), + () => deleteDocuments(Array.from(selectedRows).map((index) => documentIds[index as number])), + "Cancel", + undefined, + ); + }, [deleteDocuments, documentIds, isPreferredApiMongoDB, selectedRows]); + + // If editor state changes, update the nav + // TODO Put whatever the buttons callback use in the dependency array: find a better way to maintain + useEffect( + () => updateNavbarWithTabsButtons({ _collection, selectedRows, @@ -523,97 +848,550 @@ const DocumentsTabComponent: React.FunctionComponent<{ onSaveNewDocumentClick, onRevertNewDocumentClick, onSaveExistingDocumentClick, - onRevertExisitingDocumentClick, - onDeleteExisitingDocumentsClick, - }); - }, []); + onRevertExistingDocumentClick: onRevertExistingDocumentClick, + onDeleteExistingDocumentsClick: onDeleteExistingDocumentsClick, + }), + [ + _collection, + selectedRows, + editorState, + isPreferredApiMongoDB, + onNewDocumentClick, + onSaveNewDocumentClick, + onRevertNewDocumentClick, + onSaveExistingDocumentClick, + onRevertExistingDocumentClick, + onDeleteExistingDocumentsClick, + ], + ); - const isEditorDirty = useCallback((): boolean => { - switch (editorState) { - case ViewModels.DocumentExplorerState.noDocumentSelected: - case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: - return false; + const onShowFilterClick = () => { + setIsFilterCreated(true); + setIsFilterExpanded(true); - case ViewModels.DocumentExplorerState.newDocumentValid: - case ViewModels.DocumentExplorerState.newDocumentInvalid: - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: - return true; + // TODO convert this + $(".filterDocExpanded").addClass("active"); + $("#content").addClass("active"); + $(".querydropdown").focus(); + }; - case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: - return true; - // return ( - // this.selectedDocumentContent.getEditableOriginalValue() !== - // this.selectedDocumentContent.getEditableCurrentValue() - // ); + const queryTimeoutEnabled = useCallback( + (): boolean => !isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled), + [isPreferredApiMongoDB], + ); - default: - return false; + let buildQuery = useCallback( + (filter: string): string => { + return QueryUtils.buildDocumentsQuery(filter, partitionKeyProperties, partitionKey); + }, + [partitionKeyProperties, partitionKey], + ); + + const createIterator = useCallback((): QueryIterator => { + const _queryAbortController = new AbortController(); + setQueryAbortController(_queryAbortController); + const filter: string = filterContent.trim(); + const query: string = buildQuery(filter); + // 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, buildQuery, resourceTokenPartitionKey, isQueryCopilotSampleContainer, _collection]); + + /** + * Query first page of documents + * Select and query first document and display content + */ + // const autoPopulateContent = async (applyFilterButtonPressed?: boolean) => { + // // reset iterator + // setDocumentsIterator({ + // iterator: createIterator(), + // applyFilterButtonPressed, + // }); + // // load documents + // await loadNextPage(applyFilterButtonPressed); + + // // // Select first document and load content + // // if (documentIds.length > 0) { + // // documentIds[0].click(); + // // } + // }; + + const onHideFilterClick = (): void => { + setIsFilterExpanded(false); + + // this.isFilterExpanded(false); + + // $(".filterDocExpanded").removeClass("active"); + // $("#content").removeClass("active"); + // $(".queryButton").focus(); + }; + + const onCloseButtonKeyDown: KeyboardEventHandler = (event) => { + if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { + onHideFilterClick(); + event.stopPropagation(); + return false; + } + return true; + }; + + // const accessibleDocumentList = new AccessibleVerticalList(documentIds); + // accessibleDocumentList.setOnSelect( + // (selectedDocument: DocumentId) => selectedDocument && selectedDocument.click(), + // ); + // this.selectedDocumentId.subscribe((newSelectedDocumentId: DocumentId) => + // accessibleDocumentList.updateCurrentItem(newSelectedDocumentId), + // ); + // this.documentIds.subscribe((newDocuments: DocumentId[]) => { + // accessibleDocumentList.updateItemList(newDocuments); + // if (newDocuments.length > 0) { + // this.dataContentsGridScrollHeight( + // newDocuments.length * DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px", + // ); + // } else { + // this.dataContentsGridScrollHeight( + // DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px", + // ); + // } + // }); + + let loadNextPage = useCallback( + (iterator: QueryIterator, applyFilterButtonClicked?: boolean): Promise => { + 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) { + queryAbortController.abort(); + } else { + useDialog + .getState() + .showOkCancelModalDialog( + QueryConstants.CancelQueryTitle, + format(QueryConstants.CancelQuerySubTextTemplate, QueryConstants.CancelQueryTimeoutThresholdReached), + "Yes", + () => queryAbortController.abort(), + "No", + undefined, + ); + } + } + }, queryTimeout); + setCancelQueryTimeoutID(cancelQueryTimeoutID); } - }, [editorState]); + return _loadNextPageInternal(iterator) + .then( + (documentsIdsResponse = []) => { + const currentDocuments = documentIds; + const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid); + const nextDocumentIds = documentsIdsResponse + // filter documents already loaded in observable + .filter((d: DataModels.DocumentId) => { + return currentDocumentsRids.indexOf(d._rid) < 0; + }) + // map raw response to view model + .map((rawDocument: DataModels.DocumentId & { _partitionKeyValue: string[] }) => { + const partitionKeyValue = rawDocument._partitionKeyValue; - 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], - ); + // TODO: Mock documentsTab. Fix this + const partitionKey = _partitionKey || (_collection && _collection.partitionKey); + const partitionKeyPropertyHeaders = _collection?.partitionKeyPropertyHeaders || partitionKey?.paths; + const partitionKeyProperties = partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) => + partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""), + ); - // Update tab if isExecuting has changed - useEffect(() => { - onIsExecutingChange(isExecuting); - }, [onIsExecutingChange, isExecuting]); + return newDocumentId(rawDocument, partitionKeyProperties, partitionKeyValue); + }); - const onNewDocumentClick = useCallback( - (): void => confirmDiscardingChange(() => initializeNewDocument()), - [confirmDiscardingChange], - ); + const merged = currentDocuments.concat(nextDocumentIds); + setDocumentIds(merged); + if (onLoadStartKey !== null && onLoadStartKey !== undefined) { + TelemetryProcessor.traceSuccess( + Action.Tab, + { + databaseName: _collection.databaseId, + collectionName: _collection.id(), - const initializeNewDocument = (): void => { - // this.selectedDocumentId(null); - const defaultDocument: string = renderObjectForEditor({ id: "replace_with_new_document_id" }, null, 4); - setInitialDocumentContent(defaultDocument); - setSelectedDocumentContent(defaultDocument); - setSelectedDocumentContentBaseline(defaultDocument); - setSelectedRows(new Set()); - setClickedRow(undefined); - setEditorState(ViewModels.DocumentExplorerState.newDocumentValid); + dataExplorerArea: Constants.Areas.Tab, + tabTitle, //tabTitle(), + }, + onLoadStartKey, + ); + setOnLoadStartKey(undefined); + } + }, + (error) => { + onExecutionErrorChange(true); + const errorMessage = getErrorMessage(error); + logConsoleError(errorMessage); + if (onLoadStartKey !== null && onLoadStartKey !== undefined) { + TelemetryProcessor.traceFailure( + Action.Tab, + { + databaseName: _collection.databaseId, + collectionName: _collection.id(), + + dataExplorerArea: Constants.Areas.Tab, + tabTitle, // tabTitle(), + error: errorMessage, + errorStack: getErrorStack(error), + }, + onLoadStartKey, + ); + setOnLoadStartKey(undefined); + } + }, + ) + .finally(() => { + setIsExecuting(false); + if (applyFilterButtonClicked && queryTimeoutEnabled()) { + clearTimeout(cancelQueryTimeoutID); + if (!automaticallyCancelQueryAfterTimeout) { + useDialog.getState().closeDialog(); + } + } + }); + }, + [ + onExecutionErrorChange, + queryTimeoutEnabled, + isExecuting, + queryAbortController, + documentIds, + onLoadStartKey, + _partitionKey, + _collection, + newDocumentId, + tabTitle, + cancelQueryTimeoutID, + ], + ); + + useEffect(() => { + if (documentsIterator) { + loadNextPage(documentsIterator.iterator, documentsIterator.applyFilterButtonPressed); + } + }, [ + documentsIterator, + // loadNextPage + ]); + + const onRefreshKeyInput: KeyboardEventHandler = (event) => { + if (event.key === " " || event.key === "Enter") { + const focusElement = event.target as HTMLElement; + refreshDocumentsGrid(false); + focusElement && focusElement.focus(); + event.stopPropagation(); + event.preventDefault(); + } + }; + + const onLoadMoreKeyInput: KeyboardEventHandler = (event) => { + if (event.key === " " || event.key === "Enter") { + const focusElement = event.target as HTMLElement; + loadNextPage(documentsIterator.iterator); + focusElement && focusElement.focus(); + event.stopPropagation(); + event.preventDefault(); + } + }; + + // TODO: use this when generating column headers + const showPartitionKey = (() => { + if (!_collection) { + return false; + } + + if (!_collection.partitionKey) { + return false; + } + + if (_collection.partitionKey.systemKey && isPreferredApiMongoDB) { + return false; + } + + return true; + })(); + + const _isQueryCopilotSampleContainer = + _collection?.isSampleCollection && + _collection?.databaseId === QueryCopilotSampleDatabaseId && + _collection?.id() === QueryCopilotSampleContainerId; + + // Table config here + const tableItems: DocumentsTableComponentItem[] = documentIds.map((documentId) => { + const item: Record & { id: string } = { + id: documentId.id(), }; - let onSaveNewDocumentClick = useCallback((): Promise => { - onExecutionErrorChange(false); + if (partitionKeyPropertyHeaders && documentId.stringPartitionKeyValues) { + for (let i = 0; i < partitionKeyPropertyHeaders.length; i++) { + item[partitionKeyPropertyHeaders[i]] = documentId.stringPartitionKeyValues[i]; + } + } + + return item; + }); + + /** + * replicate logic of selectedDocument.click(); + * Document has been clicked on in table + * @param tabRowId + */ + const onDocumentClicked = (tabRowId: TableRowId) => { + const index = tabRowId as number; + setClickedRow(index); + loadDocument(documentIds[index]); + }; + + let loadDocument = (documentId: DocumentId) => + (_isQueryCopilotSampleContainer ? readSampleDocument(documentId) : readDocument(_collection, documentId)).then( + (content) => { + initDocumentEditor(documentId, content); + }, + ); + + const initDocumentEditor = (documentId: DocumentId, documentContent: unknown): void => { + if (documentId) { + const content: string = renderObjectForEditor(documentContent, null, 4); + setSelectedDocumentContentBaseline(content); + setSelectedDocumentContent(content); + setInitialDocumentContent(content); + + const newState = documentId + ? ViewModels.DocumentExplorerState.exisitingDocumentNoEdits + : ViewModels.DocumentExplorerState.newDocumentValid; + setEditorState(newState); + } + }; + + const _onEditorContentChange = (newContent: string): void => { + setSelectedDocumentContent(newContent); + + if ( + selectedDocumentContentBaseline === initialDocumentContent && + newContent === initialDocumentContent && + editorState !== ViewModels.DocumentExplorerState.newDocumentValid + ) { + setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + return; + } + + // Mongo uses BSON format for _id, trying to parse it as JSON blocks normal flow in an edit + // Bypass validation for mongo + if (isPreferredApiMongoDB) { + onValidDocumentEdit(); + return; + } + + try { + JSON.parse(newContent); + onValidDocumentEdit(); + } catch (e) { + onInvalidDocumentEdit(); + } + }; + + const onValidDocumentEdit = (): void => { + if ( + editorState === ViewModels.DocumentExplorerState.newDocumentInvalid || + editorState === ViewModels.DocumentExplorerState.newDocumentValid + ) { + setEditorState(ViewModels.DocumentExplorerState.newDocumentValid); + return; + } + + setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid); + }; + + const onInvalidDocumentEdit = (): void => { + if ( + editorState === ViewModels.DocumentExplorerState.newDocumentInvalid || + editorState === ViewModels.DocumentExplorerState.newDocumentValid + ) { + setEditorState(ViewModels.DocumentExplorerState.newDocumentInvalid); + return; + } + + if ( + editorState === ViewModels.DocumentExplorerState.exisitingDocumentNoEdits || + editorState === ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid + ) { + setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid); + return; + } + }; + + const tableContainerRef = useRef(null); + const [tableContainerSizePx, setTableContainerSizePx] = useState<{ height: number; width: number }>(undefined); + useEffect(() => { + if (!tableContainerRef.current) { + return undefined; + } + const resizeObserver = new ResizeObserver(() => + setTableContainerSizePx({ + height: tableContainerRef.current.offsetHeight, + width: tableContainerRef.current.offsetWidth, + }), + ); + resizeObserver.observe(tableContainerRef.current); + return () => resizeObserver.disconnect(); // clean up + }, []); + + const columnHeaders = { + idHeader: isPreferredApiMongoDB ? "_id" : "id", + partitionKeyHeaders: (showPartitionKey && partitionKeyPropertyHeaders) || [], + }; + + const onSelectedRowsChange = (selectedRows: Set) => { + confirmDiscardingChange(() => { + if (selectedRows.size === 0) { + setSelectedDocumentContent(undefined); + setClickedRow(undefined); + setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected); + } + + // Find if clickedRow is in selectedRows.If not, clear clickedRow and content + if (clickedRow !== undefined && !selectedRows.has(clickedRow)) { + setClickedRow(undefined); + setSelectedDocumentContent(undefined); + setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected); + } + + // If only one selection, we consider as a click + if (selectedRows.size === 1) { + setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + } + + setSelectedRows(selectedRows); + }); + }; + + // ********* Override here for mongo (from MongoDocumentsTab) ********** + if (isPreferredApiMongoDB) { + loadDocument = (documentId: DocumentId) => + MongoProxyClient.readDocument(_collection.databaseId, _collection as ViewModels.Collection, documentId).then( + (content) => { + initDocumentEditor(documentId, content); + }, + ); + + renderObjectForEditor = (value: unknown): string => MongoUtility.tojson(value, null, false); + + const _hasShardKeySpecified = (document: unknown): boolean => { + return Boolean(extractPartitionKeyValues(document, _getPartitionKeyDefinition() as PartitionKeyDefinition)); + }; + + const _getPartitionKeyDefinition = (): DataModels.PartitionKey => { + let partitionKey: DataModels.PartitionKey = _partitionKey; + + if ( + _partitionKey && + _partitionKey.paths && + _partitionKey.paths.length && + _partitionKey.paths.length > 0 && + _partitionKey.paths[0].indexOf("$v") > -1 + ) { + // Convert BsonSchema2 to /path format + partitionKey = { + kind: partitionKey.kind, + paths: ["/" + partitionKeyProperties?.[0].replace(/\./g, "/")], + version: partitionKey.version, + }; + } + + return partitionKey; + }; + + lastFilterContents = ['{"id":"foo"}', "{ qty: { $gte: 20 } }"]; + partitionKeyProperties = partitionKeyProperties?.map((partitionKeyProperty, i) => { + if (partitionKeyProperty && ~partitionKeyProperty.indexOf(`"`)) { + partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, ""); + } + + if (partitionKeyProperty && partitionKeyProperty.indexOf("$v") > -1) { + // From $v.shard.$v.key.$v > shard.key + partitionKeyProperty = partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, ""); + partitionKeyPropertyHeaders[i] = "/" + partitionKeyProperty; + } + + return partitionKeyProperty; + }); + + __deleteDocument = (documentId: DocumentId): Promise => + MongoProxyClient.deleteDocument(_collection.databaseId, _collection as ViewModels.Collection, documentId); + + onSaveNewDocumentClick = useCallback((): Promise => { + const documentContent = JSON.parse(selectedDocumentContent); + // this.displayedError(""); const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, { dataExplorerArea: Constants.Areas.Tab, tabTitle, }); - const sanitizedContent = selectedDocumentContent.replace("\n", ""); - const document = JSON.parse(sanitizedContent); + + const partitionKeyProperty = partitionKeyProperties?.[0]; + if (partitionKeyProperty !== "_id" && !_hasShardKeySpecified(documentContent)) { + const message = `The document is lacking the shard property: ${partitionKeyProperty}`; + // TODO: Display error message here + + // this.displayedError(message); + // const that = this; + // setTimeout(() => { + // that.displayedError(""); + // }, Constants.ClientDefaults.errorNotificationTimeoutMs); + // this.isExecutionError(true); + TelemetryProcessor.traceFailure( + Action.CreateDocument, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + error: message, + }, + startKey, + ); + Logger.logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab"); + throw new Error("Document without shard key"); + } + + onExecutionErrorChange(false); setIsExecuting(true); - return createDocument(_collection, document) + return MongoProxyClient.createDocument( + _collection.databaseId, + _collection as ViewModels.Collection, + partitionKeyProperties?.[0], + documentContent, + ) .then( - (savedDocument: DataModels.DocumentId) => { - const value: string = renderObjectForEditor(savedDocument || {}, null, 4); - setSelectedDocumentContentBaseline(value); - setInitialDocumentContent(value); - const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues( + (savedDocument: { _self: unknown }) => { + const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues( savedDocument, - partitionKey as PartitionKeyDefinition, + _getPartitionKeyDefinition() as PartitionKeyDefinition, ); - const id = newDocumentId(savedDocument, partitionKeyProperties, partitionKeyValueArray as string[]); + + const id = new ObjectId(this, savedDocument, partitionKeyArray); const ids = documentIds; ids.push(id); + delete savedDocument._self; + + const value: string = renderObjectForEditor(savedDocument || {}, null, 4); + setSelectedDocumentContentBaseline(value); // this.selectedDocumentId(id); setDocumentIds(ids); @@ -646,51 +1424,47 @@ const DocumentsTabComponent: React.FunctionComponent<{ .then(() => setSelectedRows(new Set([documentIds.length - 1]))) .finally(() => setIsExecuting(false)); }, [ - onExecutionErrorChange, - tabTitle, selectedDocumentContent, - _collection, - partitionKey, - newDocumentId, + tabTitle, partitionKeyProperties, + _hasShardKeySpecified, + onExecutionErrorChange, + _collection, + _getPartitionKeyDefinition, documentIds, ]); - const onRevertNewDocumentClick = useCallback((): void => { - setInitialDocumentContent(""); - setSelectedDocumentContent(""); - setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected); - }, [setInitialDocumentContent, setSelectedDocumentContent, setEditorState]); - - let onSaveExistingDocumentClick = useCallback((): Promise => { + onSaveExistingDocumentClick = (): Promise => { // const selectedDocumentId = this.selectedDocumentId(); - - const documentContent = JSON.parse(selectedDocumentContent); - - const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues( - documentContent, - partitionKey as PartitionKeyDefinition, - ); - - const selectedDocumentId = documentIds[clickedRow as number]; - selectedDocumentId.partitionKeyValue = partitionKeyValueArray; - + const documentContent = selectedDocumentContent; onExecutionErrorChange(false); + setIsExecuting(true); const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, { dataExplorerArea: Constants.Areas.Tab, tabTitle, }); - setIsExecuting(true); - return updateDocument(_collection, selectedDocumentId, documentContent) + + const selectedDocumentId = documentIds[clickedRow as number]; + return MongoProxyClient.updateDocument( + _collection.databaseId, + _collection as ViewModels.Collection, + selectedDocumentId, + documentContent, + ) .then( - (updatedDocument: Item & { _rid: string }) => { + (updatedDocument: { _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); + const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues( + updatedDocument, + _getPartitionKeyDefinition() as PartitionKeyDefinition, + ); + + const id = new ObjectId(this, updatedDocument, partitionKeyArray); + documentId.id(id.id()); } }); setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); @@ -720,973 +1494,199 @@ const DocumentsTabComponent: React.FunctionComponent<{ }, ) .finally(() => setIsExecuting(false)); - }, [onExecutionErrorChange, tabTitle, selectedDocumentContent, _collection, partitionKey, documentIds, clickedRow]); + }; - const onRevertExisitingDocumentClick = useCallback((): void => { - setSelectedDocumentContentBaseline(initialDocumentContent); - // this.initialDocumentContent.valueHasMutated(); - setSelectedDocumentContent(selectedDocumentContentBaseline); - // setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - }, [ - initialDocumentContent, - selectedDocumentContentBaseline, - setSelectedDocumentContent, - // setEditorState, - ]); + buildQuery = (filter: string): string => { + return filter || "{}"; + }; - let __deleteDocument = useCallback( - (documentId: DocumentId): Promise => deleteDocument(_collection, documentId), - [_collection], - ); + loadNextPage = (): Promise => { + setIsExecuting(true); + onExecutionErrorChange(false); + const filter: string = filterContent.trim(); + const query: string = buildQuery(filter); - const _deleteDocuments = useCallback( - (documentId: DocumentId): Promise => { - onExecutionErrorChange(false); - const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle, - }); - setIsExecuting(true); - return __deleteDocument(documentId) - .then( - () => { + return MongoProxyClient.queryDocuments( + _collection.databaseId, + _collection as ViewModels.Collection, + true, + query, + continuationToken, + ) + .then( + ({ continuationToken: newContinuationToken, documents }) => { + setContinuationToken(newContinuationToken); + let currentDocuments = documentIds; + const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid); + const nextDocumentIds = documents + .filter((d: { _rid: string }) => { + return currentDocumentsRids.indexOf(d._rid) < 0; + }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .map((rawDocument: any) => { + const partitionKeyValue = rawDocument._partitionKeyValue; + return newDocumentId(rawDocument, partitionKeyProperties, [partitionKeyValue]); + // return new DocumentId(this, rawDocument, [partitionKeyValue]); + }); + + const merged = currentDocuments.concat(nextDocumentIds); + + setDocumentIds(merged); + // currentDocuments = this.documentIds();/ + currentDocuments = merged; + + if (filterContent.length > 0 && currentDocuments.length > 0) { + currentDocuments[0].click(); + } else { + setSelectedDocumentContent(""); + // this.selectedDocumentId(null); + setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected); + } + if (_onLoadStartKey !== null && _onLoadStartKey !== undefined) { TelemetryProcessor.traceSuccess( - Action.DeleteDocument, + Action.Tab, { + databaseName: _collection.databaseId, + collectionName: _collection.id(), + dataExplorerArea: Constants.Areas.Tab, tabTitle, }, - startKey, + _onLoadStartKey, ); - return documentId; - }, - (error) => { - onExecutionErrorChange(true); - console.error(error); + // TODO: Set on Load start key to null to stop telemetry traces + setOnLoadStartKey(null); + } + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (error: any) => { + if (onLoadStartKey !== null && onLoadStartKey !== undefined) { TelemetryProcessor.traceFailure( - Action.DeleteDocument, + Action.Tab, { + databaseName: _collection.databaseId, + collectionName: _collection.id(), + dataExplorerArea: Constants.Areas.Tab, tabTitle, error: getErrorMessage(error), errorStack: getErrorStack(error), }, - startKey, + _onLoadStartKey, ); - return undefined; - }, - ) - .finally(() => setIsExecuting(false)); - }, - [__deleteDocument, onExecutionErrorChange, tabTitle], - ); - - const deleteDocuments = useCallback( - (toDeleteDocumentIds: DocumentId[]): void => { - onExecutionErrorChange(false); - setIsExecuting(true); - const promises = toDeleteDocumentIds.map((documentId) => _deleteDocuments(documentId)); - Promise.all(promises) - .then((deletedDocumentIds: DocumentId[]) => { - const newDocumentIds = [...documentIds]; - deletedDocumentIds.forEach((deletedDocumentId) => { - if (deletedDocumentId !== undefined) { - // documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid); - const index = toDeleteDocumentIds.findIndex((documentId) => documentId.rid === deletedDocumentId.rid); - if (index !== -1) { - newDocumentIds.splice(index, 1); - } - } - }); - setDocumentIds(newDocumentIds); - - setSelectedDocumentContent(undefined); - setClickedRow(undefined); - setSelectedRows(new Set()); - setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected); - }) - .finally(() => setIsExecuting(false)); - }, - [onExecutionErrorChange, _deleteDocuments, documentIds], - ); - - const onDeleteExisitingDocumentsClick = useCallback(async (): Promise => { - // const selectedDocumentId = this.selectedDocumentId(); - - // 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", - // async () => await _deleteDocuments(selectedDocumentId), - () => deleteDocuments(Array.from(selectedRows).map((index) => documentIds[index as number])), - "Cancel", - undefined, - ); - }, [deleteDocuments, documentIds, isPreferredApiMongoDB, selectedRows]); - - // If editor state changes, update the nav - // TODO Put whatever the buttons callback use in the dependency array: find a better way to maintain - useEffect( - () => - updateNavbarWithTabsButtons({ - _collection, - selectedRows, - editorState, - isPreferredApiMongoDB, - onNewDocumentClick, - onSaveNewDocumentClick, - onRevertNewDocumentClick, - onSaveExistingDocumentClick, - onRevertExisitingDocumentClick, - onDeleteExisitingDocumentsClick, - }), - [ - _collection, - selectedRows, - editorState, - isPreferredApiMongoDB, - onNewDocumentClick, - onSaveNewDocumentClick, - onRevertNewDocumentClick, - onSaveExistingDocumentClick, - onRevertExisitingDocumentClick, - onDeleteExisitingDocumentsClick, - ], - ); - - const onShowFilterClick = () => { - setIsFilterCreated(true); - setIsFilterExpanded(true); - - // TODO convert this - $(".filterDocExpanded").addClass("active"); - $("#content").addClass("active"); - $(".querydropdown").focus(); - }; - - const queryTimeoutEnabled = useCallback( - (): boolean => !isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled), - [isPreferredApiMongoDB], - ); - - let buildQuery = useCallback( - (filter: string): string => { - return QueryUtils.buildDocumentsQuery(filter, partitionKeyProperties, partitionKey); - }, - [partitionKeyProperties, partitionKey], - ); - - const createIterator = useCallback((): QueryIterator => { - const _queryAbortController = new AbortController(); - setQueryAbortController(_queryAbortController); - const filter: string = filterContent.trim(); - const query: string = buildQuery(filter); - // 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, buildQuery, resourceTokenPartitionKey, isQueryCopilotSampleContainer, _collection]); - - /** - * Query first page of documents - * Select and query first document and display content - */ - // const autoPopulateContent = async (applyFilterButtonPressed?: boolean) => { - // // reset iterator - // setDocumentsIterator({ - // iterator: createIterator(), - // applyFilterButtonPressed, - // }); - // // load documents - // await loadNextPage(applyFilterButtonPressed); - - // // // Select first document and load content - // // if (documentIds.length > 0) { - // // documentIds[0].click(); - // // } - // }; - - const onHideFilterClick = (): void => { - setIsFilterExpanded(false); - - // this.isFilterExpanded(false); - - // $(".filterDocExpanded").removeClass("active"); - // $("#content").removeClass("active"); - // $(".queryButton").focus(); - }; - - const onCloseButtonKeyDown: KeyboardEventHandler = (event) => { - if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { - onHideFilterClick(); - event.stopPropagation(); - return false; - } - return true; - }; - - // const accessibleDocumentList = new AccessibleVerticalList(documentIds); - // accessibleDocumentList.setOnSelect( - // (selectedDocument: DocumentId) => selectedDocument && selectedDocument.click(), - // ); - // this.selectedDocumentId.subscribe((newSelectedDocumentId: DocumentId) => - // accessibleDocumentList.updateCurrentItem(newSelectedDocumentId), - // ); - // this.documentIds.subscribe((newDocuments: DocumentId[]) => { - // accessibleDocumentList.updateItemList(newDocuments); - // if (newDocuments.length > 0) { - // this.dataContentsGridScrollHeight( - // newDocuments.length * DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px", - // ); - // } else { - // this.dataContentsGridScrollHeight( - // DocumentsGridMetrics.IndividualRowHeight + DocumentsGridMetrics.BufferHeight + "px", - // ); - // } - // }); - - let loadNextPage = useCallback( - (iterator: QueryIterator, applyFilterButtonClicked?: boolean): Promise => { - 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) { - queryAbortController.abort(); - } else { - useDialog - .getState() - .showOkCancelModalDialog( - QueryConstants.CancelQueryTitle, - format(QueryConstants.CancelQuerySubTextTemplate, QueryConstants.CancelQueryTimeoutThresholdReached), - "Yes", - () => queryAbortController.abort(), - "No", - undefined, - ); - } + // TODO: Set on Load start key to null to stop telemetry traces + setOnLoadStartKey(undefined); } - }, queryTimeout); - setCancelQueryTimeoutID(cancelQueryTimeoutID); - } - return _loadNextPageInternal(iterator) - .then( - (documentsIdsResponse = []) => { - const currentDocuments = documentIds; - const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid); - const nextDocumentIds = documentsIdsResponse - // filter documents already loaded in observable - .filter((d: DataModels.DocumentId) => { - return currentDocumentsRids.indexOf(d._rid) < 0; - }) - // map raw response to view model - .map((rawDocument: DataModels.DocumentId & { _partitionKeyValue: string[] }) => { - const partitionKeyValue = rawDocument._partitionKeyValue; - - // TODO: Mock documentsTab. Fix this - const partitionKey = _partitionKey || (_collection && _collection.partitionKey); - const partitionKeyPropertyHeaders = _collection?.partitionKeyPropertyHeaders || partitionKey?.paths; - const partitionKeyProperties = partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) => - partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""), - ); - - return newDocumentId(rawDocument, partitionKeyProperties, partitionKeyValue); - }); - - const merged = currentDocuments.concat(nextDocumentIds); - setDocumentIds(merged); - if (onLoadStartKey !== null && onLoadStartKey !== undefined) { - TelemetryProcessor.traceSuccess( - Action.Tab, - { - databaseName: _collection.databaseId, - collectionName: _collection.id(), - - dataExplorerArea: Constants.Areas.Tab, - tabTitle, //tabTitle(), - }, - onLoadStartKey, - ); - setOnLoadStartKey(undefined); - } - }, - (error) => { - onExecutionErrorChange(true); - const errorMessage = getErrorMessage(error); - logConsoleError(errorMessage); - if (onLoadStartKey !== null && onLoadStartKey !== undefined) { - TelemetryProcessor.traceFailure( - Action.Tab, - { - databaseName: _collection.databaseId, - collectionName: _collection.id(), - - dataExplorerArea: Constants.Areas.Tab, - tabTitle, // tabTitle(), - error: errorMessage, - errorStack: getErrorStack(error), - }, - onLoadStartKey, - ); - setOnLoadStartKey(undefined); - } - }, - ) - .finally(() => { - setIsExecuting(false); - if (applyFilterButtonClicked && queryTimeoutEnabled()) { - clearTimeout(cancelQueryTimeoutID); - if (!automaticallyCancelQueryAfterTimeout) { - useDialog.getState().closeDialog(); - } - } - }); - }, - [ - onExecutionErrorChange, - queryTimeoutEnabled, - isExecuting, - queryAbortController, - documentIds, - onLoadStartKey, - _partitionKey, - _collection, - newDocumentId, - tabTitle, - cancelQueryTimeoutID, - ], - ); - - useEffect(() => { - if (documentsIterator) { - loadNextPage(documentsIterator.iterator, documentsIterator.applyFilterButtonPressed); - } - }, [ - documentsIterator, - // loadNextPage - ]); - - const onRefreshKeyInput: KeyboardEventHandler = (event) => { - if (event.key === " " || event.key === "Enter") { - const focusElement = event.target as HTMLElement; - refreshDocumentsGrid(false); - focusElement && focusElement.focus(); - event.stopPropagation(); - event.preventDefault(); - } - }; - - const onLoadMoreKeyInput: KeyboardEventHandler = (event) => { - if (event.key === " " || event.key === "Enter") { - const focusElement = event.target as HTMLElement; - loadNextPage(documentsIterator.iterator); - focusElement && focusElement.focus(); - event.stopPropagation(); - event.preventDefault(); - } - }; - - // TODO: use this when generating column headers - const showPartitionKey = (() => { - if (!_collection) { - return false; - } - - if (!_collection.partitionKey) { - return false; - } - - if (_collection.partitionKey.systemKey && isPreferredApiMongoDB) { - return false; - } - - return true; - })(); - - const _isQueryCopilotSampleContainer = - _collection?.isSampleCollection && - _collection?.databaseId === QueryCopilotSampleDatabaseId && - _collection?.id() === QueryCopilotSampleContainerId; - - // Table config here - const tableItems: DocumentsTableComponentItem[] = documentIds.map((documentId) => { - const item: Record & { id: string } = { - id: documentId.id(), - }; - - if (partitionKeyPropertyHeaders && documentId.stringPartitionKeyValues) { - for (let i = 0; i < partitionKeyPropertyHeaders.length; i++) { - item[partitionKeyPropertyHeaders[i]] = documentId.stringPartitionKeyValues[i]; - } - } - - return item; - }); - - /** - * replicate logic of selectedDocument.click(); - * Document has been clicked on in table - * @param tabRowId - */ - const onDocumentClicked = (tabRowId: TableRowId) => { - const index = tabRowId as number; - setClickedRow(index); - loadDocument(documentIds[index]); - }; - - let loadDocument = (documentId: DocumentId) => - (_isQueryCopilotSampleContainer ? readSampleDocument(documentId) : readDocument(_collection, documentId)).then( - (content) => { - initDocumentEditor(documentId, content); - }, - ); - - const initDocumentEditor = (documentId: DocumentId, documentContent: unknown): void => { - if (documentId) { - const content: string = renderObjectForEditor(documentContent, null, 4); - setSelectedDocumentContentBaseline(content); - setSelectedDocumentContent(content); - setInitialDocumentContent(content); - - const newState = documentId - ? ViewModels.DocumentExplorerState.exisitingDocumentNoEdits - : ViewModels.DocumentExplorerState.newDocumentValid; - setEditorState(newState); - } - }; - - const _onEditorContentChange = (newContent: string): void => { - setSelectedDocumentContent(newContent); - - if ( - selectedDocumentContentBaseline === initialDocumentContent && - newContent === initialDocumentContent && - editorState !== ViewModels.DocumentExplorerState.newDocumentValid - ) { - setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - return; - } - - // Mongo uses BSON format for _id, trying to parse it as JSON blocks normal flow in an edit - // Bypass validation for mongo - if (isPreferredApiMongoDB) { - onValidDocumentEdit(); - return; - } - - try { - JSON.parse(newContent); - onValidDocumentEdit(); - } catch (e) { - onInvalidDocumentEdit(); - } - }; - - const onValidDocumentEdit = (): void => { - if ( - editorState === ViewModels.DocumentExplorerState.newDocumentInvalid || - editorState === ViewModels.DocumentExplorerState.newDocumentValid - ) { - setEditorState(ViewModels.DocumentExplorerState.newDocumentValid); - return; - } - - setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid); - }; - - const onInvalidDocumentEdit = (): void => { - if ( - editorState === ViewModels.DocumentExplorerState.newDocumentInvalid || - editorState === ViewModels.DocumentExplorerState.newDocumentValid - ) { - setEditorState(ViewModels.DocumentExplorerState.newDocumentInvalid); - return; - } - - if ( - editorState === ViewModels.DocumentExplorerState.exisitingDocumentNoEdits || - editorState === ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid - ) { - setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid); - return; - } - }; - - const tableContainerRef = useRef(null); - const [tableContainerSizePx, setTableContainerSizePx] = useState<{ height: number; width: number }>(undefined); - useEffect(() => { - if (!tableContainerRef.current) { - return undefined; - } - const resizeObserver = new ResizeObserver(() => - setTableContainerSizePx({ - height: tableContainerRef.current.offsetHeight, - width: tableContainerRef.current.offsetWidth, - }), - ); - resizeObserver.observe(tableContainerRef.current); - return () => resizeObserver.disconnect(); // clean up - }, []); - - const columnHeaders = { - idHeader: isPreferredApiMongoDB ? "_id" : "id", - partitionKeyHeaders: (showPartitionKey && partitionKeyPropertyHeaders) || [], - }; - - const onSelectedRowsChange = (selectedRows: Set) => { - confirmDiscardingChange(() => { - if (selectedRows.size === 0) { - setSelectedDocumentContent(undefined); - setClickedRow(undefined); - setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected); - } - - // Find if clickedRow is in selectedRows.If not, clear clickedRow and content - if (clickedRow !== undefined && !selectedRows.has(clickedRow)) { - setClickedRow(undefined); - setSelectedDocumentContent(undefined); - setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected); - } - - // If only one selection, we consider as a click - if (selectedRows.size === 1) { - setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - } - - setSelectedRows(selectedRows); - }); - }; - - // ********* Override here for mongo (from MongoDocumentsTab) ********** - if (isPreferredApiMongoDB) { - loadDocument = (documentId: DocumentId) => - MongoProxyClient.readDocument(_collection.databaseId, _collection as ViewModels.Collection, documentId).then( - (content) => { - initDocumentEditor(documentId, content); }, - ); + ) + .finally(() => setIsExecuting(false)); + }; + } + // ***************** Mongo *************************** - renderObjectForEditor = (value: unknown): string => MongoUtility.tojson(value, null, false); - - const _hasShardKeySpecified = (document: unknown): boolean => { - return Boolean(extractPartitionKeyValues(document, _getPartitionKeyDefinition() as PartitionKeyDefinition)); - }; - - const _getPartitionKeyDefinition = (): DataModels.PartitionKey => { - let partitionKey: DataModels.PartitionKey = _partitionKey; - - if ( - _partitionKey && - _partitionKey.paths && - _partitionKey.paths.length && - _partitionKey.paths.length > 0 && - _partitionKey.paths[0].indexOf("$v") > -1 - ) { - // Convert BsonSchema2 to /path format - partitionKey = { - kind: partitionKey.kind, - paths: ["/" + partitionKeyProperties?.[0].replace(/\./g, "/")], - version: partitionKey.version, - }; - } - - return partitionKey; - }; - - lastFilterContents = ['{"id":"foo"}', "{ qty: { $gte: 20 } }"]; - partitionKeyProperties = partitionKeyProperties?.map((partitionKeyProperty, i) => { - if (partitionKeyProperty && ~partitionKeyProperty.indexOf(`"`)) { - partitionKeyProperty = partitionKeyProperty.replace(/["]+/g, ""); - } - - if (partitionKeyProperty && partitionKeyProperty.indexOf("$v") > -1) { - // From $v.shard.$v.key.$v > shard.key - partitionKeyProperty = partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, ""); - partitionKeyPropertyHeaders[i] = "/" + partitionKeyProperty; - } - - return partitionKeyProperty; - }); - - __deleteDocument = (documentId: DocumentId): Promise => - MongoProxyClient.deleteDocument(_collection.databaseId, _collection as ViewModels.Collection, documentId); - - onSaveNewDocumentClick = useCallback((): Promise => { - const documentContent = JSON.parse(selectedDocumentContent); - // this.displayedError(""); - const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle, + const refreshDocumentsGrid = useCallback( + async (applyFilterButtonPressed?: boolean): Promise => { + // clear documents grid + setDocumentIds([]); + try { + // reset iterator + // setDocumentsIterator(createIterator()); + // load documents + // await autoPopulateContent(applyFilterButtonPressed); + setDocumentsIterator({ + iterator: createIterator(), + applyFilterButtonPressed, }); - const partitionKeyProperty = partitionKeyProperties?.[0]; - if (partitionKeyProperty !== "_id" && !_hasShardKeySpecified(documentContent)) { - const message = `The document is lacking the shard property: ${partitionKeyProperty}`; - // TODO: Display error message here + // collapse filter + setAppliedFilter(filterContent); + setIsFilterExpanded(applyFilterButtonPressed); + document.getElementById("errorStatusIcon")?.focus(); + } catch (error) { + console.error(); + useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error)); + } + }, + [createIterator, filterContent], + ); - // this.displayedError(message); - // const that = this; - // setTimeout(() => { - // that.displayedError(""); - // }, Constants.ClientDefaults.errorNotificationTimeoutMs); - // this.isExecutionError(true); - TelemetryProcessor.traceFailure( - Action.CreateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle, - error: message, - }, - startKey, - ); - Logger.logError("Failed to save new document: Document shard key not defined", "MongoDocumentsTab"); - throw new Error("Document without shard key"); - } - - onExecutionErrorChange(false); - setIsExecuting(true); - return MongoProxyClient.createDocument( - _collection.databaseId, - _collection as ViewModels.Collection, - partitionKeyProperties?.[0], - documentContent, - ) - .then( - (savedDocument: { _self: unknown }) => { - const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues( - savedDocument, - _getPartitionKeyDefinition() as PartitionKeyDefinition, - ); - - const id = new ObjectId(this, savedDocument, partitionKeyArray); - const ids = documentIds; - ids.push(id); - delete savedDocument._self; - - const value: string = renderObjectForEditor(savedDocument || {}, null, 4); - setSelectedDocumentContentBaseline(value); - - // this.selectedDocumentId(id); - setDocumentIds(ids); - setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - 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]))) - .finally(() => setIsExecuting(false)); - }, [ - selectedDocumentContent, - tabTitle, - partitionKeyProperties, - _hasShardKeySpecified, - onExecutionErrorChange, - _collection, - _getPartitionKeyDefinition, - documentIds, - ]); - - onSaveExistingDocumentClick = (): Promise => { - // const selectedDocumentId = this.selectedDocumentId(); - const documentContent = selectedDocumentContent; - onExecutionErrorChange(false); - setIsExecuting(true); - const startKey: number = TelemetryProcessor.traceStart(Action.UpdateDocument, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle, - }); - - const selectedDocumentId = documentIds[clickedRow as number]; - return MongoProxyClient.updateDocument( - _collection.databaseId, - _collection as ViewModels.Collection, - selectedDocumentId, - documentContent, - ) - .then( - (updatedDocument: { _rid: string }) => { - const value: string = renderObjectForEditor(updatedDocument || {}, null, 4); - setSelectedDocumentContentBaseline(value); - - documentIds.forEach((documentId: DocumentId) => { - if (documentId.rid === updatedDocument._rid) { - const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues( - updatedDocument, - _getPartitionKeyDefinition() as PartitionKeyDefinition, - ); - - const id = new ObjectId(this, updatedDocument, partitionKeyArray); - documentId.id(id.id()); - } - }); - setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - TelemetryProcessor.traceSuccess( - Action.UpdateDocument, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle, - }, - startKey, - ); - }, - (error) => { - 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)); - }; - - buildQuery = (filter: string): string => { - return filter || "{}"; - }; - - loadNextPage = (): Promise => { - setIsExecuting(true); - onExecutionErrorChange(false); - const filter: string = filterContent.trim(); - const query: string = buildQuery(filter); - - return MongoProxyClient.queryDocuments( - _collection.databaseId, - _collection as ViewModels.Collection, - true, - query, - continuationToken, - ) - .then( - ({ continuationToken: newContinuationToken, documents }) => { - setContinuationToken(newContinuationToken); - let currentDocuments = documentIds; - const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid); - const nextDocumentIds = documents - .filter((d: { _rid: string }) => { - return currentDocumentsRids.indexOf(d._rid) < 0; - }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .map((rawDocument: any) => { - const partitionKeyValue = rawDocument._partitionKeyValue; - return newDocumentId(rawDocument, partitionKeyProperties, [partitionKeyValue]); - // return new DocumentId(this, rawDocument, [partitionKeyValue]); - }); - - const merged = currentDocuments.concat(nextDocumentIds); - - setDocumentIds(merged); - // currentDocuments = this.documentIds();/ - currentDocuments = merged; - - if (filterContent.length > 0 && currentDocuments.length > 0) { - currentDocuments[0].click(); - } else { - setSelectedDocumentContent(""); - // this.selectedDocumentId(null); - setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected); - } - if (_onLoadStartKey !== null && _onLoadStartKey !== undefined) { - TelemetryProcessor.traceSuccess( - Action.Tab, - { - databaseName: _collection.databaseId, - collectionName: _collection.id(), - - dataExplorerArea: Constants.Areas.Tab, - tabTitle, - }, - _onLoadStartKey, - ); - // TODO: Set on Load start key to null to stop telemetry traces - setOnLoadStartKey(null); - } - }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (error: any) => { - if (onLoadStartKey !== null && onLoadStartKey !== undefined) { - TelemetryProcessor.traceFailure( - Action.Tab, - { - databaseName: _collection.databaseId, - collectionName: _collection.id(), - - dataExplorerArea: Constants.Areas.Tab, - tabTitle, - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }, - _onLoadStartKey, - ); - // TODO: Set on Load start key to null to stop telemetry traces - setOnLoadStartKey(undefined); - } - }, - ) - .finally(() => setIsExecuting(false)); - }; - } - // ***************** Mongo *************************** - - const refreshDocumentsGrid = useCallback( - async (applyFilterButtonPressed?: boolean): Promise => { - // clear documents grid - setDocumentIds([]); - try { - // reset iterator - // setDocumentsIterator(createIterator()); - // load documents - // await autoPopulateContent(applyFilterButtonPressed); - setDocumentsIterator({ - iterator: createIterator(), - applyFilterButtonPressed, - }); - - // collapse filter - setAppliedFilter(filterContent); - setIsFilterExpanded(applyFilterButtonPressed); - document.getElementById("errorStatusIcon")?.focus(); - } catch (error) { - console.error(); - useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error)); - } - }, - [createIterator, filterContent], - ); - - return ( - -
+
- {/* */} - {isFilterCreated && ( -
- {/* */} - {!isFilterExpanded && !isPreferredApiMongoDB && ( -
- SELECT * FROM c - {appliedFilter} - -
- )} - {!isFilterExpanded && isPreferredApiMongoDB && ( -
- {appliedFilter.length > 0 && ( - Filter : - )} - {!(appliedFilter.length > 0) && ( - - No filter applied - - )} - {appliedFilter} - -
- )} - {/* */} + > + Edit Filter + +
+ )} + {!isFilterExpanded && isPreferredApiMongoDB && ( +
+ {appliedFilter.length > 0 && ( + Filter : + )} + {!(appliedFilter.length > 0) && ( + + No filter applied + + )} + {appliedFilter} + +
+ )} + {/* */} - {/* */} - {isFilterExpanded && ( -
-
-
- {!isPreferredApiMongoDB && ( - - {" "} - SELECT * FROM c{" "} - - )} - setFilterContent(e.target.value)} + {/* */} + {isFilterExpanded && ( +
+
+
+ {!isPreferredApiMongoDB && ( + + {" "} + SELECT * FROM c{" "} + + )} + setFilterContent(e.target.value)} /* data-bind=" W attr:{ @@ -1695,134 +1695,134 @@ const DocumentsTabComponent: React.FunctionComponent<{ css: { placeholderVisible: filterContent().length === 0 }, textInput: filterContent" */ - /> + /> - - {lastFilterContents.map((filter) => ( - + + {lastFilterContents.map((filter) => ( + - - - - - {!isPreferredApiMongoDB && isExecuting && ( - + + + {!isPreferredApiMongoDB && isExecuting && ( + - )} - - queryAbortController.abort()} + tabIndex={0} + > + Cancel Query + + )} + + - Hide filter - -
+ > + Hide filter +
- )} - {/* */} -
- )} - {/* */} - - {/* doesn't like to be a flex child */} -
- -
-
-
- {selectedDocumentContent && selectedRows.size <= 1 && ( - - )} - {selectedRows.size > 1 && ( - Number of selected documents: {selectedRows.size} - )} -
-
+ )} + {/* */}
+ )} + {/* */} + + {/* doesn't like to be a flex child */} +
+ +
+
+
+ {selectedDocumentContent && selectedRows.size <= 1 && ( + + )} + {selectedRows.size > 1 && ( + Number of selected documents: {selectedRows.size} + )} +
+
- - ); - }; +
+ + ); +}; diff --git a/src/Explorer/Theme/ThemeUtil.ts b/src/Explorer/Theme/ThemeUtil.ts index b0d3d763b..fe4075c69 100644 --- a/src/Explorer/Theme/ThemeUtil.ts +++ b/src/Explorer/Theme/ThemeUtil.ts @@ -22,7 +22,7 @@ const appThemePortalBrandRamp: BrandVariants = { 160: "#CDD8EF", }; -export function getPlatformTheme (platform: Platform) : Theme { +export function getPlatformTheme(platform: Platform): Theme { if (platform === Platform.Fabric) { return createLightTheme(appThemeFabricTealBrandRamp); } else { diff --git a/src/Platform/Fabric/FabricTheme.tsx b/src/Platform/Fabric/FabricTheme.tsx index 18d84a127..446f1ad93 100644 --- a/src/Platform/Fabric/FabricTheme.tsx +++ b/src/Platform/Fabric/FabricTheme.tsx @@ -209,22 +209,22 @@ export const appThemeFabric: Theme = createTheme({ }); export const appThemeFabricTealBrandRamp: BrandVariants = { - 10: '#001919', - 20: '#012826', - 30: '#01322E', - 40: '#033f38', - 50: '#054d43', - 60: '#0a5c50', - 70: '#0c695a', - 80: '#117865', - 90: '#1f937e', - 100: '#2aaC94', - 110: '#3abb9f', - 120: '#52c7aa', - 130: '#78d3b9', - 140: '#9ee0cb', - 150: '#c0ecdd', - 160: '#e3f7ef', + 10: "#001919", + 20: "#012826", + 30: "#01322E", + 40: "#033f38", + 50: "#054d43", + 60: "#0a5c50", + 70: "#0c695a", + 80: "#117865", + 90: "#1f937e", + 100: "#2aaC94", + 110: "#3abb9f", + 120: "#52c7aa", + 130: "#78d3b9", + 140: "#9ee0cb", + 150: "#c0ecdd", + 160: "#e3f7ef", }; -export const appThemeFabricV9 = createLightTheme(appThemeFabricTealBrandRamp); \ No newline at end of file +export const appThemeFabricV9 = createLightTheme(appThemeFabricTealBrandRamp);