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

View File

@ -10,9 +10,7 @@ import javafx.collections.ObservableList
import javafx.collections.ObservableMap
import javafx.collections.transformation.FilteredList
import org.fxmisc.easybind.EasyBind
import org.slf4j.LoggerFactory
import java.util.function.Predicate
import kotlin.concurrent.thread
/**
* Here follows utility extension functions that help reduce the visual load when developing RX code. Each function should
@ -276,4 +274,4 @@ fun <A> ObservableList<A>.last(): ObservableValue<A?> {
null
}
}, arrayOf(this))
}
}

View File

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

View File

@ -1,16 +1,18 @@
package com.r3corda.client.model
import com.r3corda.client.fxutils.foldToObservableList
import com.r3corda.client.fxutils.map
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.node.services.NetworkMapCache
import com.r3corda.node.services.network.NetworkMapService
import javafx.collections.ObservableList
import kotlinx.support.jdk8.collections.removeIf
import rx.Observable
import java.security.PublicKey
class NetworkIdentityModel {
private val networkIdentityObservable: Observable<NetworkMapCache.MapChange> by observable(NodeMonitorModel::networkMap)
private val networkIdentityObservable by observable(NodeMonitorModel::networkMap)
val networkIdentities: ObservableList<NodeInfo> =
private val networkIdentities: ObservableList<NodeInfo> =
networkIdentityObservable.foldToObservableList(Unit) { update, _accumulator, observableList ->
observableList.removeIf {
when (update.type) {
@ -21,4 +23,18 @@ class NetworkIdentityModel {
}
observableList.addAll(update.node)
}
private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable)
val parties: ObservableList<NodeInfo> = networkIdentities.filtered { !it.isCordaService() }
val notaries: ObservableList<NodeInfo> = networkIdentities.filtered { it.advertisedServices.any { it.info.type.isNotary() } }
val myIdentity = rpcProxy.map { it?.nodeIdentity() }
private fun NodeInfo.isCordaService(): Boolean {
return advertisedServices.any { it.info.type == NetworkMapService.type || it.info.type.isNotary() }
}
fun lookup(publicKey: PublicKey): NodeInfo? {
return parties.firstOrNull { it.legalIdentity.owningKey == publicKey } ?: notaries.firstOrNull { it.notaryIdentity.owningKey == publicKey }
}
}

View File

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

View File

@ -25,8 +25,6 @@ apply plugin: 'kotlin'
apply plugin: 'application'
sourceCompatibility = 1.8
applicationDefaultJvmArgs = ["-javaagent:${rootProject.configurations.quasar.singleFile}"]
mainClassName = 'com.r3corda.explorer.Main'
sourceSets {
@ -53,7 +51,7 @@ dependencies {
testCompile group: 'junit', name: 'junit', version: '4.11'
// TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's.
compile 'no.tornado:tornadofx:1.5.1'
compile 'no.tornado:tornadofx:1.5.6'
// Corda Core: Data structures and basic types needed to work with Corda.
compile project(':core')
@ -74,4 +72,12 @@ dependencies {
// Humanize: formatting
compile 'com.github.mfornos:humanize-icu:1.2.2'
// Controls FX: more java FX components http://fxexperience.com/controlsfx/
compile 'org.controlsfx:controlsfx:8.40.12'
}
task(runDemoNodes, dependsOn: 'classes', type: JavaExec) {
main = 'com.r3corda.explorer.MainKt'
classpath = sourceSets.main.runtimeClasspath
}

View File

@ -1,60 +1,72 @@
package com.r3corda.explorer
import com.r3corda.client.mock.EventGenerator
import com.r3corda.client.model.Models
import com.r3corda.client.model.NodeMonitorModel
import com.r3corda.core.node.services.ServiceInfo
import com.r3corda.explorer.model.IdentityModel
import com.r3corda.explorer.views.runInFxApplicationThread
import com.r3corda.node.driver.PortAllocation
import com.r3corda.node.driver.driver
import com.r3corda.node.services.config.configureTestSSL
import com.r3corda.node.services.config.FullNodeConfiguration
import com.r3corda.node.services.messaging.ArtemisMessagingComponent
import com.r3corda.node.services.transactions.SimpleNotaryService
import javafx.stage.Stage
import org.controlsfx.dialog.ExceptionDialog
import tornadofx.App
import java.util.*
/**
* Main class for Explorer, you will need Tornado FX to run the explorer.
*/
class Main : App() {
override val primaryView = MainWindow::class
override fun start(stage: Stage) {
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
throwable.printStackTrace()
System.exit(1)
// Show exceptions in exception dialog.
runInFxApplicationThread {
// [showAndWait] need to be in the FX thread
ExceptionDialog(throwable).showAndWait()
System.exit(1)
}
}
super.start(stage)
}
}
/**
* This main method will starts 3 nodes (Notary, Alice and Bob) locally for UI testing, they will be on localhost:20002, 20004, 20006 respectively.
*/
fun main(args: Array<String>) {
val portAllocation = PortAllocation.Incremental(20000)
driver(portAllocation = portAllocation) {
val notary = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
val alice = startNode("Alice")
val bob = startNode("Bob")
val notaryNode = notary.get()
val aliceNode = alice.get()
val bobNode = bob.get()
arrayOf(notaryNode, aliceNode, bobNode).forEach {
println("${it.nodeInfo.legalIdentity} started on ${ArtemisMessagingComponent.toHostAndPort(it.nodeInfo.address)}")
}
super.start(stage)
// Register with alice to use alice's RPC proxy to create random events.
Models.get<NodeMonitorModel>(Main::class).register(ArtemisMessagingComponent.toHostAndPort(aliceNode.nodeInfo.address), FullNodeConfiguration(aliceNode.config), "user1", "test")
val rpcProxy = Models.get<NodeMonitorModel>(Main::class).proxyObservable.get()
// start the driver on another thread
// TODO Change this to connecting to an actual node (specified on cli/in a config) once we're happy with the code
Thread({
val portAllocation = PortAllocation.Incremental(20000)
driver(portAllocation = portAllocation) {
val aliceNodeFuture = startNode("Alice")
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
val aliceNode = aliceNodeFuture.get().nodeInfo
val notaryNode = notaryNodeFuture.get().nodeInfo
Models.get<IdentityModel>(Main::class).notary.set(notaryNode.notaryIdentity)
Models.get<IdentityModel>(Main::class).myIdentity.set(aliceNode.legalIdentity)
Models.get<NodeMonitorModel>(Main::class).register(aliceNode, configureTestSSL(), "user1", "test")
startNode("Bob").get()
/* for (i in 0 .. 10000) {
Thread.sleep(500)
val eventGenerator = EventGenerator(
parties = listOf(aliceNode.legalIdentity),
notary = notaryNode.notaryIdentity
)
eventGenerator.clientToServiceCommandGenerator.map { command ->
aliceOutStream.onNext(command)
}.generate(Random())
}*/
waitForAllNodesToFinish()
}
}).start()
for (i in 0..10000) {
Thread.sleep(500)
val eventGenerator = EventGenerator(
parties = listOf(aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity),
notary = notaryNode.nodeInfo.notaryIdentity
)
eventGenerator.clientToServiceCommandGenerator.map { command ->
rpcProxy?.executeCommand(command)
}.generate(Random())
}
waitForAllNodesToFinish()
}
}

View File

@ -1,9 +1,14 @@
package com.r3corda.explorer
import com.r3corda.client.model.Models
import com.r3corda.client.model.NodeMonitorModel
import com.r3corda.explorer.views.LoginView
import com.r3corda.explorer.views.TopLevel
import com.r3corda.node.services.config.configureTestSSL
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory
import jfxtras.resources.JFXtrasFontRoboto
import tornadofx.*
import tornadofx.View
import tornadofx.importStylesheet
/**
* The root view embeds the [Shell] and provides support for the status bar, and modal dialogs.
@ -11,10 +16,14 @@ import tornadofx.*
class MainWindow : View() {
private val toplevel: TopLevel by inject()
override val root = toplevel.root
private val loginView by inject<LoginView>()
init {
// Do this first before creating the notification bar, so it can autosize itself properly.
loadFontsAndStyles()
loginView.login { hostAndPort, username, password ->
Models.get<NodeMonitorModel>(MainWindow::class).register(hostAndPort, configureTestSSL(), username, password)
}
}
private fun loadFontsAndStyles() {
@ -23,4 +32,4 @@ class MainWindow : View() {
FontAwesomeIconFactory.get() // Force initialisation.
root.styleClass += "root"
}
}
}

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
import javafx.beans.property.SimpleObjectProperty
import javafx.scene.image.Image
enum class SelectedView {
Home,
Cash,
Transaction,
NewTransaction
enum class SelectedView(val displayableName: String, val image: Image, val subviews: Array<SelectedView> = emptyArray()) {
Home("Home", getImage("home.png")),
Transaction("Transaction", getImage("tx.png")),
Setting("Setting", getImage("settings_lrg.png")),
NewTransaction("New Transaction", getImage("cash.png")),
Cash("Cash", getImage("cash.png"), arrayOf(Transaction, NewTransaction)),
NetworkMap("Network Map", getImage("cash.png")),
Vault("Vault", getImage("cash.png"), arrayOf(Cash)),
Network("Network", getImage("inst.png"), arrayOf(NetworkMap, Transaction))
}
private fun getImage(imageName: String): Image {
val basePath = "/com/r3corda/explorer/images"
return Image("$basePath/$imageName")
}
class TopLevelModel {

View File

@ -1,212 +1,142 @@
package com.r3corda.explorer.views
import com.r3corda.client.fxutils.*
import com.r3corda.client.model.ContractStateModel
import com.r3corda.client.model.observableList
import com.r3corda.client.model.observableValue
import com.r3corda.client.model.*
import com.r3corda.contracts.asset.Cash
import com.r3corda.core.contracts.Amount
import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.contracts.withoutIssuer
import com.r3corda.core.crypto.Party
import com.r3corda.explorer.formatters.AmountFormatter
import com.r3corda.explorer.identicon.identicon
import com.r3corda.explorer.identicon.identiconToolTip
import com.r3corda.explorer.model.ReportingCurrencyModel
import com.r3corda.explorer.model.SettingsModel
import com.r3corda.explorer.ui.*
import com.sun.javafx.collections.ObservableListWrapper
import javafx.application.Platform
import javafx.beans.binding.Bindings
import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.geometry.Insets
import javafx.scene.Node
import javafx.scene.Parent
import javafx.scene.chart.NumberAxis
import javafx.scene.control.*
import javafx.scene.image.ImageView
import javafx.scene.input.MouseButton
import javafx.scene.input.MouseEvent
import javafx.scene.layout.HBox
import javafx.scene.layout.BorderPane
import javafx.scene.layout.VBox
import org.fxmisc.easybind.EasyBind
import tornadofx.UIComponent
import tornadofx.View
import tornadofx.*
import java.time.Instant
import java.time.LocalDateTime
import java.util.*
sealed class FilterCriteria {
abstract fun matches(string: String): Boolean
class CashViewer : View(), CordaView {
// Inject UI elements.
override val root: BorderPane by fxml()
object All : FilterCriteria() {
override fun matches(string: String) = true
// View's widget.
override val viewName = "Cash"
override val widget: Node = vbox {
padding = Insets(0.0, 10.0, 0.0, 0.0)
val xAxis = NumberAxis().apply {
//isAutoRanging = true
isMinorTickVisible = false
isForceZeroInRange = false
tickLabelFormatter = stringConverter {
Instant.ofEpochMilli(it.toLong()).atZone(TimeZone.getDefault().toZoneId()).toLocalTime().toString()
}
}
val yAxis = NumberAxis().apply {
isAutoRanging = true
isMinorTickVisible = false
isForceZeroInRange = false
tickLabelFormatter = stringConverter { it.toStringWithSuffix(0) }
}
linechart(null, xAxis, yAxis) {
series("USD") {
runAsync {
while (true) {
Thread.sleep(1000)
Platform.runLater {
// Modify data in UI thread.
if (data.size > 300) data.remove(0, 1)
data(System.currentTimeMillis(), sumAmount.value.quantity)
}
}
}
}
createSymbols = false
animated = false
}
}
class FilterString(val filterString: String) : FilterCriteria() {
override fun matches(string: String) = string.contains(filterString)
}
}
class CashViewer : View() {
// Inject UI elements
override val root: SplitPane by fxml()
val topSplitPane: SplitPane by fxid()
// Left pane
val leftPane: VBox by fxid()
val searchCriteriaTextField: TextField by fxid()
val searchCancelImageView: ImageView by fxid()
val totalMatchingLabel: Label by fxid()
val cashViewerTable: TreeTableView<ViewerNode> by fxid()
val cashViewerTableIssuerCurrency: TreeTableColumn<ViewerNode, String> by fxid()
val cashViewerTableLocalCurrency: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid()
val cashViewerTableEquiv: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid()
private val leftPane: VBox by fxid()
private val splitPane: SplitPane by fxid()
private val totalMatchingLabel: Label by fxid()
private val cashViewerTable: TreeTableView<ViewerNode> by fxid()
private val cashViewerTableIssuerCurrency: TreeTableColumn<ViewerNode, String> by fxid()
private val cashViewerTableLocalCurrency: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid()
private val cashViewerTableEquiv: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid()
// Right pane
val rightPane: VBox by fxid()
val totalPositionsLabel: Label by fxid()
val equivSumLabel: Label by fxid()
val cashStatesList: ListView<StateRow> by fxid()
private val rightPane: VBox by fxid()
private val totalPositionsLabel: Label by fxid()
private val cashStatesList: ListView<StateRow> by fxid()
private val toggleButton by fxid<Button>()
// Inject observables
val cashStates by observableList(ContractStateModel::cashStates)
val reportingCurrency: ObservableValue<Currency> by observableValue(SettingsModel::reportingCurrency)
val reportingExchange: ObservableValue<Pair<Currency, (Amount<Currency>) -> Amount<Currency>>>
by observableValue(ReportingCurrencyModel::reportingExchange)
private val cashStates by observableList(ContractStateModel::cashStates)
private val reportingCurrency by observableValue(SettingsModel::reportingCurrency)
private val reportingExchange by observableValue(ReportingCurrencyModel::reportingExchange)
private val exchangeRate: ObservableValue<ExchangeRate> by observableValue(ExchangeRateModel::exchangeRate)
private val sumAmount = AmountBindings.sumAmountExchange(
cashStates.map { it.state.data.amount.withoutIssuer() },
reportingCurrency,
exchangeRate
)
private val selectedNode = cashViewerTable.singleRowSelection().map {
when (it) {
is SingleRowSelection.Selected -> it.node
else -> null
}
}
private val view = ChosenList(selectedNode.map {
when (it) {
null -> FXCollections.observableArrayList(leftPane)
else -> FXCollections.observableArrayList(leftPane, rightPane)
}
})
/**
* This holds the data for each row in the TreeTable.
*/
sealed class ViewerNode {
object Root : ViewerNode()
class IssuerNode(
val issuer: Party,
val sumEquivAmount: ObservableValue<out Amount<Currency>>,
val states: ObservableList<StateAndRef<Cash.State>>
) : ViewerNode()
class CurrencyNode(
val amount: ObservableValue<Amount<Currency>>,
val equivAmount: ObservableValue<Amount<Currency>>,
val states: ObservableList<StateAndRef<Cash.State>>
) : ViewerNode()
sealed class ViewerNode(val equivAmount: ObservableValue<out Amount<Currency>>,
val states: ObservableList<StateAndRef<Cash.State>>) {
class IssuerNode(val issuer: Party,
sumEquivAmount: ObservableValue<out Amount<Currency>>,
states: ObservableList<StateAndRef<Cash.State>>) : ViewerNode(sumEquivAmount, states)
class CurrencyNode(val amount: ObservableValue<Amount<Currency>>,
equivAmount: ObservableValue<Amount<Currency>>,
states: ObservableList<StateAndRef<Cash.State>>) : ViewerNode(equivAmount, states)
}
/**
* We allow filtering by both issuer and currency. We do this by filtering by both at the same time and picking the
* one which produces more results, which seems to work, as the set of currency strings don't really overlap with
* issuer strings.
*/
/**
* Holds the filtering criterion based on the input text
*/
private val filterCriteria = searchCriteriaTextField.textProperty().map { text ->
if (text.isBlank()) {
FilterCriteria.All
} else {
FilterCriteria.FilterString(text)
}
}
/**
* Filter cash states based on issuer.
*/
private val issueFilteredCashStates = cashStates.filter(filterCriteria.map { criteria ->
{ state: StateAndRef<Cash.State> ->
criteria.matches(state.state.data.amount.token.issuer.party.toString())
}
})
/**
* Now filter cash states based on currency.
*/
private val currencyFilteredCashStates = cashStates.filter(filterCriteria.map { criteria ->
{ state: StateAndRef<Cash.State> ->
criteria.matches(state.state.data.amount.token.product.toString())
}
})
/**
* Now we pick which one to use.
*/
private val filteredCashStates = ChosenList(filterCriteria.map {
if (issueFilteredCashStates.size > currencyFilteredCashStates.size) {
issueFilteredCashStates
} else {
currencyFilteredCashStates
}
})
/**
* This is where we aggregate the list of cash states into the TreeTable structure.
*/
val cashViewerIssueNodes: ObservableList<TreeItem<out ViewerNode.IssuerNode>> =
/**
* First we group the states based on the issuer. [memberStates] is all states holding currency issued by [issuer]
*/
AggregatedList(filteredCashStates, { it.state.data.amount.token.issuer.party }) { issuer, memberStates ->
/**
* Next we create subgroups based on currency. [memberStates] here is all states holding currency [currency] issued by [issuer] above.
* Note that these states will not be displayed in the TreeTable, but rather in the side pane if the user clicks on the row.
*/
val currencyNodes = AggregatedList(memberStates, { it.state.data.amount.token.product }) { currency, memberStates ->
/**
* We sum the states in the subgroup, to be displayed in the "Local Currency" column
*/
val amounts = memberStates.map { it.state.data.amount.withoutIssuer() }
val sumAmount = amounts.foldObservable(Amount(0, currency), Amount<Currency>::plus)
/**
* We exchange the sum to the reporting currency, to be displayed in the "<currency> Equiv" column.
*/
val equivSumAmount = EasyBind.combine(sumAmount, reportingExchange) { sum, exchange ->
exchange.second(sum)
}
/**
* Finally assemble the actual TreeTable Currency node.
*/
TreeItem(ViewerNode.CurrencyNode(sumAmount, equivSumAmount, memberStates))
}
/**
* Now that we have all nodes per currency, we sum the exchanged amounts, to be displayed in the
* "<currency> Equiv" column, this time on the issuer level.
*/
val equivAmounts = currencyNodes.map { it.value.equivAmount }.flatten()
val equivSumAmount = reportingCurrency.bind { currency ->
equivAmounts.foldObservable(Amount(0, currency), Amount<Currency>::plus)
}
/**
* Assemble the Issuer node.
*/
val treeItem = TreeItem(ViewerNode.IssuerNode(issuer, equivSumAmount, memberStates))
/**
* Bind the children in the TreeTable structure.
*
* TODO Perhaps we shouldn't do this here, but rather have a generic way of binding nodes to the treetable once.
*/
treeItem.isExpanded = true
val children: List<TreeItem<out ViewerNode.IssuerNode>> = treeItem.children
Bindings.bindContent(children, currencyNodes)
treeItem
}
/**
* Now we build up the Observables needed for the side pane, given that the user clicks on a row.
*/
val selectedViewerNode = cashViewerTable.singleRowSelection()
/**
* Holds data for a single state, to be displayed in the list in the side pane.
*/
data class StateRow (
val originated: LocalDateTime,
val stateAndRef: StateAndRef<Cash.State>
)
data class StateRow(val originated: LocalDateTime, val stateAndRef: StateAndRef<Cash.State>)
/**
* A small class describing the graphics of a single state.
*/
inner class StateRowGraphic(
val stateRow: StateRow
) : UIComponent() {
override val root: HBox by fxml("CashStateViewer.fxml")
inner class StateRowGraphic(val stateRow: StateRow) : UIComponent() {
override val root: Parent by fxml("CashStateViewer.fxml")
val equivLabel: Label by fxid()
val stateIdValueLabel: Label by fxid()
@ -224,8 +154,12 @@ class CashViewer : View() {
val amountFormatter = AmountFormatter.boring
val equivFormatter = AmountFormatter.boring
stateIdValueLabel.apply {
text = stateRow.stateAndRef.ref.toString().substring(0, 16) + "...[${stateRow.stateAndRef.ref.index}]"
graphic = ImageView(identicon(stateRow.stateAndRef.ref.txhash, 10.0))
tooltip = identiconToolTip(stateRow.stateAndRef.ref.txhash)
}
equivLabel.textProperty().bind(equivAmount.map { it.token.currencyCode.toString() })
stateIdValueLabel.text = stateRow.stateAndRef.ref.toString()
issuerValueLabel.text = stateRow.stateAndRef.state.data.amount.token.issuer.toString()
originatedValueLabel.text = stateRow.originated.toString()
amountValueLabel.text = amountFormatter.format(amountNoIssuer)
@ -233,132 +167,140 @@ class CashViewer : View() {
}
}
/**
* The list of states related to the current selection. If none or the root is selected it's empty, if an issuer or
* currency node is selected it's the relevant states.
*/
private val noSelectionStates = FXCollections.observableArrayList<StateAndRef<Cash.State>>()
private val selectedViewerNodeStates = ChosenList(selectedViewerNode.map { selection ->
when (selection) {
is SingleRowSelection.None -> noSelectionStates
is SingleRowSelection.Selected ->
when (selection.node) {
CashViewer.ViewerNode.Root -> noSelectionStates
is CashViewer.ViewerNode.IssuerNode -> selection.node.states
is CashViewer.ViewerNode.CurrencyNode -> selection.node.states
}
}
})
/**
* We re-display the exchanged sum amount, if we have a selection.
*/
private val noSelectionSumEquiv = reportingCurrency.map { Amount(0, it) }
private val selectedViewerNodeSumEquiv = selectedViewerNode.bindOut { selection ->
when (selection) {
is SingleRowSelection.None -> noSelectionSumEquiv
is SingleRowSelection.Selected ->
when (selection.node) {
ViewerNode.Root -> noSelectionSumEquiv
is ViewerNode.IssuerNode -> selection.node.sumEquivAmount
is ViewerNode.CurrencyNode -> selection.node.equivAmount
}
}
}
/**
* We add some extra timestamp data here to the selected states.
*
* TODO update this once we have actual timestamps.
*/
private val stateRows = selectedViewerNodeStates.map { StateRow(LocalDateTime.now(), it) }
/**
* We only display the right pane if a node is selected in the TreeTable.
*/
private val onlyLeftPaneShown = FXCollections.observableArrayList<Node>(leftPane)
private val bothPanesShown = FXCollections.observableArrayList<Node>(leftPane, rightPane)
private val panesShown = ChosenList<Node>(selectedViewerNode.map {
when (it) {
is SingleRowSelection.None -> onlyLeftPaneShown
is SingleRowSelection.Selected -> bothPanesShown
}
})
// Wire up UI
init {
searchCancelImageView.setOnMouseClicked { event: MouseEvent ->
if (event.button == MouseButton.PRIMARY) {
searchCriteriaTextField.text = ""
Bindings.bindContent(splitPane.items, view)
/**
* We allow filtering by both issuer and currency. We do this by filtering by both at the same time and picking the
* one which produces more results, which seems to work, as the set of currency strings don't really overlap with
* issuer strings.
*/
val searchField = SearchField(cashStates, arrayOf(
{ state, text -> state.state.data.amount.token.product.toString().contains(text, true) },
{ state, text -> state.state.data.amount.token.issuer.party.toString().contains(text, true) }
))
root.top = searchField.root
/**
* This is where we aggregate the list of cash states into the TreeTable structure.
*/
val cashViewerIssueNodes: ObservableList<TreeItem<out ViewerNode.IssuerNode>> =
/**
* First we group the states based on the issuer. [memberStates] is all states holding currency issued by [issuer]
*/
AggregatedList(searchField.filteredData, { it.state.data.amount.token.issuer.party }) { issuer, memberStates ->
/**
* Next we create subgroups based on currency. [memberStates] here is all states holding currency [currency] issued by [issuer] above.
* Note that these states will not be displayed in the TreeTable, but rather in the side pane if the user clicks on the row.
*/
val currencyNodes = AggregatedList(memberStates, { it.state.data.amount.token.product }) { currency, memberStates ->
/**
* We sum the states in the subgroup, to be displayed in the "Local Currency" column
*/
val amounts = memberStates.map { it.state.data.amount.withoutIssuer() }
val sumAmount = amounts.foldObservable(Amount(0, currency), Amount<Currency>::plus)
/**
* We exchange the sum to the reporting currency, to be displayed in the "<currency> Equiv" column.
*/
val equivSumAmount = EasyBind.combine(sumAmount, reportingExchange) { sum, exchange ->
exchange.second(sum)
}
/**
* Finally assemble the actual TreeTable Currency node.
*/
TreeItem(ViewerNode.CurrencyNode(sumAmount, equivSumAmount, memberStates))
}
/**
* Now that we have all nodes per currency, we sum the exchanged amounts, to be displayed in the
* "<currency> Equiv" column, this time on the issuer level.
*/
val equivAmounts = currencyNodes.map { it.value.equivAmount }.flatten()
val equivSumAmount = reportingCurrency.bind { currency ->
equivAmounts.foldObservable(Amount(0, currency), Amount<Currency>::plus)
}
/**
* Assemble the Issuer node.
*/
val treeItem = TreeItem(ViewerNode.IssuerNode(issuer, equivSumAmount, memberStates))
/**
* Bind the children in the TreeTable structure.
*
* TODO Perhaps we shouldn't do this here, but rather have a generic way of binding nodes to the treetable once.
*/
treeItem.isExpanded = true
val children: List<TreeItem<out ViewerNode.IssuerNode>> = treeItem.children
Bindings.bindContent(children, currencyNodes)
treeItem
}
cashViewerTable.apply() {
root = TreeItem()
val children: List<TreeItem<out ViewerNode>> = root.children
Bindings.bindContent(children, cashViewerIssueNodes)
root.isExpanded = true
isShowRoot = false
// TODO use smart resize
setColumnPrefWidthPolicy { tableWidthWithoutPaddingAndBorder, column ->
Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / columns.size).toInt()
}
}
Bindings.bindContent(topSplitPane.items, panesShown)
totalPositionsLabel.textProperty().bind(Bindings.size(selectedViewerNodeStates).map {
val plural = if (it == 1) "" else "s"
"Total $it position$plural"
})
val equivSumLabelFormatter = AmountFormatter.boring
equivSumLabel.textProperty().bind(selectedViewerNodeSumEquiv.map {
equivSumLabelFormatter.format(it)
})
Bindings.bindContent(cashStatesList.items, stateRows)
cashStatesList.setCustomCellFactory { StateRowGraphic(it).root }
val cellFactory = AmountFormatter.boring.toTreeTableCellFactory<ViewerNode, Amount<Currency>>()
// TODO use smart resize
cashViewerTable.setColumnPrefWidthPolicy { tableWidthWithoutPaddingAndBorder, column ->
Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / cashViewerTable.columns.size).toInt()
}
val currencyCellFactory = AmountFormatter.boring.toTreeTableCellFactory<ViewerNode, Amount<Currency>>()
cashViewerTableIssuerCurrency.setCellValueFactory {
val node = it.value.value
when (node) {
ViewerNode.Root -> "".lift()
is ViewerNode.IssuerNode -> node.issuer.toString().lift()
is ViewerNode.CurrencyNode -> node.amount.map { it.token.toString() }
}
}
cashViewerTableLocalCurrency.setCellValueFactory {
val node = it.value.value
when (node) {
ViewerNode.Root -> null.lift()
is ViewerNode.IssuerNode -> null.lift()
is ViewerNode.CurrencyNode -> node.amount.map { it }
cashViewerTableLocalCurrency.apply {
setCellValueFactory {
val node = it.value.value
when (node) {
is ViewerNode.IssuerNode -> null.lift()
is ViewerNode.CurrencyNode -> node.amount.map { it }
}
}
cellFactory = currencyCellFactory
/**
* We must set this, otherwise on sort an exception will be thrown, as it will try to compare Amounts of differing currency
*/
isSortable = false
}
cashViewerTableLocalCurrency.cellFactory = cellFactory
/**
* We must set this, otherwise on sort an exception will be thrown, as it will try to compare Amounts of differing currency
*/
cashViewerTableLocalCurrency.isSortable = false
cashViewerTableEquiv.setCellValueFactory {
val node = it.value.value
when (node) {
ViewerNode.Root -> null.lift()
is ViewerNode.IssuerNode -> node.sumEquivAmount.map { it }
is ViewerNode.CurrencyNode -> node.equivAmount.map { it }
cashViewerTableEquiv.apply {
setCellValueFactory {
it.value.value.equivAmount.map { it }
}
cellFactory = currencyCellFactory
textProperty().bind(reportingCurrency.map { "$it Equiv" })
}
cashViewerTableEquiv.cellFactory = cellFactory
cashViewerTableEquiv.textProperty().bind(reportingCurrency.map { "$it Equiv" })
cashViewerTable.root = TreeItem(ViewerNode.Root)
val children: List<TreeItem<out ViewerNode>> = cashViewerTable.root.children
Bindings.bindContent(children, cashViewerIssueNodes)
cashViewerTable.root.isExpanded = true
cashViewerTable.isShowRoot = false
// Right Pane.
totalPositionsLabel.textProperty().bind(cashStatesList.itemsProperty().map {
val plural = if (it.size == 1) "" else "s"
"Total ${it.size} position$plural"
})
cashStatesList.apply {
// TODO update this once we have actual timestamps.
itemsProperty().bind(selectedNode.map { it?.states?.map { StateRow(LocalDateTime.now(), it) } ?: ObservableListWrapper(emptyList()) })
setCustomCellFactory { StateRowGraphic(it).root }
}
// TODO Think about i18n!
totalMatchingLabel.textProperty().bind(Bindings.size(cashViewerIssueNodes).map {
val plural = if (it == 1) "" else "s"
"Total $it matching issuer$plural"
})
toggleButton.setOnAction {
cashViewerTable.selectionModel.clearSelection()
}
}
}

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
import com.r3corda.client.fxutils.map
import com.r3corda.client.model.NetworkIdentityModel
import com.r3corda.client.model.observableValue
import com.r3corda.explorer.model.SelectedView
import com.r3corda.explorer.model.TopLevelModel
import javafx.beans.value.ObservableValue
import javafx.scene.control.Button
import javafx.scene.control.Label
import javafx.scene.image.Image
import javafx.scene.control.SplitMenuButton
import javafx.scene.image.ImageView
import javafx.scene.layout.VBox
import org.fxmisc.easybind.EasyBind
import javafx.scene.layout.GridPane
import tornadofx.View
class Header : View() {
override val root: VBox by fxml()
override val root: GridPane by fxml()
private val sectionIcon: ImageView by fxid()
private val sectionIconContainer: VBox by fxid()
private val sectionLabel: Label by fxid()
private val debugNextButton: Button by fxid()
private val debugGoStopButton: Button by fxid()
private val selectedView: ObservableValue<SelectedView> by observableValue(TopLevelModel::selectedView)
private val homeImage = Image("/com/r3corda/explorer/images/home.png")
private val cashImage = Image("/com/r3corda/explorer/images/cash.png")
private val transactionImage = Image("/com/r3corda/explorer/images/tx.png")
private val userButton: SplitMenuButton by fxid()
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
private val selectedView by observableValue(TopLevelModel::selectedView)
init {
sectionLabel.textProperty().bind(EasyBind.map(selectedView) {
when (it) {
SelectedView.Home -> "Home"
SelectedView.Cash -> "Cash"
SelectedView.Transaction -> "Transactions"
SelectedView.NewTransaction -> "New Transaction"
null -> "Home"
sectionLabel.textProperty().bind(selectedView.map { it.displayableName })
sectionLabel.graphicProperty().bind(selectedView.map {
ImageView(it.image).apply {
fitHeight = 30.0
fitWidth = 30.0
}
})
sectionIcon.imageProperty().bind(EasyBind.map(selectedView) {
when (it) {
SelectedView.Home -> homeImage
SelectedView.Cash -> cashImage
SelectedView.Transaction -> transactionImage
SelectedView.NewTransaction -> cashImage
null -> homeImage
}
})
// JavaFX bugs and doesn't invalidate the wrapping Box's height if the icon fit height is first set to
// unbounded (0.0) - which is what the label's height is initially, so we set it to 1.0 instead
val secionLabelHeightNonZero = EasyBind.map(sectionLabel.heightProperty()) {
if (it == 0.0) {
1.0
} else {
it.toDouble()
}
}
sectionIconContainer.minWidthProperty().bind(secionLabelHeightNonZero)
sectionIcon.fitWidthProperty().bind(secionLabelHeightNonZero)
sectionIcon.fitHeightProperty().bind(sectionIcon.fitWidthProperty())
userButton.textProperty().bind(myIdentity.map { it?.legalIdentity?.name })
}
}

View File

@ -1,73 +1,55 @@
package com.r3corda.explorer.views
import com.r3corda.client.fxutils.AmountBindings
import com.r3corda.client.fxutils.map
import com.r3corda.client.model.*
import com.r3corda.contracts.asset.Cash
import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.contracts.withoutIssuer
import com.r3corda.explorer.formatters.AmountFormatter
import com.r3corda.client.model.GatheredTransactionData
import com.r3corda.client.model.GatheredTransactionDataModel
import com.r3corda.client.model.observableListReadOnly
import com.r3corda.client.model.writableValue
import com.r3corda.explorer.model.SelectedView
import com.r3corda.explorer.model.SettingsModel
import com.r3corda.explorer.model.TopLevelModel
import javafx.beans.binding.Bindings
import javafx.beans.value.ObservableValue
import javafx.beans.value.WritableValue
import javafx.collections.ObservableList
import javafx.scene.Node
import javafx.scene.Parent
import javafx.scene.control.Label
import javafx.scene.control.TitledPane
import javafx.scene.input.MouseButton
import javafx.scene.input.MouseEvent
import javafx.scene.layout.TilePane
import tornadofx.View
import java.util.*
import tornadofx.find
class Home : View() {
override val root: TilePane by fxml()
override val root: Parent by fxml()
private val tilePane: TilePane by fxid()
private val ourCashPane: TitledPane by fxid()
private val ourCashLabel: Label by fxid()
private val ourTransactionsPane: TitledPane by fxid()
private val ourTransactionsLabel: Label by fxid()
private val newTransaction: TitledPane by fxid()
private val selectedView: WritableValue<SelectedView> by writableValue(TopLevelModel::selectedView)
private val cashStates: ObservableList<StateAndRef<Cash.State>> by observableList(ContractStateModel::cashStates)
private val gatheredTransactionDataList: ObservableList<out GatheredTransactionData>
by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
private val reportingCurrency: ObservableValue<Currency> by observableValue(SettingsModel::reportingCurrency)
private val exchangeRate: ObservableValue<ExchangeRate> by observableValue(ExchangeRateModel::exchangeRate)
private val sumAmount = AmountBindings.sumAmountExchange(
cashStates.map { it.state.data.amount.withoutIssuer() },
reportingCurrency,
exchangeRate
)
init {
val formatter = AmountFormatter.boring
ourCashLabel.textProperty().bind(sumAmount.map { formatter.format(it) })
ourCashPane.setOnMouseClicked { clickEvent ->
if (clickEvent.button == MouseButton.PRIMARY) {
selectedView.value = SelectedView.Cash
}
}
// TODO: register views in view model and populate the dashboard dynamically.
ourTransactionsLabel.textProperty().bind(
Bindings.size(gatheredTransactionDataList).map { it.toString() }
)
ourTransactionsPane.setOnMouseClicked { clickEvent ->
if (clickEvent.button == MouseButton.PRIMARY) {
selectedView.value = SelectedView.Transaction
}
}
newTransaction.setOnMouseClicked { clickEvent ->
if (clickEvent.button == MouseButton.PRIMARY) {
selectedView.value = SelectedView.NewTransaction
}
ourCashPane.apply {
content = find(CashViewer::class).widget
}
tilePane.widthProperty().addListener { e ->
val prefWidth = 350
val columns: Int = ((tilePane.width - 10) / prefWidth).toInt()
tilePane.children.forEach { (it as? TitledPane)?.prefWidth = (tilePane.width - 10) / columns }
}
}
fun changeView(event: MouseEvent) {
if (event.button == MouseButton.PRIMARY) {
selectedView.value = SelectedView.valueOf((event.source as Node).id)
}
}
}

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.observableValue
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.explorer.components.ExceptionDialog
import com.r3corda.explorer.model.CashTransaction
import com.r3corda.explorer.model.IdentityModel
import com.r3corda.node.services.messaging.CordaRPCOps
import com.r3corda.node.services.messaging.TransactionBuildResult
import javafx.beans.binding.Bindings
@ -22,8 +19,8 @@ import javafx.collections.ObservableList
import javafx.scene.Node
import javafx.scene.Parent
import javafx.scene.control.*
import javafx.util.StringConverter
import javafx.util.converter.BigDecimalStringConverter
import org.controlsfx.dialog.ExceptionDialog
import tornadofx.View
import java.math.BigDecimal
import java.util.*
@ -43,21 +40,20 @@ class NewTransaction : View() {
private val transactionTypeCB: ChoiceBox<CashTransaction> by fxid()
private val amount: TextField by fxid()
private val currency: ChoiceBox<Currency> by fxid()
private val networkIdentities: ObservableList<NodeInfo> by observableList(NetworkIdentityModel::networkIdentities)
private val rpcProxy: ObservableValue<CordaRPCOps?> by observableValue(NodeMonitorModel::proxyObservable)
private val myIdentity: ObservableValue<Party?> by observableValue(IdentityModel::myIdentity)
private val notary: ObservableValue<Party?> by observableValue(IdentityModel::notary)
private val issueRefLabel: Label by fxid()
private val issueRefTextField: TextField by fxid()
// Inject data
private val parties: ObservableList<NodeInfo> by observableList(NetworkIdentityModel::parties)
private val rpcProxy: ObservableValue<CordaRPCOps?> by observableValue(NodeMonitorModel::proxyObservable)
private val myIdentity: ObservableValue<NodeInfo?> by observableValue(NetworkIdentityModel::myIdentity)
private val notaries: ObservableList<NodeInfo> by observableList(NetworkIdentityModel::notaries)
private fun ObservableValue<*>.isNotNull(): BooleanBinding {
return Bindings.createBooleanBinding({ this.value != null }, arrayOf(this))
}
fun resetScreen() {
private fun resetScreen() {
partyBChoiceBox.valueProperty().set(null)
transactionTypeCB.valueProperty().set(null)
currency.valueProperty().set(null)
@ -66,28 +62,22 @@ class NewTransaction : View() {
init {
// Disable everything when not connected to node.
val enableProperty = myIdentity.isNotNull().and(notary.isNotNull()).and(rpcProxy.isNotNull())
val notariesNotNullBinding = Bindings.createBooleanBinding({ notaries.isNotEmpty() }, arrayOf(notaries))
val enableProperty = myIdentity.isNotNull().and(rpcProxy.isNotNull()).and(notariesNotNullBinding)
root.disableProperty().bind(enableProperty.not())
transactionTypeCB.items = FXCollections.observableArrayList(CashTransaction.values().asList())
// Party A textfield always display my identity name, not editable.
partyATextField.isEditable = false
partyATextField.textProperty().bind(myIdentity.map { it?.name ?: "" })
partyATextField.textProperty().bind(myIdentity.map { it?.legalIdentity?.name ?: "" })
partyALabel.textProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameA?.let { "$it : " } })
partyATextField.visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameA }.isNotNull())
partyBLabel.textProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB?.let { "$it : " } })
partyBChoiceBox.visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB }.isNotNull())
partyBChoiceBox.items = networkIdentities
partyBChoiceBox.converter = object : StringConverter<NodeInfo?>() {
override fun toString(node: NodeInfo?): String {
return node?.legalIdentity?.name ?: ""
}
override fun fromString(string: String?): NodeInfo {
throw UnsupportedOperationException("not implemented")
}
partyBChoiceBox.apply {
visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB }.isNotNull())
partyBChoiceBox.items = parties
converter = stringConverter { it?.legalIdentity?.name ?: "" }
}
// BigDecimal text Formatter, restricting text box input to decimal values.
@ -123,7 +113,8 @@ class NewTransaction : View() {
executeButton.setOnAction { event ->
// Null checks to ensure these observable values are set, execute button should be disabled if any of these value are null, this extra checks are for precaution and getting non-nullable values without using !!.
myIdentity.value?.let { myIdentity ->
notary.value?.let { notary ->
// TODO : Allow user to chose which notary to use?
notaries.first()?.let { notary ->
rpcProxy.value?.let { rpcProxy ->
Triple(myIdentity, notary, rpcProxy)
}
@ -131,13 +122,14 @@ class NewTransaction : View() {
}?.let {
val (myIdentity, notary, rpcProxy) = it
transactionTypeCB.value?.let {
// Default issuer reference to 1 if not specified.
val issueRef = OpaqueBytes(if (issueRefTextField.text.trim().isNotBlank()) issueRefTextField.text.toByteArray() else ByteArray(1, { 1 }))
// TODO : Change these commands into individual RPC methods instead of using executeCommand.
val command = when (it) {
CashTransaction.Issue -> ClientToServiceCommand.IssueCash(Amount(textFormatter.value, currency.value), issueRef, partyBChoiceBox.value.legalIdentity, notary)
CashTransaction.Pay -> ClientToServiceCommand.PayCash(Amount(textFormatter.value, Issued(PartyAndReference(myIdentity, issueRef), currency.value)), partyBChoiceBox.value.legalIdentity)
CashTransaction.Issue -> ClientToServiceCommand.IssueCash(Amount(textFormatter.value, currency.value), issueRef, partyBChoiceBox.value.legalIdentity, notary.notaryIdentity)
CashTransaction.Pay -> ClientToServiceCommand.PayCash(Amount(textFormatter.value, Issued(PartyAndReference(myIdentity.legalIdentity, issueRef), currency.value)), partyBChoiceBox.value.legalIdentity)
CashTransaction.Exit -> ClientToServiceCommand.ExitCash(Amount(textFormatter.value, currency.value), issueRef)
}
val dialog = Alert(Alert.AlertType.INFORMATION).apply {
headerText = null
contentText = "Transaction Started."
@ -165,8 +157,7 @@ class NewTransaction : View() {
dialog.close()
ExceptionDialog(it.source.exception).apply {
initOwner((event.target as Node).scene.window)
showAndWait()
}
}.showAndWait()
}
}
}

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

View File

@ -4,82 +4,43 @@ import com.r3corda.client.fxutils.*
import com.r3corda.client.model.*
import com.r3corda.contracts.asset.Cash
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.explorer.AmountDiff
import com.r3corda.explorer.formatters.AmountFormatter
import com.r3corda.explorer.formatters.Formatter
import com.r3corda.explorer.formatters.NumberFormatter
import com.r3corda.explorer.model.IdentityModel
import com.r3corda.explorer.identicon.identicon
import com.r3corda.explorer.identicon.identiconToolTip
import com.r3corda.explorer.model.ReportingCurrencyModel
import com.r3corda.explorer.sign
import com.r3corda.explorer.ui.*
import com.r3corda.explorer.ui.setCustomCellFactory
import javafx.beans.binding.Bindings
import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.geometry.Insets
import javafx.scene.Node
import javafx.scene.control.*
import javafx.scene.Parent
import javafx.scene.control.Label
import javafx.scene.control.ListView
import javafx.scene.control.TableView
import javafx.scene.control.TitledPane
import javafx.scene.layout.Background
import javafx.scene.layout.BackgroundFill
import javafx.scene.layout.BorderPane
import javafx.scene.layout.CornerRadii
import javafx.scene.layout.VBox
import javafx.scene.paint.Color
import tornadofx.View
import tornadofx.*
import java.security.PublicKey
import java.util.*
class TransactionViewer: View() {
override val root: VBox by fxml()
val topSplitPane: SplitPane by fxid()
// Top half (transactions table)
private val transactionViewTable: TableView<ViewerNode> by fxid()
private val transactionViewTransactionId: TableColumn<ViewerNode, String> by fxid()
private val transactionViewStateMachineId: TableColumn<ViewerNode, String> by fxid()
private val transactionViewClientUuid: TableColumn<ViewerNode, String> by fxid()
private val transactionViewTransactionStatus: TableColumn<ViewerNode, TransactionCreateStatus?> by fxid()
private val transactionViewProtocolStatus: TableColumn<ViewerNode, String> by fxid()
private val transactionViewStateMachineStatus: TableColumn<ViewerNode, StateMachineStatus?> by fxid()
private val transactionViewCommandTypes: TableColumn<ViewerNode, String> by fxid()
private val transactionViewTotalValueEquiv: TableColumn<ViewerNode, AmountDiff<Currency>> by fxid()
// Bottom half (details)
private val contractStatesTitledPane: TitledPane by fxid()
private val contractStatesInputsCountLabel: Label by fxid()
private val contractStatesInputStatesTable: TableView<StateNode> by fxid()
private val contractStatesInputStatesId: TableColumn<StateNode, String> by fxid()
private val contractStatesInputStatesType: TableColumn<StateNode, String> by fxid()
private val contractStatesInputStatesOwner: TableColumn<StateNode, String> by fxid()
private val contractStatesInputStatesLocalCurrency: TableColumn<StateNode, Currency?> by fxid()
private val contractStatesInputStatesAmount: TableColumn<StateNode, Long?> by fxid()
private val contractStatesInputStatesEquiv: TableColumn<StateNode, Amount<Currency>?> by fxid()
private val contractStatesOutputsCountLabel: Label by fxid()
private val contractStatesOutputStatesTable: TableView<StateNode> by fxid()
private val contractStatesOutputStatesId: TableColumn<StateNode, String> by fxid()
private val contractStatesOutputStatesType: TableColumn<StateNode, String> by fxid()
private val contractStatesOutputStatesOwner: TableColumn<StateNode, String> by fxid()
private val contractStatesOutputStatesLocalCurrency: TableColumn<StateNode, Currency?> by fxid()
private val contractStatesOutputStatesAmount: TableColumn<StateNode, Long?> by fxid()
private val contractStatesOutputStatesEquiv: TableColumn<StateNode, Amount<Currency>?> by fxid()
private val signaturesTitledPane: TitledPane by fxid()
private val signaturesList: ListView<PublicKey> by fxid()
private val matchingTransactionsLabel: Label by fxid()
class TransactionViewer : View() {
override val root by fxml<BorderPane>()
private val transactionViewTable by fxid<TableView<ViewerNode>>()
private val matchingTransactionsLabel by fxid<Label>()
// Inject data
private val gatheredTransactionDataList: ObservableList<out GatheredTransactionData>
by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
private val reportingExchange: ObservableValue<Pair<Currency, (Amount<Currency>) -> Amount<Currency>>>
by observableValue(ReportingCurrencyModel::reportingExchange)
private val myIdentity: ObservableValue<Party?> by observableValue(IdentityModel::myIdentity)
private val gatheredTransactionDataList by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
private val reportingExchange by observableValue(ReportingCurrencyModel::reportingExchange)
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
/**
* This is what holds data for a single transaction node. Note how a lot of these are nullable as we often simply don't
@ -92,7 +53,7 @@ class TransactionViewer: View() {
val stateMachineStatus: ObservableValue<out StateMachineStatus?>,
val protocolStatus: ObservableValue<out ProtocolStatus?>,
val commandTypes: Collection<Class<CommandData>>,
val totalValueEquiv: ObservableValue<AmountDiff<Currency>?>
val totalValueEquiv: ObservableValue<AmountDiff<Currency>>
)
/**
@ -121,282 +82,148 @@ class TransactionViewer: View() {
stateMachineStatus = stateMachineProperty { it.stateMachineStatus },
commandTypes = it.transaction.transaction.tx.commands.map { it.value.javaClass },
totalValueEquiv = {
val resolvedInputs = it.transaction.inputs.sequence().map { resolution ->
when (resolution) {
is PartiallyResolvedTransaction.InputResolution.Unresolved -> null
is PartiallyResolvedTransaction.InputResolution.Resolved -> resolution.stateAndRef
}
}.fold(listOf()) { inputs: List<StateAndRef<ContractState>>?, state: StateAndRef<ContractState>? ->
if (inputs != null && state != null) {
inputs + state
} else {
null
}
}
val resolvedInputs = it.transaction.inputs.sequence()
.map { (it as? PartiallyResolvedTransaction.InputResolution.Resolved)?.stateAndRef?.state }
.filterNotNull().toList().lift()
::calculateTotalEquiv.lift(
myIdentity,
reportingExchange,
resolvedInputs.lift(),
resolvedInputs,
it.transaction.transaction.tx.outputs.lift()
)
}()
)
}
/**
* The detail panes are only filled out if a transaction is selected
*/
private val selectedViewerNode = transactionViewTable.singleRowSelection()
private val selectedTransaction = selectedViewerNode.map {
when (it) {
is SingleRowSelection.None -> null
is SingleRowSelection.Selected -> it.node.transaction
}
}
private val inputStateNodes = ChosenList(selectedTransaction.map { transaction ->
if (transaction == null) {
FXCollections.emptyObservableList<StateNode>()
} else {
FXCollections.observableArrayList(transaction.inputs.map { inputResolution ->
StateNode(inputResolution, inputResolution.value.stateRef)
})
}
})
private val outputStateNodes = ChosenList(selectedTransaction.map {
if (it == null) {
FXCollections.emptyObservableList<StateNode>()
} else {
FXCollections.observableArrayList(it.transaction.tx.outputs.mapIndexed { index, transactionState ->
val stateRef = StateRef(it.id, index)
StateNode(PartiallyResolvedTransaction.InputResolution.Resolved(StateAndRef(transactionState, stateRef)).lift(), stateRef)
})
}
})
private val signatures = ChosenList(selectedTransaction.map {
if (it == null) {
FXCollections.emptyObservableList<PublicKey>()
} else {
FXCollections.observableArrayList(it.transaction.sigs.map { it.by })
}
})
/**
* We only display the detail panes if there is a node selected.
*/
private val allNodesShown = FXCollections.observableArrayList<Node>(
transactionViewTable,
contractStatesTitledPane,
signaturesTitledPane
)
private val onlyTransactionsTableShown = FXCollections.observableArrayList<Node>(
transactionViewTable
)
private val topSplitPaneNodesShown = ChosenList(
selectedViewerNode.map { selection ->
if (selection is SingleRowSelection.None<*>) {
onlyTransactionsTableShown
} else {
allNodesShown
}
})
/**
* Both input and output state tables look the same, so we each up with [wireUpStatesTable]
*/
private fun wireUpStatesTable(
states: ObservableList<StateNode>,
statesCountLabel: Label,
statesTable: TableView<StateNode>,
statesId: TableColumn<StateNode, String>,
statesType: TableColumn<StateNode, String>,
statesOwner: TableColumn<StateNode, String>,
statesLocalCurrency: TableColumn<StateNode, Currency?>,
statesAmount: TableColumn<StateNode, Long?>,
statesEquiv: TableColumn<StateNode, Amount<Currency>?>
) {
statesCountLabel.textProperty().bind(Bindings.size(states).map { "$it" })
Bindings.bindContent(statesTable.items, states)
val unknownString = "???"
statesId.setCellValueFactory { it.value.stateRef.toString().lift() }
statesType.setCellValueFactory {
resolvedOrDefault(it.value.state, unknownString) {
it.state.data.javaClass.toString()
}
}
statesOwner.setCellValueFactory {
resolvedOrDefault(it.value.state, unknownString) {
val contractState = it.state.data
if (contractState is OwnableState) {
contractState.owner.toStringShort()
} else {
unknownString
}
}
}
statesLocalCurrency.setCellValueFactory {
resolvedOrDefault<Currency?>(it.value.state, null) {
val contractState = it.state.data
if (contractState is Cash.State) {
contractState.amount.token.product
} else {
null
}
}
}
statesAmount.setCellValueFactory {
resolvedOrDefault<Long?>(it.value.state, null) {
val contractState = it.state.data
if (contractState is Cash.State) {
contractState.amount.quantity
} else {
null
}
}
}
statesAmount.cellFactory = NumberFormatter.boringLong.toTableCellFactory()
statesEquiv.setCellValueFactory {
resolvedOrDefault<ObservableValue<Amount<Currency>?>>(it.value.state, null.lift()) {
val contractState = it.state.data
if (contractState is Cash.State) {
reportingExchange.map { exchange ->
exchange.second(contractState.amount.withoutIssuer())
}
} else {
null.lift()
}
}.bind { it }
}
statesEquiv.cellFactory = AmountFormatter.boring.toTableCellFactory()
}
init {
Bindings.bindContent(topSplitPane.items, topSplitPaneNodesShown)
val searchField = SearchField(viewerNodes, arrayOf({ viewerNode, s -> viewerNode.commandTypes.any { it.simpleName.contains(s, true) } }))
root.top = searchField.root
// Transaction table
Bindings.bindContent(transactionViewTable.items, viewerNodes)
transactionViewTable.setColumnPrefWidthPolicy { tableWidthWithoutPaddingAndBorder, column ->
Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / transactionViewTable.columns.size).toInt()
}
transactionViewTransactionId.setCellValueFactory { "${it.value.transactionId}".lift() }
transactionViewStateMachineId.setCellValueFactory { it.value.stateMachineRunId.map { "${it?.uuid ?: ""}" } }
transactionViewProtocolStatus.setCellValueFactory { it.value.protocolStatus.map { "${it ?: ""}" } }
transactionViewTransactionStatus.setCustomCellFactory {
val label = Label()
val backgroundFill = when (it) {
is TransactionCreateStatus.Started -> BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)
is TransactionCreateStatus.Failed -> BackgroundFill(Color.SALMON, CornerRadii.EMPTY, Insets.EMPTY)
null -> BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)
transactionViewTable.apply {
items = searchField.filteredData
column("Transaction ID", ViewerNode::transactionId).setCustomCellFactory {
label("$it".substring(0, 16) + "...") {
graphic = imageview {
image = identicon(it, 5.0)
}
tooltip = identiconToolTip(it)
}
}
label.background = Background(backgroundFill)
label.text = "$it"
label
}
transactionViewStateMachineStatus.setCellValueFactory { it.value.stateMachineStatus.map { it } }
transactionViewStateMachineStatus.setCustomCellFactory {
val label = Label()
val backgroundFill = when (it) {
is StateMachineStatus.Added -> BackgroundFill(Color.LIGHTYELLOW, CornerRadii.EMPTY, Insets.EMPTY)
is StateMachineStatus.Removed -> BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)
null -> BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)
column("State Machine ID", ViewerNode::stateMachineRunId).cellFormat { text = "${it?.uuid ?: ""}" }
column("Protocol status", ViewerNode::protocolStatus).cellFormat { text = "${it.value ?: ""}" }
column("SM Status", ViewerNode::stateMachineStatus).cellFormat { text = "${it.value ?: ""}" }
column("Command type(s)", ViewerNode::commandTypes).cellFormat { text = it.map { it.simpleName }.joinToString(",") }
column("Total value (USD equiv)", ViewerNode::totalValueEquiv)
.cellFormat { text = "${it.positivity.sign}${AmountFormatter.boring.format(it.amount)}" }
rowExpander(true) {
add(ContractStatesView(it.transaction).root)
background = Background(BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY))
prefHeight = 400.0
}.apply {
// Hide the expander column.
isVisible = false
prefWidth = 0.0
}
label.background = Background(backgroundFill)
label.text = "$it"
label
}
transactionViewCommandTypes.setCellValueFactory {
it.value.commandTypes.map { it.simpleName }.joinToString(",").lift()
}
transactionViewTotalValueEquiv.setCellValueFactory<ViewerNode, AmountDiff<Currency>> { it.value.totalValueEquiv }
transactionViewTotalValueEquiv.cellFactory = object : Formatter<AmountDiff<Currency>> {
override fun format(value: AmountDiff<Currency>) =
"${value.positivity.sign}${AmountFormatter.boring.format(value.amount)}"
}.toTableCellFactory()
// Contract states
wireUpStatesTable(
inputStateNodes,
contractStatesInputsCountLabel,
contractStatesInputStatesTable,
contractStatesInputStatesId,
contractStatesInputStatesType,
contractStatesInputStatesOwner,
contractStatesInputStatesLocalCurrency,
contractStatesInputStatesAmount,
contractStatesInputStatesEquiv
)
wireUpStatesTable(
outputStateNodes,
contractStatesOutputsCountLabel,
contractStatesOutputStatesTable,
contractStatesOutputStatesId,
contractStatesOutputStatesType,
contractStatesOutputStatesOwner,
contractStatesOutputStatesLocalCurrency,
contractStatesOutputStatesAmount,
contractStatesOutputStatesEquiv
)
// Signatures
Bindings.bindContent(signaturesList.items, signatures)
signaturesList.cellFactory = object : Formatter<PublicKey> {
override fun format(value: PublicKey) = value.toStringShort()
}.toListCellFactory()
matchingTransactionsLabel.textProperty().bind(Bindings.size(viewerNodes).map {
matchingTransactionsLabel.textProperty().bind(Bindings.size(transactionViewTable.items).map {
"$it matching transaction${if (it == 1) "" else "s"}"
})
}
private class ContractStatesView(val transaction: PartiallyResolvedTransaction) : View() {
override val root: Parent by fxml()
private val inputs: ListView<StateNode> by fxid()
private val outputs: ListView<StateNode> by fxid()
private val signatures: ListView<PublicKey> by fxid()
private val inputPane: TitledPane by fxid()
private val outputPane: TitledPane by fxid()
private val signaturesPane: TitledPane by fxid()
init {
val inputStates = transaction.inputs.map { StateNode(it, it.value.stateRef) }
val outputStates = transaction.transaction.tx.outputs.mapIndexed { index, transactionState ->
val stateRef = StateRef(transaction.id, index)
StateNode(PartiallyResolvedTransaction.InputResolution.Resolved(StateAndRef(transactionState, stateRef)).lift(), stateRef)
}
val signatureData = transaction.transaction.sigs.map { it.by }
// Bind count to TitlePane
inputPane.textProperty().bind(inputStates.lift().map { "Input (${it.count()})" })
outputPane.textProperty().bind(outputStates.lift().map { "Output (${it.count()})" })
signaturesPane.textProperty().bind(signatureData.lift().map { "Signatures (${it.count()})" })
val cellFactory = { node: StateNode ->
(node.state.value as? PartiallyResolvedTransaction.InputResolution.Resolved)?.run {
val data = stateAndRef.state.data
form {
label("${data.contract.javaClass.simpleName} (${stateAndRef.ref.toString().substring(0, 16)}...)[${stateAndRef.ref.index}]") {
graphic = imageview {
image = identicon(stateAndRef.ref.txhash, 10.0)
}
tooltip = identiconToolTip(stateAndRef.ref.txhash)
}
when (data) {
is Cash.State -> form {
fieldset {
field("Amount :") {
label(AmountFormatter.boring.format(data.amount.withoutIssuer()))
}
field("Issuer :") {
label("${data.amount.token.issuer}") {
tooltip(data.amount.token.issuer.party.owningKey.toStringShort())
}
}
field("Owner :") {
val owner = data.owner
val nodeInfo = Models.get<NetworkIdentityModel>(TransactionViewer::class).lookup(owner)
label(nodeInfo?.legalIdentity?.name ?: "???") {
tooltip(data.owner.toStringShort())
}
}
}
}
// TODO : Generic view using reflection?
else -> label {}
}
}
} ?: label { text = "???" }
}
inputs.setCustomCellFactory(cellFactory)
outputs.setCustomCellFactory(cellFactory)
inputs.items = FXCollections.observableList(inputStates)
outputs.items = FXCollections.observableList(outputStates)
signatures.items = FXCollections.observableList(signatureData)
signatures.apply {
cellFormat { key ->
val nodeInfo = Models.get<NetworkIdentityModel>(TransactionViewer::class).lookup(key)
text = "${key.toStringShort()} (${nodeInfo?.legalIdentity?.name ?: "???"})"
}
prefHeight = 185.0
}
}
}
}
/**
* We calculate the total value by subtracting relevant input states and adding relevant output states, as long as they're cash
*/
private fun calculateTotalEquiv(
identity: Party?,
reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>,
inputs: List<StateAndRef<ContractState>>?,
outputs: List<TransactionState<ContractState>>): AmountDiff<Currency>? {
if (inputs == null) {
return null
}
var sum = 0L
private fun calculateTotalEquiv(identity: NodeInfo?,
reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>,
inputs: List<TransactionState<ContractState>>,
outputs: List<TransactionState<ContractState>>): AmountDiff<Currency> {
val (reportingCurrency, exchange) = reportingCurrencyExchange
val publicKey = identity?.owningKey
inputs.forEach {
val contractState = it.state.data
if (contractState is Cash.State && publicKey == contractState.owner) {
sum -= exchange(contractState.amount.withoutIssuer()).quantity
}
val publicKey = identity?.legalIdentity?.owningKey
fun List<TransactionState<ContractState>>.sum(): Long {
return this.map { it.data as? Cash.State }
.filterNotNull()
.filter { publicKey == it.owner }
.map { exchange(it.amount.withoutIssuer()).quantity }
.sum()
}
outputs.forEach {
val contractState = it.data
if (contractState is Cash.State && publicKey == contractState.owner) {
sum += exchange(contractState.amount.withoutIssuer()).quantity
}
}
return AmountDiff.fromLong(sum, reportingCurrency)
}
fun <A> resolvedOrDefault(
state: ObservableValue<PartiallyResolvedTransaction.InputResolution>,
default: A,
resolved: (StateAndRef<*>) -> A
): ObservableValue<A> {
return state.map {
when (it) {
is PartiallyResolvedTransaction.InputResolution.Unresolved -> default
is PartiallyResolvedTransaction.InputResolution.Resolved -> resolved(it.stateAndRef)
}
}
}
return AmountDiff.fromLong(outputs.sum() - inputs.sum(), reportingCurrency)
}

View File

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

View File

@ -1,28 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<GridPane vgap="5" hgap="5" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<Label text="State ID"/>
<Label text="Issuer" GridPane.rowIndex="1"/>
<Label text="Originated" wrapText="true" GridPane.rowIndex="2"/>
<Label text="Amount" wrapText="true" GridPane.rowIndex="3"/>
<Label fx:id="equivLabel" text="USD" wrapText="true" GridPane.rowIndex="4"/>
<HBox spacing="5.0" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<children>
<VBox HBox.hgrow="SOMETIMES">
<children>
<Label text="State ID" VBox.vgrow="ALWAYS" />
<Label text="Issuer" />
<Label text="Originated" wrapText="true" />
<Label text="Amount" wrapText="true" />
<Label fx:id="equivLabel" text="USD" wrapText="true" />
</children>
</VBox>
<VBox HBox.hgrow="ALWAYS">
<children>
<Label fx:id="stateIdValueLabel" text="39043-329090-390091" />
<Label fx:id="issuerValueLabel" styleClass="counterparty" text="C-03820 HSBC GROUP PLC" />
<Label fx:id="originatedValueLabel" text="2018-04-27 11:34 UTC" />
<Label fx:id="amountValueLabel" text="GBP 0.00" wrapText="true" />
<Label fx:id="equivValueLabel" text="0.00" wrapText="true" />
</children>
</VBox>
</children>
</HBox>
<Label fx:id="stateIdValueLabel" text="39043-329090-390091" GridPane.columnIndex="1"/>
<Label fx:id="issuerValueLabel" styleClass="counterparty" text="C-03820 HSBC GROUP PLC" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
<Label fx:id="originatedValueLabel" text="2018-04-27 11:34 UTC" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
<Label fx:id="amountValueLabel" text="GBP 0.00" wrapText="true" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
<Label fx:id="equivValueLabel" text="0.00" wrapText="true" GridPane.columnIndex="1" GridPane.rowIndex="4"/>
</GridPane>

View File

@ -1,84 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.String?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.TreeTableColumn?>
<?import javafx.scene.control.TreeTableView?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<SplitPane fx:id="topSplitPane" dividerPositions="0.5" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<items>
<VBox fx:id="leftPane" spacing="5.0" styleClass="root">
<children>
<StackPane alignment="CENTER_RIGHT">
<VBox.margin>
<Insets />
</VBox.margin>
<children>
<TextField id="search" fx:id="searchCriteriaTextField" promptText="Search by issuer/currency" styleClass="search">
<opaqueInsets>
<Insets />
</opaqueInsets>
<StackPane.margin>
<Insets />
</StackPane.margin>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</TextField>
<ImageView fx:id="searchCancelImageView" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
<image>
<Image url="@../../images/clear_inactive.png" />
</image>
<StackPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</StackPane.margin>
</ImageView>
</children>
</StackPane>
<Label fx:id="totalMatchingLabel" alignment="BOTTOM_LEFT" text="Total 15 matching issuer(s)" wrapText="true">
<VBox.margin>
<Insets bottom="5.0" right="10.0" top="5.0" />
</VBox.margin>
</Label>
<TreeTableView fx:id="cashViewerTable" showRoot="false" VBox.vgrow="ALWAYS">
<columns>
<TreeTableColumn fx:id="cashViewerTableIssuerCurrency" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefWidth="100.0" styleClass="first-column" text="Issuer/Currency" />
<TreeTableColumn fx:id="cashViewerTableLocalCurrency" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefWidth="132.0" text="Local currency">
<styleClass>
<String fx:value="monetary-value" />
<String fx:value="second-column" />
</styleClass>
</TreeTableColumn>
<TreeTableColumn fx:id="cashViewerTableEquiv" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefWidth="72.0" styleClass="monetary-value" text="Equiv" />
</columns>
</TreeTableView>
</children>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</VBox>
<VBox fx:id="rightPane" spacing="5.0">
<children>
<Button mnemonicParsing="false" text="&gt;&gt;" />
<Label fx:id="totalPositionsLabel" styleClass="subline" text="Total 18 position(s)" />
<Label fx:id="equivSumLabel" styleClass="headline" text="USD 394.6k" />
<ListView fx:id="cashStatesList" VBox.vgrow="ALWAYS" />
</children>
<opaqueInsets>
<Insets />
</opaqueInsets>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</VBox>
</items>
</SplitPane>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<BorderPane stylesheets="@../css/wallet.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<padding>
<Insets right="10" left="5" bottom="5" top="5"/>
</padding>
<center>
<SplitPane fx:id="splitPane" dividerPositions="0.5">
<VBox fx:id="leftPane" spacing="5.0">
<TreeTableView fx:id="cashViewerTable" showRoot="false" VBox.vgrow="ALWAYS">
<columns>
<TreeTableColumn fx:id="cashViewerTableIssuerCurrency" styleClass="first-column" text="Issuer/Currency"/>
<TreeTableColumn fx:id="cashViewerTableLocalCurrency" text="Local currency" styleClass="monetary-value, second-column"/>
<TreeTableColumn fx:id="cashViewerTableEquiv" styleClass="monetary-value" text="Equiv"/>
</columns>
</TreeTableView>
<Label fx:id="totalMatchingLabel" text="Total 15 matching issuer(s)"/>
</VBox>
<VBox fx:id="rightPane" spacing="5.0">
<Button fx:id="toggleButton" mnemonicParsing="false" text="&gt;&gt;"/>
<ListView fx:id="cashStatesList" VBox.vgrow="ALWAYS"/>
<Label fx:id="totalPositionsLabel" text="Total 18 position(s)"/>
</VBox>
</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"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.SplitMenuButton?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.layout.GridPane?>
<GridPane hgap="10" stylesheets="@../css/wallet.css" styleClass="header-panel" vgap="5" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<padding>
<Insets left="10.0" right="10.0" top="5.0" bottom="5.0"/>
</padding>
<!-- Row 1 -->
<Label id="headline" fx:id="sectionLabel" alignment="BOTTOM_CENTER" maxHeight="Infinity" text="Home" GridPane.columnIndex="0" GridPane.hgrow="ALWAYS"/>
<VBox xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<children>
<VBox spacing="5.0">
<children>
<HBox>
<children>
<HBox alignment="CENTER_LEFT" spacing="5.0" HBox.hgrow="ALWAYS">
<children>
<VBox fx:id="sectionIconContainer" alignment="CENTER">
<children>
<ImageView fx:id="sectionIcon" fitHeight="30.0" fitWidth="30.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../../images/home.png" />
</image>
</ImageView>
</children>
</VBox>
<Label id="headline" fx:id="sectionLabel" text="Home" />
</children>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</HBox>
<HBox alignment="TOP_RIGHT" spacing="5.0">
<children>
<Button fx:id="debugNextButton" mnemonicParsing="false" text="Next" />
<Button fx:id="debugGoStopButton" mnemonicParsing="false" text="!!!" />
<SplitMenuButton mnemonicParsing="false" text="DRUTTER">
<items>
<MenuItem mnemonicParsing="false" text="Sign out" />
<MenuItem mnemonicParsing="false" text="Account settings..." />
</items>
<graphic>
<ImageView fitHeight="20.0" fitWidth="52.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../../images/user_w.png" />
</image>
</ImageView>
</graphic>
</SplitMenuButton>
<Button fx:id="settingsButton" mnemonicParsing="false" text="Settings">
<graphic>
<ImageView fitHeight="20.0" fitWidth="20.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../../images/settings_w.png" />
</image>
</ImageView>
</graphic>
</Button>
</children>
</HBox>
</children>
</HBox>
<StackPane alignment="CENTER_RIGHT">
<children>
<TextField id="search_main" promptText="Search for states, transactions, counterparties etc." styleClass="search">
<opaqueInsets>
<Insets />
</opaqueInsets>
<padding>
<Insets bottom="5.0" left="30.0" right="5.0" top="5.0" />
</padding>
</TextField>
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
<image>
<Image url="@../../images/clear_inactive.png" />
</image>
<StackPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</StackPane.margin>
</ImageView>
</children>
</StackPane>
</children>
</VBox>
<StackPane alignment="CENTER_RIGHT" />
</children>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</VBox>
<SplitMenuButton fx:id="userButton" maxHeight="Infinity" mnemonicParsing="false" text="DRUTTER" GridPane.columnIndex="3">
<items>
<MenuItem mnemonicParsing="false" text="Sign out"/>
<MenuItem mnemonicParsing="false" text="Account settings..."/>
</items>
<graphic>
<ImageView fitHeight="20.0" fitWidth="20.0" pickOnBounds="true" preserveRatio="true">
<Image url="@../images/user_w.png"/>
</ImageView>
</graphic>
</SplitMenuButton>
<!--<Button fx:id="settingsButton" maxHeight="Infinity" mnemonicParsing="false" text="Settings" GridPane.columnIndex="4">
<graphic>
<ImageView fitHeight="20.0" fitWidth="20.0" pickOnBounds="true" preserveRatio="true">
<Image url="@../images/settings_w.png"/>
</ImageView>
</graphic>
</Button>-->
<!--&lt;!&ndash; Row 2 &ndash;&gt;
<StackPane alignment="CENTER_RIGHT" GridPane.columnSpan="5" GridPane.rowIndex="1">
<TextField fx:id="search_main" promptText="Search for states, transactions, counterparties etc." styleClass="search"/>
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
<StackPane.margin>
<Insets right="10.0"/>
</StackPane.margin>
</ImageView>
</StackPane>-->
</GridPane>

View File

@ -2,19 +2,21 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.TilePane?>
<TilePane prefHeight="425.0" prefWidth="425.0" tileAlignment="TOP_LEFT" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<TitledPane id="tile_cash" fx:id="ourCashPane" alignment="CENTER" collapsible="false" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our cash">
<Label fx:id="ourCashLabel" text="USD 186.7m" textAlignment="CENTER" wrapText="true"/>
</TitledPane>
<TitledPane id="tile_debtors" fx:id="ourDebtorsPane" alignment="CENTER" collapsible="false" layoutX="232.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our debtors">
<Label text="USD 71.3m" textAlignment="CENTER" wrapText="true"/>
</TitledPane>
<TitledPane id="tile_creditors" fx:id="ourCreditorsPane" alignment="CENTER" collapsible="false" layoutX="312.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our creditors">
<Label text="USD (29.4m)" textAlignment="CENTER" wrapText="true"/>
</TitledPane>
<TitledPane id="tile_tx" fx:id="ourTransactionsPane" alignment="CENTER" collapsible="false" layoutX="392.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our transactions">
<Label fx:id="ourTransactionsLabel" text="In flight: 1,315" textAlignment="CENTER" wrapText="true"/>
</TitledPane>
<TitledPane id="tile_new_tx" fx:id="newTransaction" alignment="CENTER" collapsible="false" layoutX="472.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="New Transaction">
</TitledPane>
</TilePane>
<ScrollPane hbarPolicy="NEVER" fitToWidth="true" stylesheets="@../css/wallet.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<TilePane fx:id="tilePane" tileAlignment="TOP_LEFT" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<TitledPane id="Cash" fx:id="ourCashPane" collapsible="false" onMouseClicked="#changeView" styleClass="tile" text="Our cash">
<Label fx:id="ourCashLabel" text="USD 186.7m" textAlignment="CENTER" wrapText="true" />
</TitledPane>
<TitledPane id="tile_debtors" fx:id="ourDebtorsPane" collapsible="false" styleClass="tile" text="Our debtors">
<Label text="USD 71.3m" textAlignment="CENTER" wrapText="true" />
</TitledPane>
<TitledPane id="tile_creditors" fx:id="ourCreditorsPane" collapsible="false" styleClass="tile" text="Our creditors">
<Label text="USD (29.4m)" textAlignment="CENTER" wrapText="true" />
</TitledPane>
<TitledPane id="Transaction" fx:id="ourTransactionsPane" collapsible="false" onMouseClicked="#changeView" styleClass="tile" text="Our transactions">
<Label fx:id="ourTransactionsLabel" textAlignment="CENTER" wrapText="true" />
</TitledPane>
<TitledPane id="NewTransaction" fx:id="newTransaction" collapsible="false" onMouseClicked="#changeView" styleClass="tile" text="New Transaction">
</TitledPane>
</TilePane>
</ScrollPane>

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"?>
<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<VBox fx:id="topLevel" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<BorderPane fx:id="selectionBorderPane"/>
</VBox>
<StackPane stylesheets="@../css/wallet.css" prefHeight="650" prefWidth="900" styleClass="root, no-padding" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<SplitPane dividerPositions="0.0" styleClass="no-padding, split-pane-divider">
<VBox fx:id="sidebarPane" maxWidth="200.0" minWidth="80" styleClass="sidebar" SplitPane.resizableWithParent="false"/>
<BorderPane fx:id="selectionBorderPane" maxHeight="Infinity" minWidth="400"/>
</SplitPane>
</StackPane>

View File

@ -1,137 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.String?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.TitledPane?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>
<VBox styleClass="view" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<children>
<StackPane alignment="CENTER_RIGHT">
<children>
<TextField promptText="Filter transactions by originator, contract type..." styleClass="search">
<opaqueInsets>
<Insets />
</opaqueInsets>
<padding>
<Insets bottom="5.0" left="30.0" right="5.0" top="5.0" />
</padding>
<StackPane.margin>
<Insets bottom="5.0" top="5.0" />
</StackPane.margin>
</TextField>
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
<image>
<Image url="@../../../../../../../../../internal/explorer/src/main/resources/images/clear_inactive.png" />
</image>
<StackPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</StackPane.margin>
</ImageView>
</children>
</StackPane>
<SplitPane fx:id="topSplitPane" dividerPositions="0.3, 0.6" orientation="VERTICAL" prefHeight="562.0" prefWidth="1087.0" VBox.vgrow="ALWAYS">
<items>
<TableView fx:id="transactionViewTable" prefHeight="200.0" prefWidth="200.0">
<columns>
<TableColumn fx:id="transactionViewTransactionId" prefWidth="75.0" text="Transaction ID" />
<TableColumn fx:id="transactionViewStateMachineId" prefWidth="187.0" text="StateMachine ID" />
<TableColumn fx:id="transactionViewClientUuid" prefWidth="75.0" text="Client UUID" />
<TableColumn fx:id="transactionViewTransactionStatus" prefWidth="75.0" text="Transaction status" />
<TableColumn fx:id="transactionViewProtocolStatus" prefWidth="75.0" text="Protocol status" />
<TableColumn fx:id="transactionViewStateMachineStatus" prefWidth="75.0" text="SM Status" />
<TableColumn fx:id="transactionViewCommandTypes" prefWidth="75.0" text="Command type(s)" />
<TableColumn fx:id="transactionViewTotalValueEquiv" prefWidth="75.0" styleClass="monetary-value" text="Total value (USD equiv)" />
</columns>
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
</columnResizePolicy>
</TableView>
<TitledPane fx:id="contractStatesTitledPane" animated="false" text="Contract states">
<content>
<SplitPane dividerPositions="0.5" prefHeight="160.0" prefWidth="200.0">
<items>
<VBox prefHeight="200.0" prefWidth="100.0">
<children>
<HBox spacing="5.0">
<children>
<Label text="Inputs:" />
<Label fx:id="contractStatesInputsCountLabel" text="Label" />
</children>
</HBox>
<TableView fx:id="contractStatesInputStatesTable" prefHeight="200.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
<columns>
<TableColumn fx:id="contractStatesInputStatesId" prefWidth="75.0" text="ID" />
<TableColumn fx:id="contractStatesInputStatesType" prefWidth="75.0" text="Type" />
<TableColumn fx:id="contractStatesInputStatesOwner" prefWidth="75.0" text="Owner" />
<TableColumn fx:id="contractStatesInputStatesLocalCurrency" prefWidth="75.0" styleClass="first-column" text="Local Ccy" />
<TableColumn fx:id="contractStatesInputStatesAmount" prefWidth="75.0" text="Amount">
<styleClass>
<String fx:value="second-column" />
<String fx:value="monetary-value" />
</styleClass>
</TableColumn>
<TableColumn fx:id="contractStatesInputStatesEquiv" prefWidth="75.0" text="USD Equiv" />
</columns>
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
</columnResizePolicy>
</TableView>
</children>
</VBox>
<VBox prefHeight="200.0" prefWidth="100.0">
<children>
<HBox spacing="5.0">
<children>
<Label text="Outputs:" />
<Label fx:id="contractStatesOutputsCountLabel" text="Label" />
</children>
</HBox>
<TableView fx:id="contractStatesOutputStatesTable" prefHeight="200.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
<columns>
<TableColumn fx:id="contractStatesOutputStatesId" prefWidth="75.0" text="ID" />
<TableColumn fx:id="contractStatesOutputStatesType" prefWidth="75.0" text="Type" />
<TableColumn fx:id="contractStatesOutputStatesOwner" prefWidth="75.0" text="Owner" />
<TableColumn fx:id="contractStatesOutputStatesLocalCurrency" prefWidth="75.0" styleClass="first-column" text="Local Ccy" />
<TableColumn fx:id="contractStatesOutputStatesAmount" prefWidth="75.0" text="Amount">
<styleClass>
<String fx:value="second-column" />
<String fx:value="monetary-value" />
</styleClass>
</TableColumn>
<TableColumn fx:id="contractStatesOutputStatesEquiv" prefWidth="75.0" text="USD Equiv" />
</columns>
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
</columnResizePolicy>
</TableView>
</children>
</VBox>
</items>
</SplitPane>
</content>
</TitledPane>
<TitledPane fx:id="signaturesTitledPane" animated="false" text="Signatures">
<content>
<ListView fx:id="signaturesList" />
</content>
</TitledPane>
</items>
</SplitPane>
<HBox>
<children>
<Label fx:id="matchingTransactionsLabel" text="matching transaction(s)" />
</children>
</HBox>
</children>
</VBox>
<BorderPane stylesheets="@../css/wallet.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<padding>
<Insets top="5" left="5" right="10" bottom="5"/>
</padding>
<center>
<TableView fx:id="transactionViewTable" VBox.vgrow="ALWAYS">
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/>
</columnResizePolicy>
</TableView>
</center>
<bottom>
<Label fx:id="matchingTransactionsLabel" text="matching transaction(s)"/>
</bottom>
</BorderPane>

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) {
return databaseTransaction(database) {
services.vaultService.addNoteToTransaction(txnId, txnNote)

View File

@ -118,6 +118,10 @@ interface CordaRPCOps : RPCOps {
*/
fun executeCommand(command: ClientToServiceCommand): TransactionBuildResult
/**
* Returns Node's identity, assuming this will not change while the node is running.
*/
fun nodeIdentity(): NodeInfo
/*
* Add note(s) to an existing Vault transaction
*/

View File

@ -16,10 +16,8 @@ import com.r3corda.core.crypto.*
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.node.PhysicalLocation
import com.r3corda.core.node.ServiceEntry
import com.r3corda.core.node.services.NetworkMapCache
import com.r3corda.core.node.services.ServiceInfo
import com.r3corda.core.node.services.StateMachineTransactionMapping
import com.r3corda.core.node.services.Vault
import com.r3corda.core.node.WorldCoordinate
import com.r3corda.core.node.services.*
import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.serialization.*
import com.r3corda.core.transactions.SignedTransaction
@ -101,6 +99,7 @@ fun requirePermission(permission: String) {
*/
open class RPCException(msg: String, cause: Throwable?) : RuntimeException(msg, cause) {
constructor(msg: String) : this(msg, null)
class DeadlineExceeded(rpcName: String) : RPCException("Deadline exceeded on call to $rpcName")
}
@ -187,8 +186,12 @@ private class RPCKryo(observableSerializer: Serializer<Observable<Any>>? = null)
kryo.writeObject(output, nodeAddress.hostAndPort)
}
)
register(NodeMessagingClient.makeNetworkMapAddress(HostAndPort.fromString("localhost:0")).javaClass)
register(ServiceInfo::class.java)
register(ServiceType.getServiceType("ab", "ab").javaClass)
register(ServiceType.parse("ab").javaClass)
register(WorldCoordinate::class.java)
register(HostAndPort::class.java)
register(ServiceInfo::class.java, read = { kryo, input -> ServiceInfo.parse(input.readString()) }, write = Kryo::writeObject)
// Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway.
register(IllegalArgumentException::class.java)
// Kryo couldn't serialize Collections.unmodifiableCollection in Throwable correctly, causing null pointer exception when try to access the deserialize object.

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.FetchMapResponse
import com.r3corda.node.services.network.NetworkMapService.SubscribeResponse
import com.r3corda.node.services.transactions.SimpleNotaryService
import com.r3corda.node.utilities.AddOrRemove
import com.r3corda.protocols.sendRequest
import rx.Observable
@ -59,13 +58,7 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
override fun track(): Pair<List<NodeInfo>, Observable<MapChange>> {
synchronized(_changed) {
fun NodeInfo.isCordaService(): Boolean {
return advertisedServices.any { it.info.type in setOf(SimpleNotaryService.type, NetworkMapService.type) }
}
val currentParties = partyNodes.filter { !it.isCordaService() }
val changes = changed.filter { !it.node.isCordaService() }
return Pair(currentParties, changes.bufferUntilSubscribed())
return Pair(partyNodes, _changed.bufferUntilSubscribed())
}
}

View File

@ -1,6 +1,11 @@
package com.r3corda.node.services
import com.google.common.net.HostAndPort
import com.r3corda.core.contracts.ClientToServiceCommand
import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.generateKeyPair
import com.r3corda.core.messaging.Message
import com.r3corda.core.messaging.createMessage