import { FeedOptions } from "@azure/cosmos"; import { useDialog } from "Explorer/Controls/Dialog"; import { OnExecuteQueryClick } from "Explorer/QueryCopilot/Shared/QueryCopilotClient"; import { QueryCopilotResults } from "Explorer/QueryCopilot/Shared/QueryCopilotResults"; import { QueryCopilotSidebar } from "Explorer/QueryCopilot/V2/Sidebar/QueryCopilotSidebar"; import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection"; import { QueryConstants } from "Shared/Constants"; import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility"; import { QueryCopilotState, useQueryCopilot } from "hooks/useQueryCopilot"; import React, { Fragment } from "react"; import SplitterLayout from "react-splitter-layout"; import "react-splitter-layout/lib/index.css"; import { format } from "react-string-format"; import LaunchCopilot from "../../../../images/CopilotTabIcon.svg"; import CancelQueryIcon from "../../../../images/Entity_cancel.svg"; import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; import SaveQueryIcon from "../../../../images/save-cosmos.svg"; import { NormalizedEventKey, QueryCopilotSampleDatabaseId } from "../../../Common/Constants"; import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; import * as HeadersUtility from "../../../Common/HeadersUtility"; import { MinimalQueryIterator } from "../../../Common/IteratorUtilities"; import { queryIterator } from "../../../Common/MongoProxyClient"; import { queryDocuments } from "../../../Common/dataAccess/queryDocuments"; import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage"; import * as DataModels from "../../../Contracts/DataModels"; import * as ViewModels from "../../../Contracts/ViewModels"; import { userContext } from "../../../UserContext"; import * as QueryUtils from "../../../Utils/QueryUtils"; import { useSidePanel } from "../../../hooks/useSidePanel"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { EditorReact } from "../../Controls/Editor/EditorReact"; import Explorer from "../../Explorer"; import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter"; import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane"; import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane"; import TabsBase from "../TabsBase"; import "./QueryTabComponent.less"; enum ToggleState { Result, QueryMetrics, } export interface IDocument { metric: string; value: string; toolTip: string; } export interface ITabAccessor { onTabClickEvent: () => void; onSaveClickEvent: () => string; onCloseClickEvent: (isClicked: boolean) => void; } export interface Button { visible: boolean; enabled: boolean; isSelected?: boolean; } export interface IQueryTabComponentProps { collection: ViewModels.CollectionBase; isExecutionError: boolean; tabId: string; tabsBaseInstance: TabsBase; queryText: string; partitionKey: DataModels.PartitionKey; container: Explorer; activeTab?: TabsBase; onTabAccessor: (instance: ITabAccessor) => void; isPreferredApiMongoDB?: boolean; monacoEditorSetting?: string; viewModelcollection?: ViewModels.Collection; } interface IQueryTabStates { toggleState: ToggleState; sqlQueryEditorContent: string; selectedContent: string; queryResults: ViewModels.QueryResults; error: string; isExecutionError: boolean; isExecuting: boolean; showCopilotSidebar: boolean; queryCopilotGeneratedQuery: string; cancelQueryTimeoutID: NodeJS.Timeout; } export default class QueryTabComponent extends React.Component { public queryEditorId: string; public executeQueryButton: Button; public saveQueryButton: Button; public launchCopilotButton: Button; public splitterId: string; public isPreferredApiMongoDB: boolean; public isCloseClicked: boolean; public isCopilotTabActive: boolean; private _iterator: MinimalQueryIterator; private queryAbortController: AbortController; constructor(props: IQueryTabComponentProps) { super(props); this.state = { toggleState: ToggleState.Result, sqlQueryEditorContent: props.queryText || "SELECT * FROM c", selectedContent: "", queryResults: undefined, error: "", isExecutionError: this.props.isExecutionError, isExecuting: false, showCopilotSidebar: useQueryCopilot.getState().showCopilotSidebar, queryCopilotGeneratedQuery: useQueryCopilot.getState().query, cancelQueryTimeoutID: undefined, }; this.isCloseClicked = false; this.splitterId = this.props.tabId + "_splitter"; this.queryEditorId = `queryeditor${this.props.tabId}`; this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB; this.isCopilotTabActive = QueryCopilotSampleDatabaseId === this.props.collection.databaseId; this.executeQueryButton = { enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0, visible: true, }; const isSaveQueryBtnEnabled = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; this.saveQueryButton = { enabled: isSaveQueryBtnEnabled, visible: isSaveQueryBtnEnabled, }; this.launchCopilotButton = { enabled: userContext.apiType === "SQL" && true, visible: userContext.apiType === "SQL" && true, }; this.props.tabsBaseInstance.updateNavbarWithTabsButtons(); props.onTabAccessor({ onTabClickEvent: this.onTabClick.bind(this), onSaveClickEvent: this.getCurrentEditorQuery.bind(this), onCloseClickEvent: this.onCloseClick.bind(this), }); } public onCloseClick(isClicked: boolean): void { this.isCloseClicked = isClicked; if (useQueryCopilot.getState().wasCopilotUsed && this.isCopilotTabActive) { useQueryCopilot.getState().resetQueryCopilotStates(); } } public getCurrentEditorQuery(): string { return this.state.sqlQueryEditorContent; } public onTabClick(): void { if (!this.isCloseClicked) { useCommandBar.getState().setContextButtons(this.getTabsButtons()); } else { this.isCloseClicked = false; } } public onExecuteQueryClick = async (): Promise => { this._iterator = undefined; setTimeout(async () => { await this._executeQueryDocumentsPage(0); }, 100); }; public onSaveQueryClick = (): void => { useSidePanel.getState().openSidePanel("Save Query", ); }; public launchQueryCopilotChat = (): void => { useQueryCopilot.getState().setShowCopilotSidebar(!useQueryCopilot.getState().showCopilotSidebar); }; public onSavedQueriesClick = (): void => { useSidePanel .getState() .openSidePanel("Open Saved Queries", ); }; public toggleResult(): void { this.setState({ toggleState: ToggleState.Result, }); } public toggleMetrics(): void { this.setState({ toggleState: ToggleState.QueryMetrics, }); } public handleCopilotKeyDown = (event: KeyboardEvent): void => { if (this.isCopilotTabActive && event.altKey && event.key === "c") { this.launchQueryCopilotChat(); } }; public onToggleKeyDown = (event: React.KeyboardEvent): boolean => { if (event.key === NormalizedEventKey.LeftArrow) { this.toggleResult(); event.stopPropagation(); return false; } else if (event.key === NormalizedEventKey.RightArrow) { this.toggleMetrics(); event.stopPropagation(); return false; } return true; }; public togglesOnFocus(): void { const focusElement = document.getElementById("execute-query-toggles"); focusElement && focusElement.focus(); } private async _executeQueryDocumentsPage(firstItemIndex: number): Promise { this.queryAbortController = new AbortController(); if (this._iterator === undefined) { this._iterator = this.props.isPreferredApiMongoDB ? queryIterator( this.props.collection.databaseId, this.props.viewModelcollection, this.state.selectedContent || this.state.sqlQueryEditorContent, ) : queryDocuments( this.props.collection.databaseId, this.props.collection.id(), this.state.selectedContent || this.state.sqlQueryEditorContent, { enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey(), abortSignal: this.queryAbortController.signal, } as FeedOptions, ); } await this._queryDocumentsPage(firstItemIndex); } private async _queryDocumentsPage(firstItemIndex: number): Promise { this.props.tabsBaseInstance.isExecutionError(false); this.setState({ isExecutionError: false, }); const queryDocuments = async (firstItemIndex: number) => await queryDocumentsPage(this.props.collection && this.props.collection.id(), this._iterator, firstItemIndex); this.props.tabsBaseInstance.isExecuting(true); this.setState({ isExecuting: true, }); let automaticallyCancelQueryAfterTimeout: boolean; if (this.queryTimeoutEnabled()) { const queryTimeout: number = LocalStorageUtility.getEntryNumber(StorageKey.QueryTimeout); automaticallyCancelQueryAfterTimeout = LocalStorageUtility.getEntryBoolean( StorageKey.AutomaticallyCancelQueryAfterTimeout, ); const cancelQueryTimeoutID: NodeJS.Timeout = setTimeout(() => { if (this.state.isExecuting) { if (automaticallyCancelQueryAfterTimeout) { this.queryAbortController.abort(); } else { useDialog .getState() .showOkCancelModalDialog( QueryConstants.CancelQueryTitle, format(QueryConstants.CancelQuerySubTextTemplate, QueryConstants.CancelQueryTimeoutThresholdReached), "Yes", () => this.queryAbortController.abort(), "No", undefined, ); } } }, queryTimeout); this.setState({ cancelQueryTimeoutID, }); } useCommandBar.getState().setContextButtons(this.getTabsButtons()); try { const queryResults: ViewModels.QueryResults = await QueryUtils.queryPagesUntilContentPresent( firstItemIndex, queryDocuments, ); this.setState({ queryResults, error: "" }); } catch (error) { this.props.tabsBaseInstance.isExecutionError(true); this.setState({ isExecutionError: true, }); const errorMessage = getErrorMessage(error); this.setState({ error: errorMessage, }); document.getElementById("error-display").focus(); } finally { this.props.tabsBaseInstance.isExecuting(false); this.setState({ isExecuting: false, cancelQueryTimeoutID: undefined, }); if (this.queryTimeoutEnabled()) { clearTimeout(this.state.cancelQueryTimeoutID); if (!automaticallyCancelQueryAfterTimeout) { useDialog.getState().closeDialog(); } } this.togglesOnFocus(); useCommandBar.getState().setContextButtons(this.getTabsButtons()); } } protected getTabsButtons(): CommandButtonComponentProps[] { const buttons: CommandButtonComponentProps[] = []; if (this.executeQueryButton.visible) { const label = this.state.selectedContent?.length > 0 ? "Execute Selection" : "Execute Query"; buttons.push({ iconSrc: ExecuteQueryIcon, iconAlt: label, onCommandClick: this.isCopilotTabActive ? () => OnExecuteQueryClick() : this.onExecuteQueryClick, commandButtonLabel: label, ariaLabel: label, hasPopup: false, disabled: !this.executeQueryButton.enabled, }); } if (this.saveQueryButton.visible) { const label = "Save Query"; buttons.push({ iconSrc: SaveQueryIcon, iconAlt: label, onCommandClick: this.onSaveQueryClick, commandButtonLabel: label, ariaLabel: label, hasPopup: false, disabled: !this.saveQueryButton.enabled, }); } if (this.launchCopilotButton.visible && this.isCopilotTabActive) { const mainButtonLabel = "Launch Copilot"; const chatPaneLabel = "Open Copilot in chat pane (ALT+C)"; const copilotSettingLabel = "Copilot settings"; const openCopilotChatButton: CommandButtonComponentProps = { iconAlt: chatPaneLabel, onCommandClick: this.launchQueryCopilotChat, commandButtonLabel: chatPaneLabel, ariaLabel: chatPaneLabel, hasPopup: false, }; const copilotSettingsButton: CommandButtonComponentProps = { iconAlt: copilotSettingLabel, onCommandClick: () => undefined, commandButtonLabel: copilotSettingLabel, ariaLabel: copilotSettingLabel, hasPopup: false, }; const launchCopilotButton = { iconSrc: LaunchCopilot, iconAlt: mainButtonLabel, onCommandClick: this.launchQueryCopilotChat, commandButtonLabel: mainButtonLabel, ariaLabel: mainButtonLabel, hasPopup: false, children: [openCopilotChatButton, copilotSettingsButton], }; buttons.push(launchCopilotButton); } if (!this.props.isPreferredApiMongoDB && this.state.isExecuting) { const label = "Cancel query"; buttons.push({ iconSrc: CancelQueryIcon, iconAlt: label, onCommandClick: () => this.queryAbortController.abort(), commandButtonLabel: label, ariaLabel: label, hasPopup: false, }); } return buttons; } public onChangeContent(newContent: string): void { this.setState({ sqlQueryEditorContent: newContent, queryCopilotGeneratedQuery: "", }); if (this.isPreferredApiMongoDB) { if (newContent.length > 0) { this.executeQueryButton = { enabled: true, visible: true, }; } else { this.executeQueryButton = { enabled: false, visible: true, }; } } useCommandBar.getState().setContextButtons(this.getTabsButtons()); } public onSelectedContent(selectedContent: string): void { if (selectedContent.trim().length > 0) { this.setState({ selectedContent, }); } else { this.setState({ selectedContent: "", }); } if (this.isCopilotTabActive) { selectedContent.trim().length > 0 ? useQueryCopilot.getState().setSelectedQuery(selectedContent) : useQueryCopilot.getState().setSelectedQuery(""); } useCommandBar.getState().setContextButtons(this.getTabsButtons()); } public setEditorContent(): string { if (this.isCopilotTabActive && this.state.queryCopilotGeneratedQuery) { return this.state.queryCopilotGeneratedQuery; } return this.state.sqlQueryEditorContent; } private queryTimeoutEnabled(): boolean { return !this.isPreferredApiMongoDB && LocalStorageUtility.getEntryBoolean(StorageKey.QueryTimeoutEnabled); } private unsubscribeCopilotSidebar: () => void; componentDidMount(): void { this.unsubscribeCopilotSidebar = useQueryCopilot.subscribe((state: QueryCopilotState) => { if (this.state.showCopilotSidebar !== state.showCopilotSidebar) { this.setState({ showCopilotSidebar: state.showCopilotSidebar }); } if (this.state.queryCopilotGeneratedQuery !== state.query) { this.setState({ queryCopilotGeneratedQuery: state.query }); } }); useCommandBar.getState().setContextButtons(this.getTabsButtons()); document.addEventListener("keydown", this.handleCopilotKeyDown); } componentWillUnmount(): void { this.unsubscribeCopilotSidebar(); document.removeEventListener("keydown", this.handleCopilotKeyDown); } private getEditorAndQueryResult(): JSX.Element { return (
this.onChangeContent(newContent)} onContentSelected={(selectedContent: string) => this.onSelectedContent(selectedContent)} />
{this.isCopilotTabActive ? ( ) : ( this._executeQueryDocumentsPage(firstItemIndex) } /> )}
); } render(): JSX.Element { const shouldScaleElements = this.state.showCopilotSidebar && this.isCopilotTabActive; return (
{this.getEditorAndQueryResult()}
{shouldScaleElements && (
)}
); } }