Cleanup of the explorer code related to internal flow view work. (#832)

Cleanup of the explorer code related to internal flow view work.
Changes in simulation, widgets, minor visual.
This commit is contained in:
Katarzyna Streich 2017-06-20 10:45:42 +01:00 committed by GitHub
parent b874b3e62a
commit 20403d806a
13 changed files with 246 additions and 227 deletions

View File

@ -75,7 +75,8 @@ class NodeMonitorModel {
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
val currentStateMachines = stateMachines.map { StateMachineUpdate.Added(it) }

View File

@ -1,25 +1,15 @@
package net.corda.client.jfx.model
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.*
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
data class GatheredTransactionData(
val transaction: PartiallyResolvedTransaction,
val stateMachines: ObservableList<out StateMachineData>
)
/**
* [PartiallyResolvedTransaction] holds a [SignedTransaction] that has zero or more inputs resolved. The intent is
* to prepare clients for cases where an input can only be resolved in the future/cannot be resolved at all (for example
@ -58,53 +48,14 @@ data class PartiallyResolvedTransaction(
}
}
data class FlowStatus(val status: String)
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>
)
/**
* 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))
}
}
}
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)
}

View File

@ -1,27 +1,36 @@
package net.corda.client.mock
import net.corda.core.contracts.Amount
import net.corda.core.contracts.GBP
import net.corda.core.contracts.USD
import net.corda.core.identity.Party
import net.corda.core.serialization.OpaqueBytes
import net.corda.flows.CashFlowCommand
import java.util.*
/**
* [Generator]s for incoming/outgoing events to/from the [WalletMonitorService]. Internally it keeps track of owned
* state/ref pairs, but it doesn't necessarily generate "correct" events!
* [Generator]s for incoming/outgoing cash flow events between parties. It doesn't necessarily generate correct events!
* Especially at the beginning of simulation there might be few insufficient spend errors.
*/
class EventGenerator(val parties: List<Party>, val currencies: List<Currency>, val notary: Party) {
private val partyGenerator = Generator.pickOne(parties)
private val issueRefGenerator = Generator.intRange(0, 1).map { number -> OpaqueBytes(ByteArray(1, { number.toByte() })) }
private val amountGenerator = Generator.longRange(10000, 1000000)
private val currencyGenerator = Generator.pickOne(currencies)
protected val partyGenerator = Generator.pickOne(parties)
protected val issueRefGenerator = Generator.intRange(0, 1).map { number -> OpaqueBytes(ByteArray(1, { number.toByte() })) }
protected val amountGenerator = Generator.longRange(10000, 1000000)
protected val currencyGenerator = Generator.pickOne(currencies)
protected val currencyMap: MutableMap<Currency, Long> = mutableMapOf(USD to 0L, GBP to 0L) // Used for estimation of how much money we have in general.
private val issueCashGenerator = amountGenerator.combine(partyGenerator, issueRefGenerator, currencyGenerator) { amount, to, issueRef, ccy ->
protected fun addToMap(ccy: Currency, amount: Long) {
currencyMap.computeIfPresent(ccy) { _, value -> Math.max(0L, value + amount) }
}
protected val issueCashGenerator = amountGenerator.combine(partyGenerator, issueRefGenerator, currencyGenerator) { amount, to, issueRef, ccy ->
addToMap(ccy, amount)
CashFlowCommand.IssueCash(Amount(amount, ccy), issueRef, to, notary)
}
private val exitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator) { amount, issueRef, ccy ->
protected val exitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator) { amount, issueRef, ccy ->
addToMap(ccy, -amount)
CashFlowCommand.ExitCash(Amount(amount, ccy), issueRef)
}

View File

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

View File

@ -0,0 +1,191 @@
package net.corda.explorer
import joptsimple.OptionSet
import net.corda.client.mock.EventGenerator
import net.corda.client.mock.Generator
import net.corda.client.mock.pickOne
import net.corda.client.rpc.CordaRPCConnection
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.Amount
import net.corda.core.contracts.GBP
import net.corda.core.contracts.USD
import net.corda.core.failure
import net.corda.core.identity.Party
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.FlowHandle
import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.ServiceType
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.success
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ALICE
import net.corda.core.utilities.BOB
import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.flows.CashExitFlow
import net.corda.flows.CashFlowCommand
import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow
import net.corda.flows.IssuerFlow
import net.corda.testing.driver.NodeHandle
import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.driver
import net.corda.node.services.startFlowPermission
import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.nodeapi.User
import org.bouncycastle.asn1.x500.X500Name
import java.time.Instant
import java.util.*
class ExplorerSimulation(val options: OptionSet) {
val user = User("user1", "test", permissions = setOf(
startFlowPermission<CashPaymentFlow>()
))
val manager = User("manager", "test", permissions = setOf(
startFlowPermission<CashIssueFlow>(),
startFlowPermission<CashPaymentFlow>(),
startFlowPermission<CashExitFlow>(),
startFlowPermission<IssuerFlow.IssuanceRequester>())
)
lateinit var notaryNode: NodeHandle
lateinit var aliceNode: NodeHandle
lateinit var bobNode: NodeHandle
lateinit var issuerNodeGBP: NodeHandle
lateinit var issuerNodeUSD: NodeHandle
val RPCConnections = ArrayList<CordaRPCConnection>()
val issuers = HashMap<Currency, CordaRPCOps>()
val parties = ArrayList<Pair<Party, CordaRPCOps>>()
init {
startDemoNodes()
}
private fun onEnd() {
println("Closing RPC connections")
RPCConnections.forEach { it.close() }
}
private fun startDemoNodes() {
val portAllocation = PortAllocation.Incremental(20000)
driver(portAllocation = portAllocation) {
// TODO : Supported flow should be exposed somehow from the node instead of set of ServiceInfo.
val notary = startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)),
customOverrides = mapOf("nearestCity" to "Zurich"))
val alice = startNode(ALICE.name, rpcUsers = arrayListOf(user),
advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))),
customOverrides = mapOf("nearestCity" to "Milan"))
val bob = startNode(BOB.name, rpcUsers = arrayListOf(user),
advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))),
customOverrides = mapOf("nearestCity" to "Madrid"))
val ukBankName = X500Name("CN=UK Bank Plc,O=UK Bank Plc,L=London,C=UK")
val usaBankName = X500Name("CN=USA Bank Corp,O=USA Bank Corp,L=New York,C=USA")
val issuerGBP = startNode(ukBankName, rpcUsers = arrayListOf(manager),
advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.GBP"))),
customOverrides = mapOf("nearestCity" to "London"))
val issuerUSD = startNode(usaBankName, rpcUsers = arrayListOf(manager),
advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.USD"))),
customOverrides = mapOf("nearestCity" to "New York"))
notaryNode = notary.get()
aliceNode = alice.get()
bobNode = bob.get()
issuerNodeGBP = issuerGBP.get()
issuerNodeUSD = issuerUSD.get()
arrayOf(notaryNode, aliceNode, bobNode, issuerNodeGBP, issuerNodeUSD).forEach {
println("${it.nodeInfo.legalIdentity} started on ${it.configuration.rpcAddress}")
}
when {
options.has("S") -> startNormalSimulation()
}
waitForAllNodesToFinish()
}
}
private fun setUpRPC() {
// Register with alice to use alice's RPC proxy to create random events.
val aliceClient = aliceNode.rpcClientToNode()
val aliceConnection = aliceClient.start(user.username, user.password)
val aliceRPC = aliceConnection.proxy
val bobClient = bobNode.rpcClientToNode()
val bobConnection = bobClient.start(user.username, user.password)
val bobRPC = bobConnection.proxy
val issuerClientGBP = issuerNodeGBP.rpcClientToNode()
val issuerGBPConnection = issuerClientGBP.start(manager.username, manager.password)
val issuerRPCGBP = issuerGBPConnection.proxy
val issuerClientUSD = issuerNodeUSD.rpcClientToNode()
val issuerUSDConnection =issuerClientUSD.start(manager.username, manager.password)
val issuerRPCUSD = issuerUSDConnection.proxy
RPCConnections.addAll(listOf(aliceConnection, bobConnection, issuerGBPConnection, issuerUSDConnection))
issuers.putAll(mapOf(USD to issuerRPCUSD, GBP to issuerRPCGBP))
parties.addAll(listOf(aliceNode.nodeInfo.legalIdentity to aliceRPC,
bobNode.nodeInfo.legalIdentity to bobRPC,
issuerNodeGBP.nodeInfo.legalIdentity to issuerRPCGBP,
issuerNodeUSD.nodeInfo.legalIdentity to issuerRPCUSD))
}
private fun startSimulation(eventGenerator: EventGenerator, maxIterations: Int) {
// Log to logger when flow finish.
fun FlowHandle<SignedTransaction>.log(seq: Int, name: String) {
val out = "[$seq] $name $id :"
returnValue.success {
Main.log.info("$out ${it.id} ${(it.tx.outputs.first().data as Cash.State).amount}")
}.failure {
Main.log.info("$out ${it.message}")
}
}
for (i in 0..maxIterations) {
Thread.sleep(300)
// Issuer requests.
eventGenerator.issuerGenerator.map { command ->
when (command) {
is CashFlowCommand.IssueCash -> issuers[command.amount.token]?.let {
println("${Instant.now()} [$i] ISSUING ${command.amount} with ref ${command.issueRef} to ${command.recipient}")
command.startFlow(it).log(i, "${command.amount.token}Issuer")
}
is CashFlowCommand.ExitCash -> issuers[command.amount.token]?.let {
println("${Instant.now()} [$i] EXITING ${command.amount} with ref ${command.issueRef}")
command.startFlow(it).log(i, "${command.amount.token}Exit")
}
else -> throw IllegalArgumentException("Unsupported command: $command")
}
}.generate(SplittableRandom())
// Party pay requests.
eventGenerator.moveCashGenerator.combine(Generator.pickOne(parties)) { command, (party, rpc) ->
println("${Instant.now()} [$i] SENDING ${command.amount} from $party to ${command.recipient}")
command.startFlow(rpc).log(i, party.name.toString())
}.generate(SplittableRandom())
}
println("Simulation completed")
}
private fun startNormalSimulation() {
println("Running simulation mode ...")
setUpRPC()
val eventGenerator = EventGenerator(
parties = parties.map { it.first },
notary = notaryNode.nodeInfo.notaryIdentity,
currencies = listOf(GBP, USD)
)
val maxIterations = 100_000
// Pre allocate some money to each party.
eventGenerator.parties.forEach {
for (ref in 0..1) {
for ((currency, issuer) in issuers) {
CashFlowCommand.IssueCash(Amount(1_000_000, currency), OpaqueBytes(ByteArray(1, { ref.toByte() })), it, notaryNode.nodeInfo.notaryIdentity).startFlow(issuer)
}
}
}
startSimulation(eventGenerator, maxIterations)
onEnd()
}
}

View File

@ -11,45 +11,14 @@ import jfxtras.resources.JFXtrasFontRoboto
import joptsimple.OptionParser
import net.corda.client.jfx.model.Models
import net.corda.client.jfx.model.observableValue
import net.corda.client.mock.EventGenerator
import net.corda.client.mock.Generator
import net.corda.client.mock.pickOne
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.Amount
import net.corda.core.contracts.GBP
import net.corda.core.contracts.USD
import net.corda.core.crypto.X509Utilities
import net.corda.core.failure
import net.corda.core.messaging.FlowHandle
import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.ServiceType
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.success
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ALICE
import net.corda.core.utilities.BOB
import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.core.utilities.loggerFor
import net.corda.explorer.model.CordaViewModel
import net.corda.explorer.model.SettingsModel
import net.corda.explorer.views.*
import net.corda.explorer.views.cordapps.cash.CashViewer
import net.corda.flows.CashExitFlow
import net.corda.flows.CashFlowCommand
import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow
import net.corda.flows.IssuerFlow.IssuanceRequester
import net.corda.testing.driver.PortAllocation
import net.corda.testing.driver.driver
import net.corda.node.services.startFlowPermission
import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.nodeapi.User
import org.apache.commons.lang.SystemUtils
import org.bouncycastle.asn1.x500.X500Name
import org.controlsfx.dialog.ExceptionDialog
import tornadofx.*
import java.time.Instant
import java.util.*
/**
* Main class for Explorer, you will need Tornado FX to run the explorer.
@ -154,124 +123,7 @@ class Main : App(MainView::class) {
* On each iteration, the issuers will execute a Cash Issue or Cash Exit flow (at a 9:1 ratio) and a random party will execute a move of cash to another random party.
*/
fun main(args: Array<String>) {
val portAllocation = PortAllocation.Incremental(20000)
driver(portAllocation = portAllocation) {
val user = User("user1", "test", permissions = setOf(
startFlowPermission<CashPaymentFlow>()
))
val manager = User("manager", "test", permissions = setOf(
startFlowPermission<CashIssueFlow>(),
startFlowPermission<CashPaymentFlow>(),
startFlowPermission<CashExitFlow>(),
startFlowPermission<IssuanceRequester>())
)
// TODO : Supported flow should be exposed somehow from the node instead of set of ServiceInfo.
val notary = startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
val alice = startNode(ALICE.name, rpcUsers = arrayListOf(user),
advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))))
val bob = startNode(BOB.name, rpcUsers = arrayListOf(user),
advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))))
val issuerGBP = startNode(X500Name("CN=UK Bank Plc,O=UK Bank Plc,L=London,C=UK"), rpcUsers = arrayListOf(manager),
advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.GBP"))))
val issuerUSD = startNode(X500Name("CN=USA Bank Corp,O=USA Bank Corp,L=New York,C=US"), rpcUsers = arrayListOf(manager),
advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.USD"))))
val notaryNode = notary.get()
val aliceNode = alice.get()
val bobNode = bob.get()
val issuerNodeGBP = issuerGBP.get()
val issuerNodeUSD = issuerUSD.get()
arrayOf(notaryNode, aliceNode, bobNode, issuerNodeGBP, issuerNodeUSD).forEach {
println("${it.nodeInfo.legalIdentity} started on ${it.configuration.rpcAddress}")
}
val parser = OptionParser("S")
val options = parser.parse(*args)
if (options.has("S")) {
println("Running simulation mode ...")
// Register with alice to use alice's RPC proxy to create random events.
val aliceClient = aliceNode.rpcClientToNode()
val aliceConnection = aliceClient.start(user.username, user.password)
val aliceRPC = aliceConnection.proxy
val bobClient = bobNode.rpcClientToNode()
val bobConnection = bobClient.start(user.username, user.password)
val bobRPC = bobConnection.proxy
val issuerClientGBP = issuerNodeGBP.rpcClientToNode()
val issuerGBPConnection = issuerClientGBP.start(manager.username, manager.password)
val issuerRPCGBP = issuerGBPConnection.proxy
val issuerClientUSD = issuerNodeUSD.rpcClientToNode()
val issuerUSDConnection = issuerClientUSD.start(manager.username, manager.password)
val issuerRPCUSD = issuerUSDConnection.proxy
val issuers = mapOf(USD to issuerRPCUSD, GBP to issuerRPCGBP)
val parties = listOf(aliceNode.nodeInfo.legalIdentity to aliceRPC,
bobNode.nodeInfo.legalIdentity to bobRPC,
issuerNodeGBP.nodeInfo.legalIdentity to issuerRPCGBP,
issuerNodeUSD.nodeInfo.legalIdentity to issuerRPCUSD)
val eventGenerator = EventGenerator(
parties = parties.map { it.first },
notary = notaryNode.nodeInfo.notaryIdentity,
currencies = listOf(GBP, USD)
)
val maxIterations = 100_000
// Log to logger when flow finish.
fun FlowHandle<SignedTransaction>.log(seq: Int, name: String) {
val out = "[$seq] $name $id :"
returnValue.success {
Main.log.info("$out ${it.id} ${(it.tx.outputs.first().data as Cash.State).amount}")
}.failure {
Main.log.info("$out ${it.message}")
}
}
// Pre allocate some money to each party.
eventGenerator.parties.forEach {
for (ref in 0..1) {
for ((currency, issuer) in issuers) {
CashFlowCommand.IssueCash(Amount(1_000_000, currency), OpaqueBytes(ByteArray(1, { ref.toByte() })), it, notaryNode.nodeInfo.notaryIdentity).startFlow(issuer)
}
}
}
for (i in 0..maxIterations) {
Thread.sleep(300)
// Issuer requests.
eventGenerator.issuerGenerator.map { command ->
when (command) {
is CashFlowCommand.IssueCash -> issuers[command.amount.token]?.let {
println("${Instant.now()} [$i] ISSUING ${command.amount} with ref ${command.issueRef} to ${command.recipient}")
command.startFlow(it).log(i, "${command.amount.token}Issuer")
}
is CashFlowCommand.ExitCash -> issuers[command.amount.token]?.let {
println("${Instant.now()} [$i] EXITING ${command.amount} with ref ${command.issueRef}")
command.startFlow(it).log(i, "${command.amount.token}Exit")
}
else -> throw IllegalArgumentException("Unsupported command: $command")
}
}.generate(SplittableRandom())
// Party pay requests.
eventGenerator.moveCashGenerator.combine(Generator.pickOne(parties)) { command, (party, rpc) ->
println("${Instant.now()} [$i] SENDING ${command.amount} from $party to ${command.recipient}")
command.startFlow(rpc).log(i, party.name.toString())
}.generate(SplittableRandom())
}
println("Simulation completed")
aliceConnection.close()
bobConnection.close()
issuerGBPConnection.close()
issuerUSDConnection.close()
}
waitForAllNodesToFinish()
}
val parser = OptionParser("S")
val options = parser.parse(*args)
ExplorerSimulation(options)
}

View File

@ -195,11 +195,12 @@ fun identicon(secureHash: SecureHash, size: Double): ImageView {
return ImageView(IdenticonRenderer.getIdenticon(secureHash)).apply {
isPreserveRatio = true
fitWidth = size
styleClass += "identicon"
}
}
fun identiconToolTip(secureHash: SecureHash): Tooltip {
return Tooltip(Splitter.fixedLength(16).split("$secureHash").joinToString("\n")).apply {
fun identiconToolTip(secureHash: SecureHash, description: String? = null): Tooltip {
return Tooltip(Splitter.fixedLength(16).split("${description ?: secureHash}").joinToString("\n")).apply {
contentDisplay = ContentDisplay.TOP
textAlignment = TextAlignment.CENTER
graphic = identicon(secureHash, 90.0)

View File

@ -31,4 +31,4 @@ abstract class CordaView(title: String? = null) : View(title) {
}
}
data class CordaWidget(val name: String, val node: Node)
data class CordaWidget(val name: String, val node: Node, val icon: FontAwesomeIcon? = null)

View File

@ -1,6 +1,7 @@
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.collections.ObservableList
import javafx.scene.Node
@ -46,6 +47,7 @@ class Dashboard : CordaView() {
selectedView.value = view
}
}
it.icon?.let { graphic = FontAwesomeIconView(it).apply { glyphSize = 30.0 } }
}
}
}

View File

@ -54,7 +54,7 @@ class Network : CordaView() {
private val mapOriginalHeight = 2000.0
// UI node observables, declare here to create a strong ref to prevent GC, which removes listener from observables.
private var centralLabel: Label? = null
private var myLabel: Label? = null
private val notaryComponents = notaries.map { it.render() }
private val notaryButtons = notaryComponents.map { it.button }
private val peerComponents = peers.map { it.render() }
@ -97,7 +97,6 @@ class Network : CordaView() {
}
}
setOnMouseClicked {
centralLabel = mapLabel
mapScrollPane.centerLabel(mapLabel)
}
}
@ -129,7 +128,7 @@ class Network : CordaView() {
if (node == myIdentity.value) {
// It has to be a copy if we want to have notary both in notaries list and in identity (if we are looking at that particular notary node).
myIdentityPane.apply { center = node.renderButton(mapLabel) }
centralLabel = mapLabel
myLabel = mapLabel
}
return MapViewComponents(this, button, mapLabel)
}
@ -140,7 +139,7 @@ class Network : CordaView() {
// Run once when the screen is ready.
// TODO : Find a better way to do this.
mapPane.heightProperty().addListener { _, old, _ ->
if (old == 0.0) centralLabel?.let {
if (old == 0.0) myLabel?.let {
mapPane.applyCss()
mapPane.layout()
mapScrollPane.centerLabel(it)

View File

@ -53,7 +53,7 @@ class TransactionViewer : CordaView("Transactions") {
private val reportingCurrency by observableValue(ReportingCurrencyModel::reportingCurrency)
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
override val widgets = listOf(CordaWidget(title, TransactionWidget())).observable()
override val widgets = listOf(CordaWidget(title, TransactionWidget(), icon)).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
@ -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,11 @@ 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.
// It removes that irritating column at the end of table that does nothing.
minWidth = 26.0
maxWidth = 26.0
}
setColumnResizePolicy { true }
}
matchingTransactionsLabel.textProperty().bind(Bindings.size(transactionViewTable.items).map {
"$it matching transaction${if (it == 1) "" else "s"}"
@ -186,6 +190,8 @@ class TransactionViewer : CordaView("Transactions") {
init {
right {
label {
val hash = SecureHash.randomSHA256()
graphic = identicon(hash, 30.0)
textProperty().bind(Bindings.size(partiallyResolvedTransactions).map(Number::toString))
BorderPane.setAlignment(this, Pos.BOTTOM_RIGHT)
}

View File

@ -46,7 +46,7 @@ class CashViewer : CordaView("Cash") {
override val root: BorderPane by fxml()
override val icon: FontAwesomeIcon = FontAwesomeIcon.MONEY
// View's widget.
override val widgets = listOf(CordaWidget("Treasury", CashWidget())).observable()
override val widgets = listOf(CordaWidget("Treasury", CashWidget(), icon)).observable()
// Left pane
private val leftPane: VBox by fxid()
private val splitPane: SplitPane by fxid()

View File

@ -111,6 +111,7 @@
-fx-cursor: hand;
-fx-background-color: -color-1;
-fx-border-color: transparent;
-fx-border-radius: 2;
}
.tile .title .text, .tile:expanded .title .text {
@ -123,14 +124,15 @@
-fx-background-repeat: no-repeat;
-fx-background-position: center center;
-fx-cursor: hand;
-fx-padding: 0px;
-fx-padding: 5px;
-fx-alignment: bottom-right;
-fx-border-color: transparent; /*t r b l */
-fx-border-radius: 2;
}
.tile .label {
-fx-font-size: 2.4em;
-fx-padding: 20px;
-fx-font-size: 2.0em;
-fx-padding: 10px;
-fx-text-fill: -color-0;
-fx-font-weight: normal;
-fx-text-alignment: right;
@ -309,3 +311,8 @@
.scroll-bar:vertical {
-fx-background-color: transparent;
}
+/* Other */
.identicon {
-fx-border-radius: 2;
}