Skip to content
Draft
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;
Comment on lines 39 to +40
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe call it #authedUser: User | null so it's more explicit, we can keep the derived getCurrentUserId

#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,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to poll for refreshes or keep it like All and only on manual refreshes?

// 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.
Comment on lines +26 to +29
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agents tend to over explain with what and not why. I'd trim this a bit more (same for all of the new comments here)

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,
Comment on lines +63 to +64
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather decouple this, like maybe pass filterWorkspaces?: (ws: Workspace[]) => Workspace[] and remove any knowledge of the current user from here

) {
// 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,
);
}
}
Comment on lines +126 to +134
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we make this a filter then we can just make it a const and should be cleaner (separation of concerns)


// 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