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 @@ -25,6 +25,7 @@ import kotlinx.serialization.json.put
import org.wip.plugintoolkit.api.Capability
import org.wip.plugintoolkit.api.DataType
import org.wip.plugintoolkit.api.PluginInfo
import org.wip.plugintoolkit.api.PluginManifest
import org.wip.plugintoolkit.api.SemanticType
import org.wip.plugintoolkit.api.parseSemanticTypes
import org.wip.plugintoolkit.features.flows.logic.PathPatternResolver
Expand Down Expand Up @@ -490,10 +491,12 @@ data class Flow(
return fallbackType
}

fun isBroken(activeCapabilities: Set<String>): Boolean {
fun isBroken(activeCapabilities: Set<CapabilityIdentity>): Boolean {
val hasBrokenNode = this.nodes.any { it is Node.CapabilityNode && it.isBroken }
val hasMissingCapability =
this.nodes.filterIsInstance<Node.CapabilityNode>().any { it.capability.name !in activeCapabilities }
this.nodes.filterIsInstance<Node.CapabilityNode>().any {
CapabilityIdentity(it.pluginInfo.id, it.capability.name) !in activeCapabilities
}
val hasNotReadyNode = this.nodes.any { !it.isReady(connections) }
return hasBrokenNode || hasMissingCapability || hasNotReadyNode
}
Expand Down Expand Up @@ -521,6 +524,16 @@ data class Flow(
}
}

/** Stable, collision-free identity for a capability installed in the host. */
data class CapabilityIdentity(val pluginId: String, val capabilityName: String)

fun Iterable<PluginManifest>.capabilityIdentities(): Set<CapabilityIdentity> =
flatMap { manifest ->
manifest.capabilities.map { capability ->
CapabilityIdentity(manifest.plugin.id, capability.name)
}
}.toSet()

@Serializable
data class PortConstraints(
val regex: String? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ import org.jetbrains.compose.resources.stringResource
import org.wip.plugintoolkit.core.theme.ToolkitTheme
import org.wip.plugintoolkit.core.utils.PlatformUtils
import org.wip.plugintoolkit.features.flows.model.Node
import org.wip.plugintoolkit.features.flows.model.CapabilityIdentity
import org.wip.plugintoolkit.features.flows.model.capabilityIdentities
import org.wip.plugintoolkit.features.flows.viewmodel.ConflictResolutionAction
import org.wip.plugintoolkit.features.flows.viewmodel.FlowEvent
import org.wip.plugintoolkit.features.flows.viewmodel.FlowViewModel
Expand Down Expand Up @@ -112,6 +114,8 @@ fun FlowManagerView(
) {
val state by viewModel.state.collectAsState()
val pluginManager = org.koin.compose.koinInject<org.wip.plugintoolkit.features.plugin.logic.PluginManager>()
val loadedPlugins by pluginManager.loadedPlugins.collectAsState()
val installedPlugins by pluginManager.installedPlugins.collectAsState()
var showCreateDialog by remember { mutableStateOf(false) }
var newFlowName by remember { mutableStateOf("") }
var searchQuery by remember { mutableStateOf("") }
Expand All @@ -125,8 +129,10 @@ fun FlowManagerView(
// // flows are reloaded automatically via flowRepository
// }

val activeCapabilities = remember(state.flows) {
PluginLoader.getPlugins().flatMap { it.getManifest().getOrThrow().capabilities.map { cap -> cap.name } }.toSet()
val activeCapabilities = remember(loadedPlugins, installedPlugins) {
PluginLoader.getPlugins()
.mapNotNull { it.getManifest().getOrNull() }
.capabilityIdentities()
}

val filteredFlows = remember(state.flows, searchQuery) {
Expand Down Expand Up @@ -250,8 +256,9 @@ fun FlowManagerView(

val missingCapabilities = remember(flow, activeCapabilities) {
flow.nodes.filterIsInstance<Node.CapabilityNode>()
.map { it.capability.name }
.map { CapabilityIdentity(it.pluginInfo.id, it.capability.name) }
.filter { it !in activeCapabilities }
.map { it.capabilityName }
.distinct()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import org.wip.plugintoolkit.api.format
import org.wip.plugintoolkit.core.model.localized
import org.wip.plugintoolkit.core.theme.ToolkitTheme
import org.wip.plugintoolkit.features.flows.model.Node
import org.wip.plugintoolkit.features.flows.model.capabilityIdentities
import org.wip.plugintoolkit.features.flows.viewmodel.FlowViewModel
import org.wip.plugintoolkit.features.job.model.JobStatus
import org.wip.plugintoolkit.features.job.model.JobType
Expand Down Expand Up @@ -94,16 +95,16 @@ fun FlowRunnerView(
val pluginManager: org.wip.plugintoolkit.features.plugin.logic.PluginManager = koinInject()
val pluginLocksState by pluginManager.pluginLocksState.collectAsState()
val pluginSettingsState by pluginManager.pluginSettingsState.collectAsState()
val loadedPlugins by pluginManager.loadedPlugins.collectAsState()
val installedPlugins by pluginManager.installedPlugins.collectAsState()
val providedLocks = remember(pluginLocksState) {
pluginLocksState.values.fold(emptyMap<String, Boolean>()) { acc, map -> acc + map }
}

val activeCapabilities = remember(state.flows) {
val activeCapabilities = remember(loadedPlugins, installedPlugins) {
org.wip.plugintoolkit.features.plugin.logic.PluginLoader.getPlugins()
.mapNotNull { it.getManifest().getOrNull() }
.flatMap { it.capabilities }
.map { it.name }
.toSet()
.capabilityIdentities()
}

val executableFlows = remember(state.flows, activeCapabilities) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import org.wip.plugintoolkit.features.flows.logic.FlowRepository
import org.wip.plugintoolkit.features.flows.model.Connection
import org.wip.plugintoolkit.features.flows.model.Flow
import org.wip.plugintoolkit.features.flows.model.Node
import org.wip.plugintoolkit.features.flows.model.capabilityIdentities
import org.wip.plugintoolkit.features.job.logic.JobManager
import org.wip.plugintoolkit.features.job.model.BackgroundJob
import org.wip.plugintoolkit.features.job.model.JobStatus
Expand Down Expand Up @@ -559,9 +560,7 @@ class FlowViewModel(
fun executeFlow(flow: Flow, parameterValues: Map<String, String>) {
val activeCapabilities = org.wip.plugintoolkit.features.plugin.logic.PluginLoader.getPlugins()
.mapNotNull { it.getManifest().getOrNull() }
.flatMap { it.capabilities }
.map { it.name }
.toSet()
.capabilityIdentities()

if (flow.isBroken(activeCapabilities)) {
Logger.e { "Failed to execute flow '${flow.name}': Flow contains broken or unready nodes." }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.ui.geometry.Offset
import org.wip.plugintoolkit.api.Capability
import org.wip.plugintoolkit.api.PluginInfo
import org.wip.plugintoolkit.features.flows.model.Flow
import org.wip.plugintoolkit.features.flows.model.CapabilityIdentity
import org.wip.plugintoolkit.features.flows.model.Node
import kotlin.test.Test
import kotlin.test.assertFalse
Expand Down Expand Up @@ -52,7 +53,10 @@ class FlowBrokenTest {
)
)

val activeCapabilities = setOf("Capability A", "Capability B")
val activeCapabilities = setOf(
CapabilityIdentity("test.plugin", "Capability A"),
CapabilityIdentity("test.plugin", "Capability B")
)
assertFalse(flow.isBroken(activeCapabilities), "Flow should not be broken")
}

Expand All @@ -67,7 +71,7 @@ class FlowBrokenTest {
)

// Only Capability A is active, meaning Capability B is missing
val activeCapabilities = setOf("Capability A")
val activeCapabilities = setOf(CapabilityIdentity("test.plugin", "Capability A"))
assertTrue(flow.isBroken(activeCapabilities), "Flow should be broken due to missing capability B")
}

Expand All @@ -80,7 +84,7 @@ class FlowBrokenTest {
)
)

val activeCapabilities = setOf("Capability A")
val activeCapabilities = setOf(CapabilityIdentity("test.plugin", "Capability A"))
assertTrue(
flow.isBroken(activeCapabilities),
"Flow should be broken because the node is explicitly marked as broken"
Expand All @@ -94,7 +98,19 @@ class FlowBrokenTest {
nodes = emptyList()
)

val activeCapabilities = setOf("Capability A")
val activeCapabilities = setOf(CapabilityIdentity("test.plugin", "Capability A"))
assertFalse(flow.isBroken(activeCapabilities), "Empty flow should not be broken")
}

@Test
fun `capabilities with the same name from another plugin do not make a flow executable`() {
val flow = Flow(
name = "Namespaced Flow",
nodes = listOf(createCapabilityNode(capabilityA))
)

val activeCapabilities = setOf(CapabilityIdentity("another.plugin", "Capability A"))

assertTrue(flow.isBroken(activeCapabilities), "Capability identity must include the plugin id")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.wip.plugintoolkit.api.PluginInfo
import org.wip.plugintoolkit.api.PrimitiveType
import org.wip.plugintoolkit.features.flows.model.Connection
import org.wip.plugintoolkit.features.flows.model.Flow
import org.wip.plugintoolkit.features.flows.model.CapabilityIdentity
import org.wip.plugintoolkit.features.flows.model.InputPort
import org.wip.plugintoolkit.features.flows.model.Node
import kotlin.test.Test
Expand Down Expand Up @@ -89,12 +90,13 @@ class FlowReadinessTest {

val flow = Flow(name = "Test Flow", nodes = listOf(node))

assertTrue(flow.isBroken(setOf("TestCap")), "Flow should be broken if a node is not ready")
val activeCapabilities = setOf(CapabilityIdentity(pluginInfo.id, "TestCap"))
assertTrue(flow.isBroken(activeCapabilities), "Flow should be broken if a node is not ready")

val valueNode = node.copyWithUpdatedInput("reqPort", kotlinx.serialization.json.JsonPrimitive("value"))
val readyFlow = Flow(name = "Test Flow", nodes = listOf(valueNode))

assertFalse(readyFlow.isBroken(setOf("TestCap")), "Flow should not be broken if all nodes are ready")
assertFalse(readyFlow.isBroken(activeCapabilities), "Flow should not be broken if all nodes are ready")
}

@Test
Expand Down