Skip to content

Commit 72ddf92

Browse files
khelanmodiCopilot
andcommitted
feat(collectionView): add Index Management tab
Adds an Indexes tab inside the existing CollectionView (between Results and Query Insights) for managing collection indexes: - Indexes tab in CollectionView (no new webview, no new command, no context-menu entry) - Top-toolbar Create Index button (replaces Find Query on the Indexes tab) - Find Query editor hidden on the Indexes tab - Index table: Name, Type (color pill), Fields, Memory, Usage (tooltip), Created, Notes, Actions (Delete, Hide/Unhide) - Create Index dialog: field picker (SchemaStore), per-field asc/desc, type selector with type-specific options, large-collection warning, validation - Footer with totals; ConfirmDialog for destructive delete - Reuses Fluent UI v9 + existing theme tokens; no new runtime dependencies Backend integration: all UI <-> backend traffic flows through src/webviews/documentdb/indexView/indexViewRouter.ts. Every procedure has a BACKEND INTEGRATION POINT comment block for the backend engineer documenting expected shape, currently-called ClustersClient method, and follow-ups. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 815e5e0 commit 72ddf92

19 files changed

Lines changed: 1992 additions & 57 deletions

File tree

l10n/bundle.l10n.json

Lines changed: 61 additions & 0 deletions
Large diffs are not rendered by default.

src/commands/openCollectionView/openCollectionView.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ export async function openCollectionViewInternal(
5151
skip?: number;
5252
limit?: number;
5353
};
54+
/**
55+
* Optional tab to land on when the view opens. When invoked from the
56+
* "Indexes" tree node we pass `'tab_indexes'` so the user lands
57+
* directly on the Index Management tab instead of Documents.
58+
*/
59+
initialTab?: 'tab_result' | 'tab_indexes' | 'tab_queryInsights';
5460
},
5561
): Promise<void> {
5662
/**
@@ -79,6 +85,7 @@ export async function openCollectionViewInternal(
7985
collectionName: props.collectionName,
8086
feedbackSignalsEnabled: feedbackSignalsEnabled,
8187
initialQuery: props.initialQuery,
88+
initialTab: props.initialTab,
8289
});
8390

8491
// Clean up the ClusterSession when the tab is closed

src/tree/documentdb/IndexesItem.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,29 @@ export class IndexesItem implements TreeElement, TreeElementWithExperience, Tree
137137
contextValue: this.contextValue,
138138
label: l10n.t('Indexes'),
139139
description,
140+
tooltip: l10n.t('Double-click to open the index management view'),
140141
iconPath: new vscode.ThemeIcon('combine'), // TODO: create our onw icon here, this one's shape can change
141142
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
143+
// Reuse the existing collection-view "open from tree" command so we
144+
// pick up the double-click debounce + telemetry wrapping for free.
145+
// Passing `initialTab: 'tab_indexes'` makes the Indexes tab the
146+
// first thing the user sees.
147+
command: {
148+
title: l10n.t('Open Indexes'), // unused, but required by TreeItem
149+
command: 'vscode-documentdb.command.internal.containerView.openFromTree',
150+
arguments: [
151+
{
152+
id: this.id,
153+
viewTitle: `${this.collectionInfo.name}`,
154+
clusterId: this.cluster.clusterId,
155+
clusterDisplayName: this.cluster.name,
156+
viewId: this.cluster.viewId,
157+
databaseName: this.databaseInfo.name,
158+
collectionName: this.collectionInfo.name,
159+
initialTab: 'tab_indexes',
160+
},
161+
],
162+
},
142163
};
143164
}
144165
}

src/webviews/_integration/appRouter.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { openSurvey, promptAfterActionEventually } from '../../utils/survey';
3232
import { UsageImpact } from '../../utils/surveyTypes';
3333
import { collectionsViewRouter as collectionViewRouter } from '../documentdb/collectionView/collectionViewRouter';
3434
import { documentsViewRouter as documentViewRouter } from '../documentdb/documentView/documentsViewRouter';
35+
import { indexViewRouter } from '../documentdb/indexView/indexViewRouter';
3536
import { WEBVIEW_CONFIG } from './configuration';
3637
import { publicProcedure, publicProcedureWithTelemetry, router, type WithTelemetry } from './trpc';
3738

@@ -188,6 +189,7 @@ export const appRouter = router({
188189
mongoClusters: {
189190
documentView: documentViewRouter,
190191
collectionView: collectionViewRouter,
192+
indexView: indexViewRouter,
191193
},
192194
});
193195

src/webviews/documentdb/collectionView/CollectionView.tsx

Lines changed: 57 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { useTrpcClient } from '../../_integration/useTrpcClient';
1313
import { Announcer } from '../../components/accessibility';
1414
import { useSelectiveContextMenuPrevention } from '../../components/useSelectiveContextMenuPrevention';
1515
import { setCompletionContext } from '../../query-language-support';
16+
import { IndexesTab } from '../indexView/IndexesTab';
1617
import './collectionView.scss';
1718
import {
1819
CollectionViewContext,
@@ -122,8 +123,12 @@ export const CollectionView = (): JSX.Element => {
122123
// TODO: it's a potential data duplication in the end, consider moving it into the global context of the view
123124
const [currentQueryResults, setCurrentQueryResults] = useState<QueryResults>();
124125

125-
// Track which tab is currently active
126-
const [selectedTab, setSelectedTab] = useState<'tab_result' | 'tab_queryInsights'>('tab_result');
126+
// Track which tab is currently active. Honors `configuration.initialTab`
127+
// so an external caller (e.g. the "Indexes" tree node) can open the view
128+
// pre-pinned to the Index Management tab.
129+
const [selectedTab, setSelectedTab] = useState<'tab_result' | 'tab_indexes' | 'tab_queryInsights'>(
130+
() => configuration.initialTab ?? 'tab_result',
131+
);
127132

128133
// keep Refs updated with the current state
129134
const currentQueryResultsRef = useRef(currentQueryResults);
@@ -566,55 +571,57 @@ export const CollectionView = (): JSX.Element => {
566571
/>
567572

568573
<div className="toolbarMainView">
569-
<ToolbarMainView />
574+
<ToolbarMainView selectedTab={selectedTab} />
570575
</div>
571576

572-
<QueryEditor
573-
onExecuteRequest={() => {
574-
// Get all query values from the editor at once
575-
const query = currentContext.queryEditor?.getCurrentQuery() ?? {
576-
filter: '{ }',
577-
project: '{ }',
578-
sort: '{ }',
579-
skip: 0,
580-
limit: 0,
581-
};
582-
583-
setCurrentContext((prev) => ({
584-
...prev,
585-
activeQuery: {
586-
...prev.activeQuery,
587-
queryText: query.filter, // deprecated: kept in sync with filter
588-
filter: query.filter,
589-
project: query.project,
590-
sort: query.sort,
591-
skip: query.skip,
592-
limit: query.limit,
593-
pageNumber: 1,
594-
executionIntent: 'initial',
595-
},
596-
}));
597-
598-
trpcClient.common.reportEvent
599-
.mutate({
600-
eventName: 'executeQuery',
601-
properties: {
602-
ui: 'shortcut',
603-
},
604-
measurements: {
605-
queryLenth: query.filter.length,
577+
{selectedTab !== 'tab_indexes' && (
578+
<QueryEditor
579+
onExecuteRequest={() => {
580+
// Get all query values from the editor at once
581+
const query = currentContext.queryEditor?.getCurrentQuery() ?? {
582+
filter: '{ }',
583+
project: '{ }',
584+
sort: '{ }',
585+
skip: 0,
586+
limit: 0,
587+
};
588+
589+
setCurrentContext((prev) => ({
590+
...prev,
591+
activeQuery: {
592+
...prev.activeQuery,
593+
queryText: query.filter, // deprecated: kept in sync with filter
594+
filter: query.filter,
595+
project: query.project,
596+
sort: query.sort,
597+
skip: query.skip,
598+
limit: query.limit,
599+
pageNumber: 1,
600+
executionIntent: 'initial',
606601
},
607-
})
608-
.catch((error) => {
609-
console.debug('Failed to report an event:', error);
610-
});
611-
}}
612-
/>
602+
}));
603+
604+
trpcClient.common.reportEvent
605+
.mutate({
606+
eventName: 'executeQuery',
607+
properties: {
608+
ui: 'shortcut',
609+
},
610+
measurements: {
611+
queryLenth: query.filter.length,
612+
},
613+
})
614+
.catch((error) => {
615+
console.debug('Failed to report an event:', error);
616+
});
617+
}}
618+
/>
619+
)}
613620

614621
<TabList
615622
selectedValue={selectedTab}
616623
onTabSelect={(_event, data) => {
617-
const newTab = data.value as 'tab_result' | 'tab_queryInsights';
624+
const newTab = data.value as 'tab_result' | 'tab_indexes' | 'tab_queryInsights';
618625

619626
// Report tab switching telemetry
620627
trpcClient.common.reportEvent
@@ -634,7 +641,10 @@ export const CollectionView = (): JSX.Element => {
634641
style={{ marginTop: '-10px' }}
635642
>
636643
<Tab id="tab.results" value="tab_result">
637-
Results
644+
{l10n.t('Documents')}
645+
</Tab>
646+
<Tab id="tab.indexes" value="tab_indexes">
647+
{l10n.t('Indexes')}
638648
</Tab>
639649
<Tab id="tab.queryInsights" value="tab_queryInsights">
640650
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
@@ -690,6 +700,8 @@ export const CollectionView = (): JSX.Element => {
690700
</>
691701
)}
692702

703+
{selectedTab === 'tab_indexes' && <IndexesTab collectionName={configuration.collectionName} />}
704+
693705
{selectedTab === 'tab_queryInsights' && <QueryInsightsMain />}
694706
</div>
695707
</CollectionViewContext.Provider>

src/webviews/documentdb/collectionView/collectionViewController.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ export type CollectionViewWebviewConfigurationType = {
3737
skip?: number;
3838
limit?: number;
3939
};
40+
/**
41+
* Optional tab to land on when the view opens. Defaults to `tab_result`
42+
* (the Documents tab). Used when the user double-clicks the "Indexes"
43+
* node in the tree so the Index Management tab is the first thing
44+
* they see.
45+
*/
46+
initialTab?: 'tab_result' | 'tab_indexes' | 'tab_queryInsights';
4047
};
4148

4249
export class CollectionViewController extends WebviewControllerBase<CollectionViewWebviewConfigurationType> {

src/webviews/documentdb/collectionView/components/toolbar/ToolbarMainView.tsx

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
useOverflowMenu,
2323
} from '@fluentui/react-components';
2424
import {
25+
AddRegular,
2526
ArrowClockwiseRegular,
2627
ArrowExportRegular,
2728
ArrowImportRegular,
@@ -38,21 +39,31 @@ import { useConfiguration } from '@microsoft/vscode-ext-react-webview';
3839
import * as l10n from '@vscode/l10n';
3940
import { useContext, type JSX } from 'react';
4041
import { useTrpcClient } from '../../../../_integration/useTrpcClient';
42+
import { OPEN_CREATE_INDEX_EVENT } from '../../../indexView/constants';
4143
import { CollectionViewContext } from '../../collectionViewContext';
4244
import { type CollectionViewWebviewConfigurationType } from '../../collectionViewController';
4345
import { useHideScrollbarsDuringResize } from '../../hooks/useHideScrollbarsDuringResize';
4446
import { ToolbarDividerTransparent } from './ToolbarDividerTransparent';
4547

46-
export const ToolbarMainView = (): JSX.Element => {
48+
export interface ToolbarMainViewProps {
49+
/**
50+
* Which CollectionView tab is currently active. The primary toolbar
51+
* swaps its leading action button to match (Find Query on Results /
52+
* Query Insights, Create Index on Indexes).
53+
*/
54+
selectedTab?: 'tab_result' | 'tab_indexes' | 'tab_queryInsights';
55+
}
56+
57+
export const ToolbarMainView = ({ selectedTab }: ToolbarMainViewProps): JSX.Element => {
4758
return (
4859
<>
49-
<ToolbarQueryOperations />
60+
<ToolbarQueryOperations selectedTab={selectedTab} />
5061
<ToolbarSecondaryActions />
5162
</>
5263
);
5364
};
5465

55-
const ToolbarQueryOperations = (): JSX.Element => {
66+
const ToolbarQueryOperations = ({ selectedTab }: ToolbarMainViewProps): JSX.Element => {
5667
/**
5768
* Use the `useTrpcClient` hook to get the tRPC client
5869
*/
@@ -160,15 +171,32 @@ const ToolbarQueryOperations = (): JSX.Element => {
160171

161172
return (
162173
<Toolbar size="small" checkedValues={checkedValues} onCheckedValueChange={handleCheckedValueChange}>
163-
<ToolbarButton
164-
aria-label={l10n.t('Execute the find query')}
165-
disabled={currentContext.isLoading}
166-
icon={<PlayRegular />}
167-
onClick={handleExecuteQuery}
168-
appearance="primary"
169-
>
170-
{l10n.t('Find Query')}
171-
</ToolbarButton>
174+
{selectedTab === 'tab_indexes' ? (
175+
// On the Indexes tab the primary action is creating a new
176+
// index. We dispatch a window-level CustomEvent that the
177+
// IndexesTab component listens for; this keeps the toolbar
178+
// free of any direct coupling to the IndexesTab internals.
179+
<ToolbarButton
180+
aria-label={l10n.t('Create a new index')}
181+
icon={<AddRegular />}
182+
appearance="primary"
183+
onClick={() => {
184+
window.dispatchEvent(new CustomEvent(OPEN_CREATE_INDEX_EVENT));
185+
}}
186+
>
187+
{l10n.t('Create Index')}
188+
</ToolbarButton>
189+
) : (
190+
<ToolbarButton
191+
aria-label={l10n.t('Execute the find query')}
192+
disabled={currentContext.isLoading}
193+
icon={<PlayRegular />}
194+
onClick={handleExecuteQuery}
195+
appearance="primary"
196+
>
197+
{l10n.t('Find Query')}
198+
</ToolbarButton>
199+
)}
172200

173201
<ToolbarDividerTransparent />
174202

0 commit comments

Comments
 (0)