Skip to content

Commit 09b6551

Browse files
committed
feat: shared workspaces support
Add a `workspaceFilter` setting (defaults to `owner:me`) so users can broaden the listing to include workspaces shared with them. The URI handler now accepts an optional `owner` query parameter to disambiguate workspaces by name across owners, and the environment list surfaces the workspace owner as an environment information entry. Generated with Coder Agents on behalf of @aslilac.
1 parent c8291f7 commit 09b6551

12 files changed

Lines changed: 86 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
- `Binary destination` can now point directly to an executable, used as-is; otherwise it is treated as a base directory
88
as before
99
- support for OAuth2
10+
- workspace listing now accepts a configurable `Workspace filter` (defaults to `owner:me`); leave it blank to include
11+
workspaces shared with the current user
12+
- the URI handler accepts an optional `owner` query parameter to disambiguate shared workspaces
1013

1114
### Removed
1215

src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ class CoderRemoteEnvironment(
6464
MutableStateFlow(environmentStatus.toRemoteEnvironmentState(context))
6565
override val description: MutableStateFlow<EnvironmentDescription> =
6666
MutableStateFlow(EnvironmentDescription.General(context.i18n.pnotr(workspace.templateDisplayName)))
67-
override val additionalEnvironmentInformation: MutableMap<LocalizableString, String> = mutableMapOf()
67+
override val additionalEnvironmentInformation: MutableMap<LocalizableString, String> =
68+
mutableMapOf(context.i18n.ptrl("Owner") to workspace.ownerName)
6869
override val actionsList: MutableStateFlow<List<ActionDescription>> = MutableStateFlow(emptyList())
6970

7071
private val networkMetricsMarshaller = Moshi.Builder().build().adapter(NetworkMetrics::class.java)

src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,19 @@ open class CoderRestClient(
161161
}
162162

163163
/**
164-
* Retrieves the available workspaces created by the user.
164+
* Retrieves the available workspaces visible to the current user.
165+
*
166+
* The query string is taken from
167+
* [com.coder.toolbox.settings.ReadOnlyCoderSettings.workspaceFilter], which
168+
* defaults to `owner:me`. Users can broaden it (for example to an empty
169+
* string) to also include workspaces shared with them.
170+
*
165171
* @throws [APIResponseException].
166172
*/
167173
suspend fun workspaces(): List<Workspace> {
168-
val workspacesResponse = callWithRetry { retroRestClient.workspaces("owner:me") }
174+
val workspacesResponse = callWithRetry {
175+
retroRestClient.workspaces(context.settingsStore.workspaceFilter)
176+
}
169177
if (!workspacesResponse.isSuccessful) {
170178
throw APIResponseException(
171179
"retrieve workspaces",

src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,13 @@ interface ReadOnlyCoderSettings {
157157
*/
158158
val workspaceCreateUrl: String?
159159

160+
/**
161+
* The filter applied when listing workspaces, passed to the server as the
162+
* `q` query parameter. Defaults to `owner:me`. Set to an empty string to
163+
* include workspaces shared with the current user.
164+
*/
165+
val workspaceFilter: String
166+
160167
/**
161168
* The path where network information for SSH hosts are stored
162169
*/

src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ class CoderSettingsStore(
8888
get() = store[WORKSPACE_VIEW_URL]
8989
override val workspaceCreateUrl: String?
9090
get() = store[WORKSPACE_CREATE_URL]
91+
override val workspaceFilter: String
92+
get() = store[WORKSPACE_FILTER] ?: "owner:me"
9193

9294
/**
9395
* Where the specified deployment should put its data.
@@ -262,6 +264,10 @@ class CoderSettingsStore(
262264
store[PREFER_OAUTH2_IF_AVAILABLE] = preferAuthViaOAuth2.toString()
263265
}
264266

267+
fun updateWorkspaceFilter(filter: String) {
268+
store[WORKSPACE_FILTER] = filter
269+
}
270+
265271
private fun getDefaultGlobalDataDir(): Path {
266272
return when (getOS()) {
267273
OS.WINDOWS -> Paths.get(getWinAppData(), "coder-toolbox")

src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ internal const val NETWORK_INFO_DIR = "networkInfoDir"
5353

5454
internal const val WORKSPACE_VIEW_URL = "workspaceViewUrl"
5555
internal const val WORKSPACE_CREATE_URL = "workspaceCreateUrl"
56+
internal const val WORKSPACE_FILTER = "workspaceFilter"
5657

5758
internal const val SSH_AUTO_CONNECT_PREFIX = "ssh_auto_connect_"
5859

src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ open class CoderProtocolHandler(
4646
cli: CoderCLIManager
4747
) {
4848
val workspaceName = resolveWorkspaceName(params) ?: return
49-
val workspace = restClient.workspaces().matchName(workspaceName, url)
49+
val workspace = restClient.workspaces().matchName(workspaceName, params.owner(), url)
5050
if (workspace != null) {
5151
if (!prepareWorkspace(workspace, restClient, cli, url)) return
5252
// we resolve the agent after the workspace is started otherwise we can get misleading
@@ -82,12 +82,22 @@ open class CoderProtocolHandler(
8282
return workspace
8383
}
8484

85-
private suspend fun List<Workspace>.matchName(workspaceName: String, deploymentURL: URL): Workspace? {
86-
val workspace = this.firstOrNull { it.name == workspaceName }
85+
private suspend fun List<Workspace>.matchName(
86+
workspaceName: String,
87+
owner: String?,
88+
deploymentURL: URL,
89+
): Workspace? {
90+
val candidates = this.filter { it.name == workspaceName }
91+
val workspace = if (owner.isNullOrBlank()) {
92+
candidates.firstOrNull()
93+
} else {
94+
candidates.firstOrNull { it.ownerName == owner }
95+
}
8796
if (workspace == null) {
97+
val descriptor = if (owner.isNullOrBlank()) workspaceName else "$owner/$workspaceName"
8898
context.logAndShowError(
8999
CAN_T_HANDLE_URI_TITLE,
90-
"There is no workspace with name $workspaceName on $deploymentURL"
100+
"There is no workspace with name $descriptor on $deploymentURL"
91101
)
92102
return null
93103
}

src/main/kotlin/com/coder/toolbox/util/LinkMap.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.coder.toolbox.util
33
const val URL = "url"
44
const val TOKEN = "token"
55
const val WORKSPACE = "workspace"
6+
const val OWNER = "owner"
67
const val AGENT_NAME = "agent_name"
78
private const val IDE_PRODUCT_CODE = "ide_product_code"
89
private const val IDE_BUILD_NUMBER = "ide_build_number"
@@ -14,6 +15,8 @@ fun Map<String, String>.token() = this[TOKEN]
1415

1516
fun Map<String, String>.workspace() = this[WORKSPACE]
1617

18+
fun Map<String, String>.owner() = this[OWNER]
19+
1720
fun Map<String, String?>.agentName() = this[AGENT_NAME]
1821

1922
fun Map<String, String>.ideProductCode() = this[IDE_PRODUCT_CODE]

src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ class CoderSettingsPage(
125125
TextType.General
126126
)
127127

128+
private val workspaceFilterField = TextField(
129+
context.i18n.ptrl("Workspace filter (leave blank to include shared workspaces)"),
130+
settings.workspaceFilter,
131+
TextType.General,
132+
)
133+
128134
private lateinit var visibilityUpdateJob: Job
129135
override val fields: StateFlow<List<UiField>> = MutableStateFlow(
130136
listOf(
@@ -134,6 +140,7 @@ class CoderSettingsPage(
134140
listOf(
135141
useAppNameField,
136142
disableAutostartField,
143+
workspaceFilterField,
137144
httpLoggingField,
138145
)
139146
),
@@ -214,6 +221,7 @@ class CoderSettingsPage(
214221
updateSshLogDir(sshLogDirField.contentState.value)
215222
updateNetworkInfoDir(networkInfoDirField.contentState.value)
216223
updateSshConfigOptions(sshExtraArgs.contentState.value)
224+
updateWorkspaceFilter(workspaceFilterField.contentState.value)
217225
}
218226
}
219227
)
@@ -288,6 +296,10 @@ class CoderSettingsPage(
288296
settings.networkInfoDir
289297
}
290298

299+
workspaceFilterField.contentState.update {
300+
settings.workspaceFilter
301+
}
302+
291303
visibilityUpdateJob = context.cs.launch(CoroutineName("Signature Verification Fallback Setting")) {
292304
disableSignatureVerificationField.checkedState.collect { state ->
293305
signatureFallbackStrategyField.visibility.update {

src/test/kotlin/com/coder/toolbox/CoderRemoteProviderTest.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,14 +514,16 @@ class CoderRemoteProviderTest {
514514
private fun mockWorkspace(
515515
name: String,
516516
status: WorkspaceStatus,
517-
resources: List<WorkspaceResource>
517+
resources: List<WorkspaceResource>,
518+
ownerName: String = "owner",
518519
): Workspace {
519520
val latestBuild = mockk<WorkspaceBuild> {
520521
every { this@mockk.status } returns status
521522
every { this@mockk.resources } returns resources
522523
}
523524
return mockk {
524525
every { this@mockk.name } returns name
526+
every { this@mockk.ownerName } returns ownerName
525527
every { this@mockk.latestBuild } returns latestBuild
526528
every { this@mockk.templateDisplayName } returns name
527529
every { this@mockk.outdated } returns false

0 commit comments

Comments
 (0)