Initial changes for index

This commit is contained in:
Archie Agarwal 2025-06-11 18:17:23 +05:30
parent 64533b445f
commit dc8bb69359
3 changed files with 447 additions and 7 deletions

View File

@ -55,6 +55,8 @@ import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPa
import { SaveQueryPane } from "../../Panes/SaveQueryPane/SaveQueryPane";
import TabsBase from "../TabsBase";
import "./QueryTabComponent.less";
import { useQueryMetadataStore } from "./useQueryMetadataStore"; // adjust path if needed
enum ToggleState {
Result,
@ -196,7 +198,10 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
enabled: !!this.state.sqlQueryEditorContent && this.state.sqlQueryEditorContent.length > 0,
visible: true,
};
// const query=this.state.sqlQueryEditorContent;
// const db = this.props.collection.databaseId;
// const container = this.props.collection.id();
const isSaveQueryBtnEnabled = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
this.saveQueryButton = {
enabled: isSaveQueryBtnEnabled,
@ -260,6 +265,16 @@ class QueryTabComponentImpl extends React.Component<QueryTabComponentImplProps,
}
public onExecuteQueryClick = async (): Promise<void> => {
// console.log("i am ",this.props.collection.databaseId);
// console.log("I am query",this.state.sqlQueryEditorContent );
// console.log("i am",this.props.collection.id());
const query1=this.state.sqlQueryEditorContent;
const db = this.props.collection.databaseId;
const container = this.props.collection.id();
useQueryMetadataStore.getState().setMetadata(query1, db, container);
console.log("i am ",query1);
console.log("I am db",db);
console.log("i am cnt",container);
this._iterator = undefined;
setTimeout(async () => {

View File

@ -1,5 +1,7 @@
import { FontIcon } from "@fluentui/react";
import {
Button,
Checkbox,
DataGrid,
DataGridBody,
DataGridCell,
@ -8,26 +10,36 @@ import {
DataGridRow,
SelectTabData,
SelectTabEvent,
Spinner,
Tab,
TabList,
Table,
TableBody,
TableCell,
TableColumnDefinition,
createTableColumn,
TableHeader,
TableRow,
createTableColumn
} from "@fluentui/react-components";
import { ArrowDownloadRegular, CopyRegular } from "@fluentui/react-icons";
import { ArrowDownloadRegular, ChevronDown20Regular, ChevronRight20Regular, CircleFilled, CopyRegular } from "@fluentui/react-icons";
import copy from "clipboard-copy";
import { HttpHeaders } from "Common/Constants";
import MongoUtility from "Common/MongoUtility";
import { QueryMetrics } from "Contracts/DataModels";
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
import { IDocument } from "Explorer/Tabs/QueryTab/QueryTabComponent";
import { useQueryTabStyles } from "Explorer/Tabs/QueryTab/Styles";
import { useQueryMetadataStore } from "Explorer/Tabs/QueryTab/useQueryMetadataStore";
import React, { useCallback, useEffect, useState } from "react";
import { userContext } from "UserContext";
import copy from "clipboard-copy";
import React, { useCallback, useState } from "react";
import { logConsoleProgress } from "Utils/NotificationConsoleUtils";
import { client } from "../../../Common/CosmosClient";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import { ResultsViewProps } from "./QueryResultSection";
enum ResultsTabs {
Results = "results",
QueryStats = "queryStats",
IndexAdvisor="indexadv",
}
const ResultsTab: React.FC<ResultsViewProps> = ({ queryResults, isMongoDB, executeQueryDocumentsPage }) => {
@ -523,11 +535,400 @@ const QueryStatsTab: React.FC<Pick<ResultsViewProps, "queryResults">> = ({ query
);
};
interface IIndexMetric {
index: string;
impact: string;
section: "Included" | "Not Included" | "Header";
}
const IndexAdvisorTab: React.FC = () => {
const {userQuery, databaseId, containerId}=useQueryMetadataStore();
const [loading, setLoading] = useState(true);
const [indexMetrics, setIndexMetrics] = useState<any>(null);
const [showIncluded, setShowIncluded] = useState(true);
const [showNotIncluded, setShowNotIncluded] = useState(true);
const [selectedIndexes, setSelectedIndexes] = useState<any[]>([]);
const [selectAll, setSelectAll] = useState(false);
const [updateMessageShown, setUpdateMessageShown] = useState(false);
const [included, setIncludedIndexes] = useState<IIndexMetric[]>([]);
const [notIncluded, setNotIncludedIndexes] = useState<IIndexMetric[]>([]);
useEffect(() => {
async function fetchIndexMetrics() {
const clearMessage = logConsoleProgress(`Querying items with IndexMetrics in container ${containerId}`);
try {
const querySpec = {
query: userQuery || "SELECT TOP 10 c.id FROM c WHERE c.Item = 'value1234' ",
};
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();
}
}, [userQuery, databaseId, containerId]);
useEffect(() => {
if (!indexMetrics) return ;
const included: any[] = [];
const notIncluded: any[] = [];
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(() => {
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) => {
if (checked) {
setSelectedIndexes((prev) => [...prev, indexObj]);
} else {
setSelectedIndexes((prev) =>
prev.filter((item) => item.index !== indexObj.index)
);
}
};
const handleSelectAll = (checked: boolean) => {
setSelectAll(checked);
setSelectedIndexes(checked ? notIncluded : []);
};
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();
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 updatedPolicy = {
...containerDef.indexingPolicy,
includedPaths: [
...(containerDef.indexingPolicy?.includedPaths || []),
...newIncludedPaths,
],
compositeIndexes: [
...(containerDef.indexingPolicy?.compositeIndexes || []),
...newCompositeIndexes,
],
};
await client()
.database(databaseId)
.container(containerId)
.replace({
id: containerId,
partitionKey: containerDef.partitionKey,
indexingPolicy: updatedPolicy,
});
console.log("Indexing policy successfully updated:", updatedPolicy);
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)
);
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(
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
{Array.from({ length: count }).map((_, i) => (
<CircleFilled
key={i}
style={{
color: "#0078D4",
fontSize: "12px",
display: "inline-flex",
}}
/>
))}
</div>
);
};
const renderRow = (item: IIndexMetric, index: number) => {
const isHeader = item.section === "Header";
const isNotIncluded = item.section === "Not Included";
const isIncluded = item.section === "Included";
return (
<TableRow key={index}>
<TableCell colSpan={2}>
<div
style={{
display: "grid",
gridTemplateColumns: "30px 30px 1fr 50px 120px",
alignItems: "center",
gap: "8px",
}}>
{isNotIncluded ? (
<Checkbox
checked={selectedIndexes.some((selected)=>selected.index===item.index)}
onChange={(_, data) => handleCheckboxChange(item, data.checked === true)}
/>
) : isHeader && item.index === "Not Included in Current Policy" && notIncluded.length > 0 ? (
<Checkbox
checked={selectAll}
onChange={(_, data) => handleSelectAll(data.checked === true)}
/>
) : (
<div style={{ width: "18px", height: "18px" }}></div>
)}
{isHeader ? (
<span
style={{ cursor: "pointer" }}
onClick={() => {
if (item.index === "Included in Current Policy") {
setShowIncluded(!showIncluded);
} else if (item.index === "Not Included in Current Policy") {
setShowNotIncluded(!showNotIncluded);
}
}}
>
{item.index === "Included in Current Policy"
? showIncluded
? <ChevronDown20Regular />
: <ChevronRight20Regular />
: showNotIncluded
? <ChevronDown20Regular />
: <ChevronRight20Regular />
}
</span>
) : (
<div style={{ width: "24px" }}></div>
)}
<div style={{ fontWeight: isHeader ? "bold" : "normal" }}>
{item.index}
</div>
<div style={{ fontSize: isHeader ? 0 : undefined }}>
{isHeader ? null : item.impact}
</div>
<div>
{isHeader ? null : renderImpactDots(item.impact)}
</div>
</div>
</TableCell>
</TableRow>
);
};
const generateIndexMetricItems = (
included: { index: string; impact: string }[],
notIncluded: { index: string; impact: string }[]
): IIndexMetric[] => {
const items: IIndexMetric[] = [];
items.push({ index: "Not Included in Current Policy", impact: "", section: "Header" });
if (showNotIncluded) {
notIncluded.forEach((item) =>
items.push({ ...item, section: "Not Included" })
);
}
items.push({ index: "Included in Current Policy", impact: "", section: "Header" });
if (showIncluded) {
included.forEach((item) =>
items.push({ ...item, section: "Included" })
);
}
return items;
};
if (loading) {
return <div>
<Spinner
size="small"
style={{
'--spinner-size': '16px',
'--spinner-thickness': '2px',
'--spinner-color': '#0078D4',
} as React.CSSProperties}/>
</div>;}
return (
<div>
<div style={{ padding: "1rem", fontSize: "1.2rem", display: "flex", alignItems: "center", gap: "0.5rem" }}>
{updateMessageShown ? (
<>
<span
style={{
width: 18,
height: 18,
borderRadius: "50%",
backgroundColor: "#107C10",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}>
<FontIcon iconName="CheckMark" style={{ color: "white", fontSize: 12 }} />
</span>
<span>
Your indexing policy has been updated with the new included paths. You may review the changes in Scale & Settings.
</span>
</>
) : (
"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."
)}
</div>
<div style={{ padding: "1rem", fontSize: "1.3rem", fontWeight: "bold" }}>Indexes analysis</div>
<Table style={{ display: "block", alignItems: "center",marginBottom:"7rem" }}>
<TableHeader>
<TableRow >
<TableCell colSpan={2}>
<div
style={{
display: "grid",
gridTemplateColumns: "30px 30px 1fr 50px 120px",
alignItems: "center",
gap: "8px",
fontWeight: "bold",
}}
>
<div style={{ width: "18px", height: "18px" }}></div> {/* Checkbox column */}
<div style={{ width: "24px" }}></div> {/* Chevron column */}
<div>Index</div>
<div><span style={{ whiteSpace: "nowrap" }}>Estimated Impact</span></div>
</div>
</TableCell>
</TableRow>
</TableHeader>
<TableBody>
{generateIndexMetricItems(included, notIncluded).map(renderRow)}
</TableBody>
</Table>
{selectedIndexes.length > 0 && (
<div style={{ padding: "1rem", marginTop: "-7rem",flexWrap:"wrap"}}>
<button
onClick={handleUpdatePolicy}
style={{
backgroundColor: "#0078D4",
color: "white",
padding: "8px 16px",
border: "none",
borderRadius: "4px",
cursor: "pointer",
marginTop: "1rem",
}}
>
Update Indexing Policy with selected index(es)
</button>
</div>
)}
</div>
);
};
export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResults, executeQueryDocumentsPage }) => {
const styles = useQueryTabStyles();
const [activeTab, setActiveTab] = useState<ResultsTabs>(ResultsTabs.Results);
const onTabSelect = useCallback((event: SelectTabEvent, data: SelectTabData) => {
const onTabSelect = useCallback((event: SelectTabEvent, data: SelectTabData) =>{
setActiveTab(data.value as ResultsTabs);
}, []);
@ -548,6 +949,13 @@ export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResult
>
Query Stats
</Tab>
<Tab
data-test="QueryTab/ResultsPane/ResultsView/IndexAdvisorTab"
id={ResultsTabs.IndexAdvisor}
value={ResultsTabs.IndexAdvisor}
>
Index Advisor
</Tab>
</TabList>
<div className={styles.queryResultsTabContentContainer}>
{activeTab === ResultsTabs.Results && (
@ -558,6 +966,7 @@ export const ResultsView: React.FC<ResultsViewProps> = ({ isMongoDB, queryResult
/>
)}
{activeTab === ResultsTabs.QueryStats && <QueryStatsTab queryResults={queryResults} />}
{activeTab === ResultsTabs.IndexAdvisor && <IndexAdvisorTab />}
</div>
</div>
);

View File

@ -0,0 +1,16 @@
import create from "zustand";
interface QueryMetadataStore {
userQuery: string;
databaseId: string;
containerId: string;
setMetadata: (query1: string, db: string, container: string) => void;
}
export const useQueryMetadataStore = create<QueryMetadataStore>((set) => ({
userQuery: "",
databaseId: "",
containerId: "",
setMetadata: (query1, db, container) =>
set({ userQuery: query1, databaseId: db, containerId: container }),
}));