diff --git a/CHANGELOG.md b/CHANGELOG.md index 8817ffb..35e430f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index 300f5a9..6084880 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -57,7 +57,6 @@ class CoderRemoteProvider( private val triggerSshConfig = Channel(Channel.CONFLATED) private val triggerProviderVisible = Channel(Channel.CONFLATED) - private val settingsPage: CoderSettingsPage = CoderSettingsPage(context, triggerSshConfig) private val dialogUi = DialogUi(context) // The REST client, if we are signed in @@ -65,8 +64,18 @@ class CoderRemoteProvider( // On the first load, automatically log in if we can. private var firstRun = true + private val isInitialized: MutableStateFlow = 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, @@ -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) } @@ -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") @@ -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") diff --git a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt index 1ded07a..58b5837 100644 --- a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt +++ b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt @@ -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 @@ -45,6 +46,7 @@ open class CoderRestClient( lateinit var me: User lateinit var buildVersion: String + lateinit var appName: String init { setupSession() @@ -94,6 +96,7 @@ open class CoderRestClient( suspend fun initializeSession(): User { me = me() buildVersion = buildInfo().version + appName = appearance().applicationName return me } @@ -101,7 +104,7 @@ open class CoderRestClient( * Retrieve the current user. * @throws [APIResponseException]. */ - suspend fun me(): User { + internal suspend fun me(): User { val userResponse = retroRestClient.me() if (!userResponse.isSuccessful) { throw APIResponseException( @@ -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]. diff --git a/src/main/kotlin/com/coder/toolbox/sdk/v2/CoderV2RestFacade.kt b/src/main/kotlin/com/coder/toolbox/sdk/v2/CoderV2RestFacade.kt index adcaa6e..5e7fc13 100644 --- a/src/main/kotlin/com/coder/toolbox/sdk/v2/CoderV2RestFacade.kt +++ b/src/main/kotlin/com/coder/toolbox/sdk/v2/CoderV2RestFacade.kt @@ -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 @@ -23,6 +24,12 @@ interface CoderV2RestFacade { @GET("api/v2/users/me") suspend fun me(): Response + /** + * Returns the configuration of the visual dashboard. + */ + @GET("api/v2/appearance") + suspend fun appearance(): Response + /** * Retrieves all workspaces the authenticated user has access to. */ diff --git a/src/main/kotlin/com/coder/toolbox/sdk/v2/models/Appearance.kt b/src/main/kotlin/com/coder/toolbox/sdk/v2/models/Appearance.kt new file mode 100644 index 0000000..0c8d830 --- /dev/null +++ b/src/main/kotlin/com/coder/toolbox/sdk/v2/models/Appearance.kt @@ -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 +) diff --git a/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt b/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt index 8eed699..edf4801 100644 --- a/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt +++ b/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt @@ -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 diff --git a/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt b/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt index becdea0..ed8f009 100644 --- a/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt +++ b/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt @@ -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 @@ -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 } diff --git a/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt b/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt index d38631a..bc46c4f 100644 --- a/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt +++ b/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt @@ -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" diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt index 5d5f115..b74b2d8 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt @@ -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) : +class CoderSettingsPage( + private val context: CoderToolboxContext, + triggerSshConfig: Channel, + private val onSettingsClosed: () -> Unit +) : CoderPage(MutableStateFlow(context.i18n.ptrl("Coder Settings")), false) { private val settings = context.settingsStore.readOnly() @@ -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, @@ -95,6 +101,7 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf listOf( binarySourceField, enableDownloadsField, + useAppNameField, binaryDirectoryField, enableBinaryDirectoryFallbackField, disableSignatureVerificationField, @@ -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) @@ -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() } @@ -225,5 +236,6 @@ class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConf override fun afterHide() { visibilityUpdateJob.cancel() + onSettingsClosed() } } diff --git a/src/main/resources/localization/defaultMessages.po b/src/main/resources/localization/defaultMessages.po index 29351e3..16b6ed5 100644 --- a/src/main/resources/localization/defaultMessages.po +++ b/src/main/resources/localization/defaultMessages.po @@ -189,3 +189,6 @@ msgstr "" msgid "Workspace name" msgstr "" + +msgid "Use app name as main page title instead of URL" +msgstr "" \ No newline at end of file diff --git a/src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt b/src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt index 4a9ef88..1a84061 100644 --- a/src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt +++ b/src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt @@ -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) )