mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-19 17:01:13 +00:00
Implement query copilot UI (#1452)
This commit is contained in:
495
src/Explorer/Tabs/QueryTab/QueryResultSection.tsx
Normal file
495
src/Explorer/Tabs/QueryTab/QueryResultSection.tsx
Normal file
@@ -0,0 +1,495 @@
|
||||
import {
|
||||
DetailsList,
|
||||
DetailsListLayoutMode,
|
||||
IColumn,
|
||||
Pivot,
|
||||
PivotItem,
|
||||
SelectionMode,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@fluentui/react";
|
||||
import { HttpHeaders, NormalizedEventKey } from "Common/Constants";
|
||||
import MongoUtility from "Common/MongoUtility";
|
||||
import { InfoTooltip } from "Common/Tooltip/InfoTooltip";
|
||||
import { QueryMetrics } from "Contracts/DataModels";
|
||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||
import { IDocument } from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
||||
import { userContext } from "UserContext";
|
||||
import { useNotificationConsole } from "hooks/useNotificationConsole";
|
||||
import React from "react";
|
||||
import DownloadQueryMetrics from "../../../../images/DownloadQuery.svg";
|
||||
import QueryEditorNext from "../../../../images/Query-Editor-Next.svg";
|
||||
import RunQuery from "../../../../images/RunQuery.png";
|
||||
import InfoColor from "../../../../images/info_color.svg";
|
||||
import { QueryResults } from "../../../Contracts/ViewModels";
|
||||
|
||||
interface QueryResultProps {
|
||||
isMongoDB: boolean;
|
||||
queryEditorContent: string;
|
||||
error: string;
|
||||
isExecuting: boolean;
|
||||
queryResults: QueryResults;
|
||||
executeQueryDocumentsPage: (firstItemIndex: number) => Promise<void>;
|
||||
}
|
||||
|
||||
export const QueryResultSection: React.FC<QueryResultProps> = ({
|
||||
isMongoDB,
|
||||
queryEditorContent,
|
||||
error,
|
||||
queryResults,
|
||||
isExecuting,
|
||||
executeQueryDocumentsPage,
|
||||
}: QueryResultProps): JSX.Element => {
|
||||
const queryMetrics = React.useRef(queryResults?.headers?.[HttpHeaders.queryMetrics]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const latestQueryMetrics = queryResults?.headers?.[HttpHeaders.queryMetrics];
|
||||
if (latestQueryMetrics && Object.keys(latestQueryMetrics).length > 0) {
|
||||
queryMetrics.current = latestQueryMetrics;
|
||||
}
|
||||
}, [queryResults]);
|
||||
|
||||
const onRender = (item: IDocument): JSX.Element => (
|
||||
<>
|
||||
<InfoTooltip>{`${item.toolTip}`}</InfoTooltip>
|
||||
<Text style={{ paddingLeft: 10, margin: 0 }}>{`${item.metric}`}</Text>
|
||||
</>
|
||||
);
|
||||
const columns: IColumn[] = [
|
||||
{
|
||||
key: "column2",
|
||||
name: "METRIC",
|
||||
minWidth: 200,
|
||||
data: String,
|
||||
fieldName: "metric",
|
||||
onRender,
|
||||
},
|
||||
{
|
||||
key: "column3",
|
||||
name: "VALUE",
|
||||
minWidth: 200,
|
||||
data: String,
|
||||
fieldName: "value",
|
||||
},
|
||||
];
|
||||
const maybeSubQuery = queryEditorContent && /.*\(.*SELECT.*\)/i.test(queryEditorContent);
|
||||
const queryResultsString = queryResults
|
||||
? isMongoDB
|
||||
? MongoUtility.tojson(queryResults.documents, undefined, false)
|
||||
: JSON.stringify(queryResults.documents, undefined, 4)
|
||||
: "";
|
||||
|
||||
const onErrorDetailsClick = (): boolean => {
|
||||
useNotificationConsole.getState().expandConsole();
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const onErrorDetailsKeyPress = (event: React.KeyboardEvent<HTMLAnchorElement>): boolean => {
|
||||
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||
onErrorDetailsClick();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const onDownloadQueryMetricsCsvClick = (): boolean => {
|
||||
downloadQueryMetricsCsvData();
|
||||
return false;
|
||||
};
|
||||
|
||||
const onDownloadQueryMetricsCsvKeyPress = (event: React.KeyboardEvent<HTMLAnchorElement>): boolean => {
|
||||
if (event.key === NormalizedEventKey.Space || NormalizedEventKey.Enter) {
|
||||
downloadQueryMetricsCsvData();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const downloadQueryMetricsCsvData = (): void => {
|
||||
const csvData: string = 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();
|
||||
}
|
||||
};
|
||||
|
||||
const getAggregatedQueryMetrics = (): QueryMetrics => {
|
||||
const aggregatedQueryMetrics = {
|
||||
documentLoadTime: 0,
|
||||
documentWriteTime: 0,
|
||||
indexHitDocumentCount: 0,
|
||||
outputDocumentCount: 0,
|
||||
outputDocumentSize: 0,
|
||||
indexLookupTime: 0,
|
||||
retrievedDocumentCount: 0,
|
||||
retrievedDocumentSize: 0,
|
||||
vmExecutionTime: 0,
|
||||
runtimeExecutionTimes: {
|
||||
queryEngineExecutionTime: 0,
|
||||
systemFunctionExecutionTime: 0,
|
||||
userDefinedFunctionExecutionTime: 0,
|
||||
},
|
||||
totalQueryExecutionTime: 0,
|
||||
} as QueryMetrics;
|
||||
|
||||
if (queryMetrics.current) {
|
||||
Object.keys(queryMetrics.current).forEach((partitionKeyRangeId) => {
|
||||
const queryMetricsPerPartition = queryMetrics.current[partitionKeyRangeId];
|
||||
if (!queryMetricsPerPartition) {
|
||||
return;
|
||||
}
|
||||
aggregatedQueryMetrics.documentLoadTime += queryMetricsPerPartition.documentLoadTime?.totalMilliseconds() || 0;
|
||||
aggregatedQueryMetrics.documentWriteTime +=
|
||||
queryMetricsPerPartition.documentWriteTime?.totalMilliseconds() || 0;
|
||||
aggregatedQueryMetrics.indexHitDocumentCount += queryMetricsPerPartition.indexHitDocumentCount || 0;
|
||||
aggregatedQueryMetrics.outputDocumentCount += queryMetricsPerPartition.outputDocumentCount || 0;
|
||||
aggregatedQueryMetrics.outputDocumentSize += queryMetricsPerPartition.outputDocumentSize || 0;
|
||||
aggregatedQueryMetrics.indexLookupTime += queryMetricsPerPartition.indexLookupTime?.totalMilliseconds() || 0;
|
||||
aggregatedQueryMetrics.retrievedDocumentCount += queryMetricsPerPartition.retrievedDocumentCount || 0;
|
||||
aggregatedQueryMetrics.retrievedDocumentSize += queryMetricsPerPartition.retrievedDocumentSize || 0;
|
||||
aggregatedQueryMetrics.vmExecutionTime += queryMetricsPerPartition.vmExecutionTime?.totalMilliseconds() || 0;
|
||||
aggregatedQueryMetrics.totalQueryExecutionTime +=
|
||||
queryMetricsPerPartition.totalQueryExecutionTime?.totalMilliseconds() || 0;
|
||||
aggregatedQueryMetrics.runtimeExecutionTimes.queryEngineExecutionTime +=
|
||||
queryMetricsPerPartition.runtimeExecutionTimes?.queryEngineExecutionTime?.totalMilliseconds() || 0;
|
||||
aggregatedQueryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime +=
|
||||
queryMetricsPerPartition.runtimeExecutionTimes?.systemFunctionExecutionTime?.totalMilliseconds() || 0;
|
||||
aggregatedQueryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime +=
|
||||
queryMetricsPerPartition.runtimeExecutionTimes?.userDefinedFunctionExecutionTime?.totalMilliseconds() || 0;
|
||||
});
|
||||
}
|
||||
|
||||
return aggregatedQueryMetrics;
|
||||
};
|
||||
|
||||
const generateQueryMetricsCsvData = (): string => {
|
||||
if (queryMetrics.current) {
|
||||
let csvData =
|
||||
[
|
||||
"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";
|
||||
|
||||
Object.keys(queryMetrics.current).forEach((partitionKeyRangeId) => {
|
||||
const queryMetricsPerPartition = queryMetrics.current[partitionKeyRangeId];
|
||||
csvData +=
|
||||
[
|
||||
partitionKeyRangeId,
|
||||
queryMetricsPerPartition.retrievedDocumentCount,
|
||||
queryMetricsPerPartition.retrievedDocumentSize,
|
||||
queryMetricsPerPartition.outputDocumentCount,
|
||||
queryMetricsPerPartition.outputDocumentSize,
|
||||
queryMetricsPerPartition.indexHitDocumentCount,
|
||||
queryMetricsPerPartition.indexLookupTime?.totalMilliseconds(),
|
||||
queryMetricsPerPartition.documentLoadTime?.totalMilliseconds(),
|
||||
queryMetricsPerPartition.runtimeExecutionTimes?.queryEngineExecutionTime?.totalMilliseconds(),
|
||||
queryMetricsPerPartition.runtimeExecutionTimes?.systemFunctionExecutionTime?.totalMilliseconds(),
|
||||
queryMetricsPerPartition.runtimeExecutionTimes?.userDefinedFunctionExecutionTime?.totalMilliseconds(),
|
||||
queryMetricsPerPartition.documentWriteTime?.totalMilliseconds(),
|
||||
].join(",") + "\n";
|
||||
});
|
||||
|
||||
return csvData;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const onFetchNextPageClick = async (): Promise<void> => {
|
||||
const { firstItemIndex, itemCount } = queryResults;
|
||||
await executeQueryDocumentsPage(firstItemIndex + itemCount - 1);
|
||||
};
|
||||
|
||||
const generateQueryStatsItems = (): IDocument[] => {
|
||||
const items: IDocument[] = [
|
||||
{
|
||||
metric: "Request Charge",
|
||||
value: `${queryResults.requestCharge} RUs`,
|
||||
toolTip: "Request Charge",
|
||||
},
|
||||
{
|
||||
metric: "Showing Results",
|
||||
value: queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`,
|
||||
toolTip: "Showing Results",
|
||||
},
|
||||
];
|
||||
|
||||
if (userContext.apiType === "SQL") {
|
||||
const aggregatedQueryMetrics = getAggregatedQueryMetrics();
|
||||
items.push(
|
||||
{
|
||||
metric: "Retrieved document count",
|
||||
value: aggregatedQueryMetrics.retrievedDocumentCount?.toString() || "",
|
||||
toolTip: "Total number of retrieved documents",
|
||||
},
|
||||
{
|
||||
metric: "Retrieved document size",
|
||||
value: `${aggregatedQueryMetrics.retrievedDocumentSize?.toString() || 0} bytes`,
|
||||
toolTip: "Total size of retrieved documents in bytes",
|
||||
},
|
||||
{
|
||||
metric: "Output document count",
|
||||
value: aggregatedQueryMetrics.outputDocumentCount?.toString() || "",
|
||||
toolTip: "Number of output documents",
|
||||
},
|
||||
{
|
||||
metric: "Output document size",
|
||||
value: `${aggregatedQueryMetrics.outputDocumentSize?.toString() || 0} bytes`,
|
||||
toolTip: "Total size of output documents in bytes",
|
||||
},
|
||||
{
|
||||
metric: "Index hit document count",
|
||||
value: aggregatedQueryMetrics.indexHitDocumentCount?.toString() || "",
|
||||
toolTip: "Total number of documents matched by the filter",
|
||||
},
|
||||
{
|
||||
metric: "Index lookup time",
|
||||
value: `${aggregatedQueryMetrics.indexLookupTime?.toString() || 0} ms`,
|
||||
toolTip: "Time spent in physical index layer",
|
||||
},
|
||||
{
|
||||
metric: "Document load time",
|
||||
value: `${aggregatedQueryMetrics.documentLoadTime?.toString() || 0} ms`,
|
||||
toolTip: "Time spent in loading documents",
|
||||
},
|
||||
{
|
||||
metric: "Query engine execution time",
|
||||
value: `${aggregatedQueryMetrics.runtimeExecutionTimes?.queryEngineExecutionTime?.toString() || 0} ms`,
|
||||
toolTip:
|
||||
"Time spent by the query engine to execute the query expression (excludes other execution times like load documents or write results)",
|
||||
},
|
||||
{
|
||||
metric: "System function execution time",
|
||||
value: `${aggregatedQueryMetrics.runtimeExecutionTimes?.systemFunctionExecutionTime?.toString() || 0} ms`,
|
||||
toolTip: "Total time spent executing system (built-in) functions",
|
||||
},
|
||||
{
|
||||
metric: "User defined function execution time",
|
||||
value: `${
|
||||
aggregatedQueryMetrics.runtimeExecutionTimes?.userDefinedFunctionExecutionTime?.toString() || 0
|
||||
} ms`,
|
||||
toolTip: "Total time spent executing user-defined functions",
|
||||
},
|
||||
{
|
||||
metric: "Document write time",
|
||||
value: `${aggregatedQueryMetrics.documentWriteTime.toString() || 0} ms`,
|
||||
toolTip: "Time spent to write query result set to response buffer",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (queryResults.roundTrips) {
|
||||
items.push({
|
||||
metric: "Round Trips",
|
||||
value: queryResults.roundTrips?.toString(),
|
||||
toolTip: "Number of round trips",
|
||||
});
|
||||
}
|
||||
|
||||
if (queryResults.activityId) {
|
||||
items.push({
|
||||
metric: "Activity id",
|
||||
value: queryResults.activityId,
|
||||
toolTip: "",
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack style={{ height: "100%" }}>
|
||||
{isMongoDB && queryEditorContent.length === 0 && (
|
||||
<div className="mongoQueryHelper">
|
||||
Start by writing a Mongo query, for example: <strong>{"{'id':'foo'}"}</strong> or{" "}
|
||||
<strong>
|
||||
{"{ "}
|
||||
{" }"}
|
||||
</strong>{" "}
|
||||
to get all the documents.
|
||||
</div>
|
||||
)}
|
||||
{maybeSubQuery && (
|
||||
<div className="warningErrorContainer" aria-live="assertive">
|
||||
<div className="warningErrorContent">
|
||||
<span>
|
||||
<img className="paneErrorIcon" src={InfoColor} alt="Error" />
|
||||
</span>
|
||||
<span className="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>
|
||||
)}
|
||||
{/* <!-- Query Errors Tab - Start--> */}
|
||||
{error && (
|
||||
<div className="active queryErrorsHeaderContainer">
|
||||
<span className="queryErrors" data-toggle="tab">
|
||||
Errors
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* <!-- Query Errors Tab - End --> */}
|
||||
{/* <!-- Query Results & Errors Content Container - Start--> */}
|
||||
<div className="queryResultErrorContentContainer">
|
||||
{!queryResults && !error && !isExecuting && (
|
||||
<div className="queryEditorWatermark">
|
||||
<p>
|
||||
<img src={RunQuery} alt="Execute Query Watermark" />
|
||||
</p>
|
||||
<p className="queryEditorWatermarkText">Execute a query to see the results</p>
|
||||
</div>
|
||||
)}
|
||||
{(queryResults || !!error) && (
|
||||
<div className="queryResultsErrorsContent">
|
||||
{!error && (
|
||||
<Pivot aria-label="Successful execution" style={{ height: "100%" }}>
|
||||
<PivotItem
|
||||
headerText="Results"
|
||||
headerButtonProps={{
|
||||
"data-order": 1,
|
||||
"data-title": "Results",
|
||||
}}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<div className="result-metadata">
|
||||
<span>
|
||||
<span>
|
||||
{queryResults.itemCount > 0
|
||||
? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}`
|
||||
: `0 - 0`}
|
||||
</span>
|
||||
</span>
|
||||
{queryResults.hasMoreResults && (
|
||||
<>
|
||||
<span className="queryResultDivider">|</span>
|
||||
<span className="queryResultNextEnable">
|
||||
<a onClick={() => onFetchNextPageClick()}>
|
||||
<span>Load more</span>
|
||||
<img className="queryResultnextImg" src={QueryEditorNext} alt="Fetch next page" />
|
||||
</a>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{queryResults && queryResultsString?.length > 0 && !error && (
|
||||
<div
|
||||
style={{
|
||||
paddingBottom: "100px",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<EditorReact
|
||||
language={"json"}
|
||||
content={queryResultsString}
|
||||
isReadOnly={true}
|
||||
ariaLabel={"Query results"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="Query Stats"
|
||||
headerButtonProps={{
|
||||
"data-order": 2,
|
||||
"data-title": "Query Stats",
|
||||
}}
|
||||
style={{ height: "100%", overflowY: "scroll" }}
|
||||
>
|
||||
{queryResults && !error && (
|
||||
<div className="queryMetricsSummaryContainer">
|
||||
<div className="queryMetricsSummary">
|
||||
<h5>Query Statistics</h5>
|
||||
<DetailsList
|
||||
items={generateQueryStatsItems()}
|
||||
columns={columns}
|
||||
selectionMode={SelectionMode.none}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
compact={true}
|
||||
/>
|
||||
</div>
|
||||
{userContext.apiType === "SQL" && (
|
||||
<div className="downloadMetricsLinkContainer">
|
||||
<a
|
||||
id="downloadMetricsLink"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onDownloadQueryMetricsCsvClick()}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) =>
|
||||
onDownloadQueryMetricsCsvKeyPress(event)
|
||||
}
|
||||
>
|
||||
<img
|
||||
className="downloadCsvImg"
|
||||
src={DownloadQueryMetrics}
|
||||
alt="download query metrics csv"
|
||||
/>
|
||||
<span>Per-partition query metrics (CSV)</span>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
)}
|
||||
{/* <!-- Query Errors Content - Start--> */}
|
||||
{!!error && (
|
||||
<div className="tab-pane active">
|
||||
<div className="errorContent">
|
||||
<span className="errorMessage">{error}</span>
|
||||
<span className="errorDetailsLink">
|
||||
<a
|
||||
onClick={() => onErrorDetailsClick()}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) => onErrorDetailsKeyPress(event)}
|
||||
id="error-display"
|
||||
tabIndex={0}
|
||||
aria-label="Error details link"
|
||||
>
|
||||
More details
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* <!-- Query Errors Content - End--> */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,30 +1,22 @@
|
||||
import { DetailsList, DetailsListLayoutMode, IColumn, Pivot, PivotItem, SelectionMode, Text } from "@fluentui/react";
|
||||
import { FeedOptions } from "@azure/cosmos";
|
||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||
import React, { Fragment } from "react";
|
||||
import SplitterLayout from "react-splitter-layout";
|
||||
import "react-splitter-layout/lib/index.css";
|
||||
import DownloadQueryMetrics from "../../../../images/DownloadQuery.svg";
|
||||
import ExecuteQueryIcon from "../../../../images/ExecuteQuery.svg";
|
||||
import InfoColor from "../../../../images/info_color.svg";
|
||||
import QueryEditorNext from "../../../../images/Query-Editor-Next.svg";
|
||||
import RunQuery from "../../../../images/RunQuery.png";
|
||||
import SaveQueryIcon from "../../../../images/save-cosmos.svg";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
|
||||
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
||||
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
|
||||
import * as HeadersUtility from "../../../Common/HeadersUtility";
|
||||
import { MinimalQueryIterator } from "../../../Common/IteratorUtilities";
|
||||
import { queryIterator } from "../../../Common/MongoProxyClient";
|
||||
import MongoUtility from "../../../Common/MongoUtility";
|
||||
import { Splitter } from "../../../Common/Splitter";
|
||||
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
|
||||
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
|
||||
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useNotificationConsole } from "../../../hooks/useNotificationConsole";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import * as QueryUtils from "../../../Utils/QueryUtils";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import { EditorReact } from "../../Controls/Editor/EditorReact";
|
||||
import Explorer from "../../Explorer";
|
||||
@@ -43,7 +35,6 @@ export interface IDocument {
|
||||
metric: string;
|
||||
value: string;
|
||||
toolTip: string;
|
||||
isQueryMetricsEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface ITabAccessor {
|
||||
@@ -74,30 +65,13 @@ export interface IQueryTabComponentProps {
|
||||
}
|
||||
|
||||
interface IQueryTabStates {
|
||||
queryMetrics: Map<string, DataModels.QueryMetrics>;
|
||||
aggregatedQueryMetrics: DataModels.QueryMetrics;
|
||||
activityId: string;
|
||||
roundTrips: number;
|
||||
toggleState: ToggleState;
|
||||
isQueryMetricsEnabled: boolean;
|
||||
showingDocumentsDisplayText: string;
|
||||
requestChargeDisplayText: string;
|
||||
initialEditorContent: string;
|
||||
sqlQueryEditorContent: string;
|
||||
selectedContent: string;
|
||||
_executeQueryButtonTitle: string;
|
||||
sqlStatementToExecute: string;
|
||||
queryResults: string;
|
||||
statusMessge: string;
|
||||
statusIcon: string;
|
||||
allResultsMetadata: ViewModels.QueryResultsMetadata[];
|
||||
queryResults: ViewModels.QueryResults;
|
||||
error: string;
|
||||
isTemplateReady: boolean;
|
||||
_isSaveQueriesEnabled: boolean;
|
||||
isExecutionError: boolean;
|
||||
isExecuting: boolean;
|
||||
columns: IColumn[];
|
||||
items: IDocument[];
|
||||
}
|
||||
|
||||
export default class QueryTabComponent extends React.Component<IQueryTabComponentProps, IQueryTabStates> {
|
||||
@@ -105,90 +79,39 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
public executeQueryButton: Button;
|
||||
public saveQueryButton: Button;
|
||||
public splitterId: string;
|
||||
public splitter: Splitter;
|
||||
public isPreferredApiMongoDB: boolean;
|
||||
public resultsDisplay: string;
|
||||
protected monacoSettings: ViewModels.MonacoEditorSettings;
|
||||
protected _iterator: MinimalQueryIterator;
|
||||
private _resourceTokenPartitionKey: string;
|
||||
_partitionKey: DataModels.PartitionKey;
|
||||
public maybeSubQuery: boolean;
|
||||
public isCloseClicked: boolean;
|
||||
public allItems: IDocument[];
|
||||
public defaultQueryText: string;
|
||||
private _iterator: MinimalQueryIterator;
|
||||
|
||||
constructor(props: IQueryTabComponentProps) {
|
||||
super(props);
|
||||
const columns: IColumn[] = [
|
||||
{
|
||||
key: "column2",
|
||||
name: "METRIC",
|
||||
minWidth: 200,
|
||||
data: String,
|
||||
fieldName: "metric",
|
||||
onRender: this.onRenderColumnItem,
|
||||
},
|
||||
{
|
||||
key: "column3",
|
||||
name: "VALUE",
|
||||
minWidth: 200,
|
||||
data: String,
|
||||
fieldName: "value",
|
||||
},
|
||||
];
|
||||
|
||||
if (this.props.isPreferredApiMongoDB) {
|
||||
this.defaultQueryText = props.queryText;
|
||||
} else {
|
||||
this.defaultQueryText = props.queryText !== void 0 ? props.queryText : "SELECT * FROM c";
|
||||
}
|
||||
|
||||
this.state = {
|
||||
queryMetrics: new Map(),
|
||||
aggregatedQueryMetrics: undefined,
|
||||
activityId: "",
|
||||
roundTrips: undefined,
|
||||
toggleState: ToggleState.Result,
|
||||
isQueryMetricsEnabled: userContext.apiType === "SQL" || false,
|
||||
showingDocumentsDisplayText: this.resultsDisplay,
|
||||
requestChargeDisplayText: "",
|
||||
initialEditorContent: this.defaultQueryText,
|
||||
sqlQueryEditorContent: this.defaultQueryText,
|
||||
sqlQueryEditorContent: props.queryText || "SELECT * FROM c",
|
||||
selectedContent: "",
|
||||
_executeQueryButtonTitle: "Execute Query",
|
||||
sqlStatementToExecute: this.defaultQueryText,
|
||||
queryResults: "",
|
||||
statusMessge: "",
|
||||
statusIcon: "",
|
||||
allResultsMetadata: [],
|
||||
queryResults: undefined,
|
||||
error: "",
|
||||
isTemplateReady: false,
|
||||
_isSaveQueriesEnabled: userContext.apiType === "SQL" || userContext.apiType === "Gremlin",
|
||||
isExecutionError: this.props.isExecutionError,
|
||||
isExecuting: false,
|
||||
columns: columns,
|
||||
items: [],
|
||||
};
|
||||
this.isCloseClicked = false;
|
||||
this.splitterId = this.props.tabId + "_splitter";
|
||||
this.queryEditorId = `queryeditor${this.props.tabId}`;
|
||||
this._partitionKey = props.partitionKey;
|
||||
this.isPreferredApiMongoDB = this.props.isPreferredApiMongoDB;
|
||||
this.monacoSettings = new ViewModels.MonacoEditorSettings(this.props.monacoEditorSetting, false);
|
||||
|
||||
this.executeQueryButton = {
|
||||
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
|
||||
visible: true,
|
||||
};
|
||||
const sql = this.state.sqlQueryEditorContent;
|
||||
this.maybeSubQuery = sql && /.*\(.*SELECT.*\)/i.test(sql);
|
||||
|
||||
const isSaveQueryBtnEnabled = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||
this.saveQueryButton = {
|
||||
enabled: this.state._isSaveQueriesEnabled,
|
||||
visible: this.state._isSaveQueriesEnabled,
|
||||
enabled: isSaveQueryBtnEnabled,
|
||||
visible: isSaveQueryBtnEnabled,
|
||||
};
|
||||
|
||||
this._buildCommandBarOptions();
|
||||
this.props.tabsBaseInstance.updateNavbarWithTabsButtons();
|
||||
props.onTabAccessor({
|
||||
onTabClickEvent: this.onTabClick.bind(this),
|
||||
onSaveClickEvent: this.getCurrentEditorQuery.bind(this),
|
||||
@@ -196,165 +119,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
});
|
||||
}
|
||||
|
||||
public onRenderColumnItem(item: IDocument): JSX.Element {
|
||||
if (item.toolTip !== "") {
|
||||
return (
|
||||
<>
|
||||
<InfoTooltip>{`${item.toolTip}`}</InfoTooltip>
|
||||
<Text style={{ paddingLeft: 10, margin: 0 }}>{`${item.metric}`}</Text>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public generateDetailsList(): IDocument[] {
|
||||
const items: IDocument[] = [];
|
||||
const allItems: IDocument[] = [
|
||||
{
|
||||
metric: "Request Charge",
|
||||
value: this.state.requestChargeDisplayText,
|
||||
toolTip: "Request Charge",
|
||||
isQueryMetricsEnabled: true,
|
||||
},
|
||||
{
|
||||
metric: "Showing Results",
|
||||
value: this.state.showingDocumentsDisplayText,
|
||||
toolTip: "Showing Results",
|
||||
isQueryMetricsEnabled: true,
|
||||
},
|
||||
{
|
||||
metric: "Retrieved document count",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.retrievedDocumentCount !== undefined
|
||||
? this.state.aggregatedQueryMetrics.retrievedDocumentCount.toString()
|
||||
: "",
|
||||
toolTip: "Total number of retrieved documents",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "Retrieved document size",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.retrievedDocumentSize !== undefined
|
||||
? this.state.aggregatedQueryMetrics.retrievedDocumentSize.toString() + " bytes"
|
||||
: "",
|
||||
toolTip: "Total size of retrieved documents in bytes",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "Output document count",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.outputDocumentCount !== undefined
|
||||
? this.state.aggregatedQueryMetrics.outputDocumentCount.toString()
|
||||
: "",
|
||||
toolTip: "Number of output documents",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "Output document size",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.outputDocumentSize !== undefined
|
||||
? this.state.aggregatedQueryMetrics.outputDocumentSize.toString() + " bytes"
|
||||
: "",
|
||||
toolTip: "Total size of output documents in bytes",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "Index hit document count",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.indexHitDocumentCount !== undefined
|
||||
? this.state.aggregatedQueryMetrics.indexHitDocumentCount.toString()
|
||||
: "",
|
||||
toolTip: "Total number of documents matched by the filter",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "Index lookup time",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.indexLookupTime !== undefined
|
||||
? this.state.aggregatedQueryMetrics.indexLookupTime.toString() + " ms"
|
||||
: "",
|
||||
toolTip: "Time spent in physical index layer",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "Document load time",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.documentLoadTime !== undefined
|
||||
? this.state.aggregatedQueryMetrics.documentLoadTime.toString() + " ms"
|
||||
: "",
|
||||
toolTip: "Time spent in loading documents",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "Query engine execution time",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.runtimeExecutionTimes.queryEngineExecutionTime !== undefined
|
||||
? this.state.aggregatedQueryMetrics.runtimeExecutionTimes.queryEngineExecutionTime.toString() + " ms"
|
||||
: "",
|
||||
toolTip:
|
||||
"Time spent by the query engine to execute the query expression (excludes other execution times like load documents or write results)",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "System function execution time",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime !== undefined
|
||||
? this.state.aggregatedQueryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime.toString() + " ms"
|
||||
: "",
|
||||
toolTip: "Total time spent executing system (built-in) functions",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "User defined function execution time",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime !== undefined
|
||||
? this.state.aggregatedQueryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime.toString() +
|
||||
" ms"
|
||||
: "",
|
||||
toolTip: "Total time spent executing user-defined functions",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "Document write time",
|
||||
value:
|
||||
this.state.aggregatedQueryMetrics.documentWriteTime !== undefined
|
||||
? this.state.aggregatedQueryMetrics.documentWriteTime.toString() + " ms"
|
||||
: "",
|
||||
toolTip: "Time spent to write query result set to response buffer",
|
||||
isQueryMetricsEnabled: this.state.isQueryMetricsEnabled,
|
||||
},
|
||||
{
|
||||
metric: "Round Trips",
|
||||
value: this.state.roundTrips ? this.state.roundTrips.toString() : "",
|
||||
toolTip: "",
|
||||
isQueryMetricsEnabled: true,
|
||||
},
|
||||
{
|
||||
metric: "Activity id",
|
||||
value: this.state.activityId ? this.state.activityId : "",
|
||||
toolTip: "",
|
||||
isQueryMetricsEnabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
allItems.forEach((item) => {
|
||||
if (item.metric === "Round Trips" || item.metric === "Activity id") {
|
||||
if (item.metric === "Round Trips" && this.state.roundTrips !== undefined) {
|
||||
items.push(item);
|
||||
} else if (item.metric === "Activity id" && this.state.activityId !== undefined) {
|
||||
items.push(item);
|
||||
}
|
||||
} else {
|
||||
if (item.isQueryMetricsEnabled) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
public onCloseClick(isClicked: boolean): void {
|
||||
this.isCloseClicked = isClicked;
|
||||
}
|
||||
@@ -372,14 +136,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
}
|
||||
|
||||
public onExecuteQueryClick = async (): Promise<void> => {
|
||||
const sqlStatement = this.state.selectedContent || this.state.sqlQueryEditorContent;
|
||||
|
||||
this.setState({
|
||||
sqlStatementToExecute: sqlStatement,
|
||||
allResultsMetadata: [],
|
||||
queryResults: "",
|
||||
});
|
||||
|
||||
this._iterator = undefined;
|
||||
setTimeout(async () => {
|
||||
await this._executeQueryDocumentsPage(0);
|
||||
@@ -396,31 +152,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
.openSidePanel("Open Saved Queries", <BrowseQueriesPane explorer={this.props.collection.container} />);
|
||||
};
|
||||
|
||||
public async onFetchNextPageClick(): Promise<void> {
|
||||
const allResultsMetadata = (this.state.allResultsMetadata && this.state.allResultsMetadata) || [];
|
||||
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
|
||||
const firstResultIndex: number = (metadata && Number(metadata.firstItemIndex)) || 1;
|
||||
const itemCount: number = (metadata && Number(metadata.itemCount)) || 0;
|
||||
|
||||
await this._executeQueryDocumentsPage(firstResultIndex + itemCount - 1);
|
||||
}
|
||||
|
||||
//eslint-disable-next-line
|
||||
public onErrorDetailsClick = (): boolean => {
|
||||
useNotificationConsole.getState().expandConsole();
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
public onErrorDetailsKeyPress = (event: React.KeyboardEvent<HTMLAnchorElement>): boolean => {
|
||||
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||
this.onErrorDetailsClick();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
public toggleResult(): void {
|
||||
this.setState({
|
||||
toggleState: ToggleState.Result,
|
||||
@@ -452,54 +183,30 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
focusElement && focusElement.focus();
|
||||
}
|
||||
|
||||
public isResultToggled(): boolean {
|
||||
return this.state.toggleState === ToggleState.Result;
|
||||
}
|
||||
|
||||
public isMetricsToggled(): boolean {
|
||||
return this.state.toggleState === ToggleState.QueryMetrics;
|
||||
}
|
||||
|
||||
public onDownloadQueryMetricsCsvClick = (): boolean => {
|
||||
this._downloadQueryMetricsCsvData();
|
||||
return false;
|
||||
};
|
||||
|
||||
public onDownloadQueryMetricsCsvKeyPress = (event: React.KeyboardEvent<HTMLAnchorElement>): boolean => {
|
||||
if (event.key === NormalizedEventKey.Space || NormalizedEventKey.Enter) {
|
||||
this._downloadQueryMetricsCsvData();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
//eslint-disable-next-line
|
||||
private async _executeQueryDocumentsPage(firstItemIndex: number): Promise<any> {
|
||||
this.setState({
|
||||
error: "",
|
||||
roundTrips: undefined,
|
||||
});
|
||||
|
||||
private async _executeQueryDocumentsPage(firstItemIndex: number): Promise<void> {
|
||||
if (this._iterator === undefined) {
|
||||
if (this.isPreferredApiMongoDB) {
|
||||
this._initIteratorMongo();
|
||||
} else {
|
||||
this._initIterator();
|
||||
}
|
||||
this._iterator = this.props.isPreferredApiMongoDB
|
||||
? queryIterator(
|
||||
this.props.collection.databaseId,
|
||||
this.props.viewModelcollection,
|
||||
this.state.selectedContent || this.state.sqlQueryEditorContent
|
||||
)
|
||||
: queryDocuments(
|
||||
this.props.collection.databaseId,
|
||||
this.props.collection.id(),
|
||||
this.state.selectedContent || this.state.sqlQueryEditorContent,
|
||||
{ enableCrossPartitionQuery: HeadersUtility.shouldEnableCrossPartitionKey() } as FeedOptions
|
||||
);
|
||||
}
|
||||
|
||||
await this._queryDocumentsPage(firstItemIndex);
|
||||
}
|
||||
|
||||
private async _queryDocumentsPage(firstItemIndex: number): Promise<void> {
|
||||
let results: string;
|
||||
|
||||
this.props.tabsBaseInstance.isExecutionError(false);
|
||||
this.setState({
|
||||
isExecutionError: false,
|
||||
});
|
||||
this._resetAggregateQueryMetrics();
|
||||
|
||||
const queryDocuments = async (firstItemIndex: number) =>
|
||||
await queryDocumentsPage(this.props.collection && this.props.collection.id(), this._iterator, firstItemIndex);
|
||||
@@ -513,44 +220,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
firstItemIndex,
|
||||
queryDocuments
|
||||
);
|
||||
const allResultsMetadata = (this.state.allResultsMetadata && this.state.allResultsMetadata) || [];
|
||||
const metadata: ViewModels.QueryResultsMetadata = allResultsMetadata[allResultsMetadata.length - 1];
|
||||
const resultsMetadata: ViewModels.QueryResultsMetadata = {
|
||||
hasMoreResults: queryResults.hasMoreResults,
|
||||
itemCount: queryResults.itemCount,
|
||||
firstItemIndex: queryResults.firstItemIndex,
|
||||
lastItemIndex: queryResults.lastItemIndex,
|
||||
};
|
||||
this.state.allResultsMetadata.push(resultsMetadata);
|
||||
|
||||
this.setState({
|
||||
activityId: queryResults.activityId,
|
||||
roundTrips: queryResults.roundTrips,
|
||||
});
|
||||
|
||||
const documents = queryResults.documents;
|
||||
if (this.isPreferredApiMongoDB) {
|
||||
results = MongoUtility.tojson(documents, undefined, false);
|
||||
} else {
|
||||
results = this.props.tabsBaseInstance.renderObjectForEditor(documents, undefined, 4);
|
||||
}
|
||||
|
||||
const resultsDisplay: string =
|
||||
queryResults.itemCount > 0 ? `${queryResults.firstItemIndex} - ${queryResults.lastItemIndex}` : `0 - 0`;
|
||||
|
||||
this.setState({
|
||||
showingDocumentsDisplayText: resultsDisplay,
|
||||
requestChargeDisplayText: `${queryResults.requestCharge} RUs`,
|
||||
queryResults: results,
|
||||
});
|
||||
|
||||
this._updateQueryMetricsMap(queryResults.headers[Constants.HttpHeaders.queryMetrics]);
|
||||
|
||||
if (queryResults.itemCount === 0 && metadata !== undefined && metadata.itemCount >= 0) {
|
||||
// we let users query for the next page because the SDK sometimes specifies there are more elements
|
||||
// even though there aren't any so we should not update the prior query results.
|
||||
return;
|
||||
}
|
||||
this.setState({ queryResults });
|
||||
} catch (error) {
|
||||
this.props.tabsBaseInstance.isExecutionError(true);
|
||||
this.setState({
|
||||
@@ -571,228 +241,10 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
}
|
||||
}
|
||||
|
||||
private _updateQueryMetricsMap(metricsMap: { [partitionKeyRange: string]: DataModels.QueryMetrics }): void {
|
||||
if (!metricsMap) {
|
||||
this.allItems = this.generateDetailsList();
|
||||
this.setState({
|
||||
items: this.allItems,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(metricsMap).forEach((key: string) => {
|
||||
this.state.queryMetrics.set(key, metricsMap[key]);
|
||||
});
|
||||
|
||||
this._aggregateQueryMetrics(this.state.queryMetrics);
|
||||
this.allItems = this.generateDetailsList();
|
||||
this.setState({
|
||||
items: this.allItems,
|
||||
});
|
||||
}
|
||||
|
||||
private _aggregateQueryMetrics(metricsMap: Map<string, DataModels.QueryMetrics>): DataModels.QueryMetrics {
|
||||
if (!metricsMap) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const aggregatedMetrics: DataModels.QueryMetrics = this.state.aggregatedQueryMetrics;
|
||||
metricsMap.forEach((queryMetrics) => {
|
||||
if (queryMetrics) {
|
||||
aggregatedMetrics.documentLoadTime =
|
||||
this._normalize(queryMetrics.documentLoadTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.documentLoadTime);
|
||||
aggregatedMetrics.documentWriteTime =
|
||||
this._normalize(queryMetrics.documentWriteTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.documentWriteTime);
|
||||
aggregatedMetrics.indexHitDocumentCount =
|
||||
this._normalize(queryMetrics.indexHitDocumentCount) +
|
||||
this._normalize(aggregatedMetrics.indexHitDocumentCount);
|
||||
aggregatedMetrics.outputDocumentCount =
|
||||
this._normalize(queryMetrics.outputDocumentCount) + this._normalize(aggregatedMetrics.outputDocumentCount);
|
||||
aggregatedMetrics.outputDocumentSize =
|
||||
this._normalize(queryMetrics.outputDocumentSize) + this._normalize(aggregatedMetrics.outputDocumentSize);
|
||||
aggregatedMetrics.indexLookupTime =
|
||||
this._normalize(queryMetrics.indexLookupTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.indexLookupTime);
|
||||
aggregatedMetrics.retrievedDocumentCount =
|
||||
this._normalize(queryMetrics.retrievedDocumentCount) +
|
||||
this._normalize(aggregatedMetrics.retrievedDocumentCount);
|
||||
aggregatedMetrics.retrievedDocumentSize =
|
||||
this._normalize(queryMetrics.retrievedDocumentSize) +
|
||||
this._normalize(aggregatedMetrics.retrievedDocumentSize);
|
||||
aggregatedMetrics.vmExecutionTime =
|
||||
this._normalize(queryMetrics.vmExecutionTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.vmExecutionTime);
|
||||
aggregatedMetrics.totalQueryExecutionTime =
|
||||
this._normalize(queryMetrics.totalQueryExecutionTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.totalQueryExecutionTime);
|
||||
|
||||
aggregatedMetrics.runtimeExecutionTimes.queryEngineExecutionTime =
|
||||
this._normalize(queryMetrics.runtimeExecutionTimes.queryEngineExecutionTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.runtimeExecutionTimes.queryEngineExecutionTime);
|
||||
aggregatedMetrics.runtimeExecutionTimes.systemFunctionExecutionTime =
|
||||
this._normalize(queryMetrics.runtimeExecutionTimes.systemFunctionExecutionTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.runtimeExecutionTimes.systemFunctionExecutionTime);
|
||||
aggregatedMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime =
|
||||
this._normalize(queryMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime.totalMilliseconds()) +
|
||||
this._normalize(aggregatedMetrics.runtimeExecutionTimes.userDefinedFunctionExecutionTime);
|
||||
}
|
||||
});
|
||||
|
||||
return aggregatedMetrics;
|
||||
}
|
||||
|
||||
public _downloadQueryMetricsCsvData(): void {
|
||||
const csvData: string = this._generateQueryMetricsCsvData();
|
||||
if (!csvData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (navigator.msSaveBlob) {
|
||||
// for IE and Edge
|
||||
navigator.msSaveBlob(
|
||||
new Blob([csvData], { type: "data:text/csv;charset=utf-8" }),
|
||||
"PerPartitionQueryMetrics.csv"
|
||||
);
|
||||
} else {
|
||||
const downloadLink: HTMLAnchorElement = document.createElement("a");
|
||||
downloadLink.href = "data:text/csv;charset=utf-8," + encodeURI(csvData);
|
||||
downloadLink.target = "_self";
|
||||
downloadLink.download = "QueryMetricsPerPartition.csv";
|
||||
|
||||
// for some reason, FF displays the download prompt only when
|
||||
// the link is added to the dom so we add and remove it
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
downloadLink.remove();
|
||||
}
|
||||
}
|
||||
|
||||
protected _initIterator(): void {
|
||||
const options = QueryTabComponent.getIteratorOptions();
|
||||
if (this._resourceTokenPartitionKey) {
|
||||
options.partitionKey = this._resourceTokenPartitionKey;
|
||||
}
|
||||
|
||||
this._iterator = queryDocuments(
|
||||
this.props.collection.databaseId,
|
||||
this.props.collection.id(),
|
||||
this.state.sqlStatementToExecute,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
protected _initIteratorMongo(): Promise<MinimalQueryIterator> {
|
||||
//eslint-disable-next-line
|
||||
const options: any = {};
|
||||
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
|
||||
this._iterator = queryIterator(
|
||||
this.props.collection.databaseId,
|
||||
this.props.viewModelcollection,
|
||||
this.state.sqlStatementToExecute
|
||||
);
|
||||
const mongoPromise: Promise<MinimalQueryIterator> = new Promise((resolve) => {
|
||||
resolve(this._iterator);
|
||||
});
|
||||
return mongoPromise;
|
||||
}
|
||||
|
||||
//eslint-disable-next-line
|
||||
public static getIteratorOptions(collection?: ViewModels.Collection): any {
|
||||
//eslint-disable-next-line
|
||||
const options: any = {};
|
||||
options.enableCrossPartitionQuery = HeadersUtility.shouldEnableCrossPartitionKey();
|
||||
return options;
|
||||
}
|
||||
|
||||
private _normalize(value: number): number {
|
||||
if (!value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private _resetAggregateQueryMetrics(): void {
|
||||
this.setState({
|
||||
aggregatedQueryMetrics: {
|
||||
clientSideMetrics: {},
|
||||
documentLoadTime: undefined,
|
||||
documentWriteTime: undefined,
|
||||
indexHitDocumentCount: undefined,
|
||||
outputDocumentCount: undefined,
|
||||
outputDocumentSize: undefined,
|
||||
indexLookupTime: undefined,
|
||||
retrievedDocumentCount: undefined,
|
||||
retrievedDocumentSize: undefined,
|
||||
vmExecutionTime: undefined,
|
||||
queryPreparationTimes: undefined,
|
||||
runtimeExecutionTimes: {
|
||||
queryEngineExecutionTime: undefined,
|
||||
systemFunctionExecutionTime: undefined,
|
||||
userDefinedFunctionExecutionTime: undefined,
|
||||
},
|
||||
totalQueryExecutionTime: undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _generateQueryMetricsCsvData(): string {
|
||||
if (!this.state.queryMetrics) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const queryMetrics = this.state.queryMetrics;
|
||||
let csvData = "";
|
||||
const columnHeaders: string =
|
||||
[
|
||||
"Partition key range id",
|
||||
"Retrieved document count",
|
||||
"Retrieved document size (in bytes)",
|
||||
"Output document count",
|
||||
"Output document size (in bytes)",
|
||||
"Index hit document count",
|
||||
"Index lookup time (ms)",
|
||||
"Document load time (ms)",
|
||||
"Query engine execution time (ms)",
|
||||
"System function execution time (ms)",
|
||||
"User defined function execution time (ms)",
|
||||
"Document write time (ms)",
|
||||
].join(",") + "\n";
|
||||
csvData = csvData + columnHeaders;
|
||||
queryMetrics.forEach((queryMetric, partitionKeyRangeId) => {
|
||||
const partitionKeyRangeData: string =
|
||||
[
|
||||
partitionKeyRangeId,
|
||||
queryMetric.retrievedDocumentCount,
|
||||
queryMetric.retrievedDocumentSize,
|
||||
queryMetric.outputDocumentCount,
|
||||
queryMetric.outputDocumentSize,
|
||||
queryMetric.indexHitDocumentCount,
|
||||
queryMetric.indexLookupTime && queryMetric.indexLookupTime.totalMilliseconds(),
|
||||
queryMetric.documentLoadTime && queryMetric.documentLoadTime.totalMilliseconds(),
|
||||
queryMetric.runtimeExecutionTimes &&
|
||||
queryMetric.runtimeExecutionTimes.queryEngineExecutionTime &&
|
||||
queryMetric.runtimeExecutionTimes.queryEngineExecutionTime.totalMilliseconds(),
|
||||
queryMetric.runtimeExecutionTimes &&
|
||||
queryMetric.runtimeExecutionTimes.systemFunctionExecutionTime &&
|
||||
queryMetric.runtimeExecutionTimes.systemFunctionExecutionTime.totalMilliseconds(),
|
||||
queryMetric.runtimeExecutionTimes &&
|
||||
queryMetric.runtimeExecutionTimes.userDefinedFunctionExecutionTime &&
|
||||
queryMetric.runtimeExecutionTimes.userDefinedFunctionExecutionTime.totalMilliseconds(),
|
||||
queryMetric.documentWriteTime && queryMetric.documentWriteTime.totalMilliseconds(),
|
||||
].join(",") + "\n";
|
||||
csvData = csvData + partitionKeyRangeData;
|
||||
});
|
||||
|
||||
return csvData;
|
||||
}
|
||||
|
||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
if (this.executeQueryButton.visible) {
|
||||
const label = this.state._executeQueryButtonTitle;
|
||||
const label = this.state.selectedContent?.length > 0 ? "Execute Selection" : "Execute Query";
|
||||
buttons.push({
|
||||
iconSrc: ExecuteQueryIcon,
|
||||
iconAlt: label,
|
||||
@@ -820,10 +272,6 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
return buttons;
|
||||
}
|
||||
|
||||
private _buildCommandBarOptions(): void {
|
||||
this.props.tabsBaseInstance.updateNavbarWithTabsButtons();
|
||||
}
|
||||
|
||||
public onChangeContent(newContent: string): void {
|
||||
this.setState({
|
||||
sqlQueryEditorContent: newContent,
|
||||
@@ -848,13 +296,11 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
public onSelectedContent(selectedContent: string): void {
|
||||
if (selectedContent.trim().length > 0) {
|
||||
this.setState({
|
||||
selectedContent: selectedContent,
|
||||
_executeQueryButtonTitle: "Execute Selection",
|
||||
selectedContent,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
selectedContent: "",
|
||||
_executeQueryButtonTitle: "Execute Query",
|
||||
});
|
||||
}
|
||||
useCommandBar.getState().setContextButtons(this.getTabsButtons());
|
||||
@@ -874,7 +320,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
<div className="queryEditor" style={{ height: "100%" }}>
|
||||
<EditorReact
|
||||
language={"sql"}
|
||||
content={this.state.initialEditorContent}
|
||||
content={this.state.sqlQueryEditorContent}
|
||||
isReadOnly={false}
|
||||
ariaLabel={"Editing Query"}
|
||||
lineNumbers={"on"}
|
||||
@@ -883,174 +329,14 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
<Fragment>
|
||||
{this.isPreferredApiMongoDB && this.state.sqlQueryEditorContent.length === 0 && (
|
||||
<div className="mongoQueryHelper">
|
||||
Start by writing a Mongo query, for example: <strong>{"{'id':'foo'}"}</strong> or{" "}
|
||||
<strong>
|
||||
{"{ "}
|
||||
{" }"}
|
||||
</strong>{" "}
|
||||
to get all the documents.
|
||||
</div>
|
||||
)}
|
||||
{this.maybeSubQuery && (
|
||||
<div className="warningErrorContainer" aria-live="assertive">
|
||||
<div className="warningErrorContent">
|
||||
<span>
|
||||
<img className="paneErrorIcon" src={InfoColor} alt="Error" />
|
||||
</span>
|
||||
<span className="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>
|
||||
)}
|
||||
{/* <!-- Query Errors Tab - Start--> */}
|
||||
{!!this.state.error && (
|
||||
<div className="active queryErrorsHeaderContainer">
|
||||
<span className="queryErrors" data-toggle="tab">
|
||||
Errors
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* <!-- Query Errors Tab - End --> */}
|
||||
{/* <!-- Query Results & Errors Content Container - Start--> */}
|
||||
<div className="queryResultErrorContentContainer">
|
||||
{this.state.allResultsMetadata.length === 0 &&
|
||||
!this.state.error &&
|
||||
!this.state.queryResults &&
|
||||
!this.props.tabsBaseInstance.isExecuting() && (
|
||||
<div className="queryEditorWatermark">
|
||||
<p>
|
||||
<img src={RunQuery} alt="Execute Query Watermark" />
|
||||
</p>
|
||||
<p className="queryEditorWatermarkText">Execute a query to see the results</p>
|
||||
</div>
|
||||
)}
|
||||
{(this.state.allResultsMetadata.length > 0 || !!this.state.error || this.state.queryResults) && (
|
||||
<div className="queryResultsErrorsContent">
|
||||
{!this.state.error && (
|
||||
<Pivot aria-label="Successful execution" style={{ height: "100%" }}>
|
||||
<PivotItem
|
||||
headerText="Results"
|
||||
headerButtonProps={{
|
||||
"data-order": 1,
|
||||
"data-title": "Results",
|
||||
}}
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<div className="result-metadata">
|
||||
<span>
|
||||
<span>{this.state.showingDocumentsDisplayText}</span>
|
||||
</span>
|
||||
{this.state.allResultsMetadata[this.state.allResultsMetadata.length - 1]
|
||||
.hasMoreResults && (
|
||||
<>
|
||||
<span className="queryResultDivider">|</span>
|
||||
<span className="queryResultNextEnable">
|
||||
<a onClick={this.onFetchNextPageClick.bind(this)}>
|
||||
<span>Load more</span>
|
||||
<img className="queryResultnextImg" src={QueryEditorNext} alt="Fetch next page" />
|
||||
</a>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{this.state.queryResults &&
|
||||
this.state.queryResults.length > 0 &&
|
||||
this.state.allResultsMetadata.length > 0 &&
|
||||
!this.state.error && (
|
||||
<div
|
||||
style={{
|
||||
paddingBottom: "100px",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<EditorReact
|
||||
language={"json"}
|
||||
content={this.state.queryResults}
|
||||
isReadOnly={true}
|
||||
ariaLabel={"Query results"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
headerText="Query Stats"
|
||||
headerButtonProps={{
|
||||
"data-order": 2,
|
||||
"data-title": "Query Stats",
|
||||
}}
|
||||
style={{ height: "100%", overflowY: "scroll" }}
|
||||
>
|
||||
{this.state.allResultsMetadata.length > 0 && !this.state.error && (
|
||||
<div className="queryMetricsSummaryContainer">
|
||||
<div className="queryMetricsSummary">
|
||||
<h5>Query Statistics</h5>
|
||||
<DetailsList
|
||||
items={this.state.items}
|
||||
columns={this.state.columns}
|
||||
selectionMode={SelectionMode.none}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
compact={true}
|
||||
/>
|
||||
</div>
|
||||
{this.state.isQueryMetricsEnabled && (
|
||||
<div className="downloadMetricsLinkContainer">
|
||||
<a
|
||||
id="downloadMetricsLink"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => this.onDownloadQueryMetricsCsvClick()}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) =>
|
||||
this.onDownloadQueryMetricsCsvKeyPress(event)
|
||||
}
|
||||
>
|
||||
<img
|
||||
className="downloadCsvImg"
|
||||
src={DownloadQueryMetrics}
|
||||
alt="download query metrics csv"
|
||||
/>
|
||||
<span>Per-partition query metrics (CSV)</span>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
)}
|
||||
{/* <!-- Query Errors Content - Start--> */}
|
||||
{!!this.state.error && (
|
||||
<div className="tab-pane active">
|
||||
<div className="errorContent">
|
||||
<span className="errorMessage">{this.state.error}</span>
|
||||
<span className="errorDetailsLink">
|
||||
<a
|
||||
onClick={() => this.onErrorDetailsClick()}
|
||||
onKeyPress={(event: React.KeyboardEvent<HTMLAnchorElement>) =>
|
||||
this.onErrorDetailsKeyPress(event)
|
||||
}
|
||||
id="error-display"
|
||||
tabIndex={0}
|
||||
aria-label="Error details link"
|
||||
>
|
||||
More details
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* <!-- Query Errors Content - End--> */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
<QueryResultSection
|
||||
isMongoDB={this.props.isPreferredApiMongoDB}
|
||||
queryEditorContent={this.state.sqlQueryEditorContent}
|
||||
error={this.state.error}
|
||||
queryResults={this.state.queryResults}
|
||||
isExecuting={this.state.isExecuting}
|
||||
executeQueryDocumentsPage={(firstItemIndex: number) => this._executeQueryDocumentsPage(firstItemIndex)}
|
||||
/>
|
||||
</SplitterLayout>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user