Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,13 @@
"visibility": "visible",
"icon": "media/logo-white.svg"
},
{
"id": "sharedWorkspaces",
"name": "Shared Workspaces",
"visibility": "visible",
"icon": "media/logo-white.svg",
"when": "coder.authenticated"
},
{
"id": "allWorkspaces",
"name": "All Workspaces",
Expand Down Expand Up @@ -432,6 +439,12 @@
"category": "Coder",
"icon": "$(search)"
},
{
"command": "coder.searchSharedWorkspaces",
"title": "Search",
"category": "Coder",
"icon": "$(search)"
},
{
"command": "coder.searchAllWorkspaces",
"title": "Search",
Expand Down Expand Up @@ -560,6 +573,10 @@
"command": "coder.searchMyWorkspaces",
"when": "false"
},
{
"command": "coder.searchSharedWorkspaces",
"when": "false"
},
{
"command": "coder.searchAllWorkspaces",
"when": "false"
Expand Down Expand Up @@ -607,6 +624,16 @@
"when": "coder.authenticated && view == myWorkspaces",
"group": "navigation@3"
},
{
"command": "coder.refreshWorkspaces",
"when": "coder.authenticated && view == sharedWorkspaces",
"group": "navigation@2"
},
{
"command": "coder.searchSharedWorkspaces",
"when": "coder.authenticated && view == sharedWorkspaces",
"group": "navigation@3"
},
{
"command": "coder.searchAllWorkspaces",
"when": "coder.authenticated && view == allWorkspaces",
Expand Down
1 change: 1 addition & 0 deletions src/core/commandManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const CODER_COMMAND_IDS = [
"coder.refreshWorkspaces",
"coder.viewLogs",
"coder.searchMyWorkspaces",
"coder.searchSharedWorkspaces",
"coder.searchAllWorkspaces",
"coder.manageCredentials",
"coder.applyRecommendedSettings",
Expand Down
11 changes: 11 additions & 0 deletions src/deployment/deploymentManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class DeploymentManager implements vscode.Disposable {
private readonly telemetryService: TelemetryService;

#deployment: Deployment | null = null;
#currentUserId: string | undefined;
#authListenerDisposable: vscode.Disposable | undefined;
#crossWindowSyncDisposable: vscode.Disposable | undefined;

Expand Down Expand Up @@ -83,6 +84,14 @@ export class DeploymentManager implements vscode.Disposable {
return this.contextManager.get("coder.authenticated");
}

/**
* Get the id of the currently authenticated user, if any. Used by the
* Shared workspaces view to filter out the current user's workspaces.
*/
public getCurrentUserId(): string | undefined {
return this.#currentUserId;
}

/**
* Attempt to change to a deployment after validating authentication.
* Only changes deployment if authentication succeeds.
Expand Down Expand Up @@ -127,6 +136,7 @@ export class DeploymentManager implements vscode.Disposable {
user: deployment.user.username,
});
this.#deployment = { ...deployment };
this.#currentUserId = deployment.user.id;
this.telemetryService.setDeploymentUrl(deployment.url);

// Updates client credentials
Expand Down Expand Up @@ -173,6 +183,7 @@ export class DeploymentManager implements vscode.Disposable {
this.client.setCredentials(undefined, undefined);
this.updateAuthContexts(undefined);
this.contextManager.set("coder.agentsEnabled", false);
this.#currentUserId = undefined;
this.clearWorkspaces();
}

Expand Down
33 changes: 32 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from "./workspace/workspacesProvider";

const MY_WORKSPACES_TREE_ID = "myWorkspaces";
const SHARED_WORKSPACES_TREE_ID = "sharedWorkspaces";
const ALL_WORKSPACES_TREE_ID = "allWorkspaces";

export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
Expand Down Expand Up @@ -174,6 +175,19 @@ async function doActivate(
);
ctx.subscriptions.push(allWorkspacesProvider);

const sharedWorkspacesProvider = new WorkspaceProvider(
WorkspaceQuery.Shared,
client,
output,
isAuthenticated,
undefined,
// Filter out workspaces owned by the current user. The deployment
// manager is created below; we capture it via the closure and read it
// lazily, since the callback only fires when workspaces are fetched.
() => deploymentManager.getCurrentUserId(),
);
ctx.subscriptions.push(sharedWorkspacesProvider);

// createTreeView, unlike registerTreeDataProvider, gives us the tree view API
// (so we can see when it is visible) but otherwise they have the same effect.
const myWsTree = vscode.window.createTreeView(MY_WORKSPACES_TREE_ID, {
Expand All @@ -189,6 +203,19 @@ async function doActivate(
ctx.subscriptions,
);

const sharedWsTree = vscode.window.createTreeView(SHARED_WORKSPACES_TREE_ID, {
treeDataProvider: sharedWorkspacesProvider,
});
ctx.subscriptions.push(sharedWsTree);
sharedWorkspacesProvider.setVisibility(sharedWsTree.visible);
sharedWsTree.onDidChangeVisibility(
(event) => {
sharedWorkspacesProvider.setVisibility(event.visible);
},
undefined,
ctx.subscriptions,
);

const allWsTree = vscode.window.createTreeView(ALL_WORKSPACES_TREE_ID, {
treeDataProvider: allWorkspacesProvider,
});
Expand All @@ -207,7 +234,7 @@ async function doActivate(
serviceContainer,
client,
oauthSessionManager,
[myWorkspacesProvider, allWorkspacesProvider],
[myWorkspacesProvider, sharedWorkspacesProvider, allWorkspacesProvider],
);
ctx.subscriptions.push(deploymentManager);

Expand Down Expand Up @@ -313,12 +340,16 @@ async function doActivate(
);
commandManager.register("coder.refreshWorkspaces", () => {
void myWorkspacesProvider.fetchAndRefresh();
void sharedWorkspacesProvider.fetchAndRefresh();
void allWorkspacesProvider.fetchAndRefresh();
});
commandManager.register("coder.viewLogs", commands.viewLogs.bind(commands));
commandManager.register("coder.searchMyWorkspaces", async () =>
showTreeViewSearch(MY_WORKSPACES_TREE_ID),
);
commandManager.register("coder.searchSharedWorkspaces", async () =>
showTreeViewSearch(SHARED_WORKSPACES_TREE_ID),
);
commandManager.register("coder.searchAllWorkspaces", async () =>
showTreeViewSearch(ALL_WORKSPACES_TREE_ID),
);
Expand Down
31 changes: 28 additions & 3 deletions src/workspace/workspacesProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ import { type Logger } from "../logging/logger";
export enum WorkspaceQuery {
Mine = "owner:me",
All = "",
// Shared returns workspaces the user has access to via sharing but does not
// own. The server-side `shared:true` filter also includes workspaces the
// user owns and has shared out, so the provider filters those out
// client-side using the current user's id.
Shared = "shared:true",
}

/**
Expand Down Expand Up @@ -53,6 +58,10 @@ export class WorkspaceProvider
private readonly logger: Logger,
private readonly isAuthenticated: () => boolean,
private readonly timerSeconds?: number,
// Returns the id of the currently authenticated user. Used by the Shared
// query to filter out workspaces owned by the current user.
private readonly getCurrentUserId: () => string | undefined = () =>
undefined,
) {
// No initialization.
}
Expand Down Expand Up @@ -112,6 +121,18 @@ export class WorkspaceProvider
q: this.getWorkspacesQuery,
});

// `shared:true` also matches workspaces the current user shared out;
// keep only the ones owned by someone else.
let workspaces = resp.workspaces;
if (this.getWorkspacesQuery === WorkspaceQuery.Shared) {
const currentUserId = this.getCurrentUserId();
if (currentUserId) {
workspaces = workspaces.filter(
(workspace) => workspace.owner_id !== currentUserId,
);
}
}

// We could have logged out while waiting for the query, or logged into a
// different deployment.
const url2 = this.client.getAxiosInstance().defaults.baseURL;
Expand All @@ -133,7 +154,7 @@ export class WorkspaceProvider
// have this separate map held outside the tree.
const showMetadata = this.getWorkspacesQuery === WorkspaceQuery.Mine;
if (showMetadata) {
const agents = extractAllAgents(resp.workspaces);
const agents = extractAllAgents(workspaces);
for (const agent of agents) {
// If we have an existing watcher, re-use it.
const oldWatcher = this.agentWatchers.get(agent.id);
Expand All @@ -159,11 +180,15 @@ export class WorkspaceProvider
}
}

// Show the owner alongside the workspace name when the list may contain
// workspaces owned by other users.
const showOwner = this.getWorkspacesQuery !== WorkspaceQuery.Mine;

// Create tree items for each workspace
const workspaceTreeItems = resp.workspaces.map((workspace: Workspace) => {
const workspaceTreeItems = workspaces.map((workspace: Workspace) => {
const workspaceTreeItem = new WorkspaceTreeItem(
workspace,
this.getWorkspacesQuery === WorkspaceQuery.All,
showOwner,
showMetadata,
);

Expand Down
Loading