mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-28 13:21:42 +00:00
overwrite fixes
This commit is contained in:
@@ -2,91 +2,86 @@ import { CircleFilled } from "@fluentui/react-icons";
|
||||
import type { IIndexMetric } from "Explorer/Tabs/QueryTab/ResultsView";
|
||||
import { useIndexAdvisorStyles } from "Explorer/Tabs/QueryTab/StylesAdvisor";
|
||||
import * as React from "react";
|
||||
interface IndexObject {
|
||||
index: string;
|
||||
impact: string;
|
||||
section: "Included" | "Not Included" | "Header";
|
||||
composite?: {
|
||||
path: string;
|
||||
order: "ascending" | "descending";
|
||||
}[];
|
||||
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: string | IndexMetricsJson): {
|
||||
export function parseIndexMetrics(indexMetrics: IndexMetricsResponse): {
|
||||
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(",");
|
||||
// Process UtilizedIndexes (Included in Current Policy)
|
||||
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 sectionMap: Record<string, "Included" | "Not Included"> = {
|
||||
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+/);
|
||||
// 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+/);
|
||||
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 in Current Policy)
|
||||
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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,9 @@ import copy from "clipboard-copy";
|
||||
import { HttpHeaders } from "Common/Constants";
|
||||
import MongoUtility from "Common/MongoUtility";
|
||||
import { QueryMetrics } from "Contracts/DataModels";
|
||||
import { QueryResults } from "Contracts/ViewModels";
|
||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||
import { parseIndexMetrics, renderImpactDots } from "Explorer/Tabs/QueryTab/IndexAdvisorUtils";
|
||||
import { parseIndexMetrics, renderImpactDots, type IndexMetricsResponse } 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";
|
||||
@@ -394,9 +395,8 @@ const QueryStatsTab: React.FC<Pick<ResultsViewProps, "queryResults">> = ({ 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",
|
||||
},
|
||||
{
|
||||
@@ -544,11 +544,11 @@ export interface IIndexMetric {
|
||||
path?: string;
|
||||
composite?: { path: string; order: string }[];
|
||||
}
|
||||
export const IndexAdvisorTab: React.FC = () => {
|
||||
export const IndexAdvisorTab: React.FC<{ queryResults?: QueryResults }> = ({ queryResults }) => {
|
||||
const style = useIndexAdvisorStyles();
|
||||
const { userQuery, databaseId, containerId } = useQueryMetadataStore();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [indexMetrics, setIndexMetrics] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [indexMetrics, setIndexMetrics] = useState<IndexMetricsResponse | null>(null);
|
||||
const [showIncluded, setShowIncluded] = useState(true);
|
||||
const [showNotIncluded, setShowNotIncluded] = useState(true);
|
||||
const [selectedIndexes, setSelectedIndexes] = useState<IIndexMetric[]>([]);
|
||||
@@ -560,32 +560,47 @@ export const IndexAdvisorTab: React.FC = () => {
|
||||
const [justUpdatedPolicy, setJustUpdatedPolicy] = useState(false);
|
||||
const indexingMetricsDocLink = "https://learn.microsoft.com/azure/cosmos-db/nosql/index-metrics";
|
||||
|
||||
const fetchIndexMetrics = async () => {
|
||||
if (!userQuery || !databaseId || !containerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const clearMessage = logConsoleProgress(`Querying items with IndexMetrics in container ${containerId}`);
|
||||
try {
|
||||
const containerRef = client().database(databaseId).container(containerId);
|
||||
const { resource: containerDef } = await containerRef.read();
|
||||
|
||||
const querySpec = {
|
||||
query: userQuery,
|
||||
};
|
||||
const sdkResponse = await client()
|
||||
.database(databaseId)
|
||||
.container(containerId)
|
||||
.items.query(querySpec, {
|
||||
populateIndexMetrics: true,
|
||||
})
|
||||
.fetchAll();
|
||||
|
||||
const parsedMetrics = typeof sdkResponse.indexMetrics === 'string'
|
||||
? JSON.parse(sdkResponse.indexMetrics)
|
||||
: sdkResponse.indexMetrics;
|
||||
|
||||
setIndexMetrics(parsedMetrics);
|
||||
} catch (error) {
|
||||
handleError(error, "queryItemsWithIndexMetrics", `Error querying items from ${containerId}`);
|
||||
} finally {
|
||||
clearMessage();
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch index metrics when query results change (i.e., when Execute Query is clicked)
|
||||
useEffect(() => {
|
||||
const fetchIndexMetrics = async () => {
|
||||
const clearMessage = logConsoleProgress(`Querying items with IndexMetrics in container ${containerId}`);
|
||||
try {
|
||||
const querySpec = {
|
||||
query: userQuery,
|
||||
};
|
||||
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) {
|
||||
if (userQuery && databaseId && containerId && queryResults) {
|
||||
fetchIndexMetrics();
|
||||
}
|
||||
}, [userQuery, databaseId, containerId]);
|
||||
}, [queryResults]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!indexMetrics) {
|
||||
@@ -869,7 +884,7 @@ export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResult
|
||||
/>
|
||||
)}
|
||||
{activeTab === ResultsTabs.QueryStats && <QueryStatsTab queryResults={queryResults} />}
|
||||
{activeTab === ResultsTabs.IndexAdvisor && <IndexAdvisorTab />}
|
||||
{activeTab === ResultsTabs.IndexAdvisor && <IndexAdvisorTab queryResults={queryResults} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user