diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index cbd3b70b9..b463bcbc1 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -1,5 +1,4 @@ import { IPivotItemProps, IPivotProps, Pivot, PivotItem } from "@fluentui/react"; -import { readCollection } from "Common/dataAccess/readCollection"; import { ComputedPropertiesComponent, ComputedPropertiesComponentProps, @@ -304,9 +303,10 @@ export class SettingsComponent extends React.Component { + this.unsubscribe = useIndexingPolicyStore.subscribe((_,) => { this.refreshCollectionData(); - }); + }, + (state) => state.indexingPolicy); this.refreshCollectionData(); } componentWillUnmount(): void { @@ -934,15 +934,23 @@ export class SettingsComponent extends React.Component => { - // Fetch the latest collection data from backend - const latestCollection = await readCollection(this.collection.databaseId, this.collection.id()); - // Update the observable and state - this.collection.rawDataModel = latestCollection; - this.collection.indexingPolicy(latestCollection.indexingPolicy); - // console.log("Fetched latest indexing policy:", latestCollection.indexingPolicy); + const storePolicy = useIndexingPolicyStore.getState().indexingPolicy; + if (!storePolicy) { + console.warn("No indexing policy found in store."); + return; + } + const indexingPolicy: DataModels.IndexingPolicy = { + ...storePolicy, + automatic: storePolicy.automatic ?? true, + indexingMode: storePolicy.indexingMode ?? "consistent", + includedPaths: storePolicy.includedPaths ?? [], + excludedPaths: storePolicy.excludedPaths ?? [], + }; + + this.collection.indexingPolicy(indexingPolicy); this.setState({ - indexingPolicyContent: latestCollection.indexingPolicy, - indexingPolicyContentBaseline: latestCollection.indexingPolicy, + indexingPolicyContent: indexingPolicy, + indexingPolicyContentBaseline: indexingPolicy, }); }; diff --git a/src/Explorer/Tabs/QueryTab/ResultsView.tsx b/src/Explorer/Tabs/QueryTab/ResultsView.tsx index cd7078a46..c4de53359 100644 --- a/src/Explorer/Tabs/QueryTab/ResultsView.tsx +++ b/src/Explorer/Tabs/QueryTab/ResultsView.tsx @@ -1,3 +1,4 @@ +import type { CompositePath, IndexingPolicy } from "@azure/cosmos"; import { FontIcon } from "@fluentui/react"; import { Button, @@ -35,8 +36,9 @@ import { logConsoleProgress } from "Utils/NotificationConsoleUtils"; import create from "zustand"; import { client } from "../../../Common/CosmosClient"; import { handleError } from "../../../Common/ErrorHandlingUtils"; -import * as DataModels from "../../../Contracts/DataModels"; +import { useIndexAdvisorStyles } from "./indexadv"; import { ResultsViewProps } from "./QueryResultSection"; + enum ResultsTabs { Results = "results", QueryStats = "queryStats", @@ -538,17 +540,17 @@ interface IIndexMetric { index: string; impact: string; section: "Included" | "Not Included" | "Header"; + path?: string; + composite?: { path: string; order: string }[]; } -interface IndexAdvisorTabProps { - onPolicyUpdated: (newPolicy: DataModels.IndexingPolicy) => void; -} -const IndexAdvisorTab: React.FC = ({ onPolicyUpdated }) => { +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([]); + const [selectedIndexes, setSelectedIndexes] = useState([]); const [selectAll, setSelectAll] = useState(false); const [updateMessageShown, setUpdateMessageShown] = useState(false); const [included, setIncludedIndexes] = useState([]); @@ -569,10 +571,8 @@ const IndexAdvisorTab: React.FC = ({ onPolicyUpdated }) => }) .fetchAll(); setIndexMetrics(sdkResponse.indexMetrics); - console.log("Index Metrics:", sdkResponse.indexMetrics); - // console.log("Query Results:", sdkResponse.resources); + console.log(sdkResponse.indexMetrics); console.log(typeof sdkResponse.indexMetrics); - console.log(typeof sdkResponse.resources) } catch (error) { handleError(error, "queryItemsWithIndexMetrics", `Error querying items from ${containerId}`); } finally { @@ -585,11 +585,125 @@ const IndexAdvisorTab: React.FC = ({ onPolicyUpdated }) => } }, [userQuery, databaseId, containerId]); + // useEffect(() => { + // if (!indexMetrics) { return }; + + // const included: IIndexMetric[] = []; + // const notIncluded: IIndexMetric[] = []; + // 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(() => { if (!indexMetrics) return; - const included: any[] = []; - const notIncluded: any[] = []; + let metricsObj: any = indexMetrics; + if (typeof indexMetrics === "string" && indexMetrics.trim().startsWith("{")) { + try { + metricsObj = JSON.parse(indexMetrics); + console.log("Parsed index metrics as JSON:", metricsObj); + } catch { + metricsObj = null; + } + } + + if (metricsObj && typeof metricsObj === "object" && metricsObj.UtilizedIndexes) { + const included: IIndexMetric[] = []; + const notIncluded: IIndexMetric[] = []; + + metricsObj.UtilizedIndexes.SingleIndexes?.forEach((si: any) => { + included.push({ + index: si.IndexSpec, + impact: "", + section: "Included", + path: si.IndexSpec, + }); + }); + metricsObj.UtilizedIndexes.CompositeIndexes?.forEach((ci: any) => { + included.push({ + index: ci.IndexSpecs.join(", "), + impact: "", + section: "Included", + composite: ci.IndexSpecs.map((spec: string) => { + const [path, order] = spec.split(" "); + return { path, order: (order || "ASC").toLowerCase() === "desc" ? "descending" : "ascending" }; + }), + }); + }); + + metricsObj.PotentialIndexes.SingleIndexes?.forEach((si: any) => { + notIncluded.push({ + index: si.IndexSpec, + impact: si.IndexImpactScore || "", + section: "Not Included", + path: si.IndexSpec, + }); + }); + metricsObj.PotentialIndexes.CompositeIndexes?.forEach((ci: any) => { + notIncluded.push({ + index: ci.IndexSpecs.join(", "), + impact: ci.IndexImpactScore || "", + section: "Not Included", + composite: ci.IndexSpecs.map((spec: string) => { + const [path, order] = spec.split(" "); + return { path, order: (order || "ASC").toLowerCase() === "desc" ? "descending" : "ascending" }; + }), + }); + }); + + setIncludedIndexes(included); + setNotIncludedIndexes(notIncluded); + return; + } + + const included: IIndexMetric[] = []; + const notIncluded: IIndexMetric[] = []; const lines = indexMetrics.split("\n").map((line: string) => line.trim()).filter(Boolean); let currentSection = ""; for (let i = 0; i < lines.length; i++) { @@ -639,13 +753,12 @@ const IndexAdvisorTab: React.FC = ({ onPolicyUpdated }) => 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) => { + const handleCheckboxChange = (indexObj: IIndexMetric, checked: boolean) => { if (checked) { setSelectedIndexes((prev) => [...prev, indexObj]); } else { @@ -661,31 +774,28 @@ const IndexAdvisorTab: React.FC = ({ onPolicyUpdated }) => }; 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(); - // console.log("def1", containerDef.indexingPolicy); + const containerRef = client().database(databaseId).container(containerId); + const { resource: containerDef } = await containerRef.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 newCompositeIndexes: CompositePath[][] = selectedIndexes + .filter(index => Array.isArray(index.composite)) + .map(index => + (index.composite as { path: string; order: string }[]).map(comp => ({ + path: comp.path, + order: comp.order === "descending" ? "descending" : "ascending", + })) as CompositePath[] + ); - const updatedPolicy = { + const updatedPolicy: IndexingPolicy = { ...containerDef.indexingPolicy, includedPaths: [ ...(containerDef.indexingPolicy?.includedPaths || []), @@ -699,71 +809,45 @@ const IndexAdvisorTab: React.FC = ({ onPolicyUpdated }) => indexingMode: containerDef.indexingPolicy?.indexingMode ?? "consistent", excludedPaths: containerDef.indexingPolicy?.excludedPaths ?? [], }; + await containerRef.replace({ + id: containerId, + partitionKey: containerDef.partitionKey, + indexingPolicy: updatedPolicy, + }); - await client() - .database(databaseId) - .container(containerId) - .replace({ - id: containerId, - partitionKey: containerDef.partitionKey, - indexingPolicy: updatedPolicy, - }); useIndexingPolicyStore.getState().setIndexingPolicyOnly(updatedPolicy); - // console.log("Indexing policy successfully updated:", updatedPolicy); - const { resource: containerDef2 } = await client() - .database(databaseId) - .container(containerId) - .read(); - onPolicyUpdated(containerDef2.indexingPolicy as DataModels.IndexingPolicy); - // console.log("def2", containerDef2.indexingPolicy); - - 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) - ); - + const selectedIndexSet = new Set(selectedIndexes.map(s => s.index)); + const updatedNotIncluded: typeof notIncluded = []; + const newlyIncluded: typeof included = []; + for (const item of notIncluded) { + if (selectedIndexSet.has(item.index)) { + newlyIncluded.push(item); + } else { + updatedNotIncluded.push(item); + } + } + const newIncluded = [...included, ...newlyIncluded]; + const newNotIncluded = updatedNotIncluded; + setIncludedIndexes(newIncluded); + setNotIncludedIndexes(newNotIncluded); 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) => ( ))}
@@ -773,30 +857,21 @@ const IndexAdvisorTab: React.FC = ({ onPolicyUpdated }) => 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)} - /> + onChange={(_, data) => handleCheckboxChange(item, data.checked === true)} /> ) : isHeader && item.index === "Not Included in Current Policy" && notIncluded.length > 0 ? ( handleSelectAll(data.checked === true)} - /> + onChange={(_, data) => handleSelectAll(data.checked === true)} /> ) : ( -
+
)} {isHeader ? ( = ({ onPolicyUpdated }) => } else if (item.index === "Not Included in Current Policy") { setShowNotIncluded(!showNotIncluded); } - }} - > + }}> {item.index === "Included in Current Policy" - ? showIncluded - ? - : - : showNotIncluded - ? - : + ? showIncluded ? : + : showNotIncluded ? : } ) : ( -
+
)} -
+
{item.index}
-
- {isHeader ? null : item.impact} +
+ {!isHeader && item.impact}
- {isHeader ? null : renderImpactDots(item.impact)} + {!isHeader && renderImpactDots(item.impact)}
); }; - const generateIndexMetricItems = ( - - included: { index: string; impact: string }[], - notIncluded: { index: string; impact: string }[] - ): IIndexMetric[] => { + const indexMetricItems = React.useMemo(() => { const items: IIndexMetric[] = []; - items.push({ index: "Not Included in Current Policy", impact: "", section: "Header" }); if (showNotIncluded) { notIncluded.forEach((item) => @@ -855,7 +920,7 @@ const IndexAdvisorTab: React.FC = ({ onPolicyUpdated }) => ); } return items; - }; + }, [included, notIncluded, showIncluded, showNotIncluded]); if (loading) { return
@@ -871,19 +936,11 @@ const IndexAdvisorTab: React.FC = ({ onPolicyUpdated }) => return (
-
+
{updateMessageShown ? ( <> + className={style.indexAdvisorSuccessIcon}> @@ -894,22 +951,14 @@ const IndexAdvisorTab: React.FC = ({ onPolicyUpdated }) => "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
- +
Indexes analysis
+
- + -
-
{/* Checkbox column */} -
{/* Chevron column */} +
+
+
Index
Estimated Impact
@@ -917,22 +966,14 @@ const IndexAdvisorTab: React.FC = ({ onPolicyUpdated }) => - {generateIndexMetricItems(included, notIncluded).map(renderRow)} + {indexMetricItems.map(renderRow)}
{selectedIndexes.length > 0 && ( -
+
@@ -944,9 +985,6 @@ const IndexAdvisorTab: React.FC = ({ onPolicyUpdated }) => export const ResultsView: React.FC = ({ isMongoDB, queryResults, executeQueryDocumentsPage }) => { const styles = useQueryTabStyles(); const [activeTab, setActiveTab] = useState(ResultsTabs.Results); - const [indexingPolicy, setIndexingPolicy] = useState(null); - const [baselinePolicy, setBaselinePolicy] = useState(null); - const { setIndexingPolicies } = useIndexingPolicyStore.getState(); const onTabSelect = useCallback((event: SelectTabEvent, data: SelectTabData) => { setActiveTab(data.value as ResultsTabs); @@ -985,36 +1023,20 @@ export const ResultsView: React.FC = ({ isMongoDB, queryResult /> )} {activeTab === ResultsTabs.QueryStats && } - {activeTab === ResultsTabs.IndexAdvisor && { - const freshPolicy = JSON.parse(JSON.stringify(newPolicy)); - setIndexingPolicy(freshPolicy); - setBaselinePolicy(freshPolicy); - setIndexingPolicies(freshPolicy, freshPolicy); - } - } - />} + {activeTab === ResultsTabs.IndexAdvisor && }
); }; export interface IndexingPolicyStore { - indexingPolicy: DataModels.IndexingPolicy | null; - baselinePolicy: DataModels.IndexingPolicy | null; - setIndexingPolicies: ( - indexingPolicy: DataModels.IndexingPolicy, - baselinePolicy: DataModels.IndexingPolicy - ) => void; - setIndexingPolicyOnly: (indexingPolicy: DataModels.IndexingPolicy) => void; + indexingPolicy: IndexingPolicy | null; + setIndexingPolicyOnly: (indexingPolicy: IndexingPolicy) => void; } export const useIndexingPolicyStore = create((set) => ({ indexingPolicy: null, - baselinePolicy: null, - setIndexingPolicies: (indexingPolicy, baselinePolicy) => - set({ indexingPolicy, baselinePolicy }), setIndexingPolicyOnly: (indexingPolicy) => - set((state) => ({ indexingPolicy: { ...indexingPolicy } })), + set(() => ({ indexingPolicy: { ...indexingPolicy } })), }));