Add StateMachineViewer and data model to explorer.

Add state machine details with flow result or error, flow initiator, flow name and progress.
Split flows into categories: in progress, errored, done.
This commit is contained in:
Katarzyna Streich 2017-05-03 14:51:07 +01:00
parent f9ca498cb8
commit c734f625ad
12 changed files with 474 additions and 480 deletions

View File

@ -6,31 +6,15 @@ import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.CordaRPCClientConfiguration import net.corda.client.rpc.CordaRPCClientConfiguration
import net.corda.core.flows.StateMachineRunId import net.corda.core.flows.StateMachineRunId
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.StateMachineInfo
import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.node.services.NetworkMapCache.MapChange
import net.corda.core.node.services.StateMachineTransactionMapping import net.corda.core.node.services.StateMachineTransactionMapping
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
import net.corda.core.seconds import net.corda.core.seconds
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
data class ProgressTrackingEvent(val stateMachineId: StateMachineRunId, val message: String, val currentState: ProgressTracker?) { // TODO: RG Not a string, but a proper tracking object.
companion object {
fun createStreamFromStateMachineInfo(stateMachine: StateMachineInfo): Observable<ProgressTrackingEvent>? {
return stateMachine.progressTrackerStepAndUpdates?.let { pair ->
val (current, future) = pair
future.map { ProgressTrackingEvent(stateMachine.id, it, null ) }.startWith(ProgressTrackingEvent(stateMachine.id, current, null))
}
}
}
}
/** /**
* This model exposes raw event streams to and from the node. * This model exposes raw event streams to and from the node.
*/ */

View File

@ -0,0 +1,86 @@
package net.corda.client.jfx.model
import javafx.beans.binding.Bindings
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections
import net.corda.client.jfx.utils.LeftOuterJoinedMap
import net.corda.client.jfx.utils.flatten
import net.corda.client.jfx.utils.fold
import net.corda.client.jfx.utils.getObservableValues
import net.corda.client.jfx.utils.map
import net.corda.client.jfx.utils.recordAsAssociation
import net.corda.core.ErrorOr
import net.corda.core.flows.FlowInitiator
import net.corda.core.flows.StateMachineRunId
import net.corda.core.messaging.StateMachineInfo
import net.corda.core.messaging.StateMachineUpdate
import rx.Observable
data class ProgressTrackingEvent(val stateMachineId: StateMachineRunId, val message: String) {
companion object {
fun createStreamFromStateMachineInfo(stateMachine: StateMachineInfo): Observable<ProgressTrackingEvent>? {
return stateMachine.progressTrackerStepAndUpdates?.let { pair ->
val (current, future) = pair
future.map { ProgressTrackingEvent(stateMachine.id, it) }.startWith(ProgressTrackingEvent(stateMachine.id, current))
}
}
}
}
data class ProgressStatus(val status: String)
sealed class StateMachineStatus {
data class Added(val stateMachineName: String, val flowInitiator: FlowInitiator) : StateMachineStatus()
data class Removed(val result: ErrorOr<*>) : StateMachineStatus()
}
// TODO StateMachineData and StateMachineInfo
data class StateMachineData(
val id: StateMachineRunId,
val stateMachineName: String,
val flowInitiator: FlowInitiator,
val addRmStatus: ObservableValue<StateMachineStatus>,
val stateMachineStatus: ObservableValue<ProgressStatus?>
)
class StateMachineDataModel {
private val stateMachineUpdates by observable(NodeMonitorModel::stateMachineUpdates)
private val progressTracking by observable(NodeMonitorModel::progressTracking)
private val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
private val stateMachineStatus = stateMachineUpdates.fold(FXCollections.observableHashMap<StateMachineRunId, SimpleObjectProperty<StateMachineStatus>>()) { map, update ->
when (update) {
is StateMachineUpdate.Added -> {
val flowInitiator= update.stateMachineInfo.initiator
val added: SimpleObjectProperty<StateMachineStatus> =
SimpleObjectProperty(StateMachineStatus.Added(update.stateMachineInfo.flowLogicClassName, flowInitiator))
map[update.id] = added
}
is StateMachineUpdate.Removed -> {
val added = map[update.id]
added ?: throw Exception("State machine removed with unknown id ${update.id}")
added.set(StateMachineStatus.Removed(update.result))
}
}
}
val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
val smStatus = status.value as StateMachineStatus.Added // TODO not always added
// todo easybind
Bindings.createObjectBinding({
StateMachineData(id, smStatus.stateMachineName, smStatus.flowInitiator, status, progress.map { it?.let { ProgressStatus(it.message) } })
}, arrayOf(progress, status))
}.getObservableValues().flatten()
val stateMachinesInProgress = stateMachineDataList.filtered { it.addRmStatus.value !is StateMachineStatus.Removed }
val stateMachinesDone = stateMachineDataList.filtered { it.addRmStatus.value is StateMachineStatus.Removed }
val stateMachinesFinished = stateMachinesDone.filtered {
val res = it.addRmStatus.value as StateMachineStatus.Removed
res.result.error == null
}
val stateMachinesError = stateMachinesDone.filtered {
val res = it.addRmStatus.value as StateMachineStatus.Removed
res.result.error != null
}
}

View File

@ -1,9 +1,6 @@
package net.corda.client.jfx.model package net.corda.client.jfx.model
import javafx.beans.binding.Bindings
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.value.ObservableValue import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections
import javafx.collections.ObservableList import javafx.collections.ObservableList
import javafx.collections.ObservableMap import javafx.collections.ObservableMap
import net.corda.client.jfx.utils.* import net.corda.client.jfx.utils.*
@ -11,8 +8,6 @@ import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.StateMachineRunId
import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import org.fxmisc.easybind.EasyBind import org.fxmisc.easybind.EasyBind
@ -59,159 +54,17 @@ data class PartiallyResolvedTransaction(
} }
} }
data class FlowStatus(val status: String) // TODO Do we want to have mapping tx <-> StateMachine?
//todo after rebase - remove it rather
//class FlowStatus(
// val status: String,
// pt: ProgressTrackingEvent?,
// val progress: FlowLogic.ProgressTrackerDisplayProxy = convert(pt)
//) {
//
// companion object {
// fun convert(pt: ProgressTrackingEvent?) : FlowLogic.ProgressTrackerDisplayProxy {
// return FlowLogic.ProgressTrackerDisplayProxy(null, false, false)
///* pt?. == ProgressTracker.DONE,
// pt == null)*/
// }
// }
//}
sealed class StateMachineStatus {
abstract val stateMachineName: String
data class Added(override val stateMachineName: String) : StateMachineStatus()
data class Removed(override val stateMachineName: String) : StateMachineStatus()
}
/*data class StateMachineData(
val id: StateMachineRunId,
val flowStatus: ObservableValue<FlowStatus?>,
val stateMachineStatus: ObservableValue<StateMachineStatus>
)
*/
data class StateMachineData(
val id: StateMachineRunId,
val flowStatus: FlowStatus?,
val stateMachineStatus: StateMachineStatus
)
/** /**
* This model provides an observable list of transactions and what state machines/flows recorded them * This model provides an observable list of transactions and what state machines/flows recorded them
*/ */
class TransactionDataModel { class TransactionDataModel {
private val transactions by observable(NodeMonitorModel::transactions) private val transactions by observable(NodeMonitorModel::transactions)
private val stateMachineUpdates by observable(NodeMonitorModel::stateMachineUpdates)
private val progressTracking by observable(NodeMonitorModel::progressTracking)
private val stateMachineTransactionMapping by observable(NodeMonitorModel::stateMachineTransactionMapping)
private val collectedTransactions = transactions.recordInSequence() private val collectedTransactions = transactions.recordInSequence()
private val transactionMap = collectedTransactions.associateBy(SignedTransaction::id) private val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
private val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
private val stateMachineStatus = stateMachineUpdates.fold(FXCollections.observableHashMap<StateMachineRunId, SimpleObjectProperty<StateMachineStatus>>()) { map, update ->
when (update) {
is StateMachineUpdate.Added -> {
val added: SimpleObjectProperty<StateMachineStatus> =
SimpleObjectProperty(StateMachineStatus.Added(update.stateMachineInfo.flowLogicClassName))
map[update.id] = added
}
is StateMachineUpdate.Removed -> {
val added = map[update.id]
added ?: throw Exception("State machine removed with unknown id ${update.id}")
added.set(StateMachineStatus.Removed(added.value.stateMachineName))
}
}
}
val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
Bindings.createObjectBinding({ StateMachineData(id, progress.value?.message?.let(::FlowStatus), status.get()) }, arrayOf(progress, status))
}.getObservableValues().flatten()
/*
private val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
StateMachineData(id, progress.map { it?.let { FlowStatus(it.message) } }, status)
}.getObservableValues()*/
// TODO : Create a new screen for state machines.
private val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
private val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
val partiallyResolvedTransactions = collectedTransactions.map { val partiallyResolvedTransactions = collectedTransactions.map {
PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap) PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
} }
} }
class StateMachineDataModel {
private val transactions by observable(NodeMonitorModel::transactions)
private val stateMachineUpdates by observable(NodeMonitorModel::stateMachineUpdates)
private val progressTracking by observable(NodeMonitorModel::progressTracking)
private val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
private val progressList = progressEvents.getObservableValues()
private val stateMachineTransactionMapping by observable(NodeMonitorModel::stateMachineTransactionMapping)
private val collectedTransactions = transactions.recordInSequence()
private val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
private val stateMachineStatus = stateMachineUpdates.fold(FXCollections.observableHashMap<StateMachineRunId, SimpleObjectProperty<StateMachineStatus>>()) { map, update ->
println("**** $update")
when (update) {
is StateMachineUpdate.Added -> {
val added: SimpleObjectProperty<StateMachineStatus> =
SimpleObjectProperty(StateMachineStatus.Added(update.stateMachineInfo.flowLogicClassName))
map[update.id] = added
}
is StateMachineUpdate.Removed -> {
val added = map[update.id]
added ?: throw Exception("State machine removed with unknown id ${update.id}")
added.set(StateMachineStatus.Removed(added.value.stateMachineName))
}
}
}
val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
Bindings.createObjectBinding({ StateMachineData(id, progress.value?.message?.let(::FlowStatus), status.get()) }, arrayOf(progress, status))
}.getObservableValues().flatten()
/*
val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
StateMachineData(id, progress.map { it?.let { FlowStatus(it.message) } }, status)
}.getObservableValues()*/
// TODO : Create a new screen for state machines.
private val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
private val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
val partiallyResolvedTransactions = collectedTransactions.map {
PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
}
val flowsInProgress = partiallyResolvedTransactions
/*
data class SomeStructure(
val someInt: ObservableValue<StateMachineStatus>,
val otherData: ObservableValue<String>
)
*/
val progressObservableList = progressList.map{ struct -> struct.message }
fun asd() {
progressEvents // .flatMap { it -> it.key}
progressEvents.keys.map{ it -> { if (true) {null }else { it}}}
// val a1 = progressEvents.map{ struct -> struct.key. { if ( true) { null } else {struct }} }
// val a: ObservableList<SomeStructure> = null!!
// val x: ObservableList<SomeStructure> = ObservableList<SomeStructure>();
// val a3: ObservableList<SomeStructure> = listOf(Pair(1,"hey"))
// val x = a.map{ struct -> struct. { if ( it.javaClass == StateMachineStatus::Removed) { null} else {struct }} }
// val b = a.map { struct -> struct.someInt.map { if (it.javaClass == StateMachineStatus::Removed) { null } else { struct } } }
// val c = b.flatten().filterNotNull()
//return c
}
}

View File

@ -30,7 +30,6 @@ import java.util.*
@CordaSerializable @CordaSerializable
data class StateMachineInfo( data class StateMachineInfo(
val id: StateMachineRunId, val id: StateMachineRunId,
val sessionId: Long,
val flowLogicClassName: String, val flowLogicClassName: String,
val initiator: FlowInitiator, val initiator: FlowInitiator,
val progressTrackerStepAndUpdates: Pair<String, Observable<String>>? val progressTrackerStepAndUpdates: Pair<String, Observable<String>>?

View File

@ -131,7 +131,7 @@ class Main : App(MainView::class) {
// Stock Views. // Stock Views.
registerView<Dashboard>() registerView<Dashboard>()
registerView<TransactionViewer>() registerView<TransactionViewer>()
registerView<FlowViewer>() registerView<StateMachineViewer>()
// CordApps Views. // CordApps Views.
registerView<CashViewer>() registerView<CashViewer>()
// Tools. // Tools.

View File

@ -0,0 +1,14 @@
package net.corda.explorer.formatters
import net.corda.core.flows.FlowInitiator
object FlowInitiatorFormatter : Formatter<FlowInitiator> {
override fun format(value: FlowInitiator): String {
return when (value) {
is FlowInitiator.Scheduled -> "Started by scheduled state:: " + value.scheduledState.ref.toString() // TODO format that
is FlowInitiator.Shell -> "Started via shell"
is FlowInitiator.Peer -> "Peer legal name: " + value.party.name //TODO format that
is FlowInitiator.RPC -> "Rpc username: " + value.username
}
}
}

View File

@ -0,0 +1,7 @@
package net.corda.explorer.formatters
object FlowNameFormatter {
val boring = object : Formatter<String> {
override fun format(value: String) = value.split('.').last().replace("$", ": ") // TODO Better handling of names.
}
}

View File

@ -1,43 +1,55 @@
package net.corda.explorer.views package net.corda.explorer.views
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
import javafx.beans.property.ReadOnlyObjectWrapper import javafx.collections.ObservableList
import javafx.geometry.HPos
import javafx.geometry.Insets
import javafx.geometry.Pos import javafx.geometry.Pos
import javafx.scene.control.TableColumn import javafx.scene.Parent
import javafx.scene.control.Label
import javafx.scene.control.ScrollPane
import javafx.scene.control.TabPane
import javafx.scene.control.TableView import javafx.scene.control.TableView
import javafx.scene.control.TitledPane
import javafx.scene.layout.BorderPane import javafx.scene.layout.BorderPane
import javafx.scene.layout.GridPane
import javafx.scene.layout.VBox
import javafx.scene.text.FontWeight
import javafx.scene.text.TextAlignment
import net.corda.client.jfx.model.StateMachineData
import net.corda.client.jfx.model.StateMachineDataModel import net.corda.client.jfx.model.StateMachineDataModel
import net.corda.client.jfx.model.StateMachineStatus
import net.corda.client.jfx.model.observableList
import net.corda.client.jfx.model.observableListReadOnly import net.corda.client.jfx.model.observableListReadOnly
import net.corda.client.jfx.utils.map import net.corda.client.jfx.utils.map
//import net.corda.client.fxutils.map
//import net.corda.client.model.StateMachineDataModel
//import net.corda.client.model.observableListReadOnly
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.toBase58String
import net.corda.core.flows.FlowInitiator
import net.corda.core.transactions.SignedTransaction
import net.corda.explorer.formatters.FlowInitiatorFormatter
import net.corda.explorer.formatters.FlowNameFormatter
import net.corda.explorer.identicon.identicon import net.corda.explorer.identicon.identicon
import net.corda.explorer.identicon.identiconToolTip import net.corda.explorer.identicon.identiconToolTip
import net.corda.explorer.model.CordaView import net.corda.explorer.model.CordaView
import net.corda.explorer.model.CordaWidget import net.corda.explorer.model.CordaWidget
import net.corda.explorer.ui.setCustomCellFactory import net.corda.explorer.ui.setCustomCellFactory
import tornadofx.column import tornadofx.*
import tornadofx.label
import tornadofx.observable
import tornadofx.right
// TODO Rethink whole idea of showing communication as table, it should be tree view for each StateMachine (with subflows and other communication)
class FlowViewer : CordaView("Flow Triage") { class StateMachineViewer : CordaView("Flow Triage") {
override val root by fxml<BorderPane>() override val root by fxml<TabPane>()
override val icon = FontAwesomeIcon.HEARTBEAT override val icon = FontAwesomeIcon.HEARTBEAT
override val widgets = listOf(CordaWidget(title, FlowViewer.StateMachineWidget())).observable() override val widgets = listOf(CordaWidget(title, StateMachineViewer.StateMachineWidget())).observable()
private val progressViewTable by fxid<TableView<StateMachineData>>()
private val doneViewTable by fxid<TableView<StateMachineData>>()
private val errorViewTable by fxid<TableView<StateMachineData>>()
private val flowViewTable by fxid<TableView<FlowViewer.Flow>>() private class StateMachineWidget : BorderPane() {
private val flowColumnSessionId by fxid<TableColumn<Flow, Int>>() private val flows by observableListReadOnly(StateMachineDataModel::stateMachinesInProgress)
private val flowColumnInternalId by fxid<TableColumn<Flow, String>>()
private val flowColumnState by fxid<TableColumn<Flow, String>>()
private class StateMachineWidget() : BorderPane() {
private val flows by observableListReadOnly(StateMachineDataModel::flowsInProgress)
// TODO can add stats: in progress, errored, maybe done to the widget?
init { init {
right { right {
label { label {
@ -48,284 +60,276 @@ class FlowViewer : CordaView("Flow Triage") {
} }
} }
data class Flow(val id: String, val latestProgress: String) private val stateMachinesInProgress by observableList(StateMachineDataModel::stateMachinesInProgress)
private val stateMachinesFinished by observableList(StateMachineDataModel::stateMachinesFinished)
private val stateMachinesError by observableList(StateMachineDataModel::stateMachinesError)
private val stateMachines by observableListReadOnly(StateMachineDataModel::stateMachineDataList) fun makeColumns(table: TableView<StateMachineData>, tableItems: ObservableList<StateMachineData>, withResult: Boolean = true) {
table.apply {
// private val flows = stateMachines.map { it -> Flow(it.id.toString(), it.flowStatus.map { it.toString() }) }.filtered { ! it.latestProgress.value.contains("Done") } items = tableItems
if (withResult) {
private val flows = stateMachines.map { it -> Flow(it.id.toString(), it.flowStatus.toString()) }.filtered { rowExpander(expandOnDoubleClick = true) {
println("--> $it") add(StateMachineDetailsView(it).root)
println("Status:${it.latestProgress}")
//it.id.startsWith("[3") or it.id.startsWith("[9")
println(it.latestProgress)
println(it.latestProgress.contains("status=Done"))
!it.latestProgress.contains("status=Done") && it.latestProgress != "null"
//it.latestProgress != null
}
init {
flowViewTable.apply {
items = flows
column("ID", Flow::id) { maxWidth = 200.0 }
.setCustomCellFactory {
label("$it") {
val hash = SecureHash.Companion.randomSHA256()
graphic = identicon(hash, 15.0)
tooltip = identiconToolTip(hash)
}
}
}
flowColumnSessionId.apply {
setCellValueFactory {
ReadOnlyObjectWrapper(0)
}
}
flowColumnInternalId.setCellValueFactory {
ReadOnlyObjectWrapper(it.value.id)
}
flowColumnState.setCellValueFactory {
ReadOnlyObjectWrapper(it.value.latestProgress)
// it.value.latestProgress
}
}
}
/*
class StateMachineViewer2 : CordaView("Transactions") {
override val root by fxml<BorderPane>()
override val icon = FontAwesomeIcon.RANDOM
private val transactionViewTable by fxid<TableView<Transaction>>()
private val matchingTransactionsLabel by fxid<Label>()
// Inject data
private val transactions by observableListReadOnly(TransactionDataModel::partiallyResolvedTransactions)
private val reportingExchange by observableValue(ReportingCurrencyModel::reportingExchange)
private val reportingCurrency by observableValue(ReportingCurrencyModel::reportingCurrency)
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
override val widgets = listOf(CordaWidget(title, TransactionWidget())).observable()
/**
* This is what holds data for a single transaction node. Note how a lot of these are nullable as we often simply don't
* have the data.
*/
data class Transaction(
val tx: PartiallyResolvedTransaction,
val id: SecureHash,
val inputs: Inputs,
val outputs: ObservableList<StateAndRef<ContractState>>,
val inputParties: ObservableList<List<ObservableValue<NodeInfo?>>>,
val outputParties: ObservableList<List<ObservableValue<NodeInfo?>>>,
val commandTypes: List<Class<CommandData>>,
val totalValueEquiv: ObservableValue<AmountDiff<Currency>>
)
data class Inputs(val resolved: ObservableList<StateAndRef<ContractState>>, val unresolved: ObservableList<StateRef>)
/**
* We map the gathered data about transactions almost one-to-one to the nodes.
*/
init {
val transactions = transactions.map {
val resolved = it.inputs.sequence()
.map { it as? PartiallyResolvedTransaction.InputResolution.Resolved }
.filterNotNull()
.map { it.stateAndRef }
val unresolved = it.inputs.sequence()
.map { it as? PartiallyResolvedTransaction.InputResolution.Unresolved }
.filterNotNull()
.map { it.stateRef }
val outputs = it.transaction.tx.outputs
.mapIndexed { index, transactionState ->
val stateRef = StateRef(it.id, index)
StateAndRef(transactionState, stateRef)
}.observable()
Transaction(
tx = it,
id = it.id,
inputs = Inputs(resolved, unresolved),
outputs = outputs,
inputParties = resolved.getParties(),
outputParties = outputs.getParties(),
commandTypes = it.transaction.tx.commands.map { it.value.javaClass },
totalValueEquiv = ::calculateTotalEquiv.lift(myIdentity,
reportingExchange,
resolved.map { it.state.data }.lift(),
it.transaction.tx.outputs.map { it.data }.lift())
)
}
val searchField = SearchField(transactions,
"Transaction ID" to { tx, s -> "${tx.id}".contains(s, true) },
"Input" to { tx, s -> tx.inputs.resolved.any { it.state.data.contract.javaClass.simpleName.contains(s, true) } },
"Output" to { tx, s -> tx.outputs.any { it.state.data.contract.javaClass.simpleName.contains(s, true) } },
"Input Party" to { tx, s -> tx.inputParties.any { it.any { it.value?.legalIdentity?.name?.contains(s, true) ?: false } } },
"Output Party" to { tx, s -> tx.outputParties.any { it.any { it.value?.legalIdentity?.name?.contains(s, true) ?: false } } },
"Command Type" to { tx, s -> tx.commandTypes.any { it.simpleName.contains(s, true) } }
)
root.top = searchField.root
// Transaction table
transactionViewTable.apply {
items = searchField.filteredData
column("Transaction ID", Transaction::id) { maxWidth = 200.0 }.setCustomCellFactory {
label("$it") {
graphic = identicon(it, 15.0)
tooltip = identiconToolTip(it)
}
}
column("Input", Transaction::inputs).cellFormat {
text = it.resolved.toText()
if (!it.unresolved.isEmpty()) {
if (!text.isBlank()) {
text += ", "
}
text += "Unresolved(${it.unresolved.size})"
}
}
column("Output", Transaction::outputs).cellFormat { text = it.toText() }
column("Input Party", Transaction::inputParties).cellFormat { text = it.flatten().map { it.value?.legalIdentity?.name }.filterNotNull().toSet().joinToString() }
column("Output Party", Transaction::outputParties).cellFormat { text = it.flatten().map { it.value?.legalIdentity?.name }.filterNotNull().toSet().joinToString() }
column("Command type", Transaction::commandTypes).cellFormat { text = it.map { it.simpleName }.joinToString() }
column("Total value", Transaction::totalValueEquiv).cellFormat {
text = "${it.positivity.sign}${AmountFormatter.boring.format(it.amount)}"
titleProperty.bind(reportingCurrency.map { "Total value ($it equiv)" })
}
rowExpander {
add(ContractStatesView(it).root)
prefHeight = 400.0
}.apply { }.apply {
prefWidth = 26.0 // Column stays the same size, but we don't violate column restricted resize policy for the whole table view.
isResizable = false minWidth = 26.0
maxWidth = 26.0
}
}
column("ID", StateMachineData::id) { // TODO kill that ID column
minWidth = 100.0
maxWidth = 200.0
}.setCustomCellFactory {
label("$it") {
val hash = SecureHash.sha256(it.toString())
graphic = identicon(hash, 15.0)
tooltip = identiconToolTip(hash) //TODO Have id instead of hash.
}
}
column("Flow name", StateMachineData::stateMachineName).cellFormat { text = FlowNameFormatter.boring.format(it) }
column("Initiator", StateMachineData::flowInitiator).cellFormat { text = FlowInitiatorFormatter.format(it) }
column("Flow Status", StateMachineData::stateMachineStatus).cellFormat {
if (it == null)
text = "No progress data"
else text = it.status
} // TODO null
column("Result", StateMachineData::addRmStatus).setCustomCellFactory {
if (it is StateMachineStatus.Removed) {
if (it.result.error == null) {
label("Success") {
graphic = FontAwesomeIconView(FontAwesomeIcon.CHECK).apply {
glyphSize = 15.0
textAlignment = TextAlignment.CENTER
style = "-fx-fill: green"
}
}
} else {
label("Error") {
graphic = FontAwesomeIconView(FontAwesomeIcon.BOLT).apply {
glyphSize = 15.0
textAlignment = TextAlignment.CENTER
style = "-fx-fill: -color-4"
}
}
}
}
else {
label("In progress") {
// TODO Other icons: spnner, hourglass-half, hourglass-1, send-o, space-shuttle ;)
graphic = FontAwesomeIconView(FontAwesomeIcon.ROCKET).apply {
glyphSize = 15.0
textAlignment = TextAlignment.CENTER
style = "-fx-fill: lightslategrey"
}
}
}
} }
setColumnResizePolicy { true }
} }
matchingTransactionsLabel.textProperty().bind(Bindings.size(transactionViewTable.items).map {
"$it matching transaction${if (it == 1) "" else "s"}"
})
} }
private fun ObservableList<StateAndRef<ContractState>>.getParties() = map { it.state.data.participants.map { getModel<NetworkIdentityModel>().lookup(it) } }
private fun ObservableList<StateAndRef<ContractState>>.toText() = map { it.contract().javaClass.simpleName }.groupBy { it }.map { "${it.key} (${it.value.size})" }.joinToString()
private class TransactionWidget() : BorderPane() {
private val partiallyResolvedTransactions by observableListReadOnly(TransactionDataModel::partiallyResolvedTransactions)
// TODO : Add a scrolling table to show latest transaction.
// TODO : Add a chart to show types of transactions.
init { init {
right { makeColumns(progressViewTable, stateMachinesInProgress, false)
label { makeColumns(doneViewTable, stateMachinesFinished)
textProperty().bind(Bindings.size(partiallyResolvedTransactions).map(Number::toString)) makeColumns(errorViewTable, stateMachinesError)
BorderPane.setAlignment(this, Pos.BOTTOM_RIGHT)
}
}
}
} }
private inner class ContractStatesView(transaction: Transaction) : Fragment() { private inner class StateMachineDetailsView(val smmData: StateMachineData) : Fragment() {
override val root by fxml<Parent>() override val root by fxml<Parent>()
private val inputs by fxid<ListView<StateAndRef<ContractState>>>() private val flowNamePane by fxid<TitledPane>()
private val outputs by fxid<ListView<StateAndRef<ContractState>>>() private val flowProgressPane by fxid<TitledPane>()
private val signatures by fxid<VBox>() private val flowInitiatorPane by fxid<TitledPane>()
private val inputPane by fxid<TitledPane>() private val flowResultPane by fxid<TitledPane>()
private val outputPane by fxid<TitledPane>()
private val signaturesPane by fxid<TitledPane>()
init { init {
val signatureData = transaction.tx.transaction.sigs.map { it.by } flowNamePane.apply {
// Bind count to TitlePane content = label {
inputPane.text = "Input (${transaction.inputs.resolved.count()})" text = FlowNameFormatter.boring.format(smmData.stateMachineName)
outputPane.text = "Output (${transaction.outputs.count()})" }
signaturesPane.text = "Signatures (${signatureData.count()})" }
flowProgressPane.apply {
inputs.cellCache { getCell(it) } content = label {
outputs.cellCache { getCell(it) } text = smmData.stateMachineStatus.value?.status // TODO later we can do some magic with showing progress steps with subflows
}
inputs.items = transaction.inputs.resolved }
outputs.items = transaction.outputs.observable() flowInitiatorPane.apply {
//TODO use fxml to access initiatorGridPane
signatures.children.addAll(signatureData.map { signature -> // initiatorGridPane.apply {when...
val nodeInfo = getModel<NetworkIdentityModel>().lookup(signature) content = when (smmData.flowInitiator) {
copyableLabel(nodeInfo.map { "${signature.toStringShort()} (${it?.legalIdentity?.name ?: "???"})" }) is FlowInitiator.Shell -> ShellNode() // TODO Extend this when we will have more information on shell user.
}) is FlowInitiator.Peer -> PeerNode(smmData.flowInitiator as FlowInitiator.Peer)
is FlowInitiator.RPC -> RPCNode(smmData.flowInitiator as FlowInitiator.RPC)
is FlowInitiator.Scheduled -> ScheduledNode(smmData.flowInitiator as FlowInitiator.Scheduled)
}
}
flowResultPane.apply {
val status = smmData.addRmStatus.value
if (status is StateMachineStatus.Removed) {
content = status.result.match(onValue = { ResultNode(it) }, onError = { ErrorNode(it) })
}
}
}
} }
private fun getCell(contractState: StateAndRef<ContractState>): Node { // TODO make that Vbox part of FXML
return { private inner class ResultNode<T>(result: T) : VBox() {
init {
spacing = 10.0
padding = Insets(5.0, 5.0, 5.0, 5.0)
if (result == null) {
label("No return value from flow.")
} else if (result is SignedTransaction) {
// scrollpane {
// hbarPolicy = ScrollPane.ScrollBarPolicy.AS_NEEDED
// vbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
// TODO Make link to transaction view
label("Signed transaction")
label {
text = result.id.toString()
graphic = identicon(result.id, 30.0)
tooltip = identiconToolTip(result.id)
}
// }
} else if (result is Unit) {
label("Flow completed with success.")
}
else {
// TODO Here we could have sth different than SignedTransaction
label(result.toString())
}
}
}
// TODO make that Vbox part of FXML
private inner class ErrorNode(val error: Throwable) : VBox() {
init {
vbox {
spacing = 10.0
padding = Insets(5.0, 5.0, 5.0, 5.0)
label("Error") {
graphic = FontAwesomeIconView(FontAwesomeIcon.BOLT).apply {
glyphSize = 30
textAlignment = TextAlignment.CENTER
style = "-fx-fill: -color-4"
}
}
// TODO think of border styling
vbox {
spacing = 10.0
label { text = error::class.simpleName }
scrollpane {
hbarPolicy = ScrollPane.ScrollBarPolicy.AS_NEEDED
vbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
label { text = error.message }
}
}
}
}
}
private inner class ShellNode : Label() {
init {
label("Flow started by shell user")
}
}
// TODO make it more generic, reuse gridpane definition - to fxml
private inner class PeerNode(val initiator: FlowInitiator.Peer): GridPane() {
init {
gridpane { gridpane {
padding = Insets(0.0, 5.0, 10.0, 10.0) padding = Insets(0.0, 5.0, 10.0, 10.0)
vgap = 10.0 vgap = 10.0
hgap = 10.0 hgap = 10.0
row { row {
label("${contractState.contract().javaClass.simpleName} (${contractState.ref.toString().substring(0, 16)}...)[${contractState.ref.index}]") { label("Flow started by a peer node") {
graphic = identicon(contractState.ref.txhash, 30.0) gridpaneConstraints {
tooltip = identiconToolTip(contractState.ref.txhash) columnSpan = 2
gridpaneConstraints { columnSpan = 2 } hAlignment = HPos.CENTER
} }
} }
val data = contractState.state.data }
when (data) { // scrollpane { // TODO scrollbar vbox + hbox
is Cash.State -> { // hbarPolicy = ScrollPane.ScrollBarPolicy.AS_NEEDED
// vbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
row { row {
label("Amount :") { gridpaneConstraints { hAlignment = HPos.RIGHT } } label("Legal name: ") {
label(AmountFormatter.boring.format(data.amount.withoutIssuer())) gridpaneConstraints { hAlignment = HPos.LEFT }
style { fontWeight = FontWeight.BOLD }
minWidth = 150.0
prefWidth = 150.0
}
label(initiator.party.name) { gridpaneConstraints { hAlignment = HPos.LEFT } }
} }
row { row {
label("Issuer :") { gridpaneConstraints { hAlignment = HPos.RIGHT } } label("Owning key: ") {
label("${data.amount.token.issuer}") { gridpaneConstraints { hAlignment = HPos.LEFT }
tooltip(data.amount.token.issuer.party.owningKey.toBase58String()) style { fontWeight = FontWeight.BOLD }
minWidth = 150.0
prefWidth = 150.0
}
label(initiator.party.owningKey.toBase58String()) { gridpaneConstraints { hAlignment = HPos.LEFT } }
}
}
}
}
private inner class RPCNode(val initiator: FlowInitiator.RPC) : GridPane() {
init {
gridpane {
padding = Insets(0.0, 5.0, 10.0, 10.0)
vgap = 10.0
hgap = 10.0
row {
label("Flow started by a RPC user") {
gridpaneConstraints {
columnSpan = 2
hAlignment = HPos.CENTER
}
} }
} }
row { row {
label("Owner :") { gridpaneConstraints { hAlignment = HPos.RIGHT } } label("User name: ") {
val owner = data.owner gridpaneConstraints { hAlignment = HPos.LEFT }
val nodeInfo = getModel<NetworkIdentityModel>().lookup(owner) style { fontWeight = FontWeight.BOLD }
label(nodeInfo.map { it?.legalIdentity?.name ?: "???" }) { prefWidth = 150.0
tooltip(data.owner.toBase58String()) }
label(initiator.username) { gridpaneConstraints { hAlignment = HPos.LEFT } }
} }
} }
} }
// TODO : Generic view using reflection?
else -> label {}
}
}
}()
}
} }
private fun StateAndRef<ContractState>.contract() = this.state.data.contract // TODO test
private inner class ScheduledNode(val initiator: FlowInitiator.Scheduled) : GridPane() {
init {
gridpane {
padding = Insets(0.0, 5.0, 10.0, 10.0)
vgap = 10.0
hgap = 10.0
row {
label("Flow started as scheduled activity")
gridpaneConstraints {
columnSpan = 2
hAlignment = HPos.CENTER
}
}
row {
label("Scheduled state: ") {
gridpaneConstraints { hAlignment = HPos.LEFT }
style { fontWeight = FontWeight.BOLD }
prefWidth = 150.0
}
label(initiator.scheduledState.ref.toString()) { gridpaneConstraints { hAlignment = HPos.LEFT } } //TODO format
}
row {
label("Scheduled at: ") {
gridpaneConstraints { hAlignment = HPos.LEFT }
style { fontWeight = FontWeight.BOLD }
prefWidth = 150.0
}
label(initiator.scheduledState.scheduledAt.toString()) { gridpaneConstraints { hAlignment = HPos.LEFT } } //TODO format
}
}
}
} }
/**
* We calculate the total value by subtracting relevant input states and adding relevant output states, as long as they're cash
*/
private fun calculateTotalEquiv(identity: NodeInfo?,
reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>,
inputs: List<ContractState>,
outputs: List<ContractState>): AmountDiff<Currency> {
val (reportingCurrency, exchange) = reportingCurrencyExchange
val publicKey = identity?.legalIdentity?.owningKey
fun List<ContractState>.sum() = this.map { it as? Cash.State }
.filterNotNull()
.filter { publicKey == it.owner }
.map { exchange(it.amount.withoutIssuer()).quantity }
.sum()
// For issuing cash, if I am the issuer and not the owner (e.g. issuing cash to other party), count it as negative.
val issuedAmount = if (inputs.isEmpty()) outputs.map { it as? Cash.State }
.filterNotNull()
.filter { publicKey == it.amount.token.issuer.party.owningKey && publicKey != it.owner }
.map { exchange(it.amount.withoutIssuer()).quantity }
.sum() else 0
return AmountDiff.fromLong(outputs.sum() - inputs.sum() - issuedAmount, reportingCurrency)
} }
*/

View File

@ -117,7 +117,10 @@ class TransactionViewer : CordaView("Transactions") {
// Transaction table // Transaction table
transactionViewTable.apply { transactionViewTable.apply {
items = searchField.filteredData items = searchField.filteredData
column("Transaction ID", Transaction::id) { maxWidth = 200.0 }.setCustomCellFactory { column("Transaction ID", Transaction::id) {
minWidth = 20.0
maxWidth = 200.0
}.setCustomCellFactory {
label("$it") { label("$it") {
graphic = identicon(it, 15.0) graphic = identicon(it, 15.0)
tooltip = identiconToolTip(it) tooltip = identiconToolTip(it)
@ -159,10 +162,10 @@ class TransactionViewer : CordaView("Transactions") {
add(ContractStatesView(it).root) add(ContractStatesView(it).root)
prefHeight = 400.0 prefHeight = 400.0
}.apply { }.apply {
prefWidth = 26.0 // Column stays the same size, but we don't violate column restricted resize policy for the whole table view.
isResizable = false minWidth = 26.0
maxWidth = 26.0
} }
setColumnResizePolicy { true }
} }
matchingTransactionsLabel.textProperty().bind(Bindings.size(transactionViewTable.items).map { matchingTransactionsLabel.textProperty().bind(Bindings.size(transactionViewTable.items).map {
"$it matching transaction${if (it == 1) "" else "s"}" "$it matching transaction${if (it == 1) "" else "s"}"

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>
<BorderPane stylesheets="@../css/corda.css" xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1">
<padding>
<Insets bottom="5" left="5" right="5" top="5" />
</padding>
<center>
<TableView fx:id="flowViewTable" VBox.vgrow="ALWAYS">
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
</columnResizePolicy>
<columns>
<TableColumn fx:id="flowColumnSessionId" prefWidth="10.0" styleClass="first-column" text="Session ID" />
<TableColumn fx:id="flowColumnInternalId" prefWidth="73.0" styleClass="first-column" text="Flow Internal ID" />
<TableColumn fx:id="flowColumnState" prefWidth="173.0" styleClass="first-column" text="Flow Status" />
</columns>
</TableView>
</center>
<bottom>
<Label fx:id="matchingFlowsLabel" text="matching flow(s)" />
</bottom>
</BorderPane>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TitledPane?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.RowConstraints?>
<GridPane stylesheets="@../css/corda.css" xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1">
<padding>
<Insets bottom="5" left="5" right="5" top="5" />
</padding>
<TitledPane fx:id="flowNamePane" collapsible="false" text="Flow name" GridPane.columnSpan="2" GridPane.fillWidth="true" GridPane.rowIndex="0">
<Label fx:id="flowNameLabel"/>
</TitledPane>
<TitledPane fx:id="flowInitiatorPane" collapsible="false" maxHeight="Infinity" text="Flow initiator"
GridPane.columnIndex="0" GridPane.fillWidth="true" GridPane.rowIndex="1">
<!-- todo add gridpane - from code! -->
</TitledPane>
<TitledPane fx:id="flowResultPane" collapsible="false" maxHeight="Infinity" text="Result" GridPane.columnIndex="1" GridPane.rowIndex="1">
<!-- todo add vbox - from code! -->
</TitledPane>
<TitledPane fx:id="flowProgressPane" collapsible="false" maxWidth="Infinity" text="Progress steps" GridPane.columnSpan="2" GridPane.rowIndex="2">
</TitledPane>
<columnConstraints>
<ColumnConstraints minWidth="450.0"/>
<ColumnConstraints hgrow="ALWAYS"/>
</columnConstraints>
<rowConstraints>
<RowConstraints vgrow="ALWAYS"/>
<RowConstraints vgrow="ALWAYS"/>
<RowConstraints vgrow="ALWAYS"/>
</rowConstraints>
</GridPane>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.control.Tab?>
<TabPane stylesheets="@../css/corda.css" xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1"
tabClosingPolicy="UNAVAILABLE">
<padding>
<Insets bottom="5" left="5" right="5" top="5"/>
</padding>
<Tab text="In progress" fx:id="progressFlowsTab">
<TableView fx:id="progressViewTable" VBox.vgrow="ALWAYS">
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/>
</columnResizePolicy>
</TableView>
</Tab>
<Tab text="Finished" fx:id="doneFlowsTab">
<TableView fx:id="doneViewTable" VBox.vgrow="ALWAYS">
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/>
</columnResizePolicy>
</TableView>
</Tab>
<Tab text="With errors" fx:id="errorFlowsTab">
<TableView fx:id="errorViewTable" VBox.vgrow="ALWAYS">
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/>
</columnResizePolicy>
</TableView>
</Tab>
<!--<bottom>-->
<!--<Label fx:id="matchingFlowsLabel" text="matching flow(s)" />-->
<!--</bottom>-->
</TabPane>