import { ItemDefinition, QueryIterator, Resource } from '@azure/cosmos'; import { FluentProvider } from '@fluentui/react-components'; import Split from '@uiw/react-split'; import { KeyCodes, QueryCopilotSampleContainerId, QueryCopilotSampleDatabaseId } from "Common/Constants"; import { getErrorMessage, getErrorStack } from 'Common/ErrorHandlingUtils'; import { queryDocuments } from 'Common/dataAccess/queryDocuments'; import { readDocument } from 'Common/dataAccess/readDocument'; import { useDialog } from 'Explorer/Controls/Dialog'; import { querySampleDocuments, readSampleDocument } from 'Explorer/QueryCopilot/QueryCopilotUtilities'; import DocumentsTab from 'Explorer/Tabs/DocumentsTab'; import { dataExplorerLightTheme } from 'Explorer/Theme/ThemeUtil'; import { QueryConstants } from 'Shared/Constants'; import { LocalStorageUtility, StorageKey } from 'Shared/StorageUtility'; import { Action } from 'Shared/Telemetry/TelemetryConstants'; import { userContext } from "UserContext"; import { logConsoleError } from 'Utils/NotificationConsoleUtils'; import React, { KeyboardEventHandler, useEffect, useMemo, useState } from "react"; import { format } from "react-string-format"; import CloseIcon from "../../../../images/close-black.svg"; import * as Constants from "../../../Common/Constants"; import * as HeadersUtility from "../../../Common/HeadersUtility"; import * as DataModels from "../../../Contracts/DataModels"; import * as ViewModels from "../../../Contracts/ViewModels"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as QueryUtils from "../../../Utils/QueryUtils"; import DocumentId from "../../Tree/DocumentId"; import TabsBase from "../TabsBase"; import { DocumentsTableComponent, DocumentsTableComponentItem } from "./DocumentsTableComponent"; export class DocumentsTabV2 extends TabsBase { public partitionKey: DataModels.PartitionKey; private documentIds: DocumentId[]; private title: string; constructor(options: ViewModels.DocumentsTabOptions) { super(options); this.documentIds = options.documentIds(); this.title = options.title; } public render(): JSX.Element { return ; } public onActivate(): void { super.onActivate(); this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents); } } const DocumentsTabComponent: React.FunctionComponent<{ isPreferredApiMongoDB: boolean; documentIds: DocumentId[]; // TODO: this contains ko observables. We need to convert them to React state. tabId: string; collection: ViewModels.CollectionBase; partitionKey: DataModels.PartitionKey; onLoadStartKey: number; tabTitle: string; }> = (props) => { 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 [dataContentsGridScrollHeight, setDataContentsGridScrollHeight] = useState(undefined); const [shouldShowEditor, setShouldShowEditor] = useState(false); // Query const [documentsIterator, setDocumentsIterator] = useState<{ iterator: QueryIterator, applyFilterButtonPressed: boolean }>(undefined); const [queryAbortController, setQueryAbortController] = useState(undefined); const [resourceTokenPartitionKey, setResourceTokenPartitionKey] = useState(undefined); const [isQueryCopilotSampleContainer, setIsQueryCopilotSampleContainer] = useState(false); const [cancelQueryTimeoutID, setCancelQueryTimeoutID] = useState(undefined); const [isExecutionError, setIsExecutionError] = useState(false); const [onLoadStartKey, setOnLoadStartKey] = useState(props.onLoadStartKey); const [currentDocument, setCurrentDocument] = useState(undefined); // TODO remove this? const applyFilterButton = { enabled: true, }; const documentContentsContainerId = `documentContentsContainer${props.tabId}`; const documentContentsGridId = `documentContentsGrid${props.tabId}`; const partitionKey: DataModels.PartitionKey = props.partitionKey || (props.collection && props.collection.partitionKey); const partitionKeyPropertyHeaders: string[] = props.collection?.partitionKeyPropertyHeaders || partitionKey?.paths; const partitionKeyProperties = partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) => partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""), ); const isPreferredApiMongoDB = useMemo(() => userContext.apiType === "Mongo" || props.isPreferredApiMongoDB, [props.isPreferredApiMongoDB]); useEffect(() => { setDocumentIds(props.documentIds); }, [props.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: props.collection.databaseId, collectionName: props.collection.id(), dataExplorerArea: Constants.Areas.Tab, tabTitle: props.tabTitle, error: getErrorMessage(error), errorStack: getErrorStack(error), }, onLoadStartKey, ); setOnLoadStartKey(null); } } } }, []); useEffect(() => { if (documentsIterator) { loadNextPage(documentsIterator.applyFilterButtonPressed); } }, [documentsIterator]); const onShowFilterClick = () => { setIsFilterCreated(true); setIsFilterExpanded(true); // TODO convert this $(".filterDocExpanded").addClass("active"); $("#content").addClass("active"); $(".querydropdown").focus(); }; const queryTimeoutEnabled = (): boolean => !isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled); const buildQuery = (filter: string): string => { return QueryUtils.buildDocumentsQuery(filter, partitionKeyProperties, partitionKey); }; const createIterator = (): QueryIterator => { const _queryAbortController = new AbortController(); setQueryAbortController(_queryAbortController); const filter: string = filterContent.trim(); const query: string = buildQuery(filter); const options: any = {}; options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); if (resourceTokenPartitionKey) { options.partitionKey = resourceTokenPartitionKey; } options.abortSignal = _queryAbortController.signal; return isQueryCopilotSampleContainer ? querySampleDocuments(query, options) : queryDocuments(props.collection.databaseId, props.collection.id(), query, options); } /** * 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 refreshDocumentsGrid = 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(false); document.getElementById("errorStatusIcon")?.focus(); } catch (error) { useDialog.getState().showOkModalDialog("Refresh documents grid failed", getErrorMessage(error)); } }; const onHideFilterClick = (): Q.Promise => { // this.isFilterExpanded(false); $(".filterDocExpanded").removeClass("active"); $("#content").removeClass("active"); $(".queryButton").focus(); return Q(); }; 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", // ); // } // }); const onRefreshButtonKeyDown: KeyboardEventHandler = (event) => { if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { refreshDocumentsGrid(); event.stopPropagation(); return false; } return true; }; const loadNextPage = (applyFilterButtonClicked?: boolean): Promise => { setIsExecuting(true); setIsExecutionError(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() .then( (documentsIdsResponse = []) => { const currentDocuments = documentIds; const currentDocumentsRids = currentDocuments.map((currentDocument) => currentDocument.rid); const nextDocumentIds = documentsIdsResponse // filter documents already loaded in observable .filter((d: any) => { return currentDocumentsRids.indexOf(d._rid) < 0; }) // map raw response to view model .map((rawDocument: any) => { const partitionKeyValue = rawDocument._partitionKeyValue; // TODO: Mock documentsTab. Fix this const partitionKey = props.partitionKey || (props.collection && props.collection.partitionKey); const partitionKeyPropertyHeaders = props.collection?.partitionKeyPropertyHeaders || partitionKey?.paths; const partitionKeyProperties = partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) => partitionKeyPropertyHeader.replace(/[/]+/g, ".").substring(1).replace(/[']+/g, ""), ); return new DocumentId({ partitionKey, partitionKeyPropertyHeaders, partitionKeyProperties } as DocumentsTab, rawDocument, partitionKeyValue); }); const merged = currentDocuments.concat(nextDocumentIds); setDocumentIds(merged); if (onLoadStartKey !== null && onLoadStartKey !== undefined) { TelemetryProcessor.traceSuccess( Action.Tab, { databaseName: props.collection.databaseId, collectionName: props.collection.id(), dataExplorerArea: Constants.Areas.Tab, tabTitle: props.tabTitle, //tabTitle(), }, onLoadStartKey, ); setOnLoadStartKey(null); } }, (error) => { setIsExecutionError(true); const errorMessage = getErrorMessage(error); logConsoleError(errorMessage); if (onLoadStartKey !== null && onLoadStartKey !== undefined) { TelemetryProcessor.traceFailure( Action.Tab, { databaseName: props.collection.databaseId, collectionName: props.collection.id(), dataExplorerArea: Constants.Areas.Tab, tabTitle: props.tabTitle, // tabTitle(), error: errorMessage, errorStack: getErrorStack(error), }, onLoadStartKey, ); setOnLoadStartKey(null); } }, ) .finally(() => { setIsExecuting(false); if (applyFilterButtonClicked && queryTimeoutEnabled()) { clearTimeout(cancelQueryTimeoutID); if (!automaticallyCancelQueryAfterTimeout) { useDialog.getState().closeDialog(); } } }); }; const onLoadMoreKeyInput: KeyboardEventHandler = (event): void => { if (event.key === " " || event.key === "Enter") { const focusElement = document.getElementById(this.documentContentsGridId); this.loadNextPage(); focusElement && focusElement.focus(); event.stopPropagation(); event.preventDefault(); } }; const _loadNextPageInternal = (): Promise => { return documentsIterator.iterator.fetchNext().then((response) => response.resources); }; const showPartitionKey = (() => { if (!props.collection) { return false; } if (!props.collection.partitionKey) { return false; } if (props.collection.partitionKey.systemKey && props.isPreferredApiMongoDB) { return false; } return true; })(); const _isQueryCopilotSampleContainer = props.collection?.isSampleCollection && props.collection?.databaseId === QueryCopilotSampleDatabaseId && props.collection?.id() === QueryCopilotSampleContainerId; // Table config here const tableItems: DocumentsTableComponentItem[] = documentIds.map((documentId) => ({ id: documentId.id(), // TODO: for now, merge all the pk values into a single string/column type: documentId.partitionKeyProperties ? documentId.stringPartitionKeyValues.join(",") : undefined, })); const onSelectedDocument = (index: number) => readSingleDocument(documentIds[index]); // TODO: replicate logic of selectedDocument.click(); // TODO: Check if editor is dirty const readSingleDocument = (documentId: DocumentId) => (_isQueryCopilotSampleContainer ? readSampleDocument(documentId) : readDocument(props.collection, documentId)).then((content) => { // this.initDocumentEditor(documentId, content); setCurrentDocument(content); }); return
{/* */} {isFilterCreated &&
{/* */} {!isFilterExpanded && !isPreferredApiMongoDB &&
SELECT * FROM c {appliedFilter}
} {!isFilterExpanded && isPreferredApiMongoDB &&
{appliedFilter.length > 0 && Filter : } {!(appliedFilter.length > 0) && No filter applied } {appliedFilter}
} {/* */} {/* */} {isFilterExpanded &&
{!isPreferredApiMongoDB && SELECT * FROM c } setFilterContent(e.target.value)} /* data-bind=" W attr:{ placeholder:isPreferredApiMongoDB?'Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents.':'Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents.' }, css: { placeholderVisible: filterContent().length === 0 }, textInput: filterContent" */ /> {lastFilterContents.map((filter) => {!isPreferredApiMongoDB && isExecuting && } onHideFilterClick()} onKeyDown={onCloseButtonKeyDown} /*data-bind="click: onHideFilterClick, event: { keydown: onCloseButtonKeyDown }"*/> Hide filter
} {/* */}
} {/* */}
{JSON.stringify(currentDocument, undefined, " ")}
; }