Skip to content

Commit 3fdcdcb

Browse files
DevonLcolinmoynes
andauthored
ux: conditional toolbar visibility based on auth and workspace state (#26)
* ux: conditional toolbar visibility based on auth and workspace state - Initialize context keys on startup, sync hasDefaultWorkspace on setting changes and after picker runs - Add secret-change listener to flip toolbar on credential clear - Rename Refresh Packages to Refresh - Add when clauses to view/title menu entries for auth-aware visibility - Hide Set Default Workspace when single workspace or default already set * fix: suppress spurious credential warning on intentional sign-out - Silent refresh after credential clear, no missing-credentials popup - Add promptOnMissingCredentials flag to connect() --------- Co-authored-by: Colin Moynes <77994705+colinmoynes@users.noreply.github.com>
1 parent d6a3909 commit 3fdcdcb

8 files changed

Lines changed: 304 additions & 86 deletions

extension.js

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,23 +247,47 @@ function getDefaultWorkspace() {
247247
return config.get("defaultWorkspace") || "";
248248
}
249249

250+
async function setConnectedContext(isConnected) {
251+
await vscode.commands.executeCommand("setContext", "cloudsmith.connected", Boolean(isConnected));
252+
}
253+
254+
async function setHasMultipleWorkspacesContext(hasMultipleWorkspaces) {
255+
await vscode.commands.executeCommand(
256+
"setContext",
257+
"cloudsmith.hasMultipleWorkspaces",
258+
Boolean(hasMultipleWorkspaces)
259+
);
260+
}
261+
262+
async function updateDefaultWorkspaceContext() {
263+
await vscode.commands.executeCommand(
264+
"setContext",
265+
"cloudsmith.hasDefaultWorkspace",
266+
Boolean(getDefaultWorkspace())
267+
);
268+
}
269+
250270
async function getWorkspaces(context) {
251271
const cache = context.globalState.get('CloudsmithCache');
252272
if (cache && cache.name === 'Workspaces' && cache.workspaces) {
253273
// Check TTL — treat as stale if older than 30 minutes
254274
if (cache.lastSync && (Date.now() - cache.lastSync) < WORKSPACE_CACHE_TTL_MS) {
275+
await setHasMultipleWorkspacesContext(cache.workspaces.length > 1);
255276
return cache.workspaces;
256277
}
257278
}
258279
const cloudsmithAPI = new CloudsmithAPI(context);
259280
const result = await cloudsmithAPI.get("namespaces/?sort=slug");
260281
if (typeof result === 'string') {
261-
vscode.window.showErrorMessage(`Could not load workspaces. ${formatApiError(result)}`);
282+
await setHasMultipleWorkspacesContext(false);
283+
vscode.window.showErrorMessage("Failed to load workspaces: " + result);
262284
return null;
263285
}
264286
if (!result || result.length === 0) {
287+
await setHasMultipleWorkspacesContext(false);
265288
return [];
266289
}
290+
await setHasMultipleWorkspacesContext(result.length > 1);
267291
return result;
268292
}
269293

@@ -292,7 +316,10 @@ function buildPresetQuery(preset, customQuery) {
292316
*/
293317
async function activate(context) {
294318

295-
context.secrets.store("cloudsmith-vsc.isConnected", "false");
319+
await context.secrets.store("cloudsmith-vsc.isConnected", "false");
320+
await setConnectedContext(false);
321+
await setHasMultipleWorkspacesContext(false);
322+
await updateDefaultWorkspaceContext();
296323

297324

298325
// Define main view provider which populates with data
@@ -319,8 +346,9 @@ async function activate(context) {
319346

320347
// Listen for configuration changes to refresh tree when defaultWorkspace changes
321348
context.subscriptions.push(
322-
vscode.workspace.onDidChangeConfiguration(e => {
349+
vscode.workspace.onDidChangeConfiguration(async e => {
323350
if (e.affectsConfiguration("cloudsmith-vsc.defaultWorkspace")) {
351+
await updateDefaultWorkspaceContext();
324352
const newDefault = getDefaultWorkspace();
325353
treeView.title = newDefault ? "Repositories" : "Workspaces";
326354
treeView.description = newDefault || "";
@@ -349,6 +377,26 @@ async function activate(context) {
349377
showCollapseAll: true,
350378
});
351379

380+
context.subscriptions.push(
381+
context.secrets.onDidChange(async (e) => {
382+
if (e.key !== "cloudsmith-vsc.authToken") {
383+
return;
384+
}
385+
386+
const apiKey = await context.secrets.get("cloudsmith-vsc.authToken");
387+
if (apiKey) {
388+
return;
389+
}
390+
391+
await context.secrets.store("cloudsmith-vsc.isConnected", "false");
392+
await setConnectedContext(false);
393+
await setHasMultipleWorkspacesContext(false);
394+
cloudsmithProvider.refresh({ suppressMissingCredentialsWarning: true });
395+
searchProvider.refresh();
396+
dependencyHealthProvider.refresh();
397+
})
398+
);
399+
352400
// Create vulnerability WebView provider
353401
const vulnerabilityProvider = new VulnerabilityProvider(context);
354402
context.subscriptions.push({ dispose: () => vulnerabilityProvider.dispose() });
@@ -368,6 +416,25 @@ async function activate(context) {
368416
// Create promotion provider
369417
const promotionProvider = new PromotionProvider(context);
370418

419+
const initializeConnectionContext = async () => {
420+
const credentialManager = new CredentialManager(context);
421+
const apiKey = await credentialManager.getApiKey();
422+
if (!apiKey) {
423+
return;
424+
}
425+
426+
try {
427+
const { ConnectionManager } = require("./util/connectionManager");
428+
const connectionManager = new ConnectionManager(context);
429+
await connectionManager.checkConnectivity(apiKey);
430+
} catch {
431+
await context.secrets.store("cloudsmith-vsc.isConnected", "false");
432+
await setConnectedContext(false);
433+
}
434+
};
435+
436+
void initializeConnectionContext();
437+
371438
// Auto-scan dependencies on open if configured
372439
const autoScanConfig = vscode.workspace.getConfiguration("cloudsmith-vsc");
373440
if (autoScanConfig.get("autoScanOnOpen")) {
@@ -415,6 +482,7 @@ async function activate(context) {
415482
if (choice === "Set as default") {
416483
const config = vscode.workspace.getConfiguration("cloudsmith-vsc");
417484
await config.update("defaultWorkspace", ws.slug, vscode.ConfigurationTarget.Global);
485+
await updateDefaultWorkspaceContext();
418486
treeView.title = "Repositories";
419487
treeView.description = ws.slug;
420488
cloudsmithProvider.refresh();
@@ -517,10 +585,12 @@ async function activate(context) {
517585
const config = vscode.workspace.getConfiguration("cloudsmith-vsc");
518586
if (selected._clear) {
519587
await config.update("defaultWorkspace", "", vscode.ConfigurationTarget.Global);
588+
await updateDefaultWorkspaceContext();
520589
treeView.title = "Workspaces";
521590
treeView.description = "";
522591
} else {
523592
await config.update("defaultWorkspace", selected.description, vscode.ConfigurationTarget.Global);
593+
await updateDefaultWorkspaceContext();
524594
treeView.title = "Repositories";
525595
treeView.description = selected.description;
526596
}

package.json

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"command": "cloudsmith-vsc.openSettings",
6161
"title": "Open Cloudsmith settings",
6262
"category": "Cloudsmith",
63-
"icon": "$(settings-gear)"
63+
"icon": "$(gear)"
6464
},
6565
{
6666
"command": "cloudsmith-vsc.configureCredentials",
@@ -72,13 +72,12 @@
7272
"command": "cloudsmith-vsc.connectCloudsmith",
7373
"title": "Connect to Cloudsmith",
7474
"category": "Cloudsmith",
75-
"icon": "$(debug-disconnect)"
75+
"icon": "$(plug)"
7676
},
7777
{
7878
"command": "cloudsmith-vsc.clearCredentials",
7979
"title": "Clear stored credentials",
80-
"category": "Cloudsmith",
81-
"icon": "$(notebook-delete-cell)"
80+
"icon": "$(sign-out)"
8281
},
8382
{
8483
"command": "cloudsmith-vsc.copySelected",
@@ -107,8 +106,7 @@
107106
},
108107
{
109108
"command": "cloudsmith-vsc.refreshView",
110-
"category": "Cloudsmith",
111-
"title": "Refresh packages",
109+
"title": "Refresh",
112110
"icon": "$(refresh)"
113111
},
114112
{
@@ -229,7 +227,7 @@
229227
"command": "cloudsmith-vsc.ssoLogin",
230228
"title": "Sign in with SSO",
231229
"category": "Cloudsmith",
232-
"icon": "$(sign-in)"
230+
"icon": "$(terminal)"
233231
},
234232
{
235233
"command": "cloudsmith-vsc.importCLICredentials",
@@ -490,39 +488,39 @@
490488
],
491489
"view/title": [
492490
{
493-
"command": "cloudsmith-vsc.setDefaultWorkspace",
491+
"command": "cloudsmith-vsc.refreshView",
494492
"group": "navigation",
495-
"when": "view == cloudsmithView"
493+
"when": "view == cloudsmithView && cloudsmith.connected"
496494
},
497495
{
498-
"command": "cloudsmith-vsc.refreshView",
496+
"command": "cloudsmith-vsc.setDefaultWorkspace",
499497
"group": "navigation",
500-
"when": "view == cloudsmithView"
498+
"when": "view == cloudsmithView && cloudsmith.connected && cloudsmith.hasMultipleWorkspaces && !cloudsmith.hasDefaultWorkspace"
501499
},
502500
{
503-
"command": "cloudsmith-vsc.openSettings",
501+
"command": "cloudsmith-vsc.connectCloudsmith",
504502
"group": "navigation",
505-
"when": "view == cloudsmithView"
503+
"when": "view == cloudsmithView && !cloudsmith.connected"
506504
},
507505
{
508506
"command": "cloudsmith-vsc.configureCredentials",
509507
"group": "navigation",
510-
"when": "view == cloudsmithView"
508+
"when": "view == cloudsmithView && !cloudsmith.connected"
511509
},
512510
{
513-
"command": "cloudsmith-vsc.clearCredentials",
511+
"command": "cloudsmith-vsc.ssoLogin",
514512
"group": "navigation",
515-
"when": "view == cloudsmithView"
513+
"when": "view == cloudsmithView && !cloudsmith.connected"
516514
},
517515
{
518-
"command": "cloudsmith-vsc.connectCloudsmith",
516+
"command": "cloudsmith-vsc.openSettings",
519517
"group": "navigation",
520518
"when": "view == cloudsmithView"
521519
},
522520
{
523-
"command": "cloudsmith-vsc.ssoLogin",
521+
"command": "cloudsmith-vsc.clearCredentials",
524522
"group": "navigation",
525-
"when": "view == cloudsmithView"
523+
"when": "view == cloudsmithView && cloudsmith.connected"
526524
},
527525
{
528526
"command": "cloudsmith-vsc.searchPackages",

test/cloudsmithProvider.test.js

Lines changed: 71 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,97 @@
11
const assert = require("assert");
2-
const { CloudsmithAPI } = require("../util/cloudsmithAPI");
3-
const { ConnectionManager } = require("../util/connectionManager");
4-
const workspaceRepositoryFetcher = require("../util/workspaceRepositoryFetcher");
2+
const vscode = require("vscode");
53
const { CloudsmithProvider } = require("../views/cloudsmithProvider");
64

75
suite("CloudsmithProvider Test Suite", () => {
8-
let originalConnect;
9-
let originalGet;
10-
let originalFetchWorkspaceRepositories;
6+
let originalExecuteCommand;
7+
let originalGetConfiguration;
8+
let originalShowWarningMessage;
9+
let commandCalls;
10+
let warningCalls;
11+
let defaultWorkspace;
12+
let treeView;
1113
let provider;
12-
let cacheUpdates;
1314

1415
const context = {
16+
secrets: {
17+
onDidChange() {},
18+
async get(key) {
19+
if (key === "cloudsmith-vsc.isConnected") {
20+
return "false";
21+
}
22+
return null;
23+
},
24+
async store() {},
25+
},
1526
globalState: {
16-
update(key, value) {
17-
cacheUpdates.push({ key, value });
27+
get() {
28+
return undefined;
1829
},
30+
async update() {},
1931
},
2032
};
2133

2234
setup(() => {
23-
cacheUpdates = [];
35+
commandCalls = [];
36+
warningCalls = [];
37+
defaultWorkspace = "";
38+
treeView = { message: "ready" };
2439
provider = new CloudsmithProvider(context);
40+
provider.setTreeView(treeView);
2541

26-
originalConnect = ConnectionManager.prototype.connect;
27-
originalGet = CloudsmithAPI.prototype.get;
28-
originalFetchWorkspaceRepositories =
29-
workspaceRepositoryFetcher.fetchWorkspaceRepositories;
30-
31-
ConnectionManager.prototype.connect = async () => "true";
32-
CloudsmithAPI.prototype.get = async endpoint => {
33-
if (endpoint.startsWith("quota/")) {
34-
return {
35-
usage: {
36-
display: {
37-
storage: { used: "0 GB", plan_limit: "10 GB", percentage_used: "0" },
38-
bandwidth: { used: "0 GB", plan_limit: "10 GB", percentage_used: "0" },
39-
},
40-
},
41-
};
42-
}
42+
originalExecuteCommand = vscode.commands.executeCommand;
43+
originalGetConfiguration = vscode.workspace.getConfiguration;
44+
originalShowWarningMessage = vscode.window.showWarningMessage;
4345

44-
return [];
46+
vscode.commands.executeCommand = async (...args) => {
47+
commandCalls.push(args);
48+
};
49+
vscode.workspace.getConfiguration = () => ({
50+
get(key) {
51+
if (key === "defaultWorkspace") {
52+
return defaultWorkspace;
53+
}
54+
return "";
55+
},
56+
});
57+
vscode.window.showWarningMessage = async (...args) => {
58+
warningCalls.push(args);
59+
return undefined;
4560
};
4661
});
4762

4863
teardown(() => {
49-
ConnectionManager.prototype.connect = originalConnect;
50-
CloudsmithAPI.prototype.get = originalGet;
51-
workspaceRepositoryFetcher.fetchWorkspaceRepositories =
52-
originalFetchWorkspaceRepositories;
64+
vscode.commands.executeCommand = originalExecuteCommand;
65+
vscode.workspace.getConfiguration = originalGetConfiguration;
66+
vscode.window.showWarningMessage = originalShowWarningMessage;
5367
});
5468

55-
test("loads repositories for the default workspace through the shared fetcher", async () => {
56-
workspaceRepositoryFetcher.fetchWorkspaceRepositories = async () => ({
57-
repositories: [
58-
{ name: "repo-a", slug: "repo-a" },
59-
{ name: "repo-b", slug: "repo-b" },
60-
],
61-
error: null,
62-
warning: null,
63-
partial: false,
64-
});
69+
test("silent refresh shows the signed-out root state without warning after credentials are cleared", async () => {
70+
provider.refresh({ suppressMissingCredentialsWarning: true });
71+
72+
const nodes = await provider.getChildren();
73+
74+
assert.strictEqual(warningCalls.length, 0);
75+
assert.strictEqual(treeView.message, undefined);
76+
assert.strictEqual(nodes.length, 1);
77+
assert.strictEqual(nodes[0].getTreeItem().label, "Connect to Cloudsmith");
78+
assert.ok(
79+
commandCalls.some((call) => (
80+
call[0] === "setContext" &&
81+
call[1] === "cloudsmith.hasMultipleWorkspaces" &&
82+
call[2] === false
83+
))
84+
);
85+
});
86+
87+
test("silent refresh also shows the signed-out state when a default workspace is configured", async () => {
88+
defaultWorkspace = "workspace-a";
89+
provider.refresh({ suppressMissingCredentialsWarning: true });
6590

66-
const nodes = await provider.getRepositories("workspace-a");
91+
const nodes = await provider.getChildren();
6792

68-
assert.strictEqual(nodes.length, 3);
69-
assert.strictEqual(nodes[0].workspaceName, "workspace-a");
70-
assert.strictEqual(nodes[1].name, "repo-a");
71-
assert.strictEqual(nodes[2].name, "repo-b");
72-
assert.strictEqual(cacheUpdates.length, 1);
73-
assert.strictEqual(cacheUpdates[0].key, "CloudsmithCache");
74-
assert.deepStrictEqual(cacheUpdates[0].value.workspaces, [
75-
{ name: "workspace-a", slug: "workspace-a" },
76-
]);
93+
assert.strictEqual(warningCalls.length, 0);
94+
assert.strictEqual(nodes.length, 1);
95+
assert.strictEqual(nodes[0].getTreeItem().label, "Connect to Cloudsmith");
7796
});
7897
});

0 commit comments

Comments
 (0)