Skip to content

feat: filter and scope the workspace list#318

Merged
fioan89 merged 3 commits into
mainfrom
impl-support-for-filtering-workspaces
Jun 22, 2026
Merged

feat: filter and scope the workspace list#318
fioan89 merged 3 commits into
mainfrom
impl-support-for-filtering-workspaces

Conversation

@fioan89

@fioan89 fioan89 commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Until now the plugin pulled back workspaces without applying any meaningful scope, so the list it showed was effectively the whole deployment. There was no way to narrow it down or to deliberately choose whose workspaces you wanted to see.

This hit administrators hardest. On a large deployment the list could be huge, mixing together the workspaces of every user, with no quick way to focus on a particular person, template, or state.

This change puts that choice in the user's hands. A scope selector switches between just your own workspaces and every workspace you're allowed to access, and the selection is remembered separately for each deployment so it sticks the next time you connect. Alongside it, a search box narrows the list by name, template, owner, or status, including quoted multi-word values and a plain-text shortcut that searches by workspace name. An explicit owner in the search always takes precedence over the selected scope. Changing the scope or the search reloads the list and regenerates SSH configuration to match what is shown.

Deep links can now carry an owner as well, so a link to a workspace name that several users share resolves to the right one instead of guessing. When no owner is given the lookup stays unscoped, preserving the previous behavior.

To keep the list manageable out of the box, it now defaults to showing only your own workspaces. Anyone who relied on seeing everything can switch the scope to all workspaces, and that preference is then remembered per deployment.

  • resolves DEVEX-372
  • resolves DEVEX-374

Until now the plugin pulled back workspaces without applying any meaningful
scope, so the list it showed was effectively the whole deployment. There was no
way to narrow it down or to deliberately choose whose workspaces you wanted to
see.

This hit administrators hardest. On a large deployment the list could be huge,
mixing together the workspaces of every user, with no quick way to focus on a
particular person, template, or state.

This change puts that choice in the user's hands. A scope selector switches
between just your own workspaces and every workspace you're allowed to access,
and the selection is remembered separately for each deployment so it sticks the
next time you connect. Alongside it, a search box narrows the list by name,
template, owner, or status, including quoted multi-word values and a plain-text
shortcut that searches by workspace name. An explicit owner in the search always
takes precedence over the selected scope. Changing the scope or the search
reloads the list and regenerates SSH configuration to match what is shown.

Deep links can now carry an owner as well, so a link to a workspace name that
several users share resolves to the right one instead of guessing. When no owner
is given the lookup stays unscoped, preserving the previous behavior.

To keep the list manageable out of the box, it now defaults to showing only your
own workspaces. Anyone who relied on seeing everything can switch the scope to
all workspaces, and that preference is then remembered per deployment.
@linear-code

linear-code Bot commented Jun 16, 2026

Copy link
Copy Markdown

DEVEX-372

DEVEX-374

@fioan89 fioan89 requested review from EhabY, code-asher, jeremyruppel and zenithwolf1000 and removed request for code-asher June 16, 2026 21:31

@zenithwolf1000 zenithwolf1000 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM

val scoped = if (scope == WorkspaceScope.MY_WORKSPACES && terms.none { it.key == OWNER_KEY }) {
listOf(WorkspaceSearchTerm(OWNER_KEY, CURRENT_USER)) + terms
} else {
terms

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

TBH I feel like for large deployments, if scope is all workspaces, we should force some kinda filter or at least default to something. Not really a change requested, but wanted to call out that admins would find this view pretty useless without some query params.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

they have the search bar where they can limit status, by template name, by workspace name or even by a specific owner.

@code-asher

Copy link
Copy Markdown
Member

Have not had a chance to test it out yet but wanted to call out:

it now defaults to showing only your own workspaces. Anyone who relied on seeing everything can switch the scope to all workspaces

The default was always owner:me was it not?

Comment on lines +269 to +280
enum class WorkspaceScope {
MY_WORKSPACES,
ALL_WORKSPACES;

companion object {
fun fromValue(value: String?): WorkspaceScope = when (value?.lowercase(getDefault())) {
"all_workspaces" -> ALL_WORKSPACES
"my_workspaces" -> MY_WORKSPACES
else -> MY_WORKSPACES
}
}
}

@code-asher code-asher Jun 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just a suggestion so feel free to ignore but I wonder if we should store a query instead?

That way, if in the future we do get more filters we can store them all.

So we could rename this to WorkspaceFilter and for now we would store just owner:me or a blank string for all workspaces. Then we parse them to figure out the state of the radio.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It is an interesting approach and I think it would make a lot of sense if the search bar would also be persisted. However right now this only persist the state of of a radio button. I think if I go with the proposal right now I would end up comparing a string to decide which radio button is enabled, which is disabled.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

True, you would have to parse the setting, but you already have a parser!

I think it would be something like terms.any { it.key == "owner" && it.value == "me" } and terms.none { it.key == "owner" } for the two radio conditions.

Imagine for example we add a radio button for filtering shared workspaces, no changes to the settings would be required, just another radio button that adds shared_with_user:me and does a check like terms.any { it.key == "shared_with_user" && it.value == "me"}.

And if we ever add multiple filters at once, that would just work as well.


/**
* Parses a raw filter query into search terms. Recognized `key:value` pairs are kept as-is; any
* bare words are merged into a single `name:` term. A key may take its value inline (`status:running`)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IMO we should just send the query exactly as-is and let the backend decide what the bare words should mean, so we have parity with the dashboard.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done

val workspace = restClient.workspaces().matchName(workspaceName, url)

val ownerName = params.owner()?.takeIf { it.isNotBlank() }
val workspaces = restClient.workspaces(workspaceOwnerSearchQuery(params))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just a random thought I had, occurs to me we could also add the name if the workspace if we have it to make the query response smaller. But not something we have to do here; just a thought that occurred to me.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I'm pondering whether to change it or not. I'd probably do it in a separate PR as this one is already huge. But I guess you are right, it will certainly remove some of the code we have cause right now we request all workspaces and then we do a search by name in Toolbox.

fun filterQuery(query: String?): String? =
parse(query).asQuery()

fun workspaceQuery(query: String?, scope: WorkspaceScope): String? {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I might be reading this wrong but it seems like it is possible to (for example) type owner:someone in the search box and then select "my workspaces" but the owner:someone would take priority.

Is it possible that when a radio is selected, we can update the text field with owner:me (removing any other owner)? And vice versa, when the field is edited, check to see if an owner was added/removed and sync up the radio to match?

Similar to the dashboard. But not sure if we have that kind of flexibility here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We do. Changed the approach completely and redesigned the search functionality to look like and work like the web dashboard.

this.client!! to this.cli!!
}
linkHandler.handle(params, newUrl, this.client!!, this.cli!!)
linkHandler.handle(params, newUrl, activeSession.first, activeSession.second)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just curious why this change, when it seems like it ends up using the same this.client and this.cli in the end in both branches anyway.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I was trying to exploit the fact that refreshSession already returns a refreshed pair of http client and cli. And I was also trying to make the !! asertions safer.

Comment thread CHANGELOG.md
- notifications are now persistent popups instead of snackbars, so they survive a hidden window and no longer get dropped
- notifications are now persistent popups instead of snackbars, so they survive a hidden window and no longer get
dropped
- workspace lists now default to `My workspaces`, so users initially see only workspaces they own. Users can

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we can omit the first sentence because the default was already owner:me

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Well... the default was more like: all the workspaces your RBAC rules allow you to have access to.

Comment thread README.md Outdated

The expandable header on the Coder Workspaces page contains workspace scope and search controls.

- `My workspaces` is the new default scope. It adds `owner:me`, so Coder returns workspaces owned by the authenticated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Similarly here

Comment thread README.md Outdated
- `All workspaces` does not add an owner filter, so Coder returns every workspace the authenticated user is allowed to
read. The scope selection is remembered separately for each Coder deployment hostname.
- Search text is not remembered between sessions.
- The search box supports only `owner`, `template`, `name`, and `status`. Unsupported keys and empty values are ignored.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah I feel like we should just send it all verbatim to the backend, otherwise we have to update the plugin every time we add a key, and there is risk of drift.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done.

Comment thread README.md Outdated
- The search box supports only `owner`, `template`, `name`, and `status`. Unsupported keys and empty values are ignored.
- If the search box includes `owner`, that owner takes precedence over the selected scope. For example, `owner:bob`
still searches Bob's workspaces even when `My workspaces` is selected.
- Text without a prefix is treated as a workspace name search. For example, `bobiverse` becomes `name:bobiverse`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

bobiverse!! <3

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Are you a man of culture, like myself? :)

* @throws [APIResponseException].
*/
suspend fun workspaces(): List<Workspace> {
val workspacesResponse = callWithRetry { retroRestClient.workspaces("owner:me") }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yeah default was owner:me looks like

@fioan89

fioan89 commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator Author

Have not had a chance to test it out yet but wanted to call out:

it now defaults to showing only your own workspaces. Anyone who relied on seeing everything can switch the scope to all workspaces

The default was always owner:me was it not?

Well actually there was no q present in the query, which left the owner as "". But really the responses to the api/v2/workspaces is the intersection of the search filter and the workspaces you have read permissions on. So for regular users, you are right, but for admins - they can read everything.

Really - the All flag is something that makes sense for admins and other types of roles that can have a global view.

…ingful scope, so the list it showed was effectively the whole deployment. There was no way to narrow it down or to deliberately choose whose workspaces you wanted to see.

This hit administrators hardest. On a large deployment the list could be huge, mixing together the workspaces of every user, with no quick way to focus on a particular person, template, or state.

This change puts that choice in the user's hands and aligns the experience with the Coder web dashboard. A free-form search bar accepts the same key:value syntax the server understands — owner:, template:, status:, and others — and its text is sent verbatim to the server, which is the authoritative parser. Alongside it, three dropdowns give quick access to the most common choices: a Filters preset menu (My workspaces, All workspaces, Running, Failed, Outdated, Shared, Dormant), a Template picker populated from the deployment, and a Status picker. The dropdowns and the search bar stay in sync: picking a preset or narrowing by template or status writes the corresponding terms into the search bar, and typing directly in the search bar reflects back into the dropdowns. Any validation error from the server — for example a malformed query — is shown inline under the search bar. Changing the search reloads the list and regenerates SSH configuration to match what is shown.

The filter is not persisted between sessions. Each time you connect it resets to owner:me, exactly as the web dashboard does, so you always start with a focused view of your own workspaces and can widen from there.

Deep links can now carry an owner as well, so a link to a workspace name that several users share resolves to the right one instead of guessing. When no owner is given the lookup stays unscoped, preserving the previous behavior.
@fioan89 fioan89 merged commit cac1571 into main Jun 22, 2026
6 checks passed
@fioan89 fioan89 deleted the impl-support-for-filtering-workspaces branch June 22, 2026 19:58
@code-asher

code-asher commented Jun 22, 2026

Copy link
Copy Markdown
Member

Well actually there was no q present in the query

I could definitely be misreading it but in 0.9.0:

val workspacesResponse = callWithRetry { retroRestClient.workspaces("owner:me") }

And then:

@GET("api/v2/workspaces")
suspend fun workspaces(
@Query("q") searchParams: String,
): Response<WorkspacesResponse>

Would this not result in api/v2/workspaces?q=owner:me?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants