diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index 65775552d..483116328 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -18,7 +18,7 @@ import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter"; import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities"; import DocumentsTab from "Explorer/Tabs/DocumentsTab"; -import { dataExplorerLightTheme } from "Explorer/Theme/ThemeUtil"; +import { getPlatformTheme } from "Explorer/Theme/ThemeUtil"; import { useSelectedNode } from "Explorer/useSelectedNode"; import { KeyboardAction } from "KeyboardShortcuts"; import { QueryConstants } from "Shared/Constants"; @@ -392,453 +392,128 @@ 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, - ); - - 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, - onRevertExisitingDocumentClick, - onDeleteExisitingDocumentsClick, - }); - }, []); - - 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, + // Command buttons + const [editorState, setEditorState] = useState( + ViewModels.DocumentExplorerState.noDocumentSelected, ); - const selectedDocumentId = documentIds[clickedRow as number]; - selectedDocumentId.partitionKeyValue = partitionKeyValueArray; + const isQueryCopilotSampleContainer = + _collection?.isSampleCollection && + _collection?.databaseId === QueryCopilotSampleDatabaseId && + _collection?.id() === QueryCopilotSampleContainerId; - 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]); + // For Mongo only + const [continuationToken, setContinuationToken] = useState(undefined); - const onRevertExisitingDocumentClick = useCallback((): void => { - setSelectedDocumentContentBaseline(initialDocumentContent); - // this.initialDocumentContent.valueHasMutated(); - setSelectedDocumentContent(selectedDocumentContentBaseline); - // setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); - }, [ - initialDocumentContent, - selectedDocumentContentBaseline, - setSelectedDocumentContent, - // setEditorState, - ]); + let lastFilterContents = ['WHERE c.id = "foo"', "ORDER BY c._ts DESC", 'WHERE c.id = "foo" ORDER BY c._ts DESC']; - let __deleteDocument = useCallback( - (documentId: DocumentId): Promise => deleteDocument(_collection, documentId), - [_collection], - ); + const applyFilterButton = { + enabled: true, + visible: true, + }; - 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; + 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(), }, - (error) => { - onExecutionErrorChange(true); - console.error(error); + 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.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], - ); + setOnLoadStartKey(undefined); + } + } + } - 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, @@ -850,548 +525,95 @@ const DocumentsTabComponent: React.FunctionComponent<{ onSaveExistingDocumentClick, onRevertExisitingDocumentClick, onDeleteExisitingDocumentsClick, - }), - [ - _collection, - selectedRows, - editorState, - isPreferredApiMongoDB, - onNewDocumentClick, - onSaveNewDocumentClick, - onRevertNewDocumentClick, - onSaveExistingDocumentClick, - onRevertExisitingDocumentClick, - onDeleteExisitingDocumentsClick, - ], - ); + }); + }, []); - const onShowFilterClick = () => { - setIsFilterCreated(true); - setIsFilterExpanded(true); + const isEditorDirty = useCallback((): boolean => { + switch (editorState) { + case ViewModels.DocumentExplorerState.noDocumentSelected: + case ViewModels.DocumentExplorerState.exisitingDocumentNoEdits: + return false; - // TODO convert this - $(".filterDocExpanded").addClass("active"); - $("#content").addClass("active"); - $(".querydropdown").focus(); - }; + case ViewModels.DocumentExplorerState.newDocumentValid: + case ViewModels.DocumentExplorerState.newDocumentInvalid: + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyInvalid: + return true; - const queryTimeoutEnabled = useCallback( - (): boolean => !isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled), - [isPreferredApiMongoDB], - ); + case ViewModels.DocumentExplorerState.exisitingDocumentDirtyValid: + return true; + // return ( + // this.selectedDocumentContent.getEditableOriginalValue() !== + // this.selectedDocumentContent.getEditableCurrentValue() + // ); - 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); + default: + return false; } - 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; + }, [editorState]); - // 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 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], ); - const initDocumentEditor = (documentId: DocumentId, documentContent: unknown): void => { - if (documentId) { - const content: string = renderObjectForEditor(documentContent, null, 4); - setSelectedDocumentContentBaseline(content); - setSelectedDocumentContent(content); - setInitialDocumentContent(content); + // Update tab if isExecuting has changed + useEffect(() => { + onIsExecutingChange(isExecuting); + }, [onIsExecutingChange, isExecuting]); - const newState = documentId - ? ViewModels.DocumentExplorerState.exisitingDocumentNoEdits - : ViewModels.DocumentExplorerState.newDocumentValid; - setEditorState(newState); - } - }; + const onNewDocumentClick = useCallback( + (): void => confirmDiscardingChange(() => initializeNewDocument()), + [confirmDiscardingChange], + ); - 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 - ) { + 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); - 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(""); + let onSaveNewDocumentClick = useCallback((): Promise => { + onExecutionErrorChange(false); const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, { dataExplorerArea: Constants.Areas.Tab, tabTitle, }); - - 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); + const sanitizedContent = selectedDocumentContent.replace("\n", ""); + const document = JSON.parse(sanitizedContent); setIsExecuting(true); - return MongoProxyClient.createDocument( - _collection.databaseId, - _collection as ViewModels.Collection, - partitionKeyProperties?.[0], - documentContent, - ) + return createDocument(_collection, document) .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; - + (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); @@ -1424,47 +646,51 @@ const DocumentsTabComponent: React.FunctionComponent<{ .then(() => setSelectedRows(new Set([documentIds.length - 1]))) .finally(() => setIsExecuting(false)); }, [ - selectedDocumentContent, - tabTitle, - partitionKeyProperties, - _hasShardKeySpecified, onExecutionErrorChange, + tabTitle, + selectedDocumentContent, _collection, - _getPartitionKeyDefinition, + partitionKey, + newDocumentId, + partitionKeyProperties, documentIds, ]); - onSaveExistingDocumentClick = (): Promise => { + const onRevertNewDocumentClick = useCallback((): void => { + setInitialDocumentContent(""); + setSelectedDocumentContent(""); + setEditorState(ViewModels.DocumentExplorerState.noDocumentSelected); + }, [setInitialDocumentContent, setSelectedDocumentContent, setEditorState]); + + let onSaveExistingDocumentClick = useCallback((): Promise => { // const selectedDocumentId = this.selectedDocumentId(); - const documentContent = selectedDocumentContent; + + const documentContent = JSON.parse(selectedDocumentContent); + + const partitionKeyValueArray: PartitionKey[] = extractPartitionKeyValues( + documentContent, + partitionKey as PartitionKeyDefinition, + ); + + const selectedDocumentId = documentIds[clickedRow as number]; + selectedDocumentId.partitionKeyValue = partitionKeyValueArray; + 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, - ) + setIsExecuting(true); + return updateDocument(_collection, selectedDocumentId, documentContent) .then( - (updatedDocument: { _rid: string }) => { + (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) { - const partitionKeyArray: PartitionKey[] = extractPartitionKeyValues( - updatedDocument, - _getPartitionKeyDefinition() as PartitionKeyDefinition, - ); - - const id = new ObjectId(this, updatedDocument, partitionKeyArray); - documentId.id(id.id()); + documentId.id(updatedDocument.id); } }); setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); @@ -1494,199 +720,973 @@ const DocumentsTabComponent: React.FunctionComponent<{ }, ) .finally(() => setIsExecuting(false)); - }; + }, [onExecutionErrorChange, tabTitle, selectedDocumentContent, _collection, partitionKey, documentIds, clickedRow]); - buildQuery = (filter: string): string => { - return filter || "{}"; - }; + const onRevertExisitingDocumentClick = useCallback((): void => { + setSelectedDocumentContentBaseline(initialDocumentContent); + // this.initialDocumentContent.valueHasMutated(); + setSelectedDocumentContent(selectedDocumentContentBaseline); + // setEditorState(ViewModels.DocumentExplorerState.exisitingDocumentNoEdits); + }, [ + initialDocumentContent, + selectedDocumentContentBaseline, + setSelectedDocumentContent, + // setEditorState, + ]); - loadNextPage = (): Promise => { - setIsExecuting(true); - onExecutionErrorChange(false); - const filter: string = filterContent.trim(); - const query: string = buildQuery(filter); + let __deleteDocument = useCallback( + (documentId: DocumentId): Promise => deleteDocument(_collection, documentId), + [_collection], + ); - 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) { + 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.Tab, + Action.DeleteDocument, { - databaseName: _collection.databaseId, - collectionName: _collection.id(), - dataExplorerArea: Constants.Areas.Tab, tabTitle, }, - _onLoadStartKey, + startKey, ); - // 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) { + return documentId; + }, + (error) => { + onExecutionErrorChange(true); + console.error(error); TelemetryProcessor.traceFailure( - Action.Tab, + Action.DeleteDocument, { - databaseName: _collection.databaseId, - collectionName: _collection.id(), - dataExplorerArea: Constants.Areas.Tab, tabTitle, error: getErrorMessage(error), errorStack: getErrorStack(error), }, - _onLoadStartKey, + startKey, ); - // TODO: Set on Load start key to null to stop telemetry traces - setOnLoadStartKey(undefined); - } - }, - ) - .finally(() => setIsExecuting(false)); - }; - } - // ***************** Mongo *************************** + 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, + ); + } + } + }, 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; + } - 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, + 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, }); - // collapse filter - setAppliedFilter(filterContent); - setIsFilterExpanded(false); - document.getElementById("errorStatusIcon")?.focus(); - } catch (error) { - console.error(); - useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error)); - } - }, - [createIterator, filterContent], - ); + const partitionKeyProperty = partitionKeyProperties?.[0]; + if (partitionKeyProperty !== "_id" && !_hasShardKeySpecified(documentContent)) { + const message = `The document is lacking the shard property: ${partitionKeyProperty}`; + // TODO: Display error message here - 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 && ( + + )} + + - 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} )}
-
-
- {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 32b11a0f2..b0d3d763b 100644 --- a/src/Explorer/Theme/ThemeUtil.ts +++ b/src/Explorer/Theme/ThemeUtil.ts @@ -1,7 +1,9 @@ import { BrandVariants, Theme, createLightTheme } from "@fluentui/react-components"; +import { Platform } from "ConfigContext"; +import { appThemeFabricTealBrandRamp } from "../../Platform/Fabric/FabricTheme"; // These are the theme colors for Fluent UI 9 React components -const cosmosdb: BrandVariants = { +const appThemePortalBrandRamp: BrandVariants = { 10: "#020305", 20: "#111723", 30: "#16263D", @@ -20,6 +22,10 @@ const cosmosdb: BrandVariants = { 160: "#CDD8EF", }; -export const dataExplorerLightTheme: Theme = { - ...createLightTheme(cosmosdb), -}; +export function getPlatformTheme (platform: Platform) : Theme { + if (platform === Platform.Fabric) { + return createLightTheme(appThemeFabricTealBrandRamp); + } else { + return createLightTheme(appThemePortalBrandRamp); + } +} diff --git a/src/Explorer/Tree2/ResourceTree.tsx b/src/Explorer/Tree2/ResourceTree.tsx index 71b7973d8..45e3ba5d4 100644 --- a/src/Explorer/Tree2/ResourceTree.tsx +++ b/src/Explorer/Tree2/ResourceTree.tsx @@ -5,8 +5,9 @@ import { TreeOpenChangeData, TreeOpenChangeEvent, } from "@fluentui/react-components"; +import { configContext } from "ConfigContext"; import { TreeNode2, TreeNode2Component } from "Explorer/Controls/TreeComponent2/TreeNode2Component"; -import { dataExplorerLightTheme } from "Explorer/Theme/ThemeUtil"; +import { getPlatformTheme } from "Explorer/Theme/ThemeUtil"; import { useDatabaseTreeNodes } from "Explorer/Tree2/useDatabaseTreeNodes"; import * as React from "react"; import shallow from "zustand/shallow"; @@ -88,7 +89,7 @@ export const ResourceTree2: React.FC = ({ container }: Resour return ( <> - +