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.core.flows.StateMachineRunId
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.StateMachineInfo
import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.node.services.NetworkMapCache.MapChange
import net.corda.core.node.services.StateMachineTransactionMapping
import net.corda.core.node.services.Vault
import net.corda.core.seconds
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker
import rx.Observable
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.
*/

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
import javafx.beans.binding.Bindings
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.collections.ObservableMap
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.StateRef
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 org.fxmisc.easybind.EasyBind
@ -59,159 +54,17 @@ data class PartiallyResolvedTransaction(
}
}
data class FlowStatus(val status: String)
//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
)
// TODO Do we want to have mapping tx <-> StateMachine?
/**
* This model provides an observable list of transactions and what state machines/flows recorded them
*/
class TransactionDataModel {
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 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 {
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
data class StateMachineInfo(
val id: StateMachineRunId,
val sessionId: Long,
val flowLogicClassName: String,
val initiator: FlowInitiator,
val progressTrackerStepAndUpdates: Pair<String, Observable<String>>?

View File

@ -131,7 +131,7 @@ class Main : App(MainView::class) {
// Stock Views.
registerView<Dashboard>()
registerView<TransactionViewer>()
registerView<FlowViewer>()
registerView<StateMachineViewer>()
// CordApps Views.
registerView<CashViewer>()
// 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
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView
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.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.TitledPane
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.StateMachineStatus
import net.corda.client.jfx.model.observableList
import net.corda.client.jfx.model.observableListReadOnly
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.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.identiconToolTip
import net.corda.explorer.model.CordaView
import net.corda.explorer.model.CordaWidget
import net.corda.explorer.ui.setCustomCellFactory
import tornadofx.column
import tornadofx.label
import tornadofx.observable
import tornadofx.right
import tornadofx.*
class FlowViewer : CordaView("Flow Triage") {
override val root by fxml<BorderPane>()
// TODO Rethink whole idea of showing communication as table, it should be tree view for each StateMachine (with subflows and other communication)
class StateMachineViewer : CordaView("Flow Triage") {
override val root by fxml<TabPane>()
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 val flowColumnSessionId by fxid<TableColumn<Flow, Int>>()
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)
private class StateMachineWidget : BorderPane() {
private val flows by observableListReadOnly(StateMachineDataModel::stateMachinesInProgress)
// TODO can add stats: in progress, errored, maybe done to the widget?
init {
right {
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)
// private val flows = stateMachines.map { it -> Flow(it.id.toString(), it.flowStatus.map { it.toString() }) }.filtered { ! it.latestProgress.value.contains("Done") }
private val flows = stateMachines.map { it -> Flow(it.id.toString(), it.flowStatus.toString()) }.filtered {
println("--> $it")
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)
fun makeColumns(table: TableView<StateMachineData>, tableItems: ObservableList<StateMachineData>, withResult: Boolean = true) {
table.apply {
items = tableItems
if (withResult) {
rowExpander(expandOnDoubleClick = true) {
add(StateMachineDetailsView(it).root)
}.apply {
// Column stays the same size, but we don't violate column restricted resize policy for the whole table view.
minWidth = 26.0
maxWidth = 26.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 {
column("ID", StateMachineData::id) { // TODO kill that ID column
minWidth = 100.0
maxWidth = 200.0
}.setCustomCellFactory {
label("$it") {
graphic = identicon(it, 15.0)
tooltip = identiconToolTip(it)
val hash = SecureHash.sha256(it.toString())
graphic = identicon(hash, 15.0)
tooltip = identiconToolTip(hash) //TODO Have id instead of hash.
}
}
column("Input", Transaction::inputs).cellFormat {
text = it.resolved.toText()
if (!it.unresolved.isEmpty()) {
if (!text.isBlank()) {
text += ", "
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"
}
}
}
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 {
prefWidth = 26.0
isResizable = false
}
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 {
right {
label {
textProperty().bind(Bindings.size(partiallyResolvedTransactions).map(Number::toString))
BorderPane.setAlignment(this, Pos.BOTTOM_RIGHT)
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"
}
}
}
}
}
}
private inner class ContractStatesView(transaction: Transaction) : Fragment() {
init {
makeColumns(progressViewTable, stateMachinesInProgress, false)
makeColumns(doneViewTable, stateMachinesFinished)
makeColumns(errorViewTable, stateMachinesError)
}
private inner class StateMachineDetailsView(val smmData: StateMachineData) : Fragment() {
override val root by fxml<Parent>()
private val inputs by fxid<ListView<StateAndRef<ContractState>>>()
private val outputs by fxid<ListView<StateAndRef<ContractState>>>()
private val signatures by fxid<VBox>()
private val inputPane by fxid<TitledPane>()
private val outputPane by fxid<TitledPane>()
private val signaturesPane by fxid<TitledPane>()
private val flowNamePane by fxid<TitledPane>()
private val flowProgressPane by fxid<TitledPane>()
private val flowInitiatorPane by fxid<TitledPane>()
private val flowResultPane by fxid<TitledPane>()
init {
val signatureData = transaction.tx.transaction.sigs.map { it.by }
// Bind count to TitlePane
inputPane.text = "Input (${transaction.inputs.resolved.count()})"
outputPane.text = "Output (${transaction.outputs.count()})"
signaturesPane.text = "Signatures (${signatureData.count()})"
inputs.cellCache { getCell(it) }
outputs.cellCache { getCell(it) }
inputs.items = transaction.inputs.resolved
outputs.items = transaction.outputs.observable()
signatures.children.addAll(signatureData.map { signature ->
val nodeInfo = getModel<NetworkIdentityModel>().lookup(signature)
copyableLabel(nodeInfo.map { "${signature.toStringShort()} (${it?.legalIdentity?.name ?: "???"})" })
})
}
private fun getCell(contractState: StateAndRef<ContractState>): Node {
return {
gridpane {
padding = Insets(0.0, 5.0, 10.0, 10.0)
vgap = 10.0
hgap = 10.0
row {
label("${contractState.contract().javaClass.simpleName} (${contractState.ref.toString().substring(0, 16)}...)[${contractState.ref.index}]") {
graphic = identicon(contractState.ref.txhash, 30.0)
tooltip = identiconToolTip(contractState.ref.txhash)
gridpaneConstraints { columnSpan = 2 }
}
}
val data = contractState.state.data
when (data) {
is Cash.State -> {
row {
label("Amount :") { gridpaneConstraints { hAlignment = HPos.RIGHT } }
label(AmountFormatter.boring.format(data.amount.withoutIssuer()))
}
row {
label("Issuer :") { gridpaneConstraints { hAlignment = HPos.RIGHT } }
label("${data.amount.token.issuer}") {
tooltip(data.amount.token.issuer.party.owningKey.toBase58String())
}
}
row {
label("Owner :") { gridpaneConstraints { hAlignment = HPos.RIGHT } }
val owner = data.owner
val nodeInfo = getModel<NetworkIdentityModel>().lookup(owner)
label(nodeInfo.map { it?.legalIdentity?.name ?: "???" }) {
tooltip(data.owner.toBase58String())
}
}
}
// TODO : Generic view using reflection?
else -> label {}
}
flowNamePane.apply {
content = label {
text = FlowNameFormatter.boring.format(smmData.stateMachineName)
}
}()
}
flowProgressPane.apply {
content = label {
text = smmData.stateMachineStatus.value?.status // TODO later we can do some magic with showing progress steps with subflows
}
}
flowInitiatorPane.apply {
//TODO use fxml to access initiatorGridPane
// initiatorGridPane.apply {when...
content = when (smmData.flowInitiator) {
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 StateAndRef<ContractState>.contract() = this.state.data.contract
// TODO make that Vbox part of FXML
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 {
padding = Insets(0.0, 5.0, 10.0, 10.0)
vgap = 10.0
hgap = 10.0
row {
label("Flow started by a peer node") {
gridpaneConstraints {
columnSpan = 2
hAlignment = HPos.CENTER
}
}
}
// scrollpane { // TODO scrollbar vbox + hbox
// hbarPolicy = ScrollPane.ScrollBarPolicy.AS_NEEDED
// vbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
row {
label("Legal name: ") {
gridpaneConstraints { hAlignment = HPos.LEFT }
style { fontWeight = FontWeight.BOLD }
minWidth = 150.0
prefWidth = 150.0
}
label(initiator.party.name) { gridpaneConstraints { hAlignment = HPos.LEFT } }
}
row {
label("Owning key: ") {
gridpaneConstraints { hAlignment = HPos.LEFT }
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 {
label("User name: ") {
gridpaneConstraints { hAlignment = HPos.LEFT }
style { fontWeight = FontWeight.BOLD }
prefWidth = 150.0
}
label(initiator.username) { gridpaneConstraints { hAlignment = HPos.LEFT } }
}
}
}
}
// 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
transactionViewTable.apply {
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") {
graphic = identicon(it, 15.0)
tooltip = identiconToolTip(it)
@ -159,10 +162,10 @@ class TransactionViewer : CordaView("Transactions") {
add(ContractStatesView(it).root)
prefHeight = 400.0
}.apply {
prefWidth = 26.0
isResizable = false
// Column stays the same size, but we don't violate column restricted resize policy for the whole table view.
minWidth = 26.0
maxWidth = 26.0
}
setColumnResizePolicy { true }
}
matchingTransactionsLabel.textProperty().bind(Bindings.size(transactionViewTable.items).map {
"$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>