diff --git a/src/Explorer/Tabs/QueryTab/IndexAdvisorUtils.tsx b/src/Explorer/Tabs/QueryTab/IndexAdvisorUtils.tsx index 488ece9eb..168aa7e43 100644 --- a/src/Explorer/Tabs/QueryTab/IndexAdvisorUtils.tsx +++ b/src/Explorer/Tabs/QueryTab/IndexAdvisorUtils.tsx @@ -13,85 +13,80 @@ interface IndexObject { path?: string; } -// SDK response format -export interface IndexMetricsResponse { - UtilizedIndexes?: { - SingleIndexes?: Array<{ IndexSpec: string; IndexImpactScore?: string }>; - CompositeIndexes?: Array<{ IndexSpecs: string[]; IndexImpactScore?: string }>; - }; - PotentialIndexes?: { - SingleIndexes?: Array<{ IndexSpec: string; IndexImpactScore?: string }>; - CompositeIndexes?: Array<{ IndexSpecs: string[]; IndexImpactScore?: string }>; - }; +export interface IndexMetricsJson { + included?: IIndexMetric[]; + notIncluded?: IIndexMetric[]; } - -export function parseIndexMetrics(indexMetrics: IndexMetricsResponse): { +export function parseIndexMetrics(indexMetrics: string | IndexMetricsJson): { included: IIndexMetric[]; notIncluded: IIndexMetric[]; } { + // If already JSON, just extract arrays + if (typeof indexMetrics === "object" && indexMetrics !== null) { + return { + included: Array.isArray(indexMetrics.included) ? indexMetrics.included : [], + notIncluded: Array.isArray(indexMetrics.notIncluded) ? indexMetrics.notIncluded : [], + }; + } + + // Otherwise, parse as string (current SDK) const included: IIndexMetric[] = []; const notIncluded: IIndexMetric[] = []; + const lines = (indexMetrics as string) + .split("\n") + .map((line) => line.trim()) + .filter(Boolean); + let currentSection = ""; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith("Utilized Single Indexes") || line.startsWith("Utilized Composite Indexes")) { + currentSection = "included"; + } else if (line.startsWith("Potential Single Indexes") || line.startsWith("Potential Composite Indexes")) { + currentSection = "notIncluded"; + } else if (line.startsWith("Index Spec:")) { + const index = line.replace("Index Spec:", "").trim(); + const impactLine = lines[i + 1]; + const impact = impactLine?.includes("Index Impact Score:") ? impactLine.split(":")[1].trim() : "Unknown"; - // Process UtilizedIndexes (Included) - if (indexMetrics.UtilizedIndexes) { - // Single indexes - indexMetrics.UtilizedIndexes.SingleIndexes?.forEach((index) => { - included.push({ - index: index.IndexSpec, - impact: index.IndexImpactScore || "Utilized", - section: "Included", - path: index.IndexSpec, - }); - }); + const isComposite = index.includes(","); - // Composite indexes - indexMetrics.UtilizedIndexes.CompositeIndexes?.forEach((index) => { - const compositeSpec = index.IndexSpecs.join(", "); - included.push({ - index: compositeSpec, - impact: index.IndexImpactScore || "Utilized", - section: "Included", - composite: index.IndexSpecs.map((spec) => { - const [path, order] = spec.trim().split(/\s+/); + const sectionMap: Record = { + included: "Included", + notIncluded: "Not Included", + }; + + const indexObj: IndexObject = { index, impact, section: sectionMap[currentSection] ?? "Header" }; + if (isComposite) { + indexObj.composite = index.split(",").map((part: string) => { + const [path, order] = part.trim().split(/\s+/); return { path: path.trim(), order: order?.toLowerCase() === "desc" ? "descending" : "ascending", }; - }), - }); - }); + }); + } else { + let path = "/unknown/*"; + const pathRegex = /\/[^/\s*?]+(?:\/[^/\s*?]+)*(\/\*|\?)/; + const match = index.match(pathRegex); + if (match) { + path = match[0]; + } else { + const simplePathRegex = /\/[^/\s]+/; + const simpleMatch = index.match(simplePathRegex); + if (simpleMatch) { + path = simpleMatch[0] + "/*"; + } + } + indexObj.path = path; + } + + if (currentSection === "included") { + included.push(indexObj); + } else if (currentSection === "notIncluded") { + notIncluded.push(indexObj); + } + } } - - // Process PotentialIndexes (Not Included) - if (indexMetrics.PotentialIndexes) { - // Single indexes - indexMetrics.PotentialIndexes.SingleIndexes?.forEach((index) => { - notIncluded.push({ - index: index.IndexSpec, - impact: index.IndexImpactScore || "Unknown", - section: "Not Included", - path: index.IndexSpec, - }); - }); - - // Composite indexes - indexMetrics.PotentialIndexes.CompositeIndexes?.forEach((index) => { - const compositeSpec = index.IndexSpecs.join(", "); - notIncluded.push({ - index: compositeSpec, - impact: index.IndexImpactScore || "Unknown", - section: "Not Included", - composite: index.IndexSpecs.map((spec) => { - const [path, order] = spec.trim().split(/\s+/); - return { - path: path.trim(), - order: order?.toLowerCase() === "desc" ? "descending" : "ascending", - }; - }), - }); - }); - } - return { included, notIncluded }; } diff --git a/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx b/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx index e343cdb2c..90a8a5672 100644 --- a/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryResultSection.tsx @@ -3,21 +3,18 @@ import QueryError from "Common/QueryError"; import { IndeterminateProgressBar } from "Explorer/Controls/IndeterminateProgressBar"; import { MessageBanner } from "Explorer/Controls/MessageBanner"; import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles"; -import useZoomLevel from "hooks/useZoomLevel"; import React from "react"; -import { conditionalClass } from "Utils/StyleUtils"; import RunQuery from "../../../../images/RunQuery.png"; import { QueryResults } from "../../../Contracts/ViewModels"; import { ErrorList } from "./ErrorList"; import { ResultsView } from "./ResultsView"; +import useZoomLevel from "hooks/useZoomLevel"; +import { conditionalClass } from "Utils/StyleUtils"; export interface ResultsViewProps { isMongoDB: boolean; queryResults: QueryResults; executeQueryDocumentsPage: (firstItemIndex: number) => Promise; - queryText?: string; - databaseId?: string; - containerId?: string; } interface QueryResultProps extends ResultsViewProps { @@ -52,9 +49,6 @@ export const QueryResultSection: React.FC = ({ queryResults, executeQueryDocumentsPage, isExecuting, - queryText, - databaseId, - containerId, }: QueryResultProps): JSX.Element => { const styles = useQueryTabStyles(); const maybeSubQuery = queryEditorContent && /.*\(.*SELECT.*\)/i.test(queryEditorContent); @@ -97,9 +91,6 @@ export const QueryResultSection: React.FC = ({ queryResults={queryResults} executeQueryDocumentsPage={executeQueryDocumentsPage} isMongoDB={isMongoDB} - queryText={queryText || queryEditorContent} - databaseId={databaseId} - containerId={containerId} /> ) : ( diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index 67eca1b01..e9b37dc0c 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -375,9 +375,9 @@ class QueryTabComponentImpl extends React.Component ) : ( this._executeQueryDocumentsPage(firstItemIndex) } - databaseId={this.props.collection.databaseId} - containerId={this.props.collection.id()} /> )} diff --git a/src/Explorer/Tabs/QueryTab/ResultsView.tsx b/src/Explorer/Tabs/QueryTab/ResultsView.tsx index 59cdcdc95..ec51f989e 100644 --- a/src/Explorer/Tabs/QueryTab/ResultsView.tsx +++ b/src/Explorer/Tabs/QueryTab/ResultsView.tsx @@ -28,7 +28,8 @@ import { HttpHeaders } from "Common/Constants"; import MongoUtility from "Common/MongoUtility"; import { QueryMetrics } from "Contracts/DataModels"; import { EditorReact } from "Explorer/Controls/Editor/EditorReact"; -import { IDocument } from "Explorer/Tabs/QueryTab/QueryTabComponent"; +import { parseIndexMetrics, renderImpactDots } from "Explorer/Tabs/QueryTab/IndexAdvisorUtils"; +import { IDocument, useQueryMetadataStore } from "Explorer/Tabs/QueryTab/QueryTabComponent"; import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles"; import React, { useCallback, useEffect, useState } from "react"; import { userContext } from "UserContext"; @@ -36,7 +37,6 @@ import { logConsoleProgress } from "Utils/NotificationConsoleUtils"; import create from "zustand"; import { client } from "../../../Common/CosmosClient"; import { handleError } from "../../../Common/ErrorHandlingUtils"; -import { parseIndexMetrics, renderImpactDots, type IndexMetricsResponse } from "./IndexAdvisorUtils"; import { ResultsViewProps } from "./QueryResultSection"; import { useIndexAdvisorStyles } from "./StylesAdvisor"; enum ResultsTabs { @@ -394,8 +394,9 @@ const QueryStatsTab: React.FC> = ({ query }, { metric: "User defined function execution time", - value: `${aggregatedQueryMetrics.runtimeExecutionTimes?.userDefinedFunctionExecutionTime?.toString() || 0 - } ms`, + value: `${ + aggregatedQueryMetrics.runtimeExecutionTimes?.userDefinedFunctionExecutionTime?.toString() || 0 + } ms`, toolTip: "Total time spent executing user-defined functions", }, { @@ -543,14 +544,11 @@ export interface IIndexMetric { path?: string; composite?: { path: string; order: string }[]; } -export const IndexAdvisorTab: React.FC<{ - queryText?: string; - databaseId?: string; - containerId?: string; -}> = ({ queryText, databaseId, containerId }) => { +export const IndexAdvisorTab: React.FC = () => { const style = useIndexAdvisorStyles(); + const { userQuery, databaseId, containerId } = useQueryMetadataStore(); const [loading, setLoading] = useState(true); - const [indexMetrics, setIndexMetrics] = useState(null); + const [indexMetrics, setIndexMetrics] = useState(null); const [showIncluded, setShowIncluded] = useState(true); const [showNotIncluded, setShowNotIncluded] = useState(true); const [selectedIndexes, setSelectedIndexes] = useState([]); @@ -564,26 +562,10 @@ export const IndexAdvisorTab: React.FC<{ useEffect(() => { const fetchIndexMetrics = async () => { - // Reset all states when query parameters change - setLoading(true); - setIndexMetrics(null); - setIncludedIndexes([]); - setNotIncludedIndexes([]); - setSelectedIndexes([]); - setSelectAll(false); - setUpdateMessageShown(false); - setIsUpdating(false); - setJustUpdatedPolicy(false); - - if (!queryText || !databaseId || !containerId) { - setLoading(false); - return; - } - const clearMessage = logConsoleProgress(`Querying items with IndexMetrics in container ${containerId}`); try { const querySpec = { - query: queryText, + query: userQuery, }; const sdkResponse = await client() .database(databaseId) @@ -592,12 +574,7 @@ export const IndexAdvisorTab: React.FC<{ populateIndexMetrics: true, }) .fetchAll(); - - const parsedIndexMetrics = typeof sdkResponse.indexMetrics === 'string' - ? JSON.parse(sdkResponse.indexMetrics) - : sdkResponse.indexMetrics; - - setIndexMetrics(parsedIndexMetrics); + setIndexMetrics(sdkResponse.indexMetrics); } catch (error) { handleError(error, "queryItemsWithIndexMetrics", `Error querying items from ${containerId}`); } finally { @@ -605,9 +582,10 @@ export const IndexAdvisorTab: React.FC<{ setLoading(false); } }; - - fetchIndexMetrics(); - }, [queryText, databaseId, containerId]); + if (userQuery && databaseId && containerId) { + fetchIndexMetrics(); + } + }, [userQuery, databaseId, containerId]); useEffect(() => { if (!indexMetrics) { @@ -850,21 +828,13 @@ export const IndexAdvisorTab: React.FC<{ ); }; -export const ResultsView: React.FC = ({ - isMongoDB, - queryResults, - executeQueryDocumentsPage, - queryText, - databaseId, - containerId -}) => { +export const ResultsView: React.FC = ({ isMongoDB, queryResults, executeQueryDocumentsPage }) => { const styles = useQueryTabStyles(); const [activeTab, setActiveTab] = useState(ResultsTabs.Results); const onTabSelect = useCallback((event: SelectTabEvent, data: SelectTabData) => { setActiveTab(data.value as ResultsTabs); }, []); - return (
@@ -899,13 +869,7 @@ export const ResultsView: React.FC = ({ /> )} {activeTab === ResultsTabs.QueryStats && } - {activeTab === ResultsTabs.IndexAdvisor && ( - - )} + {activeTab === ResultsTabs.IndexAdvisor && }
);