Skip to content

Commit 18bffe8

Browse files
authored
impl: ability to application name as main page title (#220)
Netflix would like the ability to use application name displayed in the dashboard as the main page title instead of the URL. This PR adds a new option `useAppNameAsTitle` that allows users to specify whether or not they want to use the application name visible in the dashboard as Tbx main tile instead of the URL. The default will remain the URL. Unlike previous settings added for Netflix this one is also configurable from the UI (Coder Settings page) so not only via settings.json file. This is an option that probably makes sense for more users.
1 parent 186630f commit 18bffe8

File tree

11 files changed

+94
-8
lines changed

11 files changed

+94
-8
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Added
6+
7+
- application name can now be displayed as the main title page instead of the URL
8+
59
## 0.7.2 - 2025-11-03
610

711
### Changed

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,25 @@ class CoderRemoteProvider(
5757

5858
private val triggerSshConfig = Channel<Boolean>(Channel.CONFLATED)
5959
private val triggerProviderVisible = Channel<Boolean>(Channel.CONFLATED)
60-
private val settingsPage: CoderSettingsPage = CoderSettingsPage(context, triggerSshConfig)
6160
private val dialogUi = DialogUi(context)
6261

6362
// The REST client, if we are signed in
6463
private var client: CoderRestClient? = null
6564

6665
// On the first load, automatically log in if we can.
6766
private var firstRun = true
67+
6868
private val isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false)
6969
private val coderHeaderPage = NewEnvironmentPage(context.i18n.pnotr(context.deploymentUrl.toString()))
70+
private val settingsPage: CoderSettingsPage = CoderSettingsPage(context, triggerSshConfig) {
71+
client?.let { restClient ->
72+
if (context.settingsStore.useAppNameAsTitle) {
73+
coderHeaderPage.setTitle(context.i18n.pnotr(restClient.appName))
74+
} else {
75+
coderHeaderPage.setTitle(context.i18n.pnotr(restClient.url.toString()))
76+
}
77+
}
78+
}
7079
private val visibilityState = MutableStateFlow(
7180
ProviderVisibilityState(
7281
applicationVisible = false,
@@ -227,7 +236,7 @@ class CoderRemoteProvider(
227236
val url = context.settingsStore.workspaceCreateUrl ?: client?.url?.withPath("/templates").toString()
228237
context.desktop.browse(
229238
url
230-
.replace("\$workspaceOwner", client?.me()?.username ?: "")
239+
.replace("\$workspaceOwner", client?.me?.username ?: "")
231240
) {
232241
context.ui.showErrorInfoPopup(it)
233242
}
@@ -333,8 +342,11 @@ class CoderRemoteProvider(
333342
}
334343
context.logger.info("Starting initialization with the new settings")
335344
this@CoderRemoteProvider.client = restClient
336-
coderHeaderPage.setTitle(context.i18n.pnotr(restClient.url.toString()))
337-
345+
if (context.settingsStore.useAppNameAsTitle) {
346+
coderHeaderPage.setTitle(context.i18n.pnotr(restClient.appName))
347+
} else {
348+
coderHeaderPage.setTitle(context.i18n.pnotr(restClient.url.toString()))
349+
}
338350
environments.showLoadingMessage()
339351
pollJob = poll(restClient, cli)
340352
context.logger.info("Workspace poll job with name ${pollJob.toString()} was created while handling URI $uri")
@@ -421,7 +433,11 @@ class CoderRemoteProvider(
421433
context.logger.info("Cancelled workspace poll job ${pollJob.toString()} in order to start a new one")
422434
}
423435
environments.showLoadingMessage()
424-
coderHeaderPage.setTitle(context.i18n.pnotr(client.url.toString()))
436+
if (context.settingsStore.useAppNameAsTitle) {
437+
coderHeaderPage.setTitle(context.i18n.pnotr(client.appName))
438+
} else {
439+
coderHeaderPage.setTitle(context.i18n.pnotr(client.url.toString()))
440+
}
425441
context.logger.info("Displaying ${client.url} in the UI")
426442
pollJob = poll(client, cli)
427443
context.logger.info("Workspace poll job with name ${pollJob.toString()} was created")

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.coder.toolbox.sdk.ex.APIResponseException
1010
import com.coder.toolbox.sdk.interceptors.Interceptors
1111
import com.coder.toolbox.sdk.v2.CoderV2RestFacade
1212
import com.coder.toolbox.sdk.v2.models.ApiErrorResponse
13+
import com.coder.toolbox.sdk.v2.models.Appearance
1314
import com.coder.toolbox.sdk.v2.models.BuildInfo
1415
import com.coder.toolbox.sdk.v2.models.CreateWorkspaceBuildRequest
1516
import com.coder.toolbox.sdk.v2.models.Template
@@ -45,6 +46,7 @@ open class CoderRestClient(
4546

4647
lateinit var me: User
4748
lateinit var buildVersion: String
49+
lateinit var appName: String
4850

4951
init {
5052
setupSession()
@@ -94,14 +96,15 @@ open class CoderRestClient(
9496
suspend fun initializeSession(): User {
9597
me = me()
9698
buildVersion = buildInfo().version
99+
appName = appearance().applicationName
97100
return me
98101
}
99102

100103
/**
101104
* Retrieve the current user.
102105
* @throws [APIResponseException].
103106
*/
104-
suspend fun me(): User {
107+
internal suspend fun me(): User {
105108
val userResponse = retroRestClient.me()
106109
if (!userResponse.isSuccessful) {
107110
throw APIResponseException(
@@ -117,6 +120,25 @@ open class CoderRestClient(
117120
}
118121
}
119122

123+
/**
124+
* Retrieves the visual dashboard configuration.
125+
*/
126+
internal suspend fun appearance(): Appearance {
127+
val appearanceResponse = retroRestClient.appearance()
128+
if (!appearanceResponse.isSuccessful) {
129+
throw APIResponseException(
130+
"initializeSession",
131+
url,
132+
appearanceResponse.code(),
133+
appearanceResponse.parseErrorBody(moshi)
134+
)
135+
}
136+
137+
return requireNotNull(appearanceResponse.body()) {
138+
"Successful response returned null body for visual dashboard configuration"
139+
}
140+
}
141+
120142
/**
121143
* Retrieves the available workspaces created by the user.
122144
* @throws [APIResponseException].

src/main/kotlin/com/coder/toolbox/sdk/v2/CoderV2RestFacade.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.coder.toolbox.sdk.v2
22

3+
import com.coder.toolbox.sdk.v2.models.Appearance
34
import com.coder.toolbox.sdk.v2.models.BuildInfo
45
import com.coder.toolbox.sdk.v2.models.CreateWorkspaceBuildRequest
56
import com.coder.toolbox.sdk.v2.models.Template
@@ -23,6 +24,12 @@ interface CoderV2RestFacade {
2324
@GET("api/v2/users/me")
2425
suspend fun me(): Response<User>
2526

27+
/**
28+
* Returns the configuration of the visual dashboard.
29+
*/
30+
@GET("api/v2/appearance")
31+
suspend fun appearance(): Response<Appearance>
32+
2633
/**
2734
* Retrieves all workspaces the authenticated user has access to.
2835
*/
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.coder.toolbox.sdk.v2.models
2+
3+
import com.squareup.moshi.Json
4+
import com.squareup.moshi.JsonClass
5+
6+
@JsonClass(generateAdapter = true)
7+
data class Appearance(
8+
@property:Json(name = "application_name") val applicationName: String
9+
)

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ interface ReadOnlyCoderSettings {
1919
*/
2020
val defaultURL: String
2121

22+
/**
23+
* Whether to display the application name instead of the URL
24+
* in the main screen. Defaults to URL
25+
*/
26+
val useAppNameAsTitle: Boolean
27+
2228
/**
2329
* Used to download the Coder CLI which is necessary to proxy SSH
2430
* connections. The If-None-Match header will be set to the SHA1 of the CLI

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class CoderSettingsStore(
3838
// Properties implementation
3939
override val lastDeploymentURL: String? get() = store[LAST_USED_URL]
4040
override val defaultURL: String get() = store[DEFAULT_URL] ?: "https://dev.coder.com"
41+
override val useAppNameAsTitle: Boolean get() = store[APP_NAME_AS_TITLE]?.toBooleanStrictOrNull() ?: false
4142
override val binarySource: String? get() = store[BINARY_SOURCE]
4243
override val binaryDirectory: String? get() = store[BINARY_DIRECTORY]
4344
override val disableSignatureVerification: Boolean
@@ -165,6 +166,10 @@ class CoderSettingsStore(
165166
store[LAST_USED_URL] = url.toString()
166167
}
167168

169+
fun updateUseAppNameAsTitle(appNameAsTitle: Boolean) {
170+
store[APP_NAME_AS_TITLE] = appNameAsTitle.toString()
171+
}
172+
168173
fun updateBinarySource(source: String) {
169174
store[BINARY_SOURCE] = source
170175
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ internal const val LAST_USED_URL = "lastDeploymentURL"
66

77
internal const val DEFAULT_URL = "defaultURL"
88

9+
internal const val APP_NAME_AS_TITLE = "useAppNameAsTitle"
10+
911
internal const val BINARY_SOURCE = "binarySource"
1012

1113
internal const val BINARY_DIRECTORY = "binaryDirectory"

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ import kotlinx.coroutines.launch
2828
* TODO@JB: There is no scroll, and our settings do not fit. As a consequence,
2929
* I have not been able to test this page.
3030
*/
31-
class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConfig: Channel<Boolean>) :
31+
class CoderSettingsPage(
32+
private val context: CoderToolboxContext,
33+
triggerSshConfig: Channel<Boolean>,
34+
private val onSettingsClosed: () -> Unit
35+
) :
3236
CoderPage(MutableStateFlow(context.i18n.ptrl("Coder Settings")), false) {
3337
private val settings = context.settingsStore.readOnly()
3438

@@ -41,6 +45,8 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf
4145
TextField(context.i18n.ptrl("Data directory"), settings.dataDirectory ?: "", TextType.General)
4246
private val enableDownloadsField =
4347
CheckboxField(settings.enableDownloads, context.i18n.ptrl("Enable downloads"))
48+
private val useAppNameField =
49+
CheckboxField(settings.useAppNameAsTitle, context.i18n.ptrl("Use app name as main page title instead of URL"))
4450

4551
private val disableSignatureVerificationField = CheckboxField(
4652
settings.disableSignatureVerification,
@@ -95,6 +101,7 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf
95101
listOf(
96102
binarySourceField,
97103
enableDownloadsField,
104+
useAppNameField,
98105
binaryDirectoryField,
99106
enableBinaryDirectoryFallbackField,
100107
disableSignatureVerificationField,
@@ -121,6 +128,7 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf
121128
context.settingsStore.updateBinaryDirectory(binaryDirectoryField.contentState.value)
122129
context.settingsStore.updateDataDirectory(dataDirectoryField.contentState.value)
123130
context.settingsStore.updateEnableDownloads(enableDownloadsField.checkedState.value)
131+
context.settingsStore.updateUseAppNameAsTitle(useAppNameField.checkedState.value)
124132
context.settingsStore.updateDisableSignatureVerification(disableSignatureVerificationField.checkedState.value)
125133
context.settingsStore.updateSignatureFallbackStrategy(signatureFallbackStrategyField.checkedState.value)
126134
context.settingsStore.updateHttpClientLogLevel(httpLoggingField.selectedValueState.value)
@@ -164,6 +172,9 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf
164172
enableDownloadsField.checkedState.update {
165173
settings.enableDownloads
166174
}
175+
useAppNameField.checkedState.update {
176+
settings.useAppNameAsTitle
177+
}
167178
signatureFallbackStrategyField.checkedState.update {
168179
settings.fallbackOnCoderForSignatures.isAllowed()
169180
}
@@ -225,5 +236,6 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf
225236

226237
override fun afterHide() {
227238
visibilityUpdateJob.cancel()
239+
onSettingsClosed()
228240
}
229241
}

src/main/resources/localization/defaultMessages.po

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,6 @@ msgstr ""
189189

190190
msgid "Workspace name"
191191
msgstr ""
192+
193+
msgid "Use app name as main page title instead of URL"
194+
msgstr ""

0 commit comments

Comments
 (0)