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 {