diff --git a/src/Explorer/Tabs/QueryTab/IndexAdvisorUtils.ts b/src/Explorer/Tabs/QueryTab/IndexAdvisorUtils.ts
new file mode 100644
index 000000000..3ee8def76
--- /dev/null
+++ b/src/Explorer/Tabs/QueryTab/IndexAdvisorUtils.ts
@@ -0,0 +1,68 @@
+import type { IIndexMetric } from "Explorer/Tabs/QueryTab/ResultsView";
+export interface IndexMetricsJson {
+ included?: IIndexMetric[];
+ notIncluded?: IIndexMetric[];
+}
+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";
+
+ 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);
+ }
+ }
+ }
+ return { included, notIncluded };
+}
\ No newline at end of file
diff --git a/src/Explorer/Tabs/QueryTab/Indexadvisor.test.tsx b/src/Explorer/Tabs/QueryTab/Indexadvisor.test.tsx
new file mode 100644
index 000000000..c8badd866
--- /dev/null
+++ b/src/Explorer/Tabs/QueryTab/Indexadvisor.test.tsx
@@ -0,0 +1,515 @@
+// import "@testing-library/jest-dom";
+// import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+// import { IndexAdvisorTab } from "Explorer/Tabs/QueryTab/ResultsView";
+// import React from "react";
+// // Mock hooks and dependencies as needed for isolation
+
+// // ---- Mocks ----
+// const mockReplace = jest.fn();
+// const mockFetchAll = jest.fn();
+// const mockRead = jest.fn();
+// const mockLogConsoleProgress = jest.fn();
+// const mockHandleError = jest.fn();
+
+// const indexMetricsString = `
+// Utilized Single Indexes
+// Index Spec: /foo/?
+// Index Impact Score: High
+// Potential Single Indexes
+// Index Spec: /bar/?
+// Index Impact Score: Medium
+// Utilized Composite Indexes
+// Index Spec: /baz/? DESC, /qux/? ASC
+// Index Impact Score: Low
+// `;
+// mockRead.mockResolvedValue({
+// resource: {
+// indexingPolicy: {
+// automatic: true,
+// indexingMode: "consistent",
+// includedPaths: [{ path: "/*" }, { path: "/foo/?" }],
+// excludedPaths: [],
+// },
+// partitionKey: "pk",
+// },
+// });
+// mockReplace.mockResolvedValue({
+// resource: {
+// indexingPolicy: {
+// automatic: true,
+// indexingMode: "consistent",
+// includedPaths: [{ path: "/*" }],
+// excludedPaths: [],
+// },
+// },
+// });
+
+// // ---- Mock Setup ----
+
+// jest.mock("./QueryTabComponent", () => ({
+// useQueryMetadataStore: () => ({
+// userQuery: "SELECT * FROM c",
+// databaseId: "db1",
+// containerId: "col1",
+// }),
+// }));
+// jest.mock("Common/CosmosClient", () => ({
+// client: () => ({
+// database: () => ({
+// container: () => ({
+// items: {
+// query: () => ({
+// fetchAll: mockFetchAll.mockResolvedValueOnce({ indexMetrics: indexMetricsString })
+// ,
+// }),
+// },
+// read: mockRead,
+// replace: mockReplace,
+// }),
+// }),
+// }),
+// }));
+// jest.mock("./indexadv", () => ({
+// useIndexAdvisorStyles: () => ({}),
+// }));
+
+// jest.mock("../../../Utils/NotificationConsoleUtils", () => ({
+// logConsoleProgress: (...args: unknown[]) => {
+// mockLogConsoleProgress(...args); // This ensures the mock is called
+// return () => { }; // Return a dummy function if needed
+// },
+// }));
+
+// jest.mock("../../../Common/ErrorHandlingUtils", () => {
+// return {
+// handleError: (...args: unknown[]) => mockHandleError(...args),
+// };
+// });
+
+// //done
+// test("logs progress message when fetching index metrics", async () => {
+// render();
+// await waitFor(() =>
+// expect(mockLogConsoleProgress).toHaveBeenCalledWith(expect.stringContaining("IndexMetrics"))
+// );
+// console.log("Calls:", mockLogConsoleProgress.mock.calls);
+
+// });
+// //done
+// // This test checks that after loading, both index sections and their items are rendered.
+// test("renders both Included and Not Included sections after loading", async () => {
+// render();
+// await waitFor(() => expect(screen.getByText("Included in Current Policy")).toBeInTheDocument());
+// expect(screen.getByText("Not Included in Current Policy")).toBeInTheDocument();
+// expect(screen.getByText("/foo/?")).toBeInTheDocument();
+// expect(screen.getByText("/bar/?")).toBeInTheDocument();
+// });
+// //done
+// test("shows update button only when an index is selected", async () => {
+// render();
+// await waitFor(() => expect(screen.getByText("/bar/?")).toBeInTheDocument());
+// // Find the checkbox for the not included index
+// const checkboxes = screen.getAllByRole("checkbox");
+// fireEvent.click(checkboxes[1]); // Select /bar/?
+// expect(screen.getByText(/Update Indexing Policy/)).toBeInTheDocument();
+// fireEvent.click(checkboxes[1]); // Deselect /bar/?
+// expect(screen.queryByText(/Update Indexing Policy/)).not.toBeInTheDocument();
+// });
+// //done
+// // 7. Update policy triggers replace
+// test("calls replace when update policy is confirmed", async () => {
+// render();
+// await waitFor(() => expect(screen.getByText("/bar/?")).toBeInTheDocument());
+// const checkboxes = screen.getAllByRole("checkbox");
+// fireEvent.click(checkboxes[1]);
+// const updateButton = screen.getByText(/Update Indexing Policy/);
+// fireEvent.click(updateButton);
+// await waitFor(() => expect(mockReplace).toHaveBeenCalled());
+// console.log("mockReplace calls:", mockReplace.mock.calls);
+// });
+// //done same above
+// test("calls replace when update button is clicked", async () => {
+// const cosmos = require("Common/CosmosClient");
+// const mockReplace = cosmos.client().database().container().replace;
+// render();
+// await waitFor(() => expect(screen.getByText("/bar/?")).toBeInTheDocument());
+// const checkboxes = screen.getAllByRole("checkbox");
+// fireEvent.click(checkboxes[1]); // Select /bar/?
+// fireEvent.click(screen.getByText(/Update Indexing Policy/));
+// await waitFor(() => expect(mockReplace).toHaveBeenCalled());
+// console.log("mockReplace calls:", mockReplace.mock.calls);
+// });
+// //done
+// // 8. Indexing policy is fetched via read
+// test("fetches indexing policy via read", async () => {
+// render();
+// await waitFor(() => {
+// console.log("mockRead calls:", mockRead.mock.calls);
+// expect(mockRead).toHaveBeenCalled();
+// });
+// });
+// //done same
+// // 5. Checkbox selection toggles update button same
+// test("shows update button only when an index is selected", async () => {
+// render();
+// await waitFor(() => expect(screen.getByText("/bar/?")).toBeInTheDocument());
+// // screen.debug(); // 🔍 See what’s rendered
+// const checkboxes = screen.getAllByRole("checkbox");
+// expect(checkboxes.length).toBeGreaterThan(1);
+// fireEvent.click(checkboxes[1]);
+// expect(screen.getByText(/Update Indexing Policy/)).toBeInTheDocument();
+
+// fireEvent.click(checkboxes[1]);
+// expect(screen.queryByText(/Update Indexing Policy/)).not.toBeInTheDocument();
+// });
+
+// //done
+// test("selects all indexes when select-all is clicked", async () => {
+// render();
+// await waitFor(() => expect(screen.getByText("/bar/?")).toBeInTheDocument());
+// // screen.debug(); // 🔍 See what’s rendered
+// const checkboxes = screen.getAllByRole("checkbox");
+// console.log("Checkbox count:", checkboxes.length);
+
+// fireEvent.click(checkboxes[0]); // Assuming first is select-all
+// checkboxes.forEach((cb, i) => {
+// expect(cb).toBeChecked();
+// });
+// });
+// //done
+// // 1. Tab loads and spinner shows
+// test("shows spinner while loading and hides after fetchIndexMetrics resolves", async () => {
+// render();
+// expect(screen.getByRole("progressbar")).toBeInTheDocument();
+// await waitFor(() => expect(screen.queryByRole("progressbar")).not.toBeInTheDocument());
+// console.log("Spinner visibility test passed");
+// });
+// //done
+// // // 2. SDK fetchAll is called
+// test("calls fetchAll with correct query and options", async () => {
+// render();
+// await waitFor(() => expect(mockFetchAll).toHaveBeenCalled());
+// console.log("fetchAll called times:", mockFetchAll.mock.calls.length);
+// console.log("fetchAll called with args:", mockFetchAll.mock.calls[0]);
+// });
+// //done
+// // // 3. Index metrics are rendered
+// test("renders index metrics from SDK response", async () => {
+// render();
+// await waitFor(() => expect(screen.getByText("/foo/?")).toBeInTheDocument());
+// expect(screen.getByText("/bar/?")).toBeInTheDocument();
+// expect(screen.getByText("/baz/? DESC, /qux/? ASC")).toBeInTheDocument();
+// });
+// //done
+// // 9. Error handling if fetch fails
+// test("calls handleError if fetchIndexMetrics throws", async () => {
+// mockFetchAll.mockRejectedValueOnce(new Error("fail"));
+// render();
+
+// console.log("Error handler called:", mockHandleError.mock.calls.length);
+// await waitFor(() => expect(mockHandleError).toHaveBeenCalled());
+// });
+// //done same
+// test("calls handleError if fetchIndexMetrics throws2nd", async () => {
+// const cosmos = require("Common/CosmosClient");
+// mockFetchAll.mockRejectedValueOnce(new Error("fail"));
+
+// render();
+// await waitFor(() => expect(mockHandleError).toHaveBeenCalled()); // use your mock directly
+// console.log("Error handler called:", mockHandleError.mock.calls.length);
+// expect(screen.queryByRole("status")).not.toBeInTheDocument();
+// });
+
+// //10 indexing policy updates after replace is triggered
+// test("updates indexing policy after replace is triggered", async () => {
+// render();
+// screen.debug(); // Inspect the DOM
+
+// const barIndexText = await screen.findByText((content) =>
+// content.includes("/bar/?")
+// );
+// expect(barIndexText).toBeInTheDocument();
+
+// const checkboxes = screen.getAllByRole("checkbox");
+// fireEvent.click(checkboxes[1]); // Select /bar/?
+
+// const updateButton = screen.getByText(/Update Indexing Policy/);
+// fireEvent.click(updateButton);
+
+// await waitFor(() => expect(mockReplace).toHaveBeenCalled());
+
+// const updatedPolicy = mockReplace.mock.calls[0][0];
+// expect(updatedPolicy).toBeDefined();
+// expect(updatedPolicy.indexingPolicy.includedPaths).toEqual(
+// expect.arrayContaining([{ path: "/*" }, { path: "/bar/?" }])
+// );
+
+// console.log("Indexing policy updated:", updatedPolicy);
+// });
+
+// //done
+// //11 renders IndexAdvisorTab when clicked from ResultsView
+// test("renders IndexAdvisorTab when clicked from ResultsView", async () => {
+// // Simulate navigation or tab click
+// render();
+// await waitFor(() => expect(screen.getByText("Included in Current Policy")).toBeInTheDocument());
+// expect(screen.getByText("/foo/?")).toBeInTheDocument();
+// });
+// //done
+// //12 indexingPolicyStore stores updated policy on componentDidMount
+// test("IndexingPolicyStore stores updated policy on componentDidMount", async () => {
+// render();
+// await waitFor(() => expect(mockRead).toHaveBeenCalled());
+
+// const readResult = await mockRead.mock.results[0].value;
+// const policy = readResult.resource.indexingPolicy;
+
+// expect(policy).toBeDefined();
+// expect(policy.automatic).toBe(true);
+// expect(policy.indexingMode).toBe("consistent");
+// expect(policy.includedPaths).toEqual(expect.arrayContaining([{ path: "/*" }, { path: "/foo/?" }]));
+// console.log("Indexing policy stored:", policy);
+// });
+
+// // done
+// test("refreshCollectionData updates observable and re-renders", async () => {
+// render();
+// await waitFor(() => expect(screen.getByText("/bar/?")).toBeInTheDocument());
+
+// // Simulate refreshCollectionData logic
+// const checkboxes = screen.getAllByRole("checkbox");
+// fireEvent.click(checkboxes[1]); // Select /bar/?
+// fireEvent.click(screen.getByText(/Update Indexing Policy/));
+
+// await waitFor(() => expect(mockReplace).toHaveBeenCalled());
+// expect(screen.getByText("/bar/?")).toBeInTheDocument(); // Confirm re-render
+// console.log("Collection data refreshed and re-rendered", mockReplace.mock.calls);
+// });
+import "@testing-library/jest-dom";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { IndexAdvisorTab } from "Explorer/Tabs/QueryTab/ResultsView";
+import React from "react";
+
+// ---- Mocks ----
+const mockReplace = jest.fn();
+const mockFetchAll = jest.fn();
+const mockRead = jest.fn();
+const mockLogConsoleProgress = jest.fn();
+const mockHandleError = jest.fn();
+
+const indexMetricsString = `
+Utilized Single Indexes
+Index Spec: /foo/?
+Index Impact Score: High
+Potential Single Indexes
+Index Spec: /bar/?
+Index Impact Score: Medium
+Utilized Composite Indexes
+Index Spec: /baz/? DESC, /qux/? ASC
+Index Impact Score: Low
+`;
+
+jest.mock("./QueryTabComponent", () => ({
+ useQueryMetadataStore: () => ({
+ userQuery: "SELECT * FROM c",
+ databaseId: "db1",
+ containerId: "col1",
+ }),
+}));
+jest.mock("Common/CosmosClient", () => ({
+ client: () => ({
+ database: () => ({
+ container: () => ({
+ items: {
+ query: () => ({
+ fetchAll: mockFetchAll.mockResolvedValueOnce({ indexMetrics: indexMetricsString }),
+ }),
+ },
+ read: mockRead,
+ replace: mockReplace,
+ }),
+ }),
+ }),
+}));
+jest.mock("./indexadv", () => ({
+ useIndexAdvisorStyles: () => ({}),
+}));
+jest.mock("../../../Utils/NotificationConsoleUtils", () => ({
+ logConsoleProgress: (...args: unknown[]) => {
+ mockLogConsoleProgress(...args);
+ return () => { };
+ },
+}));
+jest.mock("../../../Common/ErrorHandlingUtils", () => ({
+ handleError: (...args: unknown[]) => mockHandleError(...args),
+}));
+
+beforeEach(() => {
+ mockRead.mockResolvedValue({
+ resource: {
+ indexingPolicy: {
+ automatic: true,
+ indexingMode: "consistent",
+ includedPaths: [{ path: "/*" }, { path: "/foo/?" }],
+ excludedPaths: [],
+ },
+ partitionKey: "pk",
+ },
+ });
+ mockReplace.mockResolvedValue({
+ resource: {
+ indexingPolicy: {
+ automatic: true,
+ indexingMode: "consistent",
+ includedPaths: [{ path: "/*" }],
+ excludedPaths: [],
+ },
+ },
+ });
+ jest.clearAllMocks();
+});
+
+describe("IndexAdvisorTab", () => {
+ test("logs progress message when fetching index metrics", async () => {
+ render();
+ await waitFor(() =>
+ expect(mockLogConsoleProgress).toHaveBeenCalledWith(expect.stringContaining("IndexMetrics"))
+ );
+ });
+
+ test("renders both Included and Not Included sections after loading", async () => {
+ render();
+ await waitFor(() => expect(screen.getByText("Included in Current Policy")).toBeInTheDocument());
+ expect(screen.getByText("Not Included in Current Policy")).toBeInTheDocument();
+ expect(screen.getByText("/foo/?")).toBeInTheDocument();
+ expect(screen.getByText("/bar/?")).toBeInTheDocument();
+ });
+
+ test("shows update button only when an index is selected", async () => {
+ render();
+ await waitFor(() => expect(screen.getByText("/bar/?")).toBeInTheDocument());
+ const checkboxes = screen.getAllByRole("checkbox");
+ fireEvent.click(checkboxes[1]);
+ expect(screen.getByText(/Update Indexing Policy/)).toBeInTheDocument();
+ fireEvent.click(checkboxes[1]);
+ expect(screen.queryByText(/Update Indexing Policy/)).not.toBeInTheDocument();
+ });
+
+ test("calls replace when update policy is confirmed", async () => {
+ render();
+ await waitFor(() => expect(screen.getByText("/bar/?")).toBeInTheDocument());
+ const checkboxes = screen.getAllByRole("checkbox");
+ fireEvent.click(checkboxes[1]);
+ const updateButton = screen.getByText(/Update Indexing Policy/);
+ fireEvent.click(updateButton);
+ await waitFor(() => expect(mockReplace).toHaveBeenCalled());
+ });
+
+ test("calls replace when update button is clicked", async () => {
+ const cosmos = require("Common/CosmosClient");
+ const mockReplaceLocal = cosmos.client().database().container().replace;
+ render();
+ await waitFor(() => expect(screen.getByText("/bar/?")).toBeInTheDocument());
+ const checkboxes = screen.getAllByRole("checkbox");
+ fireEvent.click(checkboxes[1]);
+ fireEvent.click(screen.getByText(/Update Indexing Policy/));
+ await waitFor(() => expect(mockReplaceLocal).toHaveBeenCalled());
+ });
+
+ test("fetches indexing policy via read", async () => {
+ render();
+ await waitFor(() => expect(mockRead).toHaveBeenCalled());
+ });
+
+ test("shows update button only when an index is selected (again)", async () => {
+ render();
+ await waitFor(() => expect(screen.getByText("/bar/?")).toBeInTheDocument());
+ const checkboxes = screen.getAllByRole("checkbox");
+ expect(checkboxes.length).toBeGreaterThan(1);
+ fireEvent.click(checkboxes[1]);
+ expect(screen.getByText(/Update Indexing Policy/)).toBeInTheDocument();
+ fireEvent.click(checkboxes[1]);
+ expect(screen.queryByText(/Update Indexing Policy/)).not.toBeInTheDocument();
+ });
+
+ test("selects all indexes when select-all is clicked", async () => {
+ render();
+ await waitFor(() => expect(screen.getByText("/bar/?")).toBeInTheDocument());
+ const checkboxes = screen.getAllByRole("checkbox");
+ fireEvent.click(checkboxes[0]);
+ checkboxes.forEach((cb) => expect(cb).toBeChecked());
+ });
+
+ test("shows spinner while loading and hides after fetchIndexMetrics resolves", async () => {
+ render();
+ expect(screen.getByRole("progressbar")).toBeInTheDocument();
+ await waitFor(() => expect(screen.queryByRole("progressbar")).not.toBeInTheDocument());
+ });
+
+ test("calls fetchAll with correct query and options", async () => {
+ render();
+ await waitFor(() => expect(mockFetchAll).toHaveBeenCalled());
+ });
+
+ test("renders index metrics from SDK response", async () => {
+ render();
+ await waitFor(() => expect(screen.getByText("/foo/?")).toBeInTheDocument());
+ expect(screen.getByText("/bar/?")).toBeInTheDocument();
+ expect(screen.getByText("/baz/? DESC, /qux/? ASC")).toBeInTheDocument();
+ });
+
+ test("calls handleError if fetchIndexMetrics throws", async () => {
+ mockFetchAll.mockRejectedValueOnce(new Error("fail"));
+ render();
+ await waitFor(() => expect(mockHandleError).toHaveBeenCalled());
+ });
+
+ test("calls handleError if fetchIndexMetrics throws (again)", async () => {
+ mockFetchAll.mockRejectedValueOnce(new Error("fail"));
+ render();
+ await waitFor(() => expect(mockHandleError).toHaveBeenCalled());
+ expect(screen.queryByRole("status")).not.toBeInTheDocument();
+ });
+
+ test("updates indexing policy after replace is triggered", async () => {
+ render();
+ await waitFor(() => expect(screen.getByText("/bar/?")).toBeInTheDocument());
+ const checkboxes = screen.getAllByRole("checkbox");
+ fireEvent.click(checkboxes[1]);
+ const updateButton = screen.getByText(/Update Indexing Policy/);
+ fireEvent.click(updateButton);
+ await waitFor(() => expect(mockReplace).toHaveBeenCalled());
+ const updatedPolicy = mockReplace.mock.calls[0][0];
+ expect(updatedPolicy).toBeDefined();
+ expect(updatedPolicy.indexingPolicy.includedPaths).toEqual(
+ expect.arrayContaining([{ path: "/*" }, { path: "/bar/?" }])
+ );
+ });
+
+ test("renders IndexAdvisorTab when clicked from ResultsView", async () => {
+ render();
+ await waitFor(() => expect(screen.getByText("Included in Current Policy")).toBeInTheDocument());
+ expect(screen.getByText("/foo/?")).toBeInTheDocument();
+ });
+
+ test("IndexingPolicyStore stores updated policy on componentDidMount", async () => {
+ render();
+ await waitFor(() => expect(mockRead).toHaveBeenCalled());
+ const readResult = await mockRead.mock.results[0].value;
+ const policy = readResult.resource.indexingPolicy;
+ expect(policy).toBeDefined();
+ expect(policy.automatic).toBe(true);
+ expect(policy.indexingMode).toBe("consistent");
+ expect(policy.includedPaths).toEqual(expect.arrayContaining([{ path: "/*" }, { path: "/foo/?" }]));
+ });
+
+ test("refreshCollectionData updates observable and re-renders", async () => {
+ render();
+ await waitFor(() => expect(screen.getByText("/bar/?")).toBeInTheDocument());
+ const checkboxes = screen.getAllByRole("checkbox");
+ fireEvent.click(checkboxes[1]);
+ fireEvent.click(screen.getByText(/Update Indexing Policy/));
+ await waitFor(() => expect(mockReplace).toHaveBeenCalled());
+ expect(screen.getByText("/bar/?")).toBeInTheDocument();
+ });
+});
diff --git a/src/Explorer/Tabs/QueryTab/ResultsView.tsx b/src/Explorer/Tabs/QueryTab/ResultsView.tsx
index 522327711..b1093add8 100644
--- a/src/Explorer/Tabs/QueryTab/ResultsView.tsx
+++ b/src/Explorer/Tabs/QueryTab/ResultsView.tsx
@@ -28,6 +28,7 @@ import { HttpHeaders } from "Common/Constants";
import MongoUtility from "Common/MongoUtility";
import { QueryMetrics } from "Contracts/DataModels";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
+import { parseIndexMetrics } 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";
@@ -38,7 +39,6 @@ import { client } from "../../../Common/CosmosClient";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import { useIndexAdvisorStyles } from "./Indexadvisor";
import { ResultsViewProps } from "./QueryResultSection";
-
enum ResultsTabs {
Results = "results",
QueryStats = "queryStats",
@@ -536,7 +536,7 @@ const QueryStatsTab: React.FC> = ({ query
);
};
-interface IIndexMetric {
+export interface IIndexMetric {
index: string;
impact: string;
section: "Included" | "Not Included" | "Header";
@@ -556,7 +556,7 @@ export const IndexAdvisorTab: React.FC = () => {
const [included, setIncludedIndexes] = useState([]);
const [notIncluded, setNotIncludedIndexes] = useState([]);
const [isUpdating, setIsUpdating] = useState(false);
-
+ const [justUpdatedPolicy, setJustUpdatedPolicy] = useState(false);
useEffect(() => {
async function fetchIndexMetrics() {
const clearMessage = logConsoleProgress(`Querying items with IndexMetrics in container ${containerId}`);
@@ -587,56 +587,14 @@ export const IndexAdvisorTab: React.FC = () => {
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);
- }
- }
- }
+ const { included, notIncluded } = parseIndexMetrics(indexMetrics);
setIncludedIndexes(included);
setNotIncludedIndexes(notIncluded);
+ if (justUpdatedPolicy) {
+ setJustUpdatedPolicy(false);
+ } else {
+ setUpdateMessageShown(false);
+ }
}, [indexMetrics]);
useEffect(() => {
@@ -719,6 +677,7 @@ export const IndexAdvisorTab: React.FC = () => {
setSelectedIndexes([]);
setSelectAll(false);
setUpdateMessageShown(true);
+ setJustUpdatedPolicy(true);
} catch (err) {
console.error("Failed to update indexing policy:", err);
} finally {