Changing UI layout

This commit is contained in:
Patrick Kuo
2016-10-19 00:47:15 +01:00
parent 6d39b71bf9
commit 7f8608c981
40 changed files with 1428 additions and 1424 deletions

View File

@ -16,7 +16,9 @@ import com.r3corda.node.driver.driver
import com.r3corda.node.internal.CordaRPCOpsImpl import com.r3corda.node.internal.CordaRPCOpsImpl
import com.r3corda.node.services.User import com.r3corda.node.services.User
import com.r3corda.node.services.config.configureTestSSL 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.messaging.StateMachineUpdate
import com.r3corda.node.services.network.NetworkMapService
import com.r3corda.node.services.transactions.SimpleNotaryService import com.r3corda.node.services.transactions.SimpleNotaryService
import com.r3corda.testing.expect import com.r3corda.testing.expect
import com.r3corda.testing.expectEvents import com.r3corda.testing.expectEvents
@ -30,7 +32,6 @@ import java.util.concurrent.CountDownLatch
import kotlin.concurrent.thread import kotlin.concurrent.thread
class NodeMonitorModelTest { class NodeMonitorModelTest {
lateinit var aliceNode: NodeInfo lateinit var aliceNode: NodeInfo
lateinit var notaryNode: NodeInfo lateinit var notaryNode: NodeInfo
val stopDriver = CountDownLatch(1) val stopDriver = CountDownLatch(1)
@ -67,7 +68,7 @@ class NodeMonitorModelTest {
networkMapUpdates = monitor.networkMap.bufferUntilSubscribed() networkMapUpdates = monitor.networkMap.bufferUntilSubscribed()
clientToService = monitor.clientToService clientToService = monitor.clientToService
monitor.register(aliceNode, configureTestSSL(), cashUser.username, cashUser.password) monitor.register(ArtemisMessagingComponent.toHostAndPort(aliceNode.address), configureTestSSL(), cashUser.username, cashUser.password)
driverStarted.countDown() driverStarted.countDown()
stopDriver.await() stopDriver.await()
} }
@ -85,17 +86,19 @@ class NodeMonitorModelTest {
fun `network map update`() { fun `network map update`() {
newNode("Bob") newNode("Bob")
newNode("Charlie") newNode("Charlie")
networkMapUpdates.expectEvents(isStrict = false) { networkMapUpdates.filter { !it.node.advertisedServices.any { it.info.type.isNotary() } }
.filter { !it.node.advertisedServices.any { it.info.type == NetworkMapService.type } }
.expectEvents(isStrict = false) {
sequence( sequence(
// TODO : Add test for remove when driver DSL support individual node shutdown. // TODO : Add test for remove when driver DSL support individual node shutdown.
expect { output: NetworkMapCache.MapChange -> expect { output: NetworkMapCache.MapChange ->
require(output.node.legalIdentity.name == "Alice") { output.node.legalIdentity.name } require(output.node.legalIdentity.name == "Alice") { "Expecting : Alice, Actual : ${output.node.legalIdentity.name}" }
}, },
expect { output: NetworkMapCache.MapChange -> expect { output: NetworkMapCache.MapChange ->
require(output.node.legalIdentity.name == "Bob") { output.node.legalIdentity.name } require(output.node.legalIdentity.name == "Bob") { "Expecting : Bob, Actual : ${output.node.legalIdentity.name}" }
}, },
expect { output: NetworkMapCache.MapChange -> expect { output: NetworkMapCache.MapChange ->
require(output.node.legalIdentity.name == "Charlie") { output.node.legalIdentity.name } require(output.node.legalIdentity.name == "Charlie") { "Expecting : Charlie, Actual : ${output.node.legalIdentity.name}" }
} }
) )
} }

View File

@ -10,9 +10,7 @@ import javafx.collections.ObservableList
import javafx.collections.ObservableMap import javafx.collections.ObservableMap
import javafx.collections.transformation.FilteredList import javafx.collections.transformation.FilteredList
import org.fxmisc.easybind.EasyBind import org.fxmisc.easybind.EasyBind
import org.slf4j.LoggerFactory
import java.util.function.Predicate 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 * Here follows utility extension functions that help reduce the visual load when developing RX code. Each function should

View File

@ -4,7 +4,6 @@ import com.r3corda.client.fxutils.*
import com.r3corda.core.contracts.ContractState import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.StateAndRef import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.contracts.StateRef import com.r3corda.core.contracts.StateRef
import com.r3corda.client.fxutils.recordInSequence
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.services.StateMachineTransactionMapping import com.r3corda.core.node.services.StateMachineTransactionMapping
import com.r3corda.core.protocols.StateMachineRunId import com.r3corda.core.protocols.StateMachineRunId
@ -15,8 +14,6 @@ import javafx.beans.value.ObservableValue
import javafx.collections.ObservableList import javafx.collections.ObservableList
import javafx.collections.ObservableMap import javafx.collections.ObservableMap
import org.fxmisc.easybind.EasyBind import org.fxmisc.easybind.EasyBind
import org.slf4j.LoggerFactory
import rx.Observable
data class GatheredTransactionData( data class GatheredTransactionData(
val transaction: PartiallyResolvedTransaction, val transaction: PartiallyResolvedTransaction,
@ -30,8 +27,7 @@ data class GatheredTransactionData(
*/ */
data class PartiallyResolvedTransaction( data class PartiallyResolvedTransaction(
val transaction: SignedTransaction, val transaction: SignedTransaction,
val inputs: List<ObservableValue<InputResolution>> val inputs: List<ObservableValue<InputResolution>>) {
) {
val id = transaction.id val id = transaction.id
sealed class InputResolution(val stateRef: StateRef) { sealed class InputResolution(val stateRef: StateRef) {
class Unresolved(stateRef: StateRef) : InputResolution(stateRef) class Unresolved(stateRef: StateRef) : InputResolution(stateRef)
@ -84,16 +80,15 @@ data class StateMachineData(
*/ */
class GatheredTransactionDataModel { class GatheredTransactionDataModel {
private val transactions: Observable<SignedTransaction> by observable(NodeMonitorModel::transactions) private val transactions by observable(NodeMonitorModel::transactions)
private val stateMachineUpdates: Observable<StateMachineUpdate> by observable(NodeMonitorModel::stateMachineUpdates) private val stateMachineUpdates by observable(NodeMonitorModel::stateMachineUpdates)
private val progressTracking: Observable<ProgressTrackingEvent> by observable(NodeMonitorModel::progressTracking) private val progressTracking by observable(NodeMonitorModel::progressTracking)
private val stateMachineTransactionMapping: Observable<StateMachineTransactionMapping> by observable(NodeMonitorModel::stateMachineTransactionMapping) private val stateMachineTransactionMapping by observable(NodeMonitorModel::stateMachineTransactionMapping)
val collectedTransactions = transactions.recordInSequence() private val collectedTransactions = transactions.recordInSequence()
val transactionMap = collectedTransactions.associateBy(SignedTransaction::id) private val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId) private val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
val stateMachineStatus: ObservableMap<StateMachineRunId, out ObservableValue<StateMachineStatus>> = private val stateMachineStatus = stateMachineUpdates.foldToObservableMap(Unit) { update, _unit, map: ObservableMap<StateMachineRunId, SimpleObjectProperty<StateMachineStatus>> ->
stateMachineUpdates.foldToObservableMap(Unit) { update, _unit, map: ObservableMap<StateMachineRunId, SimpleObjectProperty<StateMachineStatus>> ->
when (update) { when (update) {
is StateMachineUpdate.Added -> { is StateMachineUpdate.Added -> {
val added: SimpleObjectProperty<StateMachineStatus> = val added: SimpleObjectProperty<StateMachineStatus> =
@ -107,21 +102,19 @@ class GatheredTransactionDataModel {
} }
} }
} }
val stateMachineDataList: ObservableList<StateMachineData> = private val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
StateMachineData(id, progress.map { it?.let { ProtocolStatus(it.message) } }, status) StateMachineData(id, progress.map { it?.let { ProtocolStatus(it.message) } }, status)
}.getObservableValues() }.getObservableValues()
val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id) private val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
val smTxMappingList = stateMachineTransactionMapping.recordInSequence() private val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
val partiallyResolvedTransactions = collectedTransactions.map { private val partiallyResolvedTransactions = collectedTransactions.map {
PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap) PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
} }
/** /**
* We JOIN the transaction list with state machines * We JOIN the transaction list with state machines
*/ */
val gatheredTransactionDataList: ObservableList<out GatheredTransactionData> = val gatheredTransactionDataList = partiallyResolvedTransactions.leftOuterJoin(
partiallyResolvedTransactions.leftOuterJoin(
smTxMappingList, smTxMappingList,
PartiallyResolvedTransaction::id, PartiallyResolvedTransaction::id,
StateMachineTransactionMapping::transactionId StateMachineTransactionMapping::transactionId

View File

@ -1,16 +1,18 @@
package com.r3corda.client.model package com.r3corda.client.model
import com.r3corda.client.fxutils.foldToObservableList import com.r3corda.client.fxutils.foldToObservableList
import com.r3corda.client.fxutils.map
import com.r3corda.core.node.NodeInfo import com.r3corda.core.node.NodeInfo
import com.r3corda.core.node.services.NetworkMapCache import com.r3corda.core.node.services.NetworkMapCache
import com.r3corda.node.services.network.NetworkMapService
import javafx.collections.ObservableList import javafx.collections.ObservableList
import kotlinx.support.jdk8.collections.removeIf import kotlinx.support.jdk8.collections.removeIf
import rx.Observable import java.security.PublicKey
class NetworkIdentityModel { 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 -> networkIdentityObservable.foldToObservableList(Unit) { update, _accumulator, observableList ->
observableList.removeIf { observableList.removeIf {
when (update.type) { when (update.type) {
@ -21,4 +23,18 @@ class NetworkIdentityModel {
} }
observableList.addAll(update.node) 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 }
}
} }

View File

@ -1,15 +1,14 @@
package com.r3corda.client.model package com.r3corda.client.model
import com.google.common.net.HostAndPort
import com.r3corda.client.CordaRPCClient import com.r3corda.client.CordaRPCClient
import com.r3corda.core.contracts.ClientToServiceCommand 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.NetworkMapCache
import com.r3corda.core.node.services.StateMachineTransactionMapping import com.r3corda.core.node.services.StateMachineTransactionMapping
import com.r3corda.core.node.services.Vault import com.r3corda.core.node.services.Vault
import com.r3corda.core.protocols.StateMachineRunId import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.transactions.SignedTransaction import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.node.services.config.NodeSSLConfiguration 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.CordaRPCOps
import com.r3corda.node.services.messaging.StateMachineInfo import com.r3corda.node.services.messaging.StateMachineInfo
import com.r3corda.node.services.messaging.StateMachineUpdate import com.r3corda.node.services.messaging.StateMachineUpdate
@ -56,8 +55,8 @@ class NodeMonitorModel {
* Register for updates to/from a given vault. * Register for updates to/from a given vault.
* TODO provide an unsubscribe mechanism * TODO provide an unsubscribe mechanism
*/ */
fun register(vaultMonitorNodeInfo: NodeInfo, sslConfig: NodeSSLConfiguration, username: String, password: String) { fun register(nodeHostAndPort: HostAndPort, sslConfig: NodeSSLConfiguration, username: String, password: String) {
val client = CordaRPCClient(toHostAndPort(vaultMonitorNodeInfo.address), sslConfig) val client = CordaRPCClient(nodeHostAndPort, sslConfig)
client.start(username, password) client.start(username, password)
val proxy = client.proxy() val proxy = client.proxy()
@ -101,7 +100,6 @@ class NodeMonitorModel {
clientToServiceSource.subscribe { clientToServiceSource.subscribe {
proxy.executeCommand(it) proxy.executeCommand(it)
} }
proxyObservable.set(proxy) proxyObservable.set(proxy)
} }
} }

View File

@ -25,8 +25,6 @@ apply plugin: 'kotlin'
apply plugin: 'application' apply plugin: 'application'
sourceCompatibility = 1.8 sourceCompatibility = 1.8
applicationDefaultJvmArgs = ["-javaagent:${rootProject.configurations.quasar.singleFile}"]
mainClassName = 'com.r3corda.explorer.Main' mainClassName = 'com.r3corda.explorer.Main'
sourceSets { sourceSets {
@ -53,7 +51,7 @@ dependencies {
testCompile group: 'junit', name: 'junit', version: '4.11' testCompile group: 'junit', name: 'junit', version: '4.11'
// TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's. // 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. // Corda Core: Data structures and basic types needed to work with Corda.
compile project(':core') compile project(':core')
@ -74,4 +72,12 @@ dependencies {
// Humanize: formatting // Humanize: formatting
compile 'com.github.mfornos:humanize-icu:1.2.2' 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
} }

View File

@ -1,60 +1,72 @@
package com.r3corda.explorer package com.r3corda.explorer
import com.r3corda.client.mock.EventGenerator
import com.r3corda.client.model.Models import com.r3corda.client.model.Models
import com.r3corda.client.model.NodeMonitorModel import com.r3corda.client.model.NodeMonitorModel
import com.r3corda.core.node.services.ServiceInfo 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.PortAllocation
import com.r3corda.node.driver.driver 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 com.r3corda.node.services.transactions.SimpleNotaryService
import javafx.stage.Stage import javafx.stage.Stage
import org.controlsfx.dialog.ExceptionDialog
import tornadofx.App import tornadofx.App
import java.util.*
/**
* Main class for Explorer, you will need Tornado FX to run the explorer.
*/
class Main : App() { class Main : App() {
override val primaryView = MainWindow::class override val primaryView = MainWindow::class
override fun start(stage: Stage) { override fun start(stage: Stage) {
Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
throwable.printStackTrace() throwable.printStackTrace()
// Show exceptions in exception dialog.
runInFxApplicationThread {
// [showAndWait] need to be in the FX thread
ExceptionDialog(throwable).showAndWait()
System.exit(1) System.exit(1)
} }
super.start(stage)
// 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() 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)}")
}
// 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()
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()
} }
} }

View File

@ -1,9 +1,14 @@
package com.r3corda.explorer 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.explorer.views.TopLevel
import com.r3corda.node.services.config.configureTestSSL
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory
import jfxtras.resources.JFXtrasFontRoboto 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. * 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() { class MainWindow : View() {
private val toplevel: TopLevel by inject() private val toplevel: TopLevel by inject()
override val root = toplevel.root override val root = toplevel.root
private val loginView by inject<LoginView>()
init { init {
// Do this first before creating the notification bar, so it can autosize itself properly. // Do this first before creating the notification bar, so it can autosize itself properly.
loadFontsAndStyles() loadFontsAndStyles()
loginView.login { hostAndPort, username, password ->
Models.get<NodeMonitorModel>(MainWindow::class).register(hostAndPort, configureTestSSL(), username, password)
}
} }
private fun loadFontsAndStyles() { private fun loadFontsAndStyles() {

View File

@ -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
}
}

View File

@ -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))
}
}

View File

@ -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?>()
}

View File

@ -1,12 +1,22 @@
package com.r3corda.explorer.model package com.r3corda.explorer.model
import javafx.beans.property.SimpleObjectProperty import javafx.beans.property.SimpleObjectProperty
import javafx.scene.image.Image
enum class SelectedView { enum class SelectedView(val displayableName: String, val image: Image, val subviews: Array<SelectedView> = emptyArray()) {
Home, Home("Home", getImage("home.png")),
Cash, Transaction("Transaction", getImage("tx.png")),
Transaction, Setting("Setting", getImage("settings_lrg.png")),
NewTransaction 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 { class TopLevelModel {

View File

@ -1,135 +1,185 @@
package com.r3corda.explorer.views package com.r3corda.explorer.views
import com.r3corda.client.fxutils.* import com.r3corda.client.fxutils.*
import com.r3corda.client.model.ContractStateModel import com.r3corda.client.model.*
import com.r3corda.client.model.observableList
import com.r3corda.client.model.observableValue
import com.r3corda.contracts.asset.Cash import com.r3corda.contracts.asset.Cash
import com.r3corda.core.contracts.Amount import com.r3corda.core.contracts.Amount
import com.r3corda.core.contracts.StateAndRef import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.contracts.withoutIssuer import com.r3corda.core.contracts.withoutIssuer
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.explorer.formatters.AmountFormatter 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.ReportingCurrencyModel
import com.r3corda.explorer.model.SettingsModel import com.r3corda.explorer.model.SettingsModel
import com.r3corda.explorer.ui.* import com.r3corda.explorer.ui.*
import com.sun.javafx.collections.ObservableListWrapper
import javafx.application.Platform
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
import javafx.beans.value.ObservableValue import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections import javafx.collections.FXCollections
import javafx.collections.ObservableList import javafx.collections.ObservableList
import javafx.geometry.Insets
import javafx.scene.Node import javafx.scene.Node
import javafx.scene.Parent
import javafx.scene.chart.NumberAxis
import javafx.scene.control.* import javafx.scene.control.*
import javafx.scene.image.ImageView import javafx.scene.image.ImageView
import javafx.scene.input.MouseButton import javafx.scene.layout.BorderPane
import javafx.scene.input.MouseEvent
import javafx.scene.layout.HBox
import javafx.scene.layout.VBox import javafx.scene.layout.VBox
import org.fxmisc.easybind.EasyBind import org.fxmisc.easybind.EasyBind
import tornadofx.UIComponent import tornadofx.*
import tornadofx.View import java.time.Instant
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.* import java.util.*
sealed class FilterCriteria { class CashViewer : View(), CordaView {
abstract fun matches(string: String): Boolean // Inject UI elements.
override val root: BorderPane by fxml()
object All : FilterCriteria() { // View's widget.
override fun matches(string: String) = true 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 // Left pane
val leftPane: VBox by fxid() private val leftPane: VBox by fxid()
val searchCriteriaTextField: TextField by fxid() private val splitPane: SplitPane by fxid()
val searchCancelImageView: ImageView by fxid() private val totalMatchingLabel: Label by fxid()
val totalMatchingLabel: Label by fxid() private val cashViewerTable: TreeTableView<ViewerNode> by fxid()
val cashViewerTable: TreeTableView<ViewerNode> by fxid() private val cashViewerTableIssuerCurrency: TreeTableColumn<ViewerNode, String> by fxid()
val cashViewerTableIssuerCurrency: TreeTableColumn<ViewerNode, String> by fxid() private val cashViewerTableLocalCurrency: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid()
val cashViewerTableLocalCurrency: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid() private val cashViewerTableEquiv: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid()
val cashViewerTableEquiv: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid()
// Right pane // Right pane
val rightPane: VBox by fxid() private val rightPane: VBox by fxid()
val totalPositionsLabel: Label by fxid() private val totalPositionsLabel: Label by fxid()
val equivSumLabel: Label by fxid() private val cashStatesList: ListView<StateRow> by fxid()
val cashStatesList: ListView<StateRow> by fxid() private val toggleButton by fxid<Button>()
// Inject observables // Inject observables
val cashStates by observableList(ContractStateModel::cashStates) private val cashStates by observableList(ContractStateModel::cashStates)
val reportingCurrency: ObservableValue<Currency> by observableValue(SettingsModel::reportingCurrency) private val reportingCurrency by observableValue(SettingsModel::reportingCurrency)
val reportingExchange: ObservableValue<Pair<Currency, (Amount<Currency>) -> Amount<Currency>>> private val reportingExchange by observableValue(ReportingCurrencyModel::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. * This holds the data for each row in the TreeTable.
*/ */
sealed class ViewerNode { sealed class ViewerNode(val equivAmount: ObservableValue<out Amount<Currency>>,
object Root : ViewerNode() val states: ObservableList<StateAndRef<Cash.State>>) {
class IssuerNode( class IssuerNode(val issuer: Party,
val issuer: Party, sumEquivAmount: ObservableValue<out Amount<Currency>>,
val sumEquivAmount: ObservableValue<out Amount<Currency>>, states: ObservableList<StateAndRef<Cash.State>>) : ViewerNode(sumEquivAmount, states)
val states: ObservableList<StateAndRef<Cash.State>>
) : ViewerNode() class CurrencyNode(val amount: ObservableValue<Amount<Currency>>,
class CurrencyNode( equivAmount: ObservableValue<Amount<Currency>>,
val amount: ObservableValue<Amount<Currency>>, states: ObservableList<StateAndRef<Cash.State>>) : ViewerNode(equivAmount, states)
val equivAmount: ObservableValue<Amount<Currency>>,
val states: ObservableList<StateAndRef<Cash.State>>
) : ViewerNode()
} }
/**
* 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>)
/**
* A small class describing the graphics of a single state.
*/
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()
val issuerValueLabel: Label by fxid()
val originatedValueLabel: Label by fxid()
val amountValueLabel: Label by fxid()
val equivValueLabel: Label by fxid()
val equivAmount: ObservableValue<out Amount<Currency>> = reportingExchange.map {
it.second(stateRow.stateAndRef.state.data.amount.withoutIssuer())
}
init {
val amountNoIssuer = stateRow.stateAndRef.state.data.amount.withoutIssuer()
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() })
issuerValueLabel.text = stateRow.stateAndRef.state.data.amount.token.issuer.toString()
originatedValueLabel.text = stateRow.originated.toString()
amountValueLabel.text = amountFormatter.format(amountNoIssuer)
equivValueLabel.textProperty().bind(equivAmount.map { equivFormatter.format(it) })
}
}
// Wire up UI
init {
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 * 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 * one which produces more results, which seems to work, as the set of currency strings don't really overlap with
* issuer strings. * issuer strings.
*/ */
val searchField = SearchField(cashStates, arrayOf(
/** { state, text -> state.state.data.amount.token.product.toString().contains(text, true) },
* Holds the filtering criterion based on the input text { state, text -> state.state.data.amount.token.issuer.party.toString().contains(text, true) }
*/ ))
private val filterCriteria = searchCriteriaTextField.textProperty().map { text -> root.top = searchField.root
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. * This is where we aggregate the list of cash states into the TreeTable structure.
@ -138,7 +188,7 @@ class CashViewer : View() {
/** /**
* First we group the states based on the issuer. [memberStates] is all states holding currency issued by [issuer] * 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 -> 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. * 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. * Note that these states will not be displayed in the TreeTable, but rather in the side pane if the user clicks on the row.
@ -187,178 +237,70 @@ class CashViewer : View() {
treeItem treeItem
} }
/** cashViewerTable.apply() {
* Now we build up the Observables needed for the side pane, given that the user clicks on a row. root = TreeItem()
*/ val children: List<TreeItem<out ViewerNode>> = root.children
val selectedViewerNode = cashViewerTable.singleRowSelection() Bindings.bindContent(children, cashViewerIssueNodes)
root.isExpanded = true
/** isShowRoot = false
* 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>
)
/**
* 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")
val equivLabel: Label by fxid()
val stateIdValueLabel: Label by fxid()
val issuerValueLabel: Label by fxid()
val originatedValueLabel: Label by fxid()
val amountValueLabel: Label by fxid()
val equivValueLabel: Label by fxid()
val equivAmount: ObservableValue<out Amount<Currency>> = reportingExchange.map {
it.second(stateRow.stateAndRef.state.data.amount.withoutIssuer())
}
init {
val amountNoIssuer = stateRow.stateAndRef.state.data.amount.withoutIssuer()
val amountFormatter = AmountFormatter.boring
val equivFormatter = AmountFormatter.boring
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)
equivValueLabel.textProperty().bind(equivAmount.map { equivFormatter.format(it) })
}
}
/**
* 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(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 // TODO use smart resize
cashViewerTable.setColumnPrefWidthPolicy { tableWidthWithoutPaddingAndBorder, column -> setColumnPrefWidthPolicy { tableWidthWithoutPaddingAndBorder, column ->
Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / cashViewerTable.columns.size).toInt() Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / columns.size).toInt()
} }
}
val currencyCellFactory = AmountFormatter.boring.toTreeTableCellFactory<ViewerNode, Amount<Currency>>()
cashViewerTableIssuerCurrency.setCellValueFactory { cashViewerTableIssuerCurrency.setCellValueFactory {
val node = it.value.value val node = it.value.value
when (node) { when (node) {
ViewerNode.Root -> "".lift()
is ViewerNode.IssuerNode -> node.issuer.toString().lift() is ViewerNode.IssuerNode -> node.issuer.toString().lift()
is ViewerNode.CurrencyNode -> node.amount.map { it.token.toString() } is ViewerNode.CurrencyNode -> node.amount.map { it.token.toString() }
} }
} }
cashViewerTableLocalCurrency.setCellValueFactory { cashViewerTableLocalCurrency.apply {
setCellValueFactory {
val node = it.value.value val node = it.value.value
when (node) { when (node) {
ViewerNode.Root -> null.lift()
is ViewerNode.IssuerNode -> null.lift() is ViewerNode.IssuerNode -> null.lift()
is ViewerNode.CurrencyNode -> node.amount.map { it } is ViewerNode.CurrencyNode -> node.amount.map { it }
} }
} }
cashViewerTableLocalCurrency.cellFactory = cellFactory cellFactory = currencyCellFactory
/** /**
* We must set this, otherwise on sort an exception will be thrown, as it will try to compare Amounts of differing currency * 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 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 // Right Pane.
cashViewerTable.isShowRoot = false 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! // TODO Think about i18n!
totalMatchingLabel.textProperty().bind(Bindings.size(cashViewerIssueNodes).map { totalMatchingLabel.textProperty().bind(Bindings.size(cashViewerIssueNodes).map {
val plural = if (it == 1) "" else "s" val plural = if (it == 1) "" else "s"
"Total $it matching issuer$plural" "Total $it matching issuer$plural"
}) })
toggleButton.setOnAction {
cashViewerTable.selectionModel.clearSelection()
}
} }
} }

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -1,65 +1,31 @@
package com.r3corda.explorer.views 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.client.model.observableValue
import com.r3corda.explorer.model.SelectedView
import com.r3corda.explorer.model.TopLevelModel import com.r3corda.explorer.model.TopLevelModel
import javafx.beans.value.ObservableValue
import javafx.scene.control.Button
import javafx.scene.control.Label import javafx.scene.control.Label
import javafx.scene.image.Image import javafx.scene.control.SplitMenuButton
import javafx.scene.image.ImageView import javafx.scene.image.ImageView
import javafx.scene.layout.VBox import javafx.scene.layout.GridPane
import org.fxmisc.easybind.EasyBind
import tornadofx.View import tornadofx.View
class Header : 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 sectionLabel: Label by fxid()
private val debugNextButton: Button by fxid() private val userButton: SplitMenuButton by fxid()
private val debugGoStopButton: Button by fxid() private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
private val selectedView by observableValue(TopLevelModel::selectedView)
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")
init { init {
sectionLabel.textProperty().bind(EasyBind.map(selectedView) { sectionLabel.textProperty().bind(selectedView.map { it.displayableName })
when (it) { sectionLabel.graphicProperty().bind(selectedView.map {
SelectedView.Home -> "Home" ImageView(it.image).apply {
SelectedView.Cash -> "Cash" fitHeight = 30.0
SelectedView.Transaction -> "Transactions" fitWidth = 30.0
SelectedView.NewTransaction -> "New Transaction"
null -> "Home"
} }
}) })
userButton.textProperty().bind(myIdentity.map { it?.legalIdentity?.name })
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())
} }
} }

View File

@ -1,73 +1,55 @@
package com.r3corda.explorer.views package com.r3corda.explorer.views
import com.r3corda.client.fxutils.AmountBindings
import com.r3corda.client.fxutils.map import com.r3corda.client.fxutils.map
import com.r3corda.client.model.* import com.r3corda.client.model.GatheredTransactionData
import com.r3corda.contracts.asset.Cash import com.r3corda.client.model.GatheredTransactionDataModel
import com.r3corda.core.contracts.StateAndRef import com.r3corda.client.model.observableListReadOnly
import com.r3corda.core.contracts.withoutIssuer import com.r3corda.client.model.writableValue
import com.r3corda.explorer.formatters.AmountFormatter
import com.r3corda.explorer.model.SelectedView import com.r3corda.explorer.model.SelectedView
import com.r3corda.explorer.model.SettingsModel
import com.r3corda.explorer.model.TopLevelModel import com.r3corda.explorer.model.TopLevelModel
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
import javafx.beans.value.ObservableValue
import javafx.beans.value.WritableValue import javafx.beans.value.WritableValue
import javafx.collections.ObservableList import javafx.collections.ObservableList
import javafx.scene.Node
import javafx.scene.Parent
import javafx.scene.control.Label import javafx.scene.control.Label
import javafx.scene.control.TitledPane import javafx.scene.control.TitledPane
import javafx.scene.input.MouseButton import javafx.scene.input.MouseButton
import javafx.scene.input.MouseEvent
import javafx.scene.layout.TilePane import javafx.scene.layout.TilePane
import tornadofx.View import tornadofx.View
import java.util.* import tornadofx.find
class Home : View() { 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 ourCashPane: TitledPane by fxid()
private val ourCashLabel: Label by fxid()
private val ourTransactionsPane: TitledPane by fxid()
private val ourTransactionsLabel: Label 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 selectedView: WritableValue<SelectedView> by writableValue(TopLevelModel::selectedView)
private val cashStates: ObservableList<StateAndRef<Cash.State>> by observableList(ContractStateModel::cashStates)
private val gatheredTransactionDataList: ObservableList<out GatheredTransactionData> private val gatheredTransactionDataList: ObservableList<out GatheredTransactionData>
by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList) 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 { init {
val formatter = AmountFormatter.boring // TODO: register views in view model and populate the dashboard dynamically.
ourCashLabel.textProperty().bind(sumAmount.map { formatter.format(it) })
ourCashPane.setOnMouseClicked { clickEvent ->
if (clickEvent.button == MouseButton.PRIMARY) {
selectedView.value = SelectedView.Cash
}
}
ourTransactionsLabel.textProperty().bind( ourTransactionsLabel.textProperty().bind(
Bindings.size(gatheredTransactionDataList).map { it.toString() } Bindings.size(gatheredTransactionDataList).map { it.toString() }
) )
ourTransactionsPane.setOnMouseClicked { clickEvent ->
if (clickEvent.button == MouseButton.PRIMARY) { ourCashPane.apply {
selectedView.value = SelectedView.Transaction content = find(CashViewer::class).widget
} }
}
newTransaction.setOnMouseClicked { clickEvent -> tilePane.widthProperty().addListener { e ->
if (clickEvent.button == MouseButton.PRIMARY) { val prefWidth = 350
selectedView.value = SelectedView.NewTransaction 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)
}
} }
} }

View File

@ -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())
}
}

View File

@ -6,12 +6,9 @@ import com.r3corda.client.model.NodeMonitorModel
import com.r3corda.client.model.observableList import com.r3corda.client.model.observableList
import com.r3corda.client.model.observableValue import com.r3corda.client.model.observableValue
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.node.NodeInfo import com.r3corda.core.node.NodeInfo
import com.r3corda.core.serialization.OpaqueBytes import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.explorer.components.ExceptionDialog
import com.r3corda.explorer.model.CashTransaction 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.CordaRPCOps
import com.r3corda.node.services.messaging.TransactionBuildResult import com.r3corda.node.services.messaging.TransactionBuildResult
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
@ -22,8 +19,8 @@ import javafx.collections.ObservableList
import javafx.scene.Node import javafx.scene.Node
import javafx.scene.Parent import javafx.scene.Parent
import javafx.scene.control.* import javafx.scene.control.*
import javafx.util.StringConverter
import javafx.util.converter.BigDecimalStringConverter import javafx.util.converter.BigDecimalStringConverter
import org.controlsfx.dialog.ExceptionDialog
import tornadofx.View import tornadofx.View
import java.math.BigDecimal import java.math.BigDecimal
import java.util.* import java.util.*
@ -43,21 +40,20 @@ class NewTransaction : View() {
private val transactionTypeCB: ChoiceBox<CashTransaction> by fxid() private val transactionTypeCB: ChoiceBox<CashTransaction> by fxid()
private val amount: TextField by fxid() private val amount: TextField by fxid()
private val currency: ChoiceBox<Currency> 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 issueRefLabel: Label by fxid()
private val issueRefTextField: TextField 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 { private fun ObservableValue<*>.isNotNull(): BooleanBinding {
return Bindings.createBooleanBinding({ this.value != null }, arrayOf(this)) return Bindings.createBooleanBinding({ this.value != null }, arrayOf(this))
} }
fun resetScreen() { private fun resetScreen() {
partyBChoiceBox.valueProperty().set(null) partyBChoiceBox.valueProperty().set(null)
transactionTypeCB.valueProperty().set(null) transactionTypeCB.valueProperty().set(null)
currency.valueProperty().set(null) currency.valueProperty().set(null)
@ -66,28 +62,22 @@ class NewTransaction : View() {
init { init {
// Disable everything when not connected to node. // 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()) root.disableProperty().bind(enableProperty.not())
transactionTypeCB.items = FXCollections.observableArrayList(CashTransaction.values().asList()) transactionTypeCB.items = FXCollections.observableArrayList(CashTransaction.values().asList())
// Party A textfield always display my identity name, not editable. // Party A textfield always display my identity name, not editable.
partyATextField.isEditable = false 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 : " } }) partyALabel.textProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameA?.let { "$it : " } })
partyATextField.visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameA }.isNotNull()) partyATextField.visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameA }.isNotNull())
partyBLabel.textProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB?.let { "$it : " } }) partyBLabel.textProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB?.let { "$it : " } })
partyBChoiceBox.visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB }.isNotNull()) partyBChoiceBox.apply {
partyBChoiceBox.items = networkIdentities visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB }.isNotNull())
partyBChoiceBox.items = parties
partyBChoiceBox.converter = object : StringConverter<NodeInfo?>() { converter = stringConverter { it?.legalIdentity?.name ?: "" }
override fun toString(node: NodeInfo?): String {
return node?.legalIdentity?.name ?: ""
}
override fun fromString(string: String?): NodeInfo {
throw UnsupportedOperationException("not implemented")
}
} }
// BigDecimal text Formatter, restricting text box input to decimal values. // BigDecimal text Formatter, restricting text box input to decimal values.
@ -123,7 +113,8 @@ class NewTransaction : View() {
executeButton.setOnAction { event -> 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 !!. // 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 -> 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 -> rpcProxy.value?.let { rpcProxy ->
Triple(myIdentity, notary, rpcProxy) Triple(myIdentity, notary, rpcProxy)
} }
@ -131,13 +122,14 @@ class NewTransaction : View() {
}?.let { }?.let {
val (myIdentity, notary, rpcProxy) = it val (myIdentity, notary, rpcProxy) = it
transactionTypeCB.value?.let { 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 })) 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) { val command = when (it) {
CashTransaction.Issue -> ClientToServiceCommand.IssueCash(Amount(textFormatter.value, currency.value), issueRef, partyBChoiceBox.value.legalIdentity, notary) 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, issueRef), currency.value)), partyBChoiceBox.value.legalIdentity) 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) CashTransaction.Exit -> ClientToServiceCommand.ExitCash(Amount(textFormatter.value, currency.value), issueRef)
} }
val dialog = Alert(Alert.AlertType.INFORMATION).apply { val dialog = Alert(Alert.AlertType.INFORMATION).apply {
headerText = null headerText = null
contentText = "Transaction Started." contentText = "Transaction Started."
@ -165,8 +157,7 @@ class NewTransaction : View() {
dialog.close() dialog.close()
ExceptionDialog(it.source.exception).apply { ExceptionDialog(it.source.exception).apply {
initOwner((event.target as Node).scene.window) initOwner((event.target as Node).scene.window)
showAndWait() }.showAndWait()
}
} }
} }
} }

View File

@ -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 = ""
}
}
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -1,22 +1,29 @@
package com.r3corda.explorer.views package com.r3corda.explorer.views
import com.r3corda.client.fxutils.map
import com.r3corda.client.model.objectProperty import com.r3corda.client.model.objectProperty
import com.r3corda.explorer.model.SelectedView import com.r3corda.explorer.model.SelectedView
import com.r3corda.explorer.model.TopLevelModel import com.r3corda.explorer.model.TopLevelModel
import javafx.beans.property.ObjectProperty import javafx.beans.property.ObjectProperty
import javafx.scene.input.KeyCode import javafx.geometry.Pos
import javafx.scene.input.KeyEvent import javafx.scene.Parent
import javafx.scene.layout.BorderPane import javafx.scene.layout.BorderPane
import javafx.scene.layout.GridPane
import javafx.scene.layout.Pane
import javafx.scene.layout.Priority import javafx.scene.layout.Priority
import javafx.scene.layout.VBox import javafx.scene.text.TextAlignment
import org.fxmisc.easybind.EasyBind
import tornadofx.View import tornadofx.View
import tornadofx.add
import tornadofx.gridpane
import tornadofx.label
class TopLevel : View() { class TopLevel : View() {
override val root: VBox by fxml() override val root: Parent by fxml()
val selectionBorderPane: BorderPane by fxid() val selectionBorderPane: BorderPane by fxid()
val sidebarPane: Pane by fxid()
private val header: Header by inject() private val header: Header by inject()
private val sidebar: Sidebar by inject()
private val home: Home by inject() private val home: Home by inject()
private val cash: CashViewer by inject() private val cash: CashViewer by inject()
private val transaction: TransactionViewer by inject() private val transaction: TransactionViewer by inject()
@ -29,24 +36,28 @@ class TopLevel : View() {
private val transactionRoot = transaction.root private val transactionRoot = transaction.root
private val newTransactionRoot = newTransaction.root private val newTransactionRoot = newTransaction.root
private fun getView(selection: SelectedView) = when (selection) { val selectedView: ObjectProperty<SelectedView> by objectProperty(TopLevelModel::selectedView)
init {
selectionBorderPane.centerProperty().bind(selectedView.map {
when (it) {
SelectedView.Home -> homeRoot SelectedView.Home -> homeRoot
SelectedView.Cash -> cashRoot SelectedView.Cash -> cashRoot
SelectedView.Transaction -> transactionRoot SelectedView.Transaction -> transactionRoot
SelectedView.NewTransaction -> newTransactionRoot SelectedView.NewTransaction -> newTransactionRoot
} else -> gridpane {
val selectedView: ObjectProperty<SelectedView> by objectProperty(TopLevelModel::selectedView) label("Under Construction...") {
maxWidth = Double.MAX_VALUE
init { textAlignment = TextAlignment.CENTER
VBox.setVgrow(selectionBorderPane, Priority.ALWAYS) alignment = Pos.CENTER
selectionBorderPane.centerProperty().bind(EasyBind.map(selectedView) { getView(it) }) GridPane.setVgrow(this, Priority.ALWAYS)
GridPane.setHgrow(this, Priority.ALWAYS)
primaryStage.addEventHandler(KeyEvent.KEY_RELEASED) { keyEvent ->
if (keyEvent.code == KeyCode.ESCAPE) {
selectedView.value = SelectedView.Home
} }
} }
}
root.children.add(0, header.root) })
selectionBorderPane.center.styleClass.add("no-padding")
sidebarPane.add(sidebar.root)
selectionBorderPane.top = header.root
} }
} }

View File

@ -4,82 +4,43 @@ import com.r3corda.client.fxutils.*
import com.r3corda.client.model.* import com.r3corda.client.model.*
import com.r3corda.contracts.asset.Cash import com.r3corda.contracts.asset.Cash
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.protocols.StateMachineRunId import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.explorer.AmountDiff import com.r3corda.explorer.AmountDiff
import com.r3corda.explorer.formatters.AmountFormatter import com.r3corda.explorer.formatters.AmountFormatter
import com.r3corda.explorer.formatters.Formatter import com.r3corda.explorer.identicon.identicon
import com.r3corda.explorer.formatters.NumberFormatter import com.r3corda.explorer.identicon.identiconToolTip
import com.r3corda.explorer.model.IdentityModel
import com.r3corda.explorer.model.ReportingCurrencyModel import com.r3corda.explorer.model.ReportingCurrencyModel
import com.r3corda.explorer.sign import com.r3corda.explorer.sign
import com.r3corda.explorer.ui.* import com.r3corda.explorer.ui.setCustomCellFactory
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
import javafx.beans.value.ObservableValue import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.geometry.Insets import javafx.geometry.Insets
import javafx.scene.Node import javafx.scene.Parent
import javafx.scene.control.* 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.Background
import javafx.scene.layout.BackgroundFill import javafx.scene.layout.BackgroundFill
import javafx.scene.layout.BorderPane
import javafx.scene.layout.CornerRadii import javafx.scene.layout.CornerRadii
import javafx.scene.layout.VBox
import javafx.scene.paint.Color import javafx.scene.paint.Color
import tornadofx.View import tornadofx.*
import java.security.PublicKey import java.security.PublicKey
import java.util.* import java.util.*
class TransactionViewer: View() { class TransactionViewer : View() {
override val root: VBox by fxml() override val root by fxml<BorderPane>()
private val transactionViewTable by fxid<TableView<ViewerNode>>()
val topSplitPane: SplitPane by fxid() private val matchingTransactionsLabel by fxid<Label>()
// 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()
// Inject data // Inject data
private val gatheredTransactionDataList: ObservableList<out GatheredTransactionData> private val gatheredTransactionDataList by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList) private val reportingExchange by observableValue(ReportingCurrencyModel::reportingExchange)
private val reportingExchange: ObservableValue<Pair<Currency, (Amount<Currency>) -> Amount<Currency>>> private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
by observableValue(ReportingCurrencyModel::reportingExchange)
private val myIdentity: ObservableValue<Party?> by observableValue(IdentityModel::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 * 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 stateMachineStatus: ObservableValue<out StateMachineStatus?>,
val protocolStatus: ObservableValue<out ProtocolStatus?>, val protocolStatus: ObservableValue<out ProtocolStatus?>,
val commandTypes: Collection<Class<CommandData>>, 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 }, stateMachineStatus = stateMachineProperty { it.stateMachineStatus },
commandTypes = it.transaction.transaction.tx.commands.map { it.value.javaClass }, commandTypes = it.transaction.transaction.tx.commands.map { it.value.javaClass },
totalValueEquiv = { totalValueEquiv = {
val resolvedInputs = it.transaction.inputs.sequence().map { resolution -> val resolvedInputs = it.transaction.inputs.sequence()
when (resolution) { .map { (it as? PartiallyResolvedTransaction.InputResolution.Resolved)?.stateAndRef?.state }
is PartiallyResolvedTransaction.InputResolution.Unresolved -> null .filterNotNull().toList().lift()
is PartiallyResolvedTransaction.InputResolution.Resolved -> resolution.stateAndRef
}
}.fold(listOf()) { inputs: List<StateAndRef<ContractState>>?, state: StateAndRef<ContractState>? ->
if (inputs != null && state != null) {
inputs + state
} else {
null
}
}
::calculateTotalEquiv.lift( ::calculateTotalEquiv.lift(
myIdentity, myIdentity,
reportingExchange, reportingExchange,
resolvedInputs.lift(), resolvedInputs,
it.transaction.transaction.tx.outputs.lift() 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 { 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 // Transaction table
Bindings.bindContent(transactionViewTable.items, viewerNodes) transactionViewTable.apply {
items = searchField.filteredData
transactionViewTable.setColumnPrefWidthPolicy { tableWidthWithoutPaddingAndBorder, column -> column("Transaction ID", ViewerNode::transactionId).setCustomCellFactory {
Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / transactionViewTable.columns.size).toInt() label("$it".substring(0, 16) + "...") {
graphic = imageview {
image = identicon(it, 5.0)
}
tooltip = identiconToolTip(it)
}
}
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
}
} }
transactionViewTransactionId.setCellValueFactory { "${it.value.transactionId}".lift() } matchingTransactionsLabel.textProperty().bind(Bindings.size(transactionViewTable.items).map {
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)
}
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)
}
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 {
"$it matching transaction${if (it == 1) "" else "s"}" "$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 * We calculate the total value by subtracting relevant input states and adding relevant output states, as long as they're cash
*/ */
private fun calculateTotalEquiv( private fun calculateTotalEquiv(identity: NodeInfo?,
identity: Party?,
reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>, reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>,
inputs: List<StateAndRef<ContractState>>?, inputs: List<TransactionState<ContractState>>,
outputs: List<TransactionState<ContractState>>): AmountDiff<Currency>? { outputs: List<TransactionState<ContractState>>): AmountDiff<Currency> {
if (inputs == null) {
return null
}
var sum = 0L
val (reportingCurrency, exchange) = reportingCurrencyExchange val (reportingCurrency, exchange) = reportingCurrencyExchange
val publicKey = identity?.owningKey val publicKey = identity?.legalIdentity?.owningKey
inputs.forEach { fun List<TransactionState<ContractState>>.sum(): Long {
val contractState = it.state.data return this.map { it.data as? Cash.State }
if (contractState is Cash.State && publicKey == contractState.owner) { .filterNotNull()
sum -= exchange(contractState.amount.withoutIssuer()).quantity .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)
} }

View File

@ -1,8 +1,8 @@
#topLevel.root { #topLevel.root {
-fx-background-image:url('../images/r3bg.png'); -fx-background-image: url('../images/r3bg.png');
-fx-background-size: cover; -fx-background-size: cover;
-fx-background-repeat:no-repeat; -fx-background-repeat: no-repeat;
-fx-base:white; -fx-base: white;
} }
#cashViewer { #cashViewer {
@ -17,29 +17,23 @@
-fx-background-color: transparent; -fx-background-color: transparent;
} }
.root { .expand-row {
-fx-padding:5px; -fx-padding: 10;
}
.root {
-fx-padding:5px;
} }
.dialog-pane { .dialog-pane {
-fx-background-color:rgba(255,255,255,0.7); -fx-background-color: rgba(255, 255, 255, 0.7);
-fx-background-radius:2px; -fx-background-radius: 2px;
-fx-border-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 { .nested-column-header, .nested-column-header {
-fx-background-color:transparent; -fx-background-color: transparent;
-fx-wrap-text:true; -fx-wrap-text: true;
-fx-border-color:transparent; -fx-border-color: transparent;
} }
.text-field, .text-field,
.table-column, .table-column,
.label, .label,
@ -48,28 +42,27 @@
.button, .button,
.split-menu-button, .split-menu-button,
.choice-box { .choice-box {
-fx-font-family:Effra; -fx-font-family: Effra;
-fx-font-size:1em; -fx-font-size: 1em;
-fx-text-fill:rgb(63,63,63); -fx-text-fill: rgb(63, 63, 63);
-fx-font-smoothing-type: gray; -fx-font-smoothing-type: gray;
} }
.text-highlight { .text-highlight {
-fx-text-fill:rgb(20,136,204); -fx-text-fill: rgb(20, 136, 204);
} }
.context-menu { .context-menu {
-fx-background-color:rgba(255,255,255,0.9); -fx-background-color: rgba(255, 255, 255, 0.9);
} }
.titled-pane .content, .titled-pane .content,
.split-menu-button .label, .split-menu-button .label,
.split-menu-button .arrow-button, .split-menu-button .arrow-button,
.titled-pane .split-pane, .scroll-pane { .titled-pane .split-pane, .scroll-pane {
-fx-background-color:transparent; -fx-background-color: transparent;
} }
.text-field, .text-field,
.tree-table-view, .tree-table-view,
.table-view, .table-view,
@ -80,8 +73,8 @@
.split-menu-button, .split-menu-button,
.choice-box, .choice-box,
.titled-pane .title { .titled-pane .title {
-fx-border-color:rgb(150,150,150); -fx-border-color: rgb(150, 150, 150);
-fx-border-width:1px; -fx-border-width: 1px;
} }
@ -98,82 +91,116 @@
.split-menu-button:hover, .split-menu-button:hover,
.choice-box:hover, .choice-box:hover,
.titled-pane:hover .title { .titled-pane:hover .title {
-fx-border-color:rgb(20,136,204); -fx-border-color: rgb(20, 136, 204);
} }
.split-menu-button:pressed, .split-menu-button:pressed,
.button:pressed, .button:pressed,
.choice-box:pressed, .choice-box:pressed,
.titled-pane:expanded .title { .titled-pane:expanded .title {
-fx-background-color:rgb(20,136,204); -fx-background-color: rgb(20, 136, 204);
-fx-text-fill:white; -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 { .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 { .text-field, .combo-box, .choice-box, .password-field {
-fx-background-color:rgba(255,255,255,0.5); -fx-background-color: rgba(255, 255, 255, 0.5);
-fx-background-radius:2px; -fx-background-radius: 2px;
-fx-border-radius: 2px; -fx-border-radius: 2px;
} }
/* switch off highlighting for text-field when it's inside a combo-box */ /* 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 { .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, .column-header-background,
.table-column, .table-column,
.tree-table-row-cell, .column-header-background .filler { .tree-table-row-cell, .column-header-background .filler {
-fx-background-color:transparent; -fx-background-color: transparent;
-fx-label-padding:3px; -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-width: 1px;
-fx-border-color:rgb(216,216,216); /*t r b l */ -fx-border-insets: 1px;
-fx-border-width:0.5px;
-fx-border-insets: 1.5px;
-fx-background-insets: 2px; -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 { .tree-table-row-cell:even .table-column,
-fx-background-color:transparent; .table-row-cell:even .table-column,
} .title, .split-menu-button, .button {
.tree-table-row-cell:selected .table-column, -fx-background-color: rgba(20, 136, 204, 0.2);
.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);
.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 { .bad .text {
-fx-fill:rgb(236,29,36); -fx-fill: rgb(236, 29, 36);
} }
.table-row-cell:focused .table-column .text, .table-row-cell:focused .table-column .text,
.tree-table-row-cell:focused .table-column .text { .tree-table-row-cell:focused .table-column .text {
-fx-fill:white; -fx-fill: white;
} }
.table-column:hover, .table-column:hover,
@ -181,220 +208,239 @@
.table-row-cell:hover .second-column, .table-row-cell:hover .second-column,
.tree-table-row-cell:hover .first-column, .tree-table-row-cell:hover .first-column,
.tree-table-row-cell:hover .second-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, .tree-table-view .column-header-background .nested-column-header .table-column,
.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 */ /* Special formatting - columns to be presented with no join between them */
.first-column { .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-border-insets: 1.5px 0px 1.5px 1.5px;
-fx-background-insets: 2px 0px 2px 2px; -fx-background-insets: 2px 0px 2px 2px;
} }
.second-column { .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-border-insets: 1.5px 1.5px 1.5px 0px;
-fx-background-insets: 2px 2px 2px 0px; -fx-background-insets: 2px 2px 2px 0px;
} }
/* highlighting where the user has typed a key */ /* highlighting where the user has typed a key */
.tree-table-view text-area, .table-view text-area{ .tree-table-view text-area, .table-view text-area {
-fx-font-weight:bold; -fx-font-weight: bold;
-fx-fill:rgb(20,136,204); -fx-fill: rgb(20, 136, 204);
} }
.tree-table-row-cell:selected .table-column text-area, .tree-table-row-cell:selected .table-column text-area,
.table-row-cell:selected .table-column text-area .table-row-cell:selected .table-column text-area {
{ -fx-font-weight: bold;
-fx-font-weight:bold; -fx-fill: rgb(255, 255, 255);
-fx-fill:rgb(255,255,255);
} }
/* labels */ /* labels */
.dialog-pane .header-panel .label .text{ .dialog-pane .header-panel .label .text {
-fx-font-size:1em; -fx-font-size: 1em;
-fx-fill:rgb(20,136,204); -fx-fill: rgb(20, 136, 204);
} }
#headline, .headline { #headline, .headline {
-fx-font-size:2.4em; -fx-font-size: 2.4em;
} }
#subline, .subline { #subline, .subline {
-fx-font-size:1.4em; -fx-font-size: 1.4em;
} }
#headline, #subline { #headline, #subline {
-fx-text-fill:rgb(65,65,65); -fx-text-fill: rgb(65, 65, 65);
-fx-padding:0px; -fx-padding: 0px;
} }
/* search boxes */ /* search boxes */
.search { .search {
-fx-background-image:url('../images/search.png'); -fx-background-image: url('../images/search.png');
-fx-background-size:Auto 16px; -fx-background-size: Auto 16px;
-fx-background-repeat:no-repeat; -fx-background-repeat: no-repeat;
-fx-background-position:8px center; -fx-background-position: 8px center;
-fx-padding:5px 5px 5px 30px; -fx-padding: 5px 5px 5px 30px;
-fx-background-radius: 2px; -fx-background-radius: 2px;
-fx-border-radius: 2px; -fx-border-radius: 2px;
} }
.search-clear { .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 { .search-clear:hover {
-fx-image:url('../images/clear.png'); -fx-image: url('../images/clear.png');
} }
.split-menu-button, .button, .choice-box { .split-menu-button, .button, .choice-box {
-fx-background-radius:2px; -fx-background-radius: 2px;
-fx-border-radius: 2px; -fx-border-radius: 2px;
-fx-border-insets: 0.5px; -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 { .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 panes */
.split-pane-divider { .split-pane-divider {
-fx-background-color: transparent; -fx-background-color: transparent;
-fx-border-color: rgb(160,160,160); -fx-border-color: rgb(160, 160, 160);
-fx-border-width: 0 0 0 0.5px -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 */ /* Dashboard tiles */
.tile, .tile-user {
.tile,.tile-user {
-fx-padding: 10px; -fx-padding: 10px;
-fx-pref-height:200px; -fx-pref-width:200px; -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 .title, .tile:expanded .title,
.tile-user .title, .tile-user:expanded .title { .tile-user .title, .tile-user:expanded .title {
-fx-alignment:center-right; -fx-alignment: center-left;
-fx-font-size:1.4em; -fx-font-size: 1.4em;
-fx-font-weight:bold; -fx-font-weight: bold;
-fx-cursor:hand; -fx-cursor: hand;
-fx-background-radius:2px 2px 0 0; -fx-background-color: rgba(183, 210, 228, 0.2);
-fx-border-radius: 2px 2px 0 0; -fx-border-color: transparent; /*t r b l */
-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 */
} }
.tile .title .text, .tile:expanded .title .text, .tile .title .text, .tile:expanded .title .text,
.tile-user .title .text, .tile-user: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 .content,
.tile-user .content { .tile-user .content {
-fx-background-color: rgba(255,255,255,0.7); -fx-background-color: rgba(183, 210, 228, 0.2);
-fx-background-size:Auto 90%; -fx-background-size: Auto 90%;
-fx-background-repeat:no-repeat; -fx-background-repeat: no-repeat;
-fx-background-position:center center; -fx-background-position: center center;
-fx-cursor:hand; -fx-cursor: hand;
-fx-background-radius:0 0 2px 2px; -fx-padding: 0px;
-fx-border-radius: 0 0 2px 2px; -fx-alignment: bottom-right;
-fx-padding:0px; -fx-border-color: transparent; /*t r b l */
-fx-alignment:bottom-right;
-fx-border-color:rgb(150,150,150); /*t r b l */
} }
.tile .label, .tile .label,
.tile-user .label { .tile-user .label {
-fx-font-size:2.4em; -fx-font-size: 2.4em;
-fx-padding:20px; -fx-padding: 20px;
-fx-text-fill:rgb(65,65,65); -fx-text-fill: rgb(65, 65, 65);
-fx-font-weight:normal; -fx-font-weight: normal;
-fx-text-alignment:right; -fx-text-alignment: right;
} }
.tile:hover .label, .tile:hover,
.tile-user:hover .label {
-fx-padding:24px;
}
.tile:hover .content, .tile:hover .title,
.tile-user:hover .content, .tile-user:hover .title { .tile-user:hover .content, .tile-user:hover .title {
-fx-border-color:rgb(20,136,204); -fx-border-color: rgb(20, 136, 204);
-fx-background-color: rgb(20,136,204); -fx-border-width: 2;
}
.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;
} }
#tile_cash .content { #tile_cash .content {
-fx-background-image:url('../images/cash_lrg.png'); -fx-background-image: url('../images/cash_lrg.png');
} }
#tile_debtors .content { #tile_debtors .content {
-fx-background-image:url('../images/outflow_lrg.png'); -fx-background-image: url('../images/outflow_lrg.png');
} }
#tile_creditors .content { #tile_creditors .content {
-fx-background-image:url('../images/inflow_lrg.png'); -fx-background-image: url('../images/inflow_lrg.png');
} }
#tile_tx .content { #tile_tx .content {
-fx-background-image:url('../images/tx_lrg.png'); -fx-background-image: url('../images/tx_lrg.png');
} }
#tile_cpty .content { #tile_cpty .content {
-fx-background-image:url('../images/cpty_lrg.png'); -fx-background-image: url('../images/cpty_lrg.png');
} }
.tile-user .content { .tile-user .content {
-fx-background-image:url('../images/user_b.png'); -fx-background-image: url('../images/user_b.png');
} }
.tile-user-test-man .content { .tile-user-test-man .content {
-fx-background-image:url('../images/man1.png'); -fx-background-image: url('../images/man1.png');
-fx-background-size:cover; -fx-background-size: cover;
} }
.tile-user-test-woman .content { .tile-user-test-woman .content {
-fx-background-image:url('../images/woman1.png'); -fx-background-image: url('../images/woman1.png');
-fx-background-size:cover; -fx-background-size: cover;
} }
.tile-user .label { .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 { .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 { .counterparty {
-fx-background-image:url('../images/inst_128.png'); -fx-background-image: url('../images/inst_128.png');
-fx-background-size:Auto 16px; -fx-background-size: Auto 16px;
-fx-background-repeat: no-repeat; -fx-background-repeat: no-repeat;
-fx-background-position:0px center; -fx-background-position: 0px center;
-fx-padding:0 0 0 20px; -fx-padding: 0 0 0 20px;
} }
.state-panel{ .state-panel {
-fx-background-color: rgba(255,255,255,0.7); -fx-background-color: rgba(255, 255, 255, 0.7);
-fx-border-color:rgb(150,150,150); -fx-border-color: rgb(150, 150, 150);
-fx-insets:5px -fx-insets: 5px
} }

View File

@ -1,28 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.*?>
<?import javafx.scene.layout.VBox?> <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"> <Label fx:id="stateIdValueLabel" text="39043-329090-390091" GridPane.columnIndex="1"/>
<children> <Label fx:id="issuerValueLabel" styleClass="counterparty" text="C-03820 HSBC GROUP PLC" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
<VBox HBox.hgrow="SOMETIMES"> <Label fx:id="originatedValueLabel" text="2018-04-27 11:34 UTC" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
<children> <Label fx:id="amountValueLabel" text="GBP 0.00" wrapText="true" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
<Label text="State ID" VBox.vgrow="ALWAYS" /> <Label fx:id="equivValueLabel" text="0.00" wrapText="true" GridPane.columnIndex="1" GridPane.rowIndex="4"/>
<Label text="Issuer" /> </GridPane>
<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>

View File

@ -1,84 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.String?>
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?> <?import javafx.scene.control.*?>
<?import javafx.scene.control.Label?> <?import javafx.scene.layout.*?>
<?import javafx.scene.control.ListView?> <BorderPane stylesheets="@../css/wallet.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<?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> <padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets right="10" left="5" bottom="5" top="5"/>
</padding> </padding>
</TextField> <center>
<ImageView fx:id="searchCancelImageView" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear"> <SplitPane fx:id="splitPane" dividerPositions="0.5">
<image> <VBox fx:id="leftPane" spacing="5.0">
<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"> <TreeTableView fx:id="cashViewerTable" showRoot="false" VBox.vgrow="ALWAYS">
<columns> <columns>
<TreeTableColumn fx:id="cashViewerTableIssuerCurrency" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefWidth="100.0" styleClass="first-column" text="Issuer/Currency" /> <TreeTableColumn fx:id="cashViewerTableIssuerCurrency" styleClass="first-column" text="Issuer/Currency"/>
<TreeTableColumn fx:id="cashViewerTableLocalCurrency" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefWidth="132.0" text="Local currency"> <TreeTableColumn fx:id="cashViewerTableLocalCurrency" text="Local currency" styleClass="monetary-value, second-column"/>
<styleClass> <TreeTableColumn fx:id="cashViewerTableEquiv" styleClass="monetary-value" text="Equiv"/>
<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> </columns>
</TreeTableView> </TreeTableView>
</children> <Label fx:id="totalMatchingLabel" text="Total 15 matching issuer(s)"/>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</VBox> </VBox>
<VBox fx:id="rightPane" spacing="5.0"> <VBox fx:id="rightPane" spacing="5.0">
<children> <Button fx:id="toggleButton" mnemonicParsing="false" text="&gt;&gt;"/>
<Button mnemonicParsing="false" text="&gt;&gt;" /> <ListView fx:id="cashStatesList" VBox.vgrow="ALWAYS"/>
<Label fx:id="totalPositionsLabel" styleClass="subline" text="Total 18 position(s)" /> <Label fx:id="totalPositionsLabel" 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> </VBox>
</items> </SplitPane>
</SplitPane> </center>
</BorderPane>

View File

@ -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>

View File

@ -1,95 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.control.MenuItem?> <?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.SplitMenuButton?> <?import javafx.scene.control.SplitMenuButton?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.image.Image?> <?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?> <?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.StackPane?> <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">
<?import javafx.scene.layout.VBox?>
<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> <padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets left="10.0" right="10.0" top="5.0" bottom="5.0"/>
</padding> </padding>
</HBox> <!-- Row 1 -->
<HBox alignment="TOP_RIGHT" spacing="5.0"> <Label id="headline" fx:id="sectionLabel" alignment="BOTTOM_CENTER" maxHeight="Infinity" text="Home" GridPane.columnIndex="0" GridPane.hgrow="ALWAYS"/>
<children>
<Button fx:id="debugNextButton" mnemonicParsing="false" text="Next" /> <SplitMenuButton fx:id="userButton" maxHeight="Infinity" mnemonicParsing="false" text="DRUTTER" GridPane.columnIndex="3">
<Button fx:id="debugGoStopButton" mnemonicParsing="false" text="!!!" />
<SplitMenuButton mnemonicParsing="false" text="DRUTTER">
<items> <items>
<MenuItem mnemonicParsing="false" text="Sign out" /> <MenuItem mnemonicParsing="false" text="Sign out"/>
<MenuItem mnemonicParsing="false" text="Account settings..." /> <MenuItem mnemonicParsing="false" text="Account settings..."/>
</items> </items>
<graphic> <graphic>
<ImageView fitHeight="20.0" fitWidth="52.0" pickOnBounds="true" preserveRatio="true"> <ImageView fitHeight="20.0" fitWidth="20.0" pickOnBounds="true" preserveRatio="true">
<image> <Image url="@../images/user_w.png"/>
<Image url="@../../images/user_w.png" />
</image>
</ImageView> </ImageView>
</graphic> </graphic>
</SplitMenuButton> </SplitMenuButton>
<Button fx:id="settingsButton" mnemonicParsing="false" text="Settings"> <!--<Button fx:id="settingsButton" maxHeight="Infinity" mnemonicParsing="false" text="Settings" GridPane.columnIndex="4">
<graphic> <graphic>
<ImageView fitHeight="20.0" fitWidth="20.0" pickOnBounds="true" preserveRatio="true"> <ImageView fitHeight="20.0" fitWidth="20.0" pickOnBounds="true" preserveRatio="true">
<image> <Image url="@../images/settings_w.png"/>
<Image url="@../../images/settings_w.png" />
</image>
</ImageView> </ImageView>
</graphic> </graphic>
</Button> </Button>-->
</children>
</HBox> <!--&lt;!&ndash; Row 2 &ndash;&gt;
</children> <StackPane alignment="CENTER_RIGHT" GridPane.columnSpan="5" GridPane.rowIndex="1">
</HBox> <TextField fx:id="search_main" promptText="Search for states, transactions, counterparties etc." styleClass="search"/>
<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"> <ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
<image>
<Image url="@../../images/clear_inactive.png" />
</image>
<StackPane.margin> <StackPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" /> <Insets right="10.0"/>
</StackPane.margin> </StackPane.margin>
</ImageView> </ImageView>
</children> </StackPane>-->
</StackPane> </GridPane>
</children>
</VBox>
<StackPane alignment="CENTER_RIGHT" />
</children>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</VBox>

View File

@ -2,19 +2,21 @@
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.TilePane?> <?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"> <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">
<TitledPane id="tile_cash" fx:id="ourCashPane" alignment="CENTER" collapsible="false" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our cash"> <TilePane fx:id="tilePane" tileAlignment="TOP_LEFT" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<Label fx:id="ourCashLabel" text="USD 186.7m" textAlignment="CENTER" wrapText="true"/> <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>
<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"> <TitledPane id="tile_debtors" fx:id="ourDebtorsPane" collapsible="false" styleClass="tile" text="Our debtors">
<Label text="USD 71.3m" textAlignment="CENTER" wrapText="true"/> <Label text="USD 71.3m" textAlignment="CENTER" wrapText="true" />
</TitledPane> </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"> <TitledPane id="tile_creditors" fx:id="ourCreditorsPane" collapsible="false" styleClass="tile" text="Our creditors">
<Label text="USD (29.4m)" textAlignment="CENTER" wrapText="true"/> <Label text="USD (29.4m)" textAlignment="CENTER" wrapText="true" />
</TitledPane> </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"> <TitledPane id="Transaction" fx:id="ourTransactionsPane" collapsible="false" onMouseClicked="#changeView" styleClass="tile" text="Our transactions">
<Label fx:id="ourTransactionsLabel" text="In flight: 1,315" textAlignment="CENTER" wrapText="true"/> <Label fx:id="ourTransactionsLabel" textAlignment="CENTER" wrapText="true" />
</TitledPane> </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 id="NewTransaction" fx:id="newTransaction" collapsible="false" onMouseClicked="#changeView" styleClass="tile" text="New Transaction">
</TitledPane> </TitledPane>
</TilePane> </TilePane>
</ScrollPane>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,8 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.layout.BorderPane?> <?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.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">
<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"> <SplitPane dividerPositions="0.0" styleClass="no-padding, split-pane-divider">
<BorderPane fx:id="selectionBorderPane"/> <VBox fx:id="sidebarPane" maxWidth="200.0" minWidth="80" styleClass="sidebar" SplitPane.resizableWithParent="false"/>
</VBox> <BorderPane fx:id="selectionBorderPane" maxHeight="Infinity" minWidth="400"/>
</SplitPane>
</StackPane>

View File

@ -1,137 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.String?>
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?> <?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.TableView?>
<?import javafx.scene.control.TextField?> <?import javafx.scene.layout.BorderPane?>
<?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.VBox?> <?import javafx.scene.layout.VBox?>
<BorderPane stylesheets="@../css/wallet.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<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> <padding>
<Insets bottom="5.0" left="30.0" right="5.0" top="5.0" /> <Insets top="5" left="5" right="10" bottom="5"/>
</padding> </padding>
<StackPane.margin> <center>
<Insets bottom="5.0" top="5.0" /> <TableView fx:id="transactionViewTable" VBox.vgrow="ALWAYS">
</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> <columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" /> <TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/>
</columnResizePolicy> </columnResizePolicy>
</TableView> </TableView>
<TitledPane fx:id="contractStatesTitledPane" animated="false" text="Contract states"> </center>
<content> <bottom>
<SplitPane dividerPositions="0.5" prefHeight="160.0" prefWidth="200.0"> <Label fx:id="matchingTransactionsLabel" text="matching transaction(s)"/>
<items> </bottom>
<VBox prefHeight="200.0" prefWidth="100.0"> </BorderPane>
<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>

View File

@ -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>

View File

@ -77,6 +77,10 @@ class CordaRPCOpsImpl(
} }
} }
override fun nodeIdentity(): NodeInfo {
return services.myInfo
}
override fun addVaultTransactionNote(txnId: SecureHash, txnNote: String) { override fun addVaultTransactionNote(txnId: SecureHash, txnNote: String) {
return databaseTransaction(database) { return databaseTransaction(database) {
services.vaultService.addNoteToTransaction(txnId, txnNote) services.vaultService.addNoteToTransaction(txnId, txnNote)

View File

@ -118,6 +118,10 @@ interface CordaRPCOps : RPCOps {
*/ */
fun executeCommand(command: ClientToServiceCommand): TransactionBuildResult 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 * Add note(s) to an existing Vault transaction
*/ */

View File

@ -16,10 +16,8 @@ import com.r3corda.core.crypto.*
import com.r3corda.core.node.NodeInfo import com.r3corda.core.node.NodeInfo
import com.r3corda.core.node.PhysicalLocation import com.r3corda.core.node.PhysicalLocation
import com.r3corda.core.node.ServiceEntry import com.r3corda.core.node.ServiceEntry
import com.r3corda.core.node.services.NetworkMapCache import com.r3corda.core.node.WorldCoordinate
import com.r3corda.core.node.services.ServiceInfo import com.r3corda.core.node.services.*
import com.r3corda.core.node.services.StateMachineTransactionMapping
import com.r3corda.core.node.services.Vault
import com.r3corda.core.protocols.StateMachineRunId import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.serialization.* import com.r3corda.core.serialization.*
import com.r3corda.core.transactions.SignedTransaction import com.r3corda.core.transactions.SignedTransaction
@ -101,6 +99,7 @@ fun requirePermission(permission: String) {
*/ */
open class RPCException(msg: String, cause: Throwable?) : RuntimeException(msg, cause) { open class RPCException(msg: String, cause: Throwable?) : RuntimeException(msg, cause) {
constructor(msg: String) : this(msg, null) constructor(msg: String) : this(msg, null)
class DeadlineExceeded(rpcName: String) : RPCException("Deadline exceeded on call to $rpcName") 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) 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(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. // Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway.
register(IllegalArgumentException::class.java) register(IllegalArgumentException::class.java)
// Kryo couldn't serialize Collections.unmodifiableCollection in Throwable correctly, causing null pointer exception when try to access the deserialize object. // Kryo couldn't serialize Collections.unmodifiableCollection in Throwable correctly, causing null pointer exception when try to access the deserialize object.

View File

@ -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.Companion.SUBSCRIPTION_PROTOCOL_TOPIC
import com.r3corda.node.services.network.NetworkMapService.FetchMapResponse import com.r3corda.node.services.network.NetworkMapService.FetchMapResponse
import com.r3corda.node.services.network.NetworkMapService.SubscribeResponse import com.r3corda.node.services.network.NetworkMapService.SubscribeResponse
import com.r3corda.node.services.transactions.SimpleNotaryService
import com.r3corda.node.utilities.AddOrRemove import com.r3corda.node.utilities.AddOrRemove
import com.r3corda.protocols.sendRequest import com.r3corda.protocols.sendRequest
import rx.Observable import rx.Observable
@ -59,13 +58,7 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
override fun track(): Pair<List<NodeInfo>, Observable<MapChange>> { override fun track(): Pair<List<NodeInfo>, Observable<MapChange>> {
synchronized(_changed) { synchronized(_changed) {
fun NodeInfo.isCordaService(): Boolean { return Pair(partyNodes, _changed.bufferUntilSubscribed())
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())
} }
} }

View File

@ -1,6 +1,11 @@
package com.r3corda.node.services package com.r3corda.node.services
import com.google.common.net.HostAndPort 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.crypto.generateKeyPair
import com.r3corda.core.messaging.Message import com.r3corda.core.messaging.Message
import com.r3corda.core.messaging.createMessage import com.r3corda.core.messaging.createMessage