mirror of
https://github.com/corda/corda.git
synced 2025-01-21 03:55:00 +00:00
Changing UI layout
This commit is contained in:
parent
6d39b71bf9
commit
7f8608c981
@ -16,7 +16,9 @@ import com.r3corda.node.driver.driver
|
||||
import com.r3corda.node.internal.CordaRPCOpsImpl
|
||||
import com.r3corda.node.services.User
|
||||
import com.r3corda.node.services.config.configureTestSSL
|
||||
import com.r3corda.node.services.messaging.ArtemisMessagingComponent
|
||||
import com.r3corda.node.services.messaging.StateMachineUpdate
|
||||
import com.r3corda.node.services.network.NetworkMapService
|
||||
import com.r3corda.node.services.transactions.SimpleNotaryService
|
||||
import com.r3corda.testing.expect
|
||||
import com.r3corda.testing.expectEvents
|
||||
@ -30,7 +32,6 @@ import java.util.concurrent.CountDownLatch
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class NodeMonitorModelTest {
|
||||
|
||||
lateinit var aliceNode: NodeInfo
|
||||
lateinit var notaryNode: NodeInfo
|
||||
val stopDriver = CountDownLatch(1)
|
||||
@ -67,7 +68,7 @@ class NodeMonitorModelTest {
|
||||
networkMapUpdates = monitor.networkMap.bufferUntilSubscribed()
|
||||
clientToService = monitor.clientToService
|
||||
|
||||
monitor.register(aliceNode, configureTestSSL(), cashUser.username, cashUser.password)
|
||||
monitor.register(ArtemisMessagingComponent.toHostAndPort(aliceNode.address), configureTestSSL(), cashUser.username, cashUser.password)
|
||||
driverStarted.countDown()
|
||||
stopDriver.await()
|
||||
}
|
||||
@ -85,20 +86,22 @@ class NodeMonitorModelTest {
|
||||
fun `network map update`() {
|
||||
newNode("Bob")
|
||||
newNode("Charlie")
|
||||
networkMapUpdates.expectEvents(isStrict = false) {
|
||||
sequence(
|
||||
// TODO : Add test for remove when driver DSL support individual node shutdown.
|
||||
expect { output: NetworkMapCache.MapChange ->
|
||||
require(output.node.legalIdentity.name == "Alice") { output.node.legalIdentity.name }
|
||||
},
|
||||
expect { output: NetworkMapCache.MapChange ->
|
||||
require(output.node.legalIdentity.name == "Bob") { output.node.legalIdentity.name }
|
||||
},
|
||||
expect { output: NetworkMapCache.MapChange ->
|
||||
require(output.node.legalIdentity.name == "Charlie") { output.node.legalIdentity.name }
|
||||
}
|
||||
)
|
||||
}
|
||||
networkMapUpdates.filter { !it.node.advertisedServices.any { it.info.type.isNotary() } }
|
||||
.filter { !it.node.advertisedServices.any { it.info.type == NetworkMapService.type } }
|
||||
.expectEvents(isStrict = false) {
|
||||
sequence(
|
||||
// TODO : Add test for remove when driver DSL support individual node shutdown.
|
||||
expect { output: NetworkMapCache.MapChange ->
|
||||
require(output.node.legalIdentity.name == "Alice") { "Expecting : Alice, Actual : ${output.node.legalIdentity.name}" }
|
||||
},
|
||||
expect { output: NetworkMapCache.MapChange ->
|
||||
require(output.node.legalIdentity.name == "Bob") { "Expecting : Bob, Actual : ${output.node.legalIdentity.name}" }
|
||||
},
|
||||
expect { output: NetworkMapCache.MapChange ->
|
||||
require(output.node.legalIdentity.name == "Charlie") { "Expecting : Charlie, Actual : ${output.node.legalIdentity.name}" }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -10,9 +10,7 @@ import javafx.collections.ObservableList
|
||||
import javafx.collections.ObservableMap
|
||||
import javafx.collections.transformation.FilteredList
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.function.Predicate
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
/**
|
||||
* Here follows utility extension functions that help reduce the visual load when developing RX code. Each function should
|
||||
@ -276,4 +274,4 @@ fun <A> ObservableList<A>.last(): ObservableValue<A?> {
|
||||
null
|
||||
}
|
||||
}, arrayOf(this))
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ import com.r3corda.client.fxutils.*
|
||||
import com.r3corda.core.contracts.ContractState
|
||||
import com.r3corda.core.contracts.StateAndRef
|
||||
import com.r3corda.core.contracts.StateRef
|
||||
import com.r3corda.client.fxutils.recordInSequence
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.node.services.StateMachineTransactionMapping
|
||||
import com.r3corda.core.protocols.StateMachineRunId
|
||||
@ -15,8 +14,6 @@ import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.collections.ObservableMap
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
import org.slf4j.LoggerFactory
|
||||
import rx.Observable
|
||||
|
||||
data class GatheredTransactionData(
|
||||
val transaction: PartiallyResolvedTransaction,
|
||||
@ -30,8 +27,7 @@ data class GatheredTransactionData(
|
||||
*/
|
||||
data class PartiallyResolvedTransaction(
|
||||
val transaction: SignedTransaction,
|
||||
val inputs: List<ObservableValue<InputResolution>>
|
||||
) {
|
||||
val inputs: List<ObservableValue<InputResolution>>) {
|
||||
val id = transaction.id
|
||||
sealed class InputResolution(val stateRef: StateRef) {
|
||||
class Unresolved(stateRef: StateRef) : InputResolution(stateRef)
|
||||
@ -84,16 +80,15 @@ data class StateMachineData(
|
||||
*/
|
||||
class GatheredTransactionDataModel {
|
||||
|
||||
private val transactions: Observable<SignedTransaction> by observable(NodeMonitorModel::transactions)
|
||||
private val stateMachineUpdates: Observable<StateMachineUpdate> by observable(NodeMonitorModel::stateMachineUpdates)
|
||||
private val progressTracking: Observable<ProgressTrackingEvent> by observable(NodeMonitorModel::progressTracking)
|
||||
private val stateMachineTransactionMapping: Observable<StateMachineTransactionMapping> by observable(NodeMonitorModel::stateMachineTransactionMapping)
|
||||
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)
|
||||
|
||||
val collectedTransactions = transactions.recordInSequence()
|
||||
val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
|
||||
val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
|
||||
val stateMachineStatus: ObservableMap<StateMachineRunId, out ObservableValue<StateMachineStatus>> =
|
||||
stateMachineUpdates.foldToObservableMap(Unit) { update, _unit, map: ObservableMap<StateMachineRunId, SimpleObjectProperty<StateMachineStatus>> ->
|
||||
private val collectedTransactions = transactions.recordInSequence()
|
||||
private val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
|
||||
private val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
|
||||
private val stateMachineStatus = stateMachineUpdates.foldToObservableMap(Unit) { update, _unit, map: ObservableMap<StateMachineRunId, SimpleObjectProperty<StateMachineStatus>> ->
|
||||
when (update) {
|
||||
is StateMachineUpdate.Added -> {
|
||||
val added: SimpleObjectProperty<StateMachineStatus> =
|
||||
@ -107,21 +102,19 @@ class GatheredTransactionDataModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
val stateMachineDataList: ObservableList<StateMachineData> =
|
||||
LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
|
||||
private val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
|
||||
StateMachineData(id, progress.map { it?.let { ProtocolStatus(it.message) } }, status)
|
||||
}.getObservableValues()
|
||||
val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
|
||||
val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
|
||||
val partiallyResolvedTransactions = collectedTransactions.map {
|
||||
private val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
|
||||
private val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
|
||||
private val partiallyResolvedTransactions = collectedTransactions.map {
|
||||
PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
|
||||
}
|
||||
|
||||
/**
|
||||
* We JOIN the transaction list with state machines
|
||||
*/
|
||||
val gatheredTransactionDataList: ObservableList<out GatheredTransactionData> =
|
||||
partiallyResolvedTransactions.leftOuterJoin(
|
||||
val gatheredTransactionDataList = partiallyResolvedTransactions.leftOuterJoin(
|
||||
smTxMappingList,
|
||||
PartiallyResolvedTransaction::id,
|
||||
StateMachineTransactionMapping::transactionId
|
||||
|
@ -1,16 +1,18 @@
|
||||
package com.r3corda.client.model
|
||||
|
||||
import com.r3corda.client.fxutils.foldToObservableList
|
||||
import com.r3corda.client.fxutils.map
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.node.services.NetworkMapCache
|
||||
import com.r3corda.node.services.network.NetworkMapService
|
||||
import javafx.collections.ObservableList
|
||||
import kotlinx.support.jdk8.collections.removeIf
|
||||
import rx.Observable
|
||||
import java.security.PublicKey
|
||||
|
||||
class NetworkIdentityModel {
|
||||
private val networkIdentityObservable: Observable<NetworkMapCache.MapChange> by observable(NodeMonitorModel::networkMap)
|
||||
private val networkIdentityObservable by observable(NodeMonitorModel::networkMap)
|
||||
|
||||
val networkIdentities: ObservableList<NodeInfo> =
|
||||
private val networkIdentities: ObservableList<NodeInfo> =
|
||||
networkIdentityObservable.foldToObservableList(Unit) { update, _accumulator, observableList ->
|
||||
observableList.removeIf {
|
||||
when (update.type) {
|
||||
@ -21,4 +23,18 @@ class NetworkIdentityModel {
|
||||
}
|
||||
observableList.addAll(update.node)
|
||||
}
|
||||
|
||||
private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable)
|
||||
|
||||
val parties: ObservableList<NodeInfo> = networkIdentities.filtered { !it.isCordaService() }
|
||||
val notaries: ObservableList<NodeInfo> = networkIdentities.filtered { it.advertisedServices.any { it.info.type.isNotary() } }
|
||||
val myIdentity = rpcProxy.map { it?.nodeIdentity() }
|
||||
|
||||
private fun NodeInfo.isCordaService(): Boolean {
|
||||
return advertisedServices.any { it.info.type == NetworkMapService.type || it.info.type.isNotary() }
|
||||
}
|
||||
|
||||
fun lookup(publicKey: PublicKey): NodeInfo? {
|
||||
return parties.firstOrNull { it.legalIdentity.owningKey == publicKey } ?: notaries.firstOrNull { it.notaryIdentity.owningKey == publicKey }
|
||||
}
|
||||
}
|
@ -1,15 +1,14 @@
|
||||
package com.r3corda.client.model
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.r3corda.client.CordaRPCClient
|
||||
import com.r3corda.core.contracts.ClientToServiceCommand
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.node.services.NetworkMapCache
|
||||
import com.r3corda.core.node.services.StateMachineTransactionMapping
|
||||
import com.r3corda.core.node.services.Vault
|
||||
import com.r3corda.core.protocols.StateMachineRunId
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.node.services.config.NodeSSLConfiguration
|
||||
import com.r3corda.node.services.messaging.ArtemisMessagingComponent.Companion.toHostAndPort
|
||||
import com.r3corda.node.services.messaging.CordaRPCOps
|
||||
import com.r3corda.node.services.messaging.StateMachineInfo
|
||||
import com.r3corda.node.services.messaging.StateMachineUpdate
|
||||
@ -56,8 +55,8 @@ class NodeMonitorModel {
|
||||
* Register for updates to/from a given vault.
|
||||
* TODO provide an unsubscribe mechanism
|
||||
*/
|
||||
fun register(vaultMonitorNodeInfo: NodeInfo, sslConfig: NodeSSLConfiguration, username: String, password: String) {
|
||||
val client = CordaRPCClient(toHostAndPort(vaultMonitorNodeInfo.address), sslConfig)
|
||||
fun register(nodeHostAndPort: HostAndPort, sslConfig: NodeSSLConfiguration, username: String, password: String) {
|
||||
val client = CordaRPCClient(nodeHostAndPort, sslConfig)
|
||||
client.start(username, password)
|
||||
val proxy = client.proxy()
|
||||
|
||||
@ -101,7 +100,6 @@ class NodeMonitorModel {
|
||||
clientToServiceSource.subscribe {
|
||||
proxy.executeCommand(it)
|
||||
}
|
||||
|
||||
proxyObservable.set(proxy)
|
||||
}
|
||||
}
|
@ -25,8 +25,6 @@ apply plugin: 'kotlin'
|
||||
apply plugin: 'application'
|
||||
|
||||
sourceCompatibility = 1.8
|
||||
|
||||
applicationDefaultJvmArgs = ["-javaagent:${rootProject.configurations.quasar.singleFile}"]
|
||||
mainClassName = 'com.r3corda.explorer.Main'
|
||||
|
||||
sourceSets {
|
||||
@ -53,7 +51,7 @@ dependencies {
|
||||
testCompile group: 'junit', name: 'junit', version: '4.11'
|
||||
|
||||
// TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's.
|
||||
compile 'no.tornado:tornadofx:1.5.1'
|
||||
compile 'no.tornado:tornadofx:1.5.6'
|
||||
|
||||
// Corda Core: Data structures and basic types needed to work with Corda.
|
||||
compile project(':core')
|
||||
@ -74,4 +72,12 @@ dependencies {
|
||||
|
||||
// Humanize: formatting
|
||||
compile 'com.github.mfornos:humanize-icu:1.2.2'
|
||||
|
||||
// Controls FX: more java FX components http://fxexperience.com/controlsfx/
|
||||
compile 'org.controlsfx:controlsfx:8.40.12'
|
||||
}
|
||||
|
||||
task(runDemoNodes, dependsOn: 'classes', type: JavaExec) {
|
||||
main = 'com.r3corda.explorer.MainKt'
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
}
|
@ -1,60 +1,72 @@
|
||||
package com.r3corda.explorer
|
||||
|
||||
import com.r3corda.client.mock.EventGenerator
|
||||
import com.r3corda.client.model.Models
|
||||
import com.r3corda.client.model.NodeMonitorModel
|
||||
import com.r3corda.core.node.services.ServiceInfo
|
||||
import com.r3corda.explorer.model.IdentityModel
|
||||
import com.r3corda.explorer.views.runInFxApplicationThread
|
||||
import com.r3corda.node.driver.PortAllocation
|
||||
import com.r3corda.node.driver.driver
|
||||
import com.r3corda.node.services.config.configureTestSSL
|
||||
import com.r3corda.node.services.config.FullNodeConfiguration
|
||||
import com.r3corda.node.services.messaging.ArtemisMessagingComponent
|
||||
import com.r3corda.node.services.transactions.SimpleNotaryService
|
||||
import javafx.stage.Stage
|
||||
import org.controlsfx.dialog.ExceptionDialog
|
||||
import tornadofx.App
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Main class for Explorer, you will need Tornado FX to run the explorer.
|
||||
*/
|
||||
class Main : App() {
|
||||
override val primaryView = MainWindow::class
|
||||
|
||||
override fun start(stage: Stage) {
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||
throwable.printStackTrace()
|
||||
System.exit(1)
|
||||
// Show exceptions in exception dialog.
|
||||
runInFxApplicationThread {
|
||||
// [showAndWait] need to be in the FX thread
|
||||
ExceptionDialog(throwable).showAndWait()
|
||||
System.exit(1)
|
||||
}
|
||||
}
|
||||
super.start(stage)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This main method will starts 3 nodes (Notary, Alice and Bob) locally for UI testing, they will be on localhost:20002, 20004, 20006 respectively.
|
||||
*/
|
||||
fun main(args: Array<String>) {
|
||||
val portAllocation = PortAllocation.Incremental(20000)
|
||||
driver(portAllocation = portAllocation) {
|
||||
val notary = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
val alice = startNode("Alice")
|
||||
val bob = startNode("Bob")
|
||||
|
||||
val notaryNode = notary.get()
|
||||
val aliceNode = alice.get()
|
||||
val bobNode = bob.get()
|
||||
|
||||
arrayOf(notaryNode, aliceNode, bobNode).forEach {
|
||||
println("${it.nodeInfo.legalIdentity} started on ${ArtemisMessagingComponent.toHostAndPort(it.nodeInfo.address)}")
|
||||
}
|
||||
|
||||
super.start(stage)
|
||||
// Register with alice to use alice's RPC proxy to create random events.
|
||||
Models.get<NodeMonitorModel>(Main::class).register(ArtemisMessagingComponent.toHostAndPort(aliceNode.nodeInfo.address), FullNodeConfiguration(aliceNode.config), "user1", "test")
|
||||
val rpcProxy = Models.get<NodeMonitorModel>(Main::class).proxyObservable.get()
|
||||
|
||||
// start the driver on another thread
|
||||
// TODO Change this to connecting to an actual node (specified on cli/in a config) once we're happy with the code
|
||||
Thread({
|
||||
val portAllocation = PortAllocation.Incremental(20000)
|
||||
driver(portAllocation = portAllocation) {
|
||||
|
||||
val aliceNodeFuture = startNode("Alice")
|
||||
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
|
||||
val aliceNode = aliceNodeFuture.get().nodeInfo
|
||||
val notaryNode = notaryNodeFuture.get().nodeInfo
|
||||
|
||||
Models.get<IdentityModel>(Main::class).notary.set(notaryNode.notaryIdentity)
|
||||
Models.get<IdentityModel>(Main::class).myIdentity.set(aliceNode.legalIdentity)
|
||||
Models.get<NodeMonitorModel>(Main::class).register(aliceNode, configureTestSSL(), "user1", "test")
|
||||
|
||||
startNode("Bob").get()
|
||||
|
||||
/* for (i in 0 .. 10000) {
|
||||
Thread.sleep(500)
|
||||
|
||||
val eventGenerator = EventGenerator(
|
||||
parties = listOf(aliceNode.legalIdentity),
|
||||
notary = notaryNode.notaryIdentity
|
||||
)
|
||||
|
||||
eventGenerator.clientToServiceCommandGenerator.map { command ->
|
||||
aliceOutStream.onNext(command)
|
||||
}.generate(Random())
|
||||
}*/
|
||||
waitForAllNodesToFinish()
|
||||
}
|
||||
}).start()
|
||||
for (i in 0..10000) {
|
||||
Thread.sleep(500)
|
||||
val eventGenerator = EventGenerator(
|
||||
parties = listOf(aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity),
|
||||
notary = notaryNode.nodeInfo.notaryIdentity
|
||||
)
|
||||
eventGenerator.clientToServiceCommandGenerator.map { command ->
|
||||
rpcProxy?.executeCommand(command)
|
||||
}.generate(Random())
|
||||
}
|
||||
waitForAllNodesToFinish()
|
||||
}
|
||||
}
|
@ -1,9 +1,14 @@
|
||||
package com.r3corda.explorer
|
||||
|
||||
import com.r3corda.client.model.Models
|
||||
import com.r3corda.client.model.NodeMonitorModel
|
||||
import com.r3corda.explorer.views.LoginView
|
||||
import com.r3corda.explorer.views.TopLevel
|
||||
import com.r3corda.node.services.config.configureTestSSL
|
||||
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory
|
||||
import jfxtras.resources.JFXtrasFontRoboto
|
||||
import tornadofx.*
|
||||
import tornadofx.View
|
||||
import tornadofx.importStylesheet
|
||||
|
||||
/**
|
||||
* The root view embeds the [Shell] and provides support for the status bar, and modal dialogs.
|
||||
@ -11,10 +16,14 @@ import tornadofx.*
|
||||
class MainWindow : View() {
|
||||
private val toplevel: TopLevel by inject()
|
||||
override val root = toplevel.root
|
||||
private val loginView by inject<LoginView>()
|
||||
|
||||
init {
|
||||
// Do this first before creating the notification bar, so it can autosize itself properly.
|
||||
loadFontsAndStyles()
|
||||
loginView.login { hostAndPort, username, password ->
|
||||
Models.get<NodeMonitorModel>(MainWindow::class).register(hostAndPort, configureTestSSL(), username, password)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadFontsAndStyles() {
|
||||
@ -23,4 +32,4 @@ class MainWindow : View() {
|
||||
FontAwesomeIconFactory.get() // Force initialisation.
|
||||
root.styleClass += "root"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package com.r3corda.explorer.components
|
||||
|
||||
import javafx.scene.control.Alert
|
||||
import javafx.scene.control.Label
|
||||
import javafx.scene.control.TextArea
|
||||
import javafx.scene.layout.GridPane
|
||||
import javafx.scene.layout.Priority
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
class ExceptionDialog(ex: Throwable) : Alert(AlertType.ERROR) {
|
||||
|
||||
private fun Throwable.toExceptionText(): String {
|
||||
return StringWriter().use {
|
||||
PrintWriter(it).use {
|
||||
this.printStackTrace(it)
|
||||
}
|
||||
it.toString()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// Create expandable Exception.
|
||||
val label = Label("The exception stacktrace was:")
|
||||
contentText = ex.message
|
||||
|
||||
val textArea = TextArea(ex.toExceptionText())
|
||||
textArea.isEditable = false
|
||||
textArea.isWrapText = true
|
||||
|
||||
textArea.maxWidth = Double.MAX_VALUE
|
||||
textArea.maxHeight = Double.MAX_VALUE
|
||||
GridPane.setVgrow(textArea, Priority.ALWAYS)
|
||||
GridPane.setHgrow(textArea, Priority.ALWAYS)
|
||||
|
||||
val expContent = GridPane()
|
||||
expContent.maxWidth = Double.MAX_VALUE
|
||||
expContent.add(label, 0, 0)
|
||||
expContent.add(textArea, 0, 1)
|
||||
|
||||
// Set expandable Exception into the dialog pane.
|
||||
dialogPane.expandableContent = expContent
|
||||
}
|
||||
}
|
@ -0,0 +1,194 @@
|
||||
package com.r3corda.explorer.identicon
|
||||
|
||||
import com.google.common.base.Splitter
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import javafx.scene.SnapshotParameters
|
||||
import javafx.scene.canvas.Canvas
|
||||
import javafx.scene.canvas.GraphicsContext
|
||||
import javafx.scene.control.ContentDisplay
|
||||
import javafx.scene.control.Tooltip
|
||||
import javafx.scene.image.ImageView
|
||||
import javafx.scene.image.WritableImage
|
||||
import javafx.scene.paint.Color
|
||||
import javafx.scene.text.TextAlignment
|
||||
|
||||
/**
|
||||
* (The MIT License)
|
||||
* Copyright (c) 2007-2012 Don Park <donpark@docuverse.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* 'Software'), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* The code originated from : https://github.com/donpark/identicon
|
||||
* And has been modified to Kotlin and JavaFX instead of Java code using AWT
|
||||
*/
|
||||
|
||||
class IdenticonRenderer {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Each patch is a polygon created from a list of vertices on a 5 by 5 grid.
|
||||
* Vertices are numbered from 0 to 24, starting from top-left corner of the
|
||||
* grid, moving left to right and top to bottom.
|
||||
*/
|
||||
private val patchTypes = arrayOf(
|
||||
byteArrayOf(0, 4, 24, 20, 0),
|
||||
byteArrayOf(0, 4, 20, 0),
|
||||
byteArrayOf(2, 24, 20, 2),
|
||||
byteArrayOf(0, 2, 20, 22, 0),
|
||||
byteArrayOf(2, 14, 22, 10, 2),
|
||||
byteArrayOf(0, 14, 24, 22, 0),
|
||||
byteArrayOf(2, 24, 22, 13, 11, 22, 20, 2),
|
||||
byteArrayOf(0, 14, 22, 0),
|
||||
byteArrayOf(6, 8, 18, 16, 6),
|
||||
byteArrayOf(4, 20, 10, 12, 2, 4),
|
||||
byteArrayOf(0, 2, 12, 10, 0),
|
||||
byteArrayOf(10, 14, 22, 10),
|
||||
byteArrayOf(20, 12, 24, 20),
|
||||
byteArrayOf(10, 2, 12, 10),
|
||||
byteArrayOf(0, 2, 10, 0),
|
||||
byteArrayOf(0, 4, 24, 20, 0)).map(::Patch)
|
||||
|
||||
private val PATCH_CELLS = 4
|
||||
private val PATCH_GRIDS = PATCH_CELLS + 1
|
||||
private val PATCH_SYMMETRIC: Byte = 1
|
||||
private val PATCH_INVERTED: Byte = 2
|
||||
|
||||
private val patchFlags = byteArrayOf(PATCH_SYMMETRIC, 0, 0, 0, PATCH_SYMMETRIC, 0, 0, 0, PATCH_SYMMETRIC, 0, 0, 0, 0, 0, 0, (PATCH_SYMMETRIC + PATCH_INVERTED).toByte())
|
||||
}
|
||||
|
||||
private class Patch(private val byteArray: ByteArray) {
|
||||
fun x(patchSize: Double): DoubleArray {
|
||||
return byteArray.map(Byte::toInt).map { it % PATCH_GRIDS * (patchSize / PATCH_CELLS) - patchSize / 2 }.toDoubleArray()
|
||||
}
|
||||
|
||||
fun y(patchSize: Double): DoubleArray {
|
||||
return byteArray.map(Byte::toInt).map { it / PATCH_GRIDS * (patchSize / PATCH_CELLS) - patchSize / 2 }.toDoubleArray()
|
||||
}
|
||||
|
||||
val size = byteArray.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns rendered identicon image for given identicon code.
|
||||
* Size of the returned identicon image is determined by patchSize set using
|
||||
* [setPatchSize]. Since a 9-block identicon consists of 3x3 patches,
|
||||
* width and height will be 3 times the patch size.
|
||||
*/
|
||||
fun render(code: Int, patchSize: Double, backgroundColor: Color = Color.WHITE): WritableImage {
|
||||
// decode the code into parts
|
||||
val middleType = intArrayOf(0, 4, 8, 15)[code and 0x3] // bit 0-1: middle patch type
|
||||
val middleInvert = code shr 2 and 0x1 != 0 // bit 2: middle invert
|
||||
val cornerType = code shr 3 and 0x0f // bit 3-6: corner patch type
|
||||
val cornerInvert = code shr 7 and 0x1 != 0 // bit 7: corner invert
|
||||
val cornerTurn = code shr 8 and 0x3 // bit 8-9: corner turns
|
||||
val sideType = code shr 10 and 0x0f // bit 10-13: side patch type
|
||||
val sideInvert = code shr 14 and 0x1 != 0 // bit 14: side invert
|
||||
val sideTurn = code shr 15 and 0x3 // bit 15: corner turns
|
||||
val blue = code shr 16 and 0x01f // bit 16-20: blue color component
|
||||
val green = code shr 21 and 0x01f // bit 21-26: green color component
|
||||
val red = code shr 27 and 0x01f // bit 27-31: red color component
|
||||
|
||||
// color components are used at top of the range for color difference
|
||||
// use white background for now.
|
||||
// TODO: support transparency.
|
||||
val fillColor = Color.rgb(red shl 3, green shl 3, blue shl 3)
|
||||
// outline shapes with a noticeable color (complementary will do) if
|
||||
// shape color and background color are too similar (measured by color
|
||||
// distance).
|
||||
val strokeColor = if (getColorDistance(fillColor, backgroundColor) < 32.0f) fillColor.invert() else null
|
||||
|
||||
val sourceSize = patchSize * 3
|
||||
val canvas = Canvas(sourceSize, sourceSize)
|
||||
val g = canvas.graphicsContext2D
|
||||
/** Rendering Order:
|
||||
* 6 2 7
|
||||
* 5 1 3
|
||||
* 9 4 8 */
|
||||
val color = PatchColor(fillColor, strokeColor, backgroundColor)
|
||||
drawPatch(g, patchSize, patchSize, middleType, 0, patchSize, middleInvert, color)
|
||||
drawPatch(g, patchSize, 0.0, sideType, sideTurn, patchSize, sideInvert, color)
|
||||
drawPatch(g, patchSize * 2, patchSize, sideType, sideTurn + 1, patchSize, sideInvert, color)
|
||||
drawPatch(g, patchSize, patchSize * 2, sideType, sideTurn + 2, patchSize, sideInvert, color)
|
||||
drawPatch(g, 0.0, patchSize, sideType, sideTurn + 3, patchSize, sideInvert, color)
|
||||
drawPatch(g, 0.0, 0.0, cornerType, cornerTurn, patchSize, cornerInvert, color)
|
||||
drawPatch(g, patchSize * 2, 0.0, cornerType, cornerTurn + 1, patchSize, cornerInvert, color)
|
||||
drawPatch(g, patchSize * 2, patchSize * 2, cornerType, cornerTurn + 2, patchSize, cornerInvert, color)
|
||||
drawPatch(g, 0.0, patchSize * 2, cornerType, cornerTurn + 3, patchSize, cornerInvert, color)
|
||||
return canvas.snapshot(SnapshotParameters(), WritableImage(sourceSize.toInt(), sourceSize.toInt()))
|
||||
}
|
||||
|
||||
private class PatchColor(private val fillColor: Color, val strokeColor: Color?, private val backgroundColor: Color) {
|
||||
fun background(invert: Boolean) = if (invert) fillColor else backgroundColor
|
||||
fun fill(invert: Boolean) = if (invert) backgroundColor else fillColor
|
||||
}
|
||||
|
||||
private fun drawPatch(g: GraphicsContext, x: Double, y: Double, patchIndex: Int, turn: Int, patchSize: Double, _invert: Boolean, color: PatchColor) {
|
||||
val patch = patchTypes[patchIndex % patchTypes.size]
|
||||
val invert = if ((patchFlags[patchIndex].toInt() and PATCH_INVERTED.toInt()) !== 0) !_invert else _invert
|
||||
g.apply {
|
||||
// paint background
|
||||
clearRect(x, y, patchSize, patchSize)
|
||||
fill = color.background(invert)
|
||||
stroke = color.background(invert)
|
||||
fillRect(x, y, patchSize, patchSize)
|
||||
strokeRect(x, y, patchSize, patchSize)
|
||||
// offset and rotate coordinate space by patch position (x, y) and
|
||||
// 'turn' before rendering patch shape
|
||||
val saved = transform
|
||||
translate(x + patchSize / 2, y + patchSize / 2)
|
||||
rotate((turn % 4 * 90).toDouble())
|
||||
|
||||
// if stroke color was specified, apply stroke
|
||||
// stroke color should be specified if fore color is too close to the
|
||||
// back color.
|
||||
if (color.strokeColor != null) {
|
||||
stroke = color.strokeColor
|
||||
strokePolygon(patch.x(patchSize), patch.y(patchSize), patch.size)
|
||||
}
|
||||
// render rotated patch using fore color (back color if inverted)
|
||||
fill = color.fill(invert)
|
||||
fillPolygon(patch.x(patchSize), patch.y(patchSize), patch.size)
|
||||
// restore rotation
|
||||
transform = saved
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns distance between two colors.
|
||||
*/
|
||||
private fun getColorDistance(c1: Color, c2: Color): Float {
|
||||
val dx = (c1.red - c2.red) * 256
|
||||
val dy = (c1.green - c2.green) * 256
|
||||
val dz = (c1.blue - c2.blue) * 256
|
||||
return Math.sqrt(dx * dx + dy * dy + dz * dz.toDouble()).toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
fun identicon(secureHash: SecureHash, size: Double): WritableImage {
|
||||
return IdenticonRenderer().render(secureHash.hashCode(), size)
|
||||
}
|
||||
|
||||
fun identiconToolTip(secureHash: SecureHash): Tooltip {
|
||||
return Tooltip(Splitter.fixedLength(16).split("$secureHash").joinToString("\n")).apply {
|
||||
contentDisplay = ContentDisplay.TOP
|
||||
textAlignment = TextAlignment.CENTER
|
||||
graphic = ImageView(identicon(secureHash, 30.0))
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package com.r3corda.explorer.model
|
||||
|
||||
import com.r3corda.core.crypto.Party
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
|
||||
class IdentityModel {
|
||||
val myIdentity = SimpleObjectProperty<Party?>()
|
||||
val notary = SimpleObjectProperty<Party?>()
|
||||
}
|
@ -1,12 +1,22 @@
|
||||
package com.r3corda.explorer.model
|
||||
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.scene.image.Image
|
||||
|
||||
enum class SelectedView {
|
||||
Home,
|
||||
Cash,
|
||||
Transaction,
|
||||
NewTransaction
|
||||
enum class SelectedView(val displayableName: String, val image: Image, val subviews: Array<SelectedView> = emptyArray()) {
|
||||
Home("Home", getImage("home.png")),
|
||||
Transaction("Transaction", getImage("tx.png")),
|
||||
Setting("Setting", getImage("settings_lrg.png")),
|
||||
NewTransaction("New Transaction", getImage("cash.png")),
|
||||
Cash("Cash", getImage("cash.png"), arrayOf(Transaction, NewTransaction)),
|
||||
NetworkMap("Network Map", getImage("cash.png")),
|
||||
Vault("Vault", getImage("cash.png"), arrayOf(Cash)),
|
||||
Network("Network", getImage("inst.png"), arrayOf(NetworkMap, Transaction))
|
||||
}
|
||||
|
||||
private fun getImage(imageName: String): Image {
|
||||
val basePath = "/com/r3corda/explorer/images"
|
||||
return Image("$basePath/$imageName")
|
||||
}
|
||||
|
||||
class TopLevelModel {
|
||||
|
@ -1,212 +1,142 @@
|
||||
package com.r3corda.explorer.views
|
||||
|
||||
import com.r3corda.client.fxutils.*
|
||||
import com.r3corda.client.model.ContractStateModel
|
||||
import com.r3corda.client.model.observableList
|
||||
import com.r3corda.client.model.observableValue
|
||||
import com.r3corda.client.model.*
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.core.contracts.Amount
|
||||
import com.r3corda.core.contracts.StateAndRef
|
||||
import com.r3corda.core.contracts.withoutIssuer
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.explorer.formatters.AmountFormatter
|
||||
import com.r3corda.explorer.identicon.identicon
|
||||
import com.r3corda.explorer.identicon.identiconToolTip
|
||||
import com.r3corda.explorer.model.ReportingCurrencyModel
|
||||
import com.r3corda.explorer.model.SettingsModel
|
||||
import com.r3corda.explorer.ui.*
|
||||
import com.sun.javafx.collections.ObservableListWrapper
|
||||
import javafx.application.Platform
|
||||
import javafx.beans.binding.Bindings
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.geometry.Insets
|
||||
import javafx.scene.Node
|
||||
import javafx.scene.Parent
|
||||
import javafx.scene.chart.NumberAxis
|
||||
import javafx.scene.control.*
|
||||
import javafx.scene.image.ImageView
|
||||
import javafx.scene.input.MouseButton
|
||||
import javafx.scene.input.MouseEvent
|
||||
import javafx.scene.layout.HBox
|
||||
import javafx.scene.layout.BorderPane
|
||||
import javafx.scene.layout.VBox
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
import tornadofx.UIComponent
|
||||
import tornadofx.View
|
||||
import tornadofx.*
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
|
||||
sealed class FilterCriteria {
|
||||
abstract fun matches(string: String): Boolean
|
||||
class CashViewer : View(), CordaView {
|
||||
// Inject UI elements.
|
||||
override val root: BorderPane by fxml()
|
||||
|
||||
object All : FilterCriteria() {
|
||||
override fun matches(string: String) = true
|
||||
// View's widget.
|
||||
override val viewName = "Cash"
|
||||
override val widget: Node = vbox {
|
||||
padding = Insets(0.0, 10.0, 0.0, 0.0)
|
||||
val xAxis = NumberAxis().apply {
|
||||
//isAutoRanging = true
|
||||
isMinorTickVisible = false
|
||||
isForceZeroInRange = false
|
||||
tickLabelFormatter = stringConverter {
|
||||
Instant.ofEpochMilli(it.toLong()).atZone(TimeZone.getDefault().toZoneId()).toLocalTime().toString()
|
||||
}
|
||||
}
|
||||
val yAxis = NumberAxis().apply {
|
||||
isAutoRanging = true
|
||||
isMinorTickVisible = false
|
||||
isForceZeroInRange = false
|
||||
tickLabelFormatter = stringConverter { it.toStringWithSuffix(0) }
|
||||
}
|
||||
linechart(null, xAxis, yAxis) {
|
||||
series("USD") {
|
||||
runAsync {
|
||||
while (true) {
|
||||
Thread.sleep(1000)
|
||||
Platform.runLater {
|
||||
// Modify data in UI thread.
|
||||
if (data.size > 300) data.remove(0, 1)
|
||||
data(System.currentTimeMillis(), sumAmount.value.quantity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
createSymbols = false
|
||||
animated = false
|
||||
}
|
||||
}
|
||||
|
||||
class FilterString(val filterString: String) : FilterCriteria() {
|
||||
override fun matches(string: String) = string.contains(filterString)
|
||||
}
|
||||
}
|
||||
|
||||
class CashViewer : View() {
|
||||
// Inject UI elements
|
||||
override val root: SplitPane by fxml()
|
||||
|
||||
val topSplitPane: SplitPane by fxid()
|
||||
// Left pane
|
||||
val leftPane: VBox by fxid()
|
||||
val searchCriteriaTextField: TextField by fxid()
|
||||
val searchCancelImageView: ImageView by fxid()
|
||||
val totalMatchingLabel: Label by fxid()
|
||||
val cashViewerTable: TreeTableView<ViewerNode> by fxid()
|
||||
val cashViewerTableIssuerCurrency: TreeTableColumn<ViewerNode, String> by fxid()
|
||||
val cashViewerTableLocalCurrency: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid()
|
||||
val cashViewerTableEquiv: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid()
|
||||
private val leftPane: VBox by fxid()
|
||||
private val splitPane: SplitPane by fxid()
|
||||
private val totalMatchingLabel: Label by fxid()
|
||||
private val cashViewerTable: TreeTableView<ViewerNode> by fxid()
|
||||
private val cashViewerTableIssuerCurrency: TreeTableColumn<ViewerNode, String> by fxid()
|
||||
private val cashViewerTableLocalCurrency: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid()
|
||||
private val cashViewerTableEquiv: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid()
|
||||
|
||||
// Right pane
|
||||
val rightPane: VBox by fxid()
|
||||
val totalPositionsLabel: Label by fxid()
|
||||
val equivSumLabel: Label by fxid()
|
||||
val cashStatesList: ListView<StateRow> by fxid()
|
||||
private val rightPane: VBox by fxid()
|
||||
private val totalPositionsLabel: Label by fxid()
|
||||
private val cashStatesList: ListView<StateRow> by fxid()
|
||||
private val toggleButton by fxid<Button>()
|
||||
|
||||
// Inject observables
|
||||
val cashStates by observableList(ContractStateModel::cashStates)
|
||||
val reportingCurrency: ObservableValue<Currency> by observableValue(SettingsModel::reportingCurrency)
|
||||
val reportingExchange: ObservableValue<Pair<Currency, (Amount<Currency>) -> Amount<Currency>>>
|
||||
by observableValue(ReportingCurrencyModel::reportingExchange)
|
||||
private val cashStates by observableList(ContractStateModel::cashStates)
|
||||
private val reportingCurrency by observableValue(SettingsModel::reportingCurrency)
|
||||
private val reportingExchange by observableValue(ReportingCurrencyModel::reportingExchange)
|
||||
private val exchangeRate: ObservableValue<ExchangeRate> by observableValue(ExchangeRateModel::exchangeRate)
|
||||
private val sumAmount = AmountBindings.sumAmountExchange(
|
||||
cashStates.map { it.state.data.amount.withoutIssuer() },
|
||||
reportingCurrency,
|
||||
exchangeRate
|
||||
)
|
||||
|
||||
private val selectedNode = cashViewerTable.singleRowSelection().map {
|
||||
when (it) {
|
||||
is SingleRowSelection.Selected -> it.node
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private val view = ChosenList(selectedNode.map {
|
||||
when (it) {
|
||||
null -> FXCollections.observableArrayList(leftPane)
|
||||
else -> FXCollections.observableArrayList(leftPane, rightPane)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* This holds the data for each row in the TreeTable.
|
||||
*/
|
||||
sealed class ViewerNode {
|
||||
object Root : ViewerNode()
|
||||
class IssuerNode(
|
||||
val issuer: Party,
|
||||
val sumEquivAmount: ObservableValue<out Amount<Currency>>,
|
||||
val states: ObservableList<StateAndRef<Cash.State>>
|
||||
) : ViewerNode()
|
||||
class CurrencyNode(
|
||||
val amount: ObservableValue<Amount<Currency>>,
|
||||
val equivAmount: ObservableValue<Amount<Currency>>,
|
||||
val states: ObservableList<StateAndRef<Cash.State>>
|
||||
) : ViewerNode()
|
||||
sealed class ViewerNode(val equivAmount: ObservableValue<out Amount<Currency>>,
|
||||
val states: ObservableList<StateAndRef<Cash.State>>) {
|
||||
class IssuerNode(val issuer: Party,
|
||||
sumEquivAmount: ObservableValue<out Amount<Currency>>,
|
||||
states: ObservableList<StateAndRef<Cash.State>>) : ViewerNode(sumEquivAmount, states)
|
||||
|
||||
class CurrencyNode(val amount: ObservableValue<Amount<Currency>>,
|
||||
equivAmount: ObservableValue<Amount<Currency>>,
|
||||
states: ObservableList<StateAndRef<Cash.State>>) : ViewerNode(equivAmount, states)
|
||||
}
|
||||
|
||||
/**
|
||||
* We allow filtering by both issuer and currency. We do this by filtering by both at the same time and picking the
|
||||
* one which produces more results, which seems to work, as the set of currency strings don't really overlap with
|
||||
* issuer strings.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Holds the filtering criterion based on the input text
|
||||
*/
|
||||
private val filterCriteria = searchCriteriaTextField.textProperty().map { text ->
|
||||
if (text.isBlank()) {
|
||||
FilterCriteria.All
|
||||
} else {
|
||||
FilterCriteria.FilterString(text)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter cash states based on issuer.
|
||||
*/
|
||||
private val issueFilteredCashStates = cashStates.filter(filterCriteria.map { criteria ->
|
||||
{ state: StateAndRef<Cash.State> ->
|
||||
criteria.matches(state.state.data.amount.token.issuer.party.toString())
|
||||
}
|
||||
})
|
||||
/**
|
||||
* Now filter cash states based on currency.
|
||||
*/
|
||||
private val currencyFilteredCashStates = cashStates.filter(filterCriteria.map { criteria ->
|
||||
{ state: StateAndRef<Cash.State> ->
|
||||
criteria.matches(state.state.data.amount.token.product.toString())
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Now we pick which one to use.
|
||||
*/
|
||||
private val filteredCashStates = ChosenList(filterCriteria.map {
|
||||
if (issueFilteredCashStates.size > currencyFilteredCashStates.size) {
|
||||
issueFilteredCashStates
|
||||
} else {
|
||||
currencyFilteredCashStates
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* This is where we aggregate the list of cash states into the TreeTable structure.
|
||||
*/
|
||||
val cashViewerIssueNodes: ObservableList<TreeItem<out ViewerNode.IssuerNode>> =
|
||||
/**
|
||||
* First we group the states based on the issuer. [memberStates] is all states holding currency issued by [issuer]
|
||||
*/
|
||||
AggregatedList(filteredCashStates, { it.state.data.amount.token.issuer.party }) { issuer, memberStates ->
|
||||
/**
|
||||
* Next we create subgroups based on currency. [memberStates] here is all states holding currency [currency] issued by [issuer] above.
|
||||
* Note that these states will not be displayed in the TreeTable, but rather in the side pane if the user clicks on the row.
|
||||
*/
|
||||
val currencyNodes = AggregatedList(memberStates, { it.state.data.amount.token.product }) { currency, memberStates ->
|
||||
/**
|
||||
* We sum the states in the subgroup, to be displayed in the "Local Currency" column
|
||||
*/
|
||||
val amounts = memberStates.map { it.state.data.amount.withoutIssuer() }
|
||||
val sumAmount = amounts.foldObservable(Amount(0, currency), Amount<Currency>::plus)
|
||||
|
||||
/**
|
||||
* We exchange the sum to the reporting currency, to be displayed in the "<currency> Equiv" column.
|
||||
*/
|
||||
val equivSumAmount = EasyBind.combine(sumAmount, reportingExchange) { sum, exchange ->
|
||||
exchange.second(sum)
|
||||
}
|
||||
/**
|
||||
* Finally assemble the actual TreeTable Currency node.
|
||||
*/
|
||||
TreeItem(ViewerNode.CurrencyNode(sumAmount, equivSumAmount, memberStates))
|
||||
}
|
||||
|
||||
/**
|
||||
* Now that we have all nodes per currency, we sum the exchanged amounts, to be displayed in the
|
||||
* "<currency> Equiv" column, this time on the issuer level.
|
||||
*/
|
||||
val equivAmounts = currencyNodes.map { it.value.equivAmount }.flatten()
|
||||
val equivSumAmount = reportingCurrency.bind { currency ->
|
||||
equivAmounts.foldObservable(Amount(0, currency), Amount<Currency>::plus)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble the Issuer node.
|
||||
*/
|
||||
val treeItem = TreeItem(ViewerNode.IssuerNode(issuer, equivSumAmount, memberStates))
|
||||
|
||||
/**
|
||||
* Bind the children in the TreeTable structure.
|
||||
*
|
||||
* TODO Perhaps we shouldn't do this here, but rather have a generic way of binding nodes to the treetable once.
|
||||
*/
|
||||
treeItem.isExpanded = true
|
||||
val children: List<TreeItem<out ViewerNode.IssuerNode>> = treeItem.children
|
||||
Bindings.bindContent(children, currencyNodes)
|
||||
treeItem
|
||||
}
|
||||
|
||||
/**
|
||||
* Now we build up the Observables needed for the side pane, given that the user clicks on a row.
|
||||
*/
|
||||
val selectedViewerNode = cashViewerTable.singleRowSelection()
|
||||
|
||||
/**
|
||||
* Holds data for a single state, to be displayed in the list in the side pane.
|
||||
*/
|
||||
data class StateRow (
|
||||
val originated: LocalDateTime,
|
||||
val stateAndRef: StateAndRef<Cash.State>
|
||||
)
|
||||
data class StateRow(val originated: LocalDateTime, val stateAndRef: StateAndRef<Cash.State>)
|
||||
|
||||
/**
|
||||
* A small class describing the graphics of a single state.
|
||||
*/
|
||||
inner class StateRowGraphic(
|
||||
val stateRow: StateRow
|
||||
) : UIComponent() {
|
||||
override val root: HBox by fxml("CashStateViewer.fxml")
|
||||
inner class StateRowGraphic(val stateRow: StateRow) : UIComponent() {
|
||||
override val root: Parent by fxml("CashStateViewer.fxml")
|
||||
|
||||
val equivLabel: Label by fxid()
|
||||
val stateIdValueLabel: Label by fxid()
|
||||
@ -224,8 +154,12 @@ class CashViewer : View() {
|
||||
val amountFormatter = AmountFormatter.boring
|
||||
val equivFormatter = AmountFormatter.boring
|
||||
|
||||
stateIdValueLabel.apply {
|
||||
text = stateRow.stateAndRef.ref.toString().substring(0, 16) + "...[${stateRow.stateAndRef.ref.index}]"
|
||||
graphic = ImageView(identicon(stateRow.stateAndRef.ref.txhash, 10.0))
|
||||
tooltip = identiconToolTip(stateRow.stateAndRef.ref.txhash)
|
||||
}
|
||||
equivLabel.textProperty().bind(equivAmount.map { it.token.currencyCode.toString() })
|
||||
stateIdValueLabel.text = stateRow.stateAndRef.ref.toString()
|
||||
issuerValueLabel.text = stateRow.stateAndRef.state.data.amount.token.issuer.toString()
|
||||
originatedValueLabel.text = stateRow.originated.toString()
|
||||
amountValueLabel.text = amountFormatter.format(amountNoIssuer)
|
||||
@ -233,132 +167,140 @@ class CashViewer : View() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of states related to the current selection. If none or the root is selected it's empty, if an issuer or
|
||||
* currency node is selected it's the relevant states.
|
||||
*/
|
||||
private val noSelectionStates = FXCollections.observableArrayList<StateAndRef<Cash.State>>()
|
||||
private val selectedViewerNodeStates = ChosenList(selectedViewerNode.map { selection ->
|
||||
when (selection) {
|
||||
is SingleRowSelection.None -> noSelectionStates
|
||||
is SingleRowSelection.Selected ->
|
||||
when (selection.node) {
|
||||
CashViewer.ViewerNode.Root -> noSelectionStates
|
||||
is CashViewer.ViewerNode.IssuerNode -> selection.node.states
|
||||
is CashViewer.ViewerNode.CurrencyNode -> selection.node.states
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* We re-display the exchanged sum amount, if we have a selection.
|
||||
*/
|
||||
private val noSelectionSumEquiv = reportingCurrency.map { Amount(0, it) }
|
||||
private val selectedViewerNodeSumEquiv = selectedViewerNode.bindOut { selection ->
|
||||
when (selection) {
|
||||
is SingleRowSelection.None -> noSelectionSumEquiv
|
||||
is SingleRowSelection.Selected ->
|
||||
when (selection.node) {
|
||||
ViewerNode.Root -> noSelectionSumEquiv
|
||||
is ViewerNode.IssuerNode -> selection.node.sumEquivAmount
|
||||
is ViewerNode.CurrencyNode -> selection.node.equivAmount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We add some extra timestamp data here to the selected states.
|
||||
*
|
||||
* TODO update this once we have actual timestamps.
|
||||
*/
|
||||
private val stateRows = selectedViewerNodeStates.map { StateRow(LocalDateTime.now(), it) }
|
||||
|
||||
/**
|
||||
* We only display the right pane if a node is selected in the TreeTable.
|
||||
*/
|
||||
private val onlyLeftPaneShown = FXCollections.observableArrayList<Node>(leftPane)
|
||||
private val bothPanesShown = FXCollections.observableArrayList<Node>(leftPane, rightPane)
|
||||
private val panesShown = ChosenList<Node>(selectedViewerNode.map {
|
||||
when (it) {
|
||||
is SingleRowSelection.None -> onlyLeftPaneShown
|
||||
is SingleRowSelection.Selected -> bothPanesShown
|
||||
}
|
||||
})
|
||||
|
||||
// Wire up UI
|
||||
init {
|
||||
searchCancelImageView.setOnMouseClicked { event: MouseEvent ->
|
||||
if (event.button == MouseButton.PRIMARY) {
|
||||
searchCriteriaTextField.text = ""
|
||||
Bindings.bindContent(splitPane.items, view)
|
||||
/**
|
||||
* We allow filtering by both issuer and currency. We do this by filtering by both at the same time and picking the
|
||||
* one which produces more results, which seems to work, as the set of currency strings don't really overlap with
|
||||
* issuer strings.
|
||||
*/
|
||||
val searchField = SearchField(cashStates, arrayOf(
|
||||
{ state, text -> state.state.data.amount.token.product.toString().contains(text, true) },
|
||||
{ state, text -> state.state.data.amount.token.issuer.party.toString().contains(text, true) }
|
||||
))
|
||||
root.top = searchField.root
|
||||
|
||||
/**
|
||||
* This is where we aggregate the list of cash states into the TreeTable structure.
|
||||
*/
|
||||
val cashViewerIssueNodes: ObservableList<TreeItem<out ViewerNode.IssuerNode>> =
|
||||
/**
|
||||
* First we group the states based on the issuer. [memberStates] is all states holding currency issued by [issuer]
|
||||
*/
|
||||
AggregatedList(searchField.filteredData, { it.state.data.amount.token.issuer.party }) { issuer, memberStates ->
|
||||
/**
|
||||
* Next we create subgroups based on currency. [memberStates] here is all states holding currency [currency] issued by [issuer] above.
|
||||
* Note that these states will not be displayed in the TreeTable, but rather in the side pane if the user clicks on the row.
|
||||
*/
|
||||
val currencyNodes = AggregatedList(memberStates, { it.state.data.amount.token.product }) { currency, memberStates ->
|
||||
/**
|
||||
* We sum the states in the subgroup, to be displayed in the "Local Currency" column
|
||||
*/
|
||||
val amounts = memberStates.map { it.state.data.amount.withoutIssuer() }
|
||||
val sumAmount = amounts.foldObservable(Amount(0, currency), Amount<Currency>::plus)
|
||||
|
||||
/**
|
||||
* We exchange the sum to the reporting currency, to be displayed in the "<currency> Equiv" column.
|
||||
*/
|
||||
val equivSumAmount = EasyBind.combine(sumAmount, reportingExchange) { sum, exchange ->
|
||||
exchange.second(sum)
|
||||
}
|
||||
/**
|
||||
* Finally assemble the actual TreeTable Currency node.
|
||||
*/
|
||||
TreeItem(ViewerNode.CurrencyNode(sumAmount, equivSumAmount, memberStates))
|
||||
}
|
||||
|
||||
/**
|
||||
* Now that we have all nodes per currency, we sum the exchanged amounts, to be displayed in the
|
||||
* "<currency> Equiv" column, this time on the issuer level.
|
||||
*/
|
||||
val equivAmounts = currencyNodes.map { it.value.equivAmount }.flatten()
|
||||
val equivSumAmount = reportingCurrency.bind { currency ->
|
||||
equivAmounts.foldObservable(Amount(0, currency), Amount<Currency>::plus)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble the Issuer node.
|
||||
*/
|
||||
val treeItem = TreeItem(ViewerNode.IssuerNode(issuer, equivSumAmount, memberStates))
|
||||
|
||||
/**
|
||||
* Bind the children in the TreeTable structure.
|
||||
*
|
||||
* TODO Perhaps we shouldn't do this here, but rather have a generic way of binding nodes to the treetable once.
|
||||
*/
|
||||
treeItem.isExpanded = true
|
||||
val children: List<TreeItem<out ViewerNode.IssuerNode>> = treeItem.children
|
||||
Bindings.bindContent(children, currencyNodes)
|
||||
treeItem
|
||||
}
|
||||
|
||||
cashViewerTable.apply() {
|
||||
root = TreeItem()
|
||||
val children: List<TreeItem<out ViewerNode>> = root.children
|
||||
Bindings.bindContent(children, cashViewerIssueNodes)
|
||||
root.isExpanded = true
|
||||
isShowRoot = false
|
||||
// TODO use smart resize
|
||||
setColumnPrefWidthPolicy { tableWidthWithoutPaddingAndBorder, column ->
|
||||
Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / columns.size).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
Bindings.bindContent(topSplitPane.items, panesShown)
|
||||
|
||||
totalPositionsLabel.textProperty().bind(Bindings.size(selectedViewerNodeStates).map {
|
||||
val plural = if (it == 1) "" else "s"
|
||||
"Total $it position$plural"
|
||||
})
|
||||
|
||||
val equivSumLabelFormatter = AmountFormatter.boring
|
||||
equivSumLabel.textProperty().bind(selectedViewerNodeSumEquiv.map {
|
||||
equivSumLabelFormatter.format(it)
|
||||
})
|
||||
|
||||
Bindings.bindContent(cashStatesList.items, stateRows)
|
||||
|
||||
cashStatesList.setCustomCellFactory { StateRowGraphic(it).root }
|
||||
|
||||
val cellFactory = AmountFormatter.boring.toTreeTableCellFactory<ViewerNode, Amount<Currency>>()
|
||||
|
||||
// TODO use smart resize
|
||||
cashViewerTable.setColumnPrefWidthPolicy { tableWidthWithoutPaddingAndBorder, column ->
|
||||
Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / cashViewerTable.columns.size).toInt()
|
||||
}
|
||||
val currencyCellFactory = AmountFormatter.boring.toTreeTableCellFactory<ViewerNode, Amount<Currency>>()
|
||||
|
||||
cashViewerTableIssuerCurrency.setCellValueFactory {
|
||||
val node = it.value.value
|
||||
when (node) {
|
||||
ViewerNode.Root -> "".lift()
|
||||
is ViewerNode.IssuerNode -> node.issuer.toString().lift()
|
||||
is ViewerNode.CurrencyNode -> node.amount.map { it.token.toString() }
|
||||
}
|
||||
}
|
||||
cashViewerTableLocalCurrency.setCellValueFactory {
|
||||
val node = it.value.value
|
||||
when (node) {
|
||||
ViewerNode.Root -> null.lift()
|
||||
is ViewerNode.IssuerNode -> null.lift()
|
||||
is ViewerNode.CurrencyNode -> node.amount.map { it }
|
||||
cashViewerTableLocalCurrency.apply {
|
||||
setCellValueFactory {
|
||||
val node = it.value.value
|
||||
when (node) {
|
||||
is ViewerNode.IssuerNode -> null.lift()
|
||||
is ViewerNode.CurrencyNode -> node.amount.map { it }
|
||||
}
|
||||
}
|
||||
cellFactory = currencyCellFactory
|
||||
/**
|
||||
* We must set this, otherwise on sort an exception will be thrown, as it will try to compare Amounts of differing currency
|
||||
*/
|
||||
isSortable = false
|
||||
}
|
||||
cashViewerTableLocalCurrency.cellFactory = cellFactory
|
||||
/**
|
||||
* We must set this, otherwise on sort an exception will be thrown, as it will try to compare Amounts of differing currency
|
||||
*/
|
||||
cashViewerTableLocalCurrency.isSortable = false
|
||||
cashViewerTableEquiv.setCellValueFactory {
|
||||
val node = it.value.value
|
||||
when (node) {
|
||||
ViewerNode.Root -> null.lift()
|
||||
is ViewerNode.IssuerNode -> node.sumEquivAmount.map { it }
|
||||
is ViewerNode.CurrencyNode -> node.equivAmount.map { it }
|
||||
|
||||
cashViewerTableEquiv.apply {
|
||||
setCellValueFactory {
|
||||
it.value.value.equivAmount.map { it }
|
||||
}
|
||||
cellFactory = currencyCellFactory
|
||||
textProperty().bind(reportingCurrency.map { "$it Equiv" })
|
||||
}
|
||||
cashViewerTableEquiv.cellFactory = cellFactory
|
||||
cashViewerTableEquiv.textProperty().bind(reportingCurrency.map { "$it Equiv" })
|
||||
|
||||
cashViewerTable.root = TreeItem(ViewerNode.Root)
|
||||
val children: List<TreeItem<out ViewerNode>> = cashViewerTable.root.children
|
||||
Bindings.bindContent(children, cashViewerIssueNodes)
|
||||
|
||||
cashViewerTable.root.isExpanded = true
|
||||
cashViewerTable.isShowRoot = false
|
||||
// Right Pane.
|
||||
totalPositionsLabel.textProperty().bind(cashStatesList.itemsProperty().map {
|
||||
val plural = if (it.size == 1) "" else "s"
|
||||
"Total ${it.size} position$plural"
|
||||
})
|
||||
|
||||
cashStatesList.apply {
|
||||
// TODO update this once we have actual timestamps.
|
||||
itemsProperty().bind(selectedNode.map { it?.states?.map { StateRow(LocalDateTime.now(), it) } ?: ObservableListWrapper(emptyList()) })
|
||||
setCustomCellFactory { StateRowGraphic(it).root }
|
||||
}
|
||||
|
||||
// TODO Think about i18n!
|
||||
totalMatchingLabel.textProperty().bind(Bindings.size(cashViewerIssueNodes).map {
|
||||
val plural = if (it == 1) "" else "s"
|
||||
"Total $it matching issuer$plural"
|
||||
})
|
||||
|
||||
toggleButton.setOnAction {
|
||||
cashViewerTable.selectionModel.clearSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
package com.r3corda.explorer.views
|
||||
|
||||
import javafx.scene.Node
|
||||
|
||||
/**
|
||||
* Corda view interface, provides methods to construct various UI component used by the explorer UI framework.
|
||||
* TODO : Implement this interface on all views and register the views with ViewModel when UI start up, then we can use the ViewModel to dynamically create sidebar and dashboard without manual wiring.
|
||||
* TODO : Sidebar icons.
|
||||
*/
|
||||
interface CordaView {
|
||||
val widget: Node?
|
||||
val viewName: String
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package com.r3corda.explorer.views
|
||||
|
||||
import javafx.application.Platform
|
||||
import javafx.util.StringConverter
|
||||
|
||||
/**
|
||||
* Helper method to reduce boiler plate code
|
||||
*/
|
||||
fun <T> stringConverter(fromStringFunction: ((String?) -> T)? = null, toStringFunction: (T) -> String): StringConverter<T> {
|
||||
val converter = object : StringConverter<T>() {
|
||||
override fun fromString(string: String?): T {
|
||||
return fromStringFunction?.invoke(string) ?: throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
|
||||
override fun toString(o: T): String {
|
||||
return toStringFunction(o)
|
||||
}
|
||||
}
|
||||
return converter
|
||||
}
|
||||
|
||||
/**
|
||||
* Format Number to string with metric prefix.
|
||||
*/
|
||||
fun Number.toStringWithSuffix(precision: Int = 1): String {
|
||||
if (this.toDouble() < 1000) return "$this"
|
||||
val exp = (Math.log(this.toDouble()) / Math.log(1000.0)).toInt()
|
||||
return "${(this.toDouble() / Math.pow(1000.0, exp.toDouble())).format(precision)} ${"kMGTPE"[exp - 1]}"
|
||||
}
|
||||
|
||||
fun Double.format(precision: Int) = String.format("%.${precision}f", this)
|
||||
|
||||
/**
|
||||
* Helper method to make sure block runs in FX thread
|
||||
*/
|
||||
fun runInFxApplicationThread(block: () -> Unit) {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
block()
|
||||
} else {
|
||||
Platform.runLater(block)
|
||||
}
|
||||
}
|
@ -1,65 +1,31 @@
|
||||
package com.r3corda.explorer.views
|
||||
|
||||
import com.r3corda.client.fxutils.map
|
||||
import com.r3corda.client.model.NetworkIdentityModel
|
||||
import com.r3corda.client.model.observableValue
|
||||
import com.r3corda.explorer.model.SelectedView
|
||||
import com.r3corda.explorer.model.TopLevelModel
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.scene.control.Button
|
||||
import javafx.scene.control.Label
|
||||
import javafx.scene.image.Image
|
||||
import javafx.scene.control.SplitMenuButton
|
||||
import javafx.scene.image.ImageView
|
||||
import javafx.scene.layout.VBox
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
import javafx.scene.layout.GridPane
|
||||
import tornadofx.View
|
||||
|
||||
class Header : View() {
|
||||
override val root: VBox by fxml()
|
||||
override val root: GridPane by fxml()
|
||||
|
||||
private val sectionIcon: ImageView by fxid()
|
||||
private val sectionIconContainer: VBox by fxid()
|
||||
private val sectionLabel: Label by fxid()
|
||||
private val debugNextButton: Button by fxid()
|
||||
private val debugGoStopButton: Button by fxid()
|
||||
|
||||
private val selectedView: ObservableValue<SelectedView> by observableValue(TopLevelModel::selectedView)
|
||||
|
||||
private val homeImage = Image("/com/r3corda/explorer/images/home.png")
|
||||
private val cashImage = Image("/com/r3corda/explorer/images/cash.png")
|
||||
private val transactionImage = Image("/com/r3corda/explorer/images/tx.png")
|
||||
private val userButton: SplitMenuButton by fxid()
|
||||
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
|
||||
private val selectedView by observableValue(TopLevelModel::selectedView)
|
||||
|
||||
init {
|
||||
sectionLabel.textProperty().bind(EasyBind.map(selectedView) {
|
||||
when (it) {
|
||||
SelectedView.Home -> "Home"
|
||||
SelectedView.Cash -> "Cash"
|
||||
SelectedView.Transaction -> "Transactions"
|
||||
SelectedView.NewTransaction -> "New Transaction"
|
||||
null -> "Home"
|
||||
sectionLabel.textProperty().bind(selectedView.map { it.displayableName })
|
||||
sectionLabel.graphicProperty().bind(selectedView.map {
|
||||
ImageView(it.image).apply {
|
||||
fitHeight = 30.0
|
||||
fitWidth = 30.0
|
||||
}
|
||||
})
|
||||
|
||||
sectionIcon.imageProperty().bind(EasyBind.map(selectedView) {
|
||||
when (it) {
|
||||
SelectedView.Home -> homeImage
|
||||
SelectedView.Cash -> cashImage
|
||||
SelectedView.Transaction -> transactionImage
|
||||
SelectedView.NewTransaction -> cashImage
|
||||
null -> homeImage
|
||||
}
|
||||
})
|
||||
|
||||
// JavaFX bugs and doesn't invalidate the wrapping Box's height if the icon fit height is first set to
|
||||
// unbounded (0.0) - which is what the label's height is initially, so we set it to 1.0 instead
|
||||
val secionLabelHeightNonZero = EasyBind.map(sectionLabel.heightProperty()) {
|
||||
if (it == 0.0) {
|
||||
1.0
|
||||
} else {
|
||||
it.toDouble()
|
||||
}
|
||||
}
|
||||
|
||||
sectionIconContainer.minWidthProperty().bind(secionLabelHeightNonZero)
|
||||
sectionIcon.fitWidthProperty().bind(secionLabelHeightNonZero)
|
||||
sectionIcon.fitHeightProperty().bind(sectionIcon.fitWidthProperty())
|
||||
userButton.textProperty().bind(myIdentity.map { it?.legalIdentity?.name })
|
||||
}
|
||||
}
|
@ -1,73 +1,55 @@
|
||||
package com.r3corda.explorer.views
|
||||
|
||||
import com.r3corda.client.fxutils.AmountBindings
|
||||
import com.r3corda.client.fxutils.map
|
||||
import com.r3corda.client.model.*
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.core.contracts.StateAndRef
|
||||
import com.r3corda.core.contracts.withoutIssuer
|
||||
import com.r3corda.explorer.formatters.AmountFormatter
|
||||
import com.r3corda.client.model.GatheredTransactionData
|
||||
import com.r3corda.client.model.GatheredTransactionDataModel
|
||||
import com.r3corda.client.model.observableListReadOnly
|
||||
import com.r3corda.client.model.writableValue
|
||||
import com.r3corda.explorer.model.SelectedView
|
||||
import com.r3corda.explorer.model.SettingsModel
|
||||
import com.r3corda.explorer.model.TopLevelModel
|
||||
import javafx.beans.binding.Bindings
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.beans.value.WritableValue
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.scene.Node
|
||||
import javafx.scene.Parent
|
||||
import javafx.scene.control.Label
|
||||
import javafx.scene.control.TitledPane
|
||||
import javafx.scene.input.MouseButton
|
||||
import javafx.scene.input.MouseEvent
|
||||
import javafx.scene.layout.TilePane
|
||||
import tornadofx.View
|
||||
import java.util.*
|
||||
import tornadofx.find
|
||||
|
||||
class Home : View() {
|
||||
override val root: TilePane by fxml()
|
||||
|
||||
override val root: Parent by fxml()
|
||||
private val tilePane: TilePane by fxid()
|
||||
private val ourCashPane: TitledPane by fxid()
|
||||
private val ourCashLabel: Label by fxid()
|
||||
|
||||
private val ourTransactionsPane: TitledPane by fxid()
|
||||
private val ourTransactionsLabel: Label by fxid()
|
||||
|
||||
private val newTransaction: TitledPane by fxid()
|
||||
|
||||
private val selectedView: WritableValue<SelectedView> by writableValue(TopLevelModel::selectedView)
|
||||
private val cashStates: ObservableList<StateAndRef<Cash.State>> by observableList(ContractStateModel::cashStates)
|
||||
private val gatheredTransactionDataList: ObservableList<out GatheredTransactionData>
|
||||
by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
|
||||
private val reportingCurrency: ObservableValue<Currency> by observableValue(SettingsModel::reportingCurrency)
|
||||
private val exchangeRate: ObservableValue<ExchangeRate> by observableValue(ExchangeRateModel::exchangeRate)
|
||||
|
||||
private val sumAmount = AmountBindings.sumAmountExchange(
|
||||
cashStates.map { it.state.data.amount.withoutIssuer() },
|
||||
reportingCurrency,
|
||||
exchangeRate
|
||||
)
|
||||
|
||||
init {
|
||||
val formatter = AmountFormatter.boring
|
||||
|
||||
ourCashLabel.textProperty().bind(sumAmount.map { formatter.format(it) })
|
||||
ourCashPane.setOnMouseClicked { clickEvent ->
|
||||
if (clickEvent.button == MouseButton.PRIMARY) {
|
||||
selectedView.value = SelectedView.Cash
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: register views in view model and populate the dashboard dynamically.
|
||||
ourTransactionsLabel.textProperty().bind(
|
||||
Bindings.size(gatheredTransactionDataList).map { it.toString() }
|
||||
)
|
||||
ourTransactionsPane.setOnMouseClicked { clickEvent ->
|
||||
if (clickEvent.button == MouseButton.PRIMARY) {
|
||||
selectedView.value = SelectedView.Transaction
|
||||
}
|
||||
}
|
||||
newTransaction.setOnMouseClicked { clickEvent ->
|
||||
if (clickEvent.button == MouseButton.PRIMARY) {
|
||||
selectedView.value = SelectedView.NewTransaction
|
||||
}
|
||||
|
||||
ourCashPane.apply {
|
||||
content = find(CashViewer::class).widget
|
||||
}
|
||||
|
||||
tilePane.widthProperty().addListener { e ->
|
||||
val prefWidth = 350
|
||||
val columns: Int = ((tilePane.width - 10) / prefWidth).toInt()
|
||||
tilePane.children.forEach { (it as? TitledPane)?.prefWidth = (tilePane.width - 10) / columns }
|
||||
}
|
||||
}
|
||||
|
||||
fun changeView(event: MouseEvent) {
|
||||
if (event.button == MouseButton.PRIMARY) {
|
||||
selectedView.value = SelectedView.valueOf((event.source as Node).id)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package com.r3corda.explorer.views
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import javafx.beans.property.SimpleIntegerProperty
|
||||
import javafx.scene.control.*
|
||||
import javafx.util.converter.IntegerStringConverter
|
||||
import org.controlsfx.dialog.ExceptionDialog
|
||||
import tornadofx.View
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class LoginView : View() {
|
||||
override val root: DialogPane by fxml()
|
||||
|
||||
private val host by fxid<TextField>()
|
||||
private val port by fxid<TextField>()
|
||||
private val username by fxid<TextField>()
|
||||
private val password by fxid<PasswordField>()
|
||||
private val portProperty = SimpleIntegerProperty()
|
||||
|
||||
fun login(loginFunction: (HostAndPort, String, String) -> Unit) {
|
||||
val loggedIn = Dialog<Boolean>().apply {
|
||||
dialogPane = root
|
||||
var exception = false
|
||||
setResultConverter {
|
||||
exception = false
|
||||
when (it?.buttonData) {
|
||||
ButtonBar.ButtonData.OK_DONE -> try {
|
||||
// TODO : Run this async to avoid UI lockup.
|
||||
loginFunction(HostAndPort.fromParts(host.text, portProperty.value), username.text, password.text)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
ExceptionDialog(e).showAndWait()
|
||||
exception = true
|
||||
false
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
setOnCloseRequest {
|
||||
if (!result && !exception) {
|
||||
when (Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to exit Corda explorer?").apply {
|
||||
}.showAndWait().get()) {
|
||||
ButtonType.OK -> exitProcess(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.showAndWait().get()
|
||||
|
||||
if (!loggedIn) login(loginFunction)
|
||||
}
|
||||
|
||||
init {
|
||||
// Restrict text field to Integer only.
|
||||
val integerFormat = Pattern.compile("-?(\\d*)").run {
|
||||
TextFormatter<Int>(IntegerStringConverter(), null) { change ->
|
||||
val newText = change.controlNewText
|
||||
if (matcher(newText).matches()) change else null
|
||||
}
|
||||
}
|
||||
port.textFormatter = integerFormat
|
||||
portProperty.bind(integerFormat.valueProperty())
|
||||
}
|
||||
}
|
@ -6,12 +6,9 @@ import com.r3corda.client.model.NodeMonitorModel
|
||||
import com.r3corda.client.model.observableList
|
||||
import com.r3corda.client.model.observableValue
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.serialization.OpaqueBytes
|
||||
import com.r3corda.explorer.components.ExceptionDialog
|
||||
import com.r3corda.explorer.model.CashTransaction
|
||||
import com.r3corda.explorer.model.IdentityModel
|
||||
import com.r3corda.node.services.messaging.CordaRPCOps
|
||||
import com.r3corda.node.services.messaging.TransactionBuildResult
|
||||
import javafx.beans.binding.Bindings
|
||||
@ -22,8 +19,8 @@ import javafx.collections.ObservableList
|
||||
import javafx.scene.Node
|
||||
import javafx.scene.Parent
|
||||
import javafx.scene.control.*
|
||||
import javafx.util.StringConverter
|
||||
import javafx.util.converter.BigDecimalStringConverter
|
||||
import org.controlsfx.dialog.ExceptionDialog
|
||||
import tornadofx.View
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
@ -43,21 +40,20 @@ class NewTransaction : View() {
|
||||
private val transactionTypeCB: ChoiceBox<CashTransaction> by fxid()
|
||||
private val amount: TextField by fxid()
|
||||
private val currency: ChoiceBox<Currency> by fxid()
|
||||
|
||||
private val networkIdentities: ObservableList<NodeInfo> by observableList(NetworkIdentityModel::networkIdentities)
|
||||
|
||||
private val rpcProxy: ObservableValue<CordaRPCOps?> by observableValue(NodeMonitorModel::proxyObservable)
|
||||
private val myIdentity: ObservableValue<Party?> by observableValue(IdentityModel::myIdentity)
|
||||
private val notary: ObservableValue<Party?> by observableValue(IdentityModel::notary)
|
||||
|
||||
private val issueRefLabel: Label by fxid()
|
||||
private val issueRefTextField: TextField by fxid()
|
||||
|
||||
// Inject data
|
||||
private val parties: ObservableList<NodeInfo> by observableList(NetworkIdentityModel::parties)
|
||||
private val rpcProxy: ObservableValue<CordaRPCOps?> by observableValue(NodeMonitorModel::proxyObservable)
|
||||
private val myIdentity: ObservableValue<NodeInfo?> by observableValue(NetworkIdentityModel::myIdentity)
|
||||
private val notaries: ObservableList<NodeInfo> by observableList(NetworkIdentityModel::notaries)
|
||||
|
||||
private fun ObservableValue<*>.isNotNull(): BooleanBinding {
|
||||
return Bindings.createBooleanBinding({ this.value != null }, arrayOf(this))
|
||||
}
|
||||
|
||||
fun resetScreen() {
|
||||
private fun resetScreen() {
|
||||
partyBChoiceBox.valueProperty().set(null)
|
||||
transactionTypeCB.valueProperty().set(null)
|
||||
currency.valueProperty().set(null)
|
||||
@ -66,28 +62,22 @@ class NewTransaction : View() {
|
||||
|
||||
init {
|
||||
// Disable everything when not connected to node.
|
||||
val enableProperty = myIdentity.isNotNull().and(notary.isNotNull()).and(rpcProxy.isNotNull())
|
||||
val notariesNotNullBinding = Bindings.createBooleanBinding({ notaries.isNotEmpty() }, arrayOf(notaries))
|
||||
val enableProperty = myIdentity.isNotNull().and(rpcProxy.isNotNull()).and(notariesNotNullBinding)
|
||||
root.disableProperty().bind(enableProperty.not())
|
||||
transactionTypeCB.items = FXCollections.observableArrayList(CashTransaction.values().asList())
|
||||
|
||||
// Party A textfield always display my identity name, not editable.
|
||||
partyATextField.isEditable = false
|
||||
partyATextField.textProperty().bind(myIdentity.map { it?.name ?: "" })
|
||||
partyATextField.textProperty().bind(myIdentity.map { it?.legalIdentity?.name ?: "" })
|
||||
partyALabel.textProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameA?.let { "$it : " } })
|
||||
partyATextField.visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameA }.isNotNull())
|
||||
|
||||
partyBLabel.textProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB?.let { "$it : " } })
|
||||
partyBChoiceBox.visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB }.isNotNull())
|
||||
partyBChoiceBox.items = networkIdentities
|
||||
|
||||
partyBChoiceBox.converter = object : StringConverter<NodeInfo?>() {
|
||||
override fun toString(node: NodeInfo?): String {
|
||||
return node?.legalIdentity?.name ?: ""
|
||||
}
|
||||
|
||||
override fun fromString(string: String?): NodeInfo {
|
||||
throw UnsupportedOperationException("not implemented")
|
||||
}
|
||||
partyBChoiceBox.apply {
|
||||
visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB }.isNotNull())
|
||||
partyBChoiceBox.items = parties
|
||||
converter = stringConverter { it?.legalIdentity?.name ?: "" }
|
||||
}
|
||||
|
||||
// BigDecimal text Formatter, restricting text box input to decimal values.
|
||||
@ -123,7 +113,8 @@ class NewTransaction : View() {
|
||||
executeButton.setOnAction { event ->
|
||||
// Null checks to ensure these observable values are set, execute button should be disabled if any of these value are null, this extra checks are for precaution and getting non-nullable values without using !!.
|
||||
myIdentity.value?.let { myIdentity ->
|
||||
notary.value?.let { notary ->
|
||||
// TODO : Allow user to chose which notary to use?
|
||||
notaries.first()?.let { notary ->
|
||||
rpcProxy.value?.let { rpcProxy ->
|
||||
Triple(myIdentity, notary, rpcProxy)
|
||||
}
|
||||
@ -131,13 +122,14 @@ class NewTransaction : View() {
|
||||
}?.let {
|
||||
val (myIdentity, notary, rpcProxy) = it
|
||||
transactionTypeCB.value?.let {
|
||||
// Default issuer reference to 1 if not specified.
|
||||
val issueRef = OpaqueBytes(if (issueRefTextField.text.trim().isNotBlank()) issueRefTextField.text.toByteArray() else ByteArray(1, { 1 }))
|
||||
// TODO : Change these commands into individual RPC methods instead of using executeCommand.
|
||||
val command = when (it) {
|
||||
CashTransaction.Issue -> ClientToServiceCommand.IssueCash(Amount(textFormatter.value, currency.value), issueRef, partyBChoiceBox.value.legalIdentity, notary)
|
||||
CashTransaction.Pay -> ClientToServiceCommand.PayCash(Amount(textFormatter.value, Issued(PartyAndReference(myIdentity, issueRef), currency.value)), partyBChoiceBox.value.legalIdentity)
|
||||
CashTransaction.Issue -> ClientToServiceCommand.IssueCash(Amount(textFormatter.value, currency.value), issueRef, partyBChoiceBox.value.legalIdentity, notary.notaryIdentity)
|
||||
CashTransaction.Pay -> ClientToServiceCommand.PayCash(Amount(textFormatter.value, Issued(PartyAndReference(myIdentity.legalIdentity, issueRef), currency.value)), partyBChoiceBox.value.legalIdentity)
|
||||
CashTransaction.Exit -> ClientToServiceCommand.ExitCash(Amount(textFormatter.value, currency.value), issueRef)
|
||||
}
|
||||
|
||||
val dialog = Alert(Alert.AlertType.INFORMATION).apply {
|
||||
headerText = null
|
||||
contentText = "Transaction Started."
|
||||
@ -165,8 +157,7 @@ class NewTransaction : View() {
|
||||
dialog.close()
|
||||
ExceptionDialog(it.source.exception).apply {
|
||||
initOwner((event.target as Node).scene.window)
|
||||
showAndWait()
|
||||
}
|
||||
}.showAndWait()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,37 @@
|
||||
package com.r3corda.explorer.views
|
||||
|
||||
import com.r3corda.client.fxutils.ChosenList
|
||||
import com.r3corda.client.fxutils.filter
|
||||
import com.r3corda.client.fxutils.lift
|
||||
import com.r3corda.client.fxutils.map
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.scene.Parent
|
||||
import javafx.scene.control.TextField
|
||||
import javafx.scene.image.ImageView
|
||||
import javafx.scene.input.MouseButton
|
||||
import javafx.scene.input.MouseEvent
|
||||
import tornadofx.UIComponent
|
||||
import tornadofx.observable
|
||||
|
||||
class SearchField<T>(private val data: ObservableList<T>, filterCriteria: Array<(T, String) -> Boolean>) : UIComponent() {
|
||||
|
||||
override val root: Parent by fxml()
|
||||
private val textField by fxid<TextField>()
|
||||
private val clearButton by fxid<ImageView>()
|
||||
|
||||
// Currently this method apply each filter to the collection and return the collection with most matches.
|
||||
// TODO : Allow user to chose if there are matches in multiple category.
|
||||
val filteredData = ChosenList(textField.textProperty().map { text ->
|
||||
if (text.isBlank()) data else filterCriteria.map { criterion ->
|
||||
data.filter({ state: T -> criterion(state, text) }.lift())
|
||||
}.maxBy { it.size } ?: emptyList<T>().observable()
|
||||
})
|
||||
|
||||
init {
|
||||
clearButton.setOnMouseClicked { event: MouseEvent ->
|
||||
if (event.button == MouseButton.PRIMARY) {
|
||||
textField.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package com.r3corda.explorer.views
|
||||
|
||||
import com.r3corda.client.fxutils.map
|
||||
import com.r3corda.client.model.writableValue
|
||||
import com.r3corda.explorer.model.SelectedView
|
||||
import com.r3corda.explorer.model.TopLevelModel
|
||||
import javafx.beans.value.WritableValue
|
||||
import javafx.geometry.Pos
|
||||
import javafx.scene.control.ContentDisplay
|
||||
import javafx.scene.input.MouseButton
|
||||
import javafx.scene.layout.VBox
|
||||
import javafx.scene.text.Font
|
||||
import javafx.scene.text.TextAlignment
|
||||
import tornadofx.View
|
||||
import tornadofx.button
|
||||
import tornadofx.imageview
|
||||
|
||||
class Sidebar : View() {
|
||||
override val root: VBox by fxml()
|
||||
private val selectedView: WritableValue<SelectedView> by writableValue(TopLevelModel::selectedView)
|
||||
|
||||
init {
|
||||
// TODO: Obtain views from ViewModel.
|
||||
arrayOf(SelectedView.Home, SelectedView.Cash, SelectedView.Transaction, SelectedView.NewTransaction, SelectedView.Network, SelectedView.Setting).forEach { view ->
|
||||
root.apply {
|
||||
button(view.displayableName) {
|
||||
graphic = imageview {
|
||||
image = view.image
|
||||
// TODO : Use CSS instead.
|
||||
fitWidth = 35.0
|
||||
fitHeight = 35.0
|
||||
}
|
||||
styleClass.add("sidebar-menu-item")
|
||||
setOnMouseClicked { e ->
|
||||
if (e.button == MouseButton.PRIMARY) {
|
||||
selectedView.value = view
|
||||
}
|
||||
}
|
||||
// Transform to smaller icon layout when sidebar width is below 150.
|
||||
val smallIconProperty = widthProperty().map { (it.toDouble() < 150) }
|
||||
|
||||
contentDisplayProperty().bind(smallIconProperty.map { if (it) ContentDisplay.TOP else ContentDisplay.LEFT })
|
||||
textAlignmentProperty().bind(smallIconProperty.map { if (it) TextAlignment.CENTER else TextAlignment.LEFT })
|
||||
alignmentProperty().bind(smallIconProperty.map { if (it) Pos.CENTER else Pos.CENTER_LEFT })
|
||||
fontProperty().bind(smallIconProperty.map { if (it) Font.font(9.0) else Font.font(13.0) })
|
||||
wrapTextProperty().bind(smallIconProperty)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +1,29 @@
|
||||
package com.r3corda.explorer.views
|
||||
|
||||
import com.r3corda.client.fxutils.map
|
||||
import com.r3corda.client.model.objectProperty
|
||||
import com.r3corda.explorer.model.SelectedView
|
||||
import com.r3corda.explorer.model.TopLevelModel
|
||||
import javafx.beans.property.ObjectProperty
|
||||
import javafx.scene.input.KeyCode
|
||||
import javafx.scene.input.KeyEvent
|
||||
import javafx.geometry.Pos
|
||||
import javafx.scene.Parent
|
||||
import javafx.scene.layout.BorderPane
|
||||
import javafx.scene.layout.GridPane
|
||||
import javafx.scene.layout.Pane
|
||||
import javafx.scene.layout.Priority
|
||||
import javafx.scene.layout.VBox
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
import javafx.scene.text.TextAlignment
|
||||
import tornadofx.View
|
||||
import tornadofx.add
|
||||
import tornadofx.gridpane
|
||||
import tornadofx.label
|
||||
|
||||
class TopLevel : View() {
|
||||
override val root: VBox by fxml()
|
||||
override val root: Parent by fxml()
|
||||
val selectionBorderPane: BorderPane by fxid()
|
||||
val sidebarPane: Pane by fxid()
|
||||
|
||||
private val header: Header by inject()
|
||||
private val sidebar: Sidebar by inject()
|
||||
private val home: Home by inject()
|
||||
private val cash: CashViewer by inject()
|
||||
private val transaction: TransactionViewer by inject()
|
||||
@ -29,24 +36,28 @@ class TopLevel : View() {
|
||||
private val transactionRoot = transaction.root
|
||||
private val newTransactionRoot = newTransaction.root
|
||||
|
||||
private fun getView(selection: SelectedView) = when (selection) {
|
||||
SelectedView.Home -> homeRoot
|
||||
SelectedView.Cash -> cashRoot
|
||||
SelectedView.Transaction -> transactionRoot
|
||||
SelectedView.NewTransaction -> newTransactionRoot
|
||||
}
|
||||
val selectedView: ObjectProperty<SelectedView> by objectProperty(TopLevelModel::selectedView)
|
||||
|
||||
init {
|
||||
VBox.setVgrow(selectionBorderPane, Priority.ALWAYS)
|
||||
selectionBorderPane.centerProperty().bind(EasyBind.map(selectedView) { getView(it) })
|
||||
|
||||
primaryStage.addEventHandler(KeyEvent.KEY_RELEASED) { keyEvent ->
|
||||
if (keyEvent.code == KeyCode.ESCAPE) {
|
||||
selectedView.value = SelectedView.Home
|
||||
selectionBorderPane.centerProperty().bind(selectedView.map {
|
||||
when (it) {
|
||||
SelectedView.Home -> homeRoot
|
||||
SelectedView.Cash -> cashRoot
|
||||
SelectedView.Transaction -> transactionRoot
|
||||
SelectedView.NewTransaction -> newTransactionRoot
|
||||
else -> gridpane {
|
||||
label("Under Construction...") {
|
||||
maxWidth = Double.MAX_VALUE
|
||||
textAlignment = TextAlignment.CENTER
|
||||
alignment = Pos.CENTER
|
||||
GridPane.setVgrow(this, Priority.ALWAYS)
|
||||
GridPane.setHgrow(this, Priority.ALWAYS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root.children.add(0, header.root)
|
||||
})
|
||||
selectionBorderPane.center.styleClass.add("no-padding")
|
||||
sidebarPane.add(sidebar.root)
|
||||
selectionBorderPane.top = header.root
|
||||
}
|
||||
}
|
@ -4,82 +4,43 @@ import com.r3corda.client.fxutils.*
|
||||
import com.r3corda.client.model.*
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.crypto.toStringShort
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.protocols.StateMachineRunId
|
||||
import com.r3corda.explorer.AmountDiff
|
||||
import com.r3corda.explorer.formatters.AmountFormatter
|
||||
import com.r3corda.explorer.formatters.Formatter
|
||||
import com.r3corda.explorer.formatters.NumberFormatter
|
||||
import com.r3corda.explorer.model.IdentityModel
|
||||
import com.r3corda.explorer.identicon.identicon
|
||||
import com.r3corda.explorer.identicon.identiconToolTip
|
||||
import com.r3corda.explorer.model.ReportingCurrencyModel
|
||||
import com.r3corda.explorer.sign
|
||||
import com.r3corda.explorer.ui.*
|
||||
import com.r3corda.explorer.ui.setCustomCellFactory
|
||||
import javafx.beans.binding.Bindings
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.geometry.Insets
|
||||
import javafx.scene.Node
|
||||
import javafx.scene.control.*
|
||||
import javafx.scene.Parent
|
||||
import javafx.scene.control.Label
|
||||
import javafx.scene.control.ListView
|
||||
import javafx.scene.control.TableView
|
||||
import javafx.scene.control.TitledPane
|
||||
import javafx.scene.layout.Background
|
||||
import javafx.scene.layout.BackgroundFill
|
||||
import javafx.scene.layout.BorderPane
|
||||
import javafx.scene.layout.CornerRadii
|
||||
import javafx.scene.layout.VBox
|
||||
import javafx.scene.paint.Color
|
||||
import tornadofx.View
|
||||
import tornadofx.*
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
class TransactionViewer: View() {
|
||||
override val root: VBox by fxml()
|
||||
|
||||
val topSplitPane: SplitPane by fxid()
|
||||
|
||||
// Top half (transactions table)
|
||||
private val transactionViewTable: TableView<ViewerNode> by fxid()
|
||||
private val transactionViewTransactionId: TableColumn<ViewerNode, String> by fxid()
|
||||
private val transactionViewStateMachineId: TableColumn<ViewerNode, String> by fxid()
|
||||
private val transactionViewClientUuid: TableColumn<ViewerNode, String> by fxid()
|
||||
private val transactionViewTransactionStatus: TableColumn<ViewerNode, TransactionCreateStatus?> by fxid()
|
||||
private val transactionViewProtocolStatus: TableColumn<ViewerNode, String> by fxid()
|
||||
private val transactionViewStateMachineStatus: TableColumn<ViewerNode, StateMachineStatus?> by fxid()
|
||||
private val transactionViewCommandTypes: TableColumn<ViewerNode, String> by fxid()
|
||||
private val transactionViewTotalValueEquiv: TableColumn<ViewerNode, AmountDiff<Currency>> by fxid()
|
||||
|
||||
// Bottom half (details)
|
||||
private val contractStatesTitledPane: TitledPane by fxid()
|
||||
|
||||
private val contractStatesInputsCountLabel: Label by fxid()
|
||||
private val contractStatesInputStatesTable: TableView<StateNode> by fxid()
|
||||
private val contractStatesInputStatesId: TableColumn<StateNode, String> by fxid()
|
||||
private val contractStatesInputStatesType: TableColumn<StateNode, String> by fxid()
|
||||
private val contractStatesInputStatesOwner: TableColumn<StateNode, String> by fxid()
|
||||
private val contractStatesInputStatesLocalCurrency: TableColumn<StateNode, Currency?> by fxid()
|
||||
private val contractStatesInputStatesAmount: TableColumn<StateNode, Long?> by fxid()
|
||||
private val contractStatesInputStatesEquiv: TableColumn<StateNode, Amount<Currency>?> by fxid()
|
||||
|
||||
private val contractStatesOutputsCountLabel: Label by fxid()
|
||||
private val contractStatesOutputStatesTable: TableView<StateNode> by fxid()
|
||||
private val contractStatesOutputStatesId: TableColumn<StateNode, String> by fxid()
|
||||
private val contractStatesOutputStatesType: TableColumn<StateNode, String> by fxid()
|
||||
private val contractStatesOutputStatesOwner: TableColumn<StateNode, String> by fxid()
|
||||
private val contractStatesOutputStatesLocalCurrency: TableColumn<StateNode, Currency?> by fxid()
|
||||
private val contractStatesOutputStatesAmount: TableColumn<StateNode, Long?> by fxid()
|
||||
private val contractStatesOutputStatesEquiv: TableColumn<StateNode, Amount<Currency>?> by fxid()
|
||||
|
||||
private val signaturesTitledPane: TitledPane by fxid()
|
||||
private val signaturesList: ListView<PublicKey> by fxid()
|
||||
|
||||
private val matchingTransactionsLabel: Label by fxid()
|
||||
|
||||
class TransactionViewer : View() {
|
||||
override val root by fxml<BorderPane>()
|
||||
private val transactionViewTable by fxid<TableView<ViewerNode>>()
|
||||
private val matchingTransactionsLabel by fxid<Label>()
|
||||
// Inject data
|
||||
private val gatheredTransactionDataList: ObservableList<out GatheredTransactionData>
|
||||
by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
|
||||
private val reportingExchange: ObservableValue<Pair<Currency, (Amount<Currency>) -> Amount<Currency>>>
|
||||
by observableValue(ReportingCurrencyModel::reportingExchange)
|
||||
private val myIdentity: ObservableValue<Party?> by observableValue(IdentityModel::myIdentity)
|
||||
private val gatheredTransactionDataList by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
|
||||
private val reportingExchange by observableValue(ReportingCurrencyModel::reportingExchange)
|
||||
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
|
||||
|
||||
/**
|
||||
* This is what holds data for a single transaction node. Note how a lot of these are nullable as we often simply don't
|
||||
@ -92,7 +53,7 @@ class TransactionViewer: View() {
|
||||
val stateMachineStatus: ObservableValue<out StateMachineStatus?>,
|
||||
val protocolStatus: ObservableValue<out ProtocolStatus?>,
|
||||
val commandTypes: Collection<Class<CommandData>>,
|
||||
val totalValueEquiv: ObservableValue<AmountDiff<Currency>?>
|
||||
val totalValueEquiv: ObservableValue<AmountDiff<Currency>>
|
||||
)
|
||||
|
||||
/**
|
||||
@ -121,282 +82,148 @@ class TransactionViewer: View() {
|
||||
stateMachineStatus = stateMachineProperty { it.stateMachineStatus },
|
||||
commandTypes = it.transaction.transaction.tx.commands.map { it.value.javaClass },
|
||||
totalValueEquiv = {
|
||||
val resolvedInputs = it.transaction.inputs.sequence().map { resolution ->
|
||||
when (resolution) {
|
||||
is PartiallyResolvedTransaction.InputResolution.Unresolved -> null
|
||||
is PartiallyResolvedTransaction.InputResolution.Resolved -> resolution.stateAndRef
|
||||
}
|
||||
}.fold(listOf()) { inputs: List<StateAndRef<ContractState>>?, state: StateAndRef<ContractState>? ->
|
||||
if (inputs != null && state != null) {
|
||||
inputs + state
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
val resolvedInputs = it.transaction.inputs.sequence()
|
||||
.map { (it as? PartiallyResolvedTransaction.InputResolution.Resolved)?.stateAndRef?.state }
|
||||
.filterNotNull().toList().lift()
|
||||
|
||||
::calculateTotalEquiv.lift(
|
||||
myIdentity,
|
||||
reportingExchange,
|
||||
resolvedInputs.lift(),
|
||||
resolvedInputs,
|
||||
it.transaction.transaction.tx.outputs.lift()
|
||||
)
|
||||
}()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The detail panes are only filled out if a transaction is selected
|
||||
*/
|
||||
private val selectedViewerNode = transactionViewTable.singleRowSelection()
|
||||
private val selectedTransaction = selectedViewerNode.map {
|
||||
when (it) {
|
||||
is SingleRowSelection.None -> null
|
||||
is SingleRowSelection.Selected -> it.node.transaction
|
||||
}
|
||||
}
|
||||
|
||||
private val inputStateNodes = ChosenList(selectedTransaction.map { transaction ->
|
||||
if (transaction == null) {
|
||||
FXCollections.emptyObservableList<StateNode>()
|
||||
} else {
|
||||
FXCollections.observableArrayList(transaction.inputs.map { inputResolution ->
|
||||
StateNode(inputResolution, inputResolution.value.stateRef)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
private val outputStateNodes = ChosenList(selectedTransaction.map {
|
||||
if (it == null) {
|
||||
FXCollections.emptyObservableList<StateNode>()
|
||||
} else {
|
||||
FXCollections.observableArrayList(it.transaction.tx.outputs.mapIndexed { index, transactionState ->
|
||||
val stateRef = StateRef(it.id, index)
|
||||
StateNode(PartiallyResolvedTransaction.InputResolution.Resolved(StateAndRef(transactionState, stateRef)).lift(), stateRef)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
private val signatures = ChosenList(selectedTransaction.map {
|
||||
if (it == null) {
|
||||
FXCollections.emptyObservableList<PublicKey>()
|
||||
} else {
|
||||
FXCollections.observableArrayList(it.transaction.sigs.map { it.by })
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* We only display the detail panes if there is a node selected.
|
||||
*/
|
||||
private val allNodesShown = FXCollections.observableArrayList<Node>(
|
||||
transactionViewTable,
|
||||
contractStatesTitledPane,
|
||||
signaturesTitledPane
|
||||
)
|
||||
private val onlyTransactionsTableShown = FXCollections.observableArrayList<Node>(
|
||||
transactionViewTable
|
||||
)
|
||||
private val topSplitPaneNodesShown = ChosenList(
|
||||
selectedViewerNode.map { selection ->
|
||||
if (selection is SingleRowSelection.None<*>) {
|
||||
onlyTransactionsTableShown
|
||||
} else {
|
||||
allNodesShown
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Both input and output state tables look the same, so we each up with [wireUpStatesTable]
|
||||
*/
|
||||
private fun wireUpStatesTable(
|
||||
states: ObservableList<StateNode>,
|
||||
statesCountLabel: Label,
|
||||
statesTable: TableView<StateNode>,
|
||||
statesId: TableColumn<StateNode, String>,
|
||||
statesType: TableColumn<StateNode, String>,
|
||||
statesOwner: TableColumn<StateNode, String>,
|
||||
statesLocalCurrency: TableColumn<StateNode, Currency?>,
|
||||
statesAmount: TableColumn<StateNode, Long?>,
|
||||
statesEquiv: TableColumn<StateNode, Amount<Currency>?>
|
||||
) {
|
||||
statesCountLabel.textProperty().bind(Bindings.size(states).map { "$it" })
|
||||
|
||||
Bindings.bindContent(statesTable.items, states)
|
||||
|
||||
val unknownString = "???"
|
||||
|
||||
statesId.setCellValueFactory { it.value.stateRef.toString().lift() }
|
||||
statesType.setCellValueFactory {
|
||||
resolvedOrDefault(it.value.state, unknownString) {
|
||||
it.state.data.javaClass.toString()
|
||||
}
|
||||
}
|
||||
statesOwner.setCellValueFactory {
|
||||
resolvedOrDefault(it.value.state, unknownString) {
|
||||
val contractState = it.state.data
|
||||
if (contractState is OwnableState) {
|
||||
contractState.owner.toStringShort()
|
||||
} else {
|
||||
unknownString
|
||||
}
|
||||
}
|
||||
}
|
||||
statesLocalCurrency.setCellValueFactory {
|
||||
resolvedOrDefault<Currency?>(it.value.state, null) {
|
||||
val contractState = it.state.data
|
||||
if (contractState is Cash.State) {
|
||||
contractState.amount.token.product
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
statesAmount.setCellValueFactory {
|
||||
resolvedOrDefault<Long?>(it.value.state, null) {
|
||||
val contractState = it.state.data
|
||||
if (contractState is Cash.State) {
|
||||
contractState.amount.quantity
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
statesAmount.cellFactory = NumberFormatter.boringLong.toTableCellFactory()
|
||||
statesEquiv.setCellValueFactory {
|
||||
resolvedOrDefault<ObservableValue<Amount<Currency>?>>(it.value.state, null.lift()) {
|
||||
val contractState = it.state.data
|
||||
if (contractState is Cash.State) {
|
||||
reportingExchange.map { exchange ->
|
||||
exchange.second(contractState.amount.withoutIssuer())
|
||||
}
|
||||
} else {
|
||||
null.lift()
|
||||
}
|
||||
}.bind { it }
|
||||
|
||||
}
|
||||
statesEquiv.cellFactory = AmountFormatter.boring.toTableCellFactory()
|
||||
}
|
||||
|
||||
init {
|
||||
Bindings.bindContent(topSplitPane.items, topSplitPaneNodesShown)
|
||||
|
||||
val searchField = SearchField(viewerNodes, arrayOf({ viewerNode, s -> viewerNode.commandTypes.any { it.simpleName.contains(s, true) } }))
|
||||
root.top = searchField.root
|
||||
// Transaction table
|
||||
Bindings.bindContent(transactionViewTable.items, viewerNodes)
|
||||
|
||||
transactionViewTable.setColumnPrefWidthPolicy { tableWidthWithoutPaddingAndBorder, column ->
|
||||
Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / transactionViewTable.columns.size).toInt()
|
||||
}
|
||||
|
||||
transactionViewTransactionId.setCellValueFactory { "${it.value.transactionId}".lift() }
|
||||
transactionViewStateMachineId.setCellValueFactory { it.value.stateMachineRunId.map { "${it?.uuid ?: ""}" } }
|
||||
transactionViewProtocolStatus.setCellValueFactory { it.value.protocolStatus.map { "${it ?: ""}" } }
|
||||
transactionViewTransactionStatus.setCustomCellFactory {
|
||||
val label = Label()
|
||||
val backgroundFill = when (it) {
|
||||
is TransactionCreateStatus.Started -> BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)
|
||||
is TransactionCreateStatus.Failed -> BackgroundFill(Color.SALMON, CornerRadii.EMPTY, Insets.EMPTY)
|
||||
null -> BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)
|
||||
transactionViewTable.apply {
|
||||
items = searchField.filteredData
|
||||
column("Transaction ID", ViewerNode::transactionId).setCustomCellFactory {
|
||||
label("$it".substring(0, 16) + "...") {
|
||||
graphic = imageview {
|
||||
image = identicon(it, 5.0)
|
||||
}
|
||||
tooltip = identiconToolTip(it)
|
||||
}
|
||||
}
|
||||
label.background = Background(backgroundFill)
|
||||
label.text = "$it"
|
||||
label
|
||||
}
|
||||
transactionViewStateMachineStatus.setCellValueFactory { it.value.stateMachineStatus.map { it } }
|
||||
transactionViewStateMachineStatus.setCustomCellFactory {
|
||||
val label = Label()
|
||||
val backgroundFill = when (it) {
|
||||
is StateMachineStatus.Added -> BackgroundFill(Color.LIGHTYELLOW, CornerRadii.EMPTY, Insets.EMPTY)
|
||||
is StateMachineStatus.Removed -> BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)
|
||||
null -> BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)
|
||||
column("State Machine ID", ViewerNode::stateMachineRunId).cellFormat { text = "${it?.uuid ?: ""}" }
|
||||
column("Protocol status", ViewerNode::protocolStatus).cellFormat { text = "${it.value ?: ""}" }
|
||||
column("SM Status", ViewerNode::stateMachineStatus).cellFormat { text = "${it.value ?: ""}" }
|
||||
column("Command type(s)", ViewerNode::commandTypes).cellFormat { text = it.map { it.simpleName }.joinToString(",") }
|
||||
column("Total value (USD equiv)", ViewerNode::totalValueEquiv)
|
||||
.cellFormat { text = "${it.positivity.sign}${AmountFormatter.boring.format(it.amount)}" }
|
||||
rowExpander(true) {
|
||||
add(ContractStatesView(it.transaction).root)
|
||||
background = Background(BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY))
|
||||
prefHeight = 400.0
|
||||
}.apply {
|
||||
// Hide the expander column.
|
||||
isVisible = false
|
||||
prefWidth = 0.0
|
||||
}
|
||||
label.background = Background(backgroundFill)
|
||||
label.text = "$it"
|
||||
label
|
||||
}
|
||||
|
||||
transactionViewCommandTypes.setCellValueFactory {
|
||||
it.value.commandTypes.map { it.simpleName }.joinToString(",").lift()
|
||||
}
|
||||
transactionViewTotalValueEquiv.setCellValueFactory<ViewerNode, AmountDiff<Currency>> { it.value.totalValueEquiv }
|
||||
transactionViewTotalValueEquiv.cellFactory = object : Formatter<AmountDiff<Currency>> {
|
||||
override fun format(value: AmountDiff<Currency>) =
|
||||
"${value.positivity.sign}${AmountFormatter.boring.format(value.amount)}"
|
||||
}.toTableCellFactory()
|
||||
|
||||
// Contract states
|
||||
wireUpStatesTable(
|
||||
inputStateNodes,
|
||||
contractStatesInputsCountLabel,
|
||||
contractStatesInputStatesTable,
|
||||
contractStatesInputStatesId,
|
||||
contractStatesInputStatesType,
|
||||
contractStatesInputStatesOwner,
|
||||
contractStatesInputStatesLocalCurrency,
|
||||
contractStatesInputStatesAmount,
|
||||
contractStatesInputStatesEquiv
|
||||
)
|
||||
wireUpStatesTable(
|
||||
outputStateNodes,
|
||||
contractStatesOutputsCountLabel,
|
||||
contractStatesOutputStatesTable,
|
||||
contractStatesOutputStatesId,
|
||||
contractStatesOutputStatesType,
|
||||
contractStatesOutputStatesOwner,
|
||||
contractStatesOutputStatesLocalCurrency,
|
||||
contractStatesOutputStatesAmount,
|
||||
contractStatesOutputStatesEquiv
|
||||
)
|
||||
|
||||
// Signatures
|
||||
Bindings.bindContent(signaturesList.items, signatures)
|
||||
signaturesList.cellFactory = object : Formatter<PublicKey> {
|
||||
override fun format(value: PublicKey) = value.toStringShort()
|
||||
}.toListCellFactory()
|
||||
|
||||
matchingTransactionsLabel.textProperty().bind(Bindings.size(viewerNodes).map {
|
||||
matchingTransactionsLabel.textProperty().bind(Bindings.size(transactionViewTable.items).map {
|
||||
"$it matching transaction${if (it == 1) "" else "s"}"
|
||||
})
|
||||
}
|
||||
|
||||
private class ContractStatesView(val transaction: PartiallyResolvedTransaction) : View() {
|
||||
override val root: Parent by fxml()
|
||||
private val inputs: ListView<StateNode> by fxid()
|
||||
private val outputs: ListView<StateNode> by fxid()
|
||||
private val signatures: ListView<PublicKey> by fxid()
|
||||
private val inputPane: TitledPane by fxid()
|
||||
private val outputPane: TitledPane by fxid()
|
||||
private val signaturesPane: TitledPane by fxid()
|
||||
|
||||
init {
|
||||
val inputStates = transaction.inputs.map { StateNode(it, it.value.stateRef) }
|
||||
val outputStates = transaction.transaction.tx.outputs.mapIndexed { index, transactionState ->
|
||||
val stateRef = StateRef(transaction.id, index)
|
||||
StateNode(PartiallyResolvedTransaction.InputResolution.Resolved(StateAndRef(transactionState, stateRef)).lift(), stateRef)
|
||||
}
|
||||
|
||||
val signatureData = transaction.transaction.sigs.map { it.by }
|
||||
// Bind count to TitlePane
|
||||
inputPane.textProperty().bind(inputStates.lift().map { "Input (${it.count()})" })
|
||||
outputPane.textProperty().bind(outputStates.lift().map { "Output (${it.count()})" })
|
||||
signaturesPane.textProperty().bind(signatureData.lift().map { "Signatures (${it.count()})" })
|
||||
|
||||
val cellFactory = { node: StateNode ->
|
||||
(node.state.value as? PartiallyResolvedTransaction.InputResolution.Resolved)?.run {
|
||||
val data = stateAndRef.state.data
|
||||
form {
|
||||
label("${data.contract.javaClass.simpleName} (${stateAndRef.ref.toString().substring(0, 16)}...)[${stateAndRef.ref.index}]") {
|
||||
graphic = imageview {
|
||||
image = identicon(stateAndRef.ref.txhash, 10.0)
|
||||
}
|
||||
tooltip = identiconToolTip(stateAndRef.ref.txhash)
|
||||
}
|
||||
when (data) {
|
||||
is Cash.State -> form {
|
||||
fieldset {
|
||||
field("Amount :") {
|
||||
label(AmountFormatter.boring.format(data.amount.withoutIssuer()))
|
||||
}
|
||||
field("Issuer :") {
|
||||
label("${data.amount.token.issuer}") {
|
||||
tooltip(data.amount.token.issuer.party.owningKey.toStringShort())
|
||||
}
|
||||
}
|
||||
field("Owner :") {
|
||||
val owner = data.owner
|
||||
val nodeInfo = Models.get<NetworkIdentityModel>(TransactionViewer::class).lookup(owner)
|
||||
label(nodeInfo?.legalIdentity?.name ?: "???") {
|
||||
tooltip(data.owner.toStringShort())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO : Generic view using reflection?
|
||||
else -> label {}
|
||||
}
|
||||
}
|
||||
} ?: label { text = "???" }
|
||||
}
|
||||
|
||||
inputs.setCustomCellFactory(cellFactory)
|
||||
outputs.setCustomCellFactory(cellFactory)
|
||||
|
||||
inputs.items = FXCollections.observableList(inputStates)
|
||||
outputs.items = FXCollections.observableList(outputStates)
|
||||
signatures.items = FXCollections.observableList(signatureData)
|
||||
|
||||
signatures.apply {
|
||||
cellFormat { key ->
|
||||
val nodeInfo = Models.get<NetworkIdentityModel>(TransactionViewer::class).lookup(key)
|
||||
text = "${key.toStringShort()} (${nodeInfo?.legalIdentity?.name ?: "???"})"
|
||||
}
|
||||
prefHeight = 185.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We calculate the total value by subtracting relevant input states and adding relevant output states, as long as they're cash
|
||||
*/
|
||||
private fun calculateTotalEquiv(
|
||||
identity: Party?,
|
||||
reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>,
|
||||
inputs: List<StateAndRef<ContractState>>?,
|
||||
outputs: List<TransactionState<ContractState>>): AmountDiff<Currency>? {
|
||||
if (inputs == null) {
|
||||
return null
|
||||
}
|
||||
var sum = 0L
|
||||
private fun calculateTotalEquiv(identity: NodeInfo?,
|
||||
reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>,
|
||||
inputs: List<TransactionState<ContractState>>,
|
||||
outputs: List<TransactionState<ContractState>>): AmountDiff<Currency> {
|
||||
val (reportingCurrency, exchange) = reportingCurrencyExchange
|
||||
val publicKey = identity?.owningKey
|
||||
inputs.forEach {
|
||||
val contractState = it.state.data
|
||||
if (contractState is Cash.State && publicKey == contractState.owner) {
|
||||
sum -= exchange(contractState.amount.withoutIssuer()).quantity
|
||||
}
|
||||
val publicKey = identity?.legalIdentity?.owningKey
|
||||
fun List<TransactionState<ContractState>>.sum(): Long {
|
||||
return this.map { it.data as? Cash.State }
|
||||
.filterNotNull()
|
||||
.filter { publicKey == it.owner }
|
||||
.map { exchange(it.amount.withoutIssuer()).quantity }
|
||||
.sum()
|
||||
}
|
||||
outputs.forEach {
|
||||
val contractState = it.data
|
||||
if (contractState is Cash.State && publicKey == contractState.owner) {
|
||||
sum += exchange(contractState.amount.withoutIssuer()).quantity
|
||||
}
|
||||
}
|
||||
return AmountDiff.fromLong(sum, reportingCurrency)
|
||||
}
|
||||
|
||||
fun <A> resolvedOrDefault(
|
||||
state: ObservableValue<PartiallyResolvedTransaction.InputResolution>,
|
||||
default: A,
|
||||
resolved: (StateAndRef<*>) -> A
|
||||
): ObservableValue<A> {
|
||||
return state.map {
|
||||
when (it) {
|
||||
is PartiallyResolvedTransaction.InputResolution.Unresolved -> default
|
||||
is PartiallyResolvedTransaction.InputResolution.Resolved -> resolved(it.stateAndRef)
|
||||
}
|
||||
}
|
||||
}
|
||||
return AmountDiff.fromLong(outputs.sum() - inputs.sum(), reportingCurrency)
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
#topLevel.root {
|
||||
-fx-background-image:url('../images/r3bg.png');
|
||||
-fx-background-image: url('../images/r3bg.png');
|
||||
-fx-background-size: cover;
|
||||
-fx-background-repeat:no-repeat;
|
||||
-fx-base:white;
|
||||
-fx-background-repeat: no-repeat;
|
||||
-fx-base: white;
|
||||
}
|
||||
|
||||
#cashViewer {
|
||||
@ -17,59 +17,52 @@
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.root {
|
||||
-fx-padding:5px;
|
||||
}
|
||||
|
||||
.root {
|
||||
-fx-padding:5px;
|
||||
.expand-row {
|
||||
-fx-padding: 10;
|
||||
}
|
||||
|
||||
.dialog-pane {
|
||||
-fx-background-color:rgba(255,255,255,0.7);
|
||||
-fx-background-radius:2px;
|
||||
-fx-background-color: rgba(255, 255, 255, 0.7);
|
||||
-fx-background-radius: 2px;
|
||||
-fx-border-radius: 2px;
|
||||
-fx-border-color: rgb(20,136,204);
|
||||
-fx-border-color: rgb(20, 136, 204);
|
||||
}
|
||||
|
||||
|
||||
.nested-column-header, .nested-column-header {
|
||||
-fx-background-color:transparent;
|
||||
-fx-wrap-text:true;
|
||||
-fx-border-color:transparent;
|
||||
-fx-background-color: transparent;
|
||||
-fx-wrap-text: true;
|
||||
-fx-border-color: transparent;
|
||||
}
|
||||
|
||||
|
||||
.text-field,
|
||||
.table-column,
|
||||
.table-column,
|
||||
.label,
|
||||
.title,
|
||||
.combo-box,
|
||||
.title,
|
||||
.combo-box,
|
||||
.button,
|
||||
.split-menu-button,
|
||||
.choice-box {
|
||||
-fx-font-family:Effra;
|
||||
-fx-font-size:1em;
|
||||
-fx-text-fill:rgb(63,63,63);
|
||||
-fx-font-family: Effra;
|
||||
-fx-font-size: 1em;
|
||||
-fx-text-fill: rgb(63, 63, 63);
|
||||
-fx-font-smoothing-type: gray;
|
||||
}
|
||||
|
||||
.text-highlight {
|
||||
-fx-text-fill:rgb(20,136,204);
|
||||
-fx-text-fill: rgb(20, 136, 204);
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
-fx-background-color:rgba(255,255,255,0.9);
|
||||
-fx-background-color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.titled-pane .content,
|
||||
.split-menu-button .label,
|
||||
.split-menu-button .arrow-button,
|
||||
.titled-pane .split-pane, .scroll-pane {
|
||||
-fx-background-color:transparent;
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
|
||||
.text-field,
|
||||
.tree-table-view,
|
||||
.table-view,
|
||||
@ -80,8 +73,8 @@
|
||||
.split-menu-button,
|
||||
.choice-box,
|
||||
.titled-pane .title {
|
||||
-fx-border-color:rgb(150,150,150);
|
||||
-fx-border-width:1px;
|
||||
-fx-border-color: rgb(150, 150, 150);
|
||||
-fx-border-width: 1px;
|
||||
|
||||
}
|
||||
|
||||
@ -98,82 +91,116 @@
|
||||
.split-menu-button:hover,
|
||||
.choice-box:hover,
|
||||
.titled-pane:hover .title {
|
||||
-fx-border-color:rgb(20,136,204);
|
||||
-fx-border-color: rgb(20, 136, 204);
|
||||
}
|
||||
|
||||
.split-menu-button:pressed,
|
||||
.button:pressed,
|
||||
.choice-box:pressed,
|
||||
.titled-pane:expanded .title {
|
||||
-fx-background-color:rgb(20,136,204);
|
||||
-fx-text-fill:white;
|
||||
-fx-background-color: rgb(20, 136, 204);
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
|
||||
.chart {
|
||||
-fx-padding: 0;
|
||||
}
|
||||
|
||||
.chart-series-line {
|
||||
-fx-stroke-width: 2px;
|
||||
-fx-effect: null;
|
||||
}
|
||||
|
||||
.default-color0.chart-series-line {
|
||||
-fx-stroke: rgb(20, 136, 204);
|
||||
}
|
||||
|
||||
.chart-plot-background {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.chart-vertical-grid-lines {
|
||||
-fx-stroke: transparent;
|
||||
}
|
||||
|
||||
.chart-horizontal-grid-lines {
|
||||
-fx-stroke: transparent;
|
||||
}
|
||||
|
||||
.chart-alternative-row-fill {
|
||||
-fx-fill: transparent;
|
||||
-fx-stroke: transparent;
|
||||
-fx-stroke-width: 0;
|
||||
}
|
||||
|
||||
.titled-pane:expanded .title .text {
|
||||
-fx-fill:white;
|
||||
-fx-fill: white;
|
||||
}
|
||||
.titled-pane .title,.titled-pane .title:hover {
|
||||
-fx-border-width:0.5px;
|
||||
|
||||
.titled-pane .title, .titled-pane .title:hover {
|
||||
-fx-border-width: 0.5px;
|
||||
}
|
||||
|
||||
.text-field, .combo-box, .choice-box, .password-field {
|
||||
-fx-background-color:rgba(255,255,255,0.5);
|
||||
-fx-background-radius:2px;
|
||||
-fx-background-color: rgba(255, 255, 255, 0.5);
|
||||
-fx-background-radius: 2px;
|
||||
-fx-border-radius: 2px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
/* switch off highlighting for text-field when it's inside a combo-box */
|
||||
.combo-box .text-field, .combo-box .text-field:hover, .combo-box .text-field:focused {
|
||||
-fx-border-color:transparent;
|
||||
-fx-border-color: transparent;
|
||||
}
|
||||
/* table formatting */
|
||||
|
||||
/* table formatting */
|
||||
|
||||
.column-header-background,
|
||||
.table-column,
|
||||
.tree-table-row-cell, .column-header-background .filler {
|
||||
-fx-background-color:transparent;
|
||||
-fx-label-padding:3px;
|
||||
-fx-background-color: transparent;
|
||||
-fx-label-padding: 3px;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
.nested-column-header .label {
|
||||
-fx-wrap-text: true
|
||||
}
|
||||
|
||||
.nested-column-header .label { -fx-wrap-text:true}
|
||||
.table-column {
|
||||
-fx-border-style: solid;
|
||||
-fx-border-color: rgb(216, 216, 216); /*t r b l */
|
||||
|
||||
.table-column { -fx-border-style:solid;
|
||||
-fx-border-color:rgb(216,216,216); /*t r b l */
|
||||
|
||||
-fx-border-width:0.5px;
|
||||
-fx-border-insets: 1.5px;
|
||||
-fx-border-width: 1px;
|
||||
-fx-border-insets: 1px;
|
||||
-fx-background-insets: 2px;
|
||||
}
|
||||
|
||||
|
||||
.tree-table-row-cell:even .table-column,
|
||||
.table-row-cell:even .table-column,
|
||||
.title, .split-menu-button, .button {
|
||||
-fx-background-color: rgba(20,136,204,0.2);
|
||||
}
|
||||
|
||||
.table-row-cell:selected, .tree-table-row-cell:selected {
|
||||
-fx-background-color:transparent;
|
||||
}
|
||||
.tree-table-row-cell:selected .table-column,
|
||||
.tree-table-row-cell:focused .table-column,
|
||||
.table-row-cell:selected .table-column,
|
||||
.table-row-cell:focused .table-column {
|
||||
-fx-background-color:rgba(20,136,204,0.8);
|
||||
.tree-table-row-cell:even .table-column,
|
||||
.table-row-cell:even .table-column,
|
||||
.title, .split-menu-button, .button {
|
||||
-fx-background-color: rgba(20, 136, 204, 0.2);
|
||||
}
|
||||
|
||||
.table-row-cell:selected, .tree-table-row-cell:selected {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.tree-table-row-cell:selected .table-column,
|
||||
.tree-table-row-cell:focused .table-column,
|
||||
.table-row-cell:selected .table-column,
|
||||
.table-row-cell:focused .table-column {
|
||||
-fx-background-color: rgba(20, 136, 204, 0.8);
|
||||
|
||||
}
|
||||
|
||||
.bad .text {
|
||||
-fx-fill:rgb(236,29,36);
|
||||
-fx-fill: rgb(236, 29, 36);
|
||||
}
|
||||
|
||||
.table-row-cell:focused .table-column .text,
|
||||
.tree-table-row-cell:focused .table-column .text {
|
||||
-fx-fill:white;
|
||||
-fx-fill: white;
|
||||
}
|
||||
|
||||
.table-column:hover,
|
||||
@ -181,220 +208,239 @@
|
||||
.table-row-cell:hover .second-column,
|
||||
.tree-table-row-cell:hover .first-column,
|
||||
.tree-table-row-cell:hover .second-column {
|
||||
-fx-border-color:rgb(20,136,204);
|
||||
-fx-border-color: rgb(20, 136, 204);
|
||||
}
|
||||
|
||||
.tree-table-view .column-header-background .nested-column-header .table-column,
|
||||
.table-view .column-header-background .nested-column-header .table-column {
|
||||
-fx-border-color:transparent;
|
||||
-fx-border-color: transparent;
|
||||
}
|
||||
|
||||
/* Special formatting - columns to be presented with no join between them */
|
||||
|
||||
.first-column {
|
||||
-fx-border-width:0.5px 0px 0.5px 0.5px;
|
||||
-fx-border-width: 0.5px 0px 0.5px 0.5px;
|
||||
-fx-border-insets: 1.5px 0px 1.5px 1.5px;
|
||||
-fx-background-insets: 2px 0px 2px 2px;
|
||||
}
|
||||
|
||||
|
||||
.second-column {
|
||||
-fx-border-width:0.5px 0.5px 0.5px 0px;
|
||||
-fx-border-width: 0.5px 0.5px 0.5px 0px;
|
||||
-fx-border-insets: 1.5px 1.5px 1.5px 0px;
|
||||
-fx-background-insets: 2px 2px 2px 0px;
|
||||
}
|
||||
|
||||
|
||||
/* highlighting where the user has typed a key */
|
||||
.tree-table-view text-area, .table-view text-area{
|
||||
-fx-font-weight:bold;
|
||||
-fx-fill:rgb(20,136,204);
|
||||
.tree-table-view text-area, .table-view text-area {
|
||||
-fx-font-weight: bold;
|
||||
-fx-fill: rgb(20, 136, 204);
|
||||
|
||||
}
|
||||
|
||||
.tree-table-row-cell:selected .table-column text-area,
|
||||
.table-row-cell:selected .table-column text-area
|
||||
{
|
||||
-fx-font-weight:bold;
|
||||
-fx-fill:rgb(255,255,255);
|
||||
.table-row-cell:selected .table-column text-area {
|
||||
-fx-font-weight: bold;
|
||||
-fx-fill: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
|
||||
/* labels */
|
||||
.dialog-pane .header-panel .label .text{
|
||||
-fx-font-size:1em;
|
||||
-fx-fill:rgb(20,136,204);
|
||||
.dialog-pane .header-panel .label .text {
|
||||
-fx-font-size: 1em;
|
||||
-fx-fill: rgb(20, 136, 204);
|
||||
}
|
||||
|
||||
#headline, .headline {
|
||||
-fx-font-size:2.4em;
|
||||
-fx-font-size: 2.4em;
|
||||
}
|
||||
|
||||
#subline, .subline {
|
||||
-fx-font-size:1.4em;
|
||||
-fx-font-size: 1.4em;
|
||||
}
|
||||
|
||||
#headline, #subline {
|
||||
-fx-text-fill:rgb(65,65,65);
|
||||
-fx-padding:0px;
|
||||
-fx-text-fill: rgb(65, 65, 65);
|
||||
-fx-padding: 0px;
|
||||
|
||||
}
|
||||
|
||||
/* search boxes */
|
||||
.search {
|
||||
-fx-background-image:url('../images/search.png');
|
||||
-fx-background-size:Auto 16px;
|
||||
-fx-background-repeat:no-repeat;
|
||||
-fx-background-position:8px center;
|
||||
-fx-padding:5px 5px 5px 30px;
|
||||
-fx-background-image: url('../images/search.png');
|
||||
-fx-background-size: Auto 16px;
|
||||
-fx-background-repeat: no-repeat;
|
||||
-fx-background-position: 8px center;
|
||||
-fx-padding: 5px 5px 5px 30px;
|
||||
-fx-background-radius: 2px;
|
||||
-fx-border-radius: 2px;
|
||||
}
|
||||
|
||||
.search-clear {
|
||||
-fx-image:url('../images/clear_inactive.png');
|
||||
-fx-image: url('../images/clear_inactive.png');
|
||||
-fx-padding: 5px 5px 5px 30px;
|
||||
}
|
||||
|
||||
.search-clear:hover {
|
||||
-fx-image:url('../images/clear.png');
|
||||
-fx-image: url('../images/clear.png');
|
||||
}
|
||||
|
||||
.split-menu-button, .button, .choice-box {
|
||||
-fx-background-radius:2px;
|
||||
-fx-background-radius: 2px;
|
||||
-fx-border-radius: 2px;
|
||||
-fx-border-insets: 0.5px;
|
||||
-fx-background-insets:0.5px;
|
||||
-fx-background-insets: 0.5px;
|
||||
|
||||
}
|
||||
|
||||
.tree-table-row-cell .monetary-value, .monetary-value .label, .table-row-cell .monetary-value {
|
||||
-fx-alignment:center-right;
|
||||
-fx-alignment: center-right;
|
||||
}
|
||||
|
||||
|
||||
/* split panes */
|
||||
.split-pane-divider {
|
||||
-fx-background-color: transparent;
|
||||
-fx-border-color: rgb(160,160,160);
|
||||
-fx-border-width: 0 0 0 0.5px
|
||||
-fx-border-color: rgb(160, 160, 160);
|
||||
-fx-border-width: 0 0 0 0px;
|
||||
-fx-padding: 0 0 0 3;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
-fx-background-color: #c3dbe9;
|
||||
}
|
||||
|
||||
.sidebar-menu-item {
|
||||
-fx-background-color: transparent;
|
||||
-fx-max-width: infinity;
|
||||
-fx-border-width: 0;
|
||||
-fx-padding: 10, 10, 10, 10;
|
||||
-fx-text-fill: white;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.sidebar-menu-item:hover, .sidebar-menu-item:selected {
|
||||
-fx-background-color: #9edde9;
|
||||
}
|
||||
|
||||
.no-padding {
|
||||
-fx-background-insets: 0;
|
||||
-fx-padding: 0;
|
||||
}
|
||||
|
||||
/* Dashboard tiles */
|
||||
|
||||
.tile,.tile-user {
|
||||
-fx-padding: 10px;
|
||||
-fx-pref-height:200px; -fx-pref-width:200px;
|
||||
|
||||
|
||||
.tile, .tile-user {
|
||||
-fx-padding: 10px;
|
||||
-fx-pref-height: 250px;
|
||||
}
|
||||
|
||||
.corda-logo {
|
||||
-fx-font-weight: bolder;
|
||||
-fx-font-size: 1.8em;
|
||||
-fx-padding: 5;
|
||||
-fx-text-fill: rgb(20, 136, 204);
|
||||
-fx-image: url("../images/corda-logo-alpha.png");
|
||||
-fx-max-width: 30;
|
||||
-fx-max-height: 30;
|
||||
}
|
||||
|
||||
.scroll-pane > .viewport {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.tile .title, .tile:expanded .title,
|
||||
.tile-user .title, .tile-user:expanded .title {
|
||||
-fx-alignment:center-right;
|
||||
-fx-font-size:1.4em;
|
||||
-fx-font-weight:bold;
|
||||
-fx-cursor:hand;
|
||||
-fx-background-radius:2px 2px 0 0;
|
||||
-fx-border-radius: 2px 2px 0 0;
|
||||
-fx-border-width:1px 1px 0 1px;
|
||||
-fx-background-color: rgba(255,255,255,0.5);
|
||||
-fx-border-color:rgb(160,160,160); /*t r b l */
|
||||
-fx-alignment: center-left;
|
||||
-fx-font-size: 1.4em;
|
||||
-fx-font-weight: bold;
|
||||
-fx-cursor: hand;
|
||||
-fx-background-color: rgba(183, 210, 228, 0.2);
|
||||
-fx-border-color: transparent; /*t r b l */
|
||||
|
||||
}
|
||||
|
||||
.tile .title .text, .tile:expanded .title .text,
|
||||
.tile-user .title .text, .tile-user:expanded .title .text {
|
||||
-fx-fill:rgb(65,65,65);
|
||||
-fx-fill: rgb(65, 65, 65);
|
||||
}
|
||||
|
||||
.tile .content,
|
||||
.tile-user .content {
|
||||
-fx-background-color: rgba(255,255,255,0.7);
|
||||
-fx-background-size:Auto 90%;
|
||||
-fx-background-repeat:no-repeat;
|
||||
-fx-background-position:center center;
|
||||
-fx-cursor:hand;
|
||||
-fx-background-radius:0 0 2px 2px;
|
||||
-fx-border-radius: 0 0 2px 2px;
|
||||
-fx-padding:0px;
|
||||
-fx-alignment:bottom-right;
|
||||
-fx-border-color:rgb(150,150,150); /*t r b l */
|
||||
-fx-background-color: rgba(183, 210, 228, 0.2);
|
||||
-fx-background-size: Auto 90%;
|
||||
-fx-background-repeat: no-repeat;
|
||||
-fx-background-position: center center;
|
||||
-fx-cursor: hand;
|
||||
-fx-padding: 0px;
|
||||
-fx-alignment: bottom-right;
|
||||
-fx-border-color: transparent; /*t r b l */
|
||||
}
|
||||
|
||||
.tile .label,
|
||||
.tile-user .label {
|
||||
-fx-font-size:2.4em;
|
||||
-fx-padding:20px;
|
||||
-fx-text-fill:rgb(65,65,65);
|
||||
-fx-font-weight:normal;
|
||||
-fx-text-alignment:right;
|
||||
-fx-font-size: 2.4em;
|
||||
-fx-padding: 20px;
|
||||
-fx-text-fill: rgb(65, 65, 65);
|
||||
-fx-font-weight: normal;
|
||||
-fx-text-alignment: right;
|
||||
|
||||
}
|
||||
|
||||
.tile:hover .label,
|
||||
.tile-user:hover .label {
|
||||
-fx-padding:24px;
|
||||
}
|
||||
|
||||
.tile:hover .content, .tile:hover .title,
|
||||
.tile:hover,
|
||||
.tile-user:hover .content, .tile-user:hover .title {
|
||||
-fx-border-color:rgb(20,136,204);
|
||||
-fx-background-color: rgb(20,136,204);
|
||||
|
||||
}
|
||||
.tile:hover, .tile-user:hover {
|
||||
-fx-padding:4px;
|
||||
}
|
||||
|
||||
.tile:hover .label, .tile:hover .label .text, .tile:hover .title .text {
|
||||
-fx-text-fill:rgb(255,255,255);
|
||||
-fx-fill:rgb(255,255,255);
|
||||
-fx-font-weight:bold;
|
||||
-fx-effect:none;
|
||||
-fx-border-color: rgb(20, 136, 204);
|
||||
-fx-border-width: 2;
|
||||
}
|
||||
|
||||
#tile_cash .content {
|
||||
-fx-background-image:url('../images/cash_lrg.png');
|
||||
-fx-background-image: url('../images/cash_lrg.png');
|
||||
}
|
||||
|
||||
#tile_debtors .content {
|
||||
-fx-background-image:url('../images/outflow_lrg.png');
|
||||
-fx-background-image: url('../images/outflow_lrg.png');
|
||||
}
|
||||
|
||||
#tile_creditors .content {
|
||||
-fx-background-image:url('../images/inflow_lrg.png');
|
||||
-fx-background-image: url('../images/inflow_lrg.png');
|
||||
}
|
||||
|
||||
#tile_tx .content {
|
||||
-fx-background-image:url('../images/tx_lrg.png');
|
||||
-fx-background-image: url('../images/tx_lrg.png');
|
||||
}
|
||||
|
||||
#tile_cpty .content {
|
||||
-fx-background-image:url('../images/cpty_lrg.png');
|
||||
-fx-background-image: url('../images/cpty_lrg.png');
|
||||
}
|
||||
|
||||
.tile-user .content {
|
||||
-fx-background-image:url('../images/user_b.png');
|
||||
-fx-background-image: url('../images/user_b.png');
|
||||
}
|
||||
|
||||
.tile-user-test-man .content {
|
||||
-fx-background-image:url('../images/man1.png');
|
||||
-fx-background-size:cover;
|
||||
-fx-background-image: url('../images/man1.png');
|
||||
-fx-background-size: cover;
|
||||
}
|
||||
|
||||
.tile-user-test-woman .content {
|
||||
-fx-background-image:url('../images/woman1.png');
|
||||
-fx-background-size:cover;
|
||||
-fx-background-image: url('../images/woman1.png');
|
||||
-fx-background-size: cover;
|
||||
}
|
||||
|
||||
.tile-user .label {
|
||||
-fx-background-color:rgba(255,255,255,0.7);
|
||||
-fx-background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.tile-user:hover .title, .tile-user:hover .content {
|
||||
-fx-background-color:rgba(255,255,255,0.7);
|
||||
-fx-background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.counterparty {
|
||||
-fx-background-image:url('../images/inst_128.png');
|
||||
-fx-background-size:Auto 16px;
|
||||
-fx-background-image: url('../images/inst_128.png');
|
||||
-fx-background-size: Auto 16px;
|
||||
-fx-background-repeat: no-repeat;
|
||||
-fx-background-position:0px center;
|
||||
-fx-padding:0 0 0 20px;
|
||||
-fx-background-position: 0px center;
|
||||
-fx-padding: 0 0 0 20px;
|
||||
}
|
||||
|
||||
.state-panel{
|
||||
-fx-background-color: rgba(255,255,255,0.7);
|
||||
-fx-border-color:rgb(150,150,150);
|
||||
-fx-insets:5px
|
||||
.state-panel {
|
||||
-fx-background-color: rgba(255, 255, 255, 0.7);
|
||||
-fx-border-color: rgb(150, 150, 150);
|
||||
-fx-insets: 5px
|
||||
}
|
||||
|
@ -1,28 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<GridPane vgap="5" hgap="5" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<Label text="State ID"/>
|
||||
<Label text="Issuer" GridPane.rowIndex="1"/>
|
||||
<Label text="Originated" wrapText="true" GridPane.rowIndex="2"/>
|
||||
<Label text="Amount" wrapText="true" GridPane.rowIndex="3"/>
|
||||
<Label fx:id="equivLabel" text="USD" wrapText="true" GridPane.rowIndex="4"/>
|
||||
|
||||
<HBox spacing="5.0" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<VBox HBox.hgrow="SOMETIMES">
|
||||
<children>
|
||||
<Label text="State ID" VBox.vgrow="ALWAYS" />
|
||||
<Label text="Issuer" />
|
||||
<Label text="Originated" wrapText="true" />
|
||||
<Label text="Amount" wrapText="true" />
|
||||
<Label fx:id="equivLabel" text="USD" wrapText="true" />
|
||||
</children>
|
||||
</VBox>
|
||||
<VBox HBox.hgrow="ALWAYS">
|
||||
<children>
|
||||
<Label fx:id="stateIdValueLabel" text="39043-329090-390091" />
|
||||
<Label fx:id="issuerValueLabel" styleClass="counterparty" text="C-03820 HSBC GROUP PLC" />
|
||||
<Label fx:id="originatedValueLabel" text="2018-04-27 11:34 UTC" />
|
||||
<Label fx:id="amountValueLabel" text="GBP 0.00" wrapText="true" />
|
||||
<Label fx:id="equivValueLabel" text="0.00" wrapText="true" />
|
||||
</children>
|
||||
</VBox>
|
||||
</children>
|
||||
</HBox>
|
||||
<Label fx:id="stateIdValueLabel" text="39043-329090-390091" GridPane.columnIndex="1"/>
|
||||
<Label fx:id="issuerValueLabel" styleClass="counterparty" text="C-03820 HSBC GROUP PLC" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
|
||||
<Label fx:id="originatedValueLabel" text="2018-04-27 11:34 UTC" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
|
||||
<Label fx:id="amountValueLabel" text="GBP 0.00" wrapText="true" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
|
||||
<Label fx:id="equivValueLabel" text="0.00" wrapText="true" GridPane.columnIndex="1" GridPane.rowIndex="4"/>
|
||||
</GridPane>
|
||||
|
@ -1,84 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import java.lang.String?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ListView?>
|
||||
<?import javafx.scene.control.SplitPane?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.control.TreeTableColumn?>
|
||||
<?import javafx.scene.control.TreeTableView?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<SplitPane fx:id="topSplitPane" dividerPositions="0.5" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<items>
|
||||
<VBox fx:id="leftPane" spacing="5.0" styleClass="root">
|
||||
<children>
|
||||
<StackPane alignment="CENTER_RIGHT">
|
||||
<VBox.margin>
|
||||
<Insets />
|
||||
</VBox.margin>
|
||||
<children>
|
||||
<TextField id="search" fx:id="searchCriteriaTextField" promptText="Search by issuer/currency" styleClass="search">
|
||||
<opaqueInsets>
|
||||
<Insets />
|
||||
</opaqueInsets>
|
||||
<StackPane.margin>
|
||||
<Insets />
|
||||
</StackPane.margin>
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
</TextField>
|
||||
<ImageView fx:id="searchCancelImageView" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
|
||||
<image>
|
||||
<Image url="@../../images/clear_inactive.png" />
|
||||
</image>
|
||||
<StackPane.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</StackPane.margin>
|
||||
</ImageView>
|
||||
</children>
|
||||
</StackPane>
|
||||
<Label fx:id="totalMatchingLabel" alignment="BOTTOM_LEFT" text="Total 15 matching issuer(s)" wrapText="true">
|
||||
<VBox.margin>
|
||||
<Insets bottom="5.0" right="10.0" top="5.0" />
|
||||
</VBox.margin>
|
||||
</Label>
|
||||
<TreeTableView fx:id="cashViewerTable" showRoot="false" VBox.vgrow="ALWAYS">
|
||||
<columns>
|
||||
<TreeTableColumn fx:id="cashViewerTableIssuerCurrency" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefWidth="100.0" styleClass="first-column" text="Issuer/Currency" />
|
||||
<TreeTableColumn fx:id="cashViewerTableLocalCurrency" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefWidth="132.0" text="Local currency">
|
||||
<styleClass>
|
||||
<String fx:value="monetary-value" />
|
||||
<String fx:value="second-column" />
|
||||
</styleClass>
|
||||
</TreeTableColumn>
|
||||
<TreeTableColumn fx:id="cashViewerTableEquiv" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefWidth="72.0" styleClass="monetary-value" text="Equiv" />
|
||||
</columns>
|
||||
</TreeTableView>
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
</VBox>
|
||||
<VBox fx:id="rightPane" spacing="5.0">
|
||||
<children>
|
||||
<Button mnemonicParsing="false" text=">>" />
|
||||
<Label fx:id="totalPositionsLabel" styleClass="subline" text="Total 18 position(s)" />
|
||||
<Label fx:id="equivSumLabel" styleClass="headline" text="USD 394.6k" />
|
||||
<ListView fx:id="cashStatesList" VBox.vgrow="ALWAYS" />
|
||||
</children>
|
||||
<opaqueInsets>
|
||||
<Insets />
|
||||
</opaqueInsets>
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
</VBox>
|
||||
</items>
|
||||
</SplitPane>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<BorderPane stylesheets="@../css/wallet.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<padding>
|
||||
<Insets right="10" left="5" bottom="5" top="5"/>
|
||||
</padding>
|
||||
<center>
|
||||
<SplitPane fx:id="splitPane" dividerPositions="0.5">
|
||||
<VBox fx:id="leftPane" spacing="5.0">
|
||||
<TreeTableView fx:id="cashViewerTable" showRoot="false" VBox.vgrow="ALWAYS">
|
||||
<columns>
|
||||
<TreeTableColumn fx:id="cashViewerTableIssuerCurrency" styleClass="first-column" text="Issuer/Currency"/>
|
||||
<TreeTableColumn fx:id="cashViewerTableLocalCurrency" text="Local currency" styleClass="monetary-value, second-column"/>
|
||||
<TreeTableColumn fx:id="cashViewerTableEquiv" styleClass="monetary-value" text="Equiv"/>
|
||||
</columns>
|
||||
</TreeTableView>
|
||||
<Label fx:id="totalMatchingLabel" text="Total 15 matching issuer(s)"/>
|
||||
</VBox>
|
||||
<VBox fx:id="rightPane" spacing="5.0">
|
||||
<Button fx:id="toggleButton" mnemonicParsing="false" text=">>"/>
|
||||
<ListView fx:id="cashStatesList" VBox.vgrow="ALWAYS"/>
|
||||
<Label fx:id="totalPositionsLabel" text="Total 18 position(s)"/>
|
||||
</VBox>
|
||||
</SplitPane>
|
||||
</center>
|
||||
</BorderPane>
|
||||
|
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<GridPane hgap="10" styleClass="expand-row" stylesheets="@../css/wallet.css" vgap="10" xmlns="http://javafx.com/javafx/8.0.112-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<TitledPane fx:id="inputPane" collapsible="false" text="Input" GridPane.fillWidth="true">
|
||||
<ListView fx:id="inputs"/>
|
||||
</TitledPane>
|
||||
|
||||
<Label GridPane.columnIndex="1">
|
||||
<graphic>
|
||||
<FontAwesomeIconView glyphName="PLAY" glyphSize="40" style="-fx-fill: rgb(20, 136, 204);"/>
|
||||
</graphic>
|
||||
</Label>
|
||||
|
||||
<TitledPane fx:id="outputPane" collapsible="false" text="Outputs" GridPane.columnIndex="2">
|
||||
<ListView fx:id="outputs" maxWidth="Infinity"/>
|
||||
</TitledPane>
|
||||
|
||||
<TitledPane fx:id="signaturesPane" collapsible="false" text="Signatures" GridPane.columnSpan="3" GridPane.rowIndex="1">
|
||||
<ListView fx:id="signatures"/>
|
||||
</TitledPane>
|
||||
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="ALWAYS"/>
|
||||
<ColumnConstraints/>
|
||||
<ColumnConstraints hgrow="ALWAYS"/>
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints vgrow="ALWAYS"/>
|
||||
<RowConstraints/>
|
||||
</rowConstraints>
|
||||
</GridPane>
|
@ -1,95 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.MenuItem?>
|
||||
<?import javafx.scene.control.SplitMenuButton?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.layout.GridPane?>
|
||||
<GridPane hgap="10" stylesheets="@../css/wallet.css" styleClass="header-panel" vgap="5" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<padding>
|
||||
<Insets left="10.0" right="10.0" top="5.0" bottom="5.0"/>
|
||||
</padding>
|
||||
<!-- Row 1 -->
|
||||
<Label id="headline" fx:id="sectionLabel" alignment="BOTTOM_CENTER" maxHeight="Infinity" text="Home" GridPane.columnIndex="0" GridPane.hgrow="ALWAYS"/>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<VBox spacing="5.0">
|
||||
<children>
|
||||
<HBox>
|
||||
<children>
|
||||
<HBox alignment="CENTER_LEFT" spacing="5.0" HBox.hgrow="ALWAYS">
|
||||
<children>
|
||||
<VBox fx:id="sectionIconContainer" alignment="CENTER">
|
||||
<children>
|
||||
<ImageView fx:id="sectionIcon" fitHeight="30.0" fitWidth="30.0" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../../images/home.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</children>
|
||||
</VBox>
|
||||
<Label id="headline" fx:id="sectionLabel" text="Home" />
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
</HBox>
|
||||
<HBox alignment="TOP_RIGHT" spacing="5.0">
|
||||
<children>
|
||||
<Button fx:id="debugNextButton" mnemonicParsing="false" text="Next" />
|
||||
<Button fx:id="debugGoStopButton" mnemonicParsing="false" text="!!!" />
|
||||
<SplitMenuButton mnemonicParsing="false" text="DRUTTER">
|
||||
<items>
|
||||
<MenuItem mnemonicParsing="false" text="Sign out" />
|
||||
<MenuItem mnemonicParsing="false" text="Account settings..." />
|
||||
</items>
|
||||
<graphic>
|
||||
<ImageView fitHeight="20.0" fitWidth="52.0" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../../images/user_w.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
</SplitMenuButton>
|
||||
<Button fx:id="settingsButton" mnemonicParsing="false" text="Settings">
|
||||
<graphic>
|
||||
<ImageView fitHeight="20.0" fitWidth="20.0" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../../images/settings_w.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
</Button>
|
||||
</children>
|
||||
</HBox>
|
||||
</children>
|
||||
</HBox>
|
||||
<StackPane alignment="CENTER_RIGHT">
|
||||
<children>
|
||||
<TextField id="search_main" promptText="Search for states, transactions, counterparties etc." styleClass="search">
|
||||
<opaqueInsets>
|
||||
<Insets />
|
||||
</opaqueInsets>
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="30.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
</TextField>
|
||||
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
|
||||
<image>
|
||||
<Image url="@../../images/clear_inactive.png" />
|
||||
</image>
|
||||
<StackPane.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</StackPane.margin>
|
||||
</ImageView>
|
||||
</children>
|
||||
</StackPane>
|
||||
</children>
|
||||
</VBox>
|
||||
<StackPane alignment="CENTER_RIGHT" />
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
</VBox>
|
||||
<SplitMenuButton fx:id="userButton" maxHeight="Infinity" mnemonicParsing="false" text="DRUTTER" GridPane.columnIndex="3">
|
||||
<items>
|
||||
<MenuItem mnemonicParsing="false" text="Sign out"/>
|
||||
<MenuItem mnemonicParsing="false" text="Account settings..."/>
|
||||
</items>
|
||||
<graphic>
|
||||
<ImageView fitHeight="20.0" fitWidth="20.0" pickOnBounds="true" preserveRatio="true">
|
||||
<Image url="@../images/user_w.png"/>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
</SplitMenuButton>
|
||||
<!--<Button fx:id="settingsButton" maxHeight="Infinity" mnemonicParsing="false" text="Settings" GridPane.columnIndex="4">
|
||||
<graphic>
|
||||
<ImageView fitHeight="20.0" fitWidth="20.0" pickOnBounds="true" preserveRatio="true">
|
||||
<Image url="@../images/settings_w.png"/>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
</Button>-->
|
||||
|
||||
<!--<!– Row 2 –>
|
||||
<StackPane alignment="CENTER_RIGHT" GridPane.columnSpan="5" GridPane.rowIndex="1">
|
||||
<TextField fx:id="search_main" promptText="Search for states, transactions, counterparties etc." styleClass="search"/>
|
||||
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
|
||||
<StackPane.margin>
|
||||
<Insets right="10.0"/>
|
||||
</StackPane.margin>
|
||||
</ImageView>
|
||||
</StackPane>-->
|
||||
</GridPane>
|
||||
|
@ -2,19 +2,21 @@
|
||||
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.TilePane?>
|
||||
<TilePane prefHeight="425.0" prefWidth="425.0" tileAlignment="TOP_LEFT" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<TitledPane id="tile_cash" fx:id="ourCashPane" alignment="CENTER" collapsible="false" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our cash">
|
||||
<Label fx:id="ourCashLabel" text="USD 186.7m" textAlignment="CENTER" wrapText="true"/>
|
||||
</TitledPane>
|
||||
<TitledPane id="tile_debtors" fx:id="ourDebtorsPane" alignment="CENTER" collapsible="false" layoutX="232.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our debtors">
|
||||
<Label text="USD 71.3m" textAlignment="CENTER" wrapText="true"/>
|
||||
</TitledPane>
|
||||
<TitledPane id="tile_creditors" fx:id="ourCreditorsPane" alignment="CENTER" collapsible="false" layoutX="312.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our creditors">
|
||||
<Label text="USD (29.4m)" textAlignment="CENTER" wrapText="true"/>
|
||||
</TitledPane>
|
||||
<TitledPane id="tile_tx" fx:id="ourTransactionsPane" alignment="CENTER" collapsible="false" layoutX="392.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our transactions">
|
||||
<Label fx:id="ourTransactionsLabel" text="In flight: 1,315" textAlignment="CENTER" wrapText="true"/>
|
||||
</TitledPane>
|
||||
<TitledPane id="tile_new_tx" fx:id="newTransaction" alignment="CENTER" collapsible="false" layoutX="472.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="New Transaction">
|
||||
</TitledPane>
|
||||
</TilePane>
|
||||
<ScrollPane hbarPolicy="NEVER" fitToWidth="true" stylesheets="@../css/wallet.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<TilePane fx:id="tilePane" tileAlignment="TOP_LEFT" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<TitledPane id="Cash" fx:id="ourCashPane" collapsible="false" onMouseClicked="#changeView" styleClass="tile" text="Our cash">
|
||||
<Label fx:id="ourCashLabel" text="USD 186.7m" textAlignment="CENTER" wrapText="true" />
|
||||
</TitledPane>
|
||||
<TitledPane id="tile_debtors" fx:id="ourDebtorsPane" collapsible="false" styleClass="tile" text="Our debtors">
|
||||
<Label text="USD 71.3m" textAlignment="CENTER" wrapText="true" />
|
||||
</TitledPane>
|
||||
<TitledPane id="tile_creditors" fx:id="ourCreditorsPane" collapsible="false" styleClass="tile" text="Our creditors">
|
||||
<Label text="USD (29.4m)" textAlignment="CENTER" wrapText="true" />
|
||||
</TitledPane>
|
||||
<TitledPane id="Transaction" fx:id="ourTransactionsPane" collapsible="false" onMouseClicked="#changeView" styleClass="tile" text="Our transactions">
|
||||
<Label fx:id="ourTransactionsLabel" textAlignment="CENTER" wrapText="true" />
|
||||
</TitledPane>
|
||||
<TitledPane id="NewTransaction" fx:id="newTransaction" collapsible="false" onMouseClicked="#changeView" styleClass="tile" text="New Transaction">
|
||||
</TitledPane>
|
||||
</TilePane>
|
||||
</ScrollPane>
|
||||
|
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<DialogPane stylesheets="@../css/wallet.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<padding>
|
||||
<Insets top="10" bottom="10" left="50" right="50"/>
|
||||
</padding>
|
||||
<content>
|
||||
<GridPane hgap="10" vgap="10" prefWidth="400">
|
||||
<Label text="Corda Node :"/>
|
||||
<TextField fx:id="host" promptText="Host" GridPane.columnIndex="1"/>
|
||||
<TextField fx:id="port" promptText="Port" prefWidth="100" GridPane.columnIndex="2"/>
|
||||
|
||||
<Label text="Username :" GridPane.rowIndex="1"/>
|
||||
<TextField fx:id="username" promptText="Username" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.columnSpan="2"/>
|
||||
|
||||
<Label text="Password:" GridPane.rowIndex="2"/>
|
||||
<PasswordField fx:id="password" promptText="Password" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.columnSpan="2"/>
|
||||
</GridPane>
|
||||
</content>
|
||||
<ButtonType fx:id="connectButton" text="Connect" buttonData="OK_DONE"/>
|
||||
</DialogPane>
|
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<StackPane alignment="CENTER_RIGHT" stylesheets="@../css/wallet.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<TextField fx:id="textField" promptText="Filter transactions by originator, contract type..." styleClass="search">
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="30.0" right="5.0"/>
|
||||
</padding>
|
||||
<StackPane.margin>
|
||||
<Insets bottom="5.0" top="5.0"/>
|
||||
</StackPane.margin>
|
||||
</TextField>
|
||||
<ImageView fx:id="clearButton" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
|
||||
<StackPane.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/>
|
||||
</StackPane.margin>
|
||||
<Image url="@../images/clear_inactive.png"/>
|
||||
</ImageView>
|
||||
</StackPane>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.Separator?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<VBox xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<Label styleClass="corda-logo"/>
|
||||
<Separator/>
|
||||
</VBox>
|
@ -1,8 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.SplitPane?>
|
||||
<?import javafx.scene.layout.BorderPane?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<VBox fx:id="topLevel" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<BorderPane fx:id="selectionBorderPane"/>
|
||||
</VBox>
|
||||
<StackPane stylesheets="@../css/wallet.css" prefHeight="650" prefWidth="900" styleClass="root, no-padding" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<SplitPane dividerPositions="0.0" styleClass="no-padding, split-pane-divider">
|
||||
<VBox fx:id="sidebarPane" maxWidth="200.0" minWidth="80" styleClass="sidebar" SplitPane.resizableWithParent="false"/>
|
||||
<BorderPane fx:id="selectionBorderPane" maxHeight="Infinity" minWidth="400"/>
|
||||
</SplitPane>
|
||||
</StackPane>
|
@ -1,137 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import java.lang.String?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ListView?>
|
||||
<?import javafx.scene.control.SplitPane?>
|
||||
<?import javafx.scene.control.TableColumn?>
|
||||
<?import javafx.scene.control.TableView?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.control.TitledPane?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.layout.BorderPane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<VBox styleClass="view" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<StackPane alignment="CENTER_RIGHT">
|
||||
<children>
|
||||
<TextField promptText="Filter transactions by originator, contract type..." styleClass="search">
|
||||
<opaqueInsets>
|
||||
<Insets />
|
||||
</opaqueInsets>
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="30.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
<StackPane.margin>
|
||||
<Insets bottom="5.0" top="5.0" />
|
||||
</StackPane.margin>
|
||||
</TextField>
|
||||
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
|
||||
<image>
|
||||
<Image url="@../../../../../../../../../internal/explorer/src/main/resources/images/clear_inactive.png" />
|
||||
</image>
|
||||
<StackPane.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</StackPane.margin>
|
||||
</ImageView>
|
||||
</children>
|
||||
</StackPane>
|
||||
<SplitPane fx:id="topSplitPane" dividerPositions="0.3, 0.6" orientation="VERTICAL" prefHeight="562.0" prefWidth="1087.0" VBox.vgrow="ALWAYS">
|
||||
<items>
|
||||
<TableView fx:id="transactionViewTable" prefHeight="200.0" prefWidth="200.0">
|
||||
<columns>
|
||||
<TableColumn fx:id="transactionViewTransactionId" prefWidth="75.0" text="Transaction ID" />
|
||||
<TableColumn fx:id="transactionViewStateMachineId" prefWidth="187.0" text="StateMachine ID" />
|
||||
<TableColumn fx:id="transactionViewClientUuid" prefWidth="75.0" text="Client UUID" />
|
||||
<TableColumn fx:id="transactionViewTransactionStatus" prefWidth="75.0" text="Transaction status" />
|
||||
<TableColumn fx:id="transactionViewProtocolStatus" prefWidth="75.0" text="Protocol status" />
|
||||
<TableColumn fx:id="transactionViewStateMachineStatus" prefWidth="75.0" text="SM Status" />
|
||||
<TableColumn fx:id="transactionViewCommandTypes" prefWidth="75.0" text="Command type(s)" />
|
||||
<TableColumn fx:id="transactionViewTotalValueEquiv" prefWidth="75.0" styleClass="monetary-value" text="Total value (USD equiv)" />
|
||||
</columns>
|
||||
<columnResizePolicy>
|
||||
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
|
||||
</columnResizePolicy>
|
||||
</TableView>
|
||||
<TitledPane fx:id="contractStatesTitledPane" animated="false" text="Contract states">
|
||||
<content>
|
||||
<SplitPane dividerPositions="0.5" prefHeight="160.0" prefWidth="200.0">
|
||||
<items>
|
||||
<VBox prefHeight="200.0" prefWidth="100.0">
|
||||
<children>
|
||||
<HBox spacing="5.0">
|
||||
<children>
|
||||
<Label text="Inputs:" />
|
||||
<Label fx:id="contractStatesInputsCountLabel" text="Label" />
|
||||
</children>
|
||||
</HBox>
|
||||
<TableView fx:id="contractStatesInputStatesTable" prefHeight="200.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
|
||||
<columns>
|
||||
<TableColumn fx:id="contractStatesInputStatesId" prefWidth="75.0" text="ID" />
|
||||
<TableColumn fx:id="contractStatesInputStatesType" prefWidth="75.0" text="Type" />
|
||||
<TableColumn fx:id="contractStatesInputStatesOwner" prefWidth="75.0" text="Owner" />
|
||||
<TableColumn fx:id="contractStatesInputStatesLocalCurrency" prefWidth="75.0" styleClass="first-column" text="Local Ccy" />
|
||||
<TableColumn fx:id="contractStatesInputStatesAmount" prefWidth="75.0" text="Amount">
|
||||
<styleClass>
|
||||
<String fx:value="second-column" />
|
||||
<String fx:value="monetary-value" />
|
||||
</styleClass>
|
||||
</TableColumn>
|
||||
<TableColumn fx:id="contractStatesInputStatesEquiv" prefWidth="75.0" text="USD Equiv" />
|
||||
</columns>
|
||||
<columnResizePolicy>
|
||||
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
|
||||
</columnResizePolicy>
|
||||
</TableView>
|
||||
</children>
|
||||
</VBox>
|
||||
<VBox prefHeight="200.0" prefWidth="100.0">
|
||||
<children>
|
||||
<HBox spacing="5.0">
|
||||
<children>
|
||||
<Label text="Outputs:" />
|
||||
<Label fx:id="contractStatesOutputsCountLabel" text="Label" />
|
||||
</children>
|
||||
</HBox>
|
||||
<TableView fx:id="contractStatesOutputStatesTable" prefHeight="200.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
|
||||
<columns>
|
||||
<TableColumn fx:id="contractStatesOutputStatesId" prefWidth="75.0" text="ID" />
|
||||
<TableColumn fx:id="contractStatesOutputStatesType" prefWidth="75.0" text="Type" />
|
||||
<TableColumn fx:id="contractStatesOutputStatesOwner" prefWidth="75.0" text="Owner" />
|
||||
<TableColumn fx:id="contractStatesOutputStatesLocalCurrency" prefWidth="75.0" styleClass="first-column" text="Local Ccy" />
|
||||
<TableColumn fx:id="contractStatesOutputStatesAmount" prefWidth="75.0" text="Amount">
|
||||
<styleClass>
|
||||
<String fx:value="second-column" />
|
||||
<String fx:value="monetary-value" />
|
||||
</styleClass>
|
||||
</TableColumn>
|
||||
<TableColumn fx:id="contractStatesOutputStatesEquiv" prefWidth="75.0" text="USD Equiv" />
|
||||
</columns>
|
||||
<columnResizePolicy>
|
||||
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
|
||||
</columnResizePolicy>
|
||||
</TableView>
|
||||
</children>
|
||||
</VBox>
|
||||
</items>
|
||||
</SplitPane>
|
||||
</content>
|
||||
</TitledPane>
|
||||
<TitledPane fx:id="signaturesTitledPane" animated="false" text="Signatures">
|
||||
<content>
|
||||
<ListView fx:id="signaturesList" />
|
||||
</content>
|
||||
</TitledPane>
|
||||
</items>
|
||||
</SplitPane>
|
||||
<HBox>
|
||||
<children>
|
||||
<Label fx:id="matchingTransactionsLabel" text="matching transaction(s)" />
|
||||
</children>
|
||||
</HBox>
|
||||
</children>
|
||||
</VBox>
|
||||
<BorderPane stylesheets="@../css/wallet.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<padding>
|
||||
<Insets top="5" left="5" right="10" bottom="5"/>
|
||||
</padding>
|
||||
<center>
|
||||
<TableView fx:id="transactionViewTable" VBox.vgrow="ALWAYS">
|
||||
<columnResizePolicy>
|
||||
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/>
|
||||
</columnResizePolicy>
|
||||
</TableView>
|
||||
</center>
|
||||
<bottom>
|
||||
<Label fx:id="matchingTransactionsLabel" text="matching transaction(s)"/>
|
||||
</bottom>
|
||||
</BorderPane>
|
||||
|
@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
|
||||
<StackPane alignment="CENTER_RIGHT" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<TextField fx:id="SearchCriteriaTextField" promptText="Set prompt text" styleClass="search">
|
||||
<opaqueInsets>
|
||||
<Insets />
|
||||
</opaqueInsets>
|
||||
<StackPane.margin>
|
||||
<Insets />
|
||||
</StackPane.margin>
|
||||
</TextField>
|
||||
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear" StackPane.alignment="CENTER_RIGHT">
|
||||
<image>
|
||||
<Image url="@../../images/clear_inactive.png" />
|
||||
</image>
|
||||
<StackPane.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</StackPane.margin>
|
||||
</ImageView>
|
||||
</children>
|
||||
</StackPane>
|
@ -77,6 +77,10 @@ class CordaRPCOpsImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun nodeIdentity(): NodeInfo {
|
||||
return services.myInfo
|
||||
}
|
||||
|
||||
override fun addVaultTransactionNote(txnId: SecureHash, txnNote: String) {
|
||||
return databaseTransaction(database) {
|
||||
services.vaultService.addNoteToTransaction(txnId, txnNote)
|
||||
|
@ -118,6 +118,10 @@ interface CordaRPCOps : RPCOps {
|
||||
*/
|
||||
fun executeCommand(command: ClientToServiceCommand): TransactionBuildResult
|
||||
|
||||
/**
|
||||
* Returns Node's identity, assuming this will not change while the node is running.
|
||||
*/
|
||||
fun nodeIdentity(): NodeInfo
|
||||
/*
|
||||
* Add note(s) to an existing Vault transaction
|
||||
*/
|
||||
|
@ -16,10 +16,8 @@ import com.r3corda.core.crypto.*
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.node.PhysicalLocation
|
||||
import com.r3corda.core.node.ServiceEntry
|
||||
import com.r3corda.core.node.services.NetworkMapCache
|
||||
import com.r3corda.core.node.services.ServiceInfo
|
||||
import com.r3corda.core.node.services.StateMachineTransactionMapping
|
||||
import com.r3corda.core.node.services.Vault
|
||||
import com.r3corda.core.node.WorldCoordinate
|
||||
import com.r3corda.core.node.services.*
|
||||
import com.r3corda.core.protocols.StateMachineRunId
|
||||
import com.r3corda.core.serialization.*
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
@ -101,6 +99,7 @@ fun requirePermission(permission: String) {
|
||||
*/
|
||||
open class RPCException(msg: String, cause: Throwable?) : RuntimeException(msg, cause) {
|
||||
constructor(msg: String) : this(msg, null)
|
||||
|
||||
class DeadlineExceeded(rpcName: String) : RPCException("Deadline exceeded on call to $rpcName")
|
||||
}
|
||||
|
||||
@ -187,8 +186,12 @@ private class RPCKryo(observableSerializer: Serializer<Observable<Any>>? = null)
|
||||
kryo.writeObject(output, nodeAddress.hostAndPort)
|
||||
}
|
||||
)
|
||||
register(NodeMessagingClient.makeNetworkMapAddress(HostAndPort.fromString("localhost:0")).javaClass)
|
||||
register(ServiceInfo::class.java)
|
||||
register(ServiceType.getServiceType("ab", "ab").javaClass)
|
||||
register(ServiceType.parse("ab").javaClass)
|
||||
register(WorldCoordinate::class.java)
|
||||
register(HostAndPort::class.java)
|
||||
register(ServiceInfo::class.java, read = { kryo, input -> ServiceInfo.parse(input.readString()) }, write = Kryo::writeObject)
|
||||
// Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway.
|
||||
register(IllegalArgumentException::class.java)
|
||||
// Kryo couldn't serialize Collections.unmodifiableCollection in Throwable correctly, causing null pointer exception when try to access the deserialize object.
|
||||
|
@ -25,7 +25,6 @@ import com.r3corda.node.services.network.NetworkMapService.Companion.FETCH_PROTO
|
||||
import com.r3corda.node.services.network.NetworkMapService.Companion.SUBSCRIPTION_PROTOCOL_TOPIC
|
||||
import com.r3corda.node.services.network.NetworkMapService.FetchMapResponse
|
||||
import com.r3corda.node.services.network.NetworkMapService.SubscribeResponse
|
||||
import com.r3corda.node.services.transactions.SimpleNotaryService
|
||||
import com.r3corda.node.utilities.AddOrRemove
|
||||
import com.r3corda.protocols.sendRequest
|
||||
import rx.Observable
|
||||
@ -59,13 +58,7 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
|
||||
|
||||
override fun track(): Pair<List<NodeInfo>, Observable<MapChange>> {
|
||||
synchronized(_changed) {
|
||||
fun NodeInfo.isCordaService(): Boolean {
|
||||
return advertisedServices.any { it.info.type in setOf(SimpleNotaryService.type, NetworkMapService.type) }
|
||||
}
|
||||
|
||||
val currentParties = partyNodes.filter { !it.isCordaService() }
|
||||
val changes = changed.filter { !it.node.isCordaService() }
|
||||
return Pair(currentParties, changes.bufferUntilSubscribed())
|
||||
return Pair(partyNodes, _changed.bufferUntilSubscribed())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,11 @@
|
||||
package com.r3corda.node.services
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.r3corda.core.contracts.ClientToServiceCommand
|
||||
import com.r3corda.core.contracts.ContractState
|
||||
import com.r3corda.core.contracts.StateAndRef
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.crypto.generateKeyPair
|
||||
import com.r3corda.core.messaging.Message
|
||||
import com.r3corda.core.messaging.createMessage
|
||||
|
Loading…
Reference in New Issue
Block a user