From 5da9724debd80ecd1ed73665b36e5a3c471320c3 Mon Sep 17 00:00:00 2001 From: vaidankarswapnil <81285216+vaidankarswapnil@users.noreply.github.com> Date: Tue, 15 Jun 2021 00:53:19 +0530 Subject: [PATCH] Migrate Mongo Query Tab to React(#854) Co-authored-by: Steve Faulkner --- .eslintignore | 6 +- src/Explorer/Tabs/MongoQueryTab.ts | 29 - .../Tabs/MongoQueryTab/MongoQueryTab.tsx | 47 ++ src/Explorer/Tabs/QueryTab.html | 335 ---------- src/Explorer/Tabs/QueryTab.less | 311 --------- src/Explorer/Tabs/QueryTab.test.ts | 96 --- src/Explorer/Tabs/QueryTab.ts | 594 ------------------ src/Explorer/Tabs/QueryTab/QueryTab.tsx | 5 +- src/Explorer/Tabs/TabsManager.test.ts | 31 +- src/Explorer/Tree/Collection.ts | 30 +- src/Explorer/Tree/ResourceTokenCollection.ts | 32 +- src/Main.tsx | 1 - 12 files changed, 109 insertions(+), 1408 deletions(-) delete mode 100644 src/Explorer/Tabs/MongoQueryTab.ts create mode 100644 src/Explorer/Tabs/MongoQueryTab/MongoQueryTab.tsx delete mode 100644 src/Explorer/Tabs/QueryTab.html delete mode 100644 src/Explorer/Tabs/QueryTab.less delete mode 100644 src/Explorer/Tabs/QueryTab.test.ts delete mode 100644 src/Explorer/Tabs/QueryTab.ts diff --git a/.eslintignore b/.eslintignore index a880fcc44..09b531837 100644 --- a/.eslintignore +++ b/.eslintignore @@ -143,11 +143,11 @@ src/Explorer/Tabs/DocumentsTab.test.ts src/Explorer/Tabs/DocumentsTab.ts src/Explorer/Tabs/GraphTab.ts src/Explorer/Tabs/MongoDocumentsTab.ts -src/Explorer/Tabs/MongoQueryTab.ts +# src/Explorer/Tabs/MongoQueryTab.ts src/Explorer/Tabs/MongoShellTab.ts src/Explorer/Tabs/NotebookV2Tab.ts -src/Explorer/Tabs/QueryTab.test.ts -src/Explorer/Tabs/QueryTab.ts +# src/Explorer/Tabs/QueryTab.test.ts +# src/Explorer/Tabs/QueryTab.ts src/Explorer/Tabs/QueryTablesTab.ts src/Explorer/Tabs/ScriptTabBase.ts src/Explorer/Tabs/StoredProcedureTab.ts diff --git a/src/Explorer/Tabs/MongoQueryTab.ts b/src/Explorer/Tabs/MongoQueryTab.ts deleted file mode 100644 index 2126456ba..000000000 --- a/src/Explorer/Tabs/MongoQueryTab.ts +++ /dev/null @@ -1,29 +0,0 @@ -import Q from "q"; -import * as HeadersUtility from "../../Common/HeadersUtility"; -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; - - constructor(options: ViewModels.QueryTabOptions) { - options.queryText = ""; // override sql query editor content for now so we only display mongo related help items - super(options); - this.isPreferredApiMongoDB = true; - this.monacoSettings = new ViewModels.MonacoEditorSettings("plaintext", false); - } - /** Renders a Javascript object to be displayed inside Monaco Editor */ - public renderObjectForEditor(value: any, replacer: any, space: string | number): string { - return MongoUtility.tojson(value, null, false); - } - - protected _initIterator(): Q.Promise { - let options: any = {}; - options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); - this._iterator = queryIterator(this.collection.databaseId, this.collection, this.sqlStatementToExecute()); - return Q(this._iterator); - } -} diff --git a/src/Explorer/Tabs/MongoQueryTab/MongoQueryTab.tsx b/src/Explorer/Tabs/MongoQueryTab/MongoQueryTab.tsx new file mode 100644 index 000000000..faecb47bb --- /dev/null +++ b/src/Explorer/Tabs/MongoQueryTab/MongoQueryTab.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import MongoUtility from "../../../Common/MongoUtility"; +import * as ViewModels from "../../../Contracts/ViewModels"; +import Explorer from "../../Explorer"; +import { NewQueryTab } from "../QueryTab/QueryTab"; +import QueryTabComponent, { IQueryTabComponentProps, ITabAccessor } from "../QueryTab/QueryTabComponent"; + +export interface IMongoQueryTabProps { + container: Explorer; + viewModelcollection?: ViewModels.Collection; +} + +export class NewMongoQueryTab extends NewQueryTab { + public collection: ViewModels.Collection; + public iMongoQueryTabComponentProps: IQueryTabComponentProps; + public queryText: string; + + constructor(options: ViewModels.QueryTabOptions, private mongoQueryTabProps: IMongoQueryTabProps) { + super(options, mongoQueryTabProps); + this.queryText = ""; + this.iMongoQueryTabComponentProps = { + collection: options.collection, + isExecutionError: this.isExecutionError(), + tabId: this.tabId, + tabsBaseInstance: this, + queryText: this.queryText, + partitionKey: this.partitionKey, + container: this.mongoQueryTabProps.container, + onTabAccessor: (instance: ITabAccessor): void => { + this.iTabAccessor = instance; + }, + isPreferredApiMongoDB: true, + monacoEditorSetting: "plaintext", + viewModelcollection: this.mongoQueryTabProps.viewModelcollection, + }; + } + + /** Renders a Javascript object to be displayed inside Monaco Editor */ + //eslint-disable-next-line + public renderObjectForEditor(value: any, replacer: any, space: string | number): string { + return MongoUtility.tojson(value, undefined, false); + } + + public render(): JSX.Element { + return ; + } +} diff --git a/src/Explorer/Tabs/QueryTab.html b/src/Explorer/Tabs/QueryTab.html deleted file mode 100644 index 9adb59c2d..000000000 --- a/src/Explorer/Tabs/QueryTab.html +++ /dev/null @@ -1,335 +0,0 @@ -
-
-
- Start by writing a Mongo query, for example: {'id':'foo'} or { } to get all the - documents. -
-
-
- 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 - -
-
-
- - -
- Splitter -
-
- - - - - - -
- Errors -
- - - -
-
-

Execute Query Watermark

-

Execute a query to see the results

-
-
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Query Statistics -
METRICVALUE
Request Charge - -
Showing Results - -
- Retrieved document count - - More information - Total number of retrieved documents - -
- Retrieved document size - - More information - Total size of retrieved documents in bytes - - - - bytes -
- Output document count - - More information - Number of output documents - -
- Output document size - - More information - Total size of output documents in bytes - - - - bytes -
- Index hit document count - - More information - Total number of documents matched by the filter - -
- Index lookup time - - More information - Time spent in physical index layer - - - ms -
- Document load time - - More information - Time spent in loading documents - - - ms -
- Query engine execution time - - More information - Time spent by the query engine to execute the query expression (excludes other execution times - like load documents or write results) - - - - ms -
- System function execution time - - More information - Total time spent executing system (built-in) functions - - - - ms -
- User defined function execution time - - More information - Total time spent executing user-defined functions - - - - ms -
- Document write time - - More information - Time spent to write query result set to response buffer - - - ms -
Round Trips
Activity id
- -
- -
-
- - - More details - -
-
- -
-
- -
-
diff --git a/src/Explorer/Tabs/QueryTab.less b/src/Explorer/Tabs/QueryTab.less deleted file mode 100644 index f672ef530..000000000 --- a/src/Explorer/Tabs/QueryTab.less +++ /dev/null @@ -1,311 +0,0 @@ -@import "../../../less/Common/Constants"; -@import "../../../less/Common/TabCommon"; - -@MongoQueryEditorHeight: 50px; -@ResultsTextFontWeight: 600; -@ToggleHeight: 30px; -@ToggleWidth: 250px; -@QueryEngineExeInfo: 305px; - -.tab-pane { - .tabContentContainer(); - - .tabPaneContentContainer { - .tabContentContainer(); - - .mongoQueryHelper { - margin:@DefaultSpace 0px 0px 44px; - position: absolute; - top: 115px; //this is to avoid the jump of query editor - } - - .queryEditorWithSplitter { - .flex-display(); - .flex-direction(); - flex-shrink: 0; - height: 100%; - width: 100%; - margin-left: @SmallSpace; - - .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(); - - - .togglesWithMetadata { - margin-top: @MediumSpace; - - .toggles { - height: @ToggleHeight; - width: @ToggleWidth; - margin-left: @MediumSpace; - - &:focus { - .focus(); - } - - .tab { - margin-right: @MediumSpace; - } - - .toggleSwitch { - .toggleSwitch(); - } - - .selectedToggle { - .selectedToggle(); - } - - .unselectedToggle { - .unselectedToggle(); - } - } - } - - .result-metadata { - padding: @LargeSpace @SmallSpace @MediumSpace @MediumSpace; - - .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; - - .queryMetricsSummary { - margin: @LargeSpace @LargeSpace 0px @DefaultSpace; - table-layout: fixed; - display: block; - height: auto; - 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 24px @MediumSpace; - - #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; - } - } - } - } -} \ No newline at end of file diff --git a/src/Explorer/Tabs/QueryTab.test.ts b/src/Explorer/Tabs/QueryTab.test.ts deleted file mode 100644 index f112db091..000000000 --- a/src/Explorer/Tabs/QueryTab.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import * as ko from "knockout"; -import { DatabaseAccount } from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { updateUserContext } from "../../UserContext"; -import Explorer from "../Explorer"; -import QueryTab from "./QueryTab"; - -describe("Query Tab", () => { - function getNewQueryTabForContainer(container: Explorer): QueryTab { - const database = { - container: container, - id: ko.observable("test"), - isDatabaseShared: () => false, - } as ViewModels.Database; - const collection = { - container: container, - databaseId: "test", - id: ko.observable("test"), - } as ViewModels.Collection; - - return new QueryTab({ - tabKind: ViewModels.CollectionTabKind.Query, - collection: collection, - database: database, - title: "", - tabPath: "", - hashLocation: "", - }); - } - - describe("shouldSetSystemPartitionKeyContainerPartitionKeyValueUndefined", () => { - const collection = { - id: ko.observable("withoutsystempk"), - partitionKey: { - systemKey: true, - }, - } as ViewModels.Collection; - - it("no container with system pk, should not set partition key option", () => { - const iteratorOptions = QueryTab.getIteratorOptions(collection); - expect(iteratorOptions.initialHeaders).toBeUndefined(); - }); - }); - - describe("isQueryMetricsEnabled()", () => { - let explorer: Explorer; - - beforeEach(() => { - explorer = new Explorer(); - }); - - it("should be true for accounts using SQL API", () => { - updateUserContext({}); - const queryTab = getNewQueryTabForContainer(explorer); - expect(queryTab.isQueryMetricsEnabled()).toBe(true); - }); - - it("should be false for accounts using other APIs", () => { - updateUserContext({ - databaseAccount: { - properties: { - capabilities: [{ name: "EnableGremlin" }], - }, - } as DatabaseAccount, - }); - const queryTab = getNewQueryTabForContainer(explorer); - expect(queryTab.isQueryMetricsEnabled()).toBe(false); - }); - }); - - describe("Save Queries command button", () => { - let explorer: Explorer; - - beforeEach(() => { - explorer = new Explorer(); - }); - - it("should be visible when using a supported API", () => { - updateUserContext({}); - const queryTab = getNewQueryTabForContainer(explorer); - expect(queryTab.saveQueryButton.visible()).toBe(true); - }); - - it("should not be visible when using an unsupported API", () => { - updateUserContext({ - databaseAccount: { - properties: { - capabilities: [{ name: "EnableMongo" }], - }, - } as DatabaseAccount, - }); - const queryTab = getNewQueryTabForContainer(explorer); - expect(queryTab.saveQueryButton.visible()).toBe(false); - }); - }); -}); diff --git a/src/Explorer/Tabs/QueryTab.ts b/src/Explorer/Tabs/QueryTab.ts deleted file mode 100644 index 50c258e8c..000000000 --- a/src/Explorer/Tabs/QueryTab.ts +++ /dev/null @@ -1,594 +0,0 @@ -import * as ko from "knockout"; -import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg"; -import SaveQueryIcon from "../../../images/save-cosmos.svg"; -import * as Constants from "../../Common/Constants"; -import { queryDocuments } from "../../Common/dataAccess/queryDocuments"; -import { queryDocumentsPage } from "../../Common/dataAccess/queryDocumentsPage"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; -import * as HeadersUtility from "../../Common/HeadersUtility"; -import { MinimalQueryIterator } from "../../Common/IteratorUtilities"; -import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter"; -import * as DataModels from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { useNotificationConsole } from "../../hooks/useNotificationConsole"; -import { Action } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import { userContext } from "../../UserContext"; -import * as QueryUtils from "../../Utils/QueryUtils"; -import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; -import template from "./QueryTab.html"; -import TabsBase from "./TabsBase"; - -enum ToggleState { - Result, - QueryMetrics, -} - -export default class QueryTab extends TabsBase implements ViewModels.WaitsForTemplate { - public readonly html = template; - public queryEditorId: string; - public executeQueryButton: ViewModels.Button; - public fetchNextPageButton: ViewModels.Button; - public saveQueryButton: ViewModels.Button; - public initialEditorContent: ko.Observable; - public maybeSubQuery: ko.Computed; - public sqlQueryEditorContent: ko.Observable; - public selectedContent: ko.Observable; - public sqlStatementToExecute: ko.Observable; - public queryResults: ko.Observable; - public error: ko.Observable; - public statusMessge: ko.Observable; - public statusIcon: ko.Observable; - public allResultsMetadata: ko.ObservableArray; - public showingDocumentsDisplayText: ko.Observable; - public requestChargeDisplayText: ko.Observable; - public isTemplateReady: ko.Observable; - public splitterId: string; - public splitter: Splitter; - public isPreferredApiMongoDB: boolean; - - public queryMetrics: ko.Observable>; - public aggregatedQueryMetrics: ko.Observable; - public activityId: ko.Observable; - public roundTrips: ko.Observable; - public toggleState: ko.Observable; - public isQueryMetricsEnabled: ko.Computed; - - protected monacoSettings: ViewModels.MonacoEditorSettings; - private _executeQueryButtonTitle: ko.Observable; - protected _iterator: MinimalQueryIterator; - private _isSaveQueriesEnabled: ko.Computed; - private _resourceTokenPartitionKey: string; - - _partitionKey: DataModels.PartitionKey; - - constructor(options: ViewModels.QueryTabOptions) { - super(options); - this.queryEditorId = `queryeditor${this.tabId}`; - this.showingDocumentsDisplayText = ko.observable(); - this.requestChargeDisplayText = ko.observable(); - const defaultQueryText = options.queryText != void 0 ? options.queryText : "SELECT * FROM c"; - this.initialEditorContent = ko.observable(defaultQueryText); - this.sqlQueryEditorContent = ko.observable(defaultQueryText); - this._executeQueryButtonTitle = ko.observable("Execute Query"); - this.selectedContent = ko.observable(); - this.selectedContent.subscribe((selectedContent: string) => { - if (!selectedContent.trim()) { - this._executeQueryButtonTitle("Execute Query"); - } else { - this._executeQueryButtonTitle("Execute Selection"); - } - }); - this.sqlStatementToExecute = ko.observable(""); - this.queryResults = ko.observable(""); - this.statusMessge = ko.observable(); - this.statusIcon = ko.observable(); - this.allResultsMetadata = ko.observableArray([]); - this.error = ko.observable(); - this._partitionKey = options.partitionKey; - this._resourceTokenPartitionKey = options.resourceTokenPartitionKey; - this.splitterId = this.tabId + "_splitter"; - this.isPreferredApiMongoDB = false; - this.aggregatedQueryMetrics = ko.observable(); - this._resetAggregateQueryMetrics(); - this.queryMetrics = ko.observable>(new Map()); - this.queryMetrics.subscribe((metrics) => this.aggregatedQueryMetrics(this._aggregateQueryMetrics(metrics))); - this.isQueryMetricsEnabled = ko.computed(() => { - return userContext.apiType === "SQL" || false; - }); - this.activityId = ko.observable(); - this.roundTrips = ko.observable(); - this.toggleState = ko.observable(ToggleState.Result); - - this.monacoSettings = new ViewModels.MonacoEditorSettings("sql", false); - - this.executeQueryButton = { - enabled: ko.computed(() => { - return !!this.sqlQueryEditorContent() && this.sqlQueryEditorContent().length > 0; - }), - - visible: ko.computed(() => { - return true; - }), - }; - - this._isSaveQueriesEnabled = ko.computed(() => { - const container = this.collection && this.collection.container; - return userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; - }); - - this.maybeSubQuery = ko.computed(function () { - const sql = this.sqlQueryEditorContent(); - return sql && /.*\(.*SELECT.*\)/i.test(sql); - }, this); - - this.saveQueryButton = { - enabled: this._isSaveQueriesEnabled, - visible: this._isSaveQueriesEnabled, - }; - - super.onTemplateReady((isTemplateReady: boolean) => { - if (isTemplateReady) { - const splitterBounds: SplitterBounds = { - min: Constants.Queries.QueryEditorMinHeightRatio * window.innerHeight, - max: $("#" + this.tabId).height() - Constants.Queries.QueryEditorMaxHeightRatio * window.innerHeight, - }; - this.splitter = new Splitter({ - splitterId: this.splitterId, - leftId: this.queryEditorId, - bounds: splitterBounds, - direction: SplitterDirection.Horizontal, - }); - } - }); - - this.fetchNextPageButton = { - enabled: ko.computed(() => { - const allResultsMetadata = this.allResultsMetadata() || []; - const numberOfResultsMetadata = allResultsMetadata.length; - - if (numberOfResultsMetadata === 0) { - return false; - } - - if (allResultsMetadata[numberOfResultsMetadata - 1].hasMoreResults) { - return true; - } - - return false; - }), - - visible: ko.computed(() => { - return true; - }), - }; - - this._buildCommandBarOptions(); - } - - public onTabClick(): void { - super.onTabClick(); - this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query); - } - - public onExecuteQueryClick = async (): Promise => { - const sqlStatement: string = this.selectedContent() || this.sqlQueryEditorContent(); - this.sqlStatementToExecute(sqlStatement); - this.allResultsMetadata([]); - this.queryResults(""); - this._iterator = undefined; - - await this._executeQueryDocumentsPage(0); - }; - - public onSaveQueryClick = (): void => { - this.collection && this.collection.container && this.collection.container.openSaveQueryPanel(); - }; - - public onSavedQueriesClick = (): void => { - this.collection && this.collection.container && this.collection.container.openBrowseQueriesPanel(); - }; - - public async onFetchNextPageClick(): Promise { - const allResultsMetadata = (this.allResultsMetadata && this.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); - } - - public onErrorDetailsClick = (src: any, event: MouseEvent): boolean => { - useNotificationConsole.getState().expandConsole(); - - return false; - }; - - public onErrorDetailsKeyPress = (src: any, event: KeyboardEvent): boolean => { - if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) { - this.onErrorDetailsClick(src, null); - return false; - } - - return true; - }; - - public toggleResult(): void { - this.toggleState(ToggleState.Result); - this.queryResults.valueHasMutated(); // needed to refresh the json-editor component - } - - public toggleMetrics(): void { - this.toggleState(ToggleState.QueryMetrics); - } - - public onToggleKeyDown = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === Constants.KeyCodes.LeftArrow) { - this.toggleResult(); - event.stopPropagation(); - return false; - } else if (event.keyCode === Constants.KeyCodes.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.toggleState() === ToggleState.Result; - } - - public isMetricsToggled(): boolean { - return this.toggleState() === ToggleState.QueryMetrics; - } - - public onDownloadQueryMetricsCsvClick = (source: any, event: MouseEvent): boolean => { - this._downloadQueryMetricsCsvData(); - return false; - }; - - public onDownloadQueryMetricsCsvKeyPress = (source: any, event: KeyboardEvent): boolean => { - if (event.keyCode === Constants.KeyCodes.Space || Constants.KeyCodes.Enter) { - this._downloadQueryMetricsCsvData(); - return false; - } - - return true; - }; - - private async _executeQueryDocumentsPage(firstItemIndex: number): Promise { - this.error(""); - this.roundTrips(undefined); - if (this._iterator === undefined) { - this._initIterator(); - } - - await this._queryDocumentsPage(firstItemIndex); - } - - // TODO: Position and enable spinner when request is in progress - private async _queryDocumentsPage(firstItemIndex: number): Promise { - this.isExecutionError(false); - this._resetAggregateQueryMetrics(); - const startKey: number = TelemetryProcessor.traceStart(Action.ExecuteQuery, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }); - let options: any = {}; - options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); - - const queryDocuments = async (firstItemIndex: number) => - await queryDocumentsPage(this.collection && this.collection.id(), this._iterator, firstItemIndex); - this.isExecuting(true); - - try { - const queryResults: ViewModels.QueryResults = await QueryUtils.queryPagesUntilContentPresent( - firstItemIndex, - queryDocuments - ); - const allResultsMetadata = (this.allResultsMetadata && this.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.allResultsMetadata.push(resultsMetadata); - this.activityId(queryResults.activityId); - this.roundTrips(queryResults.roundTrips); - - this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]); - - if (queryResults.itemCount == 0 && metadata != null && 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; - } - - const documents: any[] = queryResults.documents; - const results = this.renderObjectForEditor(documents, null, 4); - - const resultsDisplay: string = - queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`; - this.showingDocumentsDisplayText(resultsDisplay); - this.requestChargeDisplayText(`${queryResults.requestCharge} RUs`); - this.queryResults(results); - - TelemetryProcessor.traceSuccess( - Action.ExecuteQuery, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey - ); - } catch (error) { - this.isExecutionError(true); - const errorMessage = getErrorMessage(error); - this.error(errorMessage); - TelemetryProcessor.traceFailure( - Action.ExecuteQuery, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: errorMessage, - errorStack: getErrorStack(error), - }, - startKey - ); - document.getElementById("error-display").focus(); - } finally { - this.isExecuting(false); - this.togglesOnFocus(); - } - } - - private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void { - if (!metricsMap) { - return; - } - - Object.keys(metricsMap).forEach((key: string) => { - this.queryMetrics().set(key, metricsMap[key]); - }); - this.queryMetrics.valueHasMutated(); - } - - private _aggregateQueryMetrics(metricsMap: Map): DataModels.QueryMetrics { - if (!metricsMap) { - return null; - } - - const aggregatedMetrics: DataModels.QueryMetrics = this.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: any = QueryTab.getIteratorOptions(this.collection); - if (this._resourceTokenPartitionKey) { - options.partitionKey = this._resourceTokenPartitionKey; - } - - this._iterator = queryDocuments( - this.collection.databaseId, - this.collection.id(), - this.sqlStatementToExecute(), - options - ); - } - - public static getIteratorOptions(container: ViewModels.CollectionBase): any { - let options: any = {}; - options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey(); - return options; - } - - private _normalize(value: number): number { - if (!value) { - return 0; - } - - return value; - } - - private _resetAggregateQueryMetrics(): void { - this.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.queryMetrics()) { - return null; - } - - const queryMetrics = this.queryMetrics(); - let csvData: string = ""; - 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._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 { - ko.computed(() => - ko.toJSON([this.executeQueryButton.visible, this.executeQueryButton.enabled, this._executeQueryButtonTitle]) - ).subscribe(() => this.updateNavbarWithTabsButtons()); - this.updateNavbarWithTabsButtons(); - } -} diff --git a/src/Explorer/Tabs/QueryTab/QueryTab.tsx b/src/Explorer/Tabs/QueryTab/QueryTab.tsx index 33786ac33..6a847c92f 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTab.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTab.tsx @@ -31,6 +31,7 @@ export class NewQueryTab extends TabsBase { onTabAccessor: (instance: ITabAccessor): void => { this.iTabAccessor = instance; }, + isPreferredApiMongoDB: false, }; } @@ -45,7 +46,9 @@ export class NewQueryTab extends TabsBase { public onCloseTabButtonClick(): void { this.manager?.closeTab(this); - this.iTabAccessor.onCloseClickEvent(true); + if (this.iTabAccessor) { + this.iTabAccessor.onCloseClickEvent(true); + } } public getContainer(): Explorer { diff --git a/src/Explorer/Tabs/TabsManager.test.ts b/src/Explorer/Tabs/TabsManager.test.ts index 7f4509a2f..eb1e3e4b4 100644 --- a/src/Explorer/Tabs/TabsManager.test.ts +++ b/src/Explorer/Tabs/TabsManager.test.ts @@ -3,8 +3,9 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { updateUserContext } from "../../UserContext"; import Explorer from "../Explorer"; import DocumentId from "../Tree/DocumentId"; +import { container } from "./../Controls/Settings/TestUtils"; import DocumentsTab from "./DocumentsTab"; -import QueryTab from "./QueryTab"; +import { NewQueryTab } from "./QueryTab/QueryTab"; import { TabsManager } from "./TabsManager"; describe("Tabs manager tests", () => { @@ -12,10 +13,10 @@ describe("Tabs manager tests", () => { let explorer: Explorer; let database: ViewModels.Database; let collection: ViewModels.Collection; - let queryTab: QueryTab; + let queryTab: NewQueryTab; let documentsTab: DocumentsTab; - beforeAll(() => { + beforeEach(() => { explorer = new Explorer(); updateUserContext({ databaseAccount: { @@ -45,14 +46,22 @@ describe("Tabs manager tests", () => { collection.isCollectionExpanded = ko.observable(true); collection.selectedSubnodeKind = ko.observable(); - queryTab = new QueryTab({ - tabKind: ViewModels.CollectionTabKind.Query, - collection, - database, - title: "", - tabPath: "", - hashLocation: "", - }); + queryTab = new NewQueryTab( + { + tabKind: ViewModels.CollectionTabKind.Query, + collection, + database, + title: "", + tabPath: "", + hashLocation: "", + queryText: "", + partitionKey: collection.partitionKey, + onLoadStartKey: 1, + }, + { + container: container, + } + ); documentsTab = new DocumentsTab({ partitionKey: undefined, diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 98141315d..80ce3f579 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -28,7 +28,7 @@ import ConflictsTab from "../Tabs/ConflictsTab"; import DocumentsTab from "../Tabs/DocumentsTab"; import GraphTab from "../Tabs/GraphTab"; import MongoDocumentsTab from "../Tabs/MongoDocumentsTab"; -import MongoQueryTab from "../Tabs/MongoQueryTab"; +import { NewMongoQueryTab } from "../Tabs/MongoQueryTab/MongoQueryTab"; import MongoShellTab from "../Tabs/MongoShellTab"; import { NewQueryTab } from "../Tabs/QueryTab/QueryTab"; import QueryTablesTab from "../Tabs/QueryTablesTab"; @@ -648,18 +648,24 @@ export default class Collection implements ViewModels.Collection { tabTitle: title, }); - const mongoQueryTab: MongoQueryTab = new MongoQueryTab({ - tabKind: ViewModels.CollectionTabKind.Query, - title: title, - tabPath: "", - collection: this, - node: this, - hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoQuery`, - partitionKey: collection.partitionKey, - onLoadStartKey: startKey, - }); + const newMongoQueryTab: NewMongoQueryTab = new NewMongoQueryTab( + { + tabKind: ViewModels.CollectionTabKind.Query, + title: title, + tabPath: "", + collection: this, + node: this, + hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoQuery`, + partitionKey: collection.partitionKey, + onLoadStartKey: startKey, + }, + { + container: this.container, + viewModelcollection: this, + } + ); - this.container.tabsManager.activateNewTab(mongoQueryTab); + this.container.tabsManager.activateNewTab(newMongoQueryTab); } public onNewGraphClick() { diff --git a/src/Explorer/Tree/ResourceTokenCollection.ts b/src/Explorer/Tree/ResourceTokenCollection.ts index 45155fe0e..700c6a2b1 100644 --- a/src/Explorer/Tree/ResourceTokenCollection.ts +++ b/src/Explorer/Tree/ResourceTokenCollection.ts @@ -7,7 +7,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import Explorer from "../Explorer"; import DocumentsTab from "../Tabs/DocumentsTab"; -import QueryTab from "../Tabs/QueryTab"; +import { NewQueryTab } from "../Tabs/QueryTab/QueryTab"; import TabsBase from "../Tabs/TabsBase"; import DocumentId from "./DocumentId"; @@ -85,20 +85,22 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas 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, - resourceTokenPartitionKey: userContext.parsedResourceToken.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 onDocumentDBDocumentsClick() { diff --git a/src/Main.tsx b/src/Main.tsx index 470a5eabd..566317825 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -45,7 +45,6 @@ import "./Explorer/Panes/PanelComponent.less"; import { SidePanel } from "./Explorer/Panes/PanelContainerComponent"; import { SplashScreen } from "./Explorer/SplashScreen/SplashScreen"; import "./Explorer/SplashScreen/SplashScreen.less"; -import "./Explorer/Tabs/QueryTab.less"; import { Tabs } from "./Explorer/Tabs/Tabs"; import { useConfig } from "./hooks/useConfig"; import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";