Migrate Mongo Query Tab to React(#854)

Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
This commit is contained in:
vaidankarswapnil 2021-06-15 00:53:19 +05:30 committed by GitHub
parent 999fad3bad
commit 5da9724deb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 109 additions and 1408 deletions

View File

@ -143,11 +143,11 @@ src/Explorer/Tabs/DocumentsTab.test.ts
src/Explorer/Tabs/DocumentsTab.ts src/Explorer/Tabs/DocumentsTab.ts
src/Explorer/Tabs/GraphTab.ts src/Explorer/Tabs/GraphTab.ts
src/Explorer/Tabs/MongoDocumentsTab.ts src/Explorer/Tabs/MongoDocumentsTab.ts
src/Explorer/Tabs/MongoQueryTab.ts # src/Explorer/Tabs/MongoQueryTab.ts
src/Explorer/Tabs/MongoShellTab.ts src/Explorer/Tabs/MongoShellTab.ts
src/Explorer/Tabs/NotebookV2Tab.ts src/Explorer/Tabs/NotebookV2Tab.ts
src/Explorer/Tabs/QueryTab.test.ts # src/Explorer/Tabs/QueryTab.test.ts
src/Explorer/Tabs/QueryTab.ts # src/Explorer/Tabs/QueryTab.ts
src/Explorer/Tabs/QueryTablesTab.ts src/Explorer/Tabs/QueryTablesTab.ts
src/Explorer/Tabs/ScriptTabBase.ts src/Explorer/Tabs/ScriptTabBase.ts
src/Explorer/Tabs/StoredProcedureTab.ts src/Explorer/Tabs/StoredProcedureTab.ts

View File

@ -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<MinimalQueryIterator> {
let options: any = {};
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
this._iterator = queryIterator(this.collection.databaseId, this.collection, this.sqlStatementToExecute());
return Q(this._iterator);
}
}

View File

@ -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 <QueryTabComponent {...this.iMongoQueryTabComponentProps} />;
}
}

View File

@ -1,335 +0,0 @@
<div class="tab-pane" data-bind="attr:{id: tabId}" role="tabpanel">
<div class="tabPaneContentContainer">
<div class="mongoQueryHelper" data-bind="visible: isPreferredApiMongoDB && sqlQueryEditorContent().length === 0">
Start by writing a Mongo query, for example: <strong>{'id':'foo'}</strong> or <strong>{ }</strong> to get all the
documents.
</div>
<div class="warningErrorContainer" aria-live="assertive" data-bind="visible: maybeSubQuery">
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/info_color.svg" alt="Error" /></span>
<span class="warningErrorDetailsLinkContainer">
We have detected you may be using a subquery. Non-correlated subqueries are not currently supported.
<a href="https://docs.microsoft.com/en-us/azure/cosmos-db/sql-query-subquery"
>Please see Cosmos sub query documentation for further information</a
>
</span>
</div>
</div>
<div class="queryEditorWithSplitter" data-bind="attr: { id: queryEditorId }">
<editor
class="queryEditor"
data-bind="css: { mongoQueryEditor: isPreferredApiMongoDB }"
params="{
content: initialEditorContent,
contentType: monacoSettings.language,
isReadOnly: monacoSettings.readOnly,
lineNumbers: 'on',
ariaLabel: 'Editing Query',
updatedContent: sqlQueryEditorContent,
selectedContent: selectedContent
}"
></editor>
<!-- Splitter - Start -->
<div class="splitter ui-resizable-handle ui-resizable-s" data-bind="attr: { id: splitterId }">
<img class="queryEditorHorizontalSplitter" src="/HorizontalSplitter.svg" alt="Splitter" />
</div>
</div>
<!-- Splitter - End -->
<!-- Script for results metadata that is common to all APIs -->
<script type="text/html" id="result-metadata-template">
<span>
<span data-bind="text: showingDocumentsDisplayText"></span>
</span>
<span class="queryResultDivider" data-bind="visible: fetchNextPageButton.enabled"> | </span>
<span class="queryResultNextEnable" data-bind="visible: fetchNextPageButton.enabled">
<a data-bind="click: onFetchNextPageClick">
<span>Load more</span>
<img class="queryResultnextImg" src="/Query-Editor-Next.svg" alt="Fetch next page" />
</a>
</span>
</script>
<!-- Query Errors Tab - Start-->
<div class="active queryErrorsHeaderContainer" data-bind="visible: !!error()">
<span class="queryErrors" data-toggle="tab" data-bind="attr: { href: '#queryerrors' + tabId }">Errors</span>
</div>
<!-- Query Errors Tab - End -->
<!-- Query Results & Errors Content Container - Start-->
<div class="queryResultErrorContentContainer">
<div
class="queryEditorWatermark"
data-bind="visible: allResultsMetadata().length === 0 && !error() && !queryResults() && !isExecuting()"
>
<p><img src="/RunQuery.png" alt="Execute Query Watermark" /></p>
<p class="queryEditorWatermarkText">Execute a query to see the results</p>
</div>
<div
class="queryResultsErrorsContent"
data-bind="visible: allResultsMetadata().length > 0 || !!error() || queryResults()"
>
<div class="togglesWithMetadata" data-bind="visible: !error()">
<div
class="toggles"
aria-label="Successful execution"
id="execute-query-toggles"
data-bind="event: { keydown: onToggleKeyDown }"
tabindex="0"
>
<div class="tab">
<input type="radio" class="radio" value="result" />
<span
class="toggleSwitch"
role="button"
tabindex="0"
data-bind="click: toggleResult, css:{ selectedToggle: isResultToggled(), unselectedToggle: !isResultToggled() }"
aria-label="Results"
>Results</span
>
</div>
<div class="tab">
<input type="radio" class="radio" value="logs" />
<span
class="toggleSwitch"
role="button"
tabindex="0"
data-bind="click: toggleMetrics, css:{ selectedToggle: isMetricsToggled(), unselectedToggle: !isMetricsToggled() }"
aria-label="Query stats"
>Query Stats</span
>
</div>
</div>
<div
class="result-metadata"
data-bind="template: { name: 'result-metadata-template' }, visible: isResultToggled()"
></div>
</div>
<json-editor
params="{ content: queryResults, isReadOnly: true, ariaLabel: 'Query results' }"
data-bind="visible: queryResults() && queryResults().length > 0 && isResultToggled() && allResultsMetadata().length > 0 && !error()"
>
</json-editor>
<div
class="queryMetricsSummaryContainer"
data-bind="visible: isMetricsToggled() && allResultsMetadata().length > 0 && !error()"
>
<table class="queryMetricsSummary">
<caption>
Query Statistics
</caption>
<thead class="queryMetricsSummaryHead">
<tr class="queryMetricsSummaryHeader queryMetricsSummaryTuple">
<th title="METRIC" scope="col">METRIC</th>
<th title="VALUE" scope="col">VALUE</th>
</tr>
</thead>
<tbody class="queryMetricsSummaryBody" data-bind="with: aggregatedQueryMetrics">
<tr class="queryMetricsSummaryTuple">
<td title="Request Charge">Request Charge</td>
<td>
<span
data-bind="text: $parent.requestChargeDisplayText, attr: { title: $parent.requestChargeDisplayText }"
></span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple">
<td title="Showing Results">Showing Results</td>
<td>
<span
data-bind="text: $parent.showingDocumentsDisplayText, attr: { title: $parent.showingDocumentsDisplayText }"
></span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Retrieved document count">Retrieved document count</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total number of retrieved documents</span>
</span>
</td>
<td><span data-bind="text: retrievedDocumentCount, attr: { title: retrievedDocumentCount }"></span></td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Retrieved document size">Retrieved document size</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total size of retrieved documents in bytes</span>
</span>
</td>
<td>
<span data-bind="text: retrievedDocumentSize, attr: { title: retrievedDocumentSize }"></span>
<span>bytes</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Output document count">Output document count</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Number of output documents</span>
</span>
</td>
<td><span data-bind="text: outputDocumentCount, attr: { title: outputDocumentCount }"></span></td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Output document size">Output document size</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total size of output documents in bytes</span>
</span>
</td>
<td>
<span data-bind="text: outputDocumentSize, attr: { title: outputDocumentSize }"></span>
<span>bytes</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Index hit document count">Index hit document count</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total number of documents matched by the filter</span>
</span>
</td>
<td><span data-bind="text: indexHitDocumentCount, attr: { title: indexHitDocumentCount }"></span></td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Index lookup time">Index lookup time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Time spent in physical index layer</span>
</span>
</td>
<td>
<span data-bind="text: indexLookupTime, attr: { title: indexLookupTime }"></span> <span>ms</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Document load time">Document load time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Time spent in loading documents</span>
</span>
</td>
<td>
<span data-bind="text: documentLoadTime, attr: { title: documentLoadTime }"></span> <span>ms</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Query engine execution time">Query engine execution time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText queryEngineExeTimeInfo"
>Time spent by the query engine to execute the query expression (excludes other execution times
like load documents or write results)</span
>
</span>
</td>
<td>
<span
data-bind="text: runtimeExecutionTimes.queryEngineExecutionTime, attr: { title: runtimeExecutionTimes.queryEngineExecutionTime }"
></span>
<span>ms</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="System function execution time">System function execution time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total time spent executing system (built-in) functions</span>
</span>
</td>
<td>
<span
data-bind="text: runtimeExecutionTimes.systemFunctionExecutionTime, attr: { title: runtimeExecutionTimes.systemFunctionExecutionTime }"
></span>
<span>ms</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="User defined function execution time">User defined function execution time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Total time spent executing user-defined functions</span>
</span>
</td>
<td>
<span
data-bind="text: runtimeExecutionTimes.userDefinedFunctionExecutionTime, attr: { title: runtimeExecutionTimes.userDefinedFunctionExecutionTime }"
></span>
<span>ms</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.isQueryMetricsEnabled">
<td>
<span title="Document write time">Document write time</span>
<span class="queryMetricInfoTooltip" role="tooltip" tabindex="0">
<img class="infoImg" src="/info-bubble.svg" alt="More information" />
<span class="queryMetricTooltipText">Time spent to write query result set to response buffer</span>
</span>
</td>
<td>
<span data-bind="text: documentWriteTime, attr: { title: documentWriteTime }"></span> <span>ms</span>
</td>
</tr>
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.roundTrips() != null">
<td title="Round Trips">Round Trips</td>
<td><span data-bind="text: $parent.roundTrips, attr: { title: $parent.roundTrips }"></span></td>
</tr>
<!-- TODO: Report activity id for mongo queries -->
<tr class="queryMetricsSummaryTuple" data-bind="visible: $parent.activityId() != null">
<td title="Activity id">Activity id</td>
<td></td>
<td><span data-bind="text: $parent.activityId, attr: { title: $parent.activityId }"></span></td>
</tr>
</tbody>
</table>
<div class="downloadMetricsLinkContainer" data-bind="visible: $parent.isQueryMetricsEnabled">
<a
id="downloadMetricsLink"
role="button"
tabindex="0"
data-bind="event: { click: onDownloadQueryMetricsCsvClick, keypress: onDownloadQueryMetricsCsvKeyPress }"
>
<img class="downloadCsvImg" src="/DownloadQuery.svg" alt="download query metrics csv" />
<span>Per-partition query metrics (CSV)</span>
</a>
</div>
</div>
<!-- Query Errors Content - Start-->
<div
class="tab-pane active"
data-bind="
id: {
href: 'queryerrors' + tabId
},
visible: !!error()"
>
<div class="errorContent">
<span class="errorMessage" data-bind="text: error"></span>
<span class="errorDetailsLink">
<a
data-bind="click: $parent.onErrorDetailsClick, event: { keypress: $parent.onErrorDetailsKeyPress }"
id="error-display"
tabindex="0"
aria-label="Error details link"
>More details</a
>
</span>
</div>
</div>
<!-- Query Errors Content - End-->
</div>
</div>
<!-- Results & Errors Content Container - End-->
</div>
</div>

View File

@ -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;
}
}
}
}
}

View File

@ -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<string>("test"),
isDatabaseShared: () => false,
} as ViewModels.Database;
const collection = {
container: container,
databaseId: "test",
id: ko.observable<string>("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<string>("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);
});
});
});

View File

@ -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<string>;
public maybeSubQuery: ko.Computed<boolean>;
public sqlQueryEditorContent: ko.Observable<string>;
public selectedContent: ko.Observable<string>;
public sqlStatementToExecute: ko.Observable<string>;
public queryResults: ko.Observable<string>;
public error: ko.Observable<string>;
public statusMessge: ko.Observable<string>;
public statusIcon: ko.Observable<string>;
public allResultsMetadata: ko.ObservableArray<ViewModels.QueryResultsMetadata>;
public showingDocumentsDisplayText: ko.Observable<string>;
public requestChargeDisplayText: ko.Observable<string>;
public isTemplateReady: ko.Observable<boolean>;
public splitterId: string;
public splitter: Splitter;
public isPreferredApiMongoDB: boolean;
public queryMetrics: ko.Observable<Map<string, DataModels.QueryMetrics>>;
public aggregatedQueryMetrics: ko.Observable<DataModels.QueryMetrics>;
public activityId: ko.Observable<string>;
public roundTrips: ko.Observable<number>;
public toggleState: ko.Observable<ToggleState>;
public isQueryMetricsEnabled: ko.Computed<boolean>;
protected monacoSettings: ViewModels.MonacoEditorSettings;
private _executeQueryButtonTitle: ko.Observable<string>;
protected _iterator: MinimalQueryIterator;
private _isSaveQueriesEnabled: ko.Computed<boolean>;
private _resourceTokenPartitionKey: string;
_partitionKey: DataModels.PartitionKey;
constructor(options: ViewModels.QueryTabOptions) {
super(options);
this.queryEditorId = `queryeditor${this.tabId}`;
this.showingDocumentsDisplayText = ko.observable<string>();
this.requestChargeDisplayText = ko.observable<string>();
const defaultQueryText = options.queryText != void 0 ? options.queryText : "SELECT * FROM c";
this.initialEditorContent = ko.observable<string>(defaultQueryText);
this.sqlQueryEditorContent = ko.observable<string>(defaultQueryText);
this._executeQueryButtonTitle = ko.observable<string>("Execute Query");
this.selectedContent = ko.observable<string>();
this.selectedContent.subscribe((selectedContent: string) => {
if (!selectedContent.trim()) {
this._executeQueryButtonTitle("Execute Query");
} else {
this._executeQueryButtonTitle("Execute Selection");
}
});
this.sqlStatementToExecute = ko.observable<string>("");
this.queryResults = ko.observable<string>("");
this.statusMessge = ko.observable<string>();
this.statusIcon = ko.observable<string>();
this.allResultsMetadata = ko.observableArray<ViewModels.QueryResultsMetadata>([]);
this.error = ko.observable<string>();
this._partitionKey = options.partitionKey;
this._resourceTokenPartitionKey = options.resourceTokenPartitionKey;
this.splitterId = this.tabId + "_splitter";
this.isPreferredApiMongoDB = false;
this.aggregatedQueryMetrics = ko.observable<DataModels.QueryMetrics>();
this._resetAggregateQueryMetrics();
this.queryMetrics = ko.observable<Map<string, DataModels.QueryMetrics>>(new Map());
this.queryMetrics.subscribe((metrics) => this.aggregatedQueryMetrics(this._aggregateQueryMetrics(metrics)));
this.isQueryMetricsEnabled = ko.computed<boolean>(() => {
return userContext.apiType === "SQL" || false;
});
this.activityId = ko.observable<string>();
this.roundTrips = ko.observable<number>();
this.toggleState = ko.observable<ToggleState>(ToggleState.Result);
this.monacoSettings = new ViewModels.MonacoEditorSettings("sql", false);
this.executeQueryButton = {
enabled: ko.computed<boolean>(() => {
return !!this.sqlQueryEditorContent() && this.sqlQueryEditorContent().length > 0;
}),
visible: ko.computed<boolean>(() => {
return true;
}),
};
this._isSaveQueriesEnabled = ko.computed<boolean>(() => {
const container = this.collection && this.collection.container;
return userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
});
this.maybeSubQuery = ko.computed<boolean>(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<boolean>(() => {
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<boolean>(() => {
return true;
}),
};
this._buildCommandBarOptions();
}
public onTabClick(): void {
super.onTabClick();
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Query);
}
public onExecuteQueryClick = async (): Promise<void> => {
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<void> {
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<any> {
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<void> {
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<string, DataModels.QueryMetrics>): 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();
}
}

View File

@ -31,6 +31,7 @@ export class NewQueryTab extends TabsBase {
onTabAccessor: (instance: ITabAccessor): void => { onTabAccessor: (instance: ITabAccessor): void => {
this.iTabAccessor = instance; this.iTabAccessor = instance;
}, },
isPreferredApiMongoDB: false,
}; };
} }
@ -45,7 +46,9 @@ export class NewQueryTab extends TabsBase {
public onCloseTabButtonClick(): void { public onCloseTabButtonClick(): void {
this.manager?.closeTab(this); this.manager?.closeTab(this);
this.iTabAccessor.onCloseClickEvent(true); if (this.iTabAccessor) {
this.iTabAccessor.onCloseClickEvent(true);
}
} }
public getContainer(): Explorer { public getContainer(): Explorer {

View File

@ -3,8 +3,9 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { updateUserContext } from "../../UserContext"; import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import DocumentId from "../Tree/DocumentId"; import DocumentId from "../Tree/DocumentId";
import { container } from "./../Controls/Settings/TestUtils";
import DocumentsTab from "./DocumentsTab"; import DocumentsTab from "./DocumentsTab";
import QueryTab from "./QueryTab"; import { NewQueryTab } from "./QueryTab/QueryTab";
import { TabsManager } from "./TabsManager"; import { TabsManager } from "./TabsManager";
describe("Tabs manager tests", () => { describe("Tabs manager tests", () => {
@ -12,10 +13,10 @@ describe("Tabs manager tests", () => {
let explorer: Explorer; let explorer: Explorer;
let database: ViewModels.Database; let database: ViewModels.Database;
let collection: ViewModels.Collection; let collection: ViewModels.Collection;
let queryTab: QueryTab; let queryTab: NewQueryTab;
let documentsTab: DocumentsTab; let documentsTab: DocumentsTab;
beforeAll(() => { beforeEach(() => {
explorer = new Explorer(); explorer = new Explorer();
updateUserContext({ updateUserContext({
databaseAccount: { databaseAccount: {
@ -45,14 +46,22 @@ describe("Tabs manager tests", () => {
collection.isCollectionExpanded = ko.observable<boolean>(true); collection.isCollectionExpanded = ko.observable<boolean>(true);
collection.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>(); collection.selectedSubnodeKind = ko.observable<ViewModels.CollectionTabKind>();
queryTab = new QueryTab({ queryTab = new NewQueryTab(
tabKind: ViewModels.CollectionTabKind.Query, {
collection, tabKind: ViewModels.CollectionTabKind.Query,
database, collection,
title: "", database,
tabPath: "", title: "",
hashLocation: "", tabPath: "",
}); hashLocation: "",
queryText: "",
partitionKey: collection.partitionKey,
onLoadStartKey: 1,
},
{
container: container,
}
);
documentsTab = new DocumentsTab({ documentsTab = new DocumentsTab({
partitionKey: undefined, partitionKey: undefined,

View File

@ -28,7 +28,7 @@ import ConflictsTab from "../Tabs/ConflictsTab";
import DocumentsTab from "../Tabs/DocumentsTab"; import DocumentsTab from "../Tabs/DocumentsTab";
import GraphTab from "../Tabs/GraphTab"; import GraphTab from "../Tabs/GraphTab";
import MongoDocumentsTab from "../Tabs/MongoDocumentsTab"; import MongoDocumentsTab from "../Tabs/MongoDocumentsTab";
import MongoQueryTab from "../Tabs/MongoQueryTab"; import { NewMongoQueryTab } from "../Tabs/MongoQueryTab/MongoQueryTab";
import MongoShellTab from "../Tabs/MongoShellTab"; import MongoShellTab from "../Tabs/MongoShellTab";
import { NewQueryTab } from "../Tabs/QueryTab/QueryTab"; import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
import QueryTablesTab from "../Tabs/QueryTablesTab"; import QueryTablesTab from "../Tabs/QueryTablesTab";
@ -648,18 +648,24 @@ export default class Collection implements ViewModels.Collection {
tabTitle: title, tabTitle: title,
}); });
const mongoQueryTab: MongoQueryTab = new MongoQueryTab({ const newMongoQueryTab: NewMongoQueryTab = new NewMongoQueryTab(
tabKind: ViewModels.CollectionTabKind.Query, {
title: title, tabKind: ViewModels.CollectionTabKind.Query,
tabPath: "", title: title,
collection: this, tabPath: "",
node: this, collection: this,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoQuery`, node: this,
partitionKey: collection.partitionKey, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoQuery`,
onLoadStartKey: startKey, partitionKey: collection.partitionKey,
}); onLoadStartKey: startKey,
},
{
container: this.container,
viewModelcollection: this,
}
);
this.container.tabsManager.activateNewTab(mongoQueryTab); this.container.tabsManager.activateNewTab(newMongoQueryTab);
} }
public onNewGraphClick() { public onNewGraphClick() {

View File

@ -7,7 +7,7 @@ import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import DocumentsTab from "../Tabs/DocumentsTab"; import DocumentsTab from "../Tabs/DocumentsTab";
import QueryTab from "../Tabs/QueryTab"; import { NewQueryTab } from "../Tabs/QueryTab/QueryTab";
import TabsBase from "../Tabs/TabsBase"; import TabsBase from "../Tabs/TabsBase";
import DocumentId from "./DocumentId"; import DocumentId from "./DocumentId";
@ -85,20 +85,22 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
tabTitle: title, tabTitle: title,
}); });
const queryTab: QueryTab = new QueryTab({ this.container.tabsManager.activateNewTab(
tabKind: ViewModels.CollectionTabKind.Query, new NewQueryTab(
title: title, {
tabPath: "", tabKind: ViewModels.CollectionTabKind.Query,
collection: this, title: title,
node: this, tabPath: "",
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/query`, collection: this,
queryText: queryText, node: this,
partitionKey: collection.partitionKey, hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/query`,
resourceTokenPartitionKey: userContext.parsedResourceToken.partitionKey, queryText: queryText,
onLoadStartKey: startKey, partitionKey: collection.partitionKey,
}); onLoadStartKey: startKey,
},
this.container.tabsManager.activateNewTab(queryTab); { container: this.container }
)
);
} }
public onDocumentDBDocumentsClick() { public onDocumentDBDocumentsClick() {

View File

@ -45,7 +45,6 @@ import "./Explorer/Panes/PanelComponent.less";
import { SidePanel } from "./Explorer/Panes/PanelContainerComponent"; import { SidePanel } from "./Explorer/Panes/PanelContainerComponent";
import { SplashScreen } from "./Explorer/SplashScreen/SplashScreen"; import { SplashScreen } from "./Explorer/SplashScreen/SplashScreen";
import "./Explorer/SplashScreen/SplashScreen.less"; import "./Explorer/SplashScreen/SplashScreen.less";
import "./Explorer/Tabs/QueryTab.less";
import { Tabs } from "./Explorer/Tabs/Tabs"; import { Tabs } from "./Explorer/Tabs/Tabs";
import { useConfig } from "./hooks/useConfig"; import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer"; import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";