Skip to content

Commit 597bc72

Browse files
authored
feature: Add setting to remove/set custom workspace filter for connections view. (#490)
Add setting to remove/set custom workspace filter for connections view.
1 parent 39faf50 commit 597bc72

35 files changed

+331
-162
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44

55
## Unreleased
66

7+
### Added
8+
9+
- Add ability to customize filter for workspace connections view.
10+
- Add owner column to connections view table.
11+
- Add ability to connect to workspaces you don't own but have permissions for.
12+
713
## 2.14.2 - 2024-09-23
814

915
### Changed

src/main/kotlin/com/coder/gateway/CoderSettingsConfigurable.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ class CoderSettingsConfigurable : BoundConfigurable("Coder") {
144144
.bindText(state::sshLogDirectory)
145145
.comment(CoderGatewayBundle.message("gateway.connector.settings.ssh-log-directory.comment"))
146146
}.layout(RowLayout.PARENT_GRID)
147+
row(CoderGatewayBundle.message("gateway.connector.settings.workspace-filter.title")) {
148+
textField().resizableColumn().align(AlignX.FILL)
149+
.bindText(state::workspaceFilter)
150+
.comment(CoderGatewayBundle.message("gateway.connector.settings.workspace-filter.comment"))
151+
}.layout(RowLayout.PARENT_GRID)
147152
}
148153
}
149154

src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ package com.coder.gateway.cli
33
import com.coder.gateway.cli.ex.MissingVersionException
44
import com.coder.gateway.cli.ex.ResponseException
55
import com.coder.gateway.cli.ex.SSHConfigFormatException
6+
import com.coder.gateway.sdk.v2.models.User
7+
import com.coder.gateway.sdk.v2.models.Workspace
8+
import com.coder.gateway.sdk.v2.models.WorkspaceAgent
69
import com.coder.gateway.settings.CoderSettings
710
import com.coder.gateway.settings.CoderSettingsState
811
import com.coder.gateway.util.CoderHostnameVerifier
@@ -219,11 +222,12 @@ class CoderCLIManager(
219222
* This can take supported features for testing purposes only.
220223
*/
221224
fun configSsh(
222-
workspaceNames: Set<String>,
225+
workspacesAndAgents: Set<Pair<Workspace, WorkspaceAgent>>,
226+
currentUser: User,
223227
feats: Features = features,
224228
) {
225229
logger.info("Configuring SSH config at ${settings.sshConfigPath}")
226-
writeSSHConfig(modifySSHConfig(readSSHConfig(), workspaceNames, feats))
230+
writeSSHConfig(modifySSHConfig(readSSHConfig(), workspacesAndAgents, feats, currentUser))
227231
}
228232

229233
/**
@@ -245,8 +249,9 @@ class CoderCLIManager(
245249
*/
246250
private fun modifySSHConfig(
247251
contents: String?,
248-
workspaceNames: Set<String>,
252+
workspaceNames: Set<Pair<Workspace, WorkspaceAgent>>,
249253
feats: Features,
254+
currentUser: User,
250255
): String? {
251256
val host = deploymentURL.safeHost()
252257
val startBlock = "# --- START CODER JETBRAINS $host"
@@ -287,8 +292,8 @@ class CoderCLIManager(
287292
System.lineSeparator() + endBlock,
288293
transform = {
289294
"""
290-
Host ${getHostName(deploymentURL, it)}
291-
ProxyCommand ${proxyArgs.joinToString(" ")} $it
295+
Host ${getHostName(deploymentURL, it.first, currentUser, it.second)}
296+
ProxyCommand ${proxyArgs.joinToString(" ")} ${getWorkspaceParts(it.first, it.second)}
292297
ConnectTimeout 0
293298
StrictHostKeyChecking no
294299
UserKnownHostsFile /dev/null
@@ -299,8 +304,8 @@ class CoderCLIManager(
299304
.plus("\n")
300305
.plus(
301306
"""
302-
Host ${getBackgroundHostName(deploymentURL, it)}
303-
ProxyCommand ${backgroundProxyArgs.joinToString(" ")} $it
307+
Host ${getBackgroundHostName(deploymentURL, it.first, currentUser, it.second)}
308+
ProxyCommand ${backgroundProxyArgs.joinToString(" ")} ${getWorkspaceParts(it.first, it.second)}
304309
ConnectTimeout 0
305310
StrictHostKeyChecking no
306311
UserKnownHostsFile /dev/null
@@ -478,17 +483,43 @@ class CoderCLIManager(
478483

479484
private val tokenRegex = "--token [^ ]+".toRegex()
480485

486+
/**
487+
* This function returns the ssh host name generated for connecting to the workspace.
488+
*/
481489
@JvmStatic
482490
fun getHostName(
483491
url: URL,
484-
workspaceName: String,
485-
): String = "coder-jetbrains--$workspaceName--${url.safeHost()}"
492+
workspace: Workspace,
493+
currentUser: User,
494+
agent: WorkspaceAgent,
495+
): String =
496+
// For a user's own workspace, we use the old syntax without a username for backwards compatibility,
497+
// since the user might have recent connections that still use the old syntax.
498+
if (currentUser.username == workspace.ownerName) {
499+
"coder-jetbrains--${workspace.name}.${agent.name}--${url.safeHost()}"
500+
} else {
501+
"coder-jetbrains--${workspace.ownerName}--${workspace.name}.${agent.name}--${url.safeHost()}"
502+
}
486503

487-
@JvmStatic
488504
fun getBackgroundHostName(
489505
url: URL,
490-
workspaceName: String,
491-
): String = getHostName(url, workspaceName) + "--bg"
506+
workspace: Workspace,
507+
currentUser: User,
508+
agent: WorkspaceAgent,
509+
): String {
510+
return getHostName(url, workspace, currentUser, agent) + "--bg"
511+
}
512+
513+
514+
/**
515+
* This function returns the identifier for the workspace to pass to the
516+
* coder ssh proxy command.
517+
*/
518+
@JvmStatic
519+
fun getWorkspaceParts(
520+
workspace: Workspace,
521+
agent: WorkspaceAgent,
522+
): String = "${workspace.ownerName}/${workspace.name}.${agent.name}"
492523

493524
@JvmStatic
494525
fun getBackgroundHostName(

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

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,7 @@ import com.coder.gateway.sdk.convertors.OSConverter
88
import com.coder.gateway.sdk.convertors.UUIDConverter
99
import com.coder.gateway.sdk.ex.APIResponseException
1010
import com.coder.gateway.sdk.v2.CoderV2RestFacade
11-
import com.coder.gateway.sdk.v2.models.BuildInfo
12-
import com.coder.gateway.sdk.v2.models.CreateWorkspaceBuildRequest
13-
import com.coder.gateway.sdk.v2.models.Template
14-
import com.coder.gateway.sdk.v2.models.User
15-
import com.coder.gateway.sdk.v2.models.Workspace
16-
import com.coder.gateway.sdk.v2.models.WorkspaceBuild
17-
import com.coder.gateway.sdk.v2.models.WorkspaceResource
18-
import com.coder.gateway.sdk.v2.models.WorkspaceTransition
11+
import com.coder.gateway.sdk.v2.models.*
1912
import com.coder.gateway.settings.CoderSettings
2013
import com.coder.gateway.settings.CoderSettingsState
2114
import com.coder.gateway.util.CoderHostnameVerifier
@@ -166,7 +159,7 @@ open class CoderRestClient(
166159
* @throws [APIResponseException].
167160
*/
168161
fun workspaces(): List<Workspace> {
169-
val workspacesResponse = retroRestClient.workspaces("owner:me").execute()
162+
val workspacesResponse = retroRestClient.workspaces(settings.workspaceFilter).execute()
170163
if (!workspacesResponse.isSuccessful) {
171164
throw APIResponseException("retrieve workspaces", url, workspacesResponse)
172165
}
@@ -178,12 +171,12 @@ open class CoderRestClient(
178171
* Retrieves all the agent names for all workspaces, including those that
179172
* are off. Meant to be used when configuring SSH.
180173
*/
181-
fun agentNames(workspaces: List<Workspace>): Set<String> {
174+
fun withAgents(workspaces: List<Workspace>): Set<Pair<Workspace, WorkspaceAgent>> {
182175
// It is possible for there to be resources with duplicate names so we
183176
// need to use a set.
184177
return workspaces.flatMap { ws ->
185178
ws.latestBuild.resources.ifEmpty { resources(ws) }.filter { it.agents != null }.flatMap { it.agents!! }.map {
186-
"${ws.name}.${it.name}"
179+
ws to it
187180
}
188181
}.toSet()
189182
}

src/main/kotlin/com/coder/gateway/sdk/v2/models/Workspace.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ data class Workspace(
1919
@Json(name = "latest_build") val latestBuild: WorkspaceBuild,
2020
@Json(name = "outdated") val outdated: Boolean,
2121
@Json(name = "name") val name: String,
22+
@Json(name = "owner_name") val ownerName: String,
2223
)
2324

2425
/**

src/main/kotlin/com/coder/gateway/settings/CoderSettings.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ open class CoderSettingsState(
9898
open var defaultURL: String = "",
9999
// Value for --log-dir.
100100
open var sshLogDirectory: String = "",
101+
// Default filter for fetching workspaces
102+
open var workspaceFilter: String = "owner:me"
101103
)
102104

103105
/**
@@ -135,6 +137,12 @@ open class CoderSettings(
135137
val enableDownloads: Boolean
136138
get() = state.enableDownloads
137139

140+
/**
141+
* The filter to apply when fetching workspaces (default is owner:me)
142+
*/
143+
val workspaceFilter: String
144+
get() = state.workspaceFilter
145+
138146
/**
139147
* Whether falling back to the data directory is allowed if the binary
140148
* directory is not writable.

src/main/kotlin/com/coder/gateway/util/Dialogs.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,13 @@ import javax.swing.border.Border
3232
* A dialog wrapper around CoderWorkspaceStepView.
3333
*/
3434
private class CoderWorkspaceStepDialog(
35-
name: String,
3635
private val state: CoderWorkspacesStepSelection,
3736
) : DialogWrapper(true) {
3837
private val view = CoderWorkspaceProjectIDEStepView(showTitle = false)
3938

4039
init {
4140
init()
42-
title = CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.choose.text", name)
41+
title = CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.choose.text", CoderCLIManager.getWorkspaceParts(state.workspace, state.agent))
4342
}
4443

4544
override fun show() {
@@ -71,7 +70,6 @@ private class CoderWorkspaceStepDialog(
7170
}
7271

7372
fun askIDE(
74-
name: String,
7573
agent: WorkspaceAgent,
7674
workspace: Workspace,
7775
cli: CoderCLIManager,
@@ -82,7 +80,6 @@ fun askIDE(
8280
ApplicationManager.getApplication().invokeAndWait {
8381
val dialog =
8482
CoderWorkspaceStepDialog(
85-
name,
8683
CoderWorkspacesStepSelection(agent, workspace, cli, client, workspaces),
8784
)
8885
data = dialog.showAndGetData()

src/main/kotlin/com/coder/gateway/util/LinkHandler.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ open class LinkHandler(
106106
}
107107

108108
indicator?.invoke("Configuring Coder CLI...")
109-
cli.configSsh(client.agentNames(workspaces))
109+
cli.configSsh(workspacesAndAgents = client.withAgents(workspaces), currentUser = client.me)
110110

111111
val name = "${workspace.name}.${agent.name}"
112112
val openDialog =
@@ -116,14 +116,14 @@ open class LinkHandler(
116116
parameters.folder().isNullOrBlank()
117117

118118
return if (openDialog) {
119-
askIDE(name, agent, workspace, cli, client, workspaces) ?: throw MissingArgumentException("IDE selection aborted; unable to connect")
119+
askIDE(agent, workspace, cli, client, workspaces) ?: throw MissingArgumentException("IDE selection aborted; unable to connect")
120120
} else {
121121
// Check that both the domain and the redirected domain are
122122
// allowlisted. If not, check with the user whether to proceed.
123123
verifyDownloadLink(parameters)
124124
WorkspaceProjectIDE.fromInputs(
125125
name = name,
126-
hostname = CoderCLIManager.getHostName(deploymentURL.toURL(), name),
126+
hostname = CoderCLIManager.getHostName(deploymentURL.toURL(), workspace, client.me, agent),
127127
projectPath = parameters.folder(),
128128
ideProductCode = parameters.ideProductCode(),
129129
ideBuildNumber = parameters.ideBuildNumber(),

src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspaceProjectIDEStepView.kt

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -184,23 +184,22 @@ class CoderWorkspaceProjectIDEStepView(
184184

185185
// We use this when returning the connection params from data().
186186
state = data
187-
188-
val name = "${data.workspace.name}.${data.agent.name}"
187+
val name = CoderCLIManager.getWorkspaceParts(data.workspace, data.agent)
189188
logger.info("Initializing workspace step for $name")
190189

191190
val homeDirectory = data.agent.expandedDirectory ?: data.agent.directory
192191
tfProject.text = if (homeDirectory.isNullOrBlank()) "/home" else homeDirectory
193192
titleLabel.text = CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.choose.text", name)
194193
titleLabel.isVisible = showTitle
195-
terminalLink.url = data.client.url.withPath("/me/$name/terminal").toString()
194+
terminalLink.url = data.client.url.withPath("/$name/terminal").toString()
196195

197196
ideResolvingJob =
198197
cs.launch(ModalityState.current().asContextElement()) {
199198
try {
200199
logger.info("Configuring Coder CLI...")
201200
cbIDE.renderer = IDECellRenderer("Configuring Coder CLI...")
202201
withContext(Dispatchers.IO) {
203-
data.cliManager.configSsh(data.client.agentNames(data.workspaces))
202+
data.cliManager.configSsh(data.client.withAgents(data.workspaces), data.client.me)
204203
}
205204

206205
val ides =
@@ -215,7 +214,7 @@ class CoderWorkspaceProjectIDEStepView(
215214
} else {
216215
IDECellRenderer(CoderGatewayBundle.message("gateway.connector.view.coder.connect-ssh"))
217216
}
218-
val executor = createRemoteExecutor(CoderCLIManager.getBackgroundHostName(data.client.url, name))
217+
val executor = createRemoteExecutor(CoderCLIManager.getBackgroundHostName(data.client.url, data.workspace, data.client.me, data.agent))
219218

220219
if (ComponentValidator.getInstance(tfProject).isEmpty) {
221220
logger.info("Installing remote path validator...")
@@ -338,7 +337,7 @@ class CoderWorkspaceProjectIDEStepView(
338337
workspace: Workspace,
339338
agent: WorkspaceAgent,
340339
): List<IdeWithStatus> {
341-
val name = "${workspace.name}.${agent.name}"
340+
val name = CoderCLIManager.getWorkspaceParts(workspace, agent)
342341
logger.info("Retrieving available IDEs for $name...")
343342
val workspaceOS =
344343
if (agent.operatingSystem != null && agent.architecture != null) {
@@ -406,7 +405,7 @@ class CoderWorkspaceProjectIDEStepView(
406405
val name = "${state.workspace.name}.${state.agent.name}"
407406
selectedIDE.withWorkspaceProject(
408407
name = name,
409-
hostname = CoderCLIManager.getHostName(state.client.url, name),
408+
hostname = CoderCLIManager.getHostName(state.client.url, state.workspace, state.client.me, state.agent),
410409
projectPath = tfProject.text,
411410
deploymentURL = state.client.url,
412411
)

src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import com.intellij.openapi.application.ModalityState
3333
import com.intellij.openapi.application.asContextElement
3434
import com.intellij.openapi.components.service
3535
import com.intellij.openapi.diagnostic.Logger
36+
import com.intellij.openapi.observable.properties.ObservableMutableProperty
3637
import com.intellij.openapi.rd.util.launchUnderBackgroundProgress
3738
import com.intellij.openapi.ui.panel.ComponentPanelBuilder
3839
import com.intellij.openapi.ui.setEmptyState
@@ -89,7 +90,7 @@ private const val SESSION_TOKEN_KEY = "session-token"
8990
private data class CoderWorkspacesFormFields(
9091
var coderURL: String = "",
9192
var token: Pair<String, Source>? = null,
92-
var useExistingToken: Boolean = false,
93+
var useExistingToken: Boolean = false
9394
)
9495

9596
/**
@@ -751,7 +752,7 @@ class CoderWorkspacesStepView :
751752
override fun data(): CoderWorkspacesStepSelection {
752753
val selected = tableOfWorkspaces.selectedObject
753754
return withoutNull(client, cliManager, selected?.agent, selected?.workspace) { client, cli, agent, workspace ->
754-
val name = "${workspace.name}.${agent.name}"
755+
val name = CoderCLIManager.getWorkspaceParts(workspace, agent)
755756
logger.info("Returning data for $name")
756757
CoderWorkspacesStepSelection(
757758
agent = agent,
@@ -783,6 +784,7 @@ class WorkspacesTableModel :
783784
ListTableModel<WorkspaceAgentListModel>(
784785
WorkspaceIconColumnInfo(""),
785786
WorkspaceNameColumnInfo("Name"),
787+
WorkspaceOwnerColumnInfo("Owner"),
786788
WorkspaceTemplateNameColumnInfo("Template"),
787789
WorkspaceVersionColumnInfo("Version"),
788790
WorkspaceStatusColumnInfo("Status"),
@@ -849,6 +851,36 @@ class WorkspacesTableModel :
849851
}
850852
}
851853

854+
private class WorkspaceOwnerColumnInfo(columnName: String) : ColumnInfo<WorkspaceAgentListModel, String>(columnName) {
855+
override fun valueOf(item: WorkspaceAgentListModel?): String? = item?.workspace?.ownerName
856+
857+
override fun getComparator(): Comparator<WorkspaceAgentListModel> = Comparator { a, b ->
858+
a.workspace.ownerName.compareTo(b.workspace.ownerName, ignoreCase = true)
859+
}
860+
861+
override fun getRenderer(item: WorkspaceAgentListModel?): TableCellRenderer {
862+
return object : DefaultTableCellRenderer() {
863+
override fun getTableCellRendererComponent(
864+
table: JTable,
865+
value: Any,
866+
isSelected: Boolean,
867+
hasFocus: Boolean,
868+
row: Int,
869+
column: Int,
870+
): Component {
871+
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column)
872+
if (value is String) {
873+
text = value
874+
}
875+
876+
font = RelativeFont.BOLD.derive(table.tableHeader.font)
877+
border = JBUI.Borders.empty(0, 8)
878+
return this
879+
}
880+
}
881+
}
882+
}
883+
852884
private class WorkspaceTemplateNameColumnInfo(columnName: String) : ColumnInfo<WorkspaceAgentListModel, String>(columnName) {
853885
override fun valueOf(item: WorkspaceAgentListModel?): String? = item?.workspace?.templateName
854886

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