Change tabs to search field in state machine view. Some carving of UI.

This commit is contained in:
Katarzyna Streich 2017-05-11 19:04:17 +01:00
parent e402f4d5af
commit 608550c920
15 changed files with 141 additions and 147 deletions

View File

@ -4,7 +4,6 @@ import com.google.common.net.HostAndPort
import javafx.beans.property.SimpleObjectProperty import javafx.beans.property.SimpleObjectProperty
import net.corda.client.rpc.CordaRPCClient 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.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
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
@ -63,7 +62,9 @@ class NodeMonitorModel {
Observable.empty<ProgressTrackingEvent>() Observable.empty<ProgressTrackingEvent>()
} }
} }
futureProgressTrackerUpdates.startWith(currentProgressTrackerUpdates).flatMap { it }.subscribe(progressTrackingSubject)
// We need to retry, because when flow errors, we unsubscribe from progressTrackingSubject. So we end up with stream of state machine updates and no progress trackers.
futureProgressTrackerUpdates.startWith(currentProgressTrackerUpdates).flatMap { it }.retry().subscribe(progressTrackingSubject)
// Now the state machines // Now the state machines
val currentStateMachines = stateMachines.map { StateMachineUpdate.Added(it) } val currentStateMachines = stateMachines.map { StateMachineUpdate.Added(it) }

View File

@ -1,20 +1,18 @@
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.property.SimpleObjectProperty
import javafx.beans.value.ObservableValue import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections import javafx.collections.FXCollections
import net.corda.client.jfx.utils.LeftOuterJoinedMap 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.fold
import net.corda.client.jfx.utils.getObservableValues import net.corda.client.jfx.utils.getObservableValues
import net.corda.client.jfx.utils.map
import net.corda.client.jfx.utils.recordAsAssociation import net.corda.client.jfx.utils.recordAsAssociation
import net.corda.core.ErrorOr import net.corda.core.ErrorOr
import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowInitiator
import net.corda.core.flows.StateMachineRunId import net.corda.core.flows.StateMachineRunId
import net.corda.core.messaging.StateMachineInfo import net.corda.core.messaging.StateMachineInfo
import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.StateMachineUpdate
import org.fxmisc.easybind.EasyBind
import rx.Observable import rx.Observable
data class ProgressTrackingEvent(val stateMachineId: StateMachineRunId, val message: String) { data class ProgressTrackingEvent(val stateMachineId: StateMachineRunId, val message: String) {
@ -28,20 +26,19 @@ data class ProgressTrackingEvent(val stateMachineId: StateMachineRunId, val mess
} }
} }
data class ProgressStatus(val status: String) data class ProgressStatus(val status: String?)
sealed class StateMachineStatus { sealed class StateMachineStatus {
data class Added(val stateMachineName: String, val flowInitiator: FlowInitiator) : StateMachineStatus() data class Added(val stateMachineName: String, val flowInitiator: FlowInitiator) : StateMachineStatus()
data class Removed(val result: ErrorOr<*>) : StateMachineStatus() data class Removed(val result: ErrorOr<*>) : StateMachineStatus()
} }
// TODO StateMachineData and StateMachineInfo
data class StateMachineData( data class StateMachineData(
val id: StateMachineRunId, val id: StateMachineRunId,
val stateMachineName: String, val stateMachineName: String,
val flowInitiator: FlowInitiator, val flowInitiator: FlowInitiator,
val addRmStatus: ObservableValue<StateMachineStatus>, val addRmStatus: ObservableValue<StateMachineStatus>,
val stateMachineStatus: ObservableValue<ProgressStatus?> val stateMachineStatus: ObservableValue<ProgressStatus>
) )
class StateMachineDataModel { class StateMachineDataModel {
@ -65,22 +62,17 @@ class StateMachineDataModel {
} }
} }
val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress -> private val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
val smStatus = status.value as StateMachineStatus.Added // TODO not always added val smStatus = status.value as StateMachineStatus.Added
// todo easybind StateMachineData(id, smStatus.stateMachineName, smStatus.flowInitiator, status,
Bindings.createObjectBinding({ EasyBind.map(progress) {
StateMachineData(id, smStatus.stateMachineName, smStatus.flowInitiator, status, progress.map { it?.let { ProgressStatus(it.message) } }) if (it == null) {
}, arrayOf(progress, status)) ProgressStatus(null)
}.getObservableValues().flatten() } else {
ProgressStatus(it.message)
}
})
}.getObservableValues()
val stateMachinesInProgress = stateMachineDataList.filtered { it.addRmStatus.value !is StateMachineStatus.Removed } val stateMachinesAll = stateMachineDataList
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

@ -54,7 +54,6 @@ data class PartiallyResolvedTransaction(
} }
} }
// TODO Do we want to have mapping tx <-> StateMachine?
/** /**
* 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
*/ */

View File

@ -18,7 +18,7 @@ open class EventGenerator(val parties: List<Party>, val currencies: List<Currenc
protected val issueRefGenerator = Generator.intRange(0, 1).map { number -> OpaqueBytes(ByteArray(1, { number.toByte() })) } protected val issueRefGenerator = Generator.intRange(0, 1).map { number -> OpaqueBytes(ByteArray(1, { number.toByte() })) }
protected val amountGenerator = Generator.longRange(10000, 1000000) protected val amountGenerator = Generator.longRange(10000, 1000000)
protected val currencyGenerator = Generator.pickOne(currencies) protected val currencyGenerator = Generator.pickOne(currencies)
protected val currencyMap: MutableMap<Currency, Long> = mutableMapOf(USD to 0L, GBP to 0L) // Used for rough estimation of how much money we have in general. protected val currencyMap: MutableMap<Currency, Long> = mutableMapOf(USD to 0L, GBP to 0L) // Used for estimation of how much money we have in general.
protected fun addToMap(ccy: Currency, amount: Long) { protected fun addToMap(ccy: Currency, amount: Long) {
val value = currencyMap[ccy] val value = currencyMap[ccy]
@ -47,7 +47,7 @@ open class EventGenerator(val parties: List<Party>, val currencies: List<Currenc
} }
/** /**
* [Generator]s for incoming/outgoing events of starting different [Cash] flows. It invokes flows that throw exceptions * [Generator]s for incoming/outgoing events of starting different cash flows. It invokes flows that throw exceptions
* for use in explorer flow triage. Exceptions are of kind spending/exiting too much cash. * for use in explorer flow triage. Exceptions are of kind spending/exiting too much cash.
*/ */
class ErrorFlowsEventGenerator(parties: List<Party>, currencies: List<Currency>, notary: Party): EventGenerator(parties, currencies, notary) { class ErrorFlowsEventGenerator(parties: List<Party>, currencies: List<Currency>, notary: Party): EventGenerator(parties, currencies, notary) {

View File

@ -217,7 +217,7 @@ abstract class FlowLogic<out T> {
fun track(): Pair<String, Observable<String>>? { fun track(): Pair<String, Observable<String>>? {
// TODO this is not threadsafe, needs an atomic get-step-and-subscribe // TODO this is not threadsafe, needs an atomic get-step-and-subscribe
return progressTracker?.let { return progressTracker?.let {
it.currentStep.toString() to it.changes.map { it.toString() } it.currentStep.label to it.changes.map { it.toString() }
} }
} }

View File

@ -5,14 +5,13 @@ import net.corda.client.mock.ErrorFlowsEventGenerator
import net.corda.client.mock.EventGenerator import net.corda.client.mock.EventGenerator
import net.corda.client.mock.Generator import net.corda.client.mock.Generator
import net.corda.client.mock.pickOne import net.corda.client.mock.pickOne
import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.CordaRPCConnection import net.corda.client.rpc.CordaRPCConnection
import net.corda.contracts.asset.Cash import net.corda.contracts.asset.Cash
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.contracts.GBP import net.corda.core.contracts.GBP
import net.corda.core.contracts.USD import net.corda.core.contracts.USD
import net.corda.core.crypto.Party
import net.corda.core.failure import net.corda.core.failure
import net.corda.core.identity.Party
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.FlowHandle import net.corda.core.messaging.FlowHandle
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
@ -154,7 +153,7 @@ class ExplorerSimulation(val options: OptionSet) {
is CashFlowCommand.IssueCash -> issuers[command.amount.token]?.let { is CashFlowCommand.IssueCash -> issuers[command.amount.token]?.let {
println("${Instant.now()} [$i] ISSUING ${command.amount} with ref ${command.issueRef} to ${command.recipient}") println("${Instant.now()} [$i] ISSUING ${command.amount} with ref ${command.issueRef} to ${command.recipient}")
command.startFlow(it).log(i, "${command.amount.token}Issuer") command.startFlow(it).log(i, "${command.amount.token}Issuer")
} ?: command.startFlow(issuers[USD]!!).log(i, "${command.amount.token}Issuer") // TODO workaround }
is CashFlowCommand.ExitCash -> issuers[command.amount.token]?.let { is CashFlowCommand.ExitCash -> issuers[command.amount.token]?.let {
println("${Instant.now()} [$i] EXITING ${command.amount} with ref ${command.issueRef}") println("${Instant.now()} [$i] EXITING ${command.amount} with ref ${command.issueRef}")
command.startFlow(it).log(i, "${command.amount.token}Exit") command.startFlow(it).log(i, "${command.amount.token}Exit")

View File

@ -5,9 +5,9 @@ import net.corda.core.flows.FlowInitiator
object FlowInitiatorFormatter : Formatter<FlowInitiator> { object FlowInitiatorFormatter : Formatter<FlowInitiator> {
override fun format(value: FlowInitiator): String { override fun format(value: FlowInitiator): String {
return when (value) { return when (value) {
is FlowInitiator.Scheduled -> "Started by scheduled state:: " + value.scheduledState.ref.toString() // TODO format that is FlowInitiator.Scheduled -> "Started by scheduled state:: " + value.scheduledState.ref.toString() // TODO How do we want to format that?
is FlowInitiator.Shell -> "Started via shell" is FlowInitiator.Shell -> "Started via shell"
is FlowInitiator.Peer -> "Peer legal name: " + value.party.name //TODO format that is FlowInitiator.Peer -> "Peer legal name: " + PartyNameFormatter.short.format(value.party.name)
is FlowInitiator.RPC -> "Rpc username: " + value.username is FlowInitiator.RPC -> "Rpc username: " + value.username
} }
} }

View File

@ -198,8 +198,8 @@ fun identicon(secureHash: SecureHash, size: Double): ImageView {
} }
} }
fun identiconToolTip(secureHash: SecureHash): Tooltip { fun identiconToolTip(secureHash: SecureHash, description: String? = null): Tooltip {
return Tooltip(Splitter.fixedLength(16).split("$secureHash").joinToString("\n")).apply { return Tooltip(Splitter.fixedLength(16).split("${description ?: secureHash}").joinToString("\n")).apply {
contentDisplay = ContentDisplay.TOP contentDisplay = ContentDisplay.TOP
textAlignment = TextAlignment.CENTER textAlignment = TextAlignment.CENTER
graphic = identicon(secureHash, 90.0) graphic = identicon(secureHash, 90.0)

View File

@ -23,7 +23,8 @@ import tornadofx.*
* TODO : Predictive text? * TODO : Predictive text?
* TODO : Regex? * TODO : Regex?
*/ */
class SearchField<T>(private val data: ObservableList<T>, vararg filterCriteria: Pair<String, (T, String) -> Boolean>) : UIComponent() { class SearchField<T>(private val data: ObservableList<T>, val filterCriteria: List<Pair<String, (T, String) -> Boolean>>,
val disabledFields: List<String> = emptyList()) : UIComponent() {
override val root: Parent by fxml() override val root: Parent by fxml()
private val textField by fxid<TextField>() private val textField by fxid<TextField>()
private val clearButton by fxid<Node>() private val clearButton by fxid<Node>()
@ -34,7 +35,7 @@ class SearchField<T>(private val data: ObservableList<T>, vararg filterCriteria:
val text = textField.text val text = textField.text
val category = searchCategory.value val category = searchCategory.value
data.filtered { data -> data.filtered { data ->
text.isNullOrBlank() || if (category == ALL) { (text.isNullOrBlank() && textField.isVisible) || if (category == ALL) {
filterCriteria.any { it.second(data, text) } filterCriteria.any { it.second(data, text) }
} else { } else {
filterCriteria.toMap()[category]?.invoke(data, text) ?: false filterCriteria.toMap()[category]?.invoke(data, text) ?: false
@ -73,5 +74,7 @@ class SearchField<T>(private val data: ObservableList<T>, vararg filterCriteria:
} }
"Filter by $category." "Filter by $category."
}) })
textField.visibleProperty().bind(searchCategory.valueProperty().map { it !in disabledFields })
// TODO Maybe it will be better to replace these categories with comboBox? For example Result with choice: succes, in progress, error.
} }
} }

View File

@ -5,14 +5,10 @@ import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
import javafx.collections.ObservableList import javafx.collections.ObservableList
import javafx.geometry.HPos import javafx.geometry.HPos
import javafx.geometry.Insets
import javafx.geometry.Pos import javafx.geometry.Pos
import javafx.scene.Parent import javafx.scene.Parent
import javafx.scene.control.Label 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.GridPane
import javafx.scene.layout.VBox import javafx.scene.layout.VBox
@ -37,33 +33,29 @@ import net.corda.explorer.model.CordaWidget
import net.corda.explorer.ui.setCustomCellFactory import net.corda.explorer.ui.setCustomCellFactory
import tornadofx.* import tornadofx.*
// TODO Rethink whole idea of showing communication as table, it should be tree view for each StateMachine (with subflows and other communication) // TODO Rethink whole idea of showing communication as table, it should be tree view for each StateMachine (with subflows).
class StateMachineViewer : CordaView("Flow Triage") { class StateMachineViewer : CordaView("Flow Triage") {
override val root by fxml<TabPane>() override val root by fxml<BorderPane>()
override val icon = FontAwesomeIcon.HEARTBEAT override val icon = FontAwesomeIcon.HEARTBEAT
override val widgets = listOf(CordaWidget(title, StateMachineViewer.StateMachineWidget())).observable() override val widgets = listOf(CordaWidget(title, StateMachineViewer.StateMachineWidget())).observable()
private val progressViewTable by fxid<TableView<StateMachineData>>() private val allViewTable by fxid<TableView<StateMachineData>>()
private val doneViewTable by fxid<TableView<StateMachineData>>() private val matchingFlowsLabel by fxid<Label>()
private val errorViewTable by fxid<TableView<StateMachineData>>()
private val stateMachinesAll by observableList(StateMachineDataModel::stateMachinesAll)
private class StateMachineWidget : BorderPane() { private class StateMachineWidget : BorderPane() {
private val flows by observableListReadOnly(StateMachineDataModel::stateMachinesInProgress) private val stateMachinesAll by observableListReadOnly(StateMachineDataModel::stateMachinesAll)
// TODO can add stats: in progress, errored, maybe done to the widget?
init { init {
right { right {
label { label {
textProperty().bind(Bindings.size(flows).map(Number::toString)) textProperty().bind(Bindings.size(stateMachinesAll).map(Number::toString))
BorderPane.setAlignment(this, Pos.BOTTOM_RIGHT) BorderPane.setAlignment(this, Pos.BOTTOM_RIGHT)
} }
} }
} }
} }
private val stateMachinesInProgress by observableList(StateMachineDataModel::stateMachinesInProgress)
private val stateMachinesFinished by observableList(StateMachineDataModel::stateMachinesFinished)
private val stateMachinesError by observableList(StateMachineDataModel::stateMachinesError)
fun makeColumns(table: TableView<StateMachineData>, tableItems: ObservableList<StateMachineData>, withResult: Boolean = true) { fun makeColumns(table: TableView<StateMachineData>, tableItems: ObservableList<StateMachineData>, withResult: Boolean = true) {
table.apply { table.apply {
items = tableItems items = tableItems
@ -76,23 +68,21 @@ class StateMachineViewer : CordaView("Flow Triage") {
maxWidth = 26.0 maxWidth = 26.0
} }
} }
column("ID", StateMachineData::id) { // TODO kill that ID column // TODO Kill that ID column or replace it with something useful when we will have flow audit utilities.
// For now it's rather for visual purpose, so you can observe flow.
column("ID", StateMachineData::id) {
minWidth = 100.0 minWidth = 100.0
maxWidth = 200.0 maxWidth = 200.0
}.setCustomCellFactory { }.setCustomCellFactory {
label("$it") { label("$it") {
val hash = SecureHash.sha256(it.toString()) val hash = SecureHash.sha256(it.toString())
graphic = identicon(hash, 15.0) graphic = identicon(hash, 15.0)
tooltip = identiconToolTip(hash) //TODO Have id instead of hash. tooltip = identiconToolTip(hash, it.toString())
} }
} }
column("Flow name", StateMachineData::stateMachineName).cellFormat { text = FlowNameFormatter.boring.format(it) } column("Flow name", StateMachineData::stateMachineName).cellFormat { text = FlowNameFormatter.boring.format(it) }
column("Initiator", StateMachineData::flowInitiator).cellFormat { text = FlowInitiatorFormatter.format(it) } column("Initiator", StateMachineData::flowInitiator).cellFormat { text = FlowInitiatorFormatter.format(it) }
column("Flow Status", StateMachineData::stateMachineStatus).cellFormat { column("Flow Status", StateMachineData::stateMachineStatus).cellFormat { text = it.status ?: "No progress data" }
if (it == null)
text = "No progress data"
else text = it.status
} // TODO null
column("Result", StateMachineData::addRmStatus).setCustomCellFactory { column("Result", StateMachineData::addRmStatus).setCustomCellFactory {
if (it is StateMachineStatus.Removed) { if (it is StateMachineStatus.Removed) {
if (it.result.error == null) { if (it.result.error == null) {
@ -115,8 +105,7 @@ class StateMachineViewer : CordaView("Flow Triage") {
} }
else { else {
label("In progress") { label("In progress") {
// TODO Other icons: spnner, hourglass-half, hourglass-1, send-o, space-shuttle ;) graphic = FontAwesomeIconView(FontAwesomeIcon.ROCKET).apply { // Blazing fast! Try not to blink.
graphic = FontAwesomeIconView(FontAwesomeIcon.ROCKET).apply {
glyphSize = 15.0 glyphSize = 15.0
textAlignment = TextAlignment.CENTER textAlignment = TextAlignment.CENTER
style = "-fx-fill: lightslategrey" style = "-fx-fill: lightslategrey"
@ -128,15 +117,33 @@ class StateMachineViewer : CordaView("Flow Triage") {
} }
init { init {
makeColumns(progressViewTable, stateMachinesInProgress, false) val searchField = SearchField(stateMachinesAll, listOf(
makeColumns(doneViewTable, stateMachinesFinished) "Flow name" to { sm, s -> sm.stateMachineName.contains(s, true) },
makeColumns(errorViewTable, stateMachinesError) "Initiator" to { sm, s -> FlowInitiatorFormatter.format(sm.flowInitiator).contains(s, true) },
"Flow Status" to { sm, s -> val stat = sm.stateMachineStatus.value.status ?: "No progress data"
stat.contains(s, true) },
"Error" to { sm, _ -> val smAddRm = sm.addRmStatus.value
if (smAddRm is StateMachineStatus.Removed)
smAddRm.result.error != null
else false },
"Done" to { sm, _ -> val smAddRm = sm.addRmStatus.value
if (smAddRm is StateMachineStatus.Removed)
smAddRm.result.error == null
else false },
"In progress" to { sm, _ -> sm.addRmStatus.value !is StateMachineStatus.Removed }),
listOf("Error", "Done", "In progress")
)
root.top = searchField.root
makeColumns(allViewTable, searchField.filteredData)
matchingFlowsLabel.textProperty().bind(Bindings.size(allViewTable.items).map {
"$it matching flow${if (it == 1) "" else "s"}"
})
} }
private inner class StateMachineDetailsView(val smmData: StateMachineData) : Fragment() { private inner class StateMachineDetailsView(val smmData: StateMachineData) : Fragment() {
override val root by fxml<Parent>() override val root by fxml<Parent>()
private val flowNameLabel by fxid<Label>() private val flowNameLabel by fxid<Label>()
private val flowProgressPane by fxid<TitledPane>() private val flowProgressVBox by fxid<VBox>()
private val flowInitiatorGrid by fxid<GridPane>() private val flowInitiatorGrid by fxid<GridPane>()
private val flowResultVBox by fxid<VBox>() private val flowResultVBox by fxid<VBox>()
@ -144,9 +151,10 @@ class StateMachineViewer : CordaView("Flow Triage") {
flowNameLabel.apply { flowNameLabel.apply {
text = FlowNameFormatter.boring.format(smmData.stateMachineName) text = FlowNameFormatter.boring.format(smmData.stateMachineName)
} }
flowProgressPane.apply { //TODO It would be nice to have flow graph with showing progress steps with subflows + timestamps (left it for second iteration).
content = label { flowProgressVBox.apply {
text = smmData.stateMachineStatus.value?.status // TODO later we can do some magic with showing progress steps with subflows label {
text = smmData.stateMachineStatus.value?.status ?: "No progress data"
} }
} }
when (smmData.flowInitiator) { when (smmData.flowInitiator) {
@ -164,27 +172,27 @@ class StateMachineViewer : CordaView("Flow Triage") {
private fun <T>makeResultVBox(vbox: VBox, result: T) { private fun <T>makeResultVBox(vbox: VBox, result: T) {
if (result == null) { if (result == null) {
vbox.apply { label("No return value from flow.") } vbox.apply { label("No return value from flow.").apply { style { fontWeight = FontWeight.BOLD } } }
} else if (result is SignedTransaction) { } else if (result is SignedTransaction) {
// scrollpane {
// hbarPolicy = ScrollPane.ScrollBarPolicy.AS_NEEDED
// vbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
// TODO Make link to transaction view // TODO Make link to transaction view
vbox.apply { vbox.apply {
label("Signed transaction") label("Signed transaction").apply { style { fontWeight = FontWeight.BOLD } }
label { label {
text = result.id.toString() text = result.id.toString()
graphic = identicon(result.id, 30.0) graphic = identicon(result.id, 30.0)
tooltip = identiconToolTip(result.id) tooltip = identiconToolTip(result.id)
} }
// }
} }
} else if (result is Unit) { } else if (result is Unit) {
vbox.apply { label("Flow completed with success.") } vbox.apply { label("Flow completed with success.").apply { style { fontWeight = FontWeight.BOLD } } }
} }
else { else {
// TODO Here we could have sth different than SignedTransaction // TODO Here we could have sth different than SignedTransaction/Unit
vbox.apply { label(result.toString()) } vbox.apply {
label("Flow completed with success. Result: ").apply { style { fontWeight = FontWeight.BOLD } }
label(result.toString())
}
} }
} }
@ -198,21 +206,15 @@ class StateMachineViewer : CordaView("Flow Triage") {
} }
} }
} }
// TODO border styling?
vbox.apply { vbox.apply {
vbox { vbox {
spacing = 10.0 spacing = 10.0
label { text = error::class.simpleName } label { text = error::class.simpleName }
scrollpane { //TODO do that error scroll pane nicely label { text = error.message }
hbarPolicy = ScrollPane.ScrollBarPolicy.AS_NEEDED
vbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
label { text = error.message }
}
} }
} }
} }
// TODO test
private fun makeShellGrid(gridPane: GridPane) { private fun makeShellGrid(gridPane: GridPane) {
val title = gridPane.lookup("#flowInitiatorTitle") as Label val title = gridPane.lookup("#flowInitiatorTitle") as Label
title.apply { title.apply {
@ -226,9 +228,6 @@ class StateMachineViewer : CordaView("Flow Triage") {
text = "Flow started by a peer node" text = "Flow started by a peer node"
} }
gridPane.apply{ gridPane.apply{
// scrollpane { // TODO scrollbar vbox + hbox
// hbarPolicy = ScrollPane.ScrollBarPolicy.AS_NEEDED
// vbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
row { row {
label("Legal name: ") { label("Legal name: ") {
gridpaneConstraints { hAlignment = HPos.LEFT } gridpaneConstraints { hAlignment = HPos.LEFT }
@ -236,7 +235,7 @@ class StateMachineViewer : CordaView("Flow Triage") {
minWidth = 150.0 minWidth = 150.0
prefWidth = 150.0 prefWidth = 150.0
} }
label(initiator.party.name) { gridpaneConstraints { hAlignment = HPos.LEFT } } label(initiator.party.name.toString()) { gridpaneConstraints { hAlignment = HPos.LEFT } }
} }
row { row {
label("Owning key: ") { label("Owning key: ") {
@ -269,7 +268,7 @@ class StateMachineViewer : CordaView("Flow Triage") {
// TODO test // TODO test
private fun makeScheduledGrid(gridPane: GridPane, initiator: FlowInitiator.Scheduled) { private fun makeScheduledGrid(gridPane: GridPane, initiator: FlowInitiator.Scheduled) {
val title = gridPane.lookup("flowInitiatorTitle") as Label val title = gridPane.lookup("#flowInitiatorTitle") as Label
title.apply { title.apply {
text = "Flow started as scheduled activity" text = "Flow started as scheduled activity"
} }
@ -280,7 +279,7 @@ class StateMachineViewer : CordaView("Flow Triage") {
style { fontWeight = FontWeight.BOLD } style { fontWeight = FontWeight.BOLD }
prefWidth = 150.0 prefWidth = 150.0
} }
label(initiator.scheduledState.ref.toString()) { gridpaneConstraints { hAlignment = HPos.LEFT } } //TODO format label(initiator.scheduledState.ref.toString()) { gridpaneConstraints { hAlignment = HPos.LEFT } }
} }
row { row {
label("Scheduled at: ") { label("Scheduled at: ") {
@ -288,7 +287,7 @@ class StateMachineViewer : CordaView("Flow Triage") {
style { fontWeight = FontWeight.BOLD } style { fontWeight = FontWeight.BOLD }
prefWidth = 150.0 prefWidth = 150.0
} }
label(initiator.scheduledState.scheduledAt.toString()) { gridpaneConstraints { hAlignment = HPos.LEFT } } //TODO format label(initiator.scheduledState.scheduledAt.toString()) { gridpaneConstraints { hAlignment = HPos.LEFT } }
} }
} }
} }

View File

@ -105,13 +105,13 @@ class TransactionViewer : CordaView("Transactions") {
) )
} }
val searchField = SearchField(transactions, val searchField = SearchField(transactions, listOf(
"Transaction ID" to { tx, s -> "${tx.id}".contains(s, true) }, "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) } }, "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) } }, "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?.commonName?.contains(s, true) ?: false } } }, "Input Party" to { tx, s -> tx.inputParties.any { it.any { it.value?.legalIdentity?.name?.commonName?.contains(s, true) ?: false } } },
"Output Party" to { tx, s -> tx.outputParties.any { it.any { it.value?.legalIdentity?.name?.commonName?.contains(s, true) ?: false } } }, "Output Party" to { tx, s -> tx.outputParties.any { it.any { it.value?.legalIdentity?.name?.commonName?.contains(s, true) ?: false } } },
"Command Type" to { tx, s -> tx.commandTypes.any { it.simpleName.contains(s, true) } } "Command Type" to { tx, s -> tx.commandTypes.any { it.simpleName.contains(s, true) } })
) )
root.top = searchField.root root.top = searchField.root
// Transaction table // Transaction table
@ -163,6 +163,7 @@ class TransactionViewer : CordaView("Transactions") {
prefHeight = 400.0 prefHeight = 400.0
}.apply { }.apply {
// Column stays the same size, but we don't violate column restricted resize policy for the whole table view. // Column stays the same size, but we don't violate column restricted resize policy for the whole table view.
// It removes that irritating column at the end of table that does nothing.
minWidth = 26.0 minWidth = 26.0
maxWidth = 26.0 maxWidth = 26.0
} }

View File

@ -146,9 +146,9 @@ class CashViewer : CordaView("Cash") {
* one which produces more results, which seems to work, as the set of currency strings don't really overlap with * one which produces more results, which seems to work, as the set of currency strings don't really overlap with
* issuer strings. * issuer strings.
*/ */
val searchField = SearchField(cashStates, val searchField = SearchField(cashStates, listOf(
"Currency" to { state, text -> state.state.data.amount.token.product.toString().contains(text, true) }, "Currency" to { state, text -> state.state.data.amount.token.product.toString().contains(text, true) },
"Issuer" to { state, text -> state.resolveIssuer().value?.name?.commonName?.contains(text, true) ?: false } "Issuer" to { state, text -> state.resolveIssuer().value?.name?.commonName?.contains(text, true) ?: false })
) )
root.top = hbox(5.0) { root.top = hbox(5.0) {
button("New Transaction", FontAwesomeIconView(FontAwesomeIcon.PLUS)) { button("New Transaction", FontAwesomeIconView(FontAwesomeIcon.PLUS)) {

View File

@ -269,4 +269,11 @@
.connection-bank-to-regulator { .connection-bank-to-regulator {
-fx-stroke: red; -fx-stroke: red;
} }
/* Flow details */
.smm-detail-grid {
-fx-border-color: -color-3;
-fx-border-width: 1;
-fx-border-radius: 2;
}

View File

@ -2,47 +2,54 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.TitledPane?> <?import javafx.scene.control.TitledPane?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.ColumnConstraints?> <?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?> <?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<GridPane stylesheets="@../css/corda.css" xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1"> <GridPane styleClass="smm-detail-grid" stylesheets="@../css/corda.css" xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1">
<padding> <padding>
<Insets bottom="5" left="5" right="5" top="5" /> <Insets bottom="5" left="5" right="5" top="5" />
</padding> </padding>
<TitledPane fx:id="flowNamePane" collapsible="false" text="Flow name" GridPane.columnSpan="2" GridPane.fillWidth="true" GridPane.rowIndex="0"> <TitledPane fx:id="flowNamePane" collapsible="false" text="Flow name" GridPane.columnSpan="2" GridPane.fillWidth="true" GridPane.rowIndex="0">
<!--TODO text styling--> <Label fx:id="flowNameLabel" style="-fx-font-weight: bold"/>
<Label fx:id="flowNameLabel"/>
</TitledPane> </TitledPane>
<TitledPane fx:id="flowInitiatorPane" collapsible="false" maxHeight="Infinity" text="Flow initiator" <TitledPane fx:id="flowInitiatorPane" collapsible="false" maxHeight="Infinity" text="Flow initiator" GridPane.columnIndex="0" GridPane.fillWidth="true" GridPane.rowIndex="1">
GridPane.columnIndex="0" GridPane.fillWidth="true" GridPane.rowIndex="1"> <ScrollPane maxHeight="Infinity" minHeight="120">
<GridPane fx:id="flowInitiatorGrid" vgap="10.0" hgap="10.0"> <GridPane fx:id="flowInitiatorGrid" hgap="10.0" vgap="10.0">
<padding> <padding>
<Insets bottom="5.0" left="5.0" right="10.0" top="10.0" /> <Insets bottom="5.0" left="5.0" right="10.0" top="10.0" />
</padding> </padding>
<!--TODO text styling--> <Label fx:id="flowInitiatorTitle" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="LEFT" GridPane.rowIndex="0" style="-fx-font-weight: bold"/>
<Label fx:id="flowInitiatorTitle" GridPane.columnSpan="2" GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.halignment="LEFT"/> </GridPane>
</GridPane> </ScrollPane>
</TitledPane> </TitledPane>
<TitledPane fx:id="flowResultPane" collapsible="false" maxHeight="Infinity" text="Result" GridPane.columnIndex="1" GridPane.rowIndex="1"> <TitledPane fx:id="flowResultPane" collapsible="false" maxHeight="Infinity" text="Result" GridPane.columnIndex="1" GridPane.rowIndex="1">
<VBox fx:id="flowResultVBox" spacing="10.0"> <ScrollPane maxHeight="Infinity">
<VBox fx:id="flowResultVBox" spacing="10.0">
<padding>
<Insets bottom="5" left="5" right="5" top="5" />
</padding>
</VBox>
</ScrollPane>
</TitledPane>
<TitledPane fx:id="flowProgressPane" collapsible="false" text="Progress steps" GridPane.columnSpan="2" GridPane.rowIndex="2">
<!--TODO add progress graph-->
<VBox fx:id="flowProgressVBox" spacing="10.0">
<padding> <padding>
<Insets bottom="5" left="5" right="5" top="5"/> <Insets bottom="5" left="5" right="5" top="5" />
</padding> </padding>
</VBox> </VBox>
</TitledPane> </TitledPane>
<TitledPane fx:id="flowProgressPane" collapsible="false" maxWidth="Infinity" text="Progress steps" GridPane.columnSpan="2" GridPane.rowIndex="2">
<!--TODO add progress graph-->
</TitledPane>
<columnConstraints> <columnConstraints>
<ColumnConstraints minWidth="450.0"/> <ColumnConstraints minWidth="450.0" />
<ColumnConstraints hgrow="ALWAYS"/> <ColumnConstraints hgrow="ALWAYS" />
</columnConstraints> </columnConstraints>
<rowConstraints> <rowConstraints>
<RowConstraints vgrow="ALWAYS"/> <RowConstraints vgrow="ALWAYS" />
<RowConstraints vgrow="ALWAYS"/> <RowConstraints vgrow="ALWAYS" />
<RowConstraints vgrow="ALWAYS"/> <RowConstraints vgrow="ALWAYS" />
</rowConstraints> </rowConstraints>
</GridPane> </GridPane>

View File

@ -3,36 +3,22 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.TableView?> <?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TabPane?> <BorderPane stylesheets="@../css/corda.css" xmlns="http://javafx.com/javafx/8.0.76-ea"
<?import javafx.scene.control.Tab?> xmlns:fx="http://javafx.com/fxml/1">
<TabPane stylesheets="@../css/corda.css" xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1"
tabClosingPolicy="UNAVAILABLE">
<padding> <padding>
<Insets bottom="5" left="5" right="5" top="5"/> <Insets top="5" left="5" right="5" bottom="5"/>
</padding> </padding>
<Tab text="In progress" fx:id="progressFlowsTab"> <center>
<TableView fx:id="progressViewTable" VBox.vgrow="ALWAYS"> <TableView fx:id="allViewTable" VBox.vgrow="ALWAYS">
<columnResizePolicy> <columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/> <TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/>
</columnResizePolicy> </columnResizePolicy>
</TableView> </TableView>
</Tab> </center>
<Tab text="Finished" fx:id="doneFlowsTab"> <bottom>
<TableView fx:id="doneViewTable" VBox.vgrow="ALWAYS"> <Label fx:id="matchingFlowsLabel" text="matching flows(s)"/>
<columnResizePolicy> </bottom>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/> </BorderPane>
</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>