Skip to content

Commit be75a3b

Browse files
committed
static icon + minimal description for cluster
1 parent d381af3 commit be75a3b

5 files changed

Lines changed: 123 additions & 42 deletions

File tree

l10n/bundle.l10n.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
"[StreamingWriter] Starting document streaming with {0} strategy": "[StreamingWriter] Starting document streaming with {0} strategy",
110110
"[StreamingWriter] Throttle: wrote {0} docs, {1} remaining in batch": "[StreamingWriter] Throttle: wrote {0} docs, {1} remaining in batch",
111111
"[StreamingWriter] Writing {0} documents to target (may take a moment)...": "[StreamingWriter] Writing {0} documents to target (may take a moment)...",
112+
"{0}\n\nIf the key pair is correct, make sure your current IP address is on the Access List for this API key and that the key has the required project permissions.": "{0}\n\nIf the key pair is correct, make sure your current IP address is on the Access List for this API key and that the key has the required project permissions.",
112113
"{0}\n\nNothing is imported until you choose Import. Choose Preview to open the file(s) without importing.": "{0}\n\nNothing is imported until you choose Import. Choose Preview to open the file(s) without importing.",
113114
"{0}\n\nThe query did not complete successfully. Performance metrics shown are partial and measured up to the failure point.": "{0}\n\nThe query did not complete successfully. Performance metrics shown are partial and measured up to the failure point.",
114115
"{0}\n\nTip: copy the file into the same filesystem as the editor (for example your WSL or remote home directory), then drop it again, or use the \"Add Kubeconfig Source\" command to browse for it.": "{0}\n\nTip: copy the file into the same filesystem as the editor (for example your WSL or remote home directory), then drop it again, or use the \"Add Kubeconfig Source\" command to browse for it.",
@@ -194,6 +195,7 @@
194195
"Acceptable execution time": "Acceptable execution time",
195196
"Access denied (403 Forbidden)": "Access denied (403 Forbidden)",
196197
"Access denied listing services (403 Forbidden)": "Access denied listing services (403 Forbidden)",
198+
"Access denied: {0}": "Access denied: {0}",
197199
"Access denied. Verify your API key has the required permissions.": "Access denied. Verify your API key has the required permissions.",
198200
"Account information is incomplete.": "Account information is incomplete.",
199201
"Account Management Completed": "Account Management Completed",
@@ -258,6 +260,7 @@
258260
"Authentication data (properties.connectionString) is missing for \"{cluster}\".": "Authentication data (properties.connectionString) is missing for \"{cluster}\".",
259261
"Authentication failed (401 Unauthorized)": "Authentication failed (401 Unauthorized)",
260262
"Authentication failed listing services (401)": "Authentication failed listing services (401)",
263+
"Authentication failed: {0}": "Authentication failed: {0}",
261264
"Authentication failed. Please sign in again.": "Authentication failed. Please sign in again.",
262265
"Authentication is required to run this action.": "Authentication is required to run this action.",
263266
"Authentication is required to use this migration provider.": "Authentication is required to use this migration provider.",
@@ -445,6 +448,7 @@
445448
"Creating resource group \"{0}\" in location \"{1}\"...": "Creating resource group \"{0}\" in location \"{1}\"...",
446449
"Creating storage account \"{0}\" in location \"{1}\" with sku \"{2}\"...": "Creating storage account \"{0}\" in location \"{1}\" with sku \"{2}\"...",
447450
"Creating user-assigned identity \"{0}\" in location \"{1}\"\"...": "Creating user-assigned identity \"{0}\" in location \"{1}\"\"...",
451+
"Creating…": "Creating…",
448452
"Credentials may have expired. Re-authenticate with your cluster or update the kubeconfig.": "Credentials may have expired. Re-authenticate with your cluster or update the kubeconfig.",
449453
"Credentials may have expired. Re-authenticate with your cluster.": "Credentials may have expired. Re-authenticate with your cluster.",
450454
"Credentials updated successfully.": "Credentials updated successfully.",
@@ -476,6 +480,7 @@
476480
"delete this collection": "delete this collection",
477481
"delete this database": "delete this database",
478482
"Deleting...": "Deleting...",
483+
"Deleting…": "Deleting…",
479484
"Demo Task {0}": "Demo Task {0}",
480485
"Demo Task Configuration": "Demo Task Configuration",
481486
"Destination: \"{0}\"": "Destination: \"{0}\"",
@@ -820,7 +825,6 @@
820825
"Invalid connection type selected.": "Invalid connection type selected.",
821826
"Invalid filter syntax: {0}. Please use valid JSON or a DocumentDB API expression, for example: { name: \"value\" }": "Invalid filter syntax: {0}. Please use valid JSON or a DocumentDB API expression, for example: { name: \"value\" }",
822827
"Invalid folder type.": "Invalid folder type.",
823-
"Invalid MongoDB Atlas API key.": "Invalid MongoDB Atlas API key.",
824828
"Invalid node type.": "Invalid node type.",
825829
"Invalid payload for create index action": "Invalid payload for create index action",
826830
"Invalid payload for drop index action": "Invalid payload for drop index action",
@@ -919,6 +923,7 @@
919923
"Modify Index…": "Modify Index…",
920924
"Modifying index visibility ({action}) for \"{indexName}\" on collection: {collection}": "Modifying index visibility ({action}) for \"{indexName}\" on collection: {collection}",
921925
"MongoDB Atlas": "MongoDB Atlas",
926+
"MongoDB Atlas rejected the API key (HTTP {0}).": "MongoDB Atlas rejected the API key (HTTP {0}).",
922927
"MongoDB Atlas Service Discovery": "MongoDB Atlas Service Discovery",
923928
"MongoDB Emulator": "MongoDB Emulator",
924929
"Move": "Move",
@@ -1169,6 +1174,7 @@
11691174
"Rename kubeconfig source": "Rename kubeconfig source",
11701175
"Rename Kubernetes context": "Rename Kubernetes context",
11711176
"Renamed folder from \"{oldName}\" to \"{newName}\"": "Renamed folder from \"{oldName}\" to \"{newName}\"",
1177+
"Repairing…": "Repairing…",
11721178
"Report a Bug": "Report a Bug",
11731179
"report an issue": "report an issue",
11741180
"Report an issue": "Report an issue",
@@ -1438,6 +1444,11 @@
14381444
"These signals help us improve, but more context in a discussion, issue report, or a direct message adds even more value. ": "These signals help us improve, but more context in a discussion, issue report, or a direct message adds even more value. ",
14391445
"They look like binary files rather than kubeconfigs.": "They look like binary files rather than kubeconfigs.",
14401446
"This cannot be undone.": "This cannot be undone.",
1447+
"This cluster is being created. It will be available to connect once creation is complete.": "This cluster is being created. It will be available to connect once creation is complete.",
1448+
"This cluster is being deleted and will no longer be available.": "This cluster is being deleted and will no longer be available.",
1449+
"This cluster is being repaired. It may be temporarily unavailable.": "This cluster is being repaired. It may be temporarily unavailable.",
1450+
"This cluster is being updated. It may be temporarily unavailable.": "This cluster is being updated. It may be temporarily unavailable.",
1451+
"This cluster is in an unknown state. Try refreshing to update its status.": "This cluster is in an unknown state. Try refreshing to update its status.",
14411452
"This ClusterIP service requires port-forwarding. Confirm or change the local port to forward to {0}/{1}:{2}.": "This ClusterIP service requires port-forwarding. Confirm or change the local port to forward to {0}/{1}:{2}.",
14421453
"This field is not set": "This field is not set",
14431454
"This functionality requires installing the Azure Account extension.": "This functionality requires installing the Azure Account extension.",
@@ -1499,6 +1510,7 @@
14991510
"Unknown function '{0}'. Expected a BSON constructor (e.g., ObjectId, ISODate) or a known global (e.g., Date, Math).": "Unknown function '{0}'. Expected a BSON constructor (e.g., ObjectId, ISODate) or a known global (e.g., Date, Math).",
15001511
"Unknown identifier '{0}'. Expected a known global (e.g., Date, Math).": "Unknown identifier '{0}'. Expected a known global (e.g., Date, Math).",
15011512
"Unknown query generation type: {type}": "Unknown query generation type: {type}",
1513+
"Unknown state": "Unknown state",
15021514
"Unknown strategy": "Unknown strategy",
15031515
"Unknown tenant": "Unknown tenant",
15041516
"unsupported": "unsupported",
@@ -1516,6 +1528,7 @@
15161528
"Update Connection String": "Update Connection String",
15171529
"Update Saved Password": "Update Saved Password",
15181530
"Updated entity \"{name}\".": "Updated entity \"{name}\".",
1531+
"Updating…": "Updating…",
15191532
"Upload": "Upload",
15201533
"URL handling aborted. Connection was unsuccessful or the specified database/collection does not exist.": "URL handling aborted. Connection was unsuccessful or the specified database/collection does not exist.",
15211534
"Use a Service Account client ID and secret (machine-to-machine)": "Use a Service Account client ID and secret (machine-to-machine)",

src/plugins/service-atlas-mongodb/api/AtlasApiClient.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,11 +195,20 @@ export class AtlasApiClient {
195195

196196
switch (response.status) {
197197
case 401:
198-
throw new AtlasApiError(vscode.l10n.t('Authentication failed. Please sign in again.'), response.status);
198+
throw new AtlasApiError(
199+
detail
200+
? vscode.l10n.t('Authentication failed: {0}', detail)
201+
: vscode.l10n.t('Authentication failed. Please sign in again.'),
202+
response.status,
203+
detail,
204+
);
199205
case 403:
200206
throw new AtlasApiError(
201-
vscode.l10n.t('Access denied. Verify your API key has the required permissions.'),
207+
detail
208+
? vscode.l10n.t('Access denied: {0}', detail)
209+
: vscode.l10n.t('Access denied. Verify your API key has the required permissions.'),
202210
response.status,
211+
detail,
203212
);
204213
case 404:
205214
throw new AtlasApiError(vscode.l10n.t('Resource not found.'), response.status);
@@ -212,6 +221,7 @@ export class AtlasApiClient {
212221
throw new AtlasApiError(
213222
vscode.l10n.t('Atlas API error ({0}): {1}', String(response.status), detail),
214223
response.status,
224+
detail,
215225
);
216226
}
217227
}
@@ -224,6 +234,7 @@ export class AtlasApiError extends Error {
224234
constructor(
225235
message: string,
226236
public readonly statusCode: number,
237+
public readonly detail?: string,
227238
) {
228239
super(message);
229240
this.name = 'AtlasApiError';

src/plugins/service-atlas-mongodb/auth/AtlasApiKeyFlow.ts

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,29 @@ export async function executeApiKeyFlow(sessionManager: AtlasSessionManager): Pr
5555
}
5656

5757
// Step 3: Validate credentials
58+
const { AtlasApiError } = await import('../api/AtlasApiClient');
5859
try {
59-
const isValid = await validateApiKeyCredentials(publicKey.trim(), privateKey.trim());
60-
if (!isValid) {
61-
sessionManager.cancelAuthentication();
62-
void vscode.window.showErrorMessage(vscode.l10n.t('Invalid MongoDB Atlas API key.'), {
63-
modal: true,
64-
detail: vscode.l10n.t('Please verify your public and private key pair.'),
65-
});
66-
return false;
67-
}
60+
await validateApiKeyCredentials(publicKey.trim(), privateKey.trim());
6861
} catch (error) {
6962
sessionManager.cancelAuthentication();
63+
if (error instanceof AtlasApiError && (error.statusCode === 401 || error.statusCode === 403)) {
64+
const reason =
65+
error.detail && error.detail.trim().length > 0
66+
? error.detail
67+
: vscode.l10n.t('Please verify your public and private key pair.');
68+
void vscode.window.showErrorMessage(
69+
vscode.l10n.t('MongoDB Atlas rejected the API key (HTTP {0}).', String(error.statusCode)),
70+
{
71+
modal: true,
72+
detail: vscode.l10n.t(
73+
'{0}\n\nIf the key pair is correct, make sure your current IP address is on the Access List for this API key and that the key has the required project permissions.',
74+
reason,
75+
),
76+
},
77+
);
78+
return false;
79+
}
80+
7081
const errorMessage = error instanceof Error ? error.message : String(error);
7182
void vscode.window.showErrorMessage(vscode.l10n.t('Failed to validate MongoDB Atlas API key.'), {
7283
modal: true,
@@ -85,19 +96,14 @@ export async function executeApiKeyFlow(sessionManager: AtlasSessionManager): Pr
8596
/**
8697
* Validates API key credentials by making a lightweight API call.
8798
* Uses HTTP Digest Authentication as required by the Atlas Admin API.
99+
*
100+
* Resolves when the credentials are accepted. Any {@link AtlasApiError} (including 401/403)
101+
* is propagated so the caller can surface the real reason Atlas rejected the request.
88102
*/
89-
async function validateApiKeyCredentials(publicKey: string, privateKey: string): Promise<boolean> {
90-
const { AtlasApiClient, AtlasApiError } = await import('../api/AtlasApiClient');
103+
async function validateApiKeyCredentials(publicKey: string, privateKey: string): Promise<void> {
104+
const { AtlasApiClient } = await import('../api/AtlasApiClient');
91105
const client = new AtlasApiClient({ type: 'apikey', publicKey, privateKey });
92106

93-
try {
94-
// A successful call to list projects means the credentials are valid
95-
await client.listProjects();
96-
return true;
97-
} catch (error) {
98-
if (error instanceof AtlasApiError && (error.statusCode === 401 || error.statusCode === 403)) {
99-
return false;
100-
}
101-
throw error;
102-
}
107+
// A successful call to list projects means the credentials are valid.
108+
await client.listProjects();
103109
}

src/plugins/service-atlas-mongodb/discovery-tree/AtlasClusterItem.ts

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
type IActionContext,
1212
} from '@microsoft/vscode-azext-utils';
1313
import * as l10n from '@vscode/l10n';
14+
import * as path from 'path';
1415
import * as vscode from 'vscode';
1516
import { AuthMethodId } from '../../../documentdb/auth/AuthMethod';
1617
import { ClustersClient } from '../../../documentdb/ClustersClient';
@@ -23,9 +24,11 @@ import { ProvideUserNameStep } from '../../../documentdb/wizards/authenticate/Pr
2324
import { ext } from '../../../extensionVariables';
2425
import { ClusterItemBase, type EphemeralClusterCredentials } from '../../../tree/documentdb/ClusterItemBase';
2526
import { type TreeCluster } from '../../../tree/models/BaseClusterModel';
27+
import { getResourcesPath } from '../../../utils/icons';
2628
import { nonNullValue } from '../../../utils/nonNull';
2729
import { DISCOVERY_PROVIDER_ID } from '../config';
2830
import { type AtlasClusterModel } from '../models/AtlasClusterModel';
31+
import { type AtlasClusterState } from '../models/AtlasProjectModel';
2932

3033
/** Resource type identifier for telemetry */
3134
const RESOURCE_TYPE = 'atlas-mongodb-cluster';
@@ -216,18 +219,26 @@ export class AtlasClusterItem extends ClusterItemBase<AtlasClusterModel> {
216219

217220
/**
218221
* Returns the tree item representation with Atlas-specific display.
219-
* Shows state icon, tier/provider/region description, and cluster metadata tooltip.
222+
* Uses a stable provider-identity icon (the DocumentDB cluster brand mark, matching
223+
* sibling discovery plugins); transient cluster state is surfaced through the
224+
* description and tooltip rather than the icon so the tree stays visually stable
225+
* across refreshes.
220226
*/
221227
getTreeItem(): vscode.TreeItem {
222-
const stateIcon = this.getStateIcon();
223-
224228
return {
225229
id: this.id,
226230
contextValue: this.contextValue,
227231
label: this.cluster.name,
228232
description: this.buildDescription(),
229233
tooltip: this.buildTooltip(),
230-
iconPath: stateIcon,
234+
iconPath: {
235+
light: vscode.Uri.file(
236+
path.join(getResourcesPath(), 'icons', 'vscode-documentdb-cluster-light-themes.svg'),
237+
),
238+
dark: vscode.Uri.file(
239+
path.join(getResourcesPath(), 'icons', 'vscode-documentdb-cluster-dark-themes.svg'),
240+
),
241+
},
231242
collapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
232243
};
233244
}
@@ -265,17 +276,24 @@ export class AtlasClusterItem extends ClusterItemBase<AtlasClusterModel> {
265276
private buildDescription(): string {
266277
const parts: string[] = [];
267278

279+
// The tier (e.g. "M10") should show. When the tier is unavailable (e.g. serverless clusters), fall back to the provider/region pair.
268280
if (this.cluster.instanceSizeName) {
269281
parts.push(this.cluster.instanceSizeName);
282+
} else {
283+
if (this.cluster.providerName) {
284+
parts.push(this.cluster.providerName);
285+
}
286+
if (this.cluster.regionName) {
287+
parts.push(this.formatRegion(this.cluster.regionName));
288+
}
270289
}
271-
if (this.cluster.providerName) {
272-
parts.push(this.cluster.providerName);
273-
}
274-
if (this.cluster.regionName) {
275-
parts.push(this.formatRegion(this.cluster.regionName));
290+
291+
const stateLabel = this.getStateLabel();
292+
if (stateLabel) {
293+
parts.push(stateLabel);
276294
}
277295

278-
return parts.length > 0 ? parts.join(', ') : this.cluster.stateName;
296+
return parts.join(' · ');
279297
}
280298

281299
private buildTooltip(): vscode.MarkdownString {
@@ -299,6 +317,13 @@ export class AtlasClusterItem extends ClusterItemBase<AtlasClusterModel> {
299317

300318
md.appendMarkdown(`- **Project:** ${escapeMarkdown(this.cluster.projectName)}\n`);
301319

320+
const stateExplanation = this.getStateExplanation();
321+
if (stateExplanation) {
322+
md.appendMarkdown(`\n---\n`);
323+
md.appendMarkdown(escapeMarkdown(stateExplanation));
324+
return md;
325+
}
326+
302327
if (this.cluster.connectionString) {
303328
md.appendMarkdown(`\n---\n`);
304329
md.appendMarkdown(`Connection string available — expand to connect and browse databases.`);
@@ -307,18 +332,43 @@ export class AtlasClusterItem extends ClusterItemBase<AtlasClusterModel> {
307332
return md;
308333
}
309334

310-
private getStateIcon(): vscode.ThemeIcon {
335+
/**
336+
* Returns a short, localized label for the current cluster state, or `undefined` for the
337+
* normal IDLE state (which needs no annotation). Shown in the tree item description.
338+
*/
339+
private getStateLabel(): string | undefined {
340+
const labels: Record<AtlasClusterState, string | undefined> = {
341+
IDLE: undefined,
342+
CREATING: l10n.t('Creating…'),
343+
UPDATING: l10n.t('Updating…'),
344+
REPAIRING: l10n.t('Repairing…'),
345+
DELETING: l10n.t('Deleting…'),
346+
UNKNOWN: l10n.t('Unknown state'),
347+
};
348+
return labels[this.cluster.stateName];
349+
}
350+
351+
/**
352+
* Returns a localized, human-readable explanation of a non-IDLE cluster state for the
353+
* tooltip, or `undefined` when the cluster is IDLE.
354+
*/
355+
private getStateExplanation(): string | undefined {
311356
switch (this.cluster.stateName) {
312-
case 'IDLE':
313-
return new vscode.ThemeIcon('circle-filled', new vscode.ThemeColor('testing.iconPassed'));
314357
case 'CREATING':
358+
return l10n.t(
359+
'This cluster is being created. It will be available to connect once creation is complete.',
360+
);
315361
case 'UPDATING':
362+
return l10n.t('This cluster is being updated. It may be temporarily unavailable.');
316363
case 'REPAIRING':
317-
return new vscode.ThemeIcon('loading~spin');
364+
return l10n.t('This cluster is being repaired. It may be temporarily unavailable.');
318365
case 'DELETING':
319-
return new vscode.ThemeIcon('circle-filled', new vscode.ThemeColor('testing.iconFailed'));
366+
return l10n.t('This cluster is being deleted and will no longer be available.');
367+
case 'UNKNOWN':
368+
return l10n.t('This cluster is in an unknown state. Try refreshing to update its status.');
369+
case 'IDLE':
320370
default:
321-
return new vscode.ThemeIcon('circle-outline');
371+
return undefined;
322372
}
323373
}
324374

0 commit comments

Comments
 (0)