From 99e56cfaa15397bd1544efb1588236d09443f398 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Tue, 4 Mar 2025 22:27:55 +0200 Subject: [PATCH 01/14] impl: support for Toolbox 2.6.0.38881 (1) - reworked tasks for building the jar and zips - plugin metadata (i.e. extension.json) is now generated by the gradle tasks --- build.gradle.kts | 131 +++++++++++++++++++++++------- gradle/libs.versions.toml | 18 ++-- src/main/resources/extension.json | 15 ---- 3 files changed, 113 insertions(+), 51 deletions(-) delete mode 100644 src/main/resources/extension.json diff --git a/build.gradle.kts b/build.gradle.kts index a541a15..9c37295 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,15 @@ +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.github.jk1.license.filter.ExcludeTransitiveDependenciesFilter import com.github.jk1.license.render.JsonReportRenderer +import com.jetbrains.plugin.structure.toolbox.ToolboxMeta +import com.jetbrains.plugin.structure.toolbox.ToolboxPluginDescriptor import org.jetbrains.intellij.pluginRepository.PluginRepositoryFactory import org.jetbrains.kotlin.com.intellij.openapi.util.SystemInfoRt import org.jetbrains.kotlin.gradle.dsl.JvmTarget import java.nio.file.Path +import kotlin.io.path.createDirectories import kotlin.io.path.div +import kotlin.io.path.writeText plugins { alias(libs.plugins.kotlin) @@ -16,21 +21,28 @@ plugins { alias(libs.plugins.changelog) } -buildscript { - dependencies { - classpath(libs.marketplace.client) - } -} repositories { mavenCentral() maven("https://packages.jetbrains.team/maven/p/tbx/toolbox-api") } +buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath(libs.marketplace.client) + classpath(libs.plugin.structure) + } +} + jvmWrapper { unixJvmInstallDir = "jvm" winJvmInstallDir = "jvm" - linuxAarch64JvmUrl = "https://cache-redirector.jetbrains.com/intellij-jbr/jbr_jcef-21.0.5-linux-aarch64-b631.28.tar.gz" + linuxAarch64JvmUrl = + "https://cache-redirector.jetbrains.com/intellij-jbr/jbr_jcef-21.0.5-linux-aarch64-b631.28.tar.gz" linuxX64JvmUrl = "https://cache-redirector.jetbrains.com/intellij-jbr/jbr_jcef-21.0.5-linux-x64-b631.28.tar.gz" macAarch64JvmUrl = "https://cache-redirector.jetbrains.com/intellij-jbr/jbr_jcef-21.0.5-osx-aarch64-b631.28.tar.gz" macX64JvmUrl = "https://cache-redirector.jetbrains.com/intellij-jbr/jbr_jcef-21.0.5-osx-x64-b631.28.tar.gz" @@ -39,9 +51,9 @@ jvmWrapper { dependencies { compileOnly(libs.bundles.toolbox.plugin.api) + compileOnly(libs.bundles.serialization) + compileOnly(libs.coroutines.core) implementation(libs.slf4j) - implementation(libs.bundles.serialization) - implementation(libs.coroutines.core) implementation(libs.okhttp) implementation(libs.exec) implementation(libs.moshi) @@ -51,12 +63,29 @@ dependencies { testImplementation(kotlin("test")) } -val pluginId = properties("group") -val pluginName = properties("name") -val pluginVersion = properties("version") +val extension = ExtensionJson( + id = properties("group"), + version = properties("version"), + meta = ExtensionJsonMeta( + name = "Coder Toolbox", + description = "Connects your JetBrains IDE to Coder workspaces", + vendor = "Coder", + url = "https://github.com/coder/coder-jetbrains-toolbox-plugin", + ) +) + +val extensionJsonFile = layout.buildDirectory.file("generated/extension.json") +val extensionJson by tasks.registering { + inputs.property("extension", extension.toString()) + + outputs.file(extensionJsonFile) + doLast { + generateExtensionJson(extension, extensionJsonFile.get().asFile.toPath()) + } +} changelog { - version.set(pluginVersion) + version.set(extension.version) groups.set(emptyList()) title.set("Coder Toolbox Plugin Changelog") } @@ -76,15 +105,27 @@ tasks.test { useJUnitPlatform() } +tasks.jar { + archiveBaseName.set(extension.id) + dependsOn(extensionJson) +} + val assemblePlugin by tasks.registering(Jar::class) { - archiveBaseName.set(pluginId) + archiveBaseName.set(extension.id) from(sourceSets.main.get().output) } val copyPlugin by tasks.creating(Sync::class.java) { - dependsOn(assemblePlugin) - fromCompileDependencies() + dependsOn(tasks.assemble) +// fromCompileDependencies() + from(tasks.jar) + + from(extensionJsonFile) + from("src/main/resources") { + include("dependencies.json") + include("icon.svg") + } into(getPluginInstallDir()) } @@ -113,11 +154,21 @@ fun CopySpec.fromCompileDependencies() { } val pluginZip by tasks.creating(Zip::class) { - dependsOn(assemblePlugin) + dependsOn(tasks.assemble) + dependsOn(tasks.getByName("generateLicenseReport")) - fromCompileDependencies() - into(pluginId) - archiveBaseName.set(pluginName) +// fromCompileDependencies() +// into(pluginId) + from(tasks.assemble.get().outputs.files) + from(extensionJsonFile) + from("src/main/resources") { + include("dependencies.json") + } + from("src/main/resources") { + include("icon.svg") + rename("icon.svg", "pluginIcon.svg") + } + archiveBaseName.set(extension.id) } tasks.register("cleanAll", Delete::class.java) { @@ -142,7 +193,7 @@ private fun getPluginInstallDir(): Path { else -> error("Unknown os") } / "plugins" - return pluginsDir / pluginId + return pluginsDir / extension.id } val publishPlugin by tasks.creating { @@ -158,17 +209,41 @@ val publishPlugin by tasks.creating { // instance.uploader.uploadNewPlugin(pluginZip.outputs.files.singleFile, listOf("toolbox", "gateway"), LicenseUrl.APACHE_2_0, ProductFamily.TOOLBOX) // subsequent updates - instance.uploader.upload(pluginId, pluginZip.outputs.files.singleFile) + instance.uploader.upload(extension.id, pluginZip.outputs.files.singleFile) } } -// For use with kotlin-language-server. -tasks.register("classpath") { - doFirst { - File("classpath").writeText( - sourceSets["main"].runtimeClasspath.asPath +fun properties(key: String) = project.findProperty(key).toString() + +// region will be moved to the gradle plugin late +data class ExtensionJsonMeta( + val name: String, + val description: String, + val vendor: String, + val url: String?, +) + +data class ExtensionJson( + val id: String, + val version: String, + val meta: ExtensionJsonMeta, +) + +fun generateExtensionJson(extensionJson: ExtensionJson, destinationFile: Path) { + val descriptor = ToolboxPluginDescriptor( + id = extensionJson.id, + version = extensionJson.version, + apiVersion = libs.versions.toolbox.plugin.api.get(), + meta = ToolboxMeta( + name = extensionJson.meta.name, + description = extensionJson.meta.description, + vendor = extensionJson.meta.vendor, + url = extensionJson.meta.url, ) - } + ) + val extensionJson = jacksonObjectMapper().writeValueAsString(descriptor) + destinationFile.parent.createDirectories() + destinationFile.writeText(extensionJson) } -fun properties(key: String) = project.findProperty(key).toString() \ No newline at end of file +// endregion \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 00bb2c7..5b0639e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -toolbox-plugin-api = "0.7.2.6.0.38311" +toolbox-plugin-api = "1.0.38881" kotlin = "2.1.0" coroutines = "1.10.1" serialization = "1.8.0" @@ -13,6 +13,7 @@ moshi = "1.15.1" ksp = "2.1.0-1.0.29" retrofit = "2.8.2" changelog = "2.2.1" +plugin-structure = "3.298" [libraries] toolbox-core-api = { module = "com.jetbrains.toolbox:core-api", version.ref = "toolbox-plugin-api" } @@ -25,21 +26,22 @@ serialization-json-okio = { module = "org.jetbrains.kotlinx:kotlinx-serializatio okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } exec = { module = "org.zeroturnaround:zt-exec", version.ref = "exec" } -moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi"} -moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi"} -retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit"} -retrofit-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit"} +moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } +moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +retrofit-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" } +plugin-structure = { module = "org.jetbrains.intellij.plugins:structure-toolbox", version.ref = "plugin-structure" } marketplace-client = { module = "org.jetbrains.intellij:plugin-repository-rest-client", version.ref = "marketplace-client" } [bundles] -serialization = [ "serialization-core", "serialization-json", "serialization-json-okio" ] -toolbox-plugin-api = [ "toolbox-core-api", "toolbox-ui-api", "toolbox-remote-dev-api" ] +serialization = ["serialization-core", "serialization-json", "serialization-json-okio"] +toolbox-plugin-api = ["toolbox-core-api", "toolbox-ui-api", "toolbox-remote-dev-api"] [plugins] kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } dependency-license-report = { id = "com.github.jk1.dependency-license-report", version.ref = "dependency-license-report" } -ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"} +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } gradle-wrapper = { id = "me.filippov.gradle.jvm.wrapper", version.ref = "gradle-wrapper" } changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } \ No newline at end of file diff --git a/src/main/resources/extension.json b/src/main/resources/extension.json deleted file mode 100644 index 828c03c..0000000 --- a/src/main/resources/extension.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "com.coder.toolbox", - "version": "0.0.1", - "meta": { - "readableName": "Coder Toolbox", - "description": "This plugin connects your JetBrains IDE to Coder workspaces.", - "vendor": "Coder", - "url": "https://github.com/coder/coder-jetbrains-toolbox-plugin" - }, - "apiVersion": "0.3", - "compatibleVersionRange": { - "from": "2.6.0.0", - "to": "2.6.0.99999" - } -} From 059f3fc4646af72738e20e14b70fbcc3522290ab Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Tue, 4 Mar 2025 22:52:47 +0200 Subject: [PATCH 02/14] impl: support for Toolbox 2.6.0.38881 (2) - initial support for localization which is now mandatory --- build.gradle.kts | 7 ++++++- gradle/libs.versions.toml | 4 +++- .../resources/localization/defaultMessages.po | 17 +++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 src/main/resources/localization/defaultMessages.po diff --git a/build.gradle.kts b/build.gradle.kts index 9c37295..29e7019 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,6 +19,7 @@ plugins { alias(libs.plugins.ksp) alias(libs.plugins.gradle.wrapper) alias(libs.plugins.changelog) + alias(libs.plugins.gettext) } @@ -215,6 +216,11 @@ val publishPlugin by tasks.creating { fun properties(key: String) = project.findProperty(key).toString() +gettext { + potFile = project.layout.projectDirectory.file("src/main/resources/localization/defaultMessages.pot") + keywords = listOf("ptrc:1c,2", "ptrl") +} + // region will be moved to the gradle plugin late data class ExtensionJsonMeta( val name: String, @@ -245,5 +251,4 @@ fun generateExtensionJson(extensionJson: ExtensionJson, destinationFile: Path) { destinationFile.parent.createDirectories() destinationFile.writeText(extensionJson) } - // endregion \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5b0639e..babf037 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ moshi = "1.15.1" ksp = "2.1.0-1.0.29" retrofit = "2.8.2" changelog = "2.2.1" +gettext = "0.7.0" plugin-structure = "3.298" [libraries] @@ -44,4 +45,5 @@ serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref dependency-license-report = { id = "com.github.jk1.dependency-license-report", version.ref = "dependency-license-report" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } gradle-wrapper = { id = "me.filippov.gradle.jvm.wrapper", version.ref = "gradle-wrapper" } -changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } \ No newline at end of file +changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } +gettext = { id = "name.kropp.kotlinx-gettext", version.ref = "gettext" } \ No newline at end of file diff --git a/src/main/resources/localization/defaultMessages.po b/src/main/resources/localization/defaultMessages.po new file mode 100644 index 0000000..398a650 --- /dev/null +++ b/src/main/resources/localization/defaultMessages.po @@ -0,0 +1,17 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2025 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Ioan Faur , 2025. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Coder Toolbox 1.0\n" +"Report-Msgid-Bugs-To: support@coder.com\n" +"POT-Creation-Date: 2025-03-04 12:00+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" From d0af2c9f327af9c0f85d847bb52c104fa6759b4b Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 5 Mar 2025 00:26:46 +0200 Subject: [PATCH 03/14] impl: support for Toolbox 2.6.0.38881 (3) - fix initial batch of compiler errors - mostly strings converted to LocalizedString instances - while some observable properties are now moved to Kotlin's StateFlow --- .../coder/toolbox/CoderRemoteEnvironment.kt | 87 ++++++++++--------- .../com/coder/toolbox/CoderRemoteProvider.kt | 62 +++++++------ .../kotlin/com/coder/toolbox/util/Dialogs.kt | 37 +++++--- .../com/coder/toolbox/util/LinkHandler.kt | 14 ++- .../com/coder/toolbox/views/CoderPage.kt | 24 +++-- .../resources/localization/defaultMessages.po | 60 +++++++++++++ 6 files changed, 186 insertions(+), 98 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index ad9d82f..ff46f15 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -10,14 +10,19 @@ import com.coder.toolbox.util.withPath import com.coder.toolbox.views.Action import com.coder.toolbox.views.EnvironmentView import com.jetbrains.toolbox.api.core.ServiceLocator -import com.jetbrains.toolbox.api.remoteDev.AbstractRemoteProviderEnvironment +import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.remoteDev.EnvironmentVisibilityState +import com.jetbrains.toolbox.api.remoteDev.RemoteProviderEnvironment import com.jetbrains.toolbox.api.remoteDev.environments.EnvironmentContentsView -import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateConsumer +import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentDescription +import com.jetbrains.toolbox.api.remoteDev.states.RemoteEnvironmentState import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager import com.jetbrains.toolbox.api.ui.ToolboxUi +import com.jetbrains.toolbox.api.ui.actions.ActionDescription import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout @@ -35,63 +40,58 @@ class CoderRemoteEnvironment( private var workspace: Workspace, private var agent: WorkspaceAgent, private var cs: CoroutineScope, -) : AbstractRemoteProviderEnvironment("${workspace.name}.${agent.name}") { +) : RemoteProviderEnvironment("${workspace.name}.${agent.name}") { private var status = WorkspaceAndAgentStatus.from(workspace, agent) private val ui: ToolboxUi = serviceLocator.getService(ToolboxUi::class.java) + private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java) override var name: String = "${workspace.name}.${agent.name}" + override val state: StateFlow + get() = TODO("Not yet implemented") + override val description: StateFlow + get() = TODO("Not yet implemented") - init { - actionsList.add( - Action("Open web terminal") { + override val actionsList: StateFlow> = MutableStateFlow( + listOf( + Action(i18n.ptrl("Open web terminal")) { cs.launch { BrowserUtil.browse(client.url.withPath("/${workspace.ownerName}/$name/terminal").toString()) { ui.showErrorInfoPopup(it) } } }, - ) - actionsList.add( - Action("Open in dashboard") { + Action(i18n.ptrl("Open in dashboard")) { cs.launch { BrowserUtil.browse(client.url.withPath("/@${workspace.ownerName}/${workspace.name}").toString()) { ui.showErrorInfoPopup(it) } } }, - ) - actionsList.add( - Action("View template") { + + Action(i18n.ptrl("View template")) { cs.launch { BrowserUtil.browse(client.url.withPath("/templates/${workspace.templateName}").toString()) { ui.showErrorInfoPopup(it) } } }, - ) - actionsList.add( - Action("Start", enabled = { status.canStart() }) { + Action(i18n.ptrl("Start"), enabled = { status.canStart() }) { val build = client.startWorkspace(workspace) workspace = workspace.copy(latestBuild = build) update(workspace, agent) }, - ) - actionsList.add( - Action("Stop", enabled = { status.canStop() }) { + Action(i18n.ptrl("Stop"), enabled = { status.canStop() }) { val build = client.stopWorkspace(workspace) workspace = workspace.copy(latestBuild = build) update(workspace, agent) }, - ) - actionsList.add( - Action("Update", enabled = { workspace.outdated }) { + Action(i18n.ptrl("Update"), enabled = { workspace.outdated }) { val build = client.updateWorkspace(workspace) workspace = workspace.copy(latestBuild = build) update(workspace, agent) - }, - ) - } + }) + ) /** * Update the workspace/agent status to the listeners, if it has changed. @@ -103,7 +103,7 @@ class CoderRemoteEnvironment( if (newStatus != status) { status = newStatus val state = status.toRemoteEnvironmentState(serviceLocator) - listenerSet.forEach { it.consume(state) } +// listenerSet.forEach { it.consume(state) } } } @@ -111,7 +111,8 @@ class CoderRemoteEnvironment( * The contents are provided by the SSH view provided by Toolbox, all we * have to do is provide it a host name. */ - override suspend fun getContentsView(): EnvironmentContentsView = EnvironmentView(client.url, workspace, agent) + override suspend + fun getContentsView(): EnvironmentContentsView = EnvironmentView(client.url, workspace, agent) /** * Does nothing. In theory, we could do something like start the workspace @@ -124,15 +125,15 @@ class CoderRemoteEnvironment( /** * Immediately send the state to the listener and store for updates. */ - override fun addStateListener(consumer: EnvironmentStateConsumer): Boolean { - // TODO@JB: It would be ideal if we could have the workspace state and - // the connected state listed separately, since right now the - // connected state can mask the workspace state. - // TODO@JB: You can still press connect if the environment is - // unreachable. Is that expected? - consumer.consume(status.toRemoteEnvironmentState(serviceLocator)) - return super.addStateListener(consumer) - } +// override fun addStateListener(consumer: EnvironmentStateConsumer): Boolean { +// // TODO@JB: It would be ideal if we could have the workspace state and +// // the connected state listed separately, since right now the +// // connected state can mask the workspace state. +// // TODO@JB: You can still press connect if the environment is +// // unreachable. Is that expected? +// consumer.consume(status.toRemoteEnvironmentState(serviceLocator)) +// return super.addStateListener(consumer) +// } override fun onDelete() { cs.launch { @@ -140,17 +141,17 @@ class CoderRemoteEnvironment( // However, #showSnackbar works on other pages. Until JetBrains fixes this issue we are going to use the snackbar val shouldDelete = if (status.canStop()) { ui.showOkCancelPopup( - "Delete running workspace?", - "Workspace will be closed and all the information in this workspace will be lost, including all files, unsaved changes and historical.", - "Delete", - "Cancel" + i18n.ptrl("Delete running workspace?"), + i18n.ptrl("Workspace will be closed and all the information in this workspace will be lost, including all files, unsaved changes and historical."), + i18n.ptrl("Delete"), + i18n.ptrl("Cancel") ) } else { ui.showOkCancelPopup( - "Delete workspace?", - "All the information in this workspace will be lost, including all files, unsaved changes and historical.", - "Delete", - "Cancel" + i18n.ptrl("Delete workspace?"), + i18n.ptrl("All the information in this workspace will be lost, including all files, unsaved changes and historical."), + i18n.ptrl("Delete"), + i18n.ptrl("Cancel") ) } if (shouldDelete) { diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index a360229..36f9f22 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -21,17 +21,20 @@ import com.jetbrains.toolbox.api.core.PluginSecretStore import com.jetbrains.toolbox.api.core.PluginSettingsStore import com.jetbrains.toolbox.api.core.ServiceLocator import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon +import com.jetbrains.toolbox.api.core.util.LoadableState +import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState -import com.jetbrains.toolbox.api.remoteDev.RemoteEnvironmentConsumer import com.jetbrains.toolbox.api.remoteDev.RemoteProvider +import com.jetbrains.toolbox.api.remoteDev.RemoteProviderEnvironment import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager import com.jetbrains.toolbox.api.ui.ToolboxUi -import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription -import com.jetbrains.toolbox.api.ui.components.AccountDropdownField +import com.jetbrains.toolbox.api.ui.actions.ActionDescription import com.jetbrains.toolbox.api.ui.components.UiPage import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import okhttp3.OkHttpClient @@ -39,6 +42,8 @@ import java.net.URI import java.net.URL import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration.Companion.seconds +import com.jetbrains.toolbox.api.ui.components.AccountDropdownField as DropDownMenu +import com.jetbrains.toolbox.api.ui.components.AccountDropdownField as dropDownFactory class CoderRemoteProvider( private val serviceLocator: ServiceLocator, @@ -47,10 +52,10 @@ class CoderRemoteProvider( private val logger = CoderLoggerFactory.getLogger(javaClass) private val ui: ToolboxUi = serviceLocator.getService(ToolboxUi::class.java) - private val consumer: RemoteEnvironmentConsumer = serviceLocator.getService(RemoteEnvironmentConsumer::class.java) private val coroutineScope: CoroutineScope = serviceLocator.getService(CoroutineScope::class.java) private val settingsStore: PluginSettingsStore = serviceLocator.getService(PluginSettingsStore::class.java) private val secretsStore: PluginSecretStore = serviceLocator.getService(PluginSecretStore::class.java) + private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java) // Current polling job. private var pollJob: Job? = null @@ -61,8 +66,8 @@ class CoderRemoteProvider( private val settings: CoderSettings = CoderSettings(settingsService) private val secrets: CoderSecretsService = CoderSecretsService(secretsStore) private val settingsPage: CoderSettingsPage = CoderSettingsPage(settingsService) - private val dialogUi = DialogUi(settings, ui) - private val linkHandler = LinkHandler(settings, httpClient, dialogUi) + private val dialogUi = DialogUi(serviceLocator, settings) + private val linkHandler = LinkHandler(serviceLocator, settings, httpClient, dialogUi) // The REST client, if we are signed in private var client: CoderRestClient? = null @@ -75,6 +80,10 @@ class CoderRemoteProvider( // On the first load, automatically log in if we can. private var firstRun = true + override val environments: MutableStateFlow>> = MutableStateFlow( + LoadableState.Loading + ) + /** * With the provided client, start polling for workspaces. Every time a new * workspace is added, reconfigure SSH using the provided cli (including the @@ -84,7 +93,7 @@ class CoderRemoteProvider( while (isActive) { try { logger.debug("Fetching workspace agents from {}", client.url) - val environments = client.workspaces().flatMap { ws -> + val resolvedEnvironments = client.workspaces().flatMap { ws -> // Agents are not included in workspaces that are off // so fetch them separately. when (ws.latestBuild.status) { @@ -117,16 +126,16 @@ class CoderRemoteProvider( // Reconfigure if a new environment is found. // TODO@JB: Should we use the add/remove listeners instead? val newEnvironments = lastEnvironments - ?.let { environments.subtract(it) } - ?: environments + ?.let { resolvedEnvironments.subtract(it) } + ?: resolvedEnvironments if (newEnvironments.isNotEmpty()) { logger.info("Found new environment(s), reconfiguring CLI: {}", newEnvironments) cli.configSsh(newEnvironments.map { it.name }.toSet()) } - consumer.consumeEnvironments(environments, true) + environments.value = LoadableState.Value(resolvedEnvironments.toList()) - lastEnvironments = environments + lastEnvironments = resolvedEnvironments } catch (_: CancellationException) { logger.debug("{} polling loop canceled", client.url) break @@ -155,21 +164,20 @@ class CoderRemoteProvider( /** * A dropdown that appears at the top of the environment list to the right. */ - override fun getAccountDropDown(): AccountDropdownField? { + override fun getAccountDropDown(): DropDownMenu? { val username = client?.me?.username if (username != null) { - return AccountDropdownField(username, Runnable { logout() }) + return dropDownFactory(i18n.pnotr(username), { logout() }) } return null } - /** - * List of actions that appear next to the account. - */ - override fun getAdditionalPluginActions(): List = listOf( - Action("Settings", closesPage = false) { - ui.showUiPage(settingsPage) - }, + override val additionalPluginActions: StateFlow> = MutableStateFlow( + listOf( + Action(i18n.ptrl("Settings")) { + ui.showUiPage(settingsPage) + }, + ) ) /** @@ -182,7 +190,7 @@ class CoderRemoteProvider( pollJob?.cancel() client = null lastEnvironments = null - consumer.consumeEnvironments(emptyList(), true) + environments.value = LoadableState.Value(emptyList()) } override val svgIcon: SvgIcon = @@ -226,20 +234,10 @@ class CoderRemoteProvider( */ override fun setVisible(visibilityState: ProviderVisibilityState) {} - /** - * Ignored; unsure if we should use this over the consumer we get passed in. - */ - override fun addEnvironmentsListener(listener: RemoteEnvironmentConsumer) {} - - /** - * Ignored; unsure if we should use this over the consumer we get passed in. - */ - override fun removeEnvironmentsListener(listener: RemoteEnvironmentConsumer) {} - /** * Handle incoming links (like from the dashboard). */ - override fun handleUri(uri: URI) { + override suspend fun handleUri(uri: URI) { val params = uri.toQueryParameters() coroutineScope.launch { val name = linkHandler.handle(params) diff --git a/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt b/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt index 8414e9d..1986453 100644 --- a/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt +++ b/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt @@ -3,6 +3,9 @@ package com.coder.toolbox.util import com.coder.toolbox.browser.BrowserUtil import com.coder.toolbox.settings.CoderSettings import com.coder.toolbox.settings.Source +import com.jetbrains.toolbox.api.core.ServiceLocator +import com.jetbrains.toolbox.api.localization.LocalizableString +import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.ui.ToolboxUi import com.jetbrains.toolbox.api.ui.components.TextType import java.net.URL @@ -13,23 +16,33 @@ import java.net.URL * This is meant to mimic ToolboxUi. */ class DialogUi( + private val serviceLocator: ServiceLocator, private val settings: CoderSettings, - private val ui: ToolboxUi, ) { - suspend fun confirm(title: String, description: String): Boolean { - return ui.showOkCancelPopup(title, description, "Yes", "No") + private val ui: ToolboxUi = serviceLocator.getService(ToolboxUi::class.java) + private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java) + + suspend fun confirm(title: LocalizableString, description: LocalizableString): Boolean { + return ui.showOkCancelPopup(title, description, i18n.ptrl("Yes"), i18n.ptrl("No")) } suspend fun ask( - title: String, - description: String, - placeholder: String? = null, + title: LocalizableString, + description: LocalizableString, + placeholder: LocalizableString? = null, // There is no link or error support in Toolbox so for now isError and // link are unused. isError: Boolean = false, link: Pair? = null, ): String? { - return ui.showTextInputPopup(title, description, placeholder, TextType.General, "OK", "Cancel") + return ui.showTextInputPopup( + title, + description, + placeholder, + TextType.General, + i18n.ptrl("OK"), + i18n.ptrl("Cancel") + ) } private suspend fun openUrl(url: URL) { @@ -79,11 +92,13 @@ class DialogUi( // for the token. val tokenFromUser = ask( - title = "Session Token", - description = error + title = i18n.ptrl("Session Token"), + description = i18n.pnotr( + error ?: token?.second?.description("token") - ?: "No existing token for ${url.host} found.", - placeholder = token?.first, + ?: "No existing token for ${url.host} found." + ), + placeholder = token?.first?.let { i18n.pnotr(it) }, link = Pair("Session Token:", getTokenUrl.toString()), isError = error != null, ) diff --git a/src/main/kotlin/com/coder/toolbox/util/LinkHandler.kt b/src/main/kotlin/com/coder/toolbox/util/LinkHandler.kt index 9c6342e..ff438bd 100644 --- a/src/main/kotlin/com/coder/toolbox/util/LinkHandler.kt +++ b/src/main/kotlin/com/coder/toolbox/util/LinkHandler.kt @@ -10,15 +10,20 @@ import com.coder.toolbox.sdk.v2.models.WorkspaceAgent import com.coder.toolbox.sdk.v2.models.WorkspaceStatus import com.coder.toolbox.settings.CoderSettings import com.coder.toolbox.settings.Source +import com.jetbrains.toolbox.api.core.ServiceLocator +import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import okhttp3.OkHttpClient import java.net.HttpURLConnection import java.net.URL open class LinkHandler( + serviceLocator: ServiceLocator, private val settings: CoderSettings, private val httpClient: OkHttpClient?, private val dialogUi: DialogUi, ) { + private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java) + /** * Given a set of URL parameters, prepare the CLI then return a workspace to * connect. @@ -31,7 +36,10 @@ open class LinkHandler( indicator: ((t: String) -> Unit)? = null, ): String { val deploymentURL = - parameters.url() ?: dialogUi.ask("Deployment URL", "Enter the full URL of your Coder deployment") + parameters.url() ?: dialogUi.ask( + i18n.ptrl("Deployment URL"), + i18n.ptrl("Enter the full URL of your Coder deployment") + ) if (deploymentURL.isNullOrBlank()) { throw MissingArgumentException("Query parameter \"$URL\" is missing") } @@ -222,8 +230,8 @@ open class LinkHandler( } if (!dialogUi.confirm( - "Confirm download URL", - "$comment. Would you like to proceed to $linkWithRedirect?", + i18n.ptrl("Confirm download URL"), + i18n.pnotr("$comment. Would you like to proceed to $linkWithRedirect?"), ) ) { throw IllegalArgumentException("$linkWithRedirect is not allowlisted") diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt index f2ce937..065aee1 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt @@ -1,7 +1,10 @@ package com.coder.toolbox.views import com.coder.toolbox.logger.CoderLoggerFactory +import com.jetbrains.toolbox.api.core.ServiceLocator import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon +import com.jetbrains.toolbox.api.localization.LocalizableString +import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription import com.jetbrains.toolbox.api.ui.components.UiField import com.jetbrains.toolbox.api.ui.components.UiPage @@ -19,11 +22,12 @@ import java.util.function.Consumer * to use the mouse. */ abstract class CoderPage( - title: String, + private val serviceLocator: ServiceLocator, + title: LocalizableString, showIcon: Boolean = true, ) : UiPage(title) { private val logger = CoderLoggerFactory.getLogger(javaClass) - + private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java) /** * An error to display on the page. * @@ -32,7 +36,7 @@ abstract class CoderPage( protected var errorField: ValidationErrorField? = null /** Toolbox uses this to show notifications on the page. */ - private var notifier: Consumer? = null + private var notifier: ((Throwable) -> Unit)? = null /** Let Toolbox know the fields should be updated. */ protected var listener: Consumer? = null @@ -57,17 +61,17 @@ abstract class CoderPage( fun notify(logPrefix: String, ex: Throwable) { logger.error(logPrefix, ex) // It is possible the error listener is not attached yet. - notifier?.accept(ex) ?: errorBuffer.add(ex) + notifier?.let { it(ex) } ?: errorBuffer.add(ex) } /** * Immediately notify any pending errors and store for later errors. */ - override fun setActionErrorNotifier(notifier: Consumer?) { + override fun setActionErrorNotifier(notifier: ((Throwable) -> Unit)?) { this.notifier = notifier notifier?.let { errorBuffer.forEach { - notifier.accept(it) + notifier(it) } errorBuffer.clear() } @@ -77,21 +81,23 @@ abstract class CoderPage( * Set/unset the field error and update the form. */ protected fun updateError(error: String?) { - errorField = error?.let { ValidationErrorField(error) } + errorField = error?.let { ValidationErrorField(i18n.pnotr(error)) } listener?.accept(null) // Make Toolbox get the fields again. } + + } /** * An action that simply runs the provided callback. */ class Action( - description: String, + description: LocalizableString, closesPage: Boolean = false, enabled: () -> Boolean = { true }, private val actionBlock: () -> Unit, ) : RunnableActionDescription { - override val label: String = description + override val label: LocalizableString = description override val shouldClosePage: Boolean = closesPage override val isEnabled: Boolean = enabled() override fun run() { diff --git a/src/main/resources/localization/defaultMessages.po b/src/main/resources/localization/defaultMessages.po index 398a650..628f295 100644 --- a/src/main/resources/localization/defaultMessages.po +++ b/src/main/resources/localization/defaultMessages.po @@ -15,3 +15,63 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" + +msgid "Yes" +msgstr "" + +msgid "OK" +msgstr "" + +msgid "No" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Cancel" +msgstr "" + +msgid "Delete running workspace?" +msgstr "" + +msgid "Delete workspace?" +msgstr "" + +msgid "Workspace will be closed and all the information in this workspace will be lost, including all files, unsaved changes and historical." +msgstr "" + +msgid "All the information in this workspace will be lost, including all files, unsaved changes and historical." +msgstr "" + +msgid "Session Token" +msgstr "" + +msgid "Deployment URL" +msgstr "" + +msgid "Enter the full URL of your Coder deployment" +msgstr "" + +msgid "Confirm download URL" +msgstr "" + +msgid "Open web terminal" +msgstr "" + +msgid "Open in dashboard" +msgstr "" + +msgid "View template" +msgstr "" + +msgid "Start" +msgstr "" + +msgid "Stop" +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Settings" +msgstr "" \ No newline at end of file From 7cfd6f5a6b0b8d58b466cab1813988be2381eb8a Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 5 Mar 2025 21:19:48 +0200 Subject: [PATCH 04/14] impl: support for Toolbox 2.6.0.38881 (4) - fix compiler errors for the Settings page - associated strings converted to LocalizedString instances - observable properties are now moved to Kotlin's StateFlow --- .../com/coder/toolbox/CoderRemoteProvider.kt | 3 +- .../com/coder/toolbox/views/CoderPage.kt | 2 +- .../coder/toolbox/views/CoderSettingsPage.kt | 92 +++++++++++-------- .../resources/localization/defaultMessages.po | 43 ++++++++- 4 files changed, 99 insertions(+), 41 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index 36f9f22..bcfd132 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -65,7 +65,8 @@ class CoderRemoteProvider( private val settingsService = CoderSettingsService(settingsStore) private val settings: CoderSettings = CoderSettings(settingsService) private val secrets: CoderSecretsService = CoderSecretsService(secretsStore) - private val settingsPage: CoderSettingsPage = CoderSettingsPage(settingsService) + private val settingsPage: CoderSettingsPage = + CoderSettingsPage(serviceLocator, settingsService, i18n.ptrl("Coder Settings")) private val dialogUi = DialogUi(serviceLocator, settings) private val linkHandler = LinkHandler(serviceLocator, settings, httpClient, dialogUi) diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt index 065aee1..6a3ee99 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt @@ -22,7 +22,7 @@ import java.util.function.Consumer * to use the mouse. */ abstract class CoderPage( - private val serviceLocator: ServiceLocator, + serviceLocator: ServiceLocator, title: LocalizableString, showIcon: Boolean = true, ) : UiPage(title) { diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt index a4d7f19..da5387a 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt @@ -1,11 +1,16 @@ package com.coder.toolbox.views import com.coder.toolbox.services.CoderSettingsService +import com.jetbrains.toolbox.api.core.ServiceLocator +import com.jetbrains.toolbox.api.localization.LocalizableString +import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription import com.jetbrains.toolbox.api.ui.components.CheckboxField import com.jetbrains.toolbox.api.ui.components.TextField import com.jetbrains.toolbox.api.ui.components.TextType import com.jetbrains.toolbox.api.ui.components.UiField +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow /** * A page for modifying Coder settings. @@ -14,49 +19,60 @@ import com.jetbrains.toolbox.api.ui.components.UiField * 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 settings: CoderSettingsService) : CoderPage("Coder Settings", false) { +class CoderSettingsPage( + serviceLocator: ServiceLocator, + private val settings: CoderSettingsService, + title: LocalizableString, +) : CoderPage(serviceLocator, title, false) { + private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java) + // TODO: Copy over the descriptions, holding until I can test this page. - private val binarySourceField = TextField("Binary source", settings.binarySource, TextType.General) - private val binaryDirectoryField = TextField("Binary directory", settings.binaryDirectory, TextType.General) - private val dataDirectoryField = TextField("Data directory", settings.dataDirectory, TextType.General) - private val enableDownloadsField = CheckboxField(settings.enableDownloads, "Enable downloads") + private val binarySourceField = TextField(i18n.ptrl("Binary source"), settings.binarySource, TextType.General) + private val binaryDirectoryField = + TextField(i18n.ptrl("Binary directory"), settings.binaryDirectory, TextType.General) + private val dataDirectoryField = TextField(i18n.ptrl("Data directory"), settings.dataDirectory, TextType.General) + private val enableDownloadsField = CheckboxField(settings.enableDownloads, i18n.ptrl("Enable downloads")) private val enableBinaryDirectoryFallbackField = - CheckboxField(settings.enableBinaryDirectoryFallback, "Enable binary directory fallback") - private val headerCommandField = TextField("Header command", settings.headerCommand, TextType.General) - private val tlsCertPathField = TextField("TLS cert path", settings.tlsCertPath, TextType.General) - private val tlsKeyPathField = TextField("TLS key path", settings.tlsKeyPath, TextType.General) - private val tlsCAPathField = TextField("TLS CA path", settings.tlsCAPath, TextType.General) + CheckboxField(settings.enableBinaryDirectoryFallback, i18n.ptrl("Enable binary directory fallback")) + private val headerCommandField = TextField(i18n.ptrl("Header command"), settings.headerCommand, TextType.General) + private val tlsCertPathField = TextField(i18n.ptrl("TLS cert path"), settings.tlsCertPath, TextType.General) + private val tlsKeyPathField = TextField(i18n.ptrl("TLS key path"), settings.tlsKeyPath, TextType.General) + private val tlsCAPathField = TextField(i18n.ptrl("TLS CA path"), settings.tlsCAPath, TextType.General) private val tlsAlternateHostnameField = - TextField("TLS alternate hostname", settings.tlsAlternateHostname, TextType.General) - private val disableAutostartField = CheckboxField(settings.disableAutostart, "Disable autostart") + TextField(i18n.ptrl("TLS alternate hostname"), settings.tlsAlternateHostname, TextType.General) + private val disableAutostartField = CheckboxField(settings.disableAutostart, i18n.ptrl("Disable autostart")) - override val fields: MutableList = mutableListOf( - binarySourceField, - enableDownloadsField, - binaryDirectoryField, - enableBinaryDirectoryFallbackField, - dataDirectoryField, - headerCommandField, - tlsCertPathField, - tlsKeyPathField, - tlsCAPathField, - tlsAlternateHostnameField, - disableAutostartField, + override val fields: StateFlow> = MutableStateFlow( + listOf( + binarySourceField, + enableDownloadsField, + binaryDirectoryField, + enableBinaryDirectoryFallbackField, + dataDirectoryField, + headerCommandField, + tlsCertPathField, + tlsKeyPathField, + tlsCAPathField, + tlsAlternateHostnameField, + disableAutostartField + ) ) - override val actionButtons: MutableList = mutableListOf( - Action("Save", closesPage = true) { - settings.binarySource = binarySourceField.text.value - settings.binaryDirectory = binaryDirectoryField.text.value - settings.dataDirectory = dataDirectoryField.text.value - settings.enableDownloads = enableDownloadsField.checked.value - settings.enableBinaryDirectoryFallback = enableBinaryDirectoryFallbackField.checked.value - settings.headerCommand = headerCommandField.text.value - settings.tlsCertPath = tlsCertPathField.text.value - settings.tlsKeyPath = tlsKeyPathField.text.value - settings.tlsCAPath = tlsCAPathField.text.value - settings.tlsAlternateHostname = tlsAlternateHostnameField.text.value - settings.disableAutostart = disableAutostartField.checked.value - }, + override val actionButtons: StateFlow> = MutableStateFlow( + listOf( + Action(i18n.ptrl("Save"), closesPage = true) { + settings.binarySource = binarySourceField.textState.value + settings.binaryDirectory = binaryDirectoryField.textState.value + settings.dataDirectory = dataDirectoryField.textState.value + settings.enableDownloads = enableDownloadsField.checkedState.value + settings.enableBinaryDirectoryFallback = enableBinaryDirectoryFallbackField.checkedState.value + settings.headerCommand = headerCommandField.textState.value + settings.tlsCertPath = tlsCertPathField.textState.value + settings.tlsKeyPath = tlsKeyPathField.textState.value + settings.tlsCAPath = tlsCAPathField.textState.value + settings.tlsAlternateHostname = tlsAlternateHostnameField.textState.value + settings.disableAutostart = disableAutostartField.checkedState.value + }, + ) ) } diff --git a/src/main/resources/localization/defaultMessages.po b/src/main/resources/localization/defaultMessages.po index 628f295..451d4d6 100644 --- a/src/main/resources/localization/defaultMessages.po +++ b/src/main/resources/localization/defaultMessages.po @@ -25,6 +25,9 @@ msgstr "" msgid "No" msgstr "" +msgid "Save" +msgstr "" + msgid "Delete" msgstr "" @@ -74,4 +77,42 @@ msgid "Update" msgstr "" msgid "Settings" -msgstr "" \ No newline at end of file +msgstr "" + +msgid "Coder Settings" +msgstr "" + +msgid "Binary source" +msgstr "" + +msgid "Binary directory" +msgstr "" + +msgid "Data directory" +msgstr "" + +msgid "Enable downloads" +msgstr "" + +msgid "Enable binary directory fallback" +msgstr "" + +msgid "Header command" +msgstr "" + +msgid "TLS cert path" +msgstr "" + +msgid "TLS key path" +msgstr "" + +msgid "TLS CA path" +msgstr "" + +msgid "TLS alternate hostname" +msgstr "" + +msgid "Disable autostart" +msgstr "" + + From 4e5ef77d7f080d74a0b5e277acaf5c489e1982f4 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 5 Mar 2025 21:36:49 +0200 Subject: [PATCH 05/14] impl: support for Toolbox 2.6.0.38881 (5) - fix compiler errors for Coder main page - associated strings converted to LocalizedString instances - observable properties are now moved to Kotlin's StateFlow --- .../com/coder/toolbox/CoderRemoteProvider.kt | 3 +- .../com/coder/toolbox/views/ConnectPage.kt | 46 ++++++++++++------- .../resources/localization/defaultMessages.po | 10 ++++ 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index bcfd132..013b84e 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -307,11 +307,12 @@ class CoderRemoteProvider( * Create a connect page that starts polling and resets the UI on success. */ private fun createConnectPage(deploymentURL: URL, token: String?): ConnectPage = ConnectPage( + serviceLocator, deploymentURL, token, settings, httpClient, - coroutineScope, + i18n.ptrl("Connecting to Coder"), ::goToEnvironmentsPage, ) { client, cli -> // Store the URL and token for use next time. diff --git a/src/main/kotlin/com/coder/toolbox/views/ConnectPage.kt b/src/main/kotlin/com/coder/toolbox/views/ConnectPage.kt index 5270578..20b757d 100644 --- a/src/main/kotlin/com/coder/toolbox/views/ConnectPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/ConnectPage.kt @@ -6,11 +6,16 @@ import com.coder.toolbox.plugin.PluginManager import com.coder.toolbox.sdk.CoderRestClient import com.coder.toolbox.settings.CoderSettings import com.coder.toolbox.util.humanizeConnectionError +import com.jetbrains.toolbox.api.core.ServiceLocator +import com.jetbrains.toolbox.api.localization.LocalizableString +import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription import com.jetbrains.toolbox.api.ui.components.LabelField import com.jetbrains.toolbox.api.ui.components.UiField import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import okhttp3.OkHttpClient import java.net.URL @@ -19,22 +24,26 @@ import java.net.URL * A page that connects a REST client and cli to Coder. */ class ConnectPage( + private val serviceLocator: ServiceLocator, private val url: URL, private val token: String?, private val settings: CoderSettings, private val httpClient: OkHttpClient, - private val coroutineScope: CoroutineScope, + title: LocalizableString, private val onCancel: () -> Unit, private val onConnect: ( client: CoderRestClient, cli: CoderCLIManager, ) -> Unit, -) : CoderPage("Connecting to Coder") { +) : CoderPage(serviceLocator, title) { + private val coroutineScope = serviceLocator.getService(CoroutineScope::class.java) + private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java) + private var signInJob: Job? = null - private var statusField = LabelField("Connecting to ${url.host}...") + private var statusField = LabelField(i18n.pnotr("Connecting to ${url.host}...")) - override val description: String = "Please wait while we configure Toolbox for ${url.host}." + override val description: LocalizableString = i18n.pnotr("Please wait while we configure Toolbox for ${url.host}.") init { connect() @@ -45,23 +54,26 @@ class ConnectPage( * * TODO@JB: This looks kinda sparse. A centered spinner would be welcome. */ - override val fields: MutableList = listOfNotNull( + override val fields: StateFlow> = MutableStateFlow( + listOfNotNull( statusField, - errorField, - ).toMutableList() + errorField + ) + ) /** * Show a retry button on error. */ - override val actionButtons: MutableList = listOfNotNull( - if (errorField != null) Action("Retry", closesPage = false) { retry() } else null, - if (errorField != null) Action("Cancel", closesPage = false) { onCancel() } else null, - ).toMutableList() + override val actionButtons: StateFlow> = MutableStateFlow( + listOfNotNull( + if (errorField != null) Action(i18n.ptrl("Retry"), closesPage = false) { retry() } else null, + if (errorField != null) Action(i18n.ptrl("Cancel"), closesPage = false) { onCancel() } else null, + )) /** * Update the status and error fields then refresh. */ - private fun updateStatus(newStatus: String, error: String?) { + private fun updateStatus(newStatus: LocalizableString, error: String?) { statusField = LabelField(newStatus) updateError(error) // Will refresh. } @@ -70,7 +82,7 @@ class ConnectPage( * Try connecting again after an error. */ private fun retry() { - updateStatus("Connecting to ${url.host}...", null) + updateStatus(i18n.pnotr("Connecting to ${url.host}..."), null) connect() } @@ -92,13 +104,13 @@ class ConnectPage( httpClient ) client.authenticate() - updateStatus("Checking Coder binary...", error = null) + updateStatus(i18n.ptrl("Checking Coder binary..."), error = null) val cli = ensureCLI(client.url, client.buildVersion, settings) { status -> - updateStatus(status, error = null) + updateStatus(i18n.pnotr(status), error = null) } // We only need to log in if we are using token-based auth. if (client.token != null) { - updateStatus("Configuring CLI...", error = null) + updateStatus(i18n.ptrl("Configuring CLI..."), error = null) cli.login(client.token) } onConnect(client, cli) @@ -106,7 +118,7 @@ class ConnectPage( } catch (ex: Exception) { val msg = humanizeConnectionError(url, settings.requireTokenAuth, ex) notify("Failed to configure ${url.host}", ex) - updateStatus("Failed to configure ${url.host}", msg) + updateStatus(i18n.pnotr("Failed to configure ${url.host}"), msg) } } } diff --git a/src/main/resources/localization/defaultMessages.po b/src/main/resources/localization/defaultMessages.po index 451d4d6..8bacd98 100644 --- a/src/main/resources/localization/defaultMessages.po +++ b/src/main/resources/localization/defaultMessages.po @@ -115,4 +115,14 @@ msgstr "" msgid "Disable autostart" msgstr "" +msgid "Connecting to Coder" +msgstr "" + +msgid "Checking Coder binary..." +msgstr "" +msgid "Configuring CLI..." +msgstr "" + +msgid "Retry" +msgstr "" From 11be558d96a02fb0a88a6b2ac6de949ad0b61b8b Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 5 Mar 2025 21:59:27 +0200 Subject: [PATCH 06/14] impl: support for Toolbox 2.6.0.38881 (6) - fix compiler errors for the intro page - associated strings converted to LocalizedString instances - observable properties are now moved to Kotlin's StateFlow --- src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt | 3 ++- .../kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index 013b84e..ad85aff 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -220,7 +220,8 @@ class CoderRemoteProvider( * Just displays the deployment URL at the moment, but we could use this as * a form for creating new environments. */ - override fun getNewEnvironmentUiPage(): UiPage = NewEnvironmentPage(getDeploymentURL()?.first) + override fun getNewEnvironmentUiPage(): UiPage = + NewEnvironmentPage(serviceLocator, i18n.pnotr(getDeploymentURL()?.first ?: "")) /** * We always show a list of environments. diff --git a/src/main/kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt b/src/main/kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt index efe4279..de0cf93 100644 --- a/src/main/kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt @@ -1,6 +1,10 @@ package com.coder.toolbox.views +import com.jetbrains.toolbox.api.core.ServiceLocator +import com.jetbrains.toolbox.api.localization.LocalizableString import com.jetbrains.toolbox.api.ui.components.UiField +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow /** @@ -10,6 +14,7 @@ import com.jetbrains.toolbox.api.ui.components.UiField * For now we just use this to display the deployment URL since we do not * support creating environments from the plugin. */ -class NewEnvironmentPage(private val deploymentURL: String?) : CoderPage(deploymentURL ?: "") { - override val fields: MutableList = mutableListOf() +class NewEnvironmentPage(serviceLocator: ServiceLocator, deploymentURL: LocalizableString) : + CoderPage(serviceLocator, deploymentURL) { + override val fields: StateFlow> = MutableStateFlow(emptyList()) } From 9a2c44ac039285bcee3f66ed192d9d1623b976f6 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Thu, 6 Mar 2025 00:46:09 +0200 Subject: [PATCH 07/14] impl: support for Toolbox 2.6.0.38881 (7) - fix compiler errors for the Sign-In page - associated strings converted to LocalizedString instances - observable properties are now moved to Kotlin's StateFlow --- .../com/coder/toolbox/CoderRemoteProvider.kt | 10 ++++-- .../com/coder/toolbox/views/SignInPage.kt | 28 +++++++++++----- .../com/coder/toolbox/views/TokenPage.kt | 33 ++++++++++++++----- .../resources/localization/defaultMessages.po | 18 ++++++++++ 4 files changed, 70 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index ad85aff..cf61c97 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -286,9 +286,15 @@ class CoderRemoteProvider( firstRun = false // Login flow. - val signInPage = SignInPage(getDeploymentURL()) { deploymentURL -> + val signInPage = + SignInPage(serviceLocator, i18n.ptrl("Sign In to Coder"), getDeploymentURL()) { deploymentURL -> ui.showUiPage( - TokenPage(deploymentURL, getToken(deploymentURL)) { selectedToken -> + TokenPage( + serviceLocator, + i18n.ptrl("Enter your token"), + deploymentURL, + getToken(deploymentURL) + ) { selectedToken -> ui.showUiPage(createConnectPage(deploymentURL, selectedToken)) }, ) diff --git a/src/main/kotlin/com/coder/toolbox/views/SignInPage.kt b/src/main/kotlin/com/coder/toolbox/views/SignInPage.kt index 2fdbf60..875d0c6 100644 --- a/src/main/kotlin/com/coder/toolbox/views/SignInPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/SignInPage.kt @@ -1,11 +1,16 @@ package com.coder.toolbox.views import com.coder.toolbox.settings.Source +import com.jetbrains.toolbox.api.core.ServiceLocator +import com.jetbrains.toolbox.api.localization.LocalizableString +import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription import com.jetbrains.toolbox.api.ui.components.LabelField import com.jetbrains.toolbox.api.ui.components.TextField import com.jetbrains.toolbox.api.ui.components.TextType import com.jetbrains.toolbox.api.ui.components.UiField +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import java.net.URL /** @@ -15,10 +20,13 @@ import java.net.URL * enter their own. */ class SignInPage( + serviceLocator: ServiceLocator, + title: LocalizableString, private val deploymentURL: Pair?, private val onSignIn: (deploymentURL: URL) -> Unit, -) : CoderPage("Sign In to Coder") { - private val urlField = TextField("Deployment URL", deploymentURL?.first ?: "", TextType.General) +) : CoderPage(serviceLocator, title) { + private val i18n: LocalizableStringFactory = serviceLocator.getService(LocalizableStringFactory::class.java) + private val urlField = TextField(i18n.ptrl("Deployment URL"), deploymentURL?.first ?: "", TextType.General) /** * Fields for this page, displayed in order. @@ -26,24 +34,28 @@ class SignInPage( * TODO@JB: Fields are reset when you navigate back. * Ideally they remember what the user entered. */ - override val fields: MutableList = listOfNotNull( + override val fields: StateFlow> = MutableStateFlow( + listOfNotNull( urlField, - deploymentURL?.let { LabelField(deploymentURL.second.description("URL")) }, + deploymentURL?.let { LabelField(i18n.pnotr(deploymentURL.second.description("URL"))) }, errorField, - ).toMutableList() + ) + ) /** * Buttons displayed at the bottom of the page. */ - override val actionButtons: MutableList = mutableListOf( - Action("Sign In", closesPage = false) { submit() }, + override val actionButtons: StateFlow> = MutableStateFlow( + listOf( + Action(i18n.ptrl("Sign In"), closesPage = false) { submit() }, + ) ) /** * Call onSignIn with the URL, or error if blank. */ private fun submit() { - val urlRaw = urlField.text.value + val urlRaw = urlField.textState.value // Ensure the URL can be parsed. try { if (urlRaw.isBlank()) { diff --git a/src/main/kotlin/com/coder/toolbox/views/TokenPage.kt b/src/main/kotlin/com/coder/toolbox/views/TokenPage.kt index d0da1fc..edbc9c0 100644 --- a/src/main/kotlin/com/coder/toolbox/views/TokenPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/TokenPage.kt @@ -2,12 +2,17 @@ package com.coder.toolbox.views import com.coder.toolbox.settings.Source import com.coder.toolbox.util.withPath +import com.jetbrains.toolbox.api.core.ServiceLocator +import com.jetbrains.toolbox.api.localization.LocalizableString +import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription import com.jetbrains.toolbox.api.ui.components.LabelField import com.jetbrains.toolbox.api.ui.components.LinkField import com.jetbrains.toolbox.api.ui.components.TextField import com.jetbrains.toolbox.api.ui.components.TextType import com.jetbrains.toolbox.api.ui.components.UiField +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import java.net.URL /** @@ -17,11 +22,15 @@ import java.net.URL * enter their own. */ class TokenPage( + serviceLocator: ServiceLocator, + title: LocalizableString, deploymentURL: URL, token: Pair?, private val onToken: ((token: String) -> Unit), -) : CoderPage("Enter your token") { - private val tokenField = TextField("Token", token?.first ?: "", TextType.General) +) : CoderPage(serviceLocator, title) { + private val i18n: LocalizableStringFactory = serviceLocator.getService(LocalizableStringFactory::class.java) + + private val tokenField = TextField(i18n.ptrl("Token"), token?.first ?: "", TextType.General) /** * Fields for this page, displayed in order. @@ -29,22 +38,28 @@ class TokenPage( * TODO@JB: Fields are reset when you navigate back. * Ideally they remember what the user entered. */ - override val fields: MutableList = listOfNotNull( + override val fields: StateFlow> = MutableStateFlow( + listOfNotNull( tokenField, LabelField( - token?.second?.description("token") - ?: "No existing token for ${deploymentURL.host} found.", + i18n.pnotr( + token?.second?.description("token") + ?: "No existing token for ${deploymentURL.host} found." + ), ), // TODO@JB: The link text displays twice. - LinkField("Get a token", deploymentURL.withPath("/login?redirect=%2Fcli-auth").toString()), + LinkField(i18n.ptrl("Get a token"), deploymentURL.withPath("/login?redirect=%2Fcli-auth").toString()), errorField, - ).toMutableList() + ) + ) /** * Buttons displayed at the bottom of the page. */ - override val actionButtons: MutableList = mutableListOf( - Action("Connect", closesPage = false) { submit(tokenField.text.value) }, + override val actionButtons: StateFlow> = MutableStateFlow( + listOf( + Action(i18n.ptrl("Connect"), closesPage = false) { submit(tokenField.textState.value) }, + ) ) /** diff --git a/src/main/resources/localization/defaultMessages.po b/src/main/resources/localization/defaultMessages.po index 8bacd98..cba43eb 100644 --- a/src/main/resources/localization/defaultMessages.po +++ b/src/main/resources/localization/defaultMessages.po @@ -126,3 +126,21 @@ msgstr "" msgid "Retry" msgstr "" + +msgid "Sign In" +msgstr "" + +msgid "Sign In to Coder" +msgstr "" + +msgid "Enter your token" +msgstr "" + +msgid "Token" +msgstr "" + +msgid "Get a token" +msgstr "" + +msgid "Connect" +msgstr "" \ No newline at end of file From 09c99d13604996d944a36327c8d4c8d9dfdb08be Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Thu, 6 Mar 2025 00:50:05 +0200 Subject: [PATCH 08/14] build: fix build tasks - the jars were not included in the Toolbox install location nor in the zip file - the plugin needs extension.json (which is now autogenerated but initially it was only part of the zip file) so that later it can read the version. With this patch - the json is also included in the jar file --- build.gradle.kts | 56 +++++++++++++++++++++--------------------------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 29e7019..7cfcfdd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,5 @@ -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder import com.github.jk1.license.filter.ExcludeTransitiveDependenciesFilter import com.github.jk1.license.render.JsonReportRenderer import com.jetbrains.plugin.structure.toolbox.ToolboxMeta @@ -66,6 +67,7 @@ dependencies { val extension = ExtensionJson( id = properties("group"), + version = properties("version"), meta = ExtensionJsonMeta( name = "Coder Toolbox", @@ -106,36 +108,30 @@ tasks.test { useJUnitPlatform() } + tasks.jar { archiveBaseName.set(extension.id) dependsOn(extensionJson) -} - -val assemblePlugin by tasks.registering(Jar::class) { - archiveBaseName.set(extension.id) - from(sourceSets.main.get().output) + from(extensionJson.get().outputs) } val copyPlugin by tasks.creating(Sync::class.java) { - dependsOn(tasks.assemble) -// fromCompileDependencies() - from(tasks.jar) - - from(extensionJsonFile) + dependsOn(tasks.jar) + dependsOn(tasks.getByName("generateLicenseReport")) - from("src/main/resources") { - include("dependencies.json") - include("icon.svg") - } + fromCompileDependencies() into(getPluginInstallDir()) } fun CopySpec.fromCompileDependencies() { - from(assemblePlugin.get().outputs.files) + from(tasks.jar) + from(extensionJson.get().outputs.files) from("src/main/resources") { - include("extension.json") include("dependencies.json") + } + from("src/main/resources") { include("icon.svg") + rename("icon.svg", "pluginIcon.svg") } // Copy dependencies, excluding those provided by Toolbox. @@ -148,6 +144,7 @@ fun CopySpec.fromCompileDependencies() { "core-api", "ui-api", "annotations", + "localization-api" ).any { file.name.contains(it) } } }, @@ -155,21 +152,12 @@ fun CopySpec.fromCompileDependencies() { } val pluginZip by tasks.creating(Zip::class) { - dependsOn(tasks.assemble) + archiveBaseName.set(properties("name")) + dependsOn(tasks.jar) dependsOn(tasks.getByName("generateLicenseReport")) -// fromCompileDependencies() -// into(pluginId) - from(tasks.assemble.get().outputs.files) - from(extensionJsonFile) - from("src/main/resources") { - include("dependencies.json") - } - from("src/main/resources") { - include("icon.svg") - rename("icon.svg", "pluginIcon.svg") - } - archiveBaseName.set(extension.id) + fromCompileDependencies() + into(extension.id) // folder like com.coder.toolbox } tasks.register("cleanAll", Delete::class.java) { @@ -247,8 +235,12 @@ fun generateExtensionJson(extensionJson: ExtensionJson, destinationFile: Path) { url = extensionJson.meta.url, ) ) - val extensionJson = jacksonObjectMapper().writeValueAsString(descriptor) destinationFile.parent.createDirectories() - destinationFile.writeText(extensionJson) + destinationFile.writeText( + jacksonMapperBuilder() + .enable(SerializationFeature.INDENT_OUTPUT) + .build() + .writeValueAsString(descriptor) + ) } // endregion \ No newline at end of file From cce41b06462dc75eb2ed7f951f293a389e523fd5 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Thu, 6 Mar 2025 20:08:00 +0200 Subject: [PATCH 09/14] fix: workspaces were not properly loaded - proper usage of coroutine's stateflow to update values - also fixed issues were state transition were not properly rendered --- .../coder/toolbox/CoderRemoteEnvironment.kt | 29 ++++++++++--------- .../com/coder/toolbox/CoderRemoteProvider.kt | 7 +++-- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index ff46f15..2ac78ef 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout @@ -41,16 +42,16 @@ class CoderRemoteEnvironment( private var agent: WorkspaceAgent, private var cs: CoroutineScope, ) : RemoteProviderEnvironment("${workspace.name}.${agent.name}") { - private var status = WorkspaceAndAgentStatus.from(workspace, agent) + private var wsRawStatus = WorkspaceAndAgentStatus.from(workspace, agent) private val ui: ToolboxUi = serviceLocator.getService(ToolboxUi::class.java) private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java) override var name: String = "${workspace.name}.${agent.name}" - override val state: StateFlow - get() = TODO("Not yet implemented") - override val description: StateFlow - get() = TODO("Not yet implemented") + override val state: MutableStateFlow = + MutableStateFlow(wsRawStatus.toRemoteEnvironmentState(serviceLocator)) + override val description: MutableStateFlow = + MutableStateFlow(EnvironmentDescription.General(i18n.pnotr(workspace.templateName))) override val actionsList: StateFlow> = MutableStateFlow( listOf( @@ -76,12 +77,12 @@ class CoderRemoteEnvironment( } } }, - Action(i18n.ptrl("Start"), enabled = { status.canStart() }) { + Action(i18n.ptrl("Start"), enabled = { wsRawStatus.canStart() }) { val build = client.startWorkspace(workspace) workspace = workspace.copy(latestBuild = build) update(workspace, agent) }, - Action(i18n.ptrl("Stop"), enabled = { status.canStop() }) { + Action(i18n.ptrl("Stop"), enabled = { wsRawStatus.canStop() }) { val build = client.stopWorkspace(workspace) workspace = workspace.copy(latestBuild = build) update(workspace, agent) @@ -99,11 +100,11 @@ class CoderRemoteEnvironment( fun update(workspace: Workspace, agent: WorkspaceAgent) { this.workspace = workspace this.agent = agent - val newStatus = WorkspaceAndAgentStatus.from(workspace, agent) - if (newStatus != status) { - status = newStatus - val state = status.toRemoteEnvironmentState(serviceLocator) -// listenerSet.forEach { it.consume(state) } + wsRawStatus = WorkspaceAndAgentStatus.from(workspace, agent) + cs.launch { + state.update { + wsRawStatus.toRemoteEnvironmentState(serviceLocator) + } } } @@ -139,7 +140,7 @@ class CoderRemoteEnvironment( cs.launch { // TODO info and cancel pop-ups only appear on the main page where all environments are listed. // However, #showSnackbar works on other pages. Until JetBrains fixes this issue we are going to use the snackbar - val shouldDelete = if (status.canStop()) { + val shouldDelete = if (wsRawStatus.canStop()) { ui.showOkCancelPopup( i18n.ptrl("Delete running workspace?"), i18n.ptrl("Workspace will be closed and all the information in this workspace will be lost, including all files, unsaved changes and historical."), @@ -161,7 +162,7 @@ class CoderRemoteEnvironment( withTimeout(5.minutes) { var workspaceStillExists = true while (cs.isActive && workspaceStillExists) { - if (status == WorkspaceAndAgentStatus.DELETING || status == WorkspaceAndAgentStatus.DELETED) { + if (wsRawStatus == WorkspaceAndAgentStatus.DELETING || wsRawStatus == WorkspaceAndAgentStatus.DELETED) { workspaceStillExists = false serviceLocator.getService(EnvironmentUiPageManager::class.java) .showPluginEnvironmentsPage() diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index cf61c97..1d69249 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -35,6 +35,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import okhttp3.OkHttpClient @@ -82,7 +83,7 @@ class CoderRemoteProvider( private var firstRun = true override val environments: MutableStateFlow>> = MutableStateFlow( - LoadableState.Loading + LoadableState.Value(emptyList()) ) /** @@ -134,7 +135,9 @@ class CoderRemoteProvider( cli.configSsh(newEnvironments.map { it.name }.toSet()) } - environments.value = LoadableState.Value(resolvedEnvironments.toList()) + environments.update { + LoadableState.Value(resolvedEnvironments.toList()) + } lastEnvironments = resolvedEnvironments } catch (_: CancellationException) { From d75d0c73d7e782b1a9e424deecb66dc1c7a3ef28 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Thu, 6 Mar 2025 21:42:10 +0200 Subject: [PATCH 10/14] refactor: centralize all toolbox services in a context - service dependencies were resolved all over the place making refactoring harder - it promoted implicit, hidden dependencies - and also introduced tighter coupling between components. - in some cases we had to provide some i18n strings from upstream because the localization service was not available in the constructor. With this patch we resolve all the needed services during plugin load, wrap them in a context and inject the context via the constructor. It is now easier to refactor and the number of constructor parameters has been reduced. --- .../coder/toolbox/CoderRemoteEnvironment.kt | 74 ++++++++--------- .../com/coder/toolbox/CoderRemoteProvider.kt | 81 +++++++------------ .../com/coder/toolbox/CoderToolboxContext.kt | 21 +++++ .../coder/toolbox/CoderToolboxExtension.kt | 18 ++++- .../toolbox/models/WorkspaceAndAgentStatus.kt | 23 +++--- .../kotlin/com/coder/toolbox/util/Dialogs.kt | 31 +++---- .../com/coder/toolbox/util/LinkHandler.kt | 15 ++-- .../com/coder/toolbox/views/CoderPage.kt | 14 +--- .../coder/toolbox/views/CoderSettingsPage.kt | 38 +++++---- .../com/coder/toolbox/views/ConnectPage.kt | 37 ++++----- .../coder/toolbox/views/NewEnvironmentPage.kt | 6 +- .../com/coder/toolbox/views/SignInPage.kt | 20 ++--- .../com/coder/toolbox/views/TokenPage.kt | 36 ++++----- 13 files changed, 196 insertions(+), 218 deletions(-) create mode 100644 src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index 2ac78ef..5adcf93 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -9,17 +9,12 @@ import com.coder.toolbox.sdk.v2.models.WorkspaceAgent import com.coder.toolbox.util.withPath import com.coder.toolbox.views.Action import com.coder.toolbox.views.EnvironmentView -import com.jetbrains.toolbox.api.core.ServiceLocator -import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.remoteDev.EnvironmentVisibilityState import com.jetbrains.toolbox.api.remoteDev.RemoteProviderEnvironment import com.jetbrains.toolbox.api.remoteDev.environments.EnvironmentContentsView import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentDescription import com.jetbrains.toolbox.api.remoteDev.states.RemoteEnvironmentState -import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager -import com.jetbrains.toolbox.api.ui.ToolboxUi import com.jetbrains.toolbox.api.ui.actions.ActionDescription -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -36,58 +31,54 @@ import kotlin.time.Duration.Companion.seconds * Used in the environment list view. */ class CoderRemoteEnvironment( - private val serviceLocator: ServiceLocator, + private val context: CoderToolboxContext, private val client: CoderRestClient, private var workspace: Workspace, private var agent: WorkspaceAgent, - private var cs: CoroutineScope, ) : RemoteProviderEnvironment("${workspace.name}.${agent.name}") { private var wsRawStatus = WorkspaceAndAgentStatus.from(workspace, agent) - private val ui: ToolboxUi = serviceLocator.getService(ToolboxUi::class.java) - private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java) - override var name: String = "${workspace.name}.${agent.name}" override val state: MutableStateFlow = - MutableStateFlow(wsRawStatus.toRemoteEnvironmentState(serviceLocator)) + MutableStateFlow(wsRawStatus.toRemoteEnvironmentState(context)) override val description: MutableStateFlow = - MutableStateFlow(EnvironmentDescription.General(i18n.pnotr(workspace.templateName))) + MutableStateFlow(EnvironmentDescription.General(context.i18n.pnotr(workspace.templateName))) override val actionsList: StateFlow> = MutableStateFlow( listOf( - Action(i18n.ptrl("Open web terminal")) { - cs.launch { + Action(context.i18n.ptrl("Open web terminal")) { + context.cs.launch { BrowserUtil.browse(client.url.withPath("/${workspace.ownerName}/$name/terminal").toString()) { - ui.showErrorInfoPopup(it) + context.ui.showErrorInfoPopup(it) } } }, - Action(i18n.ptrl("Open in dashboard")) { - cs.launch { + Action(context.i18n.ptrl("Open in dashboard")) { + context.cs.launch { BrowserUtil.browse(client.url.withPath("/@${workspace.ownerName}/${workspace.name}").toString()) { - ui.showErrorInfoPopup(it) + context.ui.showErrorInfoPopup(it) } } }, - Action(i18n.ptrl("View template")) { - cs.launch { + Action(context.i18n.ptrl("View template")) { + context.cs.launch { BrowserUtil.browse(client.url.withPath("/templates/${workspace.templateName}").toString()) { - ui.showErrorInfoPopup(it) + context.ui.showErrorInfoPopup(it) } } }, - Action(i18n.ptrl("Start"), enabled = { wsRawStatus.canStart() }) { + Action(context.i18n.ptrl("Start"), enabled = { wsRawStatus.canStart() }) { val build = client.startWorkspace(workspace) workspace = workspace.copy(latestBuild = build) update(workspace, agent) }, - Action(i18n.ptrl("Stop"), enabled = { wsRawStatus.canStop() }) { + Action(context.i18n.ptrl("Stop"), enabled = { wsRawStatus.canStop() }) { val build = client.stopWorkspace(workspace) workspace = workspace.copy(latestBuild = build) update(workspace, agent) }, - Action(i18n.ptrl("Update"), enabled = { workspace.outdated }) { + Action(context.i18n.ptrl("Update"), enabled = { workspace.outdated }) { val build = client.updateWorkspace(workspace) workspace = workspace.copy(latestBuild = build) update(workspace, agent) @@ -101,9 +92,9 @@ class CoderRemoteEnvironment( this.workspace = workspace this.agent = agent wsRawStatus = WorkspaceAndAgentStatus.from(workspace, agent) - cs.launch { + context.cs.launch { state.update { - wsRawStatus.toRemoteEnvironmentState(serviceLocator) + wsRawStatus.toRemoteEnvironmentState(context) } } } @@ -137,35 +128,34 @@ class CoderRemoteEnvironment( // } override fun onDelete() { - cs.launch { + context.cs.launch { // TODO info and cancel pop-ups only appear on the main page where all environments are listed. // However, #showSnackbar works on other pages. Until JetBrains fixes this issue we are going to use the snackbar val shouldDelete = if (wsRawStatus.canStop()) { - ui.showOkCancelPopup( - i18n.ptrl("Delete running workspace?"), - i18n.ptrl("Workspace will be closed and all the information in this workspace will be lost, including all files, unsaved changes and historical."), - i18n.ptrl("Delete"), - i18n.ptrl("Cancel") + context.ui.showOkCancelPopup( + context.i18n.ptrl("Delete running workspace?"), + context.i18n.ptrl("Workspace will be closed and all the information in this workspace will be lost, including all files, unsaved changes and historical."), + context.i18n.ptrl("Delete"), + context.i18n.ptrl("Cancel") ) } else { - ui.showOkCancelPopup( - i18n.ptrl("Delete workspace?"), - i18n.ptrl("All the information in this workspace will be lost, including all files, unsaved changes and historical."), - i18n.ptrl("Delete"), - i18n.ptrl("Cancel") + context.ui.showOkCancelPopup( + context.i18n.ptrl("Delete workspace?"), + context.i18n.ptrl("All the information in this workspace will be lost, including all files, unsaved changes and historical."), + context.i18n.ptrl("Delete"), + context.i18n.ptrl("Cancel") ) } if (shouldDelete) { try { client.removeWorkspace(workspace) - cs.launch { + context.cs.launch { withTimeout(5.minutes) { var workspaceStillExists = true - while (cs.isActive && workspaceStillExists) { + while (context.cs.isActive && workspaceStillExists) { if (wsRawStatus == WorkspaceAndAgentStatus.DELETING || wsRawStatus == WorkspaceAndAgentStatus.DELETED) { workspaceStillExists = false - serviceLocator.getService(EnvironmentUiPageManager::class.java) - .showPluginEnvironmentsPage() + context.envPageManager.showPluginEnvironmentsPage() } else { delay(1.seconds) } @@ -173,7 +163,7 @@ class CoderRemoteEnvironment( } } } catch (e: APIResponseException) { - ui.showErrorInfoPopup(e) + context.ui.showErrorInfoPopup(e) } } } diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index 1d69249..23239e7 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -1,7 +1,6 @@ package com.coder.toolbox import com.coder.toolbox.cli.CoderCLIManager -import com.coder.toolbox.logger.CoderLoggerFactory import com.coder.toolbox.sdk.CoderRestClient import com.coder.toolbox.sdk.v2.models.WorkspaceStatus import com.coder.toolbox.services.CoderSecretsService @@ -17,20 +16,13 @@ import com.coder.toolbox.views.ConnectPage import com.coder.toolbox.views.NewEnvironmentPage import com.coder.toolbox.views.SignInPage import com.coder.toolbox.views.TokenPage -import com.jetbrains.toolbox.api.core.PluginSecretStore -import com.jetbrains.toolbox.api.core.PluginSettingsStore -import com.jetbrains.toolbox.api.core.ServiceLocator import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon import com.jetbrains.toolbox.api.core.util.LoadableState -import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState import com.jetbrains.toolbox.api.remoteDev.RemoteProvider import com.jetbrains.toolbox.api.remoteDev.RemoteProviderEnvironment -import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager -import com.jetbrains.toolbox.api.ui.ToolboxUi import com.jetbrains.toolbox.api.ui.actions.ActionDescription import com.jetbrains.toolbox.api.ui.components.UiPage -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -47,29 +39,20 @@ import com.jetbrains.toolbox.api.ui.components.AccountDropdownField as DropDownM import com.jetbrains.toolbox.api.ui.components.AccountDropdownField as dropDownFactory class CoderRemoteProvider( - private val serviceLocator: ServiceLocator, + private val context: CoderToolboxContext, private val httpClient: OkHttpClient, ) : RemoteProvider("Coder") { - private val logger = CoderLoggerFactory.getLogger(javaClass) - - private val ui: ToolboxUi = serviceLocator.getService(ToolboxUi::class.java) - private val coroutineScope: CoroutineScope = serviceLocator.getService(CoroutineScope::class.java) - private val settingsStore: PluginSettingsStore = serviceLocator.getService(PluginSettingsStore::class.java) - private val secretsStore: PluginSecretStore = serviceLocator.getService(PluginSecretStore::class.java) - private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java) - // Current polling job. private var pollJob: Job? = null private var lastEnvironments: Set? = null // Create our services from the Toolbox ones. - private val settingsService = CoderSettingsService(settingsStore) + private val settingsService = CoderSettingsService(context.settingsStore) private val settings: CoderSettings = CoderSettings(settingsService) - private val secrets: CoderSecretsService = CoderSecretsService(secretsStore) - private val settingsPage: CoderSettingsPage = - CoderSettingsPage(serviceLocator, settingsService, i18n.ptrl("Coder Settings")) - private val dialogUi = DialogUi(serviceLocator, settings) - private val linkHandler = LinkHandler(serviceLocator, settings, httpClient, dialogUi) + private val secrets: CoderSecretsService = CoderSecretsService(context.secretsStore) + private val settingsPage: CoderSettingsPage = CoderSettingsPage(context, settingsService) + private val dialogUi = DialogUi(context, settings) + private val linkHandler = LinkHandler(context, settings, httpClient, dialogUi) // The REST client, if we are signed in private var client: CoderRestClient? = null @@ -91,10 +74,10 @@ class CoderRemoteProvider( * workspace is added, reconfigure SSH using the provided cli (including the * first time). */ - private fun poll(client: CoderRestClient, cli: CoderCLIManager): Job = coroutineScope.launch { + private fun poll(client: CoderRestClient, cli: CoderCLIManager): Job = context.cs.launch { while (isActive) { try { - logger.debug("Fetching workspace agents from {}", client.url) + context.logger.debug("Fetching workspace agents from ${client.url}") val resolvedEnvironments = client.workspaces().flatMap { ws -> // Agents are not included in workspaces that are off // so fetch them separately. @@ -111,7 +94,7 @@ class CoderRemoteProvider( it.name }?.map { agent -> // If we have an environment already, update that. - val env = CoderRemoteEnvironment(serviceLocator, client, ws, agent, coroutineScope) + val env = CoderRemoteEnvironment(context, client, ws, agent) lastEnvironments?.firstOrNull { it == env }?.let { it.update(ws, agent) it @@ -131,7 +114,7 @@ class CoderRemoteProvider( ?.let { resolvedEnvironments.subtract(it) } ?: resolvedEnvironments if (newEnvironments.isNotEmpty()) { - logger.info("Found new environment(s), reconfiguring CLI: {}", newEnvironments) + context.logger.info("Found new environment(s), reconfiguring CLI: $newEnvironments") cli.configSsh(newEnvironments.map { it.name }.toSet()) } @@ -141,10 +124,10 @@ class CoderRemoteProvider( lastEnvironments = resolvedEnvironments } catch (_: CancellationException) { - logger.debug("{} polling loop canceled", client.url) + context.logger.debug("${client.url} polling loop canceled") break } catch (ex: Exception) { - logger.info("setting exception $ex") + context.logger.info(ex, "workspace polling error encountered") pollError = ex logout() break @@ -171,15 +154,15 @@ class CoderRemoteProvider( override fun getAccountDropDown(): DropDownMenu? { val username = client?.me?.username if (username != null) { - return dropDownFactory(i18n.pnotr(username), { logout() }) + return dropDownFactory(context.i18n.pnotr(username), { logout() }) } return null } override val additionalPluginActions: StateFlow> = MutableStateFlow( listOf( - Action(i18n.ptrl("Settings")) { - ui.showUiPage(settingsPage) + Action(context.i18n.ptrl("Settings")) { + context.ui.showUiPage(settingsPage) }, ) ) @@ -224,7 +207,7 @@ class CoderRemoteProvider( * a form for creating new environments. */ override fun getNewEnvironmentUiPage(): UiPage = - NewEnvironmentPage(serviceLocator, i18n.pnotr(getDeploymentURL()?.first ?: "")) + NewEnvironmentPage(context, context.i18n.pnotr(getDeploymentURL()?.first ?: "")) /** * We always show a list of environments. @@ -244,10 +227,10 @@ class CoderRemoteProvider( */ override suspend fun handleUri(uri: URI) { val params = uri.toQueryParameters() - coroutineScope.launch { + context.cs.launch { val name = linkHandler.handle(params) // TODO@JB: Now what? How do we actually connect this workspace? - logger.debug("External request for {}: {}", name, uri) + context.logger.debug("External request for $name: $uri") } } @@ -260,7 +243,7 @@ class CoderRemoteProvider( * than using multiple root pages. */ private fun goToEnvironmentsPage() { - serviceLocator.getService(EnvironmentUiPageManager::class.java).showPluginEnvironmentsPage() + context.envPageManager.showPluginEnvironmentsPage() } /** @@ -290,18 +273,17 @@ class CoderRemoteProvider( // Login flow. val signInPage = - SignInPage(serviceLocator, i18n.ptrl("Sign In to Coder"), getDeploymentURL()) { deploymentURL -> - ui.showUiPage( - TokenPage( - serviceLocator, - i18n.ptrl("Enter your token"), - deploymentURL, - getToken(deploymentURL) - ) { selectedToken -> - ui.showUiPage(createConnectPage(deploymentURL, selectedToken)) - }, - ) - } + SignInPage(context, getDeploymentURL()) { deploymentURL -> + context.ui.showUiPage( + TokenPage( + context, + deploymentURL, + getToken(deploymentURL) + ) { selectedToken -> + context.ui.showUiPage(createConnectPage(deploymentURL, selectedToken)) + }, + ) + } // We might have tried and failed to automatically log in. autologinEx?.let { signInPage.notify("Error logging in", it) } @@ -317,12 +299,11 @@ class CoderRemoteProvider( * Create a connect page that starts polling and resets the UI on success. */ private fun createConnectPage(deploymentURL: URL, token: String?): ConnectPage = ConnectPage( - serviceLocator, + context, deploymentURL, token, settings, httpClient, - i18n.ptrl("Connecting to Coder"), ::goToEnvironmentsPage, ) { client, cli -> // Store the URL and token for use next time. diff --git a/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt b/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt new file mode 100644 index 0000000..76738d5 --- /dev/null +++ b/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt @@ -0,0 +1,21 @@ +package com.coder.toolbox + +import com.jetbrains.toolbox.api.core.PluginSecretStore +import com.jetbrains.toolbox.api.core.PluginSettingsStore +import com.jetbrains.toolbox.api.core.diagnostics.Logger +import com.jetbrains.toolbox.api.localization.LocalizableStringFactory +import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette +import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager +import com.jetbrains.toolbox.api.ui.ToolboxUi +import kotlinx.coroutines.CoroutineScope + +data class CoderToolboxContext( + val ui: ToolboxUi, + val envPageManager: EnvironmentUiPageManager, + val envStateColorPalette: EnvironmentStateColorPalette, + val cs: CoroutineScope, + val logger: Logger, + val i18n: LocalizableStringFactory, + val settingsStore: PluginSettingsStore, + val secretsStore: PluginSecretStore +) diff --git a/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt b/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt index 7875cf7..191944e 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt @@ -1,10 +1,17 @@ package com.coder.toolbox import com.coder.toolbox.logger.CoderLoggerFactory +import com.jetbrains.toolbox.api.core.PluginSecretStore +import com.jetbrains.toolbox.api.core.PluginSettingsStore import com.jetbrains.toolbox.api.core.ServiceLocator import com.jetbrains.toolbox.api.core.diagnostics.Logger +import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.remoteDev.RemoteDevExtension import com.jetbrains.toolbox.api.remoteDev.RemoteProvider +import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette +import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager +import com.jetbrains.toolbox.api.ui.ToolboxUi +import kotlinx.coroutines.CoroutineScope import okhttp3.OkHttpClient /** @@ -17,7 +24,16 @@ class CoderToolboxExtension : RemoteDevExtension { CoderLoggerFactory.tLogger = serviceLocator.getService(Logger::class.java) return CoderRemoteProvider( - serviceLocator, + CoderToolboxContext( + serviceLocator.getService(ToolboxUi::class.java), + serviceLocator.getService(EnvironmentUiPageManager::class.java), + serviceLocator.getService(EnvironmentStateColorPalette::class.java), + serviceLocator.getService(CoroutineScope::class.java), + serviceLocator.getService(Logger::class.java), + serviceLocator.getService(LocalizableStringFactory::class.java), + serviceLocator.getService(PluginSettingsStore::class.java), + serviceLocator.getService(PluginSecretStore::class.java), + ), OkHttpClient(), ) } diff --git a/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt b/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt index dd5bb8b..3599782 100644 --- a/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt +++ b/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt @@ -1,14 +1,13 @@ package com.coder.toolbox.models +import com.coder.toolbox.CoderToolboxContext import com.coder.toolbox.sdk.v2.models.Workspace import com.coder.toolbox.sdk.v2.models.WorkspaceAgent import com.coder.toolbox.sdk.v2.models.WorkspaceAgentLifecycleState import com.coder.toolbox.sdk.v2.models.WorkspaceAgentStatus import com.coder.toolbox.sdk.v2.models.WorkspaceStatus -import com.jetbrains.toolbox.api.core.ServiceLocator import com.jetbrains.toolbox.api.core.ui.color.StateColor import com.jetbrains.toolbox.api.remoteDev.states.CustomRemoteEnvironmentState -import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateIcons import com.jetbrains.toolbox.api.remoteDev.states.StandardRemoteEnvironmentState @@ -59,25 +58,23 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) { * Note that a reachable environment will always display "connected" or * "disconnected" regardless of the label we give that status. */ - fun toRemoteEnvironmentState(serviceLocator: ServiceLocator): CustomRemoteEnvironmentState { + fun toRemoteEnvironmentState(context: CoderToolboxContext): CustomRemoteEnvironmentState { return CustomRemoteEnvironmentState( label, - getStateColor(serviceLocator), + getStateColor(context), ready(), // reachable // TODO@JB: How does this work? Would like a spinner for pending states. getStateIcon() ) } - private fun getStateColor(serviceLocator: ServiceLocator): StateColor { - val colorPalette = serviceLocator.getService(EnvironmentStateColorPalette::class.java) - - return if (ready()) colorPalette.getColor(StandardRemoteEnvironmentState.Active) - else if (canStart()) colorPalette.getColor(StandardRemoteEnvironmentState.Failed) - else if (pending()) colorPalette.getColor(StandardRemoteEnvironmentState.Activating) - else if (this == DELETING) colorPalette.getColor(StandardRemoteEnvironmentState.Deleting) - else if (this == DELETED) colorPalette.getColor(StandardRemoteEnvironmentState.Deleted) - else colorPalette.getColor(StandardRemoteEnvironmentState.Unreachable) + private fun getStateColor(context: CoderToolboxContext): StateColor { + return if (ready()) context.envStateColorPalette.getColor(StandardRemoteEnvironmentState.Active) + else if (canStart()) context.envStateColorPalette.getColor(StandardRemoteEnvironmentState.Failed) + else if (pending()) context.envStateColorPalette.getColor(StandardRemoteEnvironmentState.Activating) + else if (this == DELETING) context.envStateColorPalette.getColor(StandardRemoteEnvironmentState.Deleting) + else if (this == DELETED) context.envStateColorPalette.getColor(StandardRemoteEnvironmentState.Deleted) + else context.envStateColorPalette.getColor(StandardRemoteEnvironmentState.Unreachable) } private fun getStateIcon(): EnvironmentStateIcons { diff --git a/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt b/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt index 1986453..0b08a3b 100644 --- a/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt +++ b/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt @@ -1,12 +1,10 @@ package com.coder.toolbox.util +import com.coder.toolbox.CoderToolboxContext import com.coder.toolbox.browser.BrowserUtil import com.coder.toolbox.settings.CoderSettings import com.coder.toolbox.settings.Source -import com.jetbrains.toolbox.api.core.ServiceLocator import com.jetbrains.toolbox.api.localization.LocalizableString -import com.jetbrains.toolbox.api.localization.LocalizableStringFactory -import com.jetbrains.toolbox.api.ui.ToolboxUi import com.jetbrains.toolbox.api.ui.components.TextType import java.net.URL @@ -16,38 +14,35 @@ import java.net.URL * This is meant to mimic ToolboxUi. */ class DialogUi( - private val serviceLocator: ServiceLocator, + private val context: CoderToolboxContext, private val settings: CoderSettings, ) { - private val ui: ToolboxUi = serviceLocator.getService(ToolboxUi::class.java) - private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java) suspend fun confirm(title: LocalizableString, description: LocalizableString): Boolean { - return ui.showOkCancelPopup(title, description, i18n.ptrl("Yes"), i18n.ptrl("No")) + return context.ui.showOkCancelPopup(title, description, context.i18n.ptrl("Yes"), context.i18n.ptrl("No")) } suspend fun ask( title: LocalizableString, description: LocalizableString, placeholder: LocalizableString? = null, - // There is no link or error support in Toolbox so for now isError and - // link are unused. + // TODO check: there is no link or error support in Toolbox so for now isError and link are unused. isError: Boolean = false, link: Pair? = null, ): String? { - return ui.showTextInputPopup( + return context.ui.showTextInputPopup( title, description, placeholder, TextType.General, - i18n.ptrl("OK"), - i18n.ptrl("Cancel") + context.i18n.ptrl("OK"), + context.i18n.ptrl("Cancel") ) } private suspend fun openUrl(url: URL) { BrowserUtil.browse(url.toString()) { - ui.showErrorInfoPopup(it) + context.ui.showErrorInfoPopup(it) } } @@ -92,13 +87,13 @@ class DialogUi( // for the token. val tokenFromUser = ask( - title = i18n.ptrl("Session Token"), - description = i18n.pnotr( + title = context.i18n.ptrl("Session Token"), + description = context.i18n.pnotr( error - ?: token?.second?.description("token") - ?: "No existing token for ${url.host} found." + ?: token?.second?.description("token") + ?: "No existing token for ${url.host} found." ), - placeholder = token?.first?.let { i18n.pnotr(it) }, + placeholder = token?.first?.let { context.i18n.pnotr(it) }, link = Pair("Session Token:", getTokenUrl.toString()), isError = error != null, ) diff --git a/src/main/kotlin/com/coder/toolbox/util/LinkHandler.kt b/src/main/kotlin/com/coder/toolbox/util/LinkHandler.kt index ff438bd..edc493b 100644 --- a/src/main/kotlin/com/coder/toolbox/util/LinkHandler.kt +++ b/src/main/kotlin/com/coder/toolbox/util/LinkHandler.kt @@ -1,5 +1,6 @@ package com.coder.toolbox.util +import com.coder.toolbox.CoderToolboxContext import com.coder.toolbox.cli.ensureCLI import com.coder.toolbox.models.WorkspaceAndAgentStatus import com.coder.toolbox.plugin.PluginManager @@ -10,20 +11,16 @@ import com.coder.toolbox.sdk.v2.models.WorkspaceAgent import com.coder.toolbox.sdk.v2.models.WorkspaceStatus import com.coder.toolbox.settings.CoderSettings import com.coder.toolbox.settings.Source -import com.jetbrains.toolbox.api.core.ServiceLocator -import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import okhttp3.OkHttpClient import java.net.HttpURLConnection import java.net.URL open class LinkHandler( - serviceLocator: ServiceLocator, + private val context: CoderToolboxContext, private val settings: CoderSettings, private val httpClient: OkHttpClient?, private val dialogUi: DialogUi, ) { - private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java) - /** * Given a set of URL parameters, prepare the CLI then return a workspace to * connect. @@ -37,8 +34,8 @@ open class LinkHandler( ): String { val deploymentURL = parameters.url() ?: dialogUi.ask( - i18n.ptrl("Deployment URL"), - i18n.ptrl("Enter the full URL of your Coder deployment") + context.i18n.ptrl("Deployment URL"), + context.i18n.ptrl("Enter the full URL of your Coder deployment") ) if (deploymentURL.isNullOrBlank()) { throw MissingArgumentException("Query parameter \"$URL\" is missing") @@ -230,8 +227,8 @@ open class LinkHandler( } if (!dialogUi.confirm( - i18n.ptrl("Confirm download URL"), - i18n.pnotr("$comment. Would you like to proceed to $linkWithRedirect?"), + context.i18n.ptrl("Confirm download URL"), + context.i18n.pnotr("$comment. Would you like to proceed to $linkWithRedirect?"), ) ) { throw IllegalArgumentException("$linkWithRedirect is not allowlisted") diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt index 6a3ee99..6a1c4e3 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt @@ -1,10 +1,8 @@ package com.coder.toolbox.views -import com.coder.toolbox.logger.CoderLoggerFactory -import com.jetbrains.toolbox.api.core.ServiceLocator +import com.coder.toolbox.CoderToolboxContext import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon import com.jetbrains.toolbox.api.localization.LocalizableString -import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription import com.jetbrains.toolbox.api.ui.components.UiField import com.jetbrains.toolbox.api.ui.components.UiPage @@ -22,12 +20,10 @@ import java.util.function.Consumer * to use the mouse. */ abstract class CoderPage( - serviceLocator: ServiceLocator, + private val context: CoderToolboxContext, title: LocalizableString, showIcon: Boolean = true, ) : UiPage(title) { - private val logger = CoderLoggerFactory.getLogger(javaClass) - private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java) /** * An error to display on the page. * @@ -59,7 +55,7 @@ abstract class CoderPage( * Show an error as a popup on this page. */ fun notify(logPrefix: String, ex: Throwable) { - logger.error(logPrefix, ex) + context.logger.error(ex, logPrefix) // It is possible the error listener is not attached yet. notifier?.let { it(ex) } ?: errorBuffer.add(ex) } @@ -81,11 +77,9 @@ abstract class CoderPage( * Set/unset the field error and update the form. */ protected fun updateError(error: String?) { - errorField = error?.let { ValidationErrorField(i18n.pnotr(error)) } + errorField = error?.let { ValidationErrorField(context.i18n.pnotr(error)) } listener?.accept(null) // Make Toolbox get the fields again. } - - } /** diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt index da5387a..be8dafa 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt @@ -1,9 +1,7 @@ package com.coder.toolbox.views +import com.coder.toolbox.CoderToolboxContext import com.coder.toolbox.services.CoderSettingsService -import com.jetbrains.toolbox.api.core.ServiceLocator -import com.jetbrains.toolbox.api.localization.LocalizableString -import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription import com.jetbrains.toolbox.api.ui.components.CheckboxField import com.jetbrains.toolbox.api.ui.components.TextField @@ -20,27 +18,27 @@ import kotlinx.coroutines.flow.StateFlow * I have not been able to test this page. */ class CoderSettingsPage( - serviceLocator: ServiceLocator, + context: CoderToolboxContext, private val settings: CoderSettingsService, - title: LocalizableString, -) : CoderPage(serviceLocator, title, false) { - private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java) - +) : CoderPage(context, context.i18n.ptrl("Coder Settings"), false) { // TODO: Copy over the descriptions, holding until I can test this page. - private val binarySourceField = TextField(i18n.ptrl("Binary source"), settings.binarySource, TextType.General) + private val binarySourceField = + TextField(context.i18n.ptrl("Binary source"), settings.binarySource, TextType.General) private val binaryDirectoryField = - TextField(i18n.ptrl("Binary directory"), settings.binaryDirectory, TextType.General) - private val dataDirectoryField = TextField(i18n.ptrl("Data directory"), settings.dataDirectory, TextType.General) - private val enableDownloadsField = CheckboxField(settings.enableDownloads, i18n.ptrl("Enable downloads")) + TextField(context.i18n.ptrl("Binary directory"), settings.binaryDirectory, TextType.General) + private val dataDirectoryField = + TextField(context.i18n.ptrl("Data directory"), settings.dataDirectory, TextType.General) + private val enableDownloadsField = CheckboxField(settings.enableDownloads, context.i18n.ptrl("Enable downloads")) private val enableBinaryDirectoryFallbackField = - CheckboxField(settings.enableBinaryDirectoryFallback, i18n.ptrl("Enable binary directory fallback")) - private val headerCommandField = TextField(i18n.ptrl("Header command"), settings.headerCommand, TextType.General) - private val tlsCertPathField = TextField(i18n.ptrl("TLS cert path"), settings.tlsCertPath, TextType.General) - private val tlsKeyPathField = TextField(i18n.ptrl("TLS key path"), settings.tlsKeyPath, TextType.General) - private val tlsCAPathField = TextField(i18n.ptrl("TLS CA path"), settings.tlsCAPath, TextType.General) + CheckboxField(settings.enableBinaryDirectoryFallback, context.i18n.ptrl("Enable binary directory fallback")) + private val headerCommandField = + TextField(context.i18n.ptrl("Header command"), settings.headerCommand, TextType.General) + private val tlsCertPathField = TextField(context.i18n.ptrl("TLS cert path"), settings.tlsCertPath, TextType.General) + private val tlsKeyPathField = TextField(context.i18n.ptrl("TLS key path"), settings.tlsKeyPath, TextType.General) + private val tlsCAPathField = TextField(context.i18n.ptrl("TLS CA path"), settings.tlsCAPath, TextType.General) private val tlsAlternateHostnameField = - TextField(i18n.ptrl("TLS alternate hostname"), settings.tlsAlternateHostname, TextType.General) - private val disableAutostartField = CheckboxField(settings.disableAutostart, i18n.ptrl("Disable autostart")) + TextField(context.i18n.ptrl("TLS alternate hostname"), settings.tlsAlternateHostname, TextType.General) + private val disableAutostartField = CheckboxField(settings.disableAutostart, context.i18n.ptrl("Disable autostart")) override val fields: StateFlow> = MutableStateFlow( listOf( @@ -60,7 +58,7 @@ class CoderSettingsPage( override val actionButtons: StateFlow> = MutableStateFlow( listOf( - Action(i18n.ptrl("Save"), closesPage = true) { + Action(context.i18n.ptrl("Save"), closesPage = true) { settings.binarySource = binarySourceField.textState.value settings.binaryDirectory = binaryDirectoryField.textState.value settings.dataDirectory = dataDirectoryField.textState.value diff --git a/src/main/kotlin/com/coder/toolbox/views/ConnectPage.kt b/src/main/kotlin/com/coder/toolbox/views/ConnectPage.kt index 20b757d..6811b67 100644 --- a/src/main/kotlin/com/coder/toolbox/views/ConnectPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/ConnectPage.kt @@ -1,18 +1,16 @@ package com.coder.toolbox.views +import com.coder.toolbox.CoderToolboxContext import com.coder.toolbox.cli.CoderCLIManager import com.coder.toolbox.cli.ensureCLI import com.coder.toolbox.plugin.PluginManager import com.coder.toolbox.sdk.CoderRestClient import com.coder.toolbox.settings.CoderSettings import com.coder.toolbox.util.humanizeConnectionError -import com.jetbrains.toolbox.api.core.ServiceLocator import com.jetbrains.toolbox.api.localization.LocalizableString -import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription import com.jetbrains.toolbox.api.ui.components.LabelField import com.jetbrains.toolbox.api.ui.components.UiField -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -24,26 +22,23 @@ import java.net.URL * A page that connects a REST client and cli to Coder. */ class ConnectPage( - private val serviceLocator: ServiceLocator, + private val context: CoderToolboxContext, private val url: URL, private val token: String?, private val settings: CoderSettings, private val httpClient: OkHttpClient, - title: LocalizableString, private val onCancel: () -> Unit, private val onConnect: ( client: CoderRestClient, cli: CoderCLIManager, ) -> Unit, -) : CoderPage(serviceLocator, title) { - private val coroutineScope = serviceLocator.getService(CoroutineScope::class.java) - private val i18n = serviceLocator.getService(LocalizableStringFactory::class.java) - +) : CoderPage(context, context.i18n.ptrl("Connecting to Coder")) { private var signInJob: Job? = null - private var statusField = LabelField(i18n.pnotr("Connecting to ${url.host}...")) + private var statusField = LabelField(context.i18n.pnotr("Connecting to ${url.host}...")) - override val description: LocalizableString = i18n.pnotr("Please wait while we configure Toolbox for ${url.host}.") + override val description: LocalizableString = + context.i18n.pnotr("Please wait while we configure Toolbox for ${url.host}.") init { connect() @@ -56,7 +51,7 @@ class ConnectPage( */ override val fields: StateFlow> = MutableStateFlow( listOfNotNull( - statusField, + statusField, errorField ) ) @@ -66,9 +61,9 @@ class ConnectPage( */ override val actionButtons: StateFlow> = MutableStateFlow( listOfNotNull( - if (errorField != null) Action(i18n.ptrl("Retry"), closesPage = false) { retry() } else null, - if (errorField != null) Action(i18n.ptrl("Cancel"), closesPage = false) { onCancel() } else null, - )) + if (errorField != null) Action(context.i18n.ptrl("Retry"), closesPage = false) { retry() } else null, + if (errorField != null) Action(context.i18n.ptrl("Cancel"), closesPage = false) { onCancel() } else null, + )) /** * Update the status and error fields then refresh. @@ -82,7 +77,7 @@ class ConnectPage( * Try connecting again after an error. */ private fun retry() { - updateStatus(i18n.pnotr("Connecting to ${url.host}..."), null) + updateStatus(context.i18n.pnotr("Connecting to ${url.host}..."), null) connect() } @@ -91,7 +86,7 @@ class ConnectPage( */ private fun connect() { signInJob?.cancel() - signInJob = coroutineScope.launch { + signInJob = context.cs.launch { try { // The http client Toolbox gives us is already set up with the // proxy config, so we do net need to explicitly add it. @@ -104,13 +99,13 @@ class ConnectPage( httpClient ) client.authenticate() - updateStatus(i18n.ptrl("Checking Coder binary..."), error = null) + updateStatus(context.i18n.ptrl("Checking Coder binary..."), error = null) val cli = ensureCLI(client.url, client.buildVersion, settings) { status -> - updateStatus(i18n.pnotr(status), error = null) + updateStatus(context.i18n.pnotr(status), error = null) } // We only need to log in if we are using token-based auth. if (client.token != null) { - updateStatus(i18n.ptrl("Configuring CLI..."), error = null) + updateStatus(context.i18n.ptrl("Configuring CLI..."), error = null) cli.login(client.token) } onConnect(client, cli) @@ -118,7 +113,7 @@ class ConnectPage( } catch (ex: Exception) { val msg = humanizeConnectionError(url, settings.requireTokenAuth, ex) notify("Failed to configure ${url.host}", ex) - updateStatus(i18n.pnotr("Failed to configure ${url.host}"), msg) + updateStatus(context.i18n.pnotr("Failed to configure ${url.host}"), msg) } } } diff --git a/src/main/kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt b/src/main/kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt index de0cf93..56b2910 100644 --- a/src/main/kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt @@ -1,6 +1,6 @@ package com.coder.toolbox.views -import com.jetbrains.toolbox.api.core.ServiceLocator +import com.coder.toolbox.CoderToolboxContext import com.jetbrains.toolbox.api.localization.LocalizableString import com.jetbrains.toolbox.api.ui.components.UiField import kotlinx.coroutines.flow.MutableStateFlow @@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.StateFlow * For now we just use this to display the deployment URL since we do not * support creating environments from the plugin. */ -class NewEnvironmentPage(serviceLocator: ServiceLocator, deploymentURL: LocalizableString) : - CoderPage(serviceLocator, deploymentURL) { +class NewEnvironmentPage(context: CoderToolboxContext, deploymentURL: LocalizableString) : + CoderPage(context, deploymentURL) { override val fields: StateFlow> = MutableStateFlow(emptyList()) } diff --git a/src/main/kotlin/com/coder/toolbox/views/SignInPage.kt b/src/main/kotlin/com/coder/toolbox/views/SignInPage.kt index 875d0c6..f6455ba 100644 --- a/src/main/kotlin/com/coder/toolbox/views/SignInPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/SignInPage.kt @@ -1,9 +1,7 @@ package com.coder.toolbox.views +import com.coder.toolbox.CoderToolboxContext import com.coder.toolbox.settings.Source -import com.jetbrains.toolbox.api.core.ServiceLocator -import com.jetbrains.toolbox.api.localization.LocalizableString -import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription import com.jetbrains.toolbox.api.ui.components.LabelField import com.jetbrains.toolbox.api.ui.components.TextField @@ -20,13 +18,11 @@ import java.net.URL * enter their own. */ class SignInPage( - serviceLocator: ServiceLocator, - title: LocalizableString, + private val context: CoderToolboxContext, private val deploymentURL: Pair?, private val onSignIn: (deploymentURL: URL) -> Unit, -) : CoderPage(serviceLocator, title) { - private val i18n: LocalizableStringFactory = serviceLocator.getService(LocalizableStringFactory::class.java) - private val urlField = TextField(i18n.ptrl("Deployment URL"), deploymentURL?.first ?: "", TextType.General) +) : CoderPage(context, context.i18n.ptrl("Sign In to Coder")) { + private val urlField = TextField(context.i18n.ptrl("Deployment URL"), deploymentURL?.first ?: "", TextType.General) /** * Fields for this page, displayed in order. @@ -36,9 +32,9 @@ class SignInPage( */ override val fields: StateFlow> = MutableStateFlow( listOfNotNull( - urlField, - deploymentURL?.let { LabelField(i18n.pnotr(deploymentURL.second.description("URL"))) }, - errorField, + urlField, + deploymentURL?.let { LabelField(context.i18n.pnotr(deploymentURL.second.description("URL"))) }, + errorField, ) ) @@ -47,7 +43,7 @@ class SignInPage( */ override val actionButtons: StateFlow> = MutableStateFlow( listOf( - Action(i18n.ptrl("Sign In"), closesPage = false) { submit() }, + Action(context.i18n.ptrl("Sign In"), closesPage = false) { submit() }, ) ) diff --git a/src/main/kotlin/com/coder/toolbox/views/TokenPage.kt b/src/main/kotlin/com/coder/toolbox/views/TokenPage.kt index edbc9c0..4c2b016 100644 --- a/src/main/kotlin/com/coder/toolbox/views/TokenPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/TokenPage.kt @@ -1,10 +1,8 @@ package com.coder.toolbox.views +import com.coder.toolbox.CoderToolboxContext import com.coder.toolbox.settings.Source import com.coder.toolbox.util.withPath -import com.jetbrains.toolbox.api.core.ServiceLocator -import com.jetbrains.toolbox.api.localization.LocalizableString -import com.jetbrains.toolbox.api.localization.LocalizableStringFactory import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription import com.jetbrains.toolbox.api.ui.components.LabelField import com.jetbrains.toolbox.api.ui.components.LinkField @@ -22,15 +20,12 @@ import java.net.URL * enter their own. */ class TokenPage( - serviceLocator: ServiceLocator, - title: LocalizableString, + private val context: CoderToolboxContext, deploymentURL: URL, token: Pair?, private val onToken: ((token: String) -> Unit), -) : CoderPage(serviceLocator, title) { - private val i18n: LocalizableStringFactory = serviceLocator.getService(LocalizableStringFactory::class.java) - - private val tokenField = TextField(i18n.ptrl("Token"), token?.first ?: "", TextType.General) +) : CoderPage(context, context.i18n.ptrl("Enter your token")) { + private val tokenField = TextField(context.i18n.ptrl("Token"), token?.first ?: "", TextType.General) /** * Fields for this page, displayed in order. @@ -40,16 +35,19 @@ class TokenPage( */ override val fields: StateFlow> = MutableStateFlow( listOfNotNull( - tokenField, - LabelField( - i18n.pnotr( - token?.second?.description("token") - ?: "No existing token for ${deploymentURL.host} found." + tokenField, + LabelField( + context.i18n.pnotr( + token?.second?.description("token") + ?: "No existing token for ${deploymentURL.host} found." + ), + ), + // TODO@JB: The link text displays twice. + LinkField( + context.i18n.ptrl("Get a token"), + deploymentURL.withPath("/login?redirect=%2Fcli-auth").toString() ), - ), - // TODO@JB: The link text displays twice. - LinkField(i18n.ptrl("Get a token"), deploymentURL.withPath("/login?redirect=%2Fcli-auth").toString()), - errorField, + errorField, ) ) @@ -58,7 +56,7 @@ class TokenPage( */ override val actionButtons: StateFlow> = MutableStateFlow( listOf( - Action(i18n.ptrl("Connect"), closesPage = false) { submit(tokenField.textState.value) }, + Action(context.i18n.ptrl("Connect"), closesPage = false) { submit(tokenField.textState.value) }, ) ) From 142a5870b9900489aecc04b650e5564ef4ee2a28 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Thu, 6 Mar 2025 23:01:04 +0200 Subject: [PATCH 11/14] impl: get rid of last remaining of internal logger - JetBrains recommendation is to use its own diagnostic logger available via service locator - gets rid of slf4j api library --- build.gradle.kts | 3 +- gradle/libs.versions.toml | 5 +- .../com/coder/toolbox/CoderRemoteProvider.kt | 2 +- .../coder/toolbox/CoderToolboxExtension.kt | 4 - .../com/coder/toolbox/cli/CoderCLIManager.kt | 19 +- .../toolbox/logger/CoderLoggerFactory.kt | 12 - .../com/coder/toolbox/logger/LoggerImpl.kt | 235 ------------------ .../com/coder/toolbox/sdk/CoderRestClient.kt | 4 +- .../coder/toolbox/settings/CoderSettings.kt | 19 +- .../com/coder/toolbox/util/LinkHandler.kt | 2 + src/main/kotlin/com/coder/toolbox/util/TLS.kt | 10 +- .../com/coder/toolbox/views/ConnectPage.kt | 3 +- .../coder/toolbox/cli/CoderCLIManagerTest.kt | 114 +++++++-- .../coder/toolbox/diagnostics/FakeLogger.kt | 73 ++++++ .../coder/toolbox/sdk/CoderRestClientTest.kt | 92 +++++-- .../toolbox/settings/CoderSettingsTest.kt | 116 +++++---- 16 files changed, 333 insertions(+), 380 deletions(-) delete mode 100644 src/main/kotlin/com/coder/toolbox/logger/CoderLoggerFactory.kt delete mode 100644 src/main/kotlin/com/coder/toolbox/logger/LoggerImpl.kt create mode 100644 src/test/kotlin/com/coder/toolbox/diagnostics/FakeLogger.kt diff --git a/build.gradle.kts b/build.gradle.kts index 7cfcfdd..e0b1b04 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -55,7 +55,6 @@ dependencies { compileOnly(libs.bundles.toolbox.plugin.api) compileOnly(libs.bundles.serialization) compileOnly(libs.coroutines.core) - implementation(libs.slf4j) implementation(libs.okhttp) implementation(libs.exec) implementation(libs.moshi) @@ -63,6 +62,8 @@ dependencies { implementation(libs.retrofit) implementation(libs.retrofit.moshi) testImplementation(kotlin("test")) + testImplementation(libs.mokk) + testImplementation(libs.bundles.toolbox.plugin.api) } val extension = ExtensionJson( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index babf037..8306149 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,6 @@ kotlin = "2.1.0" coroutines = "1.10.1" serialization = "1.8.0" okhttp = "4.10.0" -slf4j = "2.0.3" dependency-license-report = "2.5" marketplace-client = "2.0.38" gradle-wrapper = "0.14.0" @@ -15,6 +14,7 @@ retrofit = "2.8.2" changelog = "2.2.1" gettext = "0.7.0" plugin-structure = "3.298" +mockk = "1.13.17" [libraries] toolbox-core-api = { module = "com.jetbrains.toolbox:core-api", version.ref = "toolbox-plugin-api" } @@ -25,14 +25,13 @@ serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cor serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } serialization-json-okio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "serialization" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } -slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } exec = { module = "org.zeroturnaround:zt-exec", version.ref = "exec" } moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" } plugin-structure = { module = "org.jetbrains.intellij.plugins:structure-toolbox", version.ref = "plugin-structure" } - +mokk = { module = "io.mockk:mockk", version.ref = "mockk" } marketplace-client = { module = "org.jetbrains.intellij:plugin-repository-rest-client", version.ref = "marketplace-client" } [bundles] diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index 23239e7..a449c39 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -48,7 +48,7 @@ class CoderRemoteProvider( // Create our services from the Toolbox ones. private val settingsService = CoderSettingsService(context.settingsStore) - private val settings: CoderSettings = CoderSettings(settingsService) + private val settings: CoderSettings = CoderSettings(settingsService, context.logger) private val secrets: CoderSecretsService = CoderSecretsService(context.secretsStore) private val settingsPage: CoderSettingsPage = CoderSettingsPage(context, settingsService) private val dialogUi = DialogUi(context, settings) diff --git a/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt b/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt index 191944e..8ee06d1 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt @@ -1,6 +1,5 @@ package com.coder.toolbox -import com.coder.toolbox.logger.CoderLoggerFactory import com.jetbrains.toolbox.api.core.PluginSecretStore import com.jetbrains.toolbox.api.core.PluginSettingsStore import com.jetbrains.toolbox.api.core.ServiceLocator @@ -20,9 +19,6 @@ import okhttp3.OkHttpClient class CoderToolboxExtension : RemoteDevExtension { // All services must be passed in here and threaded as necessary. override fun createRemoteProviderPluginInstance(serviceLocator: ServiceLocator): RemoteProvider { - // initialize logger factory - CoderLoggerFactory.tLogger = serviceLocator.getService(Logger::class.java) - return CoderRemoteProvider( CoderToolboxContext( serviceLocator.getService(ToolboxUi::class.java), diff --git a/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt b/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt index 707cb5b..d4f347f 100644 --- a/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt +++ b/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt @@ -1,9 +1,9 @@ package com.coder.toolbox.cli +import com.coder.toolbox.CoderToolboxContext import com.coder.toolbox.cli.ex.MissingVersionException import com.coder.toolbox.cli.ex.ResponseException import com.coder.toolbox.cli.ex.SSHConfigFormatException -import com.coder.toolbox.logger.CoderLoggerFactory import com.coder.toolbox.settings.CoderSettings import com.coder.toolbox.settings.CoderSettingsState import com.coder.toolbox.util.CoderHostnameVerifier @@ -17,6 +17,7 @@ import com.coder.toolbox.util.getHeaders import com.coder.toolbox.util.getOS import com.coder.toolbox.util.safeHost import com.coder.toolbox.util.sha1 +import com.jetbrains.toolbox.api.core.diagnostics.Logger import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonDataException @@ -55,12 +56,13 @@ internal data class Version( * from step 2 with the data directory. */ fun ensureCLI( + context: CoderToolboxContext, deploymentURL: URL, buildVersion: String, settings: CoderSettings, indicator: ((t: String) -> Unit)? = null, ): CoderCLIManager { - val cli = CoderCLIManager(deploymentURL, settings) + val cli = CoderCLIManager(deploymentURL, context.logger, settings) // Short-circuit if we already have the expected version. This // lets us bypass the 304 which is slower and may not be @@ -89,7 +91,7 @@ fun ensureCLI( } // Try falling back to the data directory. - val dataCLI = CoderCLIManager(deploymentURL, settings, true) + val dataCLI = CoderCLIManager(deploymentURL, context.logger, settings, true) val dataCLIMatches = dataCLI.matchesVersion(buildVersion) if (dataCLIMatches == true) { return dataCLI @@ -120,14 +122,13 @@ data class Features( class CoderCLIManager( // The URL of the deployment this CLI is for. private val deploymentURL: URL, + private val logger: Logger, // Plugin configuration. - private val settings: CoderSettings = CoderSettings(CoderSettingsState()), + private val settings: CoderSettings = CoderSettings(CoderSettingsState(), logger), // If the binary directory is not writable, this can be used to force the // manager to download to the data directory instead. forceDownloadToData: Boolean = false, ) { - private val logger = CoderLoggerFactory.getLogger(javaClass) - val remoteBinaryURL: URL = settings.binSource(deploymentURL) val localBinaryPath: Path = settings.binPath(deploymentURL, forceDownloadToData) val coderConfigPath: Path = settings.dataDir(deploymentURL).resolve("config") @@ -196,7 +197,7 @@ class CoderCLIManager( } catch (e: FileNotFoundException) { null } catch (e: Exception) { - logger.warn("Unable to calculate hash for $localBinaryPath", e) + logger.warn(e, "Unable to calculate hash for $localBinaryPath") null } @@ -275,7 +276,8 @@ class CoderCLIManager( if (settings.sshLogDirectory.isNotBlank()) escape(settings.sshLogDirectory) else null, if (feats.reportWorkspaceUsage) "--usage-app=jetbrains" else null, ) - val backgroundProxyArgs = baseArgs + listOfNotNull(if (feats.reportWorkspaceUsage) "--usage-app=disable" else null) + val backgroundProxyArgs = + baseArgs + listOfNotNull(if (feats.reportWorkspaceUsage) "--usage-app=disable" else null) val extraConfig = if (settings.sshConfigOptions.isNotBlank()) { "\n" + settings.sshConfigOptions.prependIndent(" ") @@ -417,6 +419,7 @@ class CoderCLIManager( is InvalidVersionException -> { logger.info("Got invalid version from $localBinaryPath: ${e.message}") } + else -> { // An error here most likely means the CLI does not exist or // it executed successfully but output no version which diff --git a/src/main/kotlin/com/coder/toolbox/logger/CoderLoggerFactory.kt b/src/main/kotlin/com/coder/toolbox/logger/CoderLoggerFactory.kt deleted file mode 100644 index 58b7fb4..0000000 --- a/src/main/kotlin/com/coder/toolbox/logger/CoderLoggerFactory.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.coder.toolbox.logger - -import org.slf4j.ILoggerFactory -import org.slf4j.Logger -import com.jetbrains.toolbox.api.core.diagnostics.Logger as ToolboxLogger - -object CoderLoggerFactory : ILoggerFactory { - var tLogger: ToolboxLogger? = null - - fun getLogger(clazz: Class): Logger = getLogger(clazz.name) - override fun getLogger(clazzName: String): Logger = LoggerImpl(clazzName, tLogger) -} \ No newline at end of file diff --git a/src/main/kotlin/com/coder/toolbox/logger/LoggerImpl.kt b/src/main/kotlin/com/coder/toolbox/logger/LoggerImpl.kt deleted file mode 100644 index a476666..0000000 --- a/src/main/kotlin/com/coder/toolbox/logger/LoggerImpl.kt +++ /dev/null @@ -1,235 +0,0 @@ -package com.coder.toolbox.logger - -import org.slf4j.Logger -import org.slf4j.Marker -import com.jetbrains.toolbox.api.core.diagnostics.Logger as ToolboxLogger - -class LoggerImpl(private val clazzName: String, private val tLogger: ToolboxLogger?) : Logger { - override fun getName(): String = clazzName - - override fun isTraceEnabled(): Boolean = true - - override fun trace(message: String) { - tLogger?.trace(message) - } - - override fun trace(message: String, arg: Any?) { - extractThrowable(arg)?.let { tLogger?.trace(it, message) } ?: tLogger?.trace(message) - } - - override fun trace(message: String, firstArg: Any?, secondArg: Any?) { - extractThrowable(firstArg, secondArg)?.let { tLogger?.trace(it, message) } ?: tLogger?.trace(message) - } - - override fun trace(message: String, vararg args: Any?) { - extractThrowable(args)?.let { tLogger?.trace(it, message) } ?: tLogger?.trace(message) - } - - override fun trace(message: String, exception: Throwable) { - tLogger?.trace(exception, message) - } - - override fun isTraceEnabled(marker: Marker): Boolean = true - - override fun trace(marker: Marker, message: String) { - tLogger?.trace(message) - } - - override fun trace(marker: Marker, message: String, arg: Any) { - extractThrowable(arg)?.let { tLogger?.trace(it, message) } ?: tLogger?.trace(message) - } - - override fun trace(marker: Marker, message: String, firstArg: Any?, secondArg: Any?) { - extractThrowable(firstArg, secondArg)?.let { tLogger?.trace(it, message) } ?: tLogger?.trace(message) - } - - override fun trace(marker: Marker, message: String, vararg args: Any?) { - extractThrowable(args)?.let { tLogger?.trace(it, message) } ?: tLogger?.trace(message) - } - - override fun trace(marker: Marker, message: String, exception: Throwable) { - tLogger?.trace(exception, message) - } - - override fun isDebugEnabled(): Boolean = true - - override fun debug(message: String) { - tLogger?.debug(message) - } - - override fun debug(message: String, arg: Any?) { - extractThrowable(arg)?.let { tLogger?.debug(it, message) } ?: tLogger?.debug(message) - } - - override fun debug(message: String, firstArg: Any?, secondArg: Any?) { - extractThrowable(firstArg, secondArg)?.let { tLogger?.debug(it, message) } ?: tLogger?.debug(message) - } - - override fun debug(message: String, vararg args: Any?) { - extractThrowable(args)?.let { tLogger?.debug(it, message) } ?: tLogger?.debug(message) - } - - override fun debug(message: String, exception: Throwable) { - tLogger?.debug(exception, message) - } - - override fun isDebugEnabled(marker: Marker): Boolean = true - - override fun debug(marker: Marker, message: String) { - tLogger?.debug(message) - } - - override fun debug(marker: Marker, message: String, arg: Any?) { - extractThrowable(arg)?.let { tLogger?.debug(it, message) } ?: tLogger?.debug(message) - } - - override fun debug(marker: Marker, message: String, firstArg: Any?, secondArg: Any?) { - extractThrowable(firstArg, secondArg)?.let { tLogger?.debug(it, message) } ?: tLogger?.debug(message) - } - - override fun debug(marker: Marker, message: String, vararg args: Any?) { - extractThrowable(args)?.let { tLogger?.debug(it, message) } ?: tLogger?.debug(message) - } - - override fun debug(marker: Marker, message: String, exception: Throwable) { - tLogger?.debug(exception, message) - } - - override fun isInfoEnabled(): Boolean = true - - override fun info(message: String) { - tLogger?.info(message) - } - - override fun info(message: String, arg: Any?) { - extractThrowable(arg)?.let { tLogger?.info(it, message) } ?: tLogger?.info(message) - } - - override fun info(message: String, firstArg: Any?, secondArg: Any?) { - extractThrowable(firstArg, secondArg)?.let { tLogger?.info(it, message) } ?: tLogger?.info(message) - } - - override fun info(message: String, vararg args: Any?) { - extractThrowable(args)?.let { tLogger?.info(it, message) } ?: tLogger?.info(message) - } - - override fun info(message: String, exception: Throwable) { - tLogger?.info(exception, message) - } - - override fun isInfoEnabled(marker: Marker): Boolean = true - - override fun info(marker: Marker, message: String) { - tLogger?.info(message) - } - - override fun info(marker: Marker, message: String, arg: Any?) { - extractThrowable(arg)?.let { tLogger?.info(it, message) } ?: tLogger?.info(message) - } - - override fun info(marker: Marker, message: String, firstArg: Any?, secondArg: Any?) { - extractThrowable(firstArg, secondArg)?.let { tLogger?.info(it, message) } ?: tLogger?.info(message) - } - - override fun info(marker: Marker, message: String, vararg args: Any?) { - extractThrowable(args)?.let { tLogger?.info(it, message) } ?: tLogger?.info(message) - } - - override fun info(marker: Marker, message: String, exception: Throwable) { - tLogger?.info(exception, message) - } - - override fun isWarnEnabled(): Boolean = true - - override fun warn(message: String) { - tLogger?.warn(message) - } - - override fun warn(message: String, arg: Any?) { - extractThrowable(arg)?.let { tLogger?.warn(it, message) } ?: tLogger?.warn(message) - } - - override fun warn(message: String, vararg args: Any?) { - extractThrowable(args)?.let { tLogger?.warn(it, message) } ?: tLogger?.warn(message) - } - - override fun warn(message: String, firstArg: Any?, secondArg: Any?) { - extractThrowable(firstArg, secondArg)?.let { tLogger?.warn(it, message) } ?: tLogger?.warn(message) - } - - override fun warn(message: String, exception: Throwable) { - tLogger?.warn(exception, message) - } - - override fun isWarnEnabled(marker: Marker): Boolean = true - - override fun warn(marker: Marker, message: String) { - tLogger?.warn(message) - } - - override fun warn(marker: Marker, message: String, arg: Any?) { - extractThrowable(arg)?.let { tLogger?.warn(it, message) } ?: tLogger?.warn(message) - } - - override fun warn(marker: Marker, message: String, firstArg: Any?, secondArg: Any?) { - extractThrowable(firstArg, secondArg)?.let { tLogger?.warn(it, message) } ?: tLogger?.warn(message) - } - - override fun warn(marker: Marker, message: String, vararg args: Any?) { - extractThrowable(args)?.let { tLogger?.warn(it, message) } ?: tLogger?.warn(message) - } - - override fun warn(marker: Marker, message: String, exception: Throwable) { - tLogger?.warn(exception, message) - } - - override fun isErrorEnabled(): Boolean = true - - override fun error(message: String) { - tLogger?.error(message) - } - - override fun error(message: String, arg: Any?) { - extractThrowable(arg)?.let { tLogger?.error(it, message) } ?: tLogger?.error(message) - } - - override fun error(message: String, firstArg: Any?, secondArg: Any?) { - extractThrowable(firstArg, secondArg)?.let { tLogger?.error(it, message) } ?: tLogger?.error(message) - } - - override fun error(message: String, vararg args: Any?) { - extractThrowable(args)?.let { tLogger?.error(it, message) } ?: tLogger?.error(message) - } - - override fun error(message: String, exception: Throwable) { - tLogger?.error(exception, message) - } - - override fun isErrorEnabled(marker: Marker): Boolean = true - - override fun error(marker: Marker, message: String) { - tLogger?.error(message) - } - - override fun error(marker: Marker, message: String, arg: Any?) { - extractThrowable(arg)?.let { tLogger?.error(it, message) } ?: tLogger?.error(message) - } - - override fun error(marker: Marker, message: String, firstArg: Any?, secondArg: Any?) { - extractThrowable(firstArg, secondArg)?.let { tLogger?.error(it, message) } ?: tLogger?.error(message) - } - - override fun error(marker: Marker, message: String, vararg args: Any?) { - extractThrowable(args)?.let { tLogger?.error(it, message) } ?: tLogger?.error(message) - } - - override fun error(marker: Marker, message: String, exception: Throwable) { - tLogger?.error(exception, message) - } - - companion object { - fun extractThrowable(vararg args: Any?): Throwable? = args.firstOrNull { it is Throwable } as? Throwable - - fun extractThrowable(arg: Any?): Throwable? = arg as? Throwable - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt index 371c818..2d2c49e 100644 --- a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt +++ b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt @@ -1,5 +1,6 @@ package com.coder.toolbox.sdk +import com.coder.toolbox.CoderToolboxContext import com.coder.toolbox.sdk.convertors.ArchConverter import com.coder.toolbox.sdk.convertors.InstantConverter import com.coder.toolbox.sdk.convertors.OSConverter @@ -49,9 +50,10 @@ data class ProxyValues( * The token can be omitted if some other authentication mechanism is in use. */ open class CoderRestClient( + context: CoderToolboxContext, val url: URL, val token: String?, - private val settings: CoderSettings = CoderSettings(CoderSettingsState()), + private val settings: CoderSettings = CoderSettings(CoderSettingsState(), context.logger), private val proxyValues: ProxyValues? = null, private val pluginVersion: String = "development", existingHttpClient: OkHttpClient? = null, diff --git a/src/main/kotlin/com/coder/toolbox/settings/CoderSettings.kt b/src/main/kotlin/com/coder/toolbox/settings/CoderSettings.kt index ddcd269..0f95798 100644 --- a/src/main/kotlin/com/coder/toolbox/settings/CoderSettings.kt +++ b/src/main/kotlin/com/coder/toolbox/settings/CoderSettings.kt @@ -1,6 +1,5 @@ package com.coder.toolbox.settings -import com.coder.toolbox.logger.CoderLoggerFactory import com.coder.toolbox.util.Arch import com.coder.toolbox.util.OS import com.coder.toolbox.util.expand @@ -9,6 +8,7 @@ import com.coder.toolbox.util.getOS import com.coder.toolbox.util.safeHost import com.coder.toolbox.util.toURL import com.coder.toolbox.util.withPath +import com.jetbrains.toolbox.api.core.diagnostics.Logger import java.net.URL import java.nio.file.Files import java.nio.file.Path @@ -34,7 +34,7 @@ enum class Source { * Return a description of the source. */ fun description(name: String): String = when (this) { - CONFIG -> "This $name was pulled from your global CLI config." + CONFIG -> "This $name was pulled from your global CLI config." DEPLOYMENT_CONFIG -> "This $name was pulled from your deployment's CLI config." LAST_USED -> "This was the last used $name." QUERY -> "This $name was pulled from the Gateway link." @@ -120,6 +120,7 @@ data class CoderTLSSettings(private val state: CoderSettingsState) { open class CoderSettings( // Raw mutable setting state. private val state: CoderSettingsState, + private val logger: Logger, // The location of the SSH config. Defaults to ~/.ssh/config. val sshConfigPath: Path = Path.of(System.getProperty("user.home")).resolve(".ssh/config"), // Overrides the default environment (for tests). @@ -127,8 +128,6 @@ open class CoderSettings( // Overrides the default binary name (for tests). private val binaryName: String? = null, ) { - private val logger = CoderLoggerFactory.getLogger(javaClass) - val tls = CoderTLSSettings(state) /** @@ -284,12 +283,12 @@ open class CoderSettings( // SSH has not been configured yet, or using some other authorization mechanism. null } to - try { - Files.readString(dir.resolve("session")) - } catch (e: Exception) { - // SSH has not been configured yet, or using some other authorization mechanism. - null - } + try { + Files.readString(dir.resolve("session")) + } catch (e: Exception) { + // SSH has not been configured yet, or using some other authorization mechanism. + null + } } /** diff --git a/src/main/kotlin/com/coder/toolbox/util/LinkHandler.kt b/src/main/kotlin/com/coder/toolbox/util/LinkHandler.kt index edc493b..31a6602 100644 --- a/src/main/kotlin/com/coder/toolbox/util/LinkHandler.kt +++ b/src/main/kotlin/com/coder/toolbox/util/LinkHandler.kt @@ -113,6 +113,7 @@ open class LinkHandler( val cli = ensureCLI( + context, deploymentURL.toURL(), client.buildInfo().version, settings, @@ -170,6 +171,7 @@ open class LinkHandler( // The http client Toolbox gives us is already set up with the // proxy config, so we do net need to explicitly add it. val client = CoderRestClient( + context, deploymentURL.toURL(), token?.first, settings, diff --git a/src/main/kotlin/com/coder/toolbox/util/TLS.kt b/src/main/kotlin/com/coder/toolbox/util/TLS.kt index 0d17560..c69aaff 100644 --- a/src/main/kotlin/com/coder/toolbox/util/TLS.kt +++ b/src/main/kotlin/com/coder/toolbox/util/TLS.kt @@ -1,6 +1,5 @@ package com.coder.toolbox.util -import com.coder.toolbox.logger.CoderLoggerFactory import com.coder.toolbox.settings.CoderTLSSettings import okhttp3.internal.tls.OkHostnameVerifier import java.io.File @@ -112,7 +111,8 @@ fun coderTrustManagers(tlsCAPath: String): Array { return trustManagerFactory.trustManagers.map { MergedSystemTrustManger(it as X509TrustManager) }.toTypedArray() } -class AlternateNameSSLSocketFactory(private val delegate: SSLSocketFactory, private val alternateName: String) : SSLSocketFactory() { +class AlternateNameSSLSocketFactory(private val delegate: SSLSocketFactory, private val alternateName: String) : + SSLSocketFactory() { override fun getDefaultCipherSuites(): Array = delegate.defaultCipherSuites override fun getSupportedCipherSuites(): Array = delegate.supportedCipherSuites @@ -182,8 +182,6 @@ class AlternateNameSSLSocketFactory(private val delegate: SSLSocketFactory, priv } class CoderHostnameVerifier(private val alternateName: String) : HostnameVerifier { - private val logger = CoderLoggerFactory.getLogger(javaClass) - override fun verify( host: String, session: SSLSession, @@ -203,7 +201,6 @@ class CoderHostnameVerifier(private val alternateName: String) : HostnameVerifie continue } val hostname = entry[1] as String - logger.debug("Found cert hostname: $hostname") if (hostname.lowercase(Locale.getDefault()) == alternateName) { return true } @@ -244,5 +241,6 @@ class MergedSystemTrustManger(private val otherTrustManager: X509TrustManager) : } } - override fun getAcceptedIssuers(): Array = otherTrustManager.acceptedIssuers + systemTrustManager.acceptedIssuers + override fun getAcceptedIssuers(): Array = + otherTrustManager.acceptedIssuers + systemTrustManager.acceptedIssuers } diff --git a/src/main/kotlin/com/coder/toolbox/views/ConnectPage.kt b/src/main/kotlin/com/coder/toolbox/views/ConnectPage.kt index 6811b67..9538d45 100644 --- a/src/main/kotlin/com/coder/toolbox/views/ConnectPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/ConnectPage.kt @@ -91,6 +91,7 @@ class ConnectPage( // The http client Toolbox gives us is already set up with the // proxy config, so we do net need to explicitly add it. val client = CoderRestClient( + context, url, token, settings, @@ -100,7 +101,7 @@ class ConnectPage( ) client.authenticate() updateStatus(context.i18n.ptrl("Checking Coder binary..."), error = null) - val cli = ensureCLI(client.url, client.buildVersion, settings) { status -> + val cli = ensureCLI(context, client.url, client.buildVersion, settings) { status -> updateStatus(context.i18n.pnotr(status), error = null) } // We only need to log in if we are using token-based auth. diff --git a/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt b/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt index 6eef3e9..131cab9 100644 --- a/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt +++ b/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt @@ -1,5 +1,6 @@ package com.coder.toolbox.cli +import com.coder.toolbox.CoderToolboxContext import com.coder.toolbox.cli.ex.MissingVersionException import com.coder.toolbox.cli.ex.ResponseException import com.coder.toolbox.cli.ex.SSHConfigFormatException @@ -14,8 +15,17 @@ import com.coder.toolbox.util.escape import com.coder.toolbox.util.getOS import com.coder.toolbox.util.sha1 import com.coder.toolbox.util.toURL +import com.jetbrains.toolbox.api.core.PluginSecretStore +import com.jetbrains.toolbox.api.core.PluginSettingsStore +import com.jetbrains.toolbox.api.core.diagnostics.Logger +import com.jetbrains.toolbox.api.localization.LocalizableStringFactory +import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette +import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager +import com.jetbrains.toolbox.api.ui.ToolboxUi import com.squareup.moshi.JsonEncodingException import com.sun.net.httpserver.HttpServer +import io.mockk.mockk +import kotlinx.coroutines.CoroutineScope import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.assertDoesNotThrow import org.zeroturnaround.exec.InvalidExitValueException @@ -34,6 +44,17 @@ import kotlin.test.assertNotEquals import kotlin.test.assertTrue internal class CoderCLIManagerTest { + private val context = CoderToolboxContext( + mockk(), + mockk(), + mockk(), + mockk(), + mockk(relaxed = true), + mockk(), + mockk(), + mockk() + ) + /** * Return the contents of a script that contains the string. */ @@ -84,7 +105,7 @@ internal class CoderCLIManagerTest { @Test fun testServerInternalError() { val (srv, url) = mockServer(HttpURLConnection.HTTP_INTERNAL_ERROR) - val ccm = CoderCLIManager(url) + val ccm = CoderCLIManager(url, context.logger) val ex = assertFailsWith( @@ -104,16 +125,17 @@ internal class CoderCLIManagerTest { dataDirectory = tmpdir.resolve("cli-data-dir").toString(), binaryDirectory = tmpdir.resolve("cli-bin-dir").toString(), ), + context.logger ) val url = URL("http://localhost") - val ccm1 = CoderCLIManager(url, settings) + val ccm1 = CoderCLIManager(url, context.logger, settings) assertEquals(settings.binSource(url), ccm1.remoteBinaryURL) assertEquals(settings.dataDir(url), ccm1.coderConfigPath.parent) assertEquals(settings.binPath(url), ccm1.localBinaryPath) // Can force using data directory. - val ccm2 = CoderCLIManager(url, settings, true) + val ccm2 = CoderCLIManager(url, context.logger, settings, true) assertEquals(settings.binSource(url), ccm2.remoteBinaryURL) assertEquals(settings.dataDir(url), ccm2.coderConfigPath.parent) assertEquals(settings.binPath(url, true), ccm2.localBinaryPath) @@ -129,10 +151,12 @@ internal class CoderCLIManagerTest { val ccm = CoderCLIManager( url, + context.logger, CoderSettings( CoderSettingsState( dataDirectory = tmpdir.resolve("cli-dir-fail-to-write").toString(), ), + context.logger ), ) @@ -161,10 +185,12 @@ internal class CoderCLIManagerTest { val ccm = CoderCLIManager( url.toURL(), + context.logger, CoderSettings( CoderSettingsState( dataDirectory = tmpdir.resolve("real-cli").toString(), ), + context.logger ), ) @@ -187,10 +213,12 @@ internal class CoderCLIManagerTest { var ccm = CoderCLIManager( url, + context.logger, CoderSettings( CoderSettingsState( dataDirectory = tmpdir.resolve("mock-cli").toString(), ), + context.logger, binaryName = "coder.bat", ), ) @@ -205,11 +233,13 @@ internal class CoderCLIManagerTest { ccm = CoderCLIManager( url, + context.logger, CoderSettings( CoderSettingsState( binarySource = "/bin/override", dataDirectory = tmpdir.resolve("mock-cli").toString(), ), + context.logger ), ) @@ -224,10 +254,12 @@ internal class CoderCLIManagerTest { val ccm = CoderCLIManager( URL("https://foo"), + context.logger, CoderSettings( CoderSettingsState( dataDirectory = tmpdir.resolve("does-not-exist").toString(), ), + context.logger ), ) @@ -238,15 +270,17 @@ internal class CoderCLIManagerTest { } @Test - fun testOverwitesWrongVersion() { + fun testOverwritesWrongVersion() { val (srv, url) = mockServer() val ccm = CoderCLIManager( url, + context.logger, CoderSettings( CoderSettingsState( dataDirectory = tmpdir.resolve("overwrite-cli").toString(), ), + context.logger ), ) @@ -276,10 +310,11 @@ internal class CoderCLIManagerTest { CoderSettingsState( dataDirectory = tmpdir.resolve("clobber-cli").toString(), ), + context.logger ) - val ccm1 = CoderCLIManager(url1, settings) - val ccm2 = CoderCLIManager(url2, settings) + val ccm1 = CoderCLIManager(url1, context.logger, settings) + val ccm2 = CoderCLIManager(url2, context.logger, settings) assertTrue(ccm1.download()) assertTrue(ccm2.download()) @@ -321,7 +356,12 @@ internal class CoderCLIManagerTest { SSHTest(listOf("foo-bar"), "existing-end", "replace-end", "no-blocks"), SSHTest(listOf("foo-bar"), "existing-end-no-newline", "replace-end-no-newline", "no-blocks"), SSHTest(listOf("foo-bar"), "existing-middle", "replace-middle", "no-blocks"), - SSHTest(listOf("foo-bar"), "existing-middle-and-unrelated", "replace-middle-ignore-unrelated", "no-related-blocks"), + SSHTest( + listOf("foo-bar"), + "existing-middle-and-unrelated", + "replace-middle-ignore-unrelated", + "no-related-blocks" + ), SSHTest(listOf("foo-bar"), "existing-only", "replace-only", "blank"), SSHTest(listOf("foo-bar"), "existing-start", "replace-start", "no-blocks"), SSHTest(listOf("foo-bar"), "no-blocks", "append-no-blocks", "no-blocks"), @@ -404,11 +444,12 @@ internal class CoderCLIManagerTest { sshConfigOptions = it.extraConfig, sshLogDirectory = it.sshLogDirectory?.toString() ?: "", ), + context.logger, sshConfigPath = tmpdir.resolve(it.input + "_to_" + it.output + ".conf"), env = it.env, ) - val ccm = CoderCLIManager(URL("https://test.coder.invalid"), settings) + val ccm = CoderCLIManager(URL("https://test.coder.invalid"), context.logger, settings) // Input is the configuration that we start with, if any. if (it.input != null) { @@ -425,7 +466,10 @@ internal class CoderCLIManagerTest { Path.of("src/test/fixtures/outputs/").resolve(it.output + ".conf").toFile().readText() .replace(newlineRe, System.lineSeparator()) .replace("/tmp/coder-toolbox/test.coder.invalid/config", escape(coderConfigPath.toString())) - .replace("/tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64", escape(ccm.localBinaryPath.toString())) + .replace( + "/tmp/coder-toolbox/test.coder.invalid/coder-linux-amd64", + escape(ccm.localBinaryPath.toString()) + ) .let { conf -> if (it.sshLogDirectory != null) { conf.replace("/tmp/coder-toolbox/test.coder.invalid/logs", it.sshLogDirectory.toString()) @@ -470,6 +514,7 @@ internal class CoderCLIManagerTest { val settings = CoderSettings( CoderSettingsState(), + context.logger, sshConfigPath = tmpdir.resolve("configured$it.conf"), ) settings.sshConfigPath.parent.toFile().mkdirs() @@ -478,7 +523,7 @@ internal class CoderCLIManagerTest { true, ) - val ccm = CoderCLIManager(URL("https://test.coder.invalid"), settings) + val ccm = CoderCLIManager(URL("https://test.coder.invalid"), context.logger, settings) assertFailsWith( exceptionClass = SSHConfigFormatException::class, @@ -498,10 +543,12 @@ internal class CoderCLIManagerTest { val ccm = CoderCLIManager( URL("https://test.coder.invalid"), + context.logger, CoderSettings( CoderSettingsState( headerCommand = it, ), + context.logger ), ) @@ -547,10 +594,12 @@ internal class CoderCLIManagerTest { val ccm = CoderCLIManager( URL("https://test.coder.parse-fail.invalid"), + context.logger, CoderSettings( CoderSettingsState( binaryDirectory = tmpdir.resolve("bad-version").toString(), ), + context.logger, binaryName = "coder.bat", ), ) @@ -598,10 +647,12 @@ internal class CoderCLIManagerTest { val ccm = CoderCLIManager( URL("https://test.coder.matches-version.invalid"), + context.logger, CoderSettings( CoderSettingsState( binaryDirectory = tmpdir.resolve("matches-version").toString(), ), + context.logger, binaryName = "coder.bat", ), ) @@ -667,8 +718,24 @@ internal class CoderCLIManagerTest { EnsureCLITest(null, null, "1.0.0", false, true, true, Result.DL_DATA), // Download to fallback. EnsureCLITest(null, null, "1.0.0", false, false, true, Result.NONE), // No download, error when used. EnsureCLITest("1.0.1", "1.0.1", "1.0.0", false, true, true, Result.DL_DATA), // Update fallback. - EnsureCLITest("1.0.1", "1.0.2", "1.0.0", false, false, true, Result.USE_BIN), // No update, use outdated. - EnsureCLITest(null, "1.0.2", "1.0.0", false, false, true, Result.USE_DATA), // No update, use outdated fallback. + EnsureCLITest( + "1.0.1", + "1.0.2", + "1.0.0", + false, + false, + true, + Result.USE_BIN + ), // No update, use outdated. + EnsureCLITest( + null, + "1.0.2", + "1.0.0", + false, + false, + true, + Result.USE_DATA + ), // No update, use outdated fallback. EnsureCLITest("1.0.0", null, "1.0.0", false, false, true, Result.USE_BIN), // Use existing. EnsureCLITest("1.0.1", "1.0.0", "1.0.0", false, false, true, Result.USE_DATA), // Use existing fallback. ) @@ -684,6 +751,7 @@ internal class CoderCLIManagerTest { dataDirectory = tmpdir.resolve("ensure-data-dir").toString(), binaryDirectory = tmpdir.resolve("ensure-bin-dir").toString(), ), + context.logger ) // Clean up from previous test. @@ -714,34 +782,39 @@ internal class CoderCLIManagerTest { Result.ERROR -> { assertFailsWith( exceptionClass = AccessDeniedException::class, - block = { ensureCLI(url, it.buildVersion, settings) }, + block = { ensureCLI(context, url, it.buildVersion, settings) }, ) } + Result.NONE -> { - val ccm = ensureCLI(url, it.buildVersion, settings) + val ccm = ensureCLI(context, url, it.buildVersion, settings) assertEquals(settings.binPath(url), ccm.localBinaryPath) assertFailsWith( exceptionClass = ProcessInitException::class, block = { ccm.version() }, ) } + Result.DL_BIN -> { - val ccm = ensureCLI(url, it.buildVersion, settings) + val ccm = ensureCLI(context, url, it.buildVersion, settings) assertEquals(settings.binPath(url), ccm.localBinaryPath) assertEquals(SemVer(url.port.toLong(), 0, 0), ccm.version()) } + Result.DL_DATA -> { - val ccm = ensureCLI(url, it.buildVersion, settings) + val ccm = ensureCLI(context, url, it.buildVersion, settings) assertEquals(settings.binPath(url, true), ccm.localBinaryPath) assertEquals(SemVer(url.port.toLong(), 0, 0), ccm.version()) } + Result.USE_BIN -> { - val ccm = ensureCLI(url, it.buildVersion, settings) + val ccm = ensureCLI(context, url, it.buildVersion, settings) assertEquals(settings.binPath(url), ccm.localBinaryPath) assertEquals(SemVer.parse(it.version ?: ""), ccm.version()) } + Result.USE_DATA -> { - val ccm = ensureCLI(url, it.buildVersion, settings) + val ccm = ensureCLI(context, url, it.buildVersion, settings) assertEquals(settings.binPath(url, true), ccm.localBinaryPath) assertEquals(SemVer.parse(it.fallbackVersion ?: ""), ccm.version()) } @@ -772,10 +845,12 @@ internal class CoderCLIManagerTest { val ccm = CoderCLIManager( url, + context.logger, CoderSettings( CoderSettingsState( dataDirectory = tmpdir.resolve("features").toString(), ), + context.logger, binaryName = "coder.bat", ), ) @@ -787,7 +862,8 @@ internal class CoderCLIManagerTest { } companion object { - private val tmpdir: Path = Path.of(System.getProperty("java.io.tmpdir")).resolve("coder-toolbox-test/cli-manager") + private val tmpdir: Path = + Path.of(System.getProperty("java.io.tmpdir")).resolve("coder-toolbox-test/cli-manager") @JvmStatic @BeforeAll diff --git a/src/test/kotlin/com/coder/toolbox/diagnostics/FakeLogger.kt b/src/test/kotlin/com/coder/toolbox/diagnostics/FakeLogger.kt new file mode 100644 index 0000000..abcbee1 --- /dev/null +++ b/src/test/kotlin/com/coder/toolbox/diagnostics/FakeLogger.kt @@ -0,0 +1,73 @@ +package com.coder.toolbox.diagnostics + +import com.jetbrains.toolbox.api.core.diagnostics.Logger + +class FakeLogger : Logger { + override fun error(exception: Throwable, message: () -> String) { + + } + + override fun error(exception: Throwable, message: String) { + } + + override fun error(message: () -> String) { + + } + + override fun error(message: String) { + + } + + override fun warn(exception: Throwable, message: () -> String) { + + } + + override fun warn(exception: Throwable, message: String) { + + } + + override fun warn(message: () -> String) { + + } + + override fun warn(message: String) { + + } + + override fun debug(exception: Throwable, message: () -> String) { + } + + override fun debug(exception: Throwable, message: String) { + } + + override fun debug(message: () -> String) { + } + + override fun debug(message: String) { + } + + override fun info(exception: Throwable, message: () -> String) { + } + + override fun info(exception: Throwable, message: String) { + } + + override fun info(message: () -> String) { + } + + override fun info(message: String) { + } + + override fun trace(exception: Throwable, message: () -> String) { + } + + override fun trace(exception: Throwable, message: String) { + } + + override fun trace(message: () -> String) { + } + + override fun trace(message: String) { + } + +} \ No newline at end of file diff --git a/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt b/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt index 53fd633..509e63a 100644 --- a/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt +++ b/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt @@ -1,5 +1,6 @@ package com.coder.toolbox.sdk +import com.coder.toolbox.CoderToolboxContext import com.coder.toolbox.sdk.convertors.InstantConverter import com.coder.toolbox.sdk.convertors.UUIDConverter import com.coder.toolbox.sdk.ex.APIResponseException @@ -15,6 +16,13 @@ import com.coder.toolbox.sdk.v2.models.WorkspacesResponse import com.coder.toolbox.settings.CoderSettings import com.coder.toolbox.settings.CoderSettingsState import com.coder.toolbox.util.sslContextFromPEMs +import com.jetbrains.toolbox.api.core.PluginSecretStore +import com.jetbrains.toolbox.api.core.PluginSettingsStore +import com.jetbrains.toolbox.api.core.diagnostics.Logger +import com.jetbrains.toolbox.api.localization.LocalizableStringFactory +import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette +import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager +import com.jetbrains.toolbox.api.ui.ToolboxUi import com.squareup.moshi.Moshi import com.squareup.moshi.Types import com.sun.net.httpserver.HttpExchange @@ -22,6 +30,8 @@ import com.sun.net.httpserver.HttpHandler import com.sun.net.httpserver.HttpServer import com.sun.net.httpserver.HttpsConfigurator import com.sun.net.httpserver.HttpsServer +import io.mockk.mockk +import kotlinx.coroutines.CoroutineScope import okio.buffer import okio.source import java.io.IOException @@ -81,6 +91,17 @@ class CoderRestClientTest { .add(UUIDConverter()) .build() + private val context = CoderToolboxContext( + mockk(), + mockk(), + mockk(), + mockk(), + mockk(relaxed = true), + mockk(), + mockk(), + mockk() + ) + data class TestWorkspace(var workspace: Workspace, var resources: List? = emptyList()) /** @@ -138,7 +159,7 @@ class CoderRestClientTest { ) tests.forEach { (endpoint, block) -> val (srv, url) = mockServer() - val client = CoderRestClient(URL(url), "token") + val client = CoderRestClient(context, URL(url), "token") srv.createContext( endpoint, BaseHttpHandler("GET") { exchange -> @@ -178,7 +199,7 @@ class CoderRestClientTest { }, ) - val client = CoderRestClient(URL(url), "token") + val client = CoderRestClient(context, URL(url), "token") assertEquals(user.username, client.me().username) val tests = listOf("invalid", null) @@ -186,7 +207,7 @@ class CoderRestClientTest { val ex = assertFailsWith( exceptionClass = APIResponseException::class, - block = { CoderRestClient(URL(url), token).me() }, + block = { CoderRestClient(context, URL(url), token).me() }, ) assertEquals(true, ex.isUnauthorized) } @@ -207,7 +228,7 @@ class CoderRestClientTest { ) tests.forEach { workspaces -> val (srv, url) = mockServer() - val client = CoderRestClient(URL(url), "token") + val client = CoderRestClient(context, URL(url), "token") srv.createContext( "/api/v2/workspaces", BaseHttpHandler("GET") { exchange -> @@ -229,31 +250,44 @@ class CoderRestClientTest { // Nothing, so no resources. emptyList(), // One workspace with an agent, but no resources. - listOf(TestWorkspace(DataGen.workspace("ws1", agents = mapOf("agent1" to "3f51da1d-306f-4a40-ac12-62bda5bc5f9a")))), + listOf( + TestWorkspace( + DataGen.workspace( + "ws1", + agents = mapOf("agent1" to "3f51da1d-306f-4a40-ac12-62bda5bc5f9a") + ) + ) + ), // One workspace with an agent and resources that do not match the agent. listOf( TestWorkspace( - workspace = DataGen.workspace("ws1", agents = mapOf("agent1" to "3f51da1d-306f-4a40-ac12-62bda5bc5f9a")), - resources = - listOf( - DataGen.resource("agent2", "968eea5e-8787-439d-88cd-5bc440216a34"), - DataGen.resource("agent3", "72fbc97b-952c-40c8-b1e5-7535f4407728"), + workspace = DataGen.workspace( + "ws1", + agents = mapOf("agent1" to "3f51da1d-306f-4a40-ac12-62bda5bc5f9a") ), + resources = + listOf( + DataGen.resource("agent2", "968eea5e-8787-439d-88cd-5bc440216a34"), + DataGen.resource("agent3", "72fbc97b-952c-40c8-b1e5-7535f4407728"), + ), ), ), // Multiple workspaces but only one has resources. listOf( TestWorkspace( - workspace = DataGen.workspace("ws1", agents = mapOf("agent1" to "3f51da1d-306f-4a40-ac12-62bda5bc5f9a")), + workspace = DataGen.workspace( + "ws1", + agents = mapOf("agent1" to "3f51da1d-306f-4a40-ac12-62bda5bc5f9a") + ), resources = emptyList(), ), TestWorkspace( workspace = DataGen.workspace("ws2"), resources = - listOf( - DataGen.resource("agent2", "968eea5e-8787-439d-88cd-5bc440216a34"), - DataGen.resource("agent3", "72fbc97b-952c-40c8-b1e5-7535f4407728"), - ), + listOf( + DataGen.resource("agent2", "968eea5e-8787-439d-88cd-5bc440216a34"), + DataGen.resource("agent3", "72fbc97b-952c-40c8-b1e5-7535f4407728"), + ), ), TestWorkspace( workspace = DataGen.workspace("ws3"), @@ -265,14 +299,15 @@ class CoderRestClientTest { val resourceEndpoint = "([^/]+)/resources".toRegex() tests.forEach { workspaces -> val (srv, url) = mockServer() - val client = CoderRestClient(URL(url), "token") + val client = CoderRestClient(context, URL(url), "token") srv.createContext( "/api/v2/templateversions", BaseHttpHandler("GET") { exchange -> val matches = resourceEndpoint.find(exchange.requestURI.path) if (matches != null) { val templateVersionId = UUID.fromString(matches.destructured.toList()[0]) - val ws = workspaces.firstOrNull { it.workspace.latestBuild.templateVersionID == templateVersionId } + val ws = + workspaces.firstOrNull { it.workspace.latestBuild.templateVersionID == templateVersionId } if (ws != null) { val body = moshi.adapter>( @@ -301,7 +336,7 @@ class CoderRestClientTest { val actions = mutableListOf>() val (srv, url) = mockServer() - val client = CoderRestClient(URL(url), "token") + val client = CoderRestClient(context, URL(url), "token") val templateEndpoint = "/api/v2/templates/([^/]+)".toRegex() srv.createContext( "/api/v2/templates", @@ -326,7 +361,8 @@ class CoderRestClientTest { val buildMatch = buildEndpoint.find(exchange.requestURI.path) if (buildMatch != null) { val workspaceId = UUID.fromString(buildMatch.destructured.toList()[0]) - val json = moshi.adapter(CreateWorkspaceBuildRequest::class.java).fromJson(exchange.requestBody.source().buffer()) + val json = moshi.adapter(CreateWorkspaceBuildRequest::class.java) + .fromJson(exchange.requestBody.source().buffer()) if (json == null) { val response = Response("No body", "No body for create workspace build request") val body = moshi.adapter(Response::class.java).toJson(response).toByteArray() @@ -398,10 +434,11 @@ class CoderRestClientTest { tlsCAPath = Path.of("src/test/fixtures/tls", "self-signed.crt").toString(), tlsAlternateHostname = "localhost", ), + context.logger ) val user = DataGen.user() val (srv, url) = mockTLSServer("self-signed") - val client = CoderRestClient(URL(url), "token", settings) + val client = CoderRestClient(context, URL(url), "token", settings) srv.createContext( "/api/v2/users/me", BaseHttpHandler("GET") { exchange -> @@ -424,9 +461,10 @@ class CoderRestClientTest { tlsCAPath = Path.of("src/test/fixtures/tls", "self-signed.crt").toString(), tlsAlternateHostname = "fake.example.com", ), + context.logger ) val (srv, url) = mockTLSServer("self-signed") - val client = CoderRestClient(URL(url), "token", settings) + val client = CoderRestClient(context, URL(url), "token", settings) assertFailsWith( exceptionClass = SSLPeerUnverifiedException::class, @@ -443,9 +481,10 @@ class CoderRestClientTest { CoderSettingsState( tlsCAPath = Path.of("src/test/fixtures/tls", "self-signed.crt").toString(), ), + context.logger ) val (srv, url) = mockTLSServer("no-signing") - val client = CoderRestClient(URL(url), "token", settings) + val client = CoderRestClient(context, URL(url), "token", settings) assertFailsWith( exceptionClass = SSLHandshakeException::class, @@ -462,10 +501,11 @@ class CoderRestClientTest { CoderSettingsState( tlsCAPath = Path.of("src/test/fixtures/tls", "chain-root.crt").toString(), ), + context.logger ) val user = DataGen.user() val (srv, url) = mockTLSServer("chain") - val client = CoderRestClient(URL(url), "token", settings) + val client = CoderRestClient(context, URL(url), "token", settings) srv.createContext( "/api/v2/users/me", BaseHttpHandler("GET") { exchange -> @@ -482,7 +522,7 @@ class CoderRestClientTest { @Test fun usesProxy() { - val settings = CoderSettings(CoderSettingsState()) + val settings = CoderSettings(CoderSettingsState(), context.logger) val workspaces = listOf(DataGen.workspace("ws1")) val (srv1, url1) = mockServer() srv1.createContext( @@ -497,6 +537,7 @@ class CoderRestClientTest { val srv2 = mockProxy() val client = CoderRestClient( + context, URL(url1), "token", settings, @@ -505,7 +546,8 @@ class CoderRestClientTest { "bar", true, object : ProxySelector() { - override fun select(uri: URI): List = listOf(Proxy(Proxy.Type.HTTP, InetSocketAddress("localhost", srv2.address.port))) + override fun select(uri: URI): List = + listOf(Proxy(Proxy.Type.HTTP, InetSocketAddress("localhost", srv2.address.port))) override fun connectFailed( uri: URI, diff --git a/src/test/kotlin/com/coder/toolbox/settings/CoderSettingsTest.kt b/src/test/kotlin/com/coder/toolbox/settings/CoderSettingsTest.kt index 7e6e3f1..6a3e69e 100644 --- a/src/test/kotlin/com/coder/toolbox/settings/CoderSettingsTest.kt +++ b/src/test/kotlin/com/coder/toolbox/settings/CoderSettingsTest.kt @@ -3,6 +3,8 @@ package com.coder.toolbox.settings import com.coder.toolbox.util.OS import com.coder.toolbox.util.getOS import com.coder.toolbox.util.withPath +import com.jetbrains.toolbox.api.core.diagnostics.Logger +import io.mockk.mockk import java.net.URL import java.nio.file.Path import kotlin.test.Test @@ -11,10 +13,12 @@ import kotlin.test.assertEquals import kotlin.test.assertNotEquals internal class CoderSettingsTest { + private val logger = mockk(relaxed = true) + @Test fun testExpands() { val state = CoderSettingsState() - val settings = CoderSettings(state) + val settings = CoderSettings(state, logger) val url = URL("http://localhost") val home = Path.of(System.getProperty("user.home")) @@ -33,9 +37,8 @@ internal class CoderSettingsTest { val url = URL("http://localhost") var settings = CoderSettings( - state, - env = - Environment( + state, logger, + env = Environment( mapOf( "LOCALAPPDATA" to "/tmp/coder-toolbox-test/localappdata", "HOME" to "/tmp/coder-toolbox-test/home", @@ -57,14 +60,14 @@ internal class CoderSettingsTest { if (getOS() == OS.LINUX) { settings = CoderSettings( - state, + state, logger, env = - Environment( - mapOf( - "XDG_DATA_HOME" to "", - "HOME" to "/tmp/coder-toolbox-test/home", + Environment( + mapOf( + "XDG_DATA_HOME" to "", + "HOME" to "/tmp/coder-toolbox-test/home", + ), ), - ), ) expected = "/tmp/coder-toolbox-test/home/.local/share/coder-toolbox/localhost" @@ -76,15 +79,15 @@ internal class CoderSettingsTest { state.dataDirectory = "/tmp/coder-toolbox-test/data-dir" settings = CoderSettings( - state, + state, logger, env = - Environment( - mapOf( - "LOCALAPPDATA" to "/ignore", - "HOME" to "/ignore", - "XDG_DATA_HOME" to "/ignore", + Environment( + mapOf( + "LOCALAPPDATA" to "/ignore", + "HOME" to "/ignore", + "XDG_DATA_HOME" to "/ignore", + ), ), - ), ) expected = "/tmp/coder-toolbox-test/data-dir/localhost" assertEquals(Path.of(expected).toAbsolutePath(), settings.dataDir(url)) @@ -93,7 +96,7 @@ internal class CoderSettingsTest { // Check that the URL is encoded and includes the port, also omit environment. val newUrl = URL("https://dev.😉-coder.com:8080") state.dataDirectory = "/tmp/coder-toolbox-test/data-dir" - settings = CoderSettings(state) + settings = CoderSettings(state, logger) expected = "/tmp/coder-toolbox-test/data-dir/dev.xn---coder-vx74e.com-8080" assertEquals(Path.of(expected).toAbsolutePath(), settings.dataDir(newUrl)) assertEquals(Path.of(expected).toAbsolutePath(), settings.binPath(newUrl).parent) @@ -102,8 +105,8 @@ internal class CoderSettingsTest { @Test fun testBinPath() { val state = CoderSettingsState() - val settings = CoderSettings(state) - val settings2 = CoderSettings(state, binaryName = "foo-bar.baz") + val settings = CoderSettings(state, logger) + val settings2 = CoderSettings(state, logger, binaryName = "foo-bar.baz") // The binary path should fall back to the data directory but that is // already tested in the data directory tests. val url = URL("http://localhost") @@ -129,15 +132,15 @@ internal class CoderSettingsTest { val state = CoderSettingsState() var settings = CoderSettings( - state, + state, logger, env = - Environment( - mapOf( - "APPDATA" to "/tmp/coder-toolbox-test/cli-appdata", - "HOME" to "/tmp/coder-toolbox-test/cli-home", - "XDG_CONFIG_HOME" to "/tmp/coder-toolbox-test/cli-xdg-config", + Environment( + mapOf( + "APPDATA" to "/tmp/coder-toolbox-test/cli-appdata", + "HOME" to "/tmp/coder-toolbox-test/cli-home", + "XDG_CONFIG_HOME" to "/tmp/coder-toolbox-test/cli-xdg-config", + ), ), - ), ) var expected = when (getOS()) { @@ -151,14 +154,14 @@ internal class CoderSettingsTest { if (getOS() == OS.LINUX) { settings = CoderSettings( - state, + state, logger, env = - Environment( - mapOf( - "XDG_CONFIG_HOME" to "", - "HOME" to "/tmp/coder-toolbox-test/cli-home", + Environment( + mapOf( + "XDG_CONFIG_HOME" to "", + "HOME" to "/tmp/coder-toolbox-test/cli-home", + ), ), - ), ) expected = "/tmp/coder-toolbox-test/cli-home/.config/coderv2" assertEquals(Path.of(expected), settings.coderConfigDir) @@ -167,16 +170,16 @@ internal class CoderSettingsTest { // Read CODER_CONFIG_DIR. settings = CoderSettings( - state, + state, logger, env = - Environment( - mapOf( - "CODER_CONFIG_DIR" to "/tmp/coder-toolbox-test/coder-config-dir", - "APPDATA" to "/ignore", - "HOME" to "/ignore", - "XDG_CONFIG_HOME" to "/ignore", + Environment( + mapOf( + "CODER_CONFIG_DIR" to "/tmp/coder-toolbox-test/coder-config-dir", + "APPDATA" to "/ignore", + "HOME" to "/ignore", + "XDG_CONFIG_HOME" to "/ignore", + ), ), - ), ) expected = "/tmp/coder-toolbox-test/coder-config-dir" assertEquals(Path.of(expected), settings.coderConfigDir) @@ -185,7 +188,7 @@ internal class CoderSettingsTest { @Test fun binSource() { val state = CoderSettingsState() - val settings = CoderSettings(state) + val settings = CoderSettings(state, logger) // As-is if no source override. val url = URL("http://localhost/") assertContains( @@ -212,23 +215,24 @@ internal class CoderSettingsTest { expected.resolve("url").toFile().writeText("http://test.toolbox.coder.com$expected") expected.resolve("session").toFile().writeText("fake-token") - var got = CoderSettings(CoderSettingsState()).readConfig(expected) + var got = CoderSettings(CoderSettingsState(), logger).readConfig(expected) assertEquals(Pair("http://test.toolbox.coder.com$expected", "fake-token"), got) // Ignore token if missing. expected.resolve("session").toFile().delete() - got = CoderSettings(CoderSettingsState()).readConfig(expected) + got = CoderSettings(CoderSettingsState(), logger).readConfig(expected) assertEquals(Pair("http://test.toolbox.coder.com$expected", null), got) } @Test fun testSSHConfigOptions() { - var settings = CoderSettings(CoderSettingsState(sshConfigOptions = "ssh config options from state")) + var settings = CoderSettings(CoderSettingsState(sshConfigOptions = "ssh config options from state"), logger) assertEquals("ssh config options from state", settings.sshConfigOptions) settings = CoderSettings( CoderSettingsState(), + logger, env = Environment(mapOf(CODER_SSH_CONFIG_OPTIONS to "ssh config options from env")), ) assertEquals("ssh config options from env", settings.sshConfigOptions) @@ -237,6 +241,7 @@ internal class CoderSettingsTest { settings = CoderSettings( CoderSettingsState(sshConfigOptions = "ssh config options from state"), + logger, env = Environment(mapOf(CODER_SSH_CONFIG_OPTIONS to "ssh config options from env")), ) assertEquals("ssh config options from state", settings.sshConfigOptions) @@ -244,16 +249,16 @@ internal class CoderSettingsTest { @Test fun testRequireTokenAuth() { - var settings = CoderSettings(CoderSettingsState()) + var settings = CoderSettings(CoderSettingsState(), logger) assertEquals(true, settings.requireTokenAuth) - settings = CoderSettings(CoderSettingsState(tlsCertPath = "cert path")) + settings = CoderSettings(CoderSettingsState(tlsCertPath = "cert path"), logger) assertEquals(true, settings.requireTokenAuth) - settings = CoderSettings(CoderSettingsState(tlsKeyPath = "key path")) + settings = CoderSettings(CoderSettingsState(tlsKeyPath = "key path"), logger) assertEquals(true, settings.requireTokenAuth) - settings = CoderSettings(CoderSettingsState(tlsCertPath = "cert path", tlsKeyPath = "key path")) + settings = CoderSettings(CoderSettingsState(tlsCertPath = "cert path", tlsKeyPath = "key path"), logger) assertEquals(false, settings.requireTokenAuth) } @@ -265,14 +270,14 @@ internal class CoderSettingsTest { dir.toFile().deleteRecursively() // No config. - var settings = CoderSettings(CoderSettingsState(), env = env) + var settings = CoderSettings(CoderSettingsState(), logger, env = env) assertEquals(null, settings.defaultURL()) // Read from global config. val globalConfigPath = settings.coderConfigDir globalConfigPath.toFile().mkdirs() globalConfigPath.resolve("url").toFile().writeText("url-from-global-config") - settings = CoderSettings(CoderSettingsState(), env = env) + settings = CoderSettings(CoderSettingsState(), logger, env = env) assertEquals("url-from-global-config" to Source.CONFIG, settings.defaultURL()) // Read from environment. @@ -283,7 +288,7 @@ internal class CoderSettingsTest { "CODER_CONFIG_DIR" to dir.toString(), ), ) - settings = CoderSettings(CoderSettingsState(), env = env) + settings = CoderSettings(CoderSettingsState(), logger, env = env) assertEquals("url-from-env" to Source.ENVIRONMENT, settings.defaultURL()) // Read from settings. @@ -292,6 +297,7 @@ internal class CoderSettingsTest { CoderSettingsState( defaultURL = "url-from-settings", ), + logger, env = env, ) assertEquals("url-from-settings" to Source.SETTINGS, settings.defaultURL()) @@ -314,7 +320,7 @@ internal class CoderSettingsTest { dir.toFile().deleteRecursively() // No config. - var settings = CoderSettings(CoderSettingsState(), env = env) + var settings = CoderSettings(CoderSettingsState(), logger, env = env) assertEquals(null, settings.token(url)) val globalConfigPath = settings.coderConfigDir @@ -349,6 +355,7 @@ internal class CoderSettingsTest { tlsKeyPath = "key", tlsCertPath = "cert", ), + logger, env = env, ) assertEquals(null, settings.token(url)) @@ -357,7 +364,7 @@ internal class CoderSettingsTest { @Test fun testDefaults() { // Test defaults for the remaining settings. - val settings = CoderSettings(CoderSettingsState()) + val settings = CoderSettings(CoderSettingsState(), logger) assertEquals(true, settings.enableDownloads) assertEquals(false, settings.enableBinaryDirectoryFallback) assertEquals("", settings.headerCommand) @@ -388,6 +395,7 @@ internal class CoderSettingsTest { ignoreSetupFailure = true, sshLogDirectory = "test ssh log directory", ), + logger, ) assertEquals(false, settings.enableDownloads) From 477d6ec070a1bd1bafdde91ba7ad3572308d6ac0 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Thu, 6 Mar 2025 23:07:37 +0200 Subject: [PATCH 12/14] fix: non-code files needed at runtime should be under resources/ folder - in Java/Kotlin, the resources folder is a directory used to store non-code files that are needed by an application at runtime. - in Maven/Gradle world, the resources folder is typically located under: * src/main/resources/ * src/test/resources/ --- .../com/coder/toolbox/cli/CoderCLIManagerTest.kt | 8 ++++---- .../com/coder/toolbox/sdk/CoderRestClientTest.kt | 12 ++++++------ .../fixtures/inputs/blank-newlines.conf | 0 src/test/{ => resources}/fixtures/inputs/blank.conf | 0 .../fixtures/inputs/existing-end-no-newline.conf | 0 .../fixtures/inputs/existing-end.conf | 0 .../inputs/existing-middle-and-unrelated.conf | 0 .../fixtures/inputs/existing-middle.conf | 0 .../fixtures/inputs/existing-only.conf | 0 .../fixtures/inputs/existing-start.conf | 0 .../fixtures/inputs/malformed-mismatched-start.conf | 0 .../fixtures/inputs/malformed-no-end.conf | 0 .../fixtures/inputs/malformed-no-start.conf | 0 .../fixtures/inputs/malformed-start-after-end.conf | 0 .../{ => resources}/fixtures/inputs/no-blocks.conf | 0 .../{ => resources}/fixtures/inputs/no-newline.conf | 0 .../fixtures/inputs/no-related-blocks.conf | 0 .../fixtures/outputs/append-blank-newlines.conf | 0 .../fixtures/outputs/append-blank.conf | 0 .../fixtures/outputs/append-no-blocks.conf | 0 .../fixtures/outputs/append-no-newline.conf | 0 .../fixtures/outputs/append-no-related-blocks.conf | 0 .../fixtures/outputs/disable-autostart.conf | 0 .../fixtures/outputs/extra-config.conf | 0 .../fixtures/outputs/header-command-windows.conf | 0 .../fixtures/outputs/header-command.conf | 0 .../{ => resources}/fixtures/outputs/log-dir.conf | 0 .../fixtures/outputs/multiple-workspaces.conf | 0 .../fixtures/outputs/no-disable-autostart.conf | 0 .../fixtures/outputs/no-report-usage.conf | 0 .../fixtures/outputs/replace-end-no-newline.conf | 0 .../fixtures/outputs/replace-end.conf | 0 .../outputs/replace-middle-ignore-unrelated.conf | 0 .../fixtures/outputs/replace-middle.conf | 0 .../fixtures/outputs/replace-only.conf | 0 .../fixtures/outputs/replace-start.conf | 0 .../fixtures/tls/chain-intermediate.crt | 0 .../fixtures/tls/chain-intermediate.key | 0 src/test/{ => resources}/fixtures/tls/chain-leaf.crt | 0 src/test/{ => resources}/fixtures/tls/chain-leaf.key | 0 src/test/{ => resources}/fixtures/tls/chain-root.crt | 0 src/test/{ => resources}/fixtures/tls/chain-root.key | 0 src/test/{ => resources}/fixtures/tls/chain.crt | 0 src/test/{ => resources}/fixtures/tls/chain.key | 0 src/test/{ => resources}/fixtures/tls/generate.bash | 0 src/test/{ => resources}/fixtures/tls/no-signing.crt | 0 src/test/{ => resources}/fixtures/tls/no-signing.key | 0 .../{ => resources}/fixtures/tls/self-signed.crt | 0 .../{ => resources}/fixtures/tls/self-signed.key | 0 49 files changed, 10 insertions(+), 10 deletions(-) rename src/test/{ => resources}/fixtures/inputs/blank-newlines.conf (100%) rename src/test/{ => resources}/fixtures/inputs/blank.conf (100%) rename src/test/{ => resources}/fixtures/inputs/existing-end-no-newline.conf (100%) rename src/test/{ => resources}/fixtures/inputs/existing-end.conf (100%) rename src/test/{ => resources}/fixtures/inputs/existing-middle-and-unrelated.conf (100%) rename src/test/{ => resources}/fixtures/inputs/existing-middle.conf (100%) rename src/test/{ => resources}/fixtures/inputs/existing-only.conf (100%) rename src/test/{ => resources}/fixtures/inputs/existing-start.conf (100%) rename src/test/{ => resources}/fixtures/inputs/malformed-mismatched-start.conf (100%) rename src/test/{ => resources}/fixtures/inputs/malformed-no-end.conf (100%) rename src/test/{ => resources}/fixtures/inputs/malformed-no-start.conf (100%) rename src/test/{ => resources}/fixtures/inputs/malformed-start-after-end.conf (100%) rename src/test/{ => resources}/fixtures/inputs/no-blocks.conf (100%) rename src/test/{ => resources}/fixtures/inputs/no-newline.conf (100%) rename src/test/{ => resources}/fixtures/inputs/no-related-blocks.conf (100%) rename src/test/{ => resources}/fixtures/outputs/append-blank-newlines.conf (100%) rename src/test/{ => resources}/fixtures/outputs/append-blank.conf (100%) rename src/test/{ => resources}/fixtures/outputs/append-no-blocks.conf (100%) rename src/test/{ => resources}/fixtures/outputs/append-no-newline.conf (100%) rename src/test/{ => resources}/fixtures/outputs/append-no-related-blocks.conf (100%) rename src/test/{ => resources}/fixtures/outputs/disable-autostart.conf (100%) rename src/test/{ => resources}/fixtures/outputs/extra-config.conf (100%) rename src/test/{ => resources}/fixtures/outputs/header-command-windows.conf (100%) rename src/test/{ => resources}/fixtures/outputs/header-command.conf (100%) rename src/test/{ => resources}/fixtures/outputs/log-dir.conf (100%) rename src/test/{ => resources}/fixtures/outputs/multiple-workspaces.conf (100%) rename src/test/{ => resources}/fixtures/outputs/no-disable-autostart.conf (100%) rename src/test/{ => resources}/fixtures/outputs/no-report-usage.conf (100%) rename src/test/{ => resources}/fixtures/outputs/replace-end-no-newline.conf (100%) rename src/test/{ => resources}/fixtures/outputs/replace-end.conf (100%) rename src/test/{ => resources}/fixtures/outputs/replace-middle-ignore-unrelated.conf (100%) rename src/test/{ => resources}/fixtures/outputs/replace-middle.conf (100%) rename src/test/{ => resources}/fixtures/outputs/replace-only.conf (100%) rename src/test/{ => resources}/fixtures/outputs/replace-start.conf (100%) rename src/test/{ => resources}/fixtures/tls/chain-intermediate.crt (100%) rename src/test/{ => resources}/fixtures/tls/chain-intermediate.key (100%) rename src/test/{ => resources}/fixtures/tls/chain-leaf.crt (100%) rename src/test/{ => resources}/fixtures/tls/chain-leaf.key (100%) rename src/test/{ => resources}/fixtures/tls/chain-root.crt (100%) rename src/test/{ => resources}/fixtures/tls/chain-root.key (100%) rename src/test/{ => resources}/fixtures/tls/chain.crt (100%) rename src/test/{ => resources}/fixtures/tls/chain.key (100%) rename src/test/{ => resources}/fixtures/tls/generate.bash (100%) rename src/test/{ => resources}/fixtures/tls/no-signing.crt (100%) rename src/test/{ => resources}/fixtures/tls/no-signing.key (100%) rename src/test/{ => resources}/fixtures/tls/self-signed.crt (100%) rename src/test/{ => resources}/fixtures/tls/self-signed.key (100%) diff --git a/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt b/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt index 131cab9..87b659a 100644 --- a/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt +++ b/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt @@ -455,7 +455,7 @@ internal class CoderCLIManagerTest { if (it.input != null) { settings.sshConfigPath.parent.toFile().mkdirs() val originalConf = - Path.of("src/test/fixtures/inputs").resolve(it.input + ".conf").toFile().readText() + Path.of("src/test/resources/fixtures/inputs").resolve(it.input + ".conf").toFile().readText() .replace(newlineRe, System.lineSeparator()) settings.sshConfigPath.toFile().writeText(originalConf) } @@ -463,7 +463,7 @@ internal class CoderCLIManagerTest { // Output is the configuration we expect to have after configuring. val coderConfigPath = ccm.localBinaryPath.parent.resolve("config") val expectedConf = - Path.of("src/test/fixtures/outputs/").resolve(it.output + ".conf").toFile().readText() + Path.of("src/test/resources/fixtures/outputs/").resolve(it.output + ".conf").toFile().readText() .replace(newlineRe, System.lineSeparator()) .replace("/tmp/coder-toolbox/test.coder.invalid/config", escape(coderConfigPath.toString())) .replace( @@ -494,7 +494,7 @@ internal class CoderCLIManagerTest { // Remove is the configuration we expect after removing. assertEquals( settings.sshConfigPath.toFile().readText(), - Path.of("src/test/fixtures/inputs").resolve(it.remove + ".conf").toFile() + Path.of("src/test/resources/fixtures/inputs").resolve(it.remove + ".conf").toFile() .readText().replace(newlineRe, System.lineSeparator()), ) } @@ -518,7 +518,7 @@ internal class CoderCLIManagerTest { sshConfigPath = tmpdir.resolve("configured$it.conf"), ) settings.sshConfigPath.parent.toFile().mkdirs() - Path.of("src/test/fixtures/inputs").resolve("$it.conf").toFile().copyTo( + Path.of("src/test/resources/fixtures/inputs").resolve("$it.conf").toFile().copyTo( settings.sshConfigPath.toFile(), true, ) diff --git a/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt b/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt index 509e63a..78a2ea1 100644 --- a/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt +++ b/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt @@ -117,8 +117,8 @@ class CoderRestClientTest { val srv = HttpsServer.create(InetSocketAddress(0), 0) val sslContext = sslContextFromPEMs( - Path.of("src/test/fixtures/tls", "$certName.crt").toString(), - Path.of("src/test/fixtures/tls", "$certName.key").toString(), + Path.of("src/test/resources/fixtures/tls", "$certName.crt").toString(), + Path.of("src/test/resources/fixtures/tls", "$certName.key").toString(), "", ) srv.httpsConfigurator = HttpsConfigurator(sslContext) @@ -431,7 +431,7 @@ class CoderRestClientTest { val settings = CoderSettings( CoderSettingsState( - tlsCAPath = Path.of("src/test/fixtures/tls", "self-signed.crt").toString(), + tlsCAPath = Path.of("src/test/resources/fixtures/tls", "self-signed.crt").toString(), tlsAlternateHostname = "localhost", ), context.logger @@ -458,7 +458,7 @@ class CoderRestClientTest { val settings = CoderSettings( CoderSettingsState( - tlsCAPath = Path.of("src/test/fixtures/tls", "self-signed.crt").toString(), + tlsCAPath = Path.of("src/test/resources/fixtures/tls", "self-signed.crt").toString(), tlsAlternateHostname = "fake.example.com", ), context.logger @@ -479,7 +479,7 @@ class CoderRestClientTest { val settings = CoderSettings( CoderSettingsState( - tlsCAPath = Path.of("src/test/fixtures/tls", "self-signed.crt").toString(), + tlsCAPath = Path.of("src/test/resources/fixtures/tls", "self-signed.crt").toString(), ), context.logger ) @@ -499,7 +499,7 @@ class CoderRestClientTest { val settings = CoderSettings( CoderSettingsState( - tlsCAPath = Path.of("src/test/fixtures/tls", "chain-root.crt").toString(), + tlsCAPath = Path.of("src/test/resources/fixtures/tls", "chain-root.crt").toString(), ), context.logger ) diff --git a/src/test/fixtures/inputs/blank-newlines.conf b/src/test/resources/fixtures/inputs/blank-newlines.conf similarity index 100% rename from src/test/fixtures/inputs/blank-newlines.conf rename to src/test/resources/fixtures/inputs/blank-newlines.conf diff --git a/src/test/fixtures/inputs/blank.conf b/src/test/resources/fixtures/inputs/blank.conf similarity index 100% rename from src/test/fixtures/inputs/blank.conf rename to src/test/resources/fixtures/inputs/blank.conf diff --git a/src/test/fixtures/inputs/existing-end-no-newline.conf b/src/test/resources/fixtures/inputs/existing-end-no-newline.conf similarity index 100% rename from src/test/fixtures/inputs/existing-end-no-newline.conf rename to src/test/resources/fixtures/inputs/existing-end-no-newline.conf diff --git a/src/test/fixtures/inputs/existing-end.conf b/src/test/resources/fixtures/inputs/existing-end.conf similarity index 100% rename from src/test/fixtures/inputs/existing-end.conf rename to src/test/resources/fixtures/inputs/existing-end.conf diff --git a/src/test/fixtures/inputs/existing-middle-and-unrelated.conf b/src/test/resources/fixtures/inputs/existing-middle-and-unrelated.conf similarity index 100% rename from src/test/fixtures/inputs/existing-middle-and-unrelated.conf rename to src/test/resources/fixtures/inputs/existing-middle-and-unrelated.conf diff --git a/src/test/fixtures/inputs/existing-middle.conf b/src/test/resources/fixtures/inputs/existing-middle.conf similarity index 100% rename from src/test/fixtures/inputs/existing-middle.conf rename to src/test/resources/fixtures/inputs/existing-middle.conf diff --git a/src/test/fixtures/inputs/existing-only.conf b/src/test/resources/fixtures/inputs/existing-only.conf similarity index 100% rename from src/test/fixtures/inputs/existing-only.conf rename to src/test/resources/fixtures/inputs/existing-only.conf diff --git a/src/test/fixtures/inputs/existing-start.conf b/src/test/resources/fixtures/inputs/existing-start.conf similarity index 100% rename from src/test/fixtures/inputs/existing-start.conf rename to src/test/resources/fixtures/inputs/existing-start.conf diff --git a/src/test/fixtures/inputs/malformed-mismatched-start.conf b/src/test/resources/fixtures/inputs/malformed-mismatched-start.conf similarity index 100% rename from src/test/fixtures/inputs/malformed-mismatched-start.conf rename to src/test/resources/fixtures/inputs/malformed-mismatched-start.conf diff --git a/src/test/fixtures/inputs/malformed-no-end.conf b/src/test/resources/fixtures/inputs/malformed-no-end.conf similarity index 100% rename from src/test/fixtures/inputs/malformed-no-end.conf rename to src/test/resources/fixtures/inputs/malformed-no-end.conf diff --git a/src/test/fixtures/inputs/malformed-no-start.conf b/src/test/resources/fixtures/inputs/malformed-no-start.conf similarity index 100% rename from src/test/fixtures/inputs/malformed-no-start.conf rename to src/test/resources/fixtures/inputs/malformed-no-start.conf diff --git a/src/test/fixtures/inputs/malformed-start-after-end.conf b/src/test/resources/fixtures/inputs/malformed-start-after-end.conf similarity index 100% rename from src/test/fixtures/inputs/malformed-start-after-end.conf rename to src/test/resources/fixtures/inputs/malformed-start-after-end.conf diff --git a/src/test/fixtures/inputs/no-blocks.conf b/src/test/resources/fixtures/inputs/no-blocks.conf similarity index 100% rename from src/test/fixtures/inputs/no-blocks.conf rename to src/test/resources/fixtures/inputs/no-blocks.conf diff --git a/src/test/fixtures/inputs/no-newline.conf b/src/test/resources/fixtures/inputs/no-newline.conf similarity index 100% rename from src/test/fixtures/inputs/no-newline.conf rename to src/test/resources/fixtures/inputs/no-newline.conf diff --git a/src/test/fixtures/inputs/no-related-blocks.conf b/src/test/resources/fixtures/inputs/no-related-blocks.conf similarity index 100% rename from src/test/fixtures/inputs/no-related-blocks.conf rename to src/test/resources/fixtures/inputs/no-related-blocks.conf diff --git a/src/test/fixtures/outputs/append-blank-newlines.conf b/src/test/resources/fixtures/outputs/append-blank-newlines.conf similarity index 100% rename from src/test/fixtures/outputs/append-blank-newlines.conf rename to src/test/resources/fixtures/outputs/append-blank-newlines.conf diff --git a/src/test/fixtures/outputs/append-blank.conf b/src/test/resources/fixtures/outputs/append-blank.conf similarity index 100% rename from src/test/fixtures/outputs/append-blank.conf rename to src/test/resources/fixtures/outputs/append-blank.conf diff --git a/src/test/fixtures/outputs/append-no-blocks.conf b/src/test/resources/fixtures/outputs/append-no-blocks.conf similarity index 100% rename from src/test/fixtures/outputs/append-no-blocks.conf rename to src/test/resources/fixtures/outputs/append-no-blocks.conf diff --git a/src/test/fixtures/outputs/append-no-newline.conf b/src/test/resources/fixtures/outputs/append-no-newline.conf similarity index 100% rename from src/test/fixtures/outputs/append-no-newline.conf rename to src/test/resources/fixtures/outputs/append-no-newline.conf diff --git a/src/test/fixtures/outputs/append-no-related-blocks.conf b/src/test/resources/fixtures/outputs/append-no-related-blocks.conf similarity index 100% rename from src/test/fixtures/outputs/append-no-related-blocks.conf rename to src/test/resources/fixtures/outputs/append-no-related-blocks.conf diff --git a/src/test/fixtures/outputs/disable-autostart.conf b/src/test/resources/fixtures/outputs/disable-autostart.conf similarity index 100% rename from src/test/fixtures/outputs/disable-autostart.conf rename to src/test/resources/fixtures/outputs/disable-autostart.conf diff --git a/src/test/fixtures/outputs/extra-config.conf b/src/test/resources/fixtures/outputs/extra-config.conf similarity index 100% rename from src/test/fixtures/outputs/extra-config.conf rename to src/test/resources/fixtures/outputs/extra-config.conf diff --git a/src/test/fixtures/outputs/header-command-windows.conf b/src/test/resources/fixtures/outputs/header-command-windows.conf similarity index 100% rename from src/test/fixtures/outputs/header-command-windows.conf rename to src/test/resources/fixtures/outputs/header-command-windows.conf diff --git a/src/test/fixtures/outputs/header-command.conf b/src/test/resources/fixtures/outputs/header-command.conf similarity index 100% rename from src/test/fixtures/outputs/header-command.conf rename to src/test/resources/fixtures/outputs/header-command.conf diff --git a/src/test/fixtures/outputs/log-dir.conf b/src/test/resources/fixtures/outputs/log-dir.conf similarity index 100% rename from src/test/fixtures/outputs/log-dir.conf rename to src/test/resources/fixtures/outputs/log-dir.conf diff --git a/src/test/fixtures/outputs/multiple-workspaces.conf b/src/test/resources/fixtures/outputs/multiple-workspaces.conf similarity index 100% rename from src/test/fixtures/outputs/multiple-workspaces.conf rename to src/test/resources/fixtures/outputs/multiple-workspaces.conf diff --git a/src/test/fixtures/outputs/no-disable-autostart.conf b/src/test/resources/fixtures/outputs/no-disable-autostart.conf similarity index 100% rename from src/test/fixtures/outputs/no-disable-autostart.conf rename to src/test/resources/fixtures/outputs/no-disable-autostart.conf diff --git a/src/test/fixtures/outputs/no-report-usage.conf b/src/test/resources/fixtures/outputs/no-report-usage.conf similarity index 100% rename from src/test/fixtures/outputs/no-report-usage.conf rename to src/test/resources/fixtures/outputs/no-report-usage.conf diff --git a/src/test/fixtures/outputs/replace-end-no-newline.conf b/src/test/resources/fixtures/outputs/replace-end-no-newline.conf similarity index 100% rename from src/test/fixtures/outputs/replace-end-no-newline.conf rename to src/test/resources/fixtures/outputs/replace-end-no-newline.conf diff --git a/src/test/fixtures/outputs/replace-end.conf b/src/test/resources/fixtures/outputs/replace-end.conf similarity index 100% rename from src/test/fixtures/outputs/replace-end.conf rename to src/test/resources/fixtures/outputs/replace-end.conf diff --git a/src/test/fixtures/outputs/replace-middle-ignore-unrelated.conf b/src/test/resources/fixtures/outputs/replace-middle-ignore-unrelated.conf similarity index 100% rename from src/test/fixtures/outputs/replace-middle-ignore-unrelated.conf rename to src/test/resources/fixtures/outputs/replace-middle-ignore-unrelated.conf diff --git a/src/test/fixtures/outputs/replace-middle.conf b/src/test/resources/fixtures/outputs/replace-middle.conf similarity index 100% rename from src/test/fixtures/outputs/replace-middle.conf rename to src/test/resources/fixtures/outputs/replace-middle.conf diff --git a/src/test/fixtures/outputs/replace-only.conf b/src/test/resources/fixtures/outputs/replace-only.conf similarity index 100% rename from src/test/fixtures/outputs/replace-only.conf rename to src/test/resources/fixtures/outputs/replace-only.conf diff --git a/src/test/fixtures/outputs/replace-start.conf b/src/test/resources/fixtures/outputs/replace-start.conf similarity index 100% rename from src/test/fixtures/outputs/replace-start.conf rename to src/test/resources/fixtures/outputs/replace-start.conf diff --git a/src/test/fixtures/tls/chain-intermediate.crt b/src/test/resources/fixtures/tls/chain-intermediate.crt similarity index 100% rename from src/test/fixtures/tls/chain-intermediate.crt rename to src/test/resources/fixtures/tls/chain-intermediate.crt diff --git a/src/test/fixtures/tls/chain-intermediate.key b/src/test/resources/fixtures/tls/chain-intermediate.key similarity index 100% rename from src/test/fixtures/tls/chain-intermediate.key rename to src/test/resources/fixtures/tls/chain-intermediate.key diff --git a/src/test/fixtures/tls/chain-leaf.crt b/src/test/resources/fixtures/tls/chain-leaf.crt similarity index 100% rename from src/test/fixtures/tls/chain-leaf.crt rename to src/test/resources/fixtures/tls/chain-leaf.crt diff --git a/src/test/fixtures/tls/chain-leaf.key b/src/test/resources/fixtures/tls/chain-leaf.key similarity index 100% rename from src/test/fixtures/tls/chain-leaf.key rename to src/test/resources/fixtures/tls/chain-leaf.key diff --git a/src/test/fixtures/tls/chain-root.crt b/src/test/resources/fixtures/tls/chain-root.crt similarity index 100% rename from src/test/fixtures/tls/chain-root.crt rename to src/test/resources/fixtures/tls/chain-root.crt diff --git a/src/test/fixtures/tls/chain-root.key b/src/test/resources/fixtures/tls/chain-root.key similarity index 100% rename from src/test/fixtures/tls/chain-root.key rename to src/test/resources/fixtures/tls/chain-root.key diff --git a/src/test/fixtures/tls/chain.crt b/src/test/resources/fixtures/tls/chain.crt similarity index 100% rename from src/test/fixtures/tls/chain.crt rename to src/test/resources/fixtures/tls/chain.crt diff --git a/src/test/fixtures/tls/chain.key b/src/test/resources/fixtures/tls/chain.key similarity index 100% rename from src/test/fixtures/tls/chain.key rename to src/test/resources/fixtures/tls/chain.key diff --git a/src/test/fixtures/tls/generate.bash b/src/test/resources/fixtures/tls/generate.bash similarity index 100% rename from src/test/fixtures/tls/generate.bash rename to src/test/resources/fixtures/tls/generate.bash diff --git a/src/test/fixtures/tls/no-signing.crt b/src/test/resources/fixtures/tls/no-signing.crt similarity index 100% rename from src/test/fixtures/tls/no-signing.crt rename to src/test/resources/fixtures/tls/no-signing.crt diff --git a/src/test/fixtures/tls/no-signing.key b/src/test/resources/fixtures/tls/no-signing.key similarity index 100% rename from src/test/fixtures/tls/no-signing.key rename to src/test/resources/fixtures/tls/no-signing.key diff --git a/src/test/fixtures/tls/self-signed.crt b/src/test/resources/fixtures/tls/self-signed.crt similarity index 100% rename from src/test/fixtures/tls/self-signed.crt rename to src/test/resources/fixtures/tls/self-signed.crt diff --git a/src/test/fixtures/tls/self-signed.key b/src/test/resources/fixtures/tls/self-signed.key similarity index 100% rename from src/test/fixtures/tls/self-signed.key rename to src/test/resources/fixtures/tls/self-signed.key From 42ac5894c9d19c73924e1ea35bc5b374826f189d Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Thu, 6 Mar 2025 23:18:37 +0200 Subject: [PATCH 13/14] fix: use template display name as env. description --- src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index 5adcf93..5aa09aa 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -42,7 +42,7 @@ class CoderRemoteEnvironment( override val state: MutableStateFlow = MutableStateFlow(wsRawStatus.toRemoteEnvironmentState(context)) override val description: MutableStateFlow = - MutableStateFlow(EnvironmentDescription.General(context.i18n.pnotr(workspace.templateName))) + MutableStateFlow(EnvironmentDescription.General(context.i18n.pnotr(workspace.templateDisplayName))) override val actionsList: StateFlow> = MutableStateFlow( listOf( From d4f933ab95704d80cd9519419f79fc1f67d6f926 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Thu, 6 Mar 2025 23:22:13 +0200 Subject: [PATCH 14/14] fix: support email - should be jetbrains-plugin@coder.com --- src/main/resources/localization/defaultMessages.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/localization/defaultMessages.po b/src/main/resources/localization/defaultMessages.po index cba43eb..837e2a0 100644 --- a/src/main/resources/localization/defaultMessages.po +++ b/src/main/resources/localization/defaultMessages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Coder Toolbox 1.0\n" -"Report-Msgid-Bugs-To: support@coder.com\n" +"Report-Msgid-Bugs-To: jetbrains-plugins@coder.com\n" "POT-Creation-Date: 2025-03-04 12:00+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n"