Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Added

- application name can now be displayed as the main title page instead of the URL

## 0.7.2 - 2025-11-03

### Changed
Expand Down
26 changes: 21 additions & 5 deletions src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,25 @@ class CoderRemoteProvider(

private val triggerSshConfig = Channel<Boolean>(Channel.CONFLATED)
private val triggerProviderVisible = Channel<Boolean>(Channel.CONFLATED)
private val settingsPage: CoderSettingsPage = CoderSettingsPage(context, triggerSshConfig)
private val dialogUi = DialogUi(context)

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

// On the first load, automatically log in if we can.
private var firstRun = true

private val isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false)
private val coderHeaderPage = NewEnvironmentPage(context.i18n.pnotr(context.deploymentUrl.toString()))
private val settingsPage: CoderSettingsPage = CoderSettingsPage(context, triggerSshConfig) {
client?.let { restClient ->
if (context.settingsStore.useAppNameAsTitle) {
coderHeaderPage.setTitle(context.i18n.pnotr(restClient.appName))
} else {
coderHeaderPage.setTitle(context.i18n.pnotr(restClient.url.toString()))
}
}
}
private val visibilityState = MutableStateFlow(
ProviderVisibilityState(
applicationVisible = false,
Expand Down Expand Up @@ -227,7 +236,7 @@ class CoderRemoteProvider(
val url = context.settingsStore.workspaceCreateUrl ?: client?.url?.withPath("/templates").toString()
context.desktop.browse(
url
.replace("\$workspaceOwner", client?.me()?.username ?: "")
.replace("\$workspaceOwner", client?.me?.username ?: "")
) {
context.ui.showErrorInfoPopup(it)
}
Expand Down Expand Up @@ -333,8 +342,11 @@ class CoderRemoteProvider(
}
context.logger.info("Starting initialization with the new settings")
this@CoderRemoteProvider.client = restClient
coderHeaderPage.setTitle(context.i18n.pnotr(restClient.url.toString()))

if (context.settingsStore.useAppNameAsTitle) {
coderHeaderPage.setTitle(context.i18n.pnotr(restClient.appName))
} else {
coderHeaderPage.setTitle(context.i18n.pnotr(restClient.url.toString()))
}
environments.showLoadingMessage()
pollJob = poll(restClient, cli)
context.logger.info("Workspace poll job with name ${pollJob.toString()} was created while handling URI $uri")
Expand Down Expand Up @@ -421,7 +433,11 @@ class CoderRemoteProvider(
context.logger.info("Cancelled workspace poll job ${pollJob.toString()} in order to start a new one")
}
environments.showLoadingMessage()
coderHeaderPage.setTitle(context.i18n.pnotr(client.url.toString()))
if (context.settingsStore.useAppNameAsTitle) {
coderHeaderPage.setTitle(context.i18n.pnotr(client.appName))
} else {
coderHeaderPage.setTitle(context.i18n.pnotr(client.url.toString()))
}
context.logger.info("Displaying ${client.url} in the UI")
pollJob = poll(client, cli)
context.logger.info("Workspace poll job with name ${pollJob.toString()} was created")
Expand Down
24 changes: 23 additions & 1 deletion src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.coder.toolbox.sdk.ex.APIResponseException
import com.coder.toolbox.sdk.interceptors.Interceptors
import com.coder.toolbox.sdk.v2.CoderV2RestFacade
import com.coder.toolbox.sdk.v2.models.ApiErrorResponse
import com.coder.toolbox.sdk.v2.models.Appearance
import com.coder.toolbox.sdk.v2.models.BuildInfo
import com.coder.toolbox.sdk.v2.models.CreateWorkspaceBuildRequest
import com.coder.toolbox.sdk.v2.models.Template
Expand Down Expand Up @@ -45,6 +46,7 @@ open class CoderRestClient(

lateinit var me: User
lateinit var buildVersion: String
lateinit var appName: String

init {
setupSession()
Expand Down Expand Up @@ -94,14 +96,15 @@ open class CoderRestClient(
suspend fun initializeSession(): User {
me = me()
buildVersion = buildInfo().version
appName = appearance().applicationName
return me
}

/**
* Retrieve the current user.
* @throws [APIResponseException].
*/
suspend fun me(): User {
internal suspend fun me(): User {
val userResponse = retroRestClient.me()
if (!userResponse.isSuccessful) {
throw APIResponseException(
Expand All @@ -117,6 +120,25 @@ open class CoderRestClient(
}
}

/**
* Retrieves the visual dashboard configuration.
*/
internal suspend fun appearance(): Appearance {
val appearanceResponse = retroRestClient.appearance()
if (!appearanceResponse.isSuccessful) {
throw APIResponseException(
"initializeSession",
url,
appearanceResponse.code(),
appearanceResponse.parseErrorBody(moshi)
)
}

return requireNotNull(appearanceResponse.body()) {
"Successful response returned null body or visual dashboard configuration"
}
}

/**
* Retrieves the available workspaces created by the user.
* @throws [APIResponseException].
Expand Down
7 changes: 7 additions & 0 deletions src/main/kotlin/com/coder/toolbox/sdk/v2/CoderV2RestFacade.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.coder.toolbox.sdk.v2

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

/**
* Returns the configuration of the visual dashboard.
*/
@GET("api/v2/appearance")
suspend fun appearance(): Response<Appearance>

/**
* Retrieves all workspaces the authenticated user has access to.
*/
Expand Down
9 changes: 9 additions & 0 deletions src/main/kotlin/com/coder/toolbox/sdk/v2/models/Appearance.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.coder.toolbox.sdk.v2.models

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class Appearance(
@property:Json(name = "application_name") val applicationName: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ interface ReadOnlyCoderSettings {
*/
val defaultURL: String

/**
* Whether to display the application name instead of the URL
* in the main screen. Defaults to URL
*/
val useAppNameAsTitle: Boolean

/**
* Used to download the Coder CLI which is necessary to proxy SSH
* connections. The If-None-Match header will be set to the SHA1 of the CLI
Expand Down
5 changes: 5 additions & 0 deletions src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class CoderSettingsStore(
// Properties implementation
override val lastDeploymentURL: String? get() = store[LAST_USED_URL]
override val defaultURL: String get() = store[DEFAULT_URL] ?: "https://dev.coder.com"
override val useAppNameAsTitle: Boolean get() = store[APP_NAME_AS_TITLE]?.toBooleanStrictOrNull() ?: false
override val binarySource: String? get() = store[BINARY_SOURCE]
override val binaryDirectory: String? get() = store[BINARY_DIRECTORY]
override val disableSignatureVerification: Boolean
Expand Down Expand Up @@ -165,6 +166,10 @@ class CoderSettingsStore(
store[LAST_USED_URL] = url.toString()
}

fun updateUseAppNameAsTitle(appNameAsTitle: Boolean) {
store[APP_NAME_AS_TITLE] = appNameAsTitle.toString()
}

fun updateBinarySource(source: String) {
store[BINARY_SOURCE] = source
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ internal const val LAST_USED_URL = "lastDeploymentURL"

internal const val DEFAULT_URL = "defaultURL"

internal const val APP_NAME_AS_TITLE = "useAppNameAsTitle"

internal const val BINARY_SOURCE = "binarySource"

internal const val BINARY_DIRECTORY = "binaryDirectory"
Expand Down
14 changes: 13 additions & 1 deletion src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ import kotlinx.coroutines.launch
* TODO@JB: There is no scroll, and our settings do not fit. As a consequence,
* I have not been able to test this page.
*/
class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConfig: Channel<Boolean>) :
class CoderSettingsPage(
private val context: CoderToolboxContext,
triggerSshConfig: Channel<Boolean>,
private val onSettingsClosed: () -> Unit
) :
CoderPage(MutableStateFlow(context.i18n.ptrl("Coder Settings")), false) {
private val settings = context.settingsStore.readOnly()

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

private val disableSignatureVerificationField = CheckboxField(
settings.disableSignatureVerification,
Expand Down Expand Up @@ -95,6 +101,7 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf
listOf(
binarySourceField,
enableDownloadsField,
useAppNameField,
binaryDirectoryField,
enableBinaryDirectoryFallbackField,
disableSignatureVerificationField,
Expand All @@ -121,6 +128,7 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf
context.settingsStore.updateBinaryDirectory(binaryDirectoryField.contentState.value)
context.settingsStore.updateDataDirectory(dataDirectoryField.contentState.value)
context.settingsStore.updateEnableDownloads(enableDownloadsField.checkedState.value)
context.settingsStore.updateUseAppNameAsTitle(useAppNameField.checkedState.value)
context.settingsStore.updateDisableSignatureVerification(disableSignatureVerificationField.checkedState.value)
context.settingsStore.updateSignatureFallbackStrategy(signatureFallbackStrategyField.checkedState.value)
context.settingsStore.updateHttpClientLogLevel(httpLoggingField.selectedValueState.value)
Expand Down Expand Up @@ -164,6 +172,9 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf
enableDownloadsField.checkedState.update {
settings.enableDownloads
}
useAppNameField.checkedState.update {
settings.useAppNameAsTitle
}
signatureFallbackStrategyField.checkedState.update {
settings.fallbackOnCoderForSignatures.isAllowed()
}
Expand Down Expand Up @@ -225,5 +236,6 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf

override fun afterHide() {
visibilityUpdateJob.cancel()
onSettingsClosed()
}
}
3 changes: 3 additions & 0 deletions src/main/resources/localization/defaultMessages.po
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,6 @@ msgstr ""

msgid "Workspace name"
msgstr ""

msgid "Use app name as main page title instead of URL"
msgstr ""
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ internal class CoderProtocolHandlerTest {
private val protocolHandler = CoderProtocolHandler(
context,
DialogUi(context),
CoderSettingsPage(context, Channel(Channel.CONFLATED)),
CoderSettingsPage(context, Channel(Channel.CONFLATED), {}),
MutableStateFlow(ProviderVisibilityState(applicationVisible = true, providerVisible = true)),
MutableStateFlow(false)
)
Expand Down
Loading