mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-11-25 15:06:55 +00:00
Migrate Mongo Query Tab to React(#854)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
This commit is contained in:
parent
999fad3bad
commit
5da9724deb
@ -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
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
47
src/Explorer/Tabs/MongoQueryTab/MongoQueryTab.tsx
Normal file
47
src/Explorer/Tabs/MongoQueryTab/MongoQueryTab.tsx
Normal 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} />;
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,8 +46,10 @@ export class NewQueryTab extends TabsBase {
|
|||||||
|
|
||||||
public onCloseTabButtonClick(): void {
|
public onCloseTabButtonClick(): void {
|
||||||
this.manager?.closeTab(this);
|
this.manager?.closeTab(this);
|
||||||
|
if (this.iTabAccessor) {
|
||||||
this.iTabAccessor.onCloseClickEvent(true);
|
this.iTabAccessor.onCloseClickEvent(true);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public getContainer(): Explorer {
|
public getContainer(): Explorer {
|
||||||
return this.props.container;
|
return this.props.container;
|
||||||
|
@ -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,
|
tabKind: ViewModels.CollectionTabKind.Query,
|
||||||
collection,
|
collection,
|
||||||
database,
|
database,
|
||||||
title: "",
|
title: "",
|
||||||
tabPath: "",
|
tabPath: "",
|
||||||
hashLocation: "",
|
hashLocation: "",
|
||||||
});
|
queryText: "",
|
||||||
|
partitionKey: collection.partitionKey,
|
||||||
|
onLoadStartKey: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
container: container,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
documentsTab = new DocumentsTab({
|
documentsTab = new DocumentsTab({
|
||||||
partitionKey: undefined,
|
partitionKey: undefined,
|
||||||
|
@ -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,7 +648,8 @@ 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,
|
tabKind: ViewModels.CollectionTabKind.Query,
|
||||||
title: title,
|
title: title,
|
||||||
tabPath: "",
|
tabPath: "",
|
||||||
@ -657,9 +658,14 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoQuery`,
|
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/mongoQuery`,
|
||||||
partitionKey: collection.partitionKey,
|
partitionKey: collection.partitionKey,
|
||||||
onLoadStartKey: startKey,
|
onLoadStartKey: startKey,
|
||||||
});
|
},
|
||||||
|
{
|
||||||
|
container: this.container,
|
||||||
|
viewModelcollection: this,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
this.container.tabsManager.activateNewTab(mongoQueryTab);
|
this.container.tabsManager.activateNewTab(newMongoQueryTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onNewGraphClick() {
|
public onNewGraphClick() {
|
||||||
|
@ -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,7 +85,9 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
|||||||
tabTitle: title,
|
tabTitle: title,
|
||||||
});
|
});
|
||||||
|
|
||||||
const queryTab: QueryTab = new QueryTab({
|
this.container.tabsManager.activateNewTab(
|
||||||
|
new NewQueryTab(
|
||||||
|
{
|
||||||
tabKind: ViewModels.CollectionTabKind.Query,
|
tabKind: ViewModels.CollectionTabKind.Query,
|
||||||
title: title,
|
title: title,
|
||||||
tabPath: "",
|
tabPath: "",
|
||||||
@ -94,11 +96,11 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
|||||||
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/query`,
|
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/query`,
|
||||||
queryText: queryText,
|
queryText: queryText,
|
||||||
partitionKey: collection.partitionKey,
|
partitionKey: collection.partitionKey,
|
||||||
resourceTokenPartitionKey: userContext.parsedResourceToken.partitionKey,
|
|
||||||
onLoadStartKey: startKey,
|
onLoadStartKey: startKey,
|
||||||
});
|
},
|
||||||
|
{ container: this.container }
|
||||||
this.container.tabsManager.activateNewTab(queryTab);
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onDocumentDBDocumentsClick() {
|
public onDocumentDBDocumentsClick() {
|
||||||
|
@ -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";
|
||||||
|
Loading…
Reference in New Issue
Block a user