mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-28 06:04:25 +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 type { IIndexMetric } from "Explorer/Tabs/QueryTab/ResultsView";
|
||||||
import { useIndexAdvisorStyles } from "Explorer/Tabs/QueryTab/StylesAdvisor";
|
import { useIndexAdvisorStyles } from "Explorer/Tabs/QueryTab/StylesAdvisor";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
interface IndexObject {
|
|
||||||
index: string;
|
// SDK response format
|
||||||
impact: string;
|
export interface IndexMetricsResponse {
|
||||||
section: "Included" | "Not Included" | "Header";
|
UtilizedIndexes?: {
|
||||||
composite?: {
|
SingleIndexes?: Array<{ IndexSpec: string; IndexImpactScore?: string }>;
|
||||||
path: string;
|
CompositeIndexes?: Array<{ IndexSpecs: string[]; IndexImpactScore?: string }>;
|
||||||
order: "ascending" | "descending";
|
};
|
||||||
}[];
|
PotentialIndexes?: {
|
||||||
path?: string;
|
SingleIndexes?: Array<{ IndexSpec: string; IndexImpactScore?: string }>;
|
||||||
|
CompositeIndexes?: Array<{ IndexSpecs: string[]; IndexImpactScore?: string }>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IndexMetricsJson {
|
export function parseIndexMetrics(indexMetrics: IndexMetricsResponse): {
|
||||||
included?: IIndexMetric[];
|
|
||||||
notIncluded?: IIndexMetric[];
|
|
||||||
}
|
|
||||||
export function parseIndexMetrics(indexMetrics: string | IndexMetricsJson): {
|
|
||||||
included: IIndexMetric[];
|
included: IIndexMetric[];
|
||||||
notIncluded: 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 included: IIndexMetric[] = [];
|
||||||
const notIncluded: 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"> = {
|
// Composite indexes
|
||||||
included: "Included",
|
indexMetrics.UtilizedIndexes.CompositeIndexes?.forEach((index) => {
|
||||||
notIncluded: "Not Included",
|
const compositeSpec = index.IndexSpecs.join(", ");
|
||||||
};
|
included.push({
|
||||||
|
index: compositeSpec,
|
||||||
const indexObj: IndexObject = { index, impact, section: sectionMap[currentSection] ?? "Header" };
|
impact: index.IndexImpactScore || "Utilized",
|
||||||
if (isComposite) {
|
section: "Included",
|
||||||
indexObj.composite = index.split(",").map((part: string) => {
|
composite: index.IndexSpecs.map((spec) => {
|
||||||
const [path, order] = part.trim().split(/\s+/);
|
const [path, order] = spec.trim().split(/\s+/);
|
||||||
return {
|
return {
|
||||||
path: path.trim(),
|
path: path.trim(),
|
||||||
order: order?.toLowerCase() === "desc" ? "descending" : "ascending",
|
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 };
|
return { included, notIncluded };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ import copy from "clipboard-copy";
|
|||||||
import { HttpHeaders } from "Common/Constants";
|
import { HttpHeaders } from "Common/Constants";
|
||||||
import MongoUtility from "Common/MongoUtility";
|
import MongoUtility from "Common/MongoUtility";
|
||||||
import { QueryMetrics } from "Contracts/DataModels";
|
import { QueryMetrics } from "Contracts/DataModels";
|
||||||
|
import { QueryResults } from "Contracts/ViewModels";
|
||||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
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 { IDocument, useQueryMetadataStore } from "Explorer/Tabs/QueryTab/QueryTabComponent";
|
||||||
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
|
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
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",
|
metric: "User defined function execution time",
|
||||||
value: `${
|
value: `${aggregatedQueryMetrics.runtimeExecutionTimes?.userDefinedFunctionExecutionTime?.toString() || 0
|
||||||
aggregatedQueryMetrics.runtimeExecutionTimes?.userDefinedFunctionExecutionTime?.toString() || 0
|
} ms`,
|
||||||
} ms`,
|
|
||||||
toolTip: "Total time spent executing user-defined functions",
|
toolTip: "Total time spent executing user-defined functions",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -544,11 +544,11 @@ export interface IIndexMetric {
|
|||||||
path?: string;
|
path?: string;
|
||||||
composite?: { path: string; order: string }[];
|
composite?: { path: string; order: string }[];
|
||||||
}
|
}
|
||||||
export const IndexAdvisorTab: React.FC = () => {
|
export const IndexAdvisorTab: React.FC<{ queryResults?: QueryResults }> = ({ queryResults }) => {
|
||||||
const style = useIndexAdvisorStyles();
|
const style = useIndexAdvisorStyles();
|
||||||
const { userQuery, databaseId, containerId } = useQueryMetadataStore();
|
const { userQuery, databaseId, containerId } = useQueryMetadataStore();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(false);
|
||||||
const [indexMetrics, setIndexMetrics] = useState<string | null>(null);
|
const [indexMetrics, setIndexMetrics] = useState<IndexMetricsResponse | null>(null);
|
||||||
const [showIncluded, setShowIncluded] = useState(true);
|
const [showIncluded, setShowIncluded] = useState(true);
|
||||||
const [showNotIncluded, setShowNotIncluded] = useState(true);
|
const [showNotIncluded, setShowNotIncluded] = useState(true);
|
||||||
const [selectedIndexes, setSelectedIndexes] = useState<IIndexMetric[]>([]);
|
const [selectedIndexes, setSelectedIndexes] = useState<IIndexMetric[]>([]);
|
||||||
@@ -560,32 +560,47 @@ export const IndexAdvisorTab: React.FC = () => {
|
|||||||
const [justUpdatedPolicy, setJustUpdatedPolicy] = useState(false);
|
const [justUpdatedPolicy, setJustUpdatedPolicy] = useState(false);
|
||||||
const indexingMetricsDocLink = "https://learn.microsoft.com/azure/cosmos-db/nosql/index-metrics";
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchIndexMetrics = async () => {
|
if (userQuery && databaseId && containerId && queryResults) {
|
||||||
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) {
|
|
||||||
fetchIndexMetrics();
|
fetchIndexMetrics();
|
||||||
}
|
}
|
||||||
}, [userQuery, databaseId, containerId]);
|
}, [queryResults]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!indexMetrics) {
|
if (!indexMetrics) {
|
||||||
@@ -869,7 +884,7 @@ export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResult
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{activeTab === ResultsTabs.QueryStats && <QueryStatsTab queryResults={queryResults} />}
|
{activeTab === ResultsTabs.QueryStats && <QueryStatsTab queryResults={queryResults} />}
|
||||||
{activeTab === ResultsTabs.IndexAdvisor && <IndexAdvisorTab />}
|
{activeTab === ResultsTabs.IndexAdvisor && <IndexAdvisorTab queryResults={queryResults} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user