diff --git a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx index c55923486..6a04fdcd5 100644 --- a/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx +++ b/src/Explorer/Tabs/QueryTab/QueryTabComponent.tsx @@ -55,6 +55,8 @@ import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPa import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane"; import TabsBase from "../TabsBase"; import "./QueryTabComponent.less"; +import { useQueryMetadataStore } from "./useQueryMetadataStore"; // adjust path if needed + enum ToggleState { Result, @@ -196,7 +198,10 @@ class QueryTabComponentImpl extends React.Component 0, visible: true, }; - + // const query=this.state.sqlQueryEditorContent; + // const db = this.props.collection.databaseId; + // const container = this.props.collection.id(); + const isSaveQueryBtnEnabled = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; this.saveQueryButton = { enabled: isSaveQueryBtnEnabled, @@ -260,6 +265,16 @@ class QueryTabComponentImpl extends React.Component => { + // console.log("i am ",this.props.collection.databaseId); + // console.log("I am query",this.state.sqlQueryEditorContent ); + // console.log("i am",this.props.collection.id()); + const query1=this.state.sqlQueryEditorContent; + const db = this.props.collection.databaseId; + const container = this.props.collection.id(); + useQueryMetadataStore.getState().setMetadata(query1, db, container); + console.log("i am ",query1); + console.log("I am db",db); + console.log("i am cnt",container); this._iterator = undefined; setTimeout(async () => { diff --git a/src/Explorer/Tabs/QueryTab/ResultsView.tsx b/src/Explorer/Tabs/QueryTab/ResultsView.tsx index 64b987b69..93af07bd4 100644 --- a/src/Explorer/Tabs/QueryTab/ResultsView.tsx +++ b/src/Explorer/Tabs/QueryTab/ResultsView.tsx @@ -1,5 +1,7 @@ +import { FontIcon } from "@fluentui/react"; import { Button, + Checkbox, DataGrid, DataGridBody, DataGridCell, @@ -8,26 +10,36 @@ import { DataGridRow, SelectTabData, SelectTabEvent, + Spinner, Tab, TabList, + Table, + TableBody, + TableCell, TableColumnDefinition, - createTableColumn, + TableHeader, + TableRow, + createTableColumn } from "@fluentui/react-components"; -import { ArrowDownloadRegular, CopyRegular } from "@fluentui/react-icons"; +import { ArrowDownloadRegular, ChevronDown20Regular, ChevronRight20Regular, CircleFilled, CopyRegular } from "@fluentui/react-icons"; +import copy from "clipboard-copy"; 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 { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles"; +import { useQueryMetadataStore } from "Explorer/Tabs/QueryTab/useQueryMetadataStore"; +import React, { useCallback, useEffect, useState } from "react"; import { userContext } from "UserContext"; -import copy from "clipboard-copy"; -import React, { useCallback, useState } from "react"; +import { logConsoleProgress } from "Utils/NotificationConsoleUtils"; +import { client } from "../../../Common/CosmosClient"; +import { handleError } from "../../../Common/ErrorHandlingUtils"; import { ResultsViewProps } from "./QueryResultSection"; - enum ResultsTabs { Results = "results", QueryStats = "queryStats", + IndexAdvisor="indexadv", } const ResultsTab: React.FC = ({ queryResults, isMongoDB, executeQueryDocumentsPage }) => { @@ -523,11 +535,400 @@ const QueryStatsTab: React.FC> = ({ query ); }; +interface IIndexMetric { + index: string; + impact: string; + section: "Included" | "Not Included" | "Header"; +} +const IndexAdvisorTab: React.FC = () => { + const {userQuery, databaseId, containerId}=useQueryMetadataStore(); + const [loading, setLoading] = useState(true); + const [indexMetrics, setIndexMetrics] = useState(null); + const [showIncluded, setShowIncluded] = useState(true); + const [showNotIncluded, setShowNotIncluded] = useState(true); + const [selectedIndexes, setSelectedIndexes] = useState([]); + const [selectAll, setSelectAll] = useState(false); + const [updateMessageShown, setUpdateMessageShown] = useState(false); + const [included, setIncludedIndexes] = useState([]); + const [notIncluded, setNotIncludedIndexes] = useState([]); + + useEffect(() => { + async function fetchIndexMetrics() { + const clearMessage = logConsoleProgress(`Querying items with IndexMetrics in container ${containerId}`); + try { + const querySpec = { + query: userQuery || "SELECT TOP 10 c.id FROM c WHERE c.Item = 'value1234' ", + }; + const sdkResponse = await client() + .database(databaseId) + .container(containerId) + .items.query(querySpec, { + populateIndexMetrics: true, + }) + .fetchAll(); + setIndexMetrics(sdkResponse.indexMetrics); + } catch (error) { + handleError(error, "queryItemsWithIndexMetrics", `Error querying items from ${containerId}`); + } finally { + clearMessage(); + setLoading(false); + } + } + if(userQuery && databaseId && containerId){ + fetchIndexMetrics(); + } + }, [userQuery, databaseId, containerId]); + +useEffect(() => { + if (!indexMetrics) return ; + + const included: any[] = []; + const notIncluded: any[] = []; + const lines = indexMetrics.split("\n").map((line: string) => 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"; + + const isComposite = index.includes(","); + const indexObj: any = { index, impact }; + 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); + } + } + } + setIncludedIndexes(included); + setNotIncludedIndexes(notIncluded); +},[indexMetrics]); + +useEffect(() => { + const allSelected = notIncluded.length > 0 && notIncluded.every((item) => selectedIndexes.some((s) => s.index === item.index)); + setSelectAll(allSelected); +}, [selectedIndexes, notIncluded]); + +const handleCheckboxChange = (indexObj: any, checked: boolean) => { + if (checked) { + setSelectedIndexes((prev) => [...prev, indexObj]); + } else { + setSelectedIndexes((prev) => + prev.filter((item) => item.index !== indexObj.index) + ); + } +}; + +const handleSelectAll = (checked: boolean) => { + setSelectAll(checked); + setSelectedIndexes(checked ? notIncluded : []); + }; + +const handleUpdatePolicy = async () => { + if (selectedIndexes.length === 0) { + console.log("No indexes selected for update"); + return; + } + try { + const { resource: containerDef } = await client() + .database(databaseId) + .container(containerId) + .read(); + + const newIncludedPaths = selectedIndexes + .filter(index => !index.composite) + .map(index => { + + return { + path: index.path, + }; + }); + + const newCompositeIndexes = selectedIndexes + .filter(index => index.composite) + .map(index => index.composite); + + const updatedPolicy = { + ...containerDef.indexingPolicy, + includedPaths: [ + ...(containerDef.indexingPolicy?.includedPaths || []), + ...newIncludedPaths, + ], + compositeIndexes: [ + ...(containerDef.indexingPolicy?.compositeIndexes || []), + ...newCompositeIndexes, + ], + }; + + await client() + .database(databaseId) + .container(containerId) + .replace({ + id: containerId, + partitionKey: containerDef.partitionKey, + indexingPolicy: updatedPolicy, + }); + + console.log("Indexing policy successfully updated:", updatedPolicy); + + const newIncluded = [...included, ...notIncluded.filter(item => + selectedIndexes.find(s => s.index === item.index) + )]; + const newNotIncluded = notIncluded.filter(item => + !selectedIndexes.find(s => s.index === item.index) + ); + + setSelectedIndexes([]); + setSelectAll(false); + setIndexMetricsFromParsed(newIncluded, newNotIncluded); + setUpdateMessageShown(true); + } catch (err) { + console.error("Failed to update indexing policy:", err); + } +}; + +const setIndexMetricsFromParsed = (included: { index: string; impact: string }[], notIncluded: { index: string; impact: string }[]) => { + const serialize = (sectionTitle: string, items: { index: string; impact: string }[], isUtilized: boolean) => + items.length + ? `${sectionTitle}\n` + + items + .map((item) => `Index Spec: ${item.index}\nIndex Impact Score: ${item.impact}`) + .join("\n") + "\n" + : ""; +const composedMetrics = + serialize("Utilized Single Indexes", included, true) + + serialize("Potential Single Indexes", notIncluded, false); + + setIndexMetrics(composedMetrics.trim()); + }; + +const renderImpactDots = (impact: string) => { + let count = 0; + if (impact === "High") count = 3; + else if (impact === "Medium") count = 2; + else if (impact === "Low") count = 1; + + return( +
+ {Array.from({ length: count }).map((_, i) => ( + + ))} +
+ ); +}; + +const renderRow = (item: IIndexMetric, index: number) => { + const isHeader = item.section === "Header"; + const isNotIncluded = item.section === "Not Included"; + const isIncluded = item.section === "Included"; + + return ( + + +
+ {isNotIncluded ? ( + selected.index===item.index)} + onChange={(_, data) => handleCheckboxChange(item, data.checked === true)} + /> + ) : isHeader && item.index === "Not Included in Current Policy" && notIncluded.length > 0 ? ( + handleSelectAll(data.checked === true)} + /> + ) : ( +
+ )} + {isHeader ? ( + { + if (item.index === "Included in Current Policy") { + setShowIncluded(!showIncluded); + } else if (item.index === "Not Included in Current Policy") { + setShowNotIncluded(!showNotIncluded); + } + }} + > + {item.index === "Included in Current Policy" + ? showIncluded + ? + : + : showNotIncluded + ? + : + } + + ) : ( +
+ )} +
+ {item.index} +
+
+ {isHeader ? null : item.impact} +
+
+ {isHeader ? null : renderImpactDots(item.impact)} +
+
+
+
+ ); +}; + const generateIndexMetricItems = ( + + included: { index: string; impact: string }[], + notIncluded: { index: string; impact: string }[] + ): IIndexMetric[] => { + const items: IIndexMetric[] = []; + + items.push({ index: "Not Included in Current Policy", impact: "", section: "Header" }); + if (showNotIncluded) { + notIncluded.forEach((item) => + items.push({ ...item, section: "Not Included" }) + ); + } + items.push({ index: "Included in Current Policy", impact: "", section: "Header" }); + if (showIncluded) { + included.forEach((item) => + items.push({ ...item, section: "Included" }) + ); + } + return items; + }; + + if (loading) { + return
+ +
;} + +return ( +
+
+ {updateMessageShown ? ( + <> + + + + + Your indexing policy has been updated with the new included paths. You may review the changes in Scale & Settings. + + + ) : ( + "Here is an analysis on the indexes utilized for executing the query. Based on the analysis, Cosmos DB recommends adding the selected indexes to your indexing policy to optimize the performance of this particular query." + )} +
+
Indexes analysis
+ + + + +
+
{/* Checkbox column */} +
{/* Chevron column */} +
Index
+
Estimated Impact
+
+
+
+
+ + {generateIndexMetricItems(included, notIncluded).map(renderRow)} + +
+ {selectedIndexes.length > 0 && ( +
+ +
+ )} +
+ ); +}; export const ResultsView: React.FC = ({ isMongoDB, queryResults, executeQueryDocumentsPage }) => { const styles = useQueryTabStyles(); const [activeTab, setActiveTab] = useState(ResultsTabs.Results); - const onTabSelect = useCallback((event: SelectTabEvent, data: SelectTabData) => { + const onTabSelect = useCallback((event: SelectTabEvent, data: SelectTabData) =>{ setActiveTab(data.value as ResultsTabs); }, []); @@ -548,6 +949,13 @@ export const ResultsView: React.FC = ({ isMongoDB, queryResult > Query Stats + + Index Advisor +
{activeTab === ResultsTabs.Results && ( @@ -558,6 +966,7 @@ export const ResultsView: React.FC = ({ isMongoDB, queryResult /> )} {activeTab === ResultsTabs.QueryStats && } + {activeTab === ResultsTabs.IndexAdvisor && }
); diff --git a/src/Explorer/Tabs/QueryTab/useQueryMetadataStore.ts b/src/Explorer/Tabs/QueryTab/useQueryMetadataStore.ts new file mode 100644 index 000000000..e090fd6af --- /dev/null +++ b/src/Explorer/Tabs/QueryTab/useQueryMetadataStore.ts @@ -0,0 +1,16 @@ +import create from "zustand"; + +interface QueryMetadataStore { + userQuery: string; + databaseId: string; + containerId: string; + setMetadata: (query1: string, db: string, container: string) => void; +} + +export const useQueryMetadataStore = create((set) => ({ + userQuery: "", + databaseId: "", + containerId: "", + setMetadata: (query1, db, container) => + set({ userQuery: query1, databaseId: db, containerId: container }), +}));