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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class DevSpacesRemoteDevExtension : RemoteDevExtension {
repository.startPolling()

logger.info("DevSpacesRemoteProvider initialized with ${dataSource::class.simpleName}")
return DevSpacesRemoteProvider(repository, localizableStringFactory, logger)
return DevSpacesRemoteProvider(repository, localizableStringFactory, logger, coroutineScope)
}

private fun createDataSource(logger: Logger, clientFactory: OpenShiftClientFactory): EnvironmentDataSource {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@ import com.jetbrains.toolbox.api.localization.LocalizableString
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.ui.actions.ActionDescription
import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription
import com.jetbrains.toolbox.api.ui.components.UiPage
import com.jetbrains.toolbox.platform.image.ImageResource
import com.jetbrains.toolbox.platform.image.image
import com.jetbrains.toolbox.platform.resource.jvm.jvmResourceReader
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import com.redhat.devtools.toolbox.environment.DevSpacesRemoteEnvironment
import java.net.URI

Expand All @@ -31,7 +37,8 @@ import java.net.URI
class DevSpacesRemoteProvider(
val repository: EnvironmentRepository,
val localizableStringFactory: LocalizableStringFactory,
val logger: Logger
val logger: Logger,
val coroutineScope: CoroutineScope
) : RemoteProvider("Dev Spaces") {

override val iconResource: ImageResource = jvmResourceReader().image("/icon.svg")
Expand All @@ -42,9 +49,21 @@ class DevSpacesRemoteProvider(
"make sure you are logged in to the correct cluster\r\n" +
"by running the 'oc login ...' command in the terminal.")

override val environments: MutableStateFlow<LoadableState<List<DevSpacesRemoteEnvironment>>> =
override val environments: StateFlow<LoadableState<List<DevSpacesRemoteEnvironment>>> =
repository.environments

override val additionalPluginActions: StateFlow<List<ActionDescription>> =
repository.currentUserOnly.map { onlyMine ->
listOf(object : RunnableActionDescription {
override val label = localizableStringFactory.pnotr(
if (onlyMine) "Show all workspaces" else "Show only my workspaces"
)
override fun run() {
repository.currentUserOnly.value = !repository.currentUserOnly.value
}
Comment on lines +61 to +63
})
}.stateIn(coroutineScope, SharingStarted.Eagerly, emptyList())

override val canCreateNewEnvironments: Boolean = false
override val isSingleEnvironment: Boolean = false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@ import com.redhat.devtools.toolbox.datasource.DataSourceException
import com.redhat.devtools.toolbox.datasource.EnvironmentDataSource
import com.redhat.devtools.toolbox.environment.*
import com.redhat.devtools.toolbox.openshift.OpenShiftClientFactory
import io.fabric8.openshift.client.OpenShiftClient
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds

/**
Expand All @@ -44,16 +49,31 @@ class EnvironmentRepository(
private val localizableStringFactory: LocalizableStringFactory,
private val clientFactory: OpenShiftClientFactory
) {
// Internal mutable state
private val _environments = MutableStateFlow<LoadableState<List<DevSpacesRemoteEnvironment>>>(
// Internal mutable state - holds the full unfiltered list
private val _allEnvironments = MutableStateFlow<LoadableState<List<DevSpacesRemoteEnvironment>>>(
LoadableState.Loading
)

// Cache of created environments by ID - allows updating existing instances
private val environmentCache = mutableMapOf<String, DevSpacesRemoteEnvironment>()

// Observable environment list for the provider
val environments: MutableStateFlow<LoadableState<List<DevSpacesRemoteEnvironment>>> = _environments
private val environmentCache = ConcurrentHashMap<String, DevSpacesRemoteEnvironment>()

// Username of the currently logged-in OpenShift user (resolved on first fetch)
private var currentUsername: String? = null
Comment on lines +60 to +61

// When true, the environments list is filtered to show only the current user's workspaces
val currentUserOnly = MutableStateFlow(true)

// Filtered view exposed to the provider — reacts to both list updates and toggle changes
val environments: StateFlow<LoadableState<List<DevSpacesRemoteEnvironment>>> =
combine(_allEnvironments, currentUserOnly) { allEnvs, onlyMine ->
if (!onlyMine || currentUsername == null) {
allEnvs
} else {
allEnvs.map { list ->
list.filter { it.getConfig().tags["owner"] == currentUsername }
}
}
}.stateIn(coroutineScope, SharingStarted.Eagerly, LoadableState.Loading)

fun startPolling() {
coroutineScope.launch(CoroutineName("EnvironmentRepository-Polling")) {
Expand All @@ -75,17 +95,21 @@ class EnvironmentRepository(
logger.debug("Refreshing environments from ${dataSource::class.simpleName}")

try {
if (currentUsername == null) {
currentUsername = resolveCurrentUsername()
}

val configs = dataSource.fetchEnvironments()

val environments = configs.map { config ->
getOrCreateEnvironment(config)
}
val environments = configs
.sortedWith(compareBy(nullsLast()) { it.tags["owner"] })
.map { config -> getOrCreateEnvironment(config) }

// Remove environments that no longer exist
val currentIds = configs.map { it.id }.toSet()
environmentCache.keys.removeAll { it !in currentIds }

_environments.value = LoadableState.Value(environments)
_allEnvironments.value = LoadableState.Value(environments)
logger.info("PLUGIN: Setting environments to ${environments.size} items: ${environments.map { it.id }}")

} catch (e: CancellationException) {
Expand Down Expand Up @@ -139,4 +163,15 @@ class EnvironmentRepository(
* Get a specific environment by ID.
*/
fun getEnvironment(id: String): DevSpacesRemoteEnvironment? = environmentCache[id]
}

private fun resolveCurrentUsername(): String? {
return try {
clientFactory.create().use { client ->
(client as? OpenShiftClient)?.currentUser()?.metadata?.name
}
} catch (e: Exception) {
logger.warn("Could not resolve current username: ${e.message}")
null
}
Comment on lines +172 to +175
}
Comment thread
SupremeMortal marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,16 @@ class DevWorkspacesDataSource(
val projects = Projects(client).list()

projects
.mapNotNull { it.metadata?.name }
.flatMap { namespace ->
DevWorkspaces(client, logger).list(namespace)
.mapNotNull { project ->
val namespace = project.metadata?.name ?: return@mapNotNull null
val namespaceOwner = project.metadata?.annotations?.get("che.eclipse.org/username")
namespace to namespaceOwner
}
.map { workspace ->
.flatMap { (namespace, namespaceOwner) ->
DevWorkspaces(client, logger).list(namespace).map { it to namespaceOwner }
}
.map { (workspace, namespaceOwner) ->
val owner = workspace.owner ?: namespaceOwner
EnvironmentConfig(
id = workspace.id,
name = MutableStateFlow(workspace.name),
Expand All @@ -49,7 +54,10 @@ class DevWorkspacesDataSource(
// availableIdeProductCodes = listOf("IU"),
// TODO: implement fetching the PROJECT_SOURCES env. var. value
projectPaths = listOf("/projects"),
tags = mapOf("namespace" to workspace.namespace)
tags = buildMap {
put("namespace", workspace.namespace)
if (owner != null) put("owner", owner)
}
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ class DevSpacesRemoteEnvironment(
private var activePortForward: LocalPortForward? = null

// Public reactive properties (observed by Toolbox UI)
override val secondaryInformation: String? = initialConfig.tags["owner"]

override var nameFlow: MutableStateFlow<String> = _currentConfig.name

override val state: MutableStateFlow<RemoteEnvironmentState> = _state
Expand Down Expand Up @@ -103,9 +105,7 @@ class DevSpacesRemoteEnvironment(
require(newConfig.id == initialConfig.id) { logger.info("Cannot change environment ID for ${initialConfig.id}") }
Comment thread
SupremeMortal marked this conversation as resolved.
_currentConfig = newConfig
_description.update {
EnvironmentDescription.General(newConfig.description?.let { text ->
localizableStringFactory.ptrl(text)
})
EnvironmentDescription.General(newConfig.description?.let { localizableStringFactory.ptrl(it) })
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ data class DevWorkspace(
val uid: String,
val started: Boolean,
val phase: String,
val cheEditor: String?
val owner: String?
) {
val running: Boolean
get() = phase == PHASE_RUNNING
Expand All @@ -36,7 +36,7 @@ data class DevWorkspace(
val metadata = resource.metadata
val spec = resource.additionalProperties["spec"] as? Map<*, *> ?: emptyMap<String, Any>()
val status = resource.additionalProperties["status"] as? Map<*, *> ?: emptyMap<String, Any>()
val cheEditor = metadata.annotations?.get("che.eclipse.org/che-editor")
val owner = metadata.annotations?.get("che.eclipse.org/username")

return DevWorkspace(
namespace = metadata.namespace ?: "",
Expand All @@ -45,7 +45,7 @@ data class DevWorkspace(
uid = metadata.uid ?: "",
started = spec["started"] as? Boolean ?: false,
phase = status["phase"] as? String ?: "",
cheEditor = cheEditor
owner = owner
)
}
}
Expand Down