From 999fad3bad04f64b5b7a3e66733794ce1a5a4fb1 Mon Sep 17 00:00:00 2001 From: vaidankarswapnil <81285216+vaidankarswapnil@users.noreply.github.com> Date: Tue, 15 Jun 2021 00:45:13 +0530 Subject: [PATCH] Migrate Query Tab to React (#852) Co-authored-by: Steve Faulkner --- less/documentDB.less | 2 +- package-lock.json | 38 +- package.json | 2 + .../BrowseQueriesPane/BrowseQueriesPane.tsx | 10 +- .../Panes/LoadQueryPane/LoadQueryPane.tsx | 22 +- .../Panes/SaveQueryPane/SaveQueryPane.tsx | 7 +- src/Explorer/Tabs/MongoDocumentsTab.ts | 2 +- src/Explorer/Tabs/MongoQueryTab.ts | 10 +- src/Explorer/Tabs/QueryTab/QueryTab.tsx | 54 + .../Tabs/QueryTab/QueryTabComponent.less | 285 +++++ .../Tabs/QueryTab/QueryTabComponent.tsx | 1090 +++++++++++++++++ src/Explorer/Tabs/TabsBase.ts | 5 +- src/Explorer/Tree/Collection.ts | 31 +- 13 files changed, 1496 insertions(+), 62 deletions(-) create mode 100644 src/Explorer/Tabs/QueryTab/QueryTab.tsx create mode 100644 src/Explorer/Tabs/QueryTab/QueryTabComponent.less create mode 100644 src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx diff --git a/less/documentDB.less b/less/documentDB.less index 159b03cd7..36744ddeb 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -3088,4 +3088,4 @@ settings-pane { .hiddenMain { display: none; height: 0px; -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 4c95dc071..50cb07343 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5650,6 +5650,15 @@ "redux": "^4.0.0" } }, + "@types/react-splitter-layout": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/react-splitter-layout/-/react-splitter-layout-3.0.1.tgz", + "integrity": "sha512-NsKq32LdG11G/Uj+xo2QmC9S8YSe8JRtxkBhsBE7ODFs0zcnzNEqFAQirP0H7rPe2WFGiu+d/44xbHsew7QAJw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-table": { "version": "6.8.7", "resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-6.8.7.tgz", @@ -17690,12 +17699,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -18499,9 +18502,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash-es": { "version": "4.17.20", @@ -18728,9 +18731,9 @@ "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==" }, "marked": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/marked/-/marked-2.0.3.tgz", - "integrity": "sha512-5otztIIcJfPc2qGTN8cVtOJEjNJZ0jwa46INMagrYfk0EvqtRuEHLsEe0LrFS0/q+ZRKT0+kXK7P2T1AN5lWRA==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-2.0.6.tgz", + "integrity": "sha512-S2mYj0FzTQa0dLddssqwRVW4EOJOVJ355Xm2Vcbm+LU7GQRGWvwbO5K87OaPSOux2AwTSgtPPaXmc8sDPrhn2A==", "dev": true }, "martinez-polygon-clipping": { @@ -21635,6 +21638,11 @@ "react-is": "^16.9.0" } }, + "react-splitter-layout": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/react-splitter-layout/-/react-splitter-layout-4.0.0.tgz", + "integrity": "sha512-SLqOjBOxRuizWUa83w6q5/u9cDWa9/yj9Iko9V9JFN8x+cqIXiDlUFWSx+icz3IIgvsN/oRIw3za5/32RjIwrA==" + }, "react-syntax-highlighter": { "version": "12.2.1", "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-12.2.1.tgz", @@ -24367,12 +24375,6 @@ "universalify": "^2.0.0" } }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", diff --git a/package.json b/package.json index 811475c19..afaaed826 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "react-i18next": "11.8.5", "react-notification-system": "0.2.17", "react-redux": "7.1.3", + "react-splitter-layout": "4.0.0", "redux": "4.0.4", "reflect-metadata": "0.1.13", "rx-jupyter": "5.5.12", @@ -123,6 +124,7 @@ "@types/react-dom": "17.0.3", "@types/react-notification-system": "0.2.39", "@types/react-redux": "7.1.7", + "@types/react-splitter-layout": "3.0.1", "@types/sanitize-html": "1.27.2", "@types/sinon": "2.3.3", "@types/styled-components": "5.1.1", diff --git a/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx b/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx index dead62b56..13ae70e44 100644 --- a/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx +++ b/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx @@ -12,7 +12,7 @@ import { QueriesGridComponentProps, } from "../../Controls/QueriesGridReactComponent/QueriesGridComponent"; import Explorer from "../../Explorer"; -import QueryTab from "../../Tabs/QueryTab"; +import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab"; interface BrowseQueriesPaneProps { explorer: Explorer; @@ -31,13 +31,13 @@ export const BrowseQueriesPane: FunctionComponent = ({ } else if (userContext.apiType === "Mongo") { selectedCollection.onNewMongoQueryClick(selectedCollection, undefined); } else { - selectedCollection.onNewQueryClick(selectedCollection, undefined); + selectedCollection.onNewQueryClick(selectedCollection, undefined, savedQuery.query); } - const queryTab = explorer.tabsManager.activeTab() as QueryTab; + + const queryTab = explorer && (explorer.tabsManager.activeTab() as NewQueryTab); queryTab.tabTitle(savedQuery.queryName); queryTab.tabPath(`${selectedCollection.databaseId}>${selectedCollection.id()}>${savedQuery.queryName}`); - queryTab.initialEditorContent(savedQuery.query); - queryTab.sqlQueryEditorContent(savedQuery.query); + trace(Action.LoadSavedQuery, ActionModifiers.Mark, { dataExplorerArea: Areas.ContextualPane, queryName: savedQuery.queryName, diff --git a/src/Explorer/Panes/LoadQueryPane/LoadQueryPane.tsx b/src/Explorer/Panes/LoadQueryPane/LoadQueryPane.tsx index 2539d42b1..ede66f656 100644 --- a/src/Explorer/Panes/LoadQueryPane/LoadQueryPane.tsx +++ b/src/Explorer/Panes/LoadQueryPane/LoadQueryPane.tsx @@ -8,7 +8,6 @@ import { useSidePanel } from "../../../hooks/useSidePanel"; import { userContext } from "../../../UserContext"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils"; import Explorer from "../../Explorer"; -import QueryTab from "../../Tabs/QueryTab"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; interface LoadQueryPaneProps { @@ -60,20 +59,19 @@ export const LoadQueryPane: FunctionComponent = ({ explorer const loadQueryFromFile = async (file: File): Promise => { const selectedCollection: Collection = explorer?.findSelectedCollection(); - if (!selectedCollection) { - logError("No collection was selected", "LoadQueryPane.loadQueryFromFile"); - } else if (userContext.apiType === "Mongo") { - selectedCollection.onNewMongoQueryClick(selectedCollection, undefined); - } else { - selectedCollection.onNewQueryClick(selectedCollection, undefined); - } const reader = new FileReader(); + let fileData: string; // eslint-disable-next-line @typescript-eslint/no-explicit-any reader.onload = (evt: any): void => { - const fileData: string = evt.target.result; - const queryTab = explorer.tabsManager.activeTab() as QueryTab; - queryTab.initialEditorContent(fileData); - queryTab.sqlQueryEditorContent(fileData); + fileData = evt.target.result; + + if (!selectedCollection) { + logError("No collection was selected", "LoadQueryPane.loadQueryFromFile"); + } else if (userContext.apiType === "Mongo") { + selectedCollection.onNewMongoQueryClick(selectedCollection, undefined); + } else { + selectedCollection.onNewQueryClick(selectedCollection, undefined, fileData); + } }; reader.onerror = (): void => { diff --git a/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.tsx b/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.tsx index bea696d06..ff5089d88 100644 --- a/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.tsx +++ b/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.tsx @@ -9,7 +9,7 @@ import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor"; import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; import Explorer from "../../Explorer"; -import QueryTab from "../../Tabs/QueryTab"; +import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; interface SaveQueryPaneProps { @@ -33,8 +33,9 @@ export const SaveQueryPane: FunctionComponent = ({ explorer logConsoleError("Failed to save query: account not setup to save queries"); } - const queryTab = explorer && (explorer.tabsManager.activeTab() as QueryTab); - const query: string = queryTab && queryTab.sqlQueryEditorContent(); + const queryTab = explorer && (explorer.tabsManager.activeTab() as NewQueryTab); + const query: string = queryTab && queryTab.iTabAccessor.onSaveClickEvent(); + if (!queryName || queryName.length === 0) { setFormError("No query name specified"); logConsoleError("Could not save query -- No query name specified. Please specify a query name."); diff --git a/src/Explorer/Tabs/MongoDocumentsTab.ts b/src/Explorer/Tabs/MongoDocumentsTab.ts index f5b2f2959..3b3866b9e 100644 --- a/src/Explorer/Tabs/MongoDocumentsTab.ts +++ b/src/Explorer/Tabs/MongoDocumentsTab.ts @@ -281,7 +281,7 @@ export default class MongoDocumentsTab extends DocumentsTab { } /** Renders a Javascript object to be displayed inside Monaco Editor */ - protected renderObjectForEditor(value: any, replacer: any, space: string | number): string { + public renderObjectForEditor(value: any, replacer: any, space: string | number): string { return MongoUtility.tojson(value, null, false); } diff --git a/src/Explorer/Tabs/MongoQueryTab.ts b/src/Explorer/Tabs/MongoQueryTab.ts index 53ba3271e..2126456ba 100644 --- a/src/Explorer/Tabs/MongoQueryTab.ts +++ b/src/Explorer/Tabs/MongoQueryTab.ts @@ -1,10 +1,10 @@ -import * as ViewModels from "../../Contracts/ViewModels"; import Q from "q"; -import MongoUtility from "../../Common/MongoUtility"; -import QueryTab from "./QueryTab"; import * as HeadersUtility from "../../Common/HeadersUtility"; -import { queryIterator } from "../../Common/MongoProxyClient"; import { MinimalQueryIterator } from "../../Common/IteratorUtilities"; +import { queryIterator } from "../../Common/MongoProxyClient"; +import MongoUtility from "../../Common/MongoUtility"; +import * as ViewModels from "../../Contracts/ViewModels"; +import QueryTab from "./QueryTab"; export default class MongoQueryTab extends QueryTab { public collection: ViewModels.Collection; @@ -16,7 +16,7 @@ export default class MongoQueryTab extends QueryTab { this.monacoSettings = new ViewModels.MonacoEditorSettings("plaintext", false); } /** Renders a Javascript object to be displayed inside Monaco Editor */ - protected renderObjectForEditor(value: any, replacer: any, space: string | number): string { + public renderObjectForEditor(value: any, replacer: any, space: string | number): string { return MongoUtility.tojson(value, null, false); } diff --git a/src/Explorer/Tabs/QueryTab/QueryTab.tsx b/src/Explorer/Tabs/QueryTab/QueryTab.tsx new file mode 100644 index 000000000..33786ac33 --- /dev/null +++ b/src/Explorer/Tabs/QueryTab/QueryTab.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import * as DataModels from "../../../Contracts/DataModels"; +import type { QueryTabOptions } from "../../../Contracts/ViewModels"; +import Explorer from "../../Explorer"; +import { IQueryTabComponentProps, ITabAccessor } from "../../Tabs/QueryTab/QueryTabComponent"; +import TabsBase from "../TabsBase"; +import QueryTabComponent from "./QueryTabComponent"; + +export interface IQueryTabProps { + container: Explorer; +} + +export class NewQueryTab extends TabsBase { + public queryText: string; + public currentQuery: string; + public partitionKey: DataModels.PartitionKey; + public iQueryTabComponentProps: IQueryTabComponentProps; + public iTabAccessor: ITabAccessor; + + constructor(options: QueryTabOptions, private props: IQueryTabProps) { + super(options); + this.partitionKey = options.partitionKey; + this.iQueryTabComponentProps = { + collection: this.collection, + isExecutionError: this.isExecutionError(), + tabId: this.tabId, + tabsBaseInstance: this, + queryText: options.queryText, + partitionKey: this.partitionKey, + container: this.props.container, + onTabAccessor: (instance: ITabAccessor): void => { + this.iTabAccessor = instance; + }, + }; + } + + public render(): JSX.Element { + return ; + } + + public onTabClick(): void { + this.manager?.activateTab(this); + this.iTabAccessor.onTabClickEvent(); + } + + public onCloseTabButtonClick(): void { + this.manager?.closeTab(this); + this.iTabAccessor.onCloseClickEvent(true); + } + + public getContainer(): Explorer { + return this.props.container; + } +} diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.less b/src/Explorer/Tabs/QueryTab/QueryTabComponent.less new file mode 100644 index 000000000..13daf455c --- /dev/null +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.less @@ -0,0 +1,285 @@ +@import "../../../../less/Common/Constants.less"; +@import "../../../../less/Common/TabCommon.less"; + +@MongoQueryEditorHeight: 50px; +@ResultsTextFontWeight: 600; +@ToggleHeight: 30px; +@ToggleWidth: 250px; +@QueryEngineExeInfo: 305px; + +.tab-pane { + .tabContentContainer(); + + .tabPaneContentContainer { + position: relative; + .tabContentContainer(); + + .mongoQueryHelper { + margin: @DefaultSpace 0px 0px 44px; + } + + .splitter-layout { + .layout-pane-primary { + overflow: hidden !important; + .queryEditor { + .flex-display(); + height: 100%; + width: 100%; + margin-top: @SmallSpace; + + .jsonEditor { + border: none; + margin-top: @SmallSpace; + } + } + } + + .queryEditor.mongoQueryEditor { + margin-top: 32px; + overflow: hidden; + } + + .queryEditorHorizontalSplitter { + margin: auto; + display: block; + } + } + + .queryErrorsHeaderContainer { + padding: 24px @LargeSpace 0px @MediumSpace; + + .queryErrors { + font-size: @mediumFontSize; + list-style-type: none; + color: @BaseDark; + font-weight: bold; + margin-left: 24px; + } + } + + .queryResultErrorContentContainer { + .flex-display(); + .flex-direction(); + font-size: @DefaultFontSize; + padding: @DefaultSpace; + height: 100%; + width: 100%; + overflow: hidden; + + .queryEditorWatermark { + text-align: center; + margin: auto; + height: 25vh; // this is to align the water mark in center of the layout. + + p { + margin-bottom: @LargeSpace; + color: @BaseHigh; + } + + .queryEditorWatermarkText { + color: @BaseHigh; + font-size: @DefaultFontSize; + font-family: @DataExplorerFont; + } + } + + .queryResultsErrorsContent { + height: 100%; + margin-left: @MediumSpace; + .flex-display(); + .flex-direction(); + + div[role="tabpanel"] { + height: 100%; + div:nth-child(1) { + height: 100%; + } + } + + .result-metadata { + padding: @LargeSpace @SmallSpace @MediumSpace @MediumSpace; + height: auto !important; + .queryResultDivider { + margin-left: @SmallSpace; + margin-right: @SmallSpace; + } + + .queryResultNextEnable { + color: @AccentMediumHigh; + font-size: @mediumFontSize; + cursor: pointer; + + img { + height: @ImgHeight; + width: @ImgWidth; + margin-left: @SmallSpace; + } + } + + .queryResultNextDisable { + color: @BaseMediumHigh; + opacity: 0.5; + font-size: @mediumFontSize; + + img { + height: @ImgHeight; + width: @ImgWidth; + margin-left: @SmallSpace; + } + } + } + + .tab-pane.active { + height: 100%; + width: 100%; + } + + .errorContent { + .flex-display(); + width: 60%; + white-space: nowrap; + font-size: @mediumFontSize; + padding: 0px @MediumSpace 0px @MediumSpace; + + .errorMessage { + padding: @SmallSpace; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .errorDetailsLink { + cursor: pointer; + padding: @SmallSpace; + } + + .queryMetricsSummaryContainer { + .flex-display(); + .flex-direction(); + overflow: hidden; + position: relative; + height: 100%; + + .queryMetricsSummary { + margin: @LargeSpace @LargeSpace 0px @DefaultSpace; + table-layout: fixed; + display: block; + height: 100%; + overflow-y: auto; + overflow-x: hidden; + + caption { + width: 100px; + } + + .queryMetricsSummaryHead { + .flex-display(); + } + + .queryMetricsSummaryHeader.queryMetricsSummaryTuple { + font-size: 10px; + } + + .queryMetricsSummaryBody { + .flex-display(); + .flex-direction(); + } + + .queryMetricsSummaryTuple { + border-bottom: 1px solid @BaseMedium; + height: 32px; + font-size: 12px; + width: 100%; + .flex-display(); + th, + td { + padding: @DefaultSpace; + + &:nth-child(1) { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + flex: 0 0 50%; + } + + &:nth-child(3) { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + flex: 0 0 50%; + } + + .queryMetricInfoTooltip { + .infoTooltip(); + + &:hover .queryMetricTooltipText { + .tooltipVisible(); + } + + &:focus .queryMetricTooltipText { + .tooltipVisible(); + } + + .queryMetricTooltipText { + top: -50px; + width: auto; + white-space: nowrap; + left: 6px; + visibility: hidden; + background-color: @BaseHigh; + color: @BaseLight; + position: absolute; + z-index: 1; + padding: @MediumSpace; + + &::after { + border-width: (2 * @MediumSpace) (2 * @MediumSpace) 0px 0px; + bottom: -14px; + .tooltipTextAfter(); + } + } + + .queryEngineExeTimeInfo { + width: @QueryEngineExeInfo; + top: -85px; + white-space: pre-wrap; + } + } + } + } + } + + .downloadMetricsLinkContainer { + margin: 24px 0px 50px @MediumSpace; + position: sticky; + #downloadMetricsLink { + color: @BaseHigh; + padding: @DefaultSpace; + font-size: @mediumFontSize; + border: @ButtonBorderWidth solid @BaseLight; + cursor: pointer; + + &:hover { + .hover(); + } + + &:active { + border: @ButtonBorderWidth dashed @AccentMedium; + .active(); + } + } + } + } + + json-editor { + .flex-display(); + .flex-direction(); + overflow: hidden; + height: 100%; + width: 100%; + padding: @SmallSpace 0px @SmallSpace @MediumSpace; + } + } + } + } +} diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx new file mode 100644 index 000000000..5d661213b --- /dev/null +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -0,0 +1,1090 @@ +import { DetailsList, DetailsListLayoutMode, IColumn, Pivot, PivotItem, SelectionMode } from "@fluentui/react"; +import React, { Fragment } from "react"; +import SplitterLayout from "react-splitter-layout"; +import "react-splitter-layout/lib/index.css"; +import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; +import SaveQueryIcon from "../../../../images/save-cosmos.svg"; +import * as Constants from "../../../Common/Constants"; +import { NormalizedEventKey } from "../../../Common/Constants"; +import { queryDocuments } from "../../../Common/dataAccess/queryDocuments"; +import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage"; +import { getErrorMessage } from "../../../Common/ErrorHandlingUtils"; +import * as HeadersUtility from "../../../Common/HeadersUtility"; +import { MinimalQueryIterator } from "../../../Common/IteratorUtilities"; +import { queryIterator } from "../../../Common/MongoProxyClient"; +import MongoUtility from "../../../Common/MongoUtility"; +import { Splitter } from "../../../Common/Splitter"; +import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip"; +import * as DataModels from "../../../Contracts/DataModels"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import { useNotificationConsole } from "../../../hooks/useNotificationConsole"; +import { userContext } from "../../../UserContext"; +import * as QueryUtils from "../../../Utils/QueryUtils"; +import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; +import { EditorReact } from "../../Controls/Editor/EditorReact"; +import Explorer from "../../Explorer"; +import { useCommandBar } from "../../Menus/CommandBar/CommandBarComponentAdapter"; +import TabsBase from "../TabsBase"; +import { TabsManager } from "../TabsManager"; +import "./QueryTabComponent.less"; + +enum ToggleState { + Result, + QueryMetrics, +} + +export interface IDocument { + metric: string; + value: string; + toolTip: string; + isQueryMetricsEnabled: boolean; +} + +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; + tabManager?: TabsManager; + onTabAccessor: (instance: ITabAccessor) => void; + isPreferredApiMongoDB?: boolean; + monacoEditorSetting?: string; + viewModelcollection?: ViewModels.Collection; +} + +interface IQueryTabStates { + queryMetrics: Map; + aggregatedQueryMetrics: DataModels.QueryMetrics; + activityId: string; + roundTrips: number; + toggleState: ToggleState; + isQueryMetricsEnabled: boolean; + showingDocumentsDisplayText: string; + requestChargeDisplayText: string; + initialEditorContent: string; + sqlQueryEditorContent: string; + selectedContent: string; + _executeQueryButtonTitle: string; + sqlStatementToExecute: string; + queryResults: string; + statusMessge: string; + statusIcon: string; + allResultsMetadata: ViewModels.QueryResultsMetadata[]; + error: string; + isTemplateReady: boolean; + _isSaveQueriesEnabled: boolean; + isExecutionError: boolean; + isExecuting: boolean; + columns: IColumn[]; + items: IDocument[]; +} + +export default class QueryTabComponent extends React.Component { + public queryEditorId: string; + public executeQueryButton: Button; + public fetchNextPageButton: Button; + public saveQueryButton: Button; + public splitterId: string; + public splitter: Splitter; + public isPreferredApiMongoDB: boolean; + public resultsDisplay: string; + protected monacoSettings: ViewModels.MonacoEditorSettings; + protected _iterator: MinimalQueryIterator; + private _resourceTokenPartitionKey: string; + _partitionKey: DataModels.PartitionKey; + public maybeSubQuery: boolean; + public isCloseClicked: boolean; + public allItems: IDocument[]; + public defaultQueryText: string; + + constructor(props: IQueryTabComponentProps) { + super(props); + const columns: IColumn[] = [ + { + key: "column1", + name: "", + minWidth: 16, + maxWidth: 16, + data: String, + fieldName: "toolTip", + onRender: this.onRenderColumnItem, + }, + { + key: "column2", + name: "METRIC", + minWidth: 200, + data: String, + fieldName: "metric", + }, + { + key: "column3", + name: "VALUE", + minWidth: 200, + data: String, + fieldName: "value", + }, + ]; + + if (this.props.isPreferredApiMongoDB) { + this.defaultQueryText = props.queryText; + } else { + this.defaultQueryText = props.queryText !== void 0 ? props.queryText : "SELECT * FROM c"; + } + + this.state = { + queryMetrics: new Map(), + aggregatedQueryMetrics: undefined, + activityId: "", + roundTrips: undefined, + toggleState: ToggleState.Result, + isQueryMetricsEnabled: userContext.apiType === "SQL" || false, + showingDocumentsDisplayText: this.resultsDisplay, + requestChargeDisplayText: "", + initialEditorContent: this.defaultQueryText, + sqlQueryEditorContent: this.defaultQueryText, + selectedContent: "", + _executeQueryButtonTitle: "Execute Query", + sqlStatementToExecute: this.defaultQueryText, + queryResults: "", + statusMessge: "", + statusIcon: "", + allResultsMetadata: [], + error: "", + isTemplateReady: false, + _isSaveQueriesEnabled: userContext.apiType === "SQL" || userContext.apiType === "Gremlin", + isExecutionError: this.props.isExecutionError, + isExecuting: false, + columns: columns, + items: [], + }; + this.isCloseClicked = false; + this.splitterId = this.props.tabId + "_splitter"; + this.queryEditorId = `queryeditor${this.props.tabId}`; + this._partitionKey = props.partitionKey; + this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB; + this.monacoSettings = new ViewModels.MonacoEditorSettings(this.props.monacoEditorSetting, false); + + this.executeQueryButton = { + enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0, + visible: true, + }; + const sql = this.state.sqlQueryEditorContent; + this.maybeSubQuery = sql && /.*\(.*SELECT.*\)/i.test(sql); + + this.saveQueryButton = { + enabled: this.state._isSaveQueriesEnabled, + visible: this.state._isSaveQueriesEnabled, + }; + this.fetchNextPageButton = { + enabled: (() => { + const allResultsMetadata = this.state.allResultsMetadata || []; + const numberOfResultsMetadata = allResultsMetadata.length; + + if (numberOfResultsMetadata === 0) { + return false; + } + + if (allResultsMetadata[numberOfResultsMetadata - 1].hasMoreResults) { + return true; + } + + return false; + })(), + visible: true, + }; + + this._buildCommandBarOptions(); + props.onTabAccessor({ + onTabClickEvent: this.onTabClick.bind(this), + onSaveClickEvent: this.getCurrentEditorQuery.bind(this), + onCloseClickEvent: this.onCloseClick.bind(this), + }); + } + + public onRenderColumnItem(item: IDocument): JSX.Element { + if (item.toolTip !== "") { + return {`${item.toolTip}`}; + } else { + return undefined; + } + } + + public generateDetailsList(): IDocument[] { + const items: IDocument[] = []; + const allItems: IDocument[] = [ + { + metric: "Request Charge", + value: this.state.requestChargeDisplayText, + toolTip: "", + isQueryMetricsEnabled: true, + }, + { + metric: "Showing Results", + value: this.state.showingDocumentsDisplayText, + toolTip: "", + isQueryMetricsEnabled: true, + }, + { + metric: "Retrieved document count", + value: + this.state.aggregatedQueryMetrics.retrievedDocumentCount !== undefined + ? this.state.aggregatedQueryMetrics.retrievedDocumentCount.toString() + : "", + toolTip: "Total number of retrieved documents", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "Retrieved document size", + value: + this.state.aggregatedQueryMetrics.retrievedDocumentSize !== undefined + ? this.state.aggregatedQueryMetrics.retrievedDocumentSize.toString() + " bytes" + : "", + toolTip: "Total size of retrieved documents in bytes", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "Output document count", + value: + this.state.aggregatedQueryMetrics.outputDocumentCount !== undefined + ? this.state.aggregatedQueryMetrics.outputDocumentCount.toString() + : "", + toolTip: "Number of output documents", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "Output document size", + value: + this.state.aggregatedQueryMetrics.outputDocumentSize !== undefined + ? this.state.aggregatedQueryMetrics.outputDocumentSize.toString() + " bytes" + : "", + toolTip: "Total size of output documents in bytes", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "Index hit document count", + value: + this.state.aggregatedQueryMetrics.indexHitDocumentCount !== undefined + ? this.state.aggregatedQueryMetrics.indexHitDocumentCount.toString() + : "", + toolTip: "Total number of documents matched by the filter", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "Index lookup time", + value: + this.state.aggregatedQueryMetrics.indexLookupTime !== undefined + ? this.state.aggregatedQueryMetrics.indexLookupTime.toString() + " ms" + : "", + toolTip: "Time spent in physical index layer", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "Document load time", + value: + this.state.aggregatedQueryMetrics.documentLoadTime !== undefined + ? this.state.aggregatedQueryMetrics.documentLoadTime.toString() + " ms" + : "", + toolTip: "Time spent in loading documents", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "Query engine execution time", + value: + this.state.aggregatedQueryMetrics.runtimeExecutionTimes.queryEngineExecutionTime !== undefined + ? this.state.aggregatedQueryMetrics.runtimeExecutionTimes.queryEngineExecutionTime.toString() + " ms" + : "", + toolTip: + "Time spent by the query engine to execute the query expression (excludes other execution times like load documents or write results)", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "System function execution time", + value: + this.state.aggregatedQueryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime !== undefined + ? this.state.aggregatedQueryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime.toString() + " ms" + : "", + toolTip: "Total time spent executing system (built-in) functions", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "User defined function execution time", + value: + this.state.aggregatedQueryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime !== undefined + ? this.state.aggregatedQueryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime.toString() + + " ms" + : "", + toolTip: "Total time spent executing user-defined functions", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "Document write time", + value: + this.state.aggregatedQueryMetrics.documentWriteTime !== undefined + ? this.state.aggregatedQueryMetrics.documentWriteTime.toString() + " ms" + : "", + toolTip: "Time spent to write query result set to response buffer", + isQueryMetricsEnabled: this.state.isQueryMetricsEnabled, + }, + { + metric: "Round Trips", + value: this.state.roundTrips ? this.state.roundTrips.toString() : "", + toolTip: "", + isQueryMetricsEnabled: true, + }, + { + metric: "Activity id", + value: this.state.activityId ? this.state.activityId : "", + toolTip: "", + isQueryMetricsEnabled: true, + }, + ]; + + allItems.forEach((item) => { + if (item.metric === "Round Trips" || item.metric === "Activity id") { + if (item.metric === "Round Trips" && this.state.roundTrips !== undefined) { + items.push(item); + } else if (item.metric === "Activity id" && this.state.activityId !== undefined) { + items.push(item); + } + } else { + if (item.isQueryMetricsEnabled) { + items.push(item); + } + } + }); + return items; + } + + public onCloseClick(isClicked: boolean): void { + this.isCloseClicked = isClicked; + } + + public getCurrentEditorQuery(): string { + return this.state.sqlQueryEditorContent; + } + + public onTabClick(): void { + setTimeout(() => { + if (!this.isCloseClicked) { + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + } else { + this.isCloseClicked = false; + } + }, 0); + } + + public onExecuteQueryClick = async (): Promise => { + const sqlStatement = this.state.selectedContent || this.state.sqlQueryEditorContent; + + this.setState({ + sqlStatementToExecute: sqlStatement, + allResultsMetadata: [], + queryResults: "", + }); + + this._iterator = undefined; + setTimeout(async () => { + await this._executeQueryDocumentsPage(0); + }, 100); + }; + + public onSaveQueryClick = (): void => { + this.props.collection && this.props.collection.container && this.props.collection.container.openSaveQueryPanel(); + }; + + public onSavedQueriesClick = (): void => { + this.props.collection && + this.props.collection.container && + this.props.collection.container.openBrowseQueriesPanel(); + }; + + public async onFetchNextPageClick(): Promise { + const allResultsMetadata = (this.state.allResultsMetadata && this.state.allResultsMetadata) || []; + const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1]; + const firstResultIndex: number = (metadata && Number(metadata.firstItemIndex)) || 1; + const itemCount: number = (metadata && Number(metadata.itemCount)) || 0; + + await this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1); + } + + //eslint-disable-next-line + public onErrorDetailsClick = (): boolean => { + useNotificationConsole.getState().expandConsole(); + + return false; + }; + + public onErrorDetailsKeyPress = (event: React.KeyboardEvent): boolean => { + if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) { + this.onErrorDetailsClick(); + return false; + } + + return true; + }; + + public toggleResult(): void { + this.setState({ + toggleState: ToggleState.Result, + }); + } + + public toggleMetrics(): void { + this.setState({ + toggleState: ToggleState.QueryMetrics, + }); + } + + 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(); + } + + public isResultToggled(): boolean { + return this.state.toggleState === ToggleState.Result; + } + + public isMetricsToggled(): boolean { + return this.state.toggleState === ToggleState.QueryMetrics; + } + + public onDownloadQueryMetricsCsvClick = (): boolean => { + this._downloadQueryMetricsCsvData(); + return false; + }; + + public onDownloadQueryMetricsCsvKeyPress = (event: React.KeyboardEvent): boolean => { + if (event.key === NormalizedEventKey.Space || NormalizedEventKey.Enter) { + this._downloadQueryMetricsCsvData(); + return false; + } + + return true; + }; + + //eslint-disable-next-line + private async _executeQueryDocumentsPage(firstItemIndex: number): Promise { + this.setState({ + error: "", + roundTrips: undefined, + }); + + if (this._iterator === undefined) { + if (this.isPreferredApiMongoDB) { + this._initIteratorMongo(); + } else { + this._initIterator(); + } + } + + await this._queryDocumentsPage(firstItemIndex); + } + + private async _queryDocumentsPage(firstItemIndex: number): Promise { + let results: string; + + this.props.tabsBaseInstance.isExecutionError(false); + this.setState({ + isExecutionError: false, + }); + this._resetAggregateQueryMetrics(); + + 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, + }); + + try { + const queryResults: ViewModels.QueryResults = await QueryUtils.queryPagesUntilContentPresent( + firstItemIndex, + queryDocuments + ); + const allResultsMetadata = (this.state.allResultsMetadata && this.state.allResultsMetadata) || []; + const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1]; + const resultsMetadata: ViewModels.QueryResultsMetadata = { + hasMoreResults: queryResults.hasMoreResults, + itemCount: queryResults.itemCount, + firstItemIndex: queryResults.firstItemIndex, + lastItemIndex: queryResults.lastItemIndex, + }; + this.state.allResultsMetadata.push(resultsMetadata); + + this.setState({ + activityId: queryResults.activityId, + roundTrips: queryResults.roundTrips, + }); + + const documents = queryResults.documents; + if (this.isPreferredApiMongoDB) { + results = MongoUtility.tojson(documents, undefined, false); + } else { + results = this.props.tabsBaseInstance.renderObjectForEditor(documents, undefined, 4); + } + + const resultsDisplay: string = + queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`; + + this.setState({ + showingDocumentsDisplayText: resultsDisplay, + requestChargeDisplayText: `${queryResults.requestCharge} RUs`, + queryResults: results, + }); + + this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]); + + if (queryResults.itemCount === 0 && metadata !== undefined && metadata.itemCount >= 0) { + // we let users query for the next page because the SDK sometimes specifies there are more elements + // even though there aren't any so we should not update the prior query results. + return; + } + } 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, + }); + this.togglesOnFocus(); + } + } + + private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void { + if (!metricsMap) { + this.allItems = this.generateDetailsList(); + this.setState({ + items: this.allItems, + }); + return; + } + + Object.keys(metricsMap).forEach((key: string) => { + this.state.queryMetrics.set(key, metricsMap[key]); + }); + + this._aggregateQueryMetrics(this.state.queryMetrics); + this.allItems = this.generateDetailsList(); + this.setState({ + items: this.allItems, + }); + } + + private _aggregateQueryMetrics(metricsMap: Map): DataModels.QueryMetrics { + if (!metricsMap) { + return undefined; + } + + const aggregatedMetrics: DataModels.QueryMetrics = this.state.aggregatedQueryMetrics; + metricsMap.forEach((queryMetrics) => { + if (queryMetrics) { + aggregatedMetrics.documentLoadTime = + queryMetrics.documentLoadTime && + this._normalize(queryMetrics.documentLoadTime.totalMilliseconds()) + + this._normalize(aggregatedMetrics.documentLoadTime); + aggregatedMetrics.documentWriteTime = + queryMetrics.documentWriteTime && + this._normalize(queryMetrics.documentWriteTime.totalMilliseconds()) + + this._normalize(aggregatedMetrics.documentWriteTime); + aggregatedMetrics.indexHitDocumentCount = + queryMetrics.indexHitDocumentCount && + this._normalize(queryMetrics.indexHitDocumentCount) + + this._normalize(aggregatedMetrics.indexHitDocumentCount); + aggregatedMetrics.outputDocumentCount = + queryMetrics.outputDocumentCount && + this._normalize(queryMetrics.outputDocumentCount) + this._normalize(aggregatedMetrics.outputDocumentCount); + aggregatedMetrics.outputDocumentSize = + queryMetrics.outputDocumentSize && + this._normalize(queryMetrics.outputDocumentSize) + this._normalize(aggregatedMetrics.outputDocumentSize); + aggregatedMetrics.indexLookupTime = + queryMetrics.indexLookupTime && + this._normalize(queryMetrics.indexLookupTime.totalMilliseconds()) + + this._normalize(aggregatedMetrics.indexLookupTime); + aggregatedMetrics.retrievedDocumentCount = + queryMetrics.retrievedDocumentCount && + this._normalize(queryMetrics.retrievedDocumentCount) + + this._normalize(aggregatedMetrics.retrievedDocumentCount); + aggregatedMetrics.retrievedDocumentSize = + queryMetrics.retrievedDocumentSize && + this._normalize(queryMetrics.retrievedDocumentSize) + + this._normalize(aggregatedMetrics.retrievedDocumentSize); + aggregatedMetrics.vmExecutionTime = + queryMetrics.vmExecutionTime && + this._normalize(queryMetrics.vmExecutionTime.totalMilliseconds()) + + this._normalize(aggregatedMetrics.vmExecutionTime); + aggregatedMetrics.totalQueryExecutionTime = + queryMetrics.totalQueryExecutionTime && + this._normalize(queryMetrics.totalQueryExecutionTime.totalMilliseconds()) + + this._normalize(aggregatedMetrics.totalQueryExecutionTime); + + aggregatedMetrics.runtimeExecutionTimes.queryEngineExecutionTime = + aggregatedMetrics.runtimeExecutionTimes && + this._normalize(queryMetrics.runtimeExecutionTimes.queryEngineExecutionTime.totalMilliseconds()) + + this._normalize(aggregatedMetrics.runtimeExecutionTimes.queryEngineExecutionTime); + aggregatedMetrics.runtimeExecutionTimes.systemFunctionExecutionTime = + aggregatedMetrics.runtimeExecutionTimes && + this._normalize(queryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime.totalMilliseconds()) + + this._normalize(aggregatedMetrics.runtimeExecutionTimes.systemFunctionExecutionTime); + aggregatedMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime = + aggregatedMetrics.runtimeExecutionTimes && + this._normalize(queryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime.totalMilliseconds()) + + this._normalize(aggregatedMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime); + } + }); + + return aggregatedMetrics; + } + + public _downloadQueryMetricsCsvData(): void { + const csvData: string = this._generateQueryMetricsCsvData(); + if (!csvData) { + return; + } + + if (navigator.msSaveBlob) { + // for IE and Edge + navigator.msSaveBlob( + new Blob([csvData], { type: "data:text/csv;charset=utf-8" }), + "PerPartitionQueryMetrics.csv" + ); + } else { + const downloadLink: HTMLAnchorElement = document.createElement("a"); + downloadLink.href = "data:text/csv;charset=utf-8," + encodeURI(csvData); + downloadLink.target = "_self"; + downloadLink.download = "QueryMetricsPerPartition.csv"; + + // for some reason, FF displays the download prompt only when + // the link is added to the dom so we add and remove it + document.body.appendChild(downloadLink); + downloadLink.click(); + downloadLink.remove(); + } + } + + protected _initIterator(): void { + const options = QueryTabComponent.getIteratorOptions(); + if (this._resourceTokenPartitionKey) { + options.partitionKey = this._resourceTokenPartitionKey; + } + + this._iterator = queryDocuments( + this.props.collection.databaseId, + this.props.collection.id(), + this.state.sqlStatementToExecute, + options + ); + } + + protected _initIteratorMongo(): Promise { + //eslint-disable-next-line + const options: any = {}; + options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); + this._iterator = queryIterator( + this.props.collection.databaseId, + this.props.viewModelcollection, + this.state.sqlStatementToExecute + ); + const mongoPromise: Promise = new Promise((resolve) => { + resolve(this._iterator); + }); + return mongoPromise; + } + + //eslint-disable-next-line + public static getIteratorOptions(collection?: ViewModels.Collection): any { + //eslint-disable-next-line + const options: any = {}; + options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); + return options; + } + + private _normalize(value: number): number { + if (!value) { + return 0; + } + + return value; + } + + private _resetAggregateQueryMetrics(): void { + this.setState({ + aggregatedQueryMetrics: { + clientSideMetrics: {}, + documentLoadTime: undefined, + documentWriteTime: undefined, + indexHitDocumentCount: undefined, + outputDocumentCount: undefined, + outputDocumentSize: undefined, + indexLookupTime: undefined, + retrievedDocumentCount: undefined, + retrievedDocumentSize: undefined, + vmExecutionTime: undefined, + queryPreparationTimes: undefined, + runtimeExecutionTimes: { + queryEngineExecutionTime: undefined, + systemFunctionExecutionTime: undefined, + userDefinedFunctionExecutionTime: undefined, + }, + totalQueryExecutionTime: undefined, + }, + }); + } + + private _generateQueryMetricsCsvData(): string { + if (!this.state.queryMetrics) { + return undefined; + } + + const queryMetrics = this.state.queryMetrics; + let csvData = ""; + const columnHeaders: string = + [ + "Partition key range id", + "Retrieved document count", + "Retrieved document size (in bytes)", + "Output document count", + "Output document size (in bytes)", + "Index hit document count", + "Index lookup time (ms)", + "Document load time (ms)", + "Query engine execution time (ms)", + "System function execution time (ms)", + "User defined function execution time (ms)", + "Document write time (ms)", + ].join(",") + "\n"; + csvData = csvData + columnHeaders; + queryMetrics.forEach((queryMetric, partitionKeyRangeId) => { + const partitionKeyRangeData: string = + [ + partitionKeyRangeId, + queryMetric.retrievedDocumentCount, + queryMetric.retrievedDocumentSize, + queryMetric.outputDocumentCount, + queryMetric.outputDocumentSize, + queryMetric.indexHitDocumentCount, + queryMetric.indexLookupTime && queryMetric.indexLookupTime.totalMilliseconds(), + queryMetric.documentLoadTime && queryMetric.documentLoadTime.totalMilliseconds(), + queryMetric.runtimeExecutionTimes && + queryMetric.runtimeExecutionTimes.queryEngineExecutionTime && + queryMetric.runtimeExecutionTimes.queryEngineExecutionTime.totalMilliseconds(), + queryMetric.runtimeExecutionTimes && + queryMetric.runtimeExecutionTimes.systemFunctionExecutionTime && + queryMetric.runtimeExecutionTimes.systemFunctionExecutionTime.totalMilliseconds(), + queryMetric.runtimeExecutionTimes && + queryMetric.runtimeExecutionTimes.userDefinedFunctionExecutionTime && + queryMetric.runtimeExecutionTimes.userDefinedFunctionExecutionTime.totalMilliseconds(), + queryMetric.documentWriteTime && queryMetric.documentWriteTime.totalMilliseconds(), + ].join(",") + "\n"; + csvData = csvData + partitionKeyRangeData; + }); + + return csvData; + } + + protected getTabsButtons(): CommandButtonComponentProps[] { + const buttons: CommandButtonComponentProps[] = []; + if (this.executeQueryButton.visible) { + const label = this.state._executeQueryButtonTitle; + buttons.push({ + iconSrc: ExecuteQueryIcon, + iconAlt: label, + onCommandClick: 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, + }); + } + + return buttons; + } + + private _buildCommandBarOptions(): void { + this.props.tabsBaseInstance.updateNavbarWithTabsButtons(); + } + + public onChangeContent(newContent: string): void { + this.setState({ + sqlQueryEditorContent: newContent, + }); + 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: selectedContent, + _executeQueryButtonTitle: "Execute Selection", + }); + } else { + this.setState({ + selectedContent: "", + _executeQueryButtonTitle: "Execute Query", + }); + } + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + } + + render(): JSX.Element { + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + + return ( + +
+
+ + +
+ this.onChangeContent(newContent)} + onContentSelected={(selectedContent: string) => this.onSelectedContent(selectedContent)} + /> +
+
+ + {this.isPreferredApiMongoDB && this.state.sqlQueryEditorContent.length === 0 && ( +
+ Start by writing a Mongo query, for example: {"{'id':'foo'}"} or{" "} + + {"{ "} + {" }"} + {" "} + to get all the documents. +
+ )} + {this.maybeSubQuery && ( +
+
+ + Error + + + We have detected you may be using a subquery. Non-correlated subqueries are not currently + supported. + + Please see Cosmos sub query documentation for further information + + +
+
+ )} + {/* */} + {!!this.state.error && ( +
+ + Errors + +
+ )} + {/* */} + {/* */} +
+ {this.state.allResultsMetadata.length === 0 && + !this.state.error && + !this.state.queryResults && + !this.props.tabsBaseInstance.isExecuting() && ( +
+

+ Execute Query Watermark +

+

Execute a query to see the results

+
+ )} + {(this.state.allResultsMetadata.length > 0 || !!this.state.error || this.state.queryResults) && ( +
+ {!this.state.error && ( + + +
+ + {this.state.showingDocumentsDisplayText} + + {this.fetchNextPageButton.enabled && |} + {this.fetchNextPageButton.enabled && ( + + + Load more + Fetch next page + + + )} +
+ {this.state.queryResults && + this.state.queryResults.length > 0 && + this.state.allResultsMetadata.length > 0 && + !this.state.error && ( +
+ +
+ )} +
+ + {this.state.allResultsMetadata.length > 0 && !this.state.error && ( + + )} + +
+ )} + {/* */} + {!!this.state.error && ( + + )} + {/* */} +
+ )} +
+
+
+
+
+
+ ); + } +} diff --git a/src/Explorer/Tabs/TabsBase.ts b/src/Explorer/Tabs/TabsBase.ts index feb0d2e03..9f9f2e48c 100644 --- a/src/Explorer/Tabs/TabsBase.ts +++ b/src/Explorer/Tabs/TabsBase.ts @@ -12,7 +12,6 @@ import Explorer from "../Explorer"; import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel"; import { TabsManager } from "./TabsManager"; - // TODO: Use specific actions for logging telemetry data export default class TabsBase extends WaitsForTemplateViewModel { private static id = 0; @@ -149,7 +148,7 @@ export default class TabsBase extends WaitsForTemplateViewModel { } /** Renders a Javascript object to be displayed inside Monaco Editor */ - protected renderObjectForEditor(value: any, replacer: any, space: string | number): string { + public renderObjectForEditor(value: any, replacer: any, space: string | number): string { return JSON.stringify(value, replacer, space); } @@ -164,7 +163,7 @@ export default class TabsBase extends WaitsForTemplateViewModel { return []; } - protected updateNavbarWithTabsButtons = (): void => { + public updateNavbarWithTabsButtons = (): void => { if (this.isActive()) { useCommandBar.getState().setContextButtons(this.getTabsButtons()); } diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 1731e18c6..98141315d 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -30,7 +30,7 @@ import GraphTab from "../Tabs/GraphTab"; import MongoDocumentsTab from "../Tabs/MongoDocumentsTab"; import MongoQueryTab from "../Tabs/MongoQueryTab"; import MongoShellTab from "../Tabs/MongoShellTab"; -import QueryTab from "../Tabs/QueryTab"; +import { NewQueryTab } from "../Tabs/QueryTab/QueryTab"; import QueryTablesTab from "../Tabs/QueryTablesTab"; import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2"; import ConflictId from "./ConflictId"; @@ -617,19 +617,22 @@ export default class Collection implements ViewModels.Collection { tabTitle: title, }); - const queryTab: QueryTab = new QueryTab({ - tabKind: ViewModels.CollectionTabKind.Query, - title: title, - tabPath: "", - collection: this, - node: this, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/query`, - queryText: queryText, - partitionKey: collection.partitionKey, - onLoadStartKey: startKey, - }); - - this.container.tabsManager.activateNewTab(queryTab); + this.container.tabsManager.activateNewTab( + new NewQueryTab( + { + tabKind: ViewModels.CollectionTabKind.Query, + title: title, + tabPath: "", + collection: this, + node: this, + hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/query`, + queryText: queryText, + partitionKey: collection.partitionKey, + onLoadStartKey: startKey, + }, + { container: this.container } + ) + ); } public onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string) {