diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt index 946815f91c..929cba3b79 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/NodeMonitorModel.kt @@ -75,7 +75,8 @@ class NodeMonitorModel { Observable.empty() } } - 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) } diff --git a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/TransactionDataModel.kt b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/TransactionDataModel.kt index 9e0cc1e6e9..899e5001da 100644 --- a/client/jfx/src/main/kotlin/net/corda/client/jfx/model/TransactionDataModel.kt +++ b/client/jfx/src/main/kotlin/net/corda/client/jfx/model/TransactionDataModel.kt @@ -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 -) - /** * [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, - val stateMachineStatus: ObservableValue -) - /** * 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>()) { map, update -> - when (update) { - is StateMachineUpdate.Added -> { - val added: SimpleObjectProperty = - 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) } diff --git a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt index f8effdfa8e..5483c816a4 100644 --- a/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt +++ b/client/mock/src/main/kotlin/net/corda/client/mock/EventGenerator.kt @@ -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, val currencies: List, 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 = 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) } diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt index 1fe15cd1f7..0530a4fa6b 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt @@ -217,7 +217,7 @@ abstract class FlowLogic { fun track(): Pair>? { // 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() } } } diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt new file mode 100644 index 0000000000..c5bfd22185 --- /dev/null +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -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() + )) + val manager = User("manager", "test", permissions = setOf( + startFlowPermission(), + startFlowPermission(), + startFlowPermission(), + startFlowPermission()) + ) + + lateinit var notaryNode: NodeHandle + lateinit var aliceNode: NodeHandle + lateinit var bobNode: NodeHandle + lateinit var issuerNodeGBP: NodeHandle + lateinit var issuerNodeUSD: NodeHandle + + val RPCConnections = ArrayList() + val issuers = HashMap() + val parties = ArrayList>() + + 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.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() + } +} diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt index 878ce3faa4..552bf31489 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/Main.kt @@ -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) { - val portAllocation = PortAllocation.Incremental(20000) - driver(portAllocation = portAllocation) { - val user = User("user1", "test", permissions = setOf( - startFlowPermission() - )) - val manager = User("manager", "test", permissions = setOf( - startFlowPermission(), - startFlowPermission(), - startFlowPermission(), - startFlowPermission()) - ) - // 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.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) } diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/identicon/IdenticonRenderer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/identicon/IdenticonRenderer.kt index d7162986d5..74f6998f93 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/identicon/IdenticonRenderer.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/identicon/IdenticonRenderer.kt @@ -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) diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/model/CordaViewModel.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/model/CordaViewModel.kt index 5967d955bf..248656b0f9 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/model/CordaViewModel.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/model/CordaViewModel.kt @@ -31,4 +31,4 @@ abstract class CordaView(title: String? = null) : View(title) { } } -data class CordaWidget(val name: String, val node: Node) \ No newline at end of file +data class CordaWidget(val name: String, val node: Node, val icon: FontAwesomeIcon? = null) \ No newline at end of file diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Dashboard.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Dashboard.kt index c5fc9ce711..7079a9ad3a 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Dashboard.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Dashboard.kt @@ -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 } } } } } diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt index 79af7868b7..c6b1163716 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt @@ -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) diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt index 71416b17e6..bd12b15927 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt @@ -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) } diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt index 0d4f8f16d7..82c0ca3291 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt @@ -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() diff --git a/tools/explorer/src/main/resources/net/corda/explorer/css/corda.css b/tools/explorer/src/main/resources/net/corda/explorer/css/corda.css index 5649dd0d88..efd4f7a3c1 100644 --- a/tools/explorer/src/main/resources/net/corda/explorer/css/corda.css +++ b/tools/explorer/src/main/resources/net/corda/explorer/css/corda.css @@ -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; +}