Skip to content

Commit bab6c21

Browse files
committed
feat: Add search filter to sidebar
1 parent a75342a commit bab6c21

File tree

4 files changed

+185
-1
lines changed

4 files changed

+185
-1
lines changed

package.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,18 @@
212212
"title": "Coder: Open App Status",
213213
"icon": "$(robot)",
214214
"when": "coder.authenticated"
215+
},
216+
{
217+
"command": "coder.searchWorkspaces",
218+
"title": "Coder: Search Workspaces",
219+
"icon": "$(search)",
220+
"when": "coder.authenticated && coder.isOwner"
221+
},
222+
{
223+
"command": "coder.clearWorkspaceSearch",
224+
"title": "Coder: Clear Workspace Search",
225+
"icon": "$(clear-all)",
226+
"when": "coder.authenticated && coder.isOwner"
215227
}
216228
],
217229
"menus": {
@@ -239,6 +251,21 @@
239251
"command": "coder.refreshWorkspaces",
240252
"when": "coder.authenticated && view == myWorkspaces",
241253
"group": "navigation"
254+
},
255+
{
256+
"command": "coder.searchWorkspaces",
257+
"when": "coder.authenticated && coder.isOwner && view == allWorkspaces",
258+
"group": "navigation"
259+
},
260+
{
261+
"command": "coder.refreshWorkspaces",
262+
"when": "coder.authenticated && coder.isOwner && view == allWorkspaces",
263+
"group": "navigation"
264+
},
265+
{
266+
"command": "coder.clearWorkspaceSearch",
267+
"when": "coder.authenticated && coder.isOwner && view == allWorkspaces",
268+
"group": "navigation"
242269
}
243270
],
244271
"view/item/context": [

src/commands.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,53 @@ export class Commands {
684684
});
685685
}
686686

687+
/**
688+
* Search/filter workspaces in the All Workspaces view.
689+
* This method will be called from the view title menu.
690+
*/
691+
public async searchWorkspaces(): Promise<void> {
692+
const quickPick = vscode.window.createQuickPick();
693+
quickPick.placeholder =
694+
"Type to search workspaces by name, owner, template, status, or agent";
695+
quickPick.title = "Search All Workspaces";
696+
quickPick.value = "";
697+
698+
// Get current search filter to show in the input
699+
const currentFilter = (await vscode.commands.executeCommand(
700+
"coder.getWorkspaceSearchFilter",
701+
)) as string;
702+
if (currentFilter) {
703+
quickPick.value = currentFilter;
704+
}
705+
706+
quickPick.ignoreFocusOut = true; // Keep open when clicking elsewhere
707+
quickPick.canSelectMany = false; // Don't show selection list
708+
709+
quickPick.onDidChangeValue((value) => {
710+
// Update the search filter in real-time as user types
711+
vscode.commands.executeCommand("coder.setWorkspaceSearchFilter", value);
712+
});
713+
714+
quickPick.onDidAccept(() => {
715+
// When user presses Enter, close the search
716+
quickPick.hide();
717+
});
718+
719+
quickPick.onDidHide(() => {
720+
// Don't clear the search when closed - keep the filter active
721+
quickPick.dispose();
722+
});
723+
724+
quickPick.show();
725+
}
726+
727+
/**
728+
* Clear the workspace search filter.
729+
*/
730+
public clearWorkspaceSearch(): void {
731+
vscode.commands.executeCommand("coder.setWorkspaceSearchFilter", "");
732+
}
733+
687734
/**
688735
* Return agents from the workspace.
689736
*

src/extension.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,23 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
290290
"coder.viewLogs",
291291
commands.viewLogs.bind(commands),
292292
);
293+
vscode.commands.registerCommand(
294+
"coder.searchWorkspaces",
295+
commands.searchWorkspaces.bind(commands),
296+
);
297+
vscode.commands.registerCommand(
298+
"coder.setWorkspaceSearchFilter",
299+
(searchTerm: string) => {
300+
allWorkspacesProvider.setSearchFilter(searchTerm);
301+
},
302+
);
303+
vscode.commands.registerCommand("coder.getWorkspaceSearchFilter", () => {
304+
return allWorkspacesProvider.getSearchFilter();
305+
});
306+
vscode.commands.registerCommand(
307+
"coder.clearWorkspaceSearch",
308+
commands.clearWorkspaceSearch.bind(commands),
309+
);
293310

294311
// Since the "onResolveRemoteAuthority:ssh-remote" activation event exists
295312
// in package.json we're able to perform actions before the authority is

src/workspacesProvider.ts

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export class WorkspaceProvider
4242
private timeout: NodeJS.Timeout | undefined;
4343
private fetching = false;
4444
private visible = false;
45+
private searchFilter = "";
4546

4647
constructor(
4748
private readonly getWorkspacesQuery: WorkspaceQuery,
@@ -52,6 +53,15 @@ export class WorkspaceProvider
5253
// No initialization.
5354
}
5455

56+
setSearchFilter(filter: string) {
57+
this.searchFilter = filter;
58+
this.refresh(undefined);
59+
}
60+
61+
getSearchFilter(): string {
62+
return this.searchFilter;
63+
}
64+
5565
// fetchAndRefresh fetches new workspaces, re-renders the entire tree, then
5666
// keeps refreshing (if a timer length was provided) as long as the user is
5767
// still logged in and no errors were encountered fetching workspaces.
@@ -300,7 +310,90 @@ export class WorkspaceProvider
300310

301311
return Promise.resolve([]);
302312
}
303-
return Promise.resolve(this.workspaces || []);
313+
314+
// Filter workspaces based on search term
315+
let filteredWorkspaces = this.workspaces || [];
316+
const trimmedFilter = this.searchFilter.trim();
317+
if (trimmedFilter) {
318+
const searchTerm = trimmedFilter.toLowerCase();
319+
filteredWorkspaces = filteredWorkspaces.filter((workspace) =>
320+
this.matchesSearchTerm(workspace, searchTerm),
321+
);
322+
}
323+
324+
return Promise.resolve(filteredWorkspaces);
325+
}
326+
327+
/**
328+
* Check if a workspace matches the given search term using smart search logic.
329+
* Prioritizes exact word matches over substring matches.
330+
*/
331+
private matchesSearchTerm(
332+
workspace: WorkspaceTreeItem,
333+
searchTerm: string,
334+
): boolean {
335+
const workspaceName = workspace.workspace.name.toLowerCase();
336+
const ownerName = workspace.workspace.owner_name.toLowerCase();
337+
const templateName = (
338+
workspace.workspace.template_display_name ||
339+
workspace.workspace.template_name ||
340+
""
341+
).toLowerCase();
342+
const status = workspace.workspace.latest_build.status.toLowerCase();
343+
344+
// Check if any agent names match the search term
345+
const agents = extractAgents(workspace.workspace.latest_build.resources);
346+
const agentNames = agents.map((agent) => agent.name.toLowerCase());
347+
const hasMatchingAgent = agentNames.some((agentName) =>
348+
agentName.includes(searchTerm),
349+
);
350+
351+
// Check if any agent metadata contains the search term
352+
const hasMatchingMetadata = agents.some((agent) => {
353+
const watcher = this.agentWatchers[agent.id];
354+
if (watcher?.metadata) {
355+
return watcher.metadata.some((metadata) => {
356+
const metadataStr = JSON.stringify(metadata).toLowerCase();
357+
return metadataStr.includes(searchTerm);
358+
});
359+
}
360+
return false;
361+
});
362+
363+
// Smart search: Try exact word match first, then fall back to substring
364+
const searchWords = searchTerm
365+
.split(/\s+/)
366+
.filter((word) => word.length > 0);
367+
const allText = [
368+
workspaceName,
369+
ownerName,
370+
templateName,
371+
status,
372+
...agentNames,
373+
].join(" ");
374+
375+
// Check for exact word matches (higher priority)
376+
const hasExactWordMatch =
377+
searchWords.length > 0 &&
378+
searchWords.some((word) => {
379+
// Escape special regex characters to prevent injection
380+
const escapedWord = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
381+
const wordBoundaryRegex = new RegExp(`\\b${escapedWord}\\b`, "i");
382+
return wordBoundaryRegex.test(allText);
383+
});
384+
385+
// Check for substring matches (lower priority) - only if no exact word match
386+
const hasSubstringMatch =
387+
!hasExactWordMatch &&
388+
(workspaceName.includes(searchTerm) ||
389+
ownerName.includes(searchTerm) ||
390+
templateName.includes(searchTerm) ||
391+
status.includes(searchTerm) ||
392+
hasMatchingAgent ||
393+
hasMatchingMetadata);
394+
395+
// Return true if either exact word match or substring match
396+
return hasExactWordMatch || hasSubstringMatch;
304397
}
305398
}
306399

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy