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 01/31] 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) { 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 02/31] 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"; From 3bc58a80e4eb3fa2b5588addbf03f3e052c45639 Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Mon, 14 Jun 2021 12:46:14 -0700 Subject: [PATCH 03/31] Remove Explorer.isHostedDataExplorerEnabled (#890) --- .../__snapshots__/SettingsComponent.test.tsx.snap | 2 -- src/Explorer/Explorer.tsx | 11 +---------- .../CommandBar/CommandBarComponentButtonFactory.tsx | 7 +++++-- .../__snapshots__/GitHubReposPanel.test.tsx.snap | 1 - .../__snapshots__/StringInputPane.test.tsx.snap | 1 - .../DeleteDatabaseConfirmationPanel.test.tsx.snap | 1 - 6 files changed, 6 insertions(+), 17 deletions(-) diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 8e49f2ccf..99f63dd81 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -34,7 +34,6 @@ exports[`SettingsComponent renders 1`] = ` "databases": [Function], "isAccountReady": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], - "isHostedDataExplorerEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], "isResourceTokenCollectionNodeSelected": [Function], @@ -129,7 +128,6 @@ exports[`SettingsComponent renders 1`] = ` "databases": [Function], "isAccountReady": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], - "isHostedDataExplorerEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], "isResourceTokenCollectionNodeSelected": [Function], diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 73c37510a..06c73429f 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -12,7 +12,7 @@ import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility" import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils"; import * as Logger from "../Common/Logger"; import { QueriesClient } from "../Common/QueriesClient"; -import { configContext, Platform } from "../ConfigContext"; +import { configContext } from "../ConfigContext"; import * as DataModels from "../Contracts/DataModels"; import * as ViewModels from "../Contracts/ViewModels"; import { GitHubOAuthService } from "../GitHub/GitHubOAuthService"; @@ -34,7 +34,6 @@ import { import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; import { stringToBlob } from "../Utils/BlobUtils"; import { isCapabilityEnabled } from "../Utils/CapabilityUtils"; -import { isRunningOnNationalCloud } from "../Utils/CloudUtils"; import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; @@ -109,9 +108,6 @@ export default class Explorer { public tabsManager: TabsManager; public gitHubOAuthService: GitHubOAuthService; - - // features - public isHostedDataExplorerEnabled: ko.Computed; public isSchemaEnabled: ko.Computed; // Notebooks @@ -222,11 +218,6 @@ export default class Explorer { return isCapabilityEnabled("EnableMongo"); }); - - this.isHostedDataExplorerEnabled = ko.computed( - () => - configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin" - ); this.selectedDatabaseId = ko.computed(() => { const selectedNode = this.selectedNode(); if (!selectedNode) { diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 6ff55ef28..36b1513af 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -166,7 +166,10 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt }, ]; - if (container.isHostedDataExplorerEnabled()) { + const showOpenFullScreen = + configContext.platform === Platform.Portal && !isRunningOnNationalCloud() && userContext.apiType !== "Gremlin"; + + if (showOpenFullScreen) { const label = "Open Full Screen"; const fullScreenButton: CommandButtonComponentProps = { iconSrc: OpenInTabIcon, @@ -178,7 +181,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt ariaLabel: label, tooltipText: label, hasPopup: false, - disabled: !container.isHostedDataExplorerEnabled(), + disabled: !showOpenFullScreen, className: "OpenFullScreen", }; buttons.push(fullScreenButton); diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index 4395d874b..047d8ae26 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -23,7 +23,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "databases": [Function], "isAccountReady": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], - "isHostedDataExplorerEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], "isResourceTokenCollectionNodeSelected": [Function], diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index b892777ee..b9f8d3bc3 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -13,7 +13,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "databases": [Function], "isAccountReady": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], - "isHostedDataExplorerEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], "isResourceTokenCollectionNodeSelected": [Function], diff --git a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap index d7b8bedaf..13088bc82 100644 --- a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap @@ -11,7 +11,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database "databases": [Function], "isAccountReady": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], - "isHostedDataExplorerEnabled": [Function], "isLastCollection": [Function], "isLastNonEmptyDatabase": [Function], "isNotebookEnabled": [Function], From 615bfeaf4895a62e005f402afb9b6f3dda092361 Mon Sep 17 00:00:00 2001 From: Hardikkumar Nai <80053762+hardiknai-techm@users.noreply.github.com> Date: Tue, 15 Jun 2021 03:04:37 +0530 Subject: [PATCH 04/31] Remove Explorer openEditTableEntityPanel and openAddTableEntityPanel (#887) Co-authored-by: Steve Faulkner --- .eslintignore | 3 - src/Explorer/Explorer.tsx | 31 --------- .../{QueryTablesTab.ts => QueryTablesTab.tsx} | 63 ++++++++++--------- 3 files changed, 34 insertions(+), 63 deletions(-) rename src/Explorer/Tabs/{QueryTablesTab.ts => QueryTablesTab.tsx} (85%) diff --git a/.eslintignore b/.eslintignore index 09b531837..7db848c1e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -146,9 +146,6 @@ src/Explorer/Tabs/MongoDocumentsTab.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/QueryTablesTab.ts src/Explorer/Tabs/ScriptTabBase.ts src/Explorer/Tabs/StoredProcedureTab.ts src/Explorer/Tabs/TabComponents.ts diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 06c73429f..4231371b5 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -58,16 +58,12 @@ import { GitHubReposPanel } from "./Panes/GitHubReposPanel/GitHubReposPanel"; import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane"; import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel"; import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; -import { AddTableEntityPanel } from "./Panes/Tables/AddTableEntityPanel"; -import { EditTableEntityPanel } from "./Panes/Tables/EditTableEntityPanel"; import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel/TableQuerySelectPanel"; import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; -import TableListViewModal from "./Tables/DataTable/TableEntityListViewModel"; import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; -import QueryTablesTab from "./Tabs/QueryTablesTab"; import { TabsManager } from "./Tabs/TabsManager"; import TerminalTab from "./Tabs/TerminalTab"; import Database from "./Tree/Database"; @@ -1512,39 +1508,12 @@ export default class Explorer { ); } - public openAddTableEntityPanel(queryTablesTab: QueryTablesTab, tableEntityListViewModel: TableListViewModal): void { - useSidePanel - .getState() - .openSidePanel( - "Add Table Entity", - - ); - } public openSetupNotebooksPanel(title: string, description: string): void { useSidePanel .getState() .openSidePanel(title, ); } - public openEditTableEntityPanel(queryTablesTab: QueryTablesTab, tableEntityListViewModel: TableListViewModal): void { - useSidePanel - .getState() - .openSidePanel( - "Edit Table Entity", - - ); - } - public openTableSelectQueryPanel(queryViewModal: QueryViewModel): void { useSidePanel.getState().openSidePanel("Select Column", ); } diff --git a/src/Explorer/Tabs/QueryTablesTab.ts b/src/Explorer/Tabs/QueryTablesTab.tsx similarity index 85% rename from src/Explorer/Tabs/QueryTablesTab.ts rename to src/Explorer/Tabs/QueryTablesTab.tsx index 59bd01cc7..b48a6a10e 100644 --- a/src/Explorer/Tabs/QueryTablesTab.ts +++ b/src/Explorer/Tabs/QueryTablesTab.tsx @@ -1,5 +1,5 @@ import * as ko from "knockout"; -import Q from "q"; +import React from "react"; import AddEntityIcon from "../../../images/AddEntity.svg"; import DeleteEntitiesIcon from "../../../images/DeleteEntities.svg"; import EditEntityIcon from "../../../images/Edit-entity.svg"; @@ -7,13 +7,16 @@ import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg"; import QueryBuilderIcon from "../../../images/Query-Builder.svg"; import QueryTextIcon from "../../../images/Query-Text.svg"; import * as ViewModels from "../../Contracts/ViewModels"; +import { useSidePanel } from "../../hooks/useSidePanel"; import { userContext } from "../../UserContext"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import Explorer from "../Explorer"; +import { AddTableEntityPanel } from "../Panes/Tables/AddTableEntityPanel"; +import { EditTableEntityPanel } from "../Panes/Tables/EditTableEntityPanel"; import TableCommands from "../Tables/DataTable/TableCommands"; import TableEntityListViewModel from "../Tables/DataTable/TableEntityListViewModel"; import QueryViewModel from "../Tables/QueryBuilder/QueryViewModel"; -import { TableDataClient } from "../Tables/TableDataClient"; +import { CassandraAPIDataClient, TableDataClient } from "../Tables/TableDataClient"; import template from "./QueryTablesTab.html"; import TabsBase from "./TabsBase"; @@ -130,34 +133,36 @@ export default class QueryTablesTab extends TabsBase { this.buildCommandBarOptions(); } - public onExecuteQueryClick = (): Q.Promise => { - this.queryViewModel().runQuery(); - return null; + public onAddEntityClick = (): void => { + useSidePanel + .getState() + .openSidePanel( + "Add Table Entity", + + ); }; - public onQueryBuilderClick = (): Q.Promise => { - this.queryViewModel().selectHelper(); - return null; + public onEditEntityClick = (): void => { + useSidePanel + .getState() + .openSidePanel( + "Edit Table Entity", + + ); }; - public onQueryTextClick = (): Q.Promise => { - this.queryViewModel().selectEditor(); - return null; - }; - - public onAddEntityClick = (): Q.Promise => { - this.container.openAddTableEntityPanel(this, this.tableEntityListViewModel()); - return null; - }; - - public onEditEntityClick = (): Q.Promise => { - this.container.openEditTableEntityPanel(this, this.tableEntityListViewModel()); - return null; - }; - - public onDeleteEntityClick = (): Q.Promise => { + public onDeleteEntityClick = (): void => { this.tableCommands.deleteEntitiesCommand(this.tableEntityListViewModel()); - return null; }; public onActivate(): void { @@ -166,7 +171,7 @@ export default class QueryTablesTab extends TabsBase { !!this.tableEntityListViewModel() && !!this.tableEntityListViewModel().table && this.tableEntityListViewModel().table.columns; - if (!!columns) { + if (columns) { columns.adjust(); $(window).resize(); } @@ -179,7 +184,7 @@ export default class QueryTablesTab extends TabsBase { buttons.push({ iconSrc: QueryBuilderIcon, iconAlt: label, - onCommandClick: this.onQueryBuilderClick, + onCommandClick: () => this.queryViewModel().selectHelper(), commandButtonLabel: label, ariaLabel: label, hasPopup: false, @@ -193,7 +198,7 @@ export default class QueryTablesTab extends TabsBase { buttons.push({ iconSrc: QueryTextIcon, iconAlt: label, - onCommandClick: this.onQueryTextClick, + onCommandClick: () => this.queryViewModel().selectEditor(), commandButtonLabel: label, ariaLabel: label, hasPopup: false, @@ -207,7 +212,7 @@ export default class QueryTablesTab extends TabsBase { buttons.push({ iconSrc: ExecuteQueryIcon, iconAlt: label, - onCommandClick: this.onExecuteQueryClick, + onCommandClick: () => this.queryViewModel().runQuery(), commandButtonLabel: label, ariaLabel: label, hasPopup: false, From 0c6324a4c1823a10d6f3a7ee29baf78a6c3ec7b9 Mon Sep 17 00:00:00 2001 From: Hardikkumar Nai <80053762+hardiknai-techm@users.noreply.github.com> Date: Tue, 15 Jun 2021 03:27:47 +0530 Subject: [PATCH 05/31] Remove Explorer.openTableSelectQueryPanel (#881) Co-authored-by: Steve Faulkner --- .eslintignore | 1 - src/Explorer/Explorer.tsx | 6 -- .../QueryBuilder/QueryBuilderViewModel.ts | 4 +- .../QueryBuilder/QueryClauseViewModel.ts | 12 ++-- .../{QueryViewModel.ts => QueryViewModel.tsx} | 59 +++++++++---------- 5 files changed, 37 insertions(+), 45 deletions(-) rename src/Explorer/Tables/QueryBuilder/{QueryViewModel.ts => QueryViewModel.tsx} (81%) diff --git a/.eslintignore b/.eslintignore index 7db848c1e..6ba78c7f9 100644 --- a/.eslintignore +++ b/.eslintignore @@ -133,7 +133,6 @@ src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts -src/Explorer/Tables/QueryBuilder/QueryViewModel.ts src/Explorer/Tables/TableDataClient.ts src/Explorer/Tables/TableEntityProcessor.ts src/Explorer/Tables/Utilities.ts diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 4231371b5..108f5df35 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -58,10 +58,8 @@ import { GitHubReposPanel } from "./Panes/GitHubReposPanel/GitHubReposPanel"; import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane"; import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel"; import { StringInputPane } from "./Panes/StringInputPane/StringInputPane"; -import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel/TableQuerySelectPanel"; import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; -import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import { TabsManager } from "./Tabs/TabsManager"; @@ -1513,8 +1511,4 @@ export default class Explorer { .getState() .openSidePanel(title, ); } - - public openTableSelectQueryPanel(queryViewModal: QueryViewModel): void { - useSidePanel.getState().openSidePanel("Select Column", ); - } } diff --git a/src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts b/src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts index 59988c26e..58fdc654a 100644 --- a/src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts +++ b/src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts @@ -792,7 +792,7 @@ export default class QueryBuilderViewModel { return null; } - public checkIfClauseChanged(clause: QueryClauseViewModel): void { - this._queryViewModel.checkIfBuilderChanged(clause); + public checkIfClauseChanged(): void { + this._queryViewModel.checkIfBuilderChanged(); } } diff --git a/src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts b/src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts index 2137f8405..8acd74518 100644 --- a/src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts +++ b/src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts @@ -89,7 +89,7 @@ export default class QueryClauseViewModel { ); this.and_or.subscribe((value) => { - this._queryBuilderViewModel.checkIfClauseChanged(this); + this._queryBuilderViewModel.checkIfClauseChanged(); }); this.field.subscribe((value) => { this.changeField(); @@ -103,13 +103,13 @@ export default class QueryClauseViewModel { // } }); this.customTimeValue.subscribe((value) => { - this._queryBuilderViewModel.checkIfClauseChanged(this); + this._queryBuilderViewModel.checkIfClauseChanged(); }); this.value.subscribe((value) => { - this._queryBuilderViewModel.checkIfClauseChanged(this); + this._queryBuilderViewModel.checkIfClauseChanged(); }); this.operator.subscribe((value) => { - this._queryBuilderViewModel.checkIfClauseChanged(this); + this._queryBuilderViewModel.checkIfClauseChanged(); }); this._groupCheckSubscription = this.checkedForGrouping.subscribe((value) => { this._queryBuilderViewModel.updateCanGroupClauses(); @@ -184,7 +184,7 @@ export default class QueryClauseViewModel { this.type(QueryBuilderConstants.TableType.String); } } - this._queryBuilderViewModel.checkIfClauseChanged(this); + this._queryBuilderViewModel.checkIfClauseChanged(); } private resetFromTimestamp(): void { @@ -216,7 +216,7 @@ export default class QueryClauseViewModel { this.timeValue(""); this.customTimeValue(""); } - this._queryBuilderViewModel.checkIfClauseChanged(this); + this._queryBuilderViewModel.checkIfClauseChanged(); } // private customTimestampDialog(): Promise { diff --git a/src/Explorer/Tables/QueryBuilder/QueryViewModel.ts b/src/Explorer/Tables/QueryBuilder/QueryViewModel.tsx similarity index 81% rename from src/Explorer/Tables/QueryBuilder/QueryViewModel.ts rename to src/Explorer/Tables/QueryBuilder/QueryViewModel.tsx index 69ba3ae47..f816cad60 100644 --- a/src/Explorer/Tables/QueryBuilder/QueryViewModel.ts +++ b/src/Explorer/Tables/QueryBuilder/QueryViewModel.tsx @@ -1,16 +1,18 @@ import * as ko from "knockout"; +import React from "react"; import * as _ from "underscore"; import { KeyCodes } from "../../../Common/Constants"; +import { useSidePanel } from "../../../hooks/useSidePanel"; import { userContext } from "../../../UserContext"; +import { TableQuerySelectPanel } from "../../Panes/Tables/TableQuerySelectPanel/TableQuerySelectPanel"; import QueryTablesTab from "../../Tabs/QueryTablesTab"; import { getQuotedCqlIdentifier } from "../CqlUtilities"; import * as DataTableUtilities from "../DataTable/DataTableUtilities"; import TableEntityListViewModel from "../DataTable/TableEntityListViewModel"; import QueryBuilderViewModel from "./QueryBuilderViewModel"; -import QueryClauseViewModel from "./QueryClauseViewModel"; export default class QueryViewModel { - public topValueLimitMessage: string = "Please input a number between 0 and 1000."; + public readonly topValueLimitMessage: string = "Please input a number between 0 and 1000."; public queryBuilderViewModel = ko.observable(); public isHelperActive = ko.observable(true); public isEditorActive = ko.observable(false); @@ -49,7 +51,7 @@ export default class QueryViewModel { this.queryTextIsReadOnly = ko.computed(() => { return userContext.apiType !== "Cassandra"; }); - let initialOptions = this._tableEntityListViewModel.headers; + const initialOptions = this._tableEntityListViewModel.headers; this.columnOptions = ko.observableArray(initialOptions); this.focusTopResult = ko.observable(false); this.focusExpandIcon = ko.observable(false); @@ -63,12 +65,12 @@ export default class QueryViewModel { this.topValue() !== this.unchangedSaveTop() ); - this.queryBuilderViewModel().clauseArray.subscribe((value) => { + this.queryBuilderViewModel().clauseArray.subscribe(() => { this.setFilter(); }); this.isExceedingLimit = ko.computed(() => { - var currentTopValue: number = this.topValue(); + const currentTopValue: number = this.topValue(); return currentTopValue < 0 || currentTopValue > 1000; }); @@ -111,7 +113,7 @@ export default class QueryViewModel { DataTableUtilities.forceRecalculateTableSize(); // Fix for 261924, forces the resize event so DataTableBindingManager will redo the calculation on table size. }; - public ontoggleAdvancedOptionsKeyDown = (source: any, event: KeyboardEvent): boolean => { + public ontoggleAdvancedOptionsKeyDown = (source: string, event: KeyboardEvent): boolean => { if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { this.toggleAdvancedOptions(); event.stopPropagation(); @@ -125,31 +127,29 @@ export default class QueryViewModel { }; private setFilter = (): string => { - var queryString = this.isEditorActive() + const queryString = this.isEditorActive() ? this.queryText() : userContext.apiType === "Cassandra" ? this.queryBuilderViewModel().getCqlFilterFromClauses() : this.queryBuilderViewModel().getODataFilterFromClauses(); - var filter = queryString; + const filter = queryString; this.queryText(filter); return this.queryText(); }; private setSqlFilter = (): string => { - var filter = this.queryBuilderViewModel().getSqlFilterFromClauses(); - return filter; + return this.queryBuilderViewModel().getSqlFilterFromClauses(); }; private setCqlFilter = (): string => { - var filter = this.queryBuilderViewModel().getCqlFilterFromClauses(); - return filter; + return this.queryBuilderViewModel().getCqlFilterFromClauses(); }; public isHelperEnabled = ko .computed(() => { return ( this.queryText() === this.unchangedText() || - this.queryText() === null || + this.queryText() === undefined || this.queryText() === "" || this.isHelperActive() ); @@ -159,13 +159,13 @@ export default class QueryViewModel { }); public runQuery = (): DataTables.DataTable => { - var filter = this.setFilter(); + let filter = this.setFilter(); if (filter && userContext.apiType !== "Cassandra") { filter = filter.replace(/"/g, "'"); } - var top = this.topValue(); - var selectOptions = this._getSelectedResults(); - var select = selectOptions; + const top = this.topValue(); + const selectOptions = this._getSelectedResults(); + const select = selectOptions; this._tableEntityListViewModel.tableQuery.filter = filter; this._tableEntityListViewModel.tableQuery.top = top; this._tableEntityListViewModel.tableQuery.select = select; @@ -177,16 +177,16 @@ export default class QueryViewModel { }; public clearQuery = (): DataTables.DataTable => { - this.queryText(null); - this.topValue(null); - this.selectText(null); + this.queryText(); + this.topValue(); + this.selectText(); this.selectMessage(""); // clears the queryBuilder and adds a new blank clause this.queryBuilderViewModel().queryClauses.removeAll(); this.queryBuilderViewModel().addNewClause(); - this._tableEntityListViewModel.tableQuery.filter = null; - this._tableEntityListViewModel.tableQuery.top = null; - this._tableEntityListViewModel.tableQuery.select = null; + this._tableEntityListViewModel.tableQuery.filter = undefined; + this._tableEntityListViewModel.tableQuery.top = undefined; + this._tableEntityListViewModel.tableQuery.select = undefined; this._tableEntityListViewModel.oDataQuery(""); this._tableEntityListViewModel.sqlQuery("SELECT * FROM c"); this._tableEntityListViewModel.cqlQuery( @@ -197,12 +197,11 @@ export default class QueryViewModel { return this._tableEntityListViewModel.reloadTable(false); }; - public selectQueryOptions(): Promise { - this.queryTablesTab.container.openTableSelectQueryPanel(this); - return null; + public selectQueryOptions() { + useSidePanel.getState().openSidePanel("Select Column", ); } - public onselectQueryOptionsKeyDown = (source: any, event: KeyboardEvent): boolean => { + public onselectQueryOptionsKeyDown = (source: string, event: KeyboardEvent): boolean => { if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) { this.selectQueryOptions(); event.stopPropagation(); @@ -212,7 +211,7 @@ export default class QueryViewModel { }; public getSelectMessage(): void { - if (_.isEmpty(this.selectText()) || this.selectText() === null) { + if (_.isEmpty(this.selectText()) || this.selectText() === undefined) { this.selectMessage(""); } else { this.selectMessage(`${this.selectText().length} of ${this.columnOptions().length} columns selected.`); @@ -220,7 +219,7 @@ export default class QueryViewModel { } public isSelected = ko.computed(() => { - return !(_.isEmpty(this.selectText()) || this.selectText() === null); + return !(_.isEmpty(this.selectText()) || this.selectText() === undefined); }); private setCheckToSave(): void { @@ -230,7 +229,7 @@ export default class QueryViewModel { this.isSaveEnabled(false); } - public checkIfBuilderChanged(clause: QueryClauseViewModel): void { + public checkIfBuilderChanged(): void { this.setFilter(); } } From 239c7edf7b821c73b4df468142d1c853a6fb2615 Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Mon, 14 Jun 2021 20:52:02 -0500 Subject: [PATCH 06/31] Fix Incorrect Image Links (#892) --- src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx | 16 ++++++++-------- .../Hosted/Components/ConnectExplorer.tsx | 14 ++++++++------ 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index 5d661213b..327f25a38 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -2,7 +2,11 @@ import { DetailsList, DetailsListLayoutMode, IColumn, Pivot, PivotItem, Selectio import React, { Fragment } from "react"; import SplitterLayout from "react-splitter-layout"; import "react-splitter-layout/lib/index.css"; +import DownloadQueryMetrics from "../../../../images/DownloadQuery.svg"; import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg"; +import InfoColor from "../../../../images/info_color.svg"; +import QueryEditorNext from "../../../../images/Query-Editor-Next.svg"; +import RunQuery from "../../../../images/RunQuery.png"; import SaveQueryIcon from "../../../../images/save-cosmos.svg"; import * as Constants from "../../../Common/Constants"; import { NormalizedEventKey } from "../../../Common/Constants"; @@ -927,7 +931,7 @@ export default class QueryTabComponent extends React.Component
- Error + Error We have detected you may be using a subquery. Non-correlated subqueries are not currently @@ -956,7 +960,7 @@ export default class QueryTabComponent extends React.Component

- Execute Query Watermark + Execute Query Watermark

Execute a query to see the results

@@ -982,11 +986,7 @@ export default class QueryTabComponent extends React.Component Load more - Fetch next page + Fetch next page )} @@ -1043,7 +1043,7 @@ export default class QueryTabComponent extends React.Component download query metrics csv Per-partition query metrics (CSV) diff --git a/src/Platform/Hosted/Components/ConnectExplorer.tsx b/src/Platform/Hosted/Components/ConnectExplorer.tsx index e7a4cbecc..3639bd113 100644 --- a/src/Platform/Hosted/Components/ConnectExplorer.tsx +++ b/src/Platform/Hosted/Components/ConnectExplorer.tsx @@ -1,9 +1,11 @@ -import * as React from "react"; import { useBoolean } from "@fluentui/react-hooks"; -import { HttpHeaders } from "../../../Common/Constants"; -import { GenerateTokenResponse } from "../../../Contracts/DataModels"; -import { configContext } from "../../../ConfigContext"; +import * as React from "react"; +import ErrorImage from "../../../../images/error.svg"; +import ConnectImage from "../../../../images/HdeConnectCosmosDB.svg"; import { AuthType } from "../../../AuthType"; +import { HttpHeaders } from "../../../Common/Constants"; +import { configContext } from "../../../ConfigContext"; +import { GenerateTokenResponse } from "../../../Contracts/DataModels"; import { isResourceTokenConnectionString } from "../Helpers/ResourceTokenUtils"; interface Props { @@ -28,7 +30,7 @@ export const ConnectExplorer: React.FunctionComponent = ({

- Azure Cosmos DB + Azure Cosmos DB

Welcome to Azure Cosmos DB

{isFormVisible ? ( @@ -68,7 +70,7 @@ export const ConnectExplorer: React.FunctionComponent = ({ }} /> - Error notification + Error notification

From af71a96d540be7864238f4e578f842fbc0062a6c Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Tue, 15 Jun 2021 14:52:21 -0500 Subject: [PATCH 07/31] Add tree and treeitem roles to Resource Tree (#895) * Add tree and treeitem roles to Resource Tree * Updates --- src/Explorer/Controls/TreeComponent/TreeComponent.tsx | 3 ++- .../TreeComponent/__snapshots__/TreeComponent.test.tsx.snap | 6 ++++++ .../Tree/__snapshots__/ResourceTreeAdapter.test.tsx.snap | 1 + .../ResourceTreeAdapterForResourceToken.test.tsx.snap | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Explorer/Controls/TreeComponent/TreeComponent.tsx b/src/Explorer/Controls/TreeComponent/TreeComponent.tsx index dd7127daa..6c5ceb817 100644 --- a/src/Explorer/Controls/TreeComponent/TreeComponent.tsx +++ b/src/Explorer/Controls/TreeComponent/TreeComponent.tsx @@ -58,7 +58,7 @@ export interface TreeComponentProps { export class TreeComponent extends React.Component { public render(): JSX.Element { return ( -
+
); @@ -172,6 +172,7 @@ export class TreeNodeComponent extends React.Component) => this.onNodeClick(event, node)} onKeyPress={(event: React.KeyboardEvent) => this.onNodeKeyPress(event, node)} + role="treeitem" >
Date: Wed, 16 Jun 2021 08:23:50 +0530 Subject: [PATCH 08/31] Migrate Trigger tab to React (#855) Co-authored-by: Steve Faulkner --- less/forms.less | 8 + src/Explorer/Tabs/TriggerTab.html | 39 --- src/Explorer/Tabs/TriggerTab.ts | 185 ------------- src/Explorer/Tabs/TriggerTab.tsx | 36 +++ src/Explorer/Tabs/TriggerTabContent.tsx | 343 ++++++++++++++++++++++++ 5 files changed, 387 insertions(+), 224 deletions(-) delete mode 100644 src/Explorer/Tabs/TriggerTab.html delete mode 100644 src/Explorer/Tabs/TriggerTab.ts create mode 100644 src/Explorer/Tabs/TriggerTab.tsx create mode 100644 src/Explorer/Tabs/TriggerTabContent.tsx diff --git a/less/forms.less b/less/forms.less index ba771a108..572134c26 100644 --- a/less/forms.less +++ b/less/forms.less @@ -200,4 +200,12 @@ .migration:disabled { background-color: #ccc; +} + +.trigger-field { + width: 40%; + margin-top: 10px +} +.trigger-form { + padding: 10px 30px 10px 30px; } \ No newline at end of file diff --git a/src/Explorer/Tabs/TriggerTab.html b/src/Explorer/Tabs/TriggerTab.html deleted file mode 100644 index 62aab2dad..000000000 --- a/src/Explorer/Tabs/TriggerTab.html +++ /dev/null @@ -1,39 +0,0 @@ -
- -
-
Trigger Id
- - - - -
Trigger Type
- - - -
Trigger Operation
- - - -
Trigger Body
-
-
- -
diff --git a/src/Explorer/Tabs/TriggerTab.ts b/src/Explorer/Tabs/TriggerTab.ts deleted file mode 100644 index c74fc67ab..000000000 --- a/src/Explorer/Tabs/TriggerTab.ts +++ /dev/null @@ -1,185 +0,0 @@ -import * as Constants from "../../Common/Constants"; -import { createTrigger } from "../../Common/dataAccess/createTrigger"; -import { updateTrigger } from "../../Common/dataAccess/updateTrigger"; -import editable from "../../Common/EditableUtility"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { Action } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types"; -import Trigger from "../Tree/Trigger"; -import ScriptTabBase from "./ScriptTabBase"; -import template from "./TriggerTab.html"; - -export default class TriggerTab extends ScriptTabBase { - public readonly html = template; - public collection: ViewModels.Collection; - public node: Trigger; - public triggerType: ViewModels.Editable; - public triggerOperation: ViewModels.Editable; - - constructor(options: ViewModels.ScriptTabOption) { - super(options); - super.onActivate.bind(this); - this.ariaLabel("Trigger Body"); - this.triggerType = editable.observable(options.resource.triggerType); - this.triggerOperation = editable.observable(options.resource.triggerOperation); - - this.formFields([this.id, this.triggerType, this.triggerOperation, this.editorContent]); - super.buildCommandBarOptions.bind(this); - super.buildCommandBarOptions(); - } - - public onSaveClick = (): void => { - this._createTrigger({ - id: this.id(), - body: this.editorContent(), - triggerOperation: this.triggerOperation() as SqlTriggerResource["triggerOperation"], - triggerType: this.triggerType() as SqlTriggerResource["triggerType"], - }); - }; - - public onUpdateClick = (): Promise => { - const data = this._getResource(); - this.isExecutionError(false); - this.isExecuting(true); - const startKey: number = TelemetryProcessor.traceStart(Action.UpdateTrigger, { - tabTitle: this.tabTitle(), - }); - - return updateTrigger(this.collection.databaseId, this.collection.id(), { - id: this.id(), - body: this.editorContent(), - triggerOperation: this.triggerOperation() as SqlTriggerResource["triggerOperation"], - triggerType: this.triggerType() as SqlTriggerResource["triggerType"], - }) - .then( - (createdResource) => { - this.resource(createdResource); - this.tabTitle(createdResource.id); - - this.node.id(createdResource.id); - this.node.body(createdResource.body as string); - this.node.triggerType(createdResource.triggerOperation); - this.node.triggerOperation(createdResource.triggerOperation); - TelemetryProcessor.traceSuccess( - Action.UpdateTrigger, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey - ); - - this.setBaselines(); - - const editorModel = this.editor().getModel(); - editorModel.setValue(createdResource.body as string); - this.editorContent.setBaseline(createdResource.body as string); - }, - (createError: any) => { - this.isExecutionError(true); - TelemetryProcessor.traceFailure( - Action.UpdateTrigger, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: getErrorMessage(createError), - errorStack: getErrorStack(createError), - }, - startKey - ); - } - ) - .finally(() => this.isExecuting(false)); - }; - - public setBaselines() { - super.setBaselines(); - - const resource = this.resource(); - this.triggerOperation.setBaseline(resource.triggerOperation); - this.triggerType.setBaseline(resource.triggerType); - } - - protected updateSelectedNode(): void { - if (this.collection == null) { - return; - } - - const database: ViewModels.Database = this.collection.getDatabase(); - if (!database.isDatabaseExpanded()) { - this.collection.container.selectedNode(database); - } else if (!this.collection.isCollectionExpanded() || !this.collection.isTriggersExpanded()) { - this.collection.container.selectedNode(this.collection); - } else { - this.collection.container.selectedNode(this.node); - } - } - - private _createTrigger(resource: SqlTriggerResource): void { - this.isExecutionError(false); - this.isExecuting(true); - const startKey: number = TelemetryProcessor.traceStart(Action.CreateTrigger, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }); - - resource.body = String(resource.body); // Ensure trigger body is converted to string - createTrigger(this.collection.databaseId, this.collection.id(), resource) - .then( - (createdResource) => { - this.tabTitle(createdResource.id); - this.isNew(false); - this.resource(createdResource); - this.hashLocation( - `${Constants.HashRoutePrefixes.collectionsWithIds( - this.collection.databaseId, - this.collection.id() - )}/triggers/${createdResource.id}` - ); - this.setBaselines(); - - const editorModel = this.editor().getModel(); - editorModel.setValue(createdResource.body as string); - this.editorContent.setBaseline(createdResource.body as string); - - this.node = this.collection.createTriggerNode(createdResource); - TelemetryProcessor.traceSuccess( - Action.CreateTrigger, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey - ); - this.editorState(ViewModels.ScriptEditorState.exisitingNoEdits); - return createdResource; - }, - (createError: any) => { - this.isExecutionError(true); - TelemetryProcessor.traceFailure( - Action.CreateTrigger, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: getErrorMessage(createError), - errorStack: getErrorStack(createError), - }, - startKey - ); - return Promise.reject(createError); - } - ) - .finally(() => this.isExecuting(false)); - } - - private _getResource() { - return { - id: this.id(), - body: this.editorContent(), - triggerOperation: this.triggerOperation(), - triggerType: this.triggerType(), - }; - } -} diff --git a/src/Explorer/Tabs/TriggerTab.tsx b/src/Explorer/Tabs/TriggerTab.tsx new file mode 100644 index 000000000..e22634e1f --- /dev/null +++ b/src/Explorer/Tabs/TriggerTab.tsx @@ -0,0 +1,36 @@ +import { TriggerDefinition } from "@azure/cosmos"; +import React from "react"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types"; +import Trigger from "../Tree/Trigger"; +import ScriptTabBase from "./ScriptTabBase"; +import { TriggerTabContent } from "./TriggerTabContent"; + +export default class TriggerTab extends ScriptTabBase { + public onSaveClick: () => void; + public onUpdateClick: () => Promise; + public collection: ViewModels.Collection; + public node: Trigger; + public triggerType: ViewModels.Editable; + public triggerOperation: ViewModels.Editable; + public triggerOptions: ViewModels.ScriptTabOption; + + constructor(options: ViewModels.ScriptTabOption) { + super(options); + super.onActivate.bind(this); + this.triggerOptions = options; + } + + addNodeInCollection(createdResource: TriggerDefinition | SqlTriggerResource): void { + this.node = this.collection.createTriggerNode(createdResource); + } + + public render(): JSX.Element { + return ( + this.addNodeInCollection(createdResource)} + /> + ); + } +} diff --git a/src/Explorer/Tabs/TriggerTabContent.tsx b/src/Explorer/Tabs/TriggerTabContent.tsx new file mode 100644 index 000000000..1bb619eaf --- /dev/null +++ b/src/Explorer/Tabs/TriggerTabContent.tsx @@ -0,0 +1,343 @@ +import { TriggerDefinition } from "@azure/cosmos"; +import { Dropdown, IDropdownOption, Label, TextField } from "@fluentui/react"; +import React, { Component } from "react"; +import DiscardIcon from "../../../images/discard.svg"; +import SaveIcon from "../../../images/save-cosmos.svg"; +import * as Constants from "../../Common/Constants"; +import { createTrigger } from "../../Common/dataAccess/createTrigger"; +import { updateTrigger } from "../../Common/dataAccess/updateTrigger"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { Action } from "../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types"; +import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; +import { EditorReact } from "../Controls/Editor/EditorReact"; +import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; +import TriggerTab from "./TriggerTab"; + +const triggerTypeOptions: IDropdownOption[] = [ + { key: "Pre", text: "Pre" }, + { key: "Post", text: "Post" }, +]; + +const triggerOperationOptions: IDropdownOption[] = [ + { key: "All", text: "All" }, + { key: "Create", text: "Create" }, + { key: "Delete", text: "Delete" }, + { key: "Replace", text: "Replace" }, +]; + +interface Ibutton { + visible: boolean; + enabled: boolean; +} + +interface ITriggerTabContentState { + [key: string]: string | boolean; + triggerId: string; + triggerBody: string; + triggerType?: "Pre" | "Post"; + triggerOperation?: "All" | "Create" | "Update" | "Delete" | "Replace"; + isIdEditable: boolean; +} + +export class TriggerTabContent extends Component { + public saveButton: Ibutton; + public updateButton: Ibutton; + public discardButton: Ibutton; + + constructor(props: TriggerTab) { + super(props); + this.saveButton = { + visible: props.isNew(), + enabled: false, + }; + this.updateButton = { + visible: !props.isNew(), + enabled: true, + }; + + this.discardButton = { + visible: true, + enabled: true, + }; + + const { id, body, triggerType, triggerOperation } = props.triggerOptions.resource; + this.state = { + triggerId: id, + triggerType: triggerType, + triggerOperation: triggerOperation, + triggerBody: body, + isIdEditable: props.isNew() ? true : false, + }; + } + + private async onSaveClick(): Promise { + const { triggerId, triggerType, triggerBody, triggerOperation } = this.state; + const resource = { + id: triggerId, + body: triggerBody, + triggerOperation: triggerOperation, + triggerType: triggerType, + }; + + this.props.isExecutionError(false); + this.props.isExecuting(true); + const startKey: number = TelemetryProcessor.traceStart(Action.CreateTrigger, { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + }); + + try { + resource.body = String(resource.body); // Ensure trigger body is converted to string + const createdResource: TriggerDefinition | SqlTriggerResource = await createTrigger( + this.props.collection.databaseId, + this.props.collection.id(), + resource + ); + if (createdResource) { + this.props.tabTitle(createdResource.id); + this.props.isNew(false); + this.props.resource(createdResource); + this.props.hashLocation( + `${Constants.HashRoutePrefixes.collectionsWithIds( + this.props.collection.databaseId, + this.props.collection.id() + )}/triggers/${createdResource.id}` + ); + this.props.editorContent.setBaseline(createdResource.body as string); + this.props.addNodeInCollection(createdResource); + this.saveButton.visible = false; + this.updateButton.visible = true; + this.setState({ isIdEditable: false }); + TelemetryProcessor.traceSuccess( + Action.CreateTrigger, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + }, + startKey + ); + this.props.editorState(ViewModels.ScriptEditorState.exisitingNoEdits); + this.props.isExecuting(false); + } + } catch (createError) { + this.props.isExecutionError(true); + TelemetryProcessor.traceFailure( + Action.CreateTrigger, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + error: getErrorMessage(createError), + errorStack: getErrorStack(createError), + }, + startKey + ); + this.props.isExecuting(false); + } + } + + private async onUpdateClick(): Promise { + this.props.isExecutionError(false); + this.props.isExecuting(true); + const startKey: number = TelemetryProcessor.traceStart(Action.UpdateTrigger, { + tabTitle: this.props.tabTitle(), + }); + + try { + const { triggerId, triggerBody, triggerOperation, triggerType } = this.state; + const createdResource = await updateTrigger(this.props.collection.databaseId, this.props.collection.id(), { + id: triggerId, + body: triggerBody, + triggerOperation: triggerOperation as SqlTriggerResource["triggerOperation"], + triggerType: triggerType as SqlTriggerResource["triggerType"], + }); + if (createdResource) { + this.props.resource(createdResource); + this.props.tabTitle(createdResource.id); + + this.props.node.id(createdResource.id); + this.props.node.body(createdResource.body as string); + this.props.node.triggerType(createdResource.triggerType); + this.props.node.triggerOperation(createdResource.triggerOperation); + TelemetryProcessor.traceSuccess( + Action.UpdateTrigger, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + }, + startKey + ); + this.props.isExecuting(false); + } + } catch (createError) { + this.props.isExecutionError(true); + TelemetryProcessor.traceFailure( + Action.UpdateTrigger, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + error: getErrorMessage(createError), + errorStack: getErrorStack(createError), + }, + startKey + ); + this.props.isExecuting(false); + } + } + + private onDiscard(): void { + const { id, body, triggerType, triggerOperation } = this.props.triggerOptions.resource; + this.setState({ + triggerId: id, + triggerType: triggerType, + triggerOperation: triggerOperation, + triggerBody: body, + }); + } + + private isValidId(id: string): boolean { + if (!id) { + return false; + } + + const invalidStartCharacters = /^[/?#\\]/; + if (invalidStartCharacters.test(id)) { + return false; + } + + const invalidMiddleCharacters = /^.+[/?#\\]/; + if (invalidMiddleCharacters.test(id)) { + return false; + } + + const invalidEndCharacters = /.*[/?#\\ ]$/; + if (invalidEndCharacters.test(id)) { + return false; + } + + return true; + } + + private isNotEmpty(value: string): boolean { + return !!value; + } + + protected getTabsButtons(): CommandButtonComponentProps[] { + const buttons: CommandButtonComponentProps[] = []; + const label = "Save"; + if (this.saveButton.visible) { + buttons.push({ + setState: this.setState, + ...this, + iconSrc: SaveIcon, + iconAlt: label, + onCommandClick: this.onSaveClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.saveButton.enabled, + }); + } + + if (this.updateButton.visible) { + const label = "Update"; + buttons.push({ + ...this, + iconSrc: SaveIcon, + iconAlt: label, + onCommandClick: this.onUpdateClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.updateButton.enabled, + }); + } + + if (this.discardButton.visible) { + const label = "Discard"; + buttons.push({ + setState: this.setState, + ...this, + iconSrc: DiscardIcon, + iconAlt: label, + onCommandClick: this.onDiscard, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.discardButton.enabled, + }); + } + return buttons; + } + + private handleTriggerIdChange = ( + _event: React.FormEvent, + newValue?: string + ): void => { + this.saveButton.enabled = this.isValidId(newValue) && this.isNotEmpty(newValue); + this.setState({ triggerId: newValue }); + }; + + private handleTriggerTypeOprationChange = ( + _event: React.FormEvent, + selectedParam: IDropdownOption, + key: string + ): void => { + this.setState({ [key]: String(selectedParam.key) }); + }; + + private handleTriggerBodyChange = (newContent: string) => { + this.setState({ triggerBody: newContent }); + }; + + render(): JSX.Element { + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + const { triggerId, triggerType, triggerOperation, triggerBody, isIdEditable } = this.state; + return ( +
+ + this.handleTriggerTypeOprationChange(event, selectedKey, "triggerType")} + /> + + this.handleTriggerTypeOprationChange(event, selectedKey, "triggerOperation") + } + /> + + +
+ ); + } +} From 6f68c75257277f86a770d4797b448ef2718034f7 Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Wed, 16 Jun 2021 09:13:11 -0500 Subject: [PATCH 09/31] Allow dynamic MSAL Authority (#896) --- src/ConfigContext.ts | 8 ++++++++ src/Utils/AuthorizationUtils.ts | 3 ++- src/hooks/useAADAuth.ts | 10 +++++----- src/hooks/useDatabaseAccounts.tsx | 3 ++- src/hooks/useDirectories.tsx | 3 ++- src/hooks/useGraphPhoto.tsx | 3 ++- src/hooks/useSubscriptions.tsx | 3 ++- 7 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 5f64cbe81..e83a4e9ec 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -120,6 +120,14 @@ export async function initializeConfiguration(): Promise { const armAPIVersion = params.get("armAPIVersion") || ""; updateConfigContext({ armAPIVersion }); } + if (params.has("armEndpoint")) { + const ARM_ENDPOINT = params.get("armEndpoint") || ""; + updateConfigContext({ ARM_ENDPOINT }); + } + if (params.has("aadEndpoint")) { + const AAD_ENDPOINT = params.get("aadEndpoint") || ""; + updateConfigContext({ AAD_ENDPOINT }); + } if (params.has("platform")) { const platform = params.get("platform"); switch (platform) { diff --git a/src/Utils/AuthorizationUtils.ts b/src/Utils/AuthorizationUtils.ts index 72418c699..0da7e310f 100644 --- a/src/Utils/AuthorizationUtils.ts +++ b/src/Utils/AuthorizationUtils.ts @@ -2,6 +2,7 @@ import * as msal from "@azure/msal-browser"; import { AuthType } from "../AuthType"; import * as Constants from "../Common/Constants"; import * as Logger from "../Common/Logger"; +import { configContext } from "../ConfigContext"; import * as ViewModels from "../Contracts/ViewModels"; import { userContext } from "../UserContext"; @@ -48,7 +49,7 @@ export function getMsalInstance() { cacheLocation: "localStorage", }, auth: { - authority: "https://login.microsoftonline.com/common", + authority: `${configContext.AAD_ENDPOINT}common`, clientId: "203f1145-856a-4232-83d4-a43568fba23d", }, }; diff --git a/src/hooks/useAADAuth.ts b/src/hooks/useAADAuth.ts index 589cc4b0f..630521f2d 100644 --- a/src/hooks/useAADAuth.ts +++ b/src/hooks/useAADAuth.ts @@ -51,7 +51,7 @@ export function useAADAuth(): ReturnType { async (id) => { const response = await msalInstance.loginPopup({ redirectUri: configContext.msalRedirectURI, - authority: `https://login.microsoftonline.com/${id}`, + authority: `${configContext.AAD_ENDPOINT}${id}`, scopes: [], }); setTenantId(response.tenantId); @@ -64,12 +64,12 @@ export function useAADAuth(): ReturnType { if (account && tenantId) { Promise.all([ msalInstance.acquireTokenSilent({ - authority: `https://login.microsoftonline.com/${tenantId}`, - scopes: ["https://graph.windows.net//.default"], + authority: `${configContext.AAD_ENDPOINT}${tenantId}`, + scopes: [`${configContext.GRAPH_ENDPOINT}/.default`], }), msalInstance.acquireTokenSilent({ - authority: `https://login.microsoftonline.com/${tenantId}`, - scopes: ["https://management.azure.com//.default"], + authority: `${configContext.AAD_ENDPOINT}${tenantId}`, + scopes: [`${configContext.ARM_ENDPOINT}/.default`], }), ]).then(([graphTokenResponse, armTokenResponse]) => { setGraphToken(graphTokenResponse.accessToken); diff --git a/src/hooks/useDatabaseAccounts.tsx b/src/hooks/useDatabaseAccounts.tsx index 97ced2799..378d4639f 100644 --- a/src/hooks/useDatabaseAccounts.tsx +++ b/src/hooks/useDatabaseAccounts.tsx @@ -1,4 +1,5 @@ import useSWR from "swr"; +import { configContext } from "../ConfigContext"; import { DatabaseAccount } from "../Contracts/DataModels"; interface AccountListResult { @@ -14,7 +15,7 @@ export async function fetchDatabaseAccounts(subscriptionId: string, accessToken: let accounts: Array = []; - let nextLink = `https://management.azure.com/subscriptions/${subscriptionId}/providers/Microsoft.DocumentDB/databaseAccounts?api-version=2020-06-01-preview`; + let nextLink = `${configContext.ARM_ENDPOINT}/subscriptions/${subscriptionId}/providers/Microsoft.DocumentDB/databaseAccounts?api-version=2020-06-01-preview`; while (nextLink) { const response: Response = await fetch(nextLink, { headers }); diff --git a/src/hooks/useDirectories.tsx b/src/hooks/useDirectories.tsx index e78ff5a14..2073cf81a 100644 --- a/src/hooks/useDirectories.tsx +++ b/src/hooks/useDirectories.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { configContext } from "../ConfigContext"; import { Tenant } from "../Contracts/DataModels"; interface TenantListResult { @@ -13,7 +14,7 @@ export async function fetchDirectories(accessToken: string): Promise { headers.append("Authorization", bearer); let tenents: Array = []; - let nextLink = `https://management.azure.com/tenants?api-version=2020-01-01`; + let nextLink = `${configContext.ARM_ENDPOINT}/tenants?api-version=2020-01-01`; while (nextLink) { const response = await fetch(nextLink, { headers }); diff --git a/src/hooks/useGraphPhoto.tsx b/src/hooks/useGraphPhoto.tsx index e09efeb04..b47d8d536 100644 --- a/src/hooks/useGraphPhoto.tsx +++ b/src/hooks/useGraphPhoto.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { configContext } from "../ConfigContext"; export async function fetchPhoto(accessToken: string): Promise { const headers = new Headers(); @@ -12,7 +13,7 @@ export async function fetchPhoto(accessToken: string): Promise { headers: headers, }; - return fetch("https://graph.windows.net/me/thumbnailPhoto?api-version=1.6", options).then((response) => + return fetch(`${configContext.GRAPH_ENDPOINT}/me/thumbnailPhoto?api-version=1.6`, options).then((response) => response.blob() ); } diff --git a/src/hooks/useSubscriptions.tsx b/src/hooks/useSubscriptions.tsx index d7ebfcbe3..e06542240 100644 --- a/src/hooks/useSubscriptions.tsx +++ b/src/hooks/useSubscriptions.tsx @@ -1,4 +1,5 @@ import useSWR from "swr"; +import { configContext } from "../ConfigContext"; import { Subscription } from "../Contracts/DataModels"; interface SubscriptionListResult { @@ -13,7 +14,7 @@ export async function fetchSubscriptions(accessToken: string): Promise = []; - let nextLink = `https://management.azure.com/subscriptions?api-version=2020-01-01`; + let nextLink = `${configContext.ARM_ENDPOINT}subscriptions?api-version=2020-01-01`; while (nextLink) { const response = await fetch(nextLink, { headers }); From 1d449e5b5238897cdbf64ff40d904543dca21a4c Mon Sep 17 00:00:00 2001 From: vaidankarswapnil <81285216+vaidankarswapnil@users.noreply.github.com> Date: Thu, 17 Jun 2021 05:32:33 +0530 Subject: [PATCH 10/31] Implemented spinner in EditorReact component (#897) --- less/documentDB.less | 7 +++++ src/Explorer/Controls/Editor/EditorReact.tsx | 28 +++++++++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/less/documentDB.less b/less/documentDB.less index 36744ddeb..1c8a7e8bb 100644 --- a/less/documentDB.less +++ b/less/documentDB.less @@ -3089,3 +3089,10 @@ settings-pane { display: none; height: 0px; } +.spinner { + width: 100%; + position: absolute; + z-index: 1; + background: white; + height: 100%; +} diff --git a/src/Explorer/Controls/Editor/EditorReact.tsx b/src/Explorer/Controls/Editor/EditorReact.tsx index 71273ed20..662b78056 100644 --- a/src/Explorer/Controls/Editor/EditorReact.tsx +++ b/src/Explorer/Controls/Editor/EditorReact.tsx @@ -1,6 +1,11 @@ +import { Spinner, SpinnerSize } from "@fluentui/react"; import * as React from "react"; import { loadMonaco, monaco } from "../../LazyMonaco"; +// import "./EditorReact.less"; +interface EditorReactStates { + showEditor: boolean; +} export interface EditorReactProps { language: string; content: string; @@ -12,30 +17,33 @@ export interface EditorReactProps { theme?: string; // Monaco editor theme } -export class EditorReact extends React.Component { +export class EditorReact extends React.Component { private rootNode: HTMLElement; private editor: monaco.editor.IStandaloneCodeEditor; private selectionListener: monaco.IDisposable; public constructor(props: EditorReactProps) { super(props); + this.state = { + showEditor: false, + }; } public componentDidMount(): void { this.createEditor(this.configureEditor.bind(this)); } - public shouldComponentUpdate(): boolean { - // Prevents component re-rendering - return false; - } - public componentWillUnmount(): void { this.selectionListener && this.selectionListener.dispose(); } public render(): JSX.Element { - return
this.setRef(elt)} />; + return ( + + {!this.state.showEditor && } +
this.setRef(elt)} /> + + ); } protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) { @@ -76,6 +84,12 @@ export class EditorReact extends React.Component { this.rootNode.innerHTML = ""; const monaco = await loadMonaco(); createCallback(monaco.editor.create(this.rootNode, options)); + + if (this.rootNode.innerHTML) { + this.setState({ + showEditor: true, + }); + } } private setRef(element: HTMLElement): void { From 05f59307c2c3af833402a00f4d69f7272169f653 Mon Sep 17 00:00:00 2001 From: Sunil Kumar Yadav <79906609+sunilyadav840@users.noreply.github.com> Date: Thu, 17 Jun 2021 05:39:22 +0530 Subject: [PATCH 11/31] Migrate userDefinedFunctionTab to React (#860) --- src/Explorer/Tabs/UserDefinedFunctionTab.html | 30 -- src/Explorer/Tabs/UserDefinedFunctionTab.ts | 162 ---------- src/Explorer/Tabs/UserDefinedFunctionTab.tsx | 41 +++ .../Tabs/UserDefinedFunctionTabContent.tsx | 306 ++++++++++++++++++ 4 files changed, 347 insertions(+), 192 deletions(-) delete mode 100644 src/Explorer/Tabs/UserDefinedFunctionTab.html delete mode 100644 src/Explorer/Tabs/UserDefinedFunctionTab.ts create mode 100644 src/Explorer/Tabs/UserDefinedFunctionTab.tsx create mode 100644 src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx diff --git a/src/Explorer/Tabs/UserDefinedFunctionTab.html b/src/Explorer/Tabs/UserDefinedFunctionTab.html deleted file mode 100644 index 259604f29..000000000 --- a/src/Explorer/Tabs/UserDefinedFunctionTab.html +++ /dev/null @@ -1,30 +0,0 @@ -
- -
-
User Defined Function Id
- - - -
User Defined Function Body
-
-
- -
diff --git a/src/Explorer/Tabs/UserDefinedFunctionTab.ts b/src/Explorer/Tabs/UserDefinedFunctionTab.ts deleted file mode 100644 index ff5a7753d..000000000 --- a/src/Explorer/Tabs/UserDefinedFunctionTab.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos"; -import * as Constants from "../../Common/Constants"; -import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction"; -import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction"; -import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { Action } from "../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; -import UserDefinedFunction from "../Tree/UserDefinedFunction"; -import ScriptTabBase from "./ScriptTabBase"; -import template from "./UserDefinedFunctionTab.html"; - -export default class UserDefinedFunctionTab extends ScriptTabBase { - public readonly html = template; - public collection: ViewModels.Collection; - public node: UserDefinedFunction; - constructor(options: ViewModels.ScriptTabOption) { - super(options); - this.ariaLabel("User Defined Function Body"); - super.onActivate.bind(this); - super.buildCommandBarOptions.bind(this); - super.buildCommandBarOptions(); - } - - public onSaveClick = (): Promise => { - const data = this._getResource(); - return this._createUserDefinedFunction(data); - }; - - public onUpdateClick = (): Promise => { - const data = this._getResource(); - this.isExecutionError(false); - this.isExecuting(true); - const startKey: number = TelemetryProcessor.traceStart(Action.UpdateUDF, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }); - - return updateUserDefinedFunction(this.collection.databaseId, this.collection.id(), data) - .then( - (createdResource) => { - this.resource(createdResource); - this.tabTitle(createdResource.id); - - this.node.id(createdResource.id); - this.node.body(createdResource.body as string); - TelemetryProcessor.traceSuccess( - Action.UpdateUDF, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }, - startKey - ); - - this.setBaselines(); - - const editorModel = this.editor().getModel(); - editorModel.setValue(createdResource.body as string); - this.editorContent.setBaseline(createdResource.body as string); - }, - (createError: any) => { - this.isExecutionError(true); - TelemetryProcessor.traceFailure( - Action.UpdateUDF, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: getErrorMessage(createError), - errorStack: getErrorStack(createError), - }, - startKey - ); - } - ) - .finally(() => this.isExecuting(false)); - }; - - protected updateSelectedNode(): void { - if (this.collection == null) { - return; - } - - const database: ViewModels.Database = this.collection.getDatabase(); - if (!database.isDatabaseExpanded()) { - this.collection.container.selectedNode(database); - } else if (!this.collection.isCollectionExpanded() || !this.collection.isUserDefinedFunctionsExpanded()) { - this.collection.container.selectedNode(this.collection); - } else { - this.collection.container.selectedNode(this.node); - } - } - - private _createUserDefinedFunction( - resource: UserDefinedFunctionDefinition - ): Promise { - this.isExecutionError(false); - this.isExecuting(true); - const startKey: number = TelemetryProcessor.traceStart(Action.CreateUDF, { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - }); - - return createUserDefinedFunction(this.collection.databaseId, this.collection.id(), resource) - .then( - (createdResource) => { - this.tabTitle(createdResource.id); - this.isNew(false); - this.resource(createdResource); - this.hashLocation( - `${Constants.HashRoutePrefixes.collectionsWithIds(this.collection.databaseId, this.collection.id())}/udfs/${ - createdResource.id - }` - ); - this.setBaselines(); - - const editorModel = this.editor().getModel(); - editorModel.setValue(createdResource.body as string); - this.editorContent.setBaseline(createdResource.body as string); - - this.node = this.collection.createUserDefinedFunctionNode(createdResource); - TelemetryProcessor.traceSuccess( - Action.CreateUDF, - { - dataExplorerArea: Constants.Areas.Tab, - - tabTitle: this.tabTitle(), - }, - startKey - ); - this.editorState(ViewModels.ScriptEditorState.exisitingNoEdits); - return createdResource; - }, - (createError: any) => { - this.isExecutionError(true); - TelemetryProcessor.traceFailure( - Action.CreateUDF, - { - dataExplorerArea: Constants.Areas.Tab, - tabTitle: this.tabTitle(), - error: getErrorMessage(createError), - errorStack: getErrorStack(createError), - }, - startKey - ); - return Promise.reject(createError); - } - ) - .finally(() => this.isExecuting(false)); - } - - private _getResource() { - const resource = { - _rid: this.resource()._rid, - _self: this.resource()._self, - id: this.id(), - body: this.editorContent(), - }; - - return resource; - } -} diff --git a/src/Explorer/Tabs/UserDefinedFunctionTab.tsx b/src/Explorer/Tabs/UserDefinedFunctionTab.tsx new file mode 100644 index 000000000..ba3744422 --- /dev/null +++ b/src/Explorer/Tabs/UserDefinedFunctionTab.tsx @@ -0,0 +1,41 @@ +import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos"; +import React from "react"; +import * as ViewModels from "../../Contracts/ViewModels"; +import UserDefinedFunction from "../Tree/UserDefinedFunction"; +import ScriptTabBase from "./ScriptTabBase"; +import UserDefinedFunctionTabContent from "./UserDefinedFunctionTabContent"; + +export default class UserDefinedFunctionTab extends ScriptTabBase { + public onSaveClick: () => Promise; + public onUpdateClick: () => Promise; + public collection: ViewModels.Collection; + public node: UserDefinedFunction; + constructor(options: ViewModels.ScriptTabOption) { + super(options); + this.ariaLabel("User Defined Function Body"); + super.onActivate.bind(this); + super.buildCommandBarOptions.bind(this); + super.buildCommandBarOptions(); + } + + addNodeInCollection(createdResource: Resource & UserDefinedFunctionDefinition): void { + this.node = this.collection.createUserDefinedFunctionNode(createdResource); + } + + updateNodeInCollection(updateResource: Resource & UserDefinedFunctionDefinition): void { + this.node.id(updateResource.id); + this.node.body(updateResource.body as string); + } + + render(): JSX.Element { + return ( + this.addNodeInCollection(createdResource)} + updateNodeInCollection={(updateResource: Resource & UserDefinedFunctionDefinition) => + this.updateNodeInCollection(updateResource) + } + /> + ); + } +} diff --git a/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx b/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx new file mode 100644 index 000000000..b927d55a0 --- /dev/null +++ b/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx @@ -0,0 +1,306 @@ +import { UserDefinedFunctionDefinition } from "@azure/cosmos"; +import { Label, TextField } from "@fluentui/react"; +import React, { Component } from "react"; +import DiscardIcon from "../../../images/discard.svg"; +import SaveIcon from "../../../images/save-cosmos.svg"; +import * as Constants from "../../Common/Constants"; +import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction"; +import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction"; +import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { Action } from "../../Shared/Telemetry/TelemetryConstants"; +import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; +import { EditorReact } from "../Controls/Editor/EditorReact"; +import { useCommandBar } from "../Menus/CommandBar/CommandBarComponentAdapter"; +import UserDefinedFunctionTab from "./UserDefinedFunctionTab"; + +interface IUserDefinedFunctionTabContentState { + udfId: string; + udfBody: string; + isUdfIdEditable: boolean; +} + +interface Ibutton { + visible: boolean; + enabled: boolean; +} + +export default class UserDefinedFunctionTabContent extends Component< + UserDefinedFunctionTab, + IUserDefinedFunctionTabContentState +> { + public saveButton: Ibutton; + public updateButton: Ibutton; + public discardButton: Ibutton; + + constructor(props: UserDefinedFunctionTab) { + super(props); + + this.saveButton = { + visible: props.isNew(), + enabled: false, + }; + this.updateButton = { + visible: !props.isNew(), + enabled: true, + }; + + this.discardButton = { + visible: true, + enabled: true, + }; + + const { id, body } = props.resource(); + this.state = { + udfId: id, + udfBody: body, + isUdfIdEditable: props.isNew() ? true : false, + }; + } + + private handleUdfIdChange = ( + _event: React.FormEvent, + newValue?: string + ): void => { + this.saveButton.enabled = this.isValidId(newValue) && this.isNotEmpty(newValue); + this.setState({ udfId: newValue }); + }; + + private handleUdfBodyChange = (newContent: string) => { + this.setState({ udfBody: newContent }); + }; + + protected getTabsButtons(): CommandButtonComponentProps[] { + const buttons: CommandButtonComponentProps[] = []; + const label = "Save"; + if (this.saveButton.visible) { + buttons.push({ + ...this, + setState: this.setState, + iconSrc: SaveIcon, + iconAlt: label, + onCommandClick: this.onSaveClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.saveButton.enabled, + }); + } + + if (this.updateButton.visible) { + const label = "Update"; + buttons.push({ + ...this, + iconSrc: SaveIcon, + iconAlt: label, + onCommandClick: this.onUpdateClick, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.updateButton.enabled, + }); + } + + if (this.discardButton.visible) { + const label = "Discard"; + buttons.push({ + setState: this.setState, + ...this, + iconSrc: DiscardIcon, + iconAlt: label, + onCommandClick: this.onDiscard, + commandButtonLabel: label, + ariaLabel: label, + hasPopup: false, + disabled: !this.discardButton.enabled, + }); + } + return buttons; + } + + private async onSaveClick(): Promise { + const { udfId, udfBody } = this.state; + const resource: UserDefinedFunctionDefinition = { + id: udfId, + body: udfBody, + }; + + this.props.isExecutionError(false); + this.props.isExecuting(true); + const startKey: number = TelemetryProcessor.traceStart(Action.CreateUDF, { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + }); + + try { + const createdResource = await createUserDefinedFunction( + this.props.collection.databaseId, + this.props.collection.id(), + resource + ); + if (createdResource) { + this.props.tabTitle(createdResource.id); + this.props.isNew(false); + this.updateButton.visible = true; + this.saveButton.visible = false; + this.props.resource(createdResource); + this.props.hashLocation( + `${Constants.HashRoutePrefixes.collectionsWithIds( + this.props.collection.databaseId, + this.props.collection.id() + )}/udfs/${createdResource.id}` + ); + this.props.addNodeInCollection(createdResource); + this.setState({ isUdfIdEditable: false }); + this.props.isExecuting(false); + TelemetryProcessor.traceSuccess( + Action.CreateUDF, + { + dataExplorerArea: Constants.Areas.Tab, + + tabTitle: this.props.tabTitle(), + }, + startKey + ); + this.props.editorState(ViewModels.ScriptEditorState.exisitingNoEdits); + } + } catch (createError) { + this.props.isExecutionError(true); + TelemetryProcessor.traceFailure( + Action.CreateUDF, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + error: getErrorMessage(createError), + errorStack: getErrorStack(createError), + }, + startKey + ); + this.props.isExecuting(false); + return Promise.reject(createError); + } + } + + private async onUpdateClick(): Promise { + const { udfId, udfBody } = this.state; + const resource: UserDefinedFunctionDefinition = { + id: udfId, + body: udfBody, + }; + this.props.isExecutionError(false); + this.props.isExecuting(true); + const startKey: number = TelemetryProcessor.traceStart(Action.UpdateUDF, { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + }); + + try { + const createdResource = await updateUserDefinedFunction( + this.props.collection.databaseId, + this.props.collection.id(), + resource + ); + + this.props.resource(createdResource); + this.props.tabTitle(createdResource.id); + this.props.updateNodeInCollection(createdResource); + this.props.isExecuting(false); + TelemetryProcessor.traceSuccess( + Action.UpdateUDF, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + }, + startKey + ); + + this.props.editorContent.setBaseline(createdResource.body as string); + } catch (createError) { + this.props.isExecutionError(true); + TelemetryProcessor.traceFailure( + Action.UpdateUDF, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle: this.props.tabTitle(), + error: getErrorMessage(createError), + errorStack: getErrorStack(createError), + }, + startKey + ); + this.props.isExecuting(false); + } + } + + private onDiscard(): void { + const { id, body } = this.props.resource(); + this.setState({ + udfId: id, + udfBody: body, + }); + } + + private isValidId(id: string): boolean { + if (!id) { + return false; + } + + const invalidStartCharacters = /^[/?#\\]/; + if (invalidStartCharacters.test(id)) { + return false; + } + + const invalidMiddleCharacters = /^.+[/?#\\]/; + if (invalidMiddleCharacters.test(id)) { + return false; + } + + const invalidEndCharacters = /.*[/?#\\ ]$/; + if (invalidEndCharacters.test(id)) { + return false; + } + + return true; + } + + private isNotEmpty(value: string): boolean { + return !!value; + } + + componentDidUpdate(_prevProps: UserDefinedFunctionTab, prevState: IUserDefinedFunctionTabContentState): void { + const { udfBody, udfId } = this.state; + if (udfId !== prevState.udfId || udfBody !== prevState.udfBody) { + useCommandBar.getState().setContextButtons(this.getTabsButtons()); + } + } + + render(): JSX.Element { + const { udfId, udfBody, isUdfIdEditable } = this.state; + return ( +
+ + + +
+ ); + } +} From c9fa44f6f4910e22cb35b6b8b877fcfd129ea78c Mon Sep 17 00:00:00 2001 From: Sunil Kumar Yadav <79906609+sunilyadav840@users.noreply.github.com> Date: Thu, 17 Jun 2021 21:24:40 +0530 Subject: [PATCH 12/31] Fix UDF placeholder text (#899) --- src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx b/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx index b927d55a0..3ce6587c3 100644 --- a/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx +++ b/src/Explorer/Tabs/UserDefinedFunctionTabContent.tsx @@ -287,7 +287,7 @@ export default class UserDefinedFunctionTabContent extends Component< readOnly={!isUdfIdEditable} type="text" pattern="[^/?#\\]*[^/?# \\]" - placeholder="Enter the new trigger id" + placeholder="Enter the new user defined function id" size={40} value={udfId} onChange={this.handleUdfIdChange} From 96e6bba38b142aaee47b567f1f07645f2f7f9520 Mon Sep 17 00:00:00 2001 From: victor-meng <56978073+victor-meng@users.noreply.github.com> Date: Fri, 18 Jun 2021 11:25:08 -0700 Subject: [PATCH 13/31] Move databases to zustand (#898) --- src/Common/QueriesClient.ts | 3 +- .../SettingsComponent.test.tsx.snap | 4 - .../ContainerSampleGenerator.test.ts | 24 ++-- .../DataSamples/ContainerSampleGenerator.ts | 3 +- .../DataSamples/DataSamplesUtil.test.ts | 3 +- src/Explorer/DataSamples/DataSamplesUtil.ts | 3 +- src/Explorer/Explorer.test.tsx | 43 ------ src/Explorer/Explorer.tsx | 133 +++--------------- src/Explorer/Panes/AddCollectionPanel.tsx | 21 ++- .../AddDatabasePanel/AddDatabasePanel.tsx | 10 +- .../BrowseQueriesPane.test.tsx | 14 +- .../BrowseQueriesPane/BrowseQueriesPane.tsx | 4 +- .../BrowseQueriesPane.test.tsx.snap | 1 - .../CassandraAddCollectionPane.tsx | 9 +- .../DeleteCollectionConfirmationPane.test.tsx | 65 +++++---- .../DeleteCollectionConfirmationPane.tsx | 5 +- ...teCollectionConfirmationPane.test.tsx.snap | 1 - .../DeleteDatabaseConfirmationPanel.test.tsx | 43 +++--- .../Panes/DeleteDatabaseConfirmationPanel.tsx | 10 +- .../GitHubReposPanel.test.tsx.snap | 2 - .../SaveQueryPane/SaveQueryPane.test.tsx | 32 +++-- .../Panes/SaveQueryPane/SaveQueryPane.tsx | 11 +- .../StringInputPane.test.tsx.snap | 2 - ...eteDatabaseConfirmationPanel.test.tsx.snap | 62 ++------ src/Explorer/SplashScreen/SplashScreen.tsx | 5 +- src/Explorer/Tree/Collection.ts | 3 +- src/Explorer/Tree/ResourceTokenCollection.ts | 3 +- src/Explorer/Tree/ResourceTreeAdapter.tsx | 26 ++-- src/Explorer/useDatabases.ts | 113 +++++++++++++++ src/RouteHandlers/TabRouteHandler.ts | 8 +- src/Shared/AddCollectionUtility.test.ts | 64 --------- src/Shared/AddCollectionUtility.ts | 23 --- src/hooks/useKnockoutExplorer.ts | 3 +- 33 files changed, 310 insertions(+), 446 deletions(-) delete mode 100644 src/Explorer/Explorer.test.tsx create mode 100644 src/Explorer/useDatabases.ts delete mode 100644 src/Shared/AddCollectionUtility.test.ts delete mode 100644 src/Shared/AddCollectionUtility.ts diff --git a/src/Common/QueriesClient.ts b/src/Common/QueriesClient.ts index dfb31a2f6..534d12879 100644 --- a/src/Common/QueriesClient.ts +++ b/src/Common/QueriesClient.ts @@ -4,6 +4,7 @@ import * as ViewModels from "../Contracts/ViewModels"; import Explorer from "../Explorer/Explorer"; import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; import DocumentId from "../Explorer/Tree/DocumentId"; +import { useDatabases } from "../Explorer/useDatabases"; import { userContext } from "../UserContext"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; @@ -176,7 +177,7 @@ export class QueriesClient { private findQueriesCollection(): ViewModels.Collection { const queriesDatabase: ViewModels.Database = _.find( - this.container.databases(), + useDatabases.getState().databases, (database: ViewModels.Database) => database.id() === SavedQueries.DatabaseName ); if (!queriesDatabase) { diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 99f63dd81..42aa68497 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -30,8 +30,6 @@ exports[`SettingsComponent renders 1`] = ` "container": Explorer { "_isInitializingNotebooks": false, "_resetNotebookWorkspace": [Function], - "canSaveQueries": [Function], - "databases": [Function], "isAccountReady": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isNotebookEnabled": [Function], @@ -124,8 +122,6 @@ exports[`SettingsComponent renders 1`] = ` "container": Explorer { "_isInitializingNotebooks": false, "_resetNotebookWorkspace": [Function], - "canSaveQueries": [Function], - "databases": [Function], "isAccountReady": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isNotebookEnabled": [Function], diff --git a/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts b/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts index b9932656a..aca891f24 100644 --- a/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts +++ b/src/Explorer/DataSamples/ContainerSampleGenerator.test.ts @@ -2,22 +2,22 @@ jest.mock("../Graph/GraphExplorerComponent/GremlinClient"); jest.mock("../../Common/dataAccess/createCollection"); jest.mock("../../Common/dataAccess/createDocument"); import * as ko from "knockout"; -import Q from "q"; import { createDocument } from "../../Common/dataAccess/createDocument"; import { DatabaseAccount } from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { updateUserContext } from "../../UserContext"; import Explorer from "../Explorer"; +import { useDatabases } from "../useDatabases"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; describe("ContainerSampleGenerator", () => { - const createExplorerStub = (database: ViewModels.Database): Explorer => { - const explorerStub = {} as Explorer; - explorerStub.databases = ko.observableArray([database]); - explorerStub.findDatabaseWithId = () => database; - explorerStub.refreshAllDatabases = () => Q.resolve(); - return explorerStub; - }; + let explorerStub: Explorer; + + beforeAll(() => { + explorerStub = { + refreshAllDatabases: () => {}, + } as Explorer; + }); beforeEach(() => { (createDocument as jest.Mock).mockResolvedValue(undefined); @@ -59,8 +59,7 @@ describe("ContainerSampleGenerator", () => { loadCollections: () => {}, } as ViewModels.Database; database.findCollectionWithId = () => collection; - - const explorerStub = createExplorerStub(database); + useDatabases.getState().addDatabases([database]); const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub); generator.setData(sampleData); @@ -108,8 +107,8 @@ describe("ContainerSampleGenerator", () => { } as ViewModels.Database; database.findCollectionWithId = () => collection; collection.databaseId = database.id(); + useDatabases.getState().addDatabases([database]); - const explorerStub = createExplorerStub(database); updateUserContext({ databaseAccount: { properties: { @@ -126,7 +125,6 @@ describe("ContainerSampleGenerator", () => { it("should not create any sample for Mongo API account", async () => { const experience = "Sample generation not supported for this API Mongo"; - const explorerStub = createExplorerStub(undefined); updateUserContext({ databaseAccount: { properties: { @@ -141,7 +139,6 @@ describe("ContainerSampleGenerator", () => { it("should not create any sample for Table API account", async () => { const experience = "Sample generation not supported for this API Tables"; - const explorerStub = createExplorerStub(undefined); updateUserContext({ databaseAccount: { properties: { @@ -163,7 +160,6 @@ describe("ContainerSampleGenerator", () => { }, } as DatabaseAccount, }); - const explorerStub = createExplorerStub(undefined); // Rejects with error that contains experience await expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience); }); diff --git a/src/Explorer/DataSamples/ContainerSampleGenerator.ts b/src/Explorer/DataSamples/ContainerSampleGenerator.ts index dd9a4adb9..44906151d 100644 --- a/src/Explorer/DataSamples/ContainerSampleGenerator.ts +++ b/src/Explorer/DataSamples/ContainerSampleGenerator.ts @@ -7,6 +7,7 @@ import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils" import GraphTab from ".././Tabs/GraphTab"; import Explorer from "../Explorer"; import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient"; +import { useDatabases } from "../useDatabases"; interface SampleDataFile extends DataModels.CreateCollectionParams { data: any[]; @@ -59,7 +60,7 @@ export class ContainerSampleGenerator { await createCollection(createRequest); await this.container.refreshAllDatabases(); - const database = this.container.findDatabaseWithId(this.sampleDataFile.databaseId); + const database = useDatabases.getState().findDatabaseWithId(this.sampleDataFile.databaseId); if (!database) { return undefined; } diff --git a/src/Explorer/DataSamples/DataSamplesUtil.test.ts b/src/Explorer/DataSamples/DataSamplesUtil.test.ts index 9a35158f5..f8ff6f8e5 100644 --- a/src/Explorer/DataSamples/DataSamplesUtil.test.ts +++ b/src/Explorer/DataSamples/DataSamplesUtil.test.ts @@ -2,6 +2,7 @@ import * as ko from "knockout"; import * as sinon from "sinon"; import { Collection, Database } from "../../Contracts/ViewModels"; import Explorer from "../Explorer"; +import { useDatabases } from "../useDatabases"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; import { DataSamplesUtil } from "./DataSamplesUtil"; @@ -16,8 +17,8 @@ describe("DataSampleUtils", () => { collections: ko.observableArray([collection]), } as Database; const explorer = {} as Explorer; - explorer.databases = ko.observableArray([database]); explorer.showOkModalDialog = () => {}; + useDatabases.getState().addDatabases([database]); const dataSamplesUtil = new DataSamplesUtil(explorer); const fakeGenerator = sinon.createStubInstance(ContainerSampleGenerator as any); diff --git a/src/Explorer/DataSamples/DataSamplesUtil.ts b/src/Explorer/DataSamples/DataSamplesUtil.ts index 63b35cfff..4007608c0 100644 --- a/src/Explorer/DataSamples/DataSamplesUtil.ts +++ b/src/Explorer/DataSamples/DataSamplesUtil.ts @@ -2,6 +2,7 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { userContext } from "../../UserContext"; import { logConsoleError, logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import Explorer from "../Explorer"; +import { useDatabases } from "../useDatabases"; import { ContainerSampleGenerator } from "./ContainerSampleGenerator"; export class DataSamplesUtil { @@ -17,7 +18,7 @@ export class DataSamplesUtil { const databaseName = generator.getDatabaseId(); const containerName = generator.getCollectionId(); - if (this.hasContainer(databaseName, containerName, this.container.databases())) { + if (this.hasContainer(databaseName, containerName, useDatabases.getState().databases)) { const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`; this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg); logConsoleError(msg); diff --git a/src/Explorer/Explorer.test.tsx b/src/Explorer/Explorer.test.tsx deleted file mode 100644 index ef595252a..000000000 --- a/src/Explorer/Explorer.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -jest.mock("./../Common/dataAccess/deleteDatabase"); -jest.mock("./../Shared/Telemetry/TelemetryProcessor"); -import * as ko from "knockout"; -import { deleteDatabase } from "./../Common/dataAccess/deleteDatabase"; -import * as ViewModels from "./../Contracts/ViewModels"; -import Explorer from "./Explorer"; - -describe("Explorer.isLastDatabase() and Explorer.isLastNonEmptyDatabase()", () => { - let explorer: Explorer; - beforeAll(() => { - (deleteDatabase as jest.Mock).mockResolvedValue(undefined); - }); - - beforeEach(() => { - explorer = new Explorer(); - }); - - it("should be true if only 1 database", () => { - const database = {} as ViewModels.Database; - explorer.databases = ko.observableArray([database]); - expect(explorer.isLastDatabase()).toBe(true); - }); - - it("should be false if only 2 databases", () => { - const database = {} as ViewModels.Database; - const database2 = {} as ViewModels.Database; - explorer.databases = ko.observableArray([database, database2]); - expect(explorer.isLastDatabase()).toBe(false); - }); - - it("should be false if not last empty database", () => { - const database = {} as ViewModels.Database; - explorer.databases = ko.observableArray([database]); - expect(explorer.isLastNonEmptyDatabase()).toBe(false); - }); - - it("should be true if last non empty database", () => { - const database = {} as ViewModels.Database; - database.collections = ko.observableArray([{} as ViewModels.Collection]); - explorer.databases = ko.observableArray([database]); - expect(explorer.isLastNonEmptyDatabase()).toBe(true); - }); -}); diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 108f5df35..bb7ca660c 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -69,6 +69,7 @@ import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; import { ResourceTreeAdapter } from "./Tree/ResourceTreeAdapter"; import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; import StoredProcedure from "./Tree/StoredProcedure"; +import { useDatabases } from "./useDatabases"; BindingHandlersRegisterer.registerBindingHandlers(); // Hold a reference to ComponentRegisterer to prevent transpiler to ignore import @@ -81,12 +82,10 @@ export interface ExplorerParams { export default class Explorer { public isFixedCollectionWithSharedThroughputSupported: ko.Computed; public isAccountReady: ko.Observable; - public canSaveQueries: ko.Computed; public queriesClient: QueriesClient; public tableDataClient: TableDataClient; // Resource Tree - public databases: ko.ObservableArray; public selectedDatabaseId: ko.Computed; public selectedCollectionId: ko.Computed; public selectedNode: ko.Observable; @@ -168,26 +167,6 @@ export default class Explorer { this.resourceTokenCollection = ko.observable(); this.isSchemaEnabled = ko.computed(() => userContext.features.enableSchema); - this.databases = ko.observableArray(); - this.canSaveQueries = ko.computed(() => { - const savedQueriesDatabase: ViewModels.Database = _.find( - this.databases(), - (database: ViewModels.Database) => database.id() === Constants.SavedQueries.DatabaseName - ); - if (!savedQueriesDatabase) { - return false; - } - const savedQueriesCollection: ViewModels.Collection = - savedQueriesDatabase && - _.find( - savedQueriesDatabase.collections(), - (collection: ViewModels.Collection) => collection.id() === Constants.SavedQueries.CollectionName - ); - if (!savedQueriesCollection) { - return false; - } - return true; - }); this.selectedNode = ko.observable(); this.selectedNode.subscribe((nodeSelected: ViewModels.TreeNode) => { // Make sure switching tabs restores tabs display @@ -641,34 +620,14 @@ export default class Explorer { return null; } if (this.selectedNode().nodeKind === "Database") { - return _.find(this.databases(), (database: ViewModels.Database) => database.id() === this.selectedNode().id()); + return _.find( + useDatabases.getState().databases, + (database: ViewModels.Database) => database.id() === this.selectedNode().id() + ); } return this.findSelectedCollection().database; } - public findDatabaseWithId(databaseId: string): ViewModels.Database { - return _.find(this.databases(), (database: ViewModels.Database) => database.id() === databaseId); - } - - public isLastNonEmptyDatabase(): boolean { - if ( - this.isLastDatabase() && - this.databases()[0] && - this.databases()[0].collections && - this.databases()[0].collections().length > 0 - ) { - return true; - } - return false; - } - - public isLastDatabase(): boolean { - if (this.databases().length > 1) { - return false; - } - return true; - } - public isSelectedDatabaseShared(): boolean { const database = this.findSelectedDatabase(); if (!!database) { @@ -691,10 +650,11 @@ export default class Explorer { let loadCollectionPromises: Q.Promise[] = []; // If the user has a lot of databases, only load expanded databases. + const databases = useDatabases.getState().databases; const databasesToLoad = - this.databases().length <= Explorer.MaxNbDatabasesToAutoExpand - ? this.databases() - : this.databases().filter((db) => db.isDatabaseExpanded() || db.id() === Constants.SavedQueries.DatabaseName); + databases.length <= Explorer.MaxNbDatabasesToAutoExpand + ? databases + : databases.filter((db) => db.isDatabaseExpanded() || db.id() === Constants.SavedQueries.DatabaseName); const startKey: number = TelemetryProcessor.traceStart(Action.LoadCollections, { dataExplorerArea: Constants.Areas.ResourceTree, @@ -739,37 +699,16 @@ export default class Explorer { } } - public findCollection(databaseId: string, collectionId: string): ViewModels.Collection { - const database: ViewModels.Database = this.databases().find( - (database: ViewModels.Database) => database.id() === databaseId - ); - return database?.collections().find((collection: ViewModels.Collection) => collection.id() === collectionId); - } - - public isLastCollection(): boolean { - let collectionCount = 0; - if (this.databases().length == 0) { - return false; - } - for (let i = 0; i < this.databases().length; i++) { - const database = this.databases()[i]; - collectionCount += database.collections().length; - if (collectionCount > 1) { - return false; - } - } - return true; - } - private getDeltaDatabases( updatedDatabaseList: DataModels.Database[] ): { toAdd: ViewModels.Database[]; toDelete: ViewModels.Database[]; } { + const databases = useDatabases.getState().databases; const newDatabases: DataModels.Database[] = _.filter(updatedDatabaseList, (database: DataModels.Database) => { const databaseExists = _.some( - this.databases(), + databases, (existingDatabase: ViewModels.Database) => existingDatabase.id() === database.id ); return !databaseExists; @@ -779,7 +718,7 @@ export default class Explorer { ); let databasesToDelete: ViewModels.Database[] = []; - ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => { + ko.utils.arrayForEach(databases, (database: ViewModels.Database) => { const databasePresentInUpdatedList = _.some( updatedDatabaseList, (db: DataModels.Database) => db.id === database.id() @@ -793,24 +732,12 @@ export default class Explorer { } private addDatabasesToList(databases: ViewModels.Database[]): void { - this.databases( - this.databases() - .concat(databases) - .sort((database1, database2) => database1.id().localeCompare(database2.id())) - ); + useDatabases.getState().addDatabases(databases); } private deleteDatabasesFromList(databasesToRemove: ViewModels.Database[]): void { - const databasesToKeep: ViewModels.Database[] = []; - - ko.utils.arrayForEach(this.databases(), (database: ViewModels.Database) => { - const shouldRemoveDatabase = _.some(databasesToRemove, (db: ViewModels.Database) => db.id === database.id); - if (!shouldRemoveDatabase) { - databasesToKeep.push(database); - } - }); - - this.databases(databasesToKeep); + const deleteDatabase = useDatabases.getState().deleteDatabase; + databasesToRemove.forEach((database) => deleteDatabase(database)); } public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise { @@ -1414,34 +1341,6 @@ export default class Explorer { } } - public async loadDatabaseOffers(): Promise { - await Promise.all( - this.databases()?.map(async (database: ViewModels.Database) => { - await database.loadOffer(); - }) - ); - } - - public isFirstResourceCreated(): boolean { - const databases: ViewModels.Database[] = this.databases(); - - if (!databases || databases.length === 0) { - return false; - } - - return databases.some((database) => { - // user has created at least one collection - if (database.collections()?.length > 0) { - return true; - } - // user has created a database with shared throughput - if (database.offer()) { - return true; - } - // use has created an empty database without shared throughput - return false; - }); - } public openDeleteCollectionConfirmationPane(): void { useSidePanel .getState() @@ -1466,7 +1365,7 @@ export default class Explorer { } public async openAddCollectionPanel(databaseId?: string): Promise { - await this.loadDatabaseOffers(); + await useDatabases.getState().loadDatabaseOffers(); useSidePanel .getState() .openSidePanel("New " + getCollectionName(), ); diff --git a/src/Explorer/Panes/AddCollectionPanel.tsx b/src/Explorer/Panes/AddCollectionPanel.tsx index aa98e4113..a946c1091 100644 --- a/src/Explorer/Panes/AddCollectionPanel.tsx +++ b/src/Explorer/Panes/AddCollectionPanel.tsx @@ -31,6 +31,7 @@ import { getUpsellMessage } from "../../Utils/PricingUtils"; import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput"; import Explorer from "../Explorer"; +import { useDatabases } from "../useDatabases"; import { PanelFooterComponent } from "./PanelFooterComponent"; import { PanelInfoErrorComponent } from "./PanelInfoErrorComponent"; import { PanelLoadingScreen } from "./PanelLoadingScreen"; @@ -125,6 +126,8 @@ export class AddCollectionPanel extends React.Component {this.state.errorMessage && ( @@ -137,7 +140,7 @@ export class AddCollectionPanel extends React.Component (this.newDatabaseThroughput = throughput)} @@ -469,9 +470,7 @@ export class AddCollectionPanel extends React.Component (this.collectionThroughput = throughput)} @@ -680,7 +679,7 @@ export class AddCollectionPanel extends React.Component ({ + return useDatabases.getState().databases?.map((database) => ({ key: database.id(), text: database.id(), })); @@ -772,9 +771,9 @@ export class AddCollectionPanel extends React.Component database.id() === this.state.selectedDatabaseId); + const selectedDatabase = useDatabases + .getState() + .databases?.find((database) => database.id() === this.state.selectedDatabaseId); return !!selectedDatabase?.offer(); } diff --git a/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx index 9bd37368b..3ee59bd30 100644 --- a/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx +++ b/src/Explorer/Panes/AddDatabasePanel/AddDatabasePanel.tsx @@ -16,6 +16,7 @@ import { isServerlessAccount } from "../../../Utils/CapabilityUtils"; import { getUpsellMessage } from "../../../Utils/PricingUtils"; import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput"; import Explorer from "../../Explorer"; +import { useDatabases } from "../../useDatabases"; import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent"; import { getTextFieldStyles } from "../PanelStyles"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; @@ -172,7 +173,12 @@ export const AddDatabasePanel: FunctionComponent = ({ {!formErrors && isFreeTierAccount && ( = ({ {!isServerlessAccount() && databaseCreateNewShared && ( (throughput = newThroughput)} diff --git a/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.test.tsx b/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.test.tsx index e052a6616..834bbed18 100644 --- a/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.test.tsx +++ b/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.test.tsx @@ -1,14 +1,16 @@ import { mount } from "enzyme"; import * as ko from "knockout"; import React from "react"; +import { SavedQueries } from "../../../Common/Constants"; import { QueriesClient } from "../../../Common/QueriesClient"; import { Query } from "../../../Contracts/DataModels"; +import { Collection, Database } from "../../../Contracts/ViewModels"; import Explorer from "../../Explorer"; +import { useDatabases } from "../../useDatabases"; import { BrowseQueriesPane } from "./BrowseQueriesPane"; describe("Browse queries panel", () => { const fakeExplorer = {} as Explorer; - fakeExplorer.canSaveQueries = ko.computed(() => true); const fakeClientQuery = {} as QueriesClient; const fakeQueryData = [] as Query[]; fakeClientQuery.getQueries = async () => fakeQueryData; @@ -17,6 +19,16 @@ describe("Browse queries panel", () => { explorer: fakeExplorer, closePanel: (): void => undefined, }; + useDatabases.getState().addDatabases([ + { + id: ko.observable(SavedQueries.DatabaseName), + collections: ko.observableArray([ + { + id: ko.observable(SavedQueries.CollectionName), + } as Collection, + ]), + } as Database, + ]); it("Should render Default properly", () => { const wrapper = mount(); diff --git a/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx b/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx index 13ae70e44..9624b4539 100644 --- a/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx +++ b/src/Explorer/Panes/BrowseQueriesPane/BrowseQueriesPane.tsx @@ -13,6 +13,7 @@ import { } from "../../Controls/QueriesGridReactComponent/QueriesGridComponent"; import Explorer from "../../Explorer"; import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab"; +import { useDatabases } from "../../useDatabases"; interface BrowseQueriesPaneProps { explorer: Explorer; @@ -45,12 +46,13 @@ export const BrowseQueriesPane: FunctionComponent = ({ }); closeSidePanel(); }; + const isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled); const props: QueriesGridComponentProps = { queriesClient: explorer.queriesClient, onQuerySelect: loadSavedQuery, containerVisible: true, - saveQueryEnabled: explorer.canSaveQueries(), + saveQueryEnabled: isSaveQueryEnabled(), }; return ( diff --git a/src/Explorer/Panes/BrowseQueriesPane/__snapshots__/BrowseQueriesPane.test.tsx.snap b/src/Explorer/Panes/BrowseQueriesPane/__snapshots__/BrowseQueriesPane.test.tsx.snap index eed894482..5fcb72a63 100644 --- a/src/Explorer/Panes/BrowseQueriesPane/__snapshots__/BrowseQueriesPane.test.tsx.snap +++ b/src/Explorer/Panes/BrowseQueriesPane/__snapshots__/BrowseQueriesPane.test.tsx.snap @@ -5,7 +5,6 @@ exports[`Browse queries panel Should render Default properly 1`] = ` closePanel={[Function]} explorer={ Object { - "canSaveQueries": [Function], "queriesClient": Object { "getQueries": [Function], }, diff --git a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx index 3f829c191..7d0703336 100644 --- a/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx +++ b/src/Explorer/Panes/CassandraAddCollectionPane/CassandraAddCollectionPane.tsx @@ -12,6 +12,7 @@ import { isServerlessAccount } from "../../../Utils/CapabilityUtils"; import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput"; import Explorer from "../../Explorer"; import { CassandraAPIDataClient } from "../../Tables/TableDataClient"; +import { useDatabases } from "../../useDatabases"; import { getTextFieldStyles } from "../PanelStyles"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; @@ -236,7 +237,7 @@ export const CassandraAddCollectionPane: FunctionComponent ({ + options={useDatabases.getState().databases?.map((keyspace) => ({ key: keyspace.id(), text: keyspace.id(), data: { @@ -253,7 +254,9 @@ export const CassandraAddCollectionPane: FunctionComponent (newKeySpaceThroughput = throughput)} @@ -324,7 +327,7 @@ export const CassandraAddCollectionPane: FunctionComponent (tableThroughput = throughput)} diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx index c75f998e3..62aeade73 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.test.tsx @@ -11,44 +11,42 @@ import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryCons import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import { updateUserContext } from "../../../UserContext"; import Explorer from "../../Explorer"; +import { useDatabases } from "../../useDatabases"; import { DeleteCollectionConfirmationPane } from "./DeleteCollectionConfirmationPane"; describe("Delete Collection Confirmation Pane", () => { - describe("Explorer.isLastCollection()", () => { - let explorer: Explorer; - - beforeEach(() => { - explorer = new Explorer(); - }); + describe("useDatabases.isLastCollection()", () => { + beforeAll(() => useDatabases.getState().clearDatabases()); + afterEach(() => useDatabases.getState().clearDatabases()); it("should be true if 1 database and 1 collection", () => { - const database = {} as Database; - database.collections = ko.observableArray([{} as Collection]); - explorer.databases = ko.observableArray([database]); - expect(explorer.isLastCollection()).toBe(true); + const database = { id: ko.observable("testDB") } as Database; + database.collections = ko.observableArray([{ id: ko.observable("testCollection") } as Collection]); + useDatabases.getState().addDatabases([database]); + expect(useDatabases.getState().isLastCollection()).toBe(true); }); it("should be false if if 1 database and 2 collection", () => { - const database = {} as Database; - database.collections = ko.observableArray([{} as Collection, {} as Collection]); - explorer.databases = ko.observableArray([database]); - expect(explorer.isLastCollection()).toBe(false); + const database = { id: ko.observable("testDB") } as Database; + database.collections = ko.observableArray([ + { id: ko.observable("coll1") } as Collection, + { id: ko.observable("coll2") } as Collection, + ]); + useDatabases.getState().addDatabases([database]); + expect(useDatabases.getState().isLastCollection()).toBe(false); }); it("should be false if 2 database and 1 collection each", () => { - const database = {} as Database; - database.collections = ko.observableArray([{} as Collection]); - const database2 = {} as Database; - database2.collections = ko.observableArray([{} as Collection]); - explorer.databases = ko.observableArray([database, database2]); - expect(explorer.isLastCollection()).toBe(false); + const database = { id: ko.observable("testDB") } as Database; + database.collections = ko.observableArray([{ id: ko.observable("coll1") } as Collection]); + const database2 = { id: ko.observable("testDB2") } as Database; + database2.collections = ko.observableArray([{ id: ko.observable("coll2") } as Collection]); + useDatabases.getState().addDatabases([database, database2]); + expect(useDatabases.getState().isLastCollection()).toBe(false); }); it("should be false if 0 databases", () => { - const database = {} as Database; - explorer.databases = ko.observableArray(); - database.collections = ko.observableArray(); - expect(explorer.isLastCollection()).toBe(false); + expect(useDatabases.getState().isLastCollection()).toBe(false); }); }); @@ -56,7 +54,6 @@ describe("Delete Collection Confirmation Pane", () => { it("should return true if last collection and database does not have shared throughput else false", () => { const fakeExplorer = new Explorer(); fakeExplorer.refreshAllDatabases = () => undefined; - fakeExplorer.isLastCollection = () => true; fakeExplorer.isSelectedDatabaseShared = () => false; const props = { @@ -65,15 +62,15 @@ describe("Delete Collection Confirmation Pane", () => { collectionName: "container", }; const wrapper = shallow(); - expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true); - - props.explorer.isLastCollection = () => true; - props.explorer.isSelectedDatabaseShared = () => true; - wrapper.setProps(props); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false); - props.explorer.isLastCollection = () => false; - props.explorer.isSelectedDatabaseShared = () => false; + const database = { id: ko.observable("testDB") } as Database; + database.collections = ko.observableArray([{ id: ko.observable("testCollection") } as Collection]); + useDatabases.getState().addDatabases([database]); + wrapper.setProps(props); + expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true); + + props.explorer.isSelectedDatabaseShared = () => true; wrapper.setProps(props); expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false); }); @@ -94,8 +91,10 @@ describe("Delete Collection Confirmation Pane", () => { fakeExplorer.selectedCollectionId = ko.computed(() => selectedCollectionId); fakeExplorer.selectedNode = ko.observable(); fakeExplorer.refreshAllDatabases = () => undefined; - fakeExplorer.isLastCollection = () => true; fakeExplorer.isSelectedDatabaseShared = () => false; + const database = { id: ko.observable("testDB") } as Database; + database.collections = ko.observableArray([{ id: ko.observable("testCollection") } as Collection]); + useDatabases.getState().addDatabases([database]); beforeAll(() => { updateUserContext({ diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx index 8fad674bd..bec5db8a0 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/DeleteCollectionConfirmationPane.tsx @@ -13,7 +13,9 @@ import { userContext } from "../../../UserContext"; import { getCollectionName } from "../../../Utils/APITypeUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import Explorer from "../../Explorer"; +import { useDatabases } from "../../useDatabases"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; + export interface DeleteCollectionConfirmationPaneProps { explorer: Explorer; } @@ -22,13 +24,14 @@ export const DeleteCollectionConfirmationPane: FunctionComponent { const closeSidePanel = useSidePanel((state) => state.closeSidePanel); + const isLastCollection = useDatabases((state) => state.isLastCollection); const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState(""); const [inputCollectionName, setInputCollectionName] = useState(""); const [formError, setFormError] = useState(""); const [isExecuting, setIsExecuting] = useState(false); const shouldRecordFeedback = (): boolean => { - return explorer.isLastCollection() && !explorer.isSelectedDatabaseShared(); + return isLastCollection() && !explorer.isSelectedDatabaseShared(); }; const collectionName = getCollectionName().toLocaleLowerCase(); const paneTitle = "Delete " + collectionName; diff --git a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap index 59e62296b..af75f4fe6 100644 --- a/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap +++ b/src/Explorer/Panes/DeleteCollectionConfirmationPane/__snapshots__/DeleteCollectionConfirmationPane.test.tsx.snap @@ -7,7 +7,6 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect explorer={ Object { "findSelectedCollection": [Function], - "isLastCollection": [Function], "isSelectedDatabaseShared": [Function], "refreshAllDatabases": [Function], "selectedCollectionId": [Function], diff --git a/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.test.tsx b/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.test.tsx index f7082c8ff..b3195252d 100644 --- a/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.test.tsx +++ b/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.test.tsx @@ -11,19 +11,20 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { updateUserContext } from "../../UserContext"; import Explorer from "../Explorer"; +import { TabsManager } from "../Tabs/TabsManager"; +import { useDatabases } from "../useDatabases"; import { DeleteDatabaseConfirmationPanel } from "./DeleteDatabaseConfirmationPanel"; describe("Delete Database Confirmation Pane", () => { describe("shouldRecordFeedback()", () => { it("should return true if last non empty database or is last database that has shared throughput, else false", () => { - const fakeExplorer = new Explorer(); + const fakeExplorer = {} as Explorer; fakeExplorer.refreshAllDatabases = () => undefined; - fakeExplorer.isLastCollection = () => true; fakeExplorer.isSelectedDatabaseShared = () => false; const database = {} as Database; - database.collections = ko.observableArray([{} as Collection]); - database.id = ko.observable("testDatabse"); + database.collections = ko.observableArray([{ id: ko.observable("testCollection") } as Collection]); + database.id = ko.observable("testDatabase"); const props = { explorer: fakeExplorer, @@ -33,29 +34,26 @@ describe("Delete Database Confirmation Pane", () => { }; const wrapper = shallow(); - props.explorer.isLastNonEmptyDatabase = () => true; + expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false); + + useDatabases.getState().addDatabases([database]); + wrapper.setProps(props); expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(true); - - props.explorer.isLastNonEmptyDatabase = () => false; - props.explorer.isLastDatabase = () => false; - wrapper.setProps(props); - expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false); - - props.explorer.isLastNonEmptyDatabase = () => false; - props.explorer.isLastDatabase = () => true; - props.explorer.isSelectedDatabaseShared = () => false; - wrapper.setProps(props); - expect(wrapper.exists(".deleteDatabaseFeedback")).toBe(false); + useDatabases.getState().clearDatabases(); }); }); describe("submit()", () => { const selectedDatabaseId = "testDatabse"; - const fakeExplorer = new Explorer(); + const database = { id: ko.observable("testDatabase") } as Database; + database.collections = ko.observableArray([{ id: ko.observable("testCollection") } as Collection]); + database.id = ko.observable(selectedDatabaseId); + const fakeExplorer = {} as Explorer; fakeExplorer.refreshAllDatabases = () => undefined; - fakeExplorer.isLastCollection = () => true; fakeExplorer.isSelectedDatabaseShared = () => false; + fakeExplorer.tabsManager = new TabsManager(); + fakeExplorer.selectedNode = ko.observable(); let wrapper: ReactWrapper; beforeAll(() => { @@ -71,13 +69,10 @@ describe("Delete Database Confirmation Pane", () => { }); (deleteDatabase as jest.Mock).mockResolvedValue(undefined); (TelemetryProcessor.trace as jest.Mock).mockReturnValue(undefined); + useDatabases.getState().addDatabases([database]); }); beforeEach(() => { - const database = {} as Database; - database.collections = ko.observableArray([{} as Collection]); - database.id = ko.observable(selectedDatabaseId); - const props = { explorer: fakeExplorer, closePanel: (): void => undefined, @@ -86,10 +81,10 @@ describe("Delete Database Confirmation Pane", () => { }; wrapper = mount(); - props.explorer.isLastNonEmptyDatabase = () => true; - wrapper.setProps(props); }); + afterAll(() => useDatabases.getState().clearDatabases()); + it("Should call delete database", () => { expect(wrapper).toMatchSnapshot(); expect(wrapper.exists("#confirmDatabaseId")).toBe(true); diff --git a/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx b/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx index 56a9982b6..64cb7a211 100644 --- a/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx +++ b/src/Explorer/Panes/DeleteDatabaseConfirmationPanel.tsx @@ -13,6 +13,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; import Explorer from "../Explorer"; +import { useDatabases } from "../useDatabases"; import { PanelInfoErrorComponent, PanelInfoErrorProps } from "./PanelInfoErrorComponent"; import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm"; @@ -26,6 +27,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent { const closeSidePanel = useSidePanel((state) => state.closeSidePanel); + const isLastNonEmptyDatabase = useDatabases((state) => state.isLastNonEmptyDatabase); const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false); const [formError, setFormError] = useState(""); @@ -70,7 +72,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent { - return explorer.isLastNonEmptyDatabase() || (explorer.isLastDatabase() && explorer.isSelectedDatabaseShared()); - }; - const props: RightPaneFormProps = { formError, isExecuting: isLoading, @@ -134,7 +132,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent
- {shouldRecordFeedback() && ( + {isLastNonEmptyDatabase() && (
Help us improve Azure Cosmos DB! diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap index 047d8ae26..7ad772ba0 100644 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap @@ -19,8 +19,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = ` "container": Explorer { "_isInitializingNotebooks": false, "_resetNotebookWorkspace": [Function], - "canSaveQueries": [Function], - "databases": [Function], "isAccountReady": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function], "isNotebookEnabled": [Function], diff --git a/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.test.tsx b/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.test.tsx index 8edacc4a0..635483a49 100644 --- a/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.test.tsx +++ b/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.test.tsx @@ -1,32 +1,38 @@ import { shallow } from "enzyme"; import * as ko from "knockout"; import React from "react"; +import { SavedQueries } from "../../../Common/Constants"; +import { Collection, Database } from "../../../Contracts/ViewModels"; import Explorer from "../../Explorer"; +import { useDatabases } from "../../useDatabases"; import { SaveQueryPane } from "./SaveQueryPane"; describe("Save Query Pane", () => { const fakeExplorer = {} as Explorer; - fakeExplorer.canSaveQueries = ko.computed(() => true); const props = { explorer: fakeExplorer, closePanel: (): void => undefined, }; - const wrapper = shallow(); - - it("should return true if can save Queries else false", () => { - fakeExplorer.canSaveQueries = ko.computed(() => true); - wrapper.setProps(props); - expect(wrapper.exists("#saveQueryInput")).toBe(true); - - fakeExplorer.canSaveQueries = ko.computed(() => false); - wrapper.setProps(props); - expect(wrapper.exists("#saveQueryInput")).toBe(false); - }); - it("should render Default properly", () => { const wrapper = shallow(); + expect(wrapper.exists("#saveQueryInput")).toBe(false); expect(wrapper).toMatchSnapshot(); }); + + it("should return true if can save Queries else false", () => { + useDatabases.getState().addDatabases([ + { + id: ko.observable(SavedQueries.DatabaseName), + collections: ko.observableArray([ + { + id: ko.observable(SavedQueries.CollectionName), + } as Collection, + ]), + } as Database, + ]); + const wrapper = shallow(); + expect(wrapper.exists("#saveQueryInput")).toBe(true); + }); }); diff --git a/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.tsx b/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.tsx index ff5089d88..f852b09c2 100644 --- a/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.tsx +++ b/src/Explorer/Panes/SaveQueryPane/SaveQueryPane.tsx @@ -10,6 +10,7 @@ import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetr import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; import Explorer from "../../Explorer"; import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab"; +import { useDatabases } from "../../useDatabases"; import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm"; interface SaveQueryPaneProps { @@ -24,11 +25,11 @@ export const SaveQueryPane: FunctionComponent = ({ explorer const setupSaveQueriesText = `For compliance reasons, we save queries in a container in your Azure Cosmos account, in a separate database called “${SavedQueries.DatabaseName}”. To proceed, we need to create a container in your account, estimated additional cost is $0.77 daily.`; const title = "Save Query"; - const { canSaveQueries } = explorer; + const isSaveQueryEnabled = useDatabases((state) => state.isSaveQueryEnabled); const submit = async (): Promise => { setFormError(""); - if (!canSaveQueries()) { + if (!isSaveQueryEnabled()) { setFormError("Cannot save query"); logConsoleError("Failed to save query: account not setup to save queries"); } @@ -129,16 +130,16 @@ export const SaveQueryPane: FunctionComponent = ({ explorer const props: RightPaneFormProps = { formError: formError, isExecuting: isLoading, - submitButtonText: canSaveQueries() ? "Save" : "Complete setup", + submitButtonText: isSaveQueryEnabled() ? "Save" : "Complete setup", onSubmit: () => { - canSaveQueries() ? submit() : setupQueries(); + isSaveQueryEnabled() ? submit() : setupQueries(); }, }; return (
- {!canSaveQueries() ? ( + {!isSaveQueryEnabled() ? ( {setupSaveQueriesText} ) : ( Help us improve Azure Cosmos DB! @@ -759,7 +719,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database variant="small" > What is the reason why you are deleting this database? @@ -1067,11 +1027,11 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database className="ms-TextField-wrapper" >