mirror of
https://github.com/corda/corda.git
synced 2025-05-02 16:53:22 +00:00
Merged in pat-explorer-more-changes (pull request #505)
Pat explorer more changes
This commit is contained in:
commit
cd34f3ae16
@ -1,6 +1,7 @@
|
|||||||
package net.corda.client.fxutils
|
package net.corda.client.fxutils
|
||||||
|
|
||||||
import javafx.beans.binding.Bindings
|
import javafx.beans.binding.Bindings
|
||||||
|
import javafx.beans.binding.BooleanBinding
|
||||||
import javafx.beans.property.ReadOnlyObjectWrapper
|
import javafx.beans.property.ReadOnlyObjectWrapper
|
||||||
import javafx.beans.property.SimpleObjectProperty
|
import javafx.beans.property.SimpleObjectProperty
|
||||||
import javafx.beans.value.ObservableValue
|
import javafx.beans.value.ObservableValue
|
||||||
@ -277,5 +278,25 @@ fun <A> ObservableList<A>.last(): ObservableValue<A?> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun <T : Any> ObservableList<T>.unique(): ObservableList<T> {
|
fun <T : Any> ObservableList<T>.unique(): ObservableList<T> {
|
||||||
return associateByAggregation { it }.getObservableValues().map { Bindings.valueAt(it, 0) }.flatten()
|
return AggregatedList(this, { it }, { key, _list -> key })
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ObservableValue<*>.isNotNull(): BooleanBinding {
|
||||||
|
return Bindings.createBooleanBinding({ this.value != null }, arrayOf(this))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return first element of the observable list as observable value.
|
||||||
|
* Return provided default value if the list is empty.
|
||||||
|
*/
|
||||||
|
fun <A> ObservableList<A>.firstOrDefault(default: ObservableValue<A?>, predicate: (A) -> Boolean): ObservableValue<A?> {
|
||||||
|
return Bindings.createObjectBinding({ this.firstOrNull(predicate) ?: default.value }, arrayOf(this, default))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return first element of the observable list as observable value.
|
||||||
|
* Return ObservableValue(null) if the list is empty.
|
||||||
|
*/
|
||||||
|
fun <A> ObservableList<A>.firstOrNullObservable(predicate: (A) -> Boolean): ObservableValue<A?> {
|
||||||
|
return Bindings.createObjectBinding({ this.firstOrNull(predicate) }, arrayOf(this))
|
||||||
}
|
}
|
@ -89,9 +89,12 @@ class EventGenerator(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val clientToServiceCommandGenerator = Generator.frequency(
|
val clientCommandGenerator = Generator.frequency(
|
||||||
0.4 to issueCashGenerator,
|
1.0 to moveCashGenerator
|
||||||
0.5 to moveCashGenerator,
|
)
|
||||||
0.1 to exitCashGenerator
|
|
||||||
|
val bankOfCordaCommandGenerator = Generator.frequency(
|
||||||
|
0.6 to issueCashGenerator,
|
||||||
|
0.4 to exitCashGenerator
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -79,7 +79,6 @@ data class StateMachineData(
|
|||||||
* This model provides an observable list of transactions and what state machines/flows recorded them
|
* This model provides an observable list of transactions and what state machines/flows recorded them
|
||||||
*/
|
*/
|
||||||
class GatheredTransactionDataModel {
|
class GatheredTransactionDataModel {
|
||||||
|
|
||||||
private val transactions by observable(NodeMonitorModel::transactions)
|
private val transactions by observable(NodeMonitorModel::transactions)
|
||||||
private val stateMachineUpdates by observable(NodeMonitorModel::stateMachineUpdates)
|
private val stateMachineUpdates by observable(NodeMonitorModel::stateMachineUpdates)
|
||||||
private val progressTracking by observable(NodeMonitorModel::progressTracking)
|
private val progressTracking by observable(NodeMonitorModel::progressTracking)
|
||||||
@ -105,25 +104,10 @@ class GatheredTransactionDataModel {
|
|||||||
private val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
|
private val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
|
||||||
StateMachineData(id, progress.map { it?.let { FlowStatus(it.message) } }, status)
|
StateMachineData(id, progress.map { it?.let { FlowStatus(it.message) } }, status)
|
||||||
}.getObservableValues()
|
}.getObservableValues()
|
||||||
|
// TODO : Create a new screen for state machines.
|
||||||
private val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
|
private val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
|
||||||
private val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
|
private val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
|
||||||
private val partiallyResolvedTransactions = collectedTransactions.map {
|
val partiallyResolvedTransactions = collectedTransactions.map {
|
||||||
PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
|
PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* We JOIN the transaction list with state machines
|
|
||||||
*/
|
|
||||||
val gatheredTransactionDataList = partiallyResolvedTransactions.leftOuterJoin(
|
|
||||||
smTxMappingList,
|
|
||||||
PartiallyResolvedTransaction::id,
|
|
||||||
StateMachineTransactionMapping::transactionId
|
|
||||||
) { transaction, mappings ->
|
|
||||||
GatheredTransactionData(
|
|
||||||
transaction,
|
|
||||||
mappings.map { mapping ->
|
|
||||||
stateMachineDataMap.getObservableValue(mapping.stateMachineRunId)
|
|
||||||
}.flatten().filterNotNull()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,22 @@
|
|||||||
package net.corda.client.model
|
package net.corda.client.model
|
||||||
|
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
import javafx.collections.ObservableList
|
import javafx.collections.ObservableList
|
||||||
import kotlinx.support.jdk8.collections.removeIf
|
import kotlinx.support.jdk8.collections.removeIf
|
||||||
|
import net.corda.client.fxutils.firstOrDefault
|
||||||
|
import net.corda.client.fxutils.firstOrNullObservable
|
||||||
import net.corda.client.fxutils.foldToObservableList
|
import net.corda.client.fxutils.foldToObservableList
|
||||||
import net.corda.client.fxutils.map
|
import net.corda.client.fxutils.map
|
||||||
import net.corda.core.crypto.CompositeKey
|
import net.corda.core.crypto.CompositeKey
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.services.NetworkMapCache
|
import net.corda.core.node.services.NetworkMapCache
|
||||||
import net.corda.node.services.network.NetworkMapService
|
import net.corda.node.services.network.NetworkMapService
|
||||||
|
import java.security.PublicKey
|
||||||
|
|
||||||
class NetworkIdentityModel {
|
class NetworkIdentityModel {
|
||||||
private val networkIdentityObservable by observable(NodeMonitorModel::networkMap)
|
private val networkIdentityObservable by observable(NodeMonitorModel::networkMap)
|
||||||
|
|
||||||
private val networkIdentities: ObservableList<NodeInfo> =
|
val networkIdentities: ObservableList<NodeInfo> =
|
||||||
networkIdentityObservable.foldToObservableList(Unit) { update, _accumulator, observableList ->
|
networkIdentityObservable.foldToObservableList(Unit) { update, _accumulator, observableList ->
|
||||||
observableList.removeIf {
|
observableList.removeIf {
|
||||||
when (update.type) {
|
when (update.type) {
|
||||||
@ -31,10 +35,15 @@ class NetworkIdentityModel {
|
|||||||
val myIdentity = rpcProxy.map { it?.nodeIdentity() }
|
val myIdentity = rpcProxy.map { it?.nodeIdentity() }
|
||||||
|
|
||||||
private fun NodeInfo.isCordaService(): Boolean {
|
private fun NodeInfo.isCordaService(): Boolean {
|
||||||
|
// TODO: better way to identify Corda service?
|
||||||
return advertisedServices.any { it.info.type == NetworkMapService.type || it.info.type.isNotary() }
|
return advertisedServices.any { it.info.type == NetworkMapService.type || it.info.type.isNotary() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun lookup(compositeKey: CompositeKey): NodeInfo? {
|
fun lookup(compositeKey: CompositeKey): ObservableValue<NodeInfo?> = parties.firstOrDefault(notaries.firstOrNullObservable { it.notaryIdentity.owningKey == compositeKey }) {
|
||||||
return parties.firstOrNull { it.legalIdentity.owningKey == compositeKey } ?: notaries.firstOrNull { it.notaryIdentity.owningKey == compositeKey }
|
it.legalIdentity.owningKey == compositeKey
|
||||||
|
}
|
||||||
|
|
||||||
|
fun lookup(publicKey: PublicKey): ObservableValue<NodeInfo?> = parties.firstOrDefault(notaries.firstOrNullObservable { it.notaryIdentity.owningKey.keys.any { it == publicKey } }) {
|
||||||
|
it.legalIdentity.owningKey.keys.any { it == publicKey }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,5 @@
|
|||||||
myLegalName = "Vast Global MegaCorp, Ltd"
|
myLegalName = "Vast Global MegaCorp, Ltd"
|
||||||
nearestCity = "The Moon"
|
nearestCity = "London"
|
||||||
emailAddress = "admin@company.com"
|
emailAddress = "admin@company.com"
|
||||||
exportJMXto = "http"
|
exportJMXto = "http"
|
||||||
keyStorePassword = "cordacadevpass"
|
keyStorePassword = "cordacadevpass"
|
||||||
|
@ -48,7 +48,7 @@ dependencies {
|
|||||||
testCompile group: 'junit', name: 'junit', version: '4.11'
|
testCompile group: 'junit', name: 'junit', version: '4.11'
|
||||||
|
|
||||||
// TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's.
|
// TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's.
|
||||||
compile 'no.tornado:tornadofx:1.5.6'
|
compile 'no.tornado:tornadofx:1.5.7'
|
||||||
|
|
||||||
// Corda Core: Data structures and basic types needed to work with Corda.
|
// Corda Core: Data structures and basic types needed to work with Corda.
|
||||||
compile project(':core')
|
compile project(':core')
|
||||||
|
@ -8,19 +8,21 @@ import javafx.scene.control.ButtonType
|
|||||||
import javafx.scene.image.Image
|
import javafx.scene.image.Image
|
||||||
import javafx.stage.Stage
|
import javafx.stage.Stage
|
||||||
import jfxtras.resources.JFXtrasFontRoboto
|
import jfxtras.resources.JFXtrasFontRoboto
|
||||||
|
import net.corda.client.CordaRPCClient
|
||||||
import net.corda.client.mock.EventGenerator
|
import net.corda.client.mock.EventGenerator
|
||||||
import net.corda.client.model.Models
|
import net.corda.client.model.Models
|
||||||
import net.corda.client.model.NodeMonitorModel
|
import net.corda.client.model.observableValue
|
||||||
import net.corda.core.node.services.ServiceInfo
|
import net.corda.core.node.services.ServiceInfo
|
||||||
|
import net.corda.core.node.services.ServiceType
|
||||||
import net.corda.explorer.model.CordaViewModel
|
import net.corda.explorer.model.CordaViewModel
|
||||||
|
import net.corda.explorer.model.SettingsModel
|
||||||
import net.corda.explorer.views.*
|
import net.corda.explorer.views.*
|
||||||
import net.corda.explorer.views.cordapps.CashViewer
|
import net.corda.explorer.views.cordapps.cash.CashViewer
|
||||||
import net.corda.flows.CashFlow
|
import net.corda.flows.CashFlow
|
||||||
import net.corda.node.driver.PortAllocation
|
import net.corda.node.driver.PortAllocation
|
||||||
import net.corda.node.driver.driver
|
import net.corda.node.driver.driver
|
||||||
import net.corda.node.services.User
|
import net.corda.node.services.User
|
||||||
import net.corda.node.services.config.FullNodeConfiguration
|
import net.corda.node.services.config.FullNodeConfiguration
|
||||||
import net.corda.node.services.config.configureTestSSL
|
|
||||||
import net.corda.node.services.messaging.ArtemisMessagingComponent
|
import net.corda.node.services.messaging.ArtemisMessagingComponent
|
||||||
import net.corda.node.services.messaging.startFlow
|
import net.corda.node.services.messaging.startFlow
|
||||||
import net.corda.node.services.startFlowPermission
|
import net.corda.node.services.startFlowPermission
|
||||||
@ -35,24 +37,25 @@ import java.util.*
|
|||||||
/**
|
/**
|
||||||
* Main class for Explorer, you will need Tornado FX to run the explorer.
|
* Main class for Explorer, you will need Tornado FX to run the explorer.
|
||||||
*/
|
*/
|
||||||
class Main : App() {
|
class Main : App(MainView::class) {
|
||||||
override val primaryView = MainView::class
|
|
||||||
private val loginView by inject<LoginView>()
|
private val loginView by inject<LoginView>()
|
||||||
|
private val fullscreen by observableValue(SettingsModel::fullscreenProperty)
|
||||||
|
|
||||||
override fun start(stage: Stage) {
|
override fun start(stage: Stage) {
|
||||||
// Login to Corda node
|
// Login to Corda node
|
||||||
loginView.login { hostAndPort, username, password ->
|
|
||||||
Models.get<NodeMonitorModel>(MainView::class).register(hostAndPort, configureTestSSL(), username, password)
|
|
||||||
}
|
|
||||||
super.start(stage)
|
super.start(stage)
|
||||||
stage.minHeight = 600.0
|
stage.minHeight = 600.0
|
||||||
stage.minWidth = 800.0
|
stage.minWidth = 800.0
|
||||||
|
stage.isFullScreen = fullscreen.value
|
||||||
stage.setOnCloseRequest {
|
stage.setOnCloseRequest {
|
||||||
val button = Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to exit Corda explorer?").apply {
|
val button = Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to exit Corda explorer?").apply {
|
||||||
initOwner(stage.scene.window)
|
initOwner(stage.scene.window)
|
||||||
}.showAndWait().get()
|
}.showAndWait().get()
|
||||||
if (button != ButtonType.OK) it.consume()
|
if (button != ButtonType.OK) it.consume()
|
||||||
}
|
}
|
||||||
|
stage.hide()
|
||||||
|
loginView.login()
|
||||||
|
stage.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -103,32 +106,55 @@ fun main(args: Array<String>) {
|
|||||||
val portAllocation = PortAllocation.Incremental(20000)
|
val portAllocation = PortAllocation.Incremental(20000)
|
||||||
driver(portAllocation = portAllocation) {
|
driver(portAllocation = portAllocation) {
|
||||||
val user = User("user1", "test", permissions = setOf(startFlowPermission<CashFlow>()))
|
val user = User("user1", "test", permissions = setOf(startFlowPermission<CashFlow>()))
|
||||||
|
// TODO : Supported flow should be exposed somehow from the node instead of set of ServiceInfo.
|
||||||
val notary = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
val notary = startNode("Notary", advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||||
val alice = startNode("Alice", rpcUsers = arrayListOf(user))
|
val alice = startNode("Alice", rpcUsers = arrayListOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))))
|
||||||
val bob = startNode("Bob", rpcUsers = arrayListOf(user))
|
val bob = startNode("Bob", rpcUsers = arrayListOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))))
|
||||||
|
val issuer = startNode("Royal Mint", rpcUsers = arrayListOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))))
|
||||||
|
|
||||||
val notaryNode = notary.get()
|
val notaryNode = notary.get()
|
||||||
val aliceNode = alice.get()
|
val aliceNode = alice.get()
|
||||||
val bobNode = bob.get()
|
val bobNode = bob.get()
|
||||||
|
val issuerNode = issuer.get()
|
||||||
|
|
||||||
arrayOf(notaryNode, aliceNode, bobNode).forEach {
|
arrayOf(notaryNode, aliceNode, bobNode, issuerNode).forEach {
|
||||||
println("${it.nodeInfo.legalIdentity} started on ${ArtemisMessagingComponent.toHostAndPort(it.nodeInfo.address)}")
|
println("${it.nodeInfo.legalIdentity} started on ${ArtemisMessagingComponent.toHostAndPort(it.nodeInfo.address)}")
|
||||||
}
|
}
|
||||||
// Register with alice to use alice's RPC proxy to create random events.
|
// 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), user.username, user.password)
|
val aliceClient = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(aliceNode.nodeInfo.address), FullNodeConfiguration(aliceNode.config))
|
||||||
val rpcProxy = Models.get<NodeMonitorModel>(Main::class).proxyObservable.get()
|
aliceClient.start(user.username, user.password)
|
||||||
|
val aliceRPC = aliceClient.proxy()
|
||||||
|
|
||||||
|
val bobClient = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(bobNode.nodeInfo.address), FullNodeConfiguration(bobNode.config))
|
||||||
|
bobClient.start(user.username, user.password)
|
||||||
|
val bobRPC = bobClient.proxy()
|
||||||
|
|
||||||
|
val issuerClient = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(issuerNode.nodeInfo.address), FullNodeConfiguration(issuerNode.config))
|
||||||
|
issuerClient.start(user.username, user.password)
|
||||||
|
val bocRPC = issuerClient.proxy()
|
||||||
|
|
||||||
for (i in 0..10) {
|
|
||||||
Thread.sleep(500)
|
|
||||||
val eventGenerator = EventGenerator(
|
val eventGenerator = EventGenerator(
|
||||||
parties = listOf(aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity),
|
parties = listOf(aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity, issuerNode.nodeInfo.legalIdentity),
|
||||||
notary = notaryNode.nodeInfo.notaryIdentity
|
notary = notaryNode.nodeInfo.notaryIdentity
|
||||||
)
|
)
|
||||||
eventGenerator.clientToServiceCommandGenerator.map { command ->
|
|
||||||
rpcProxy?.startFlow(::CashFlow, command)
|
for (i in 0..1000) {
|
||||||
|
Thread.sleep(500)
|
||||||
|
listOf(aliceRPC, bobRPC).forEach {
|
||||||
|
eventGenerator.clientCommandGenerator.map { command ->
|
||||||
|
it.startFlow(::CashFlow, command)
|
||||||
Unit
|
Unit
|
||||||
}.generate(SplittableRandom())
|
}.generate(SplittableRandom())
|
||||||
}
|
}
|
||||||
|
eventGenerator.bankOfCordaCommandGenerator.map { command ->
|
||||||
|
bocRPC.startFlow(::CashFlow, command)
|
||||||
|
Unit
|
||||||
|
}.generate(SplittableRandom())
|
||||||
|
}
|
||||||
|
|
||||||
|
aliceClient.close()
|
||||||
|
bobClient.close()
|
||||||
|
issuerClient.close()
|
||||||
waitForAllNodesToFinish()
|
waitForAllNodesToFinish()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,16 +1,19 @@
|
|||||||
package net.corda.explorer.identicon
|
package net.corda.explorer.identicon
|
||||||
|
|
||||||
import com.google.common.base.Splitter
|
import com.google.common.base.Splitter
|
||||||
import net.corda.core.crypto.SecureHash
|
import com.google.common.cache.CacheBuilder
|
||||||
|
import com.google.common.cache.CacheLoader
|
||||||
import javafx.scene.SnapshotParameters
|
import javafx.scene.SnapshotParameters
|
||||||
import javafx.scene.canvas.Canvas
|
import javafx.scene.canvas.Canvas
|
||||||
import javafx.scene.canvas.GraphicsContext
|
import javafx.scene.canvas.GraphicsContext
|
||||||
import javafx.scene.control.ContentDisplay
|
import javafx.scene.control.ContentDisplay
|
||||||
import javafx.scene.control.Tooltip
|
import javafx.scene.control.Tooltip
|
||||||
|
import javafx.scene.image.Image
|
||||||
import javafx.scene.image.ImageView
|
import javafx.scene.image.ImageView
|
||||||
import javafx.scene.image.WritableImage
|
import javafx.scene.image.WritableImage
|
||||||
import javafx.scene.paint.Color
|
import javafx.scene.paint.Color
|
||||||
import javafx.scene.text.TextAlignment
|
import javafx.scene.text.TextAlignment
|
||||||
|
import net.corda.core.crypto.SecureHash
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (The MIT License)
|
* (The MIT License)
|
||||||
@ -39,9 +42,7 @@ import javafx.scene.text.TextAlignment
|
|||||||
* And has been modified to Kotlin and JavaFX instead of Java code using AWT
|
* And has been modified to Kotlin and JavaFX instead of Java code using AWT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class IdenticonRenderer {
|
object IdenticonRenderer {
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
/**
|
||||||
* Each patch is a polygon created from a list of vertices on a 5 by 5 grid.
|
* 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
|
* Vertices are numbered from 0 to 24, starting from top-left corner of the
|
||||||
@ -71,7 +72,12 @@ class IdenticonRenderer {
|
|||||||
private val PATCH_INVERTED: Byte = 2
|
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 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 val renderingSize = 30.0
|
||||||
|
|
||||||
|
private val cache = CacheBuilder.newBuilder().build(CacheLoader.from<SecureHash, Image> { key ->
|
||||||
|
key?.let { render(key.hashCode(), renderingSize) }
|
||||||
|
})
|
||||||
|
|
||||||
private class Patch(private val byteArray: ByteArray) {
|
private class Patch(private val byteArray: ByteArray) {
|
||||||
fun x(patchSize: Double): DoubleArray {
|
fun x(patchSize: Double): DoubleArray {
|
||||||
@ -85,13 +91,17 @@ class IdenticonRenderer {
|
|||||||
val size = byteArray.size
|
val size = byteArray.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getIdenticon(hash: SecureHash): Image {
|
||||||
|
return cache.get(hash)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns rendered identicon image for given identicon code.
|
* Returns rendered identicon image for given identicon code.
|
||||||
* Size of the returned identicon image is determined by patchSize set using
|
* Size of the returned identicon image is determined by patchSize set using
|
||||||
* [setPatchSize]. Since a 9-block identicon consists of 3x3 patches,
|
* [setPatchSize]. Since a 9-block identicon consists of 3x3 patches,
|
||||||
* width and height will be 3 times the patch size.
|
* width and height will be 3 times the patch size.
|
||||||
*/
|
*/
|
||||||
fun render(code: Int, patchSize: Double, backgroundColor: Color = Color.WHITE): WritableImage {
|
private fun render(code: Int, patchSize: Double, backgroundColor: Color = Color.WHITE): Image {
|
||||||
// decode the code into parts
|
// decode the code into parts
|
||||||
val middleType = intArrayOf(0, 4, 8, 15)[code and 0x3] // bit 0-1: middle patch type
|
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 middleInvert = code shr 2 and 0x1 != 0 // bit 2: middle invert
|
||||||
@ -177,18 +187,22 @@ class IdenticonRenderer {
|
|||||||
val dx = (c1.red - c2.red) * 256
|
val dx = (c1.red - c2.red) * 256
|
||||||
val dy = (c1.green - c2.green) * 256
|
val dy = (c1.green - c2.green) * 256
|
||||||
val dz = (c1.blue - c2.blue) * 256
|
val dz = (c1.blue - c2.blue) * 256
|
||||||
return Math.sqrt(dx * dx + dy * dy + dz * dz.toDouble()).toFloat()
|
return Math.sqrt(dx * dx + dy * dy + dz * dz).toFloat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun identicon(secureHash: SecureHash, size: Double): WritableImage {
|
fun identicon(secureHash: SecureHash, size: Double): ImageView {
|
||||||
return IdenticonRenderer().render(secureHash.hashCode(), size)
|
return ImageView(IdenticonRenderer.getIdenticon(secureHash)).apply {
|
||||||
|
isPreserveRatio = true
|
||||||
|
fitWidth = size
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun identiconToolTip(secureHash: SecureHash): Tooltip {
|
fun identiconToolTip(secureHash: SecureHash): Tooltip {
|
||||||
return Tooltip(Splitter.fixedLength(16).split("$secureHash").joinToString("\n")).apply {
|
return Tooltip(Splitter.fixedLength(16).split("$secureHash").joinToString("\n")).apply {
|
||||||
contentDisplay = ContentDisplay.TOP
|
contentDisplay = ContentDisplay.TOP
|
||||||
textAlignment = TextAlignment.CENTER
|
textAlignment = TextAlignment.CENTER
|
||||||
graphic = ImageView(identicon(secureHash, 30.0))
|
graphic = identicon(secureHash, 90.0)
|
||||||
|
isAutoHide = false
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,6 +2,7 @@ package net.corda.explorer.model
|
|||||||
|
|
||||||
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
|
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
|
||||||
import javafx.beans.property.SimpleObjectProperty
|
import javafx.beans.property.SimpleObjectProperty
|
||||||
|
import javafx.collections.ObservableList
|
||||||
import javafx.scene.Node
|
import javafx.scene.Node
|
||||||
import tornadofx.View
|
import tornadofx.View
|
||||||
import tornadofx.find
|
import tornadofx.find
|
||||||
@ -24,10 +25,12 @@ class CordaViewModel {
|
|||||||
* TODO : "goto" functionality?
|
* TODO : "goto" functionality?
|
||||||
*/
|
*/
|
||||||
abstract class CordaView(title: String? = null) : View(title) {
|
abstract class CordaView(title: String? = null) : View(title) {
|
||||||
abstract val widget: Node?
|
open val widgets: ObservableList<CordaWidget> = emptyList<CordaWidget>().observable()
|
||||||
abstract val icon: FontAwesomeIcon
|
abstract val icon: FontAwesomeIcon
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (title == null) super.title = javaClass.simpleName
|
if (title == null) super.title = javaClass.simpleName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class CordaWidget(val name: String, val node: Node)
|
@ -1,17 +1,22 @@
|
|||||||
package net.corda.explorer.model
|
package net.corda.explorer.model
|
||||||
|
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
import net.corda.client.fxutils.AmountBindings
|
import net.corda.client.fxutils.AmountBindings
|
||||||
import net.corda.client.model.ExchangeRate
|
import net.corda.client.model.ExchangeRate
|
||||||
import net.corda.client.model.ExchangeRateModel
|
import net.corda.client.model.ExchangeRateModel
|
||||||
import net.corda.client.model.observableValue
|
import net.corda.client.model.observableValue
|
||||||
import net.corda.core.contracts.Amount
|
import net.corda.core.contracts.Amount
|
||||||
import javafx.beans.value.ObservableValue
|
import net.corda.core.contracts.CHF
|
||||||
|
import net.corda.core.contracts.GBP
|
||||||
|
import net.corda.core.contracts.USD
|
||||||
import org.fxmisc.easybind.EasyBind
|
import org.fxmisc.easybind.EasyBind
|
||||||
|
import tornadofx.observable
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class ReportingCurrencyModel {
|
class ReportingCurrencyModel {
|
||||||
private val exchangeRate: ObservableValue<ExchangeRate> by observableValue(ExchangeRateModel::exchangeRate)
|
private val exchangeRate: ObservableValue<ExchangeRate> by observableValue(ExchangeRateModel::exchangeRate)
|
||||||
val reportingCurrency: ObservableValue<Currency> by observableValue(SettingsModel::reportingCurrency)
|
val reportingCurrency by observableValue(SettingsModel::reportingCurrencyProperty)
|
||||||
|
val supportedCurrencies = setOf(USD, GBP, CHF).toList().observable()
|
||||||
/**
|
/**
|
||||||
* This stream provides a stream of exchange() functions that updates when either the reporting currency or the
|
* This stream provides a stream of exchange() functions that updates when either the reporting currency or the
|
||||||
* exchange rates change
|
* exchange rates change
|
||||||
|
@ -1,11 +1,94 @@
|
|||||||
package net.corda.explorer.model
|
package net.corda.explorer.model
|
||||||
|
|
||||||
import net.corda.core.contracts.USD
|
import javafx.beans.InvalidationListener
|
||||||
|
import javafx.beans.Observable
|
||||||
|
import javafx.beans.property.ObjectProperty
|
||||||
import javafx.beans.property.SimpleObjectProperty
|
import javafx.beans.property.SimpleObjectProperty
|
||||||
|
import net.corda.core.contracts.currency
|
||||||
|
import tornadofx.Component
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.reflect.KMutableProperty1
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
import kotlin.reflect.jvm.javaType
|
||||||
|
|
||||||
class SettingsModel {
|
class SettingsModel(path: Path = Paths.get("conf")) : Component(), Observable {
|
||||||
|
// Using CordaExplorer as config file name instead of TornadoFX default.
|
||||||
|
private val path = {
|
||||||
|
if (!Files.exists(path)) Files.createDirectories(path)
|
||||||
|
path.resolve("CordaExplorer.properties")
|
||||||
|
}()
|
||||||
|
private val listeners = mutableListOf<InvalidationListener>()
|
||||||
|
|
||||||
val reportingCurrency: SimpleObjectProperty<Currency> = SimpleObjectProperty(USD)
|
// Delegate to config.
|
||||||
|
private var rememberMe: Boolean by config
|
||||||
|
private var host: String by config
|
||||||
|
private var port: String by config
|
||||||
|
private var username: String by config
|
||||||
|
private var reportingCurrency: Currency by config
|
||||||
|
private var fullscreen: Boolean by config
|
||||||
|
|
||||||
|
// Create observable Properties.
|
||||||
|
val reportingCurrencyProperty = writableConfigProperty(SettingsModel::reportingCurrency)
|
||||||
|
val rememberMeProperty = writableConfigProperty(SettingsModel::rememberMe)
|
||||||
|
val hostProperty = writableConfigProperty(SettingsModel::host)
|
||||||
|
val portProperty = writableConfigProperty(SettingsModel::port)
|
||||||
|
val usernameProperty = writableConfigProperty(SettingsModel::username)
|
||||||
|
val fullscreenProperty = writableConfigProperty(SettingsModel::fullscreen)
|
||||||
|
|
||||||
|
init {
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load config from properties file.
|
||||||
|
fun load() = config.apply {
|
||||||
|
clear()
|
||||||
|
if (Files.exists(path)) Files.newInputStream(path).use { load(it) }
|
||||||
|
listeners.forEach { it.invalidated(this@SettingsModel) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save all changes in memory to properties file.
|
||||||
|
fun commit() = Files.newOutputStream(path).use { config.store(it, "") }
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private operator fun <T> Properties.getValue(receiver: Any, metadata: KProperty<*>): T {
|
||||||
|
return when (metadata.returnType.javaType) {
|
||||||
|
String::class.java -> string(metadata.name, "") as T
|
||||||
|
Int::class.java -> string(metadata.name, "0").toInt() as T
|
||||||
|
Boolean::class.java -> boolean(metadata.name) as T
|
||||||
|
Currency::class.java -> currency(string(metadata.name, "USD")) as T
|
||||||
|
else -> throw IllegalArgumentException("Unsupported type ${metadata.returnType}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private operator fun <T> Properties.setValue(receiver: Any, metadata: KProperty<*>, value: T) {
|
||||||
|
set(metadata.name to value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observable implementation for notifying properties when config reloaded.
|
||||||
|
override fun removeListener(listener: InvalidationListener?) {
|
||||||
|
listener?.let { listeners.remove(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addListener(listener: InvalidationListener?) {
|
||||||
|
listener?.let { listeners.add(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writable Object Property which write through to delegated property.
|
||||||
|
private fun <S : Observable, T> S.writableConfigProperty(k: KMutableProperty1<S, T>): ObjectProperty<T> {
|
||||||
|
val s = this
|
||||||
|
return object : SimpleObjectProperty<T>(k.get(this)) {
|
||||||
|
init {
|
||||||
|
// Add listener to reset value when config reloaded.
|
||||||
|
s.addListener { value = k.get(s) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun set(newValue: T) {
|
||||||
|
super.set(newValue)
|
||||||
|
k.set(s, newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -2,12 +2,13 @@ package net.corda.explorer.views
|
|||||||
|
|
||||||
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
|
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
|
||||||
import javafx.beans.binding.Bindings
|
import javafx.beans.binding.Bindings
|
||||||
|
import javafx.collections.ObservableList
|
||||||
import javafx.scene.Node
|
import javafx.scene.Node
|
||||||
import javafx.scene.Parent
|
import javafx.scene.Parent
|
||||||
import javafx.scene.control.TitledPane
|
import javafx.scene.control.TitledPane
|
||||||
import javafx.scene.input.MouseButton
|
import javafx.scene.input.MouseButton
|
||||||
import javafx.scene.layout.TilePane
|
import javafx.scene.layout.TilePane
|
||||||
import net.corda.client.fxutils.filterNotNull
|
import net.corda.client.fxutils.concatenate
|
||||||
import net.corda.client.fxutils.map
|
import net.corda.client.fxutils.map
|
||||||
import net.corda.client.model.observableList
|
import net.corda.client.model.observableList
|
||||||
import net.corda.client.model.writableValue
|
import net.corda.client.model.writableValue
|
||||||
@ -17,17 +18,27 @@ import net.corda.explorer.model.CordaViewModel
|
|||||||
class Dashboard : CordaView() {
|
class Dashboard : CordaView() {
|
||||||
override val root: Parent by fxml()
|
override val root: Parent by fxml()
|
||||||
override val icon = FontAwesomeIcon.DASHBOARD
|
override val icon = FontAwesomeIcon.DASHBOARD
|
||||||
override val widget: Node? = null
|
|
||||||
private val tilePane: TilePane by fxid()
|
private val tilePane: TilePane by fxid()
|
||||||
private val template: TitledPane by fxid()
|
private val template: TitledPane by fxid()
|
||||||
|
|
||||||
private val selectedView by writableValue(CordaViewModel::selectedView)
|
private val selectedView by writableValue(CordaViewModel::selectedView)
|
||||||
private val registeredViews by observableList(CordaViewModel::registeredViews)
|
private val registeredViews by observableList(CordaViewModel::registeredViews)
|
||||||
|
// This needed to be here or else it will get GCed and won't get notified.
|
||||||
|
private val widgetPanes = registeredViews.map { getWidget(it) }.concatenate()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val widgetPanes = registeredViews.map { view ->
|
Bindings.bindContent(tilePane.children, widgetPanes)
|
||||||
view.widget?.let {
|
// Dynamically change column count and width according to the window size.
|
||||||
TitledPane(view.title, it).apply {
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getWidget(view: CordaView): ObservableList<Node> {
|
||||||
|
return view.widgets.map {
|
||||||
|
TitledPane(it.name, it.node).apply {
|
||||||
styleClass.addAll(template.styleClass)
|
styleClass.addAll(template.styleClass)
|
||||||
collapsibleProperty().bind(template.collapsibleProperty())
|
collapsibleProperty().bind(template.collapsibleProperty())
|
||||||
setOnMouseClicked {
|
setOnMouseClicked {
|
||||||
@ -37,15 +48,6 @@ class Dashboard : CordaView() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.filterNotNull()
|
}
|
||||||
|
|
||||||
Bindings.bindContent(tilePane.children, widgetPanes)
|
|
||||||
|
|
||||||
// Dynamically change column count and width according to the window size.
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,15 +1,20 @@
|
|||||||
package net.corda.explorer.views
|
package net.corda.explorer.views
|
||||||
|
|
||||||
import javafx.application.Platform
|
import javafx.application.Platform
|
||||||
|
import javafx.beans.value.ObservableValue
|
||||||
import javafx.event.EventTarget
|
import javafx.event.EventTarget
|
||||||
import javafx.geometry.Pos
|
import javafx.geometry.Pos
|
||||||
import javafx.scene.Parent
|
import javafx.scene.Parent
|
||||||
|
import javafx.scene.control.TextField
|
||||||
import javafx.scene.layout.GridPane
|
import javafx.scene.layout.GridPane
|
||||||
import javafx.scene.layout.Priority
|
import javafx.scene.layout.Priority
|
||||||
import javafx.scene.text.TextAlignment
|
import javafx.scene.text.TextAlignment
|
||||||
import javafx.util.StringConverter
|
import javafx.util.StringConverter
|
||||||
|
import net.corda.client.model.Models
|
||||||
|
import tornadofx.View
|
||||||
import tornadofx.gridpane
|
import tornadofx.gridpane
|
||||||
import tornadofx.label
|
import tornadofx.label
|
||||||
|
import tornadofx.textfield
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to reduce boiler plate code
|
* Helper method to reduce boiler plate code
|
||||||
@ -50,6 +55,9 @@ fun runInFxApplicationThread(block: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Under construction label for empty page.
|
||||||
|
*/
|
||||||
fun EventTarget.underConstruction(): Parent {
|
fun EventTarget.underConstruction(): Parent {
|
||||||
return gridpane {
|
return gridpane {
|
||||||
label("Under Construction...") {
|
label("Under Construction...") {
|
||||||
@ -61,3 +69,15 @@ fun EventTarget.underConstruction(): Parent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copyable label component using textField, with css to hide the textfield border.
|
||||||
|
*/
|
||||||
|
fun EventTarget.copyableLabel(value: ObservableValue<String>? = null, op: (TextField.() -> Unit)? = null) = textfield {
|
||||||
|
value?.let { textProperty().bind(it) }
|
||||||
|
op?.invoke(this)
|
||||||
|
isEditable = false
|
||||||
|
styleClass.add("copyable-label")
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified M : Any> View.getModel(): M = Models.get(M::class, this.javaClass.kotlin)
|
||||||
|
@ -3,6 +3,10 @@ package net.corda.explorer.views
|
|||||||
import com.google.common.net.HostAndPort
|
import com.google.common.net.HostAndPort
|
||||||
import javafx.beans.property.SimpleIntegerProperty
|
import javafx.beans.property.SimpleIntegerProperty
|
||||||
import javafx.scene.control.*
|
import javafx.scene.control.*
|
||||||
|
import net.corda.client.model.NodeMonitorModel
|
||||||
|
import net.corda.client.model.objectProperty
|
||||||
|
import net.corda.explorer.model.SettingsModel
|
||||||
|
import net.corda.node.services.config.configureTestSSL
|
||||||
import org.controlsfx.dialog.ExceptionDialog
|
import org.controlsfx.dialog.ExceptionDialog
|
||||||
import tornadofx.View
|
import tornadofx.View
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
@ -10,32 +14,51 @@ import kotlin.system.exitProcess
|
|||||||
class LoginView : View() {
|
class LoginView : View() {
|
||||||
override val root by fxml<DialogPane>()
|
override val root by fxml<DialogPane>()
|
||||||
|
|
||||||
private val host by fxid<TextField>()
|
private val hostTextField by fxid<TextField>()
|
||||||
private val port by fxid<TextField>()
|
private val portTextField by fxid<TextField>()
|
||||||
private val username by fxid<TextField>()
|
private val usernameTextField by fxid<TextField>()
|
||||||
private val password by fxid<PasswordField>()
|
private val passwordTextField by fxid<PasswordField>()
|
||||||
|
private val rememberMeCheckBox by fxid<CheckBox>()
|
||||||
|
private val fullscreenCheckBox by fxid<CheckBox>()
|
||||||
private val portProperty = SimpleIntegerProperty()
|
private val portProperty = SimpleIntegerProperty()
|
||||||
|
|
||||||
fun login(loginFunction: (HostAndPort, String, String) -> Unit) {
|
private val rememberMe by objectProperty(SettingsModel::rememberMeProperty)
|
||||||
|
private val username by objectProperty(SettingsModel::usernameProperty)
|
||||||
|
private val host by objectProperty(SettingsModel::hostProperty)
|
||||||
|
private val port by objectProperty(SettingsModel::portProperty)
|
||||||
|
private val fullscreen by objectProperty(SettingsModel::fullscreenProperty)
|
||||||
|
|
||||||
|
fun login() {
|
||||||
val status = Dialog<LoginStatus>().apply {
|
val status = Dialog<LoginStatus>().apply {
|
||||||
dialogPane = root
|
dialogPane = root
|
||||||
setResultConverter {
|
setResultConverter {
|
||||||
when (it?.buttonData) {
|
when (it?.buttonData) {
|
||||||
ButtonBar.ButtonData.OK_DONE -> try {
|
ButtonBar.ButtonData.OK_DONE -> try {
|
||||||
|
root.isDisable = true
|
||||||
// TODO : Run this async to avoid UI lockup.
|
// TODO : Run this async to avoid UI lockup.
|
||||||
loginFunction(HostAndPort.fromParts(host.text, portProperty.value), username.text, password.text)
|
// TODO : Use proper SSL certificate.
|
||||||
|
getModel<NodeMonitorModel>().register(HostAndPort.fromParts(hostTextField.text, portProperty.value), configureTestSSL(), usernameTextField.text, passwordTextField.text)
|
||||||
|
if (!rememberMe.value) {
|
||||||
|
username.value = ""
|
||||||
|
host.value = ""
|
||||||
|
port.value = ""
|
||||||
|
}
|
||||||
|
getModel<SettingsModel>().commit()
|
||||||
LoginStatus.loggedIn
|
LoginStatus.loggedIn
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// TODO : Handle this in a more user friendly way.
|
// TODO : Handle this in a more user friendly way.
|
||||||
|
e.printStackTrace()
|
||||||
ExceptionDialog(e).apply { initOwner(root.scene.window) }.showAndWait()
|
ExceptionDialog(e).apply { initOwner(root.scene.window) }.showAndWait()
|
||||||
LoginStatus.exception
|
LoginStatus.exception
|
||||||
|
} finally {
|
||||||
|
root.isDisable = false
|
||||||
}
|
}
|
||||||
else -> LoginStatus.exited
|
else -> LoginStatus.exited
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setOnCloseRequest {
|
setOnCloseRequest {
|
||||||
if (result == LoginStatus.exited) {
|
if (result == LoginStatus.exited) {
|
||||||
val button = Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to exit Corda explorer?").apply {
|
val button = Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to exit Corda Explorer?").apply {
|
||||||
initOwner(root.scene.window)
|
initOwner(root.scene.window)
|
||||||
}.showAndWait().get()
|
}.showAndWait().get()
|
||||||
if (button == ButtonType.OK) {
|
if (button == ButtonType.OK) {
|
||||||
@ -44,12 +67,17 @@ class LoginView : View() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.showAndWait().get()
|
}.showAndWait().get()
|
||||||
if (status != LoginStatus.loggedIn) login(loginFunction)
|
if (status != LoginStatus.loggedIn) login()
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Restrict text field to Integer only.
|
// Restrict text field to Integer only.
|
||||||
port.textFormatter = intFormatter().apply { portProperty.bind(this.valueProperty()) }
|
portTextField.textFormatter = intFormatter().apply { portProperty.bind(this.valueProperty()) }
|
||||||
|
rememberMeCheckBox.selectedProperty().bindBidirectional(rememberMe)
|
||||||
|
fullscreenCheckBox.selectedProperty().bindBidirectional(fullscreen)
|
||||||
|
usernameTextField.textProperty().bindBidirectional(username)
|
||||||
|
hostTextField.textProperty().bindBidirectional(host)
|
||||||
|
portTextField.textProperty().bindBidirectional(port)
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum class LoginStatus {
|
private enum class LoginStatus {
|
||||||
|
@ -8,12 +8,15 @@ import javafx.geometry.Pos
|
|||||||
import javafx.scene.Parent
|
import javafx.scene.Parent
|
||||||
import javafx.scene.control.ContentDisplay
|
import javafx.scene.control.ContentDisplay
|
||||||
import javafx.scene.control.MenuButton
|
import javafx.scene.control.MenuButton
|
||||||
|
import javafx.scene.control.MenuItem
|
||||||
import javafx.scene.input.MouseButton
|
import javafx.scene.input.MouseButton
|
||||||
import javafx.scene.layout.BorderPane
|
import javafx.scene.layout.BorderPane
|
||||||
import javafx.scene.layout.StackPane
|
import javafx.scene.layout.StackPane
|
||||||
import javafx.scene.layout.VBox
|
import javafx.scene.layout.VBox
|
||||||
import javafx.scene.text.Font
|
import javafx.scene.text.Font
|
||||||
import javafx.scene.text.TextAlignment
|
import javafx.scene.text.TextAlignment
|
||||||
|
import javafx.stage.Stage
|
||||||
|
import javafx.stage.WindowEvent
|
||||||
import net.corda.client.fxutils.ChosenList
|
import net.corda.client.fxutils.ChosenList
|
||||||
import net.corda.client.fxutils.map
|
import net.corda.client.fxutils.map
|
||||||
import net.corda.client.model.NetworkIdentityModel
|
import net.corda.client.model.NetworkIdentityModel
|
||||||
@ -31,6 +34,7 @@ class MainView : View() {
|
|||||||
|
|
||||||
// Inject components.
|
// Inject components.
|
||||||
private val userButton by fxid<MenuButton>()
|
private val userButton by fxid<MenuButton>()
|
||||||
|
private val exit by fxid<MenuItem>()
|
||||||
private val sidebar by fxid<VBox>()
|
private val sidebar by fxid<VBox>()
|
||||||
private val selectionBorderPane by fxid<BorderPane>()
|
private val selectionBorderPane by fxid<BorderPane>()
|
||||||
|
|
||||||
@ -46,6 +50,9 @@ class MainView : View() {
|
|||||||
init {
|
init {
|
||||||
// Header
|
// Header
|
||||||
userButton.textProperty().bind(myIdentity.map { it?.legalIdentity?.name })
|
userButton.textProperty().bind(myIdentity.map { it?.legalIdentity?.name })
|
||||||
|
exit.setOnAction {
|
||||||
|
(root.scene.window as Stage).fireEvent(WindowEvent(root.scene.window, WindowEvent.WINDOW_CLOSE_REQUEST))
|
||||||
|
}
|
||||||
// Sidebar
|
// Sidebar
|
||||||
val menuItems = registeredViews.map {
|
val menuItems = registeredViews.map {
|
||||||
// This needed to be declared val or else it will get GCed and listener unregistered.
|
// This needed to be declared val or else it will get GCed and listener unregistered.
|
||||||
|
@ -1,13 +1,100 @@
|
|||||||
package net.corda.explorer.views
|
package net.corda.explorer.views
|
||||||
|
|
||||||
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
|
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
|
||||||
|
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView
|
||||||
|
import javafx.beans.binding.Bindings
|
||||||
|
import javafx.beans.property.SimpleObjectProperty
|
||||||
import javafx.scene.Node
|
import javafx.scene.Node
|
||||||
|
import javafx.scene.Parent
|
||||||
|
import javafx.scene.control.ContentDisplay
|
||||||
|
import javafx.scene.control.Label
|
||||||
|
import javafx.scene.control.ScrollPane
|
||||||
|
import javafx.scene.layout.BorderPane
|
||||||
|
import javafx.scene.layout.Pane
|
||||||
|
import javafx.scene.layout.VBox
|
||||||
|
import javafx.scene.text.Font
|
||||||
|
import javafx.scene.text.FontWeight
|
||||||
|
import net.corda.client.fxutils.map
|
||||||
|
import net.corda.client.model.NetworkIdentityModel
|
||||||
|
import net.corda.client.model.observableList
|
||||||
|
import net.corda.client.model.observableValue
|
||||||
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.explorer.model.CordaView
|
import net.corda.explorer.model.CordaView
|
||||||
|
import tornadofx.*
|
||||||
|
|
||||||
// TODO : Construct a node map using node info and display hem on a world map.
|
// TODO : Construct a node map using node info and display them on a world map.
|
||||||
// TODO : Allow user to see transactions between nodes on a world map.
|
// TODO : Allow user to see transactions between nodes on a world map.
|
||||||
class Network : CordaView() {
|
class Network : CordaView() {
|
||||||
override val root = underConstruction()
|
override val root by fxml<Parent>()
|
||||||
override val widget: Node? = null
|
|
||||||
override val icon = FontAwesomeIcon.GLOBE
|
override val icon = FontAwesomeIcon.GLOBE
|
||||||
|
|
||||||
|
// Inject data.
|
||||||
|
val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
|
||||||
|
val notaries by observableList(NetworkIdentityModel::notaries)
|
||||||
|
val peers by observableList(NetworkIdentityModel::parties)
|
||||||
|
|
||||||
|
// Components
|
||||||
|
private val myIdentityPane by fxid<BorderPane>()
|
||||||
|
private val notaryList by fxid<VBox>()
|
||||||
|
private val peerList by fxid<VBox>()
|
||||||
|
private val mapScrollPane by fxid<ScrollPane>()
|
||||||
|
private val mapPane by fxid<Pane>()
|
||||||
|
|
||||||
|
// Create a strong ref to prevent GC.
|
||||||
|
private val notaryButtons = notaries.map { it.render() }
|
||||||
|
private val peerButtons = peers.filtered { it != myIdentity.value }.map { it.render() }
|
||||||
|
private val coordinate = Bindings.createObjectBinding({
|
||||||
|
myIdentity.value?.physicalLocation?.coordinate?.project(mapPane.width, mapPane.height, 85.0511, -85.0511, -180.0, 180.0)?.let {
|
||||||
|
Pair(it.first - 15, it.second - 10)
|
||||||
|
}
|
||||||
|
}, arrayOf(mapPane.widthProperty(), mapPane.heightProperty(), myIdentity))
|
||||||
|
|
||||||
|
private fun NodeInfo.render(): Node {
|
||||||
|
return button {
|
||||||
|
graphic = vbox {
|
||||||
|
label(this@render.legalIdentity.name) {
|
||||||
|
font = Font.font(font.family, FontWeight.BOLD, 15.0)
|
||||||
|
}
|
||||||
|
gridpane {
|
||||||
|
hgap = 5.0
|
||||||
|
vgap = 5.0
|
||||||
|
row("Pub Key :") {
|
||||||
|
copyableLabel(SimpleObjectProperty(this@render.legalIdentity.owningKey.toBase58String()))
|
||||||
|
}
|
||||||
|
row("Services :") {
|
||||||
|
label(this@render.advertisedServices.map { it.info }.joinToString(", "))
|
||||||
|
}
|
||||||
|
this@render.physicalLocation?.apply {
|
||||||
|
row("Location :") {
|
||||||
|
label(this@apply.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
myIdentityPane.centerProperty().bind(myIdentity.map { it?.render() })
|
||||||
|
Bindings.bindContent(notaryList.children, notaryButtons)
|
||||||
|
Bindings.bindContent(peerList.children, peerButtons)
|
||||||
|
|
||||||
|
val myLocation = Label("", FontAwesomeIconView(FontAwesomeIcon.DOT_CIRCLE_ALT)).apply { contentDisplay = ContentDisplay.TOP }
|
||||||
|
myLocation.textProperty().bind(myIdentity.map { it?.legalIdentity?.name })
|
||||||
|
|
||||||
|
myLocation.layoutXProperty().bind(coordinate.map { it?.first })
|
||||||
|
myLocation.layoutYProperty().bind(coordinate.map { it?.second })
|
||||||
|
mapPane.add(myLocation)
|
||||||
|
|
||||||
|
val scroll = Bindings.createObjectBinding({
|
||||||
|
val width = mapScrollPane.content.boundsInLocal.width
|
||||||
|
val height = mapScrollPane.content.boundsInLocal.height
|
||||||
|
val x = myLocation.boundsInParent.maxX
|
||||||
|
val y = myLocation.boundsInParent.minY
|
||||||
|
Pair(x / width, y / height)
|
||||||
|
}, arrayOf(coordinate))
|
||||||
|
|
||||||
|
mapScrollPane.vvalueProperty().bind(scroll.map { it.second })
|
||||||
|
mapScrollPane.hvalueProperty().bind(scroll.map { it.first })
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,31 +1,47 @@
|
|||||||
package net.corda.explorer.views
|
package net.corda.explorer.views
|
||||||
|
|
||||||
|
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
|
||||||
|
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView
|
||||||
|
import javafx.beans.Observable
|
||||||
|
import javafx.beans.binding.Bindings
|
||||||
import javafx.collections.ObservableList
|
import javafx.collections.ObservableList
|
||||||
|
import javafx.geometry.Insets
|
||||||
|
import javafx.geometry.Pos
|
||||||
import javafx.scene.Node
|
import javafx.scene.Node
|
||||||
import javafx.scene.Parent
|
import javafx.scene.Parent
|
||||||
|
import javafx.scene.control.ComboBox
|
||||||
|
import javafx.scene.control.ListCell
|
||||||
import javafx.scene.control.TextField
|
import javafx.scene.control.TextField
|
||||||
import javafx.scene.input.MouseButton
|
import javafx.scene.input.MouseButton
|
||||||
import javafx.scene.input.MouseEvent
|
import javafx.scene.input.MouseEvent
|
||||||
import net.corda.client.fxutils.ChosenList
|
import net.corda.client.fxutils.ChosenList
|
||||||
import net.corda.client.fxutils.filter
|
|
||||||
import net.corda.client.fxutils.lift
|
|
||||||
import net.corda.client.fxutils.map
|
import net.corda.client.fxutils.map
|
||||||
import tornadofx.UIComponent
|
import tornadofx.UIComponent
|
||||||
import tornadofx.observable
|
import tornadofx.observable
|
||||||
|
|
||||||
class SearchField<T>(private val data: ObservableList<T>, vararg filterCriteria: (T, String) -> Boolean) : UIComponent() {
|
/**
|
||||||
|
* Generic search bar filters [ObservableList] with provided filterCriteria.
|
||||||
|
* TODO : Predictive text?
|
||||||
|
* TODO : Regex?
|
||||||
|
*/
|
||||||
|
class SearchField<T>(private val data: ObservableList<T>, vararg filterCriteria: Pair<String, (T, String) -> Boolean>) : UIComponent() {
|
||||||
override val root: Parent by fxml()
|
override val root: Parent by fxml()
|
||||||
private val textField by fxid<TextField>()
|
private val textField by fxid<TextField>()
|
||||||
private val clearButton by fxid<Node>()
|
private val clearButton by fxid<Node>()
|
||||||
|
private val searchCategory by fxid<ComboBox<String>>()
|
||||||
|
private val ALL = "All"
|
||||||
|
|
||||||
// Currently this method apply each filter to the collection and return the collection with most matches.
|
val filteredData = ChosenList(Bindings.createObjectBinding({
|
||||||
// TODO : Allow user to chose if there are matches in multiple category.
|
val text = textField.text
|
||||||
val filteredData = ChosenList(textField.textProperty().map { text ->
|
val category = searchCategory.value
|
||||||
if (text.isBlank()) data else filterCriteria.map { criterion ->
|
data.filtered { data ->
|
||||||
data.filter({ state: T -> criterion(state, text) }.lift())
|
text.isNullOrBlank() || if (category == ALL) {
|
||||||
}.maxBy { it.size } ?: emptyList<T>().observable()
|
filterCriteria.any { it.second(data, text) }
|
||||||
})
|
} else {
|
||||||
|
filterCriteria.toMap()[category]?.invoke(data, text) ?: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, arrayOf<Observable>(textField.textProperty(), searchCategory.valueProperty())))
|
||||||
|
|
||||||
init {
|
init {
|
||||||
clearButton.setOnMouseClicked { event: MouseEvent ->
|
clearButton.setOnMouseClicked { event: MouseEvent ->
|
||||||
@ -33,5 +49,30 @@ class SearchField<T>(private val data: ObservableList<T>, vararg filterCriteria:
|
|||||||
textField.clear()
|
textField.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
searchCategory.items = filterCriteria.map { it.first }.observable()
|
||||||
|
searchCategory.items.add(0, ALL)
|
||||||
|
searchCategory.value = ALL
|
||||||
|
|
||||||
|
val search = FontAwesomeIconView(FontAwesomeIcon.SEARCH)
|
||||||
|
searchCategory.buttonCell = object : ListCell<String>() {
|
||||||
|
override fun updateItem(item: String?, empty: Boolean) {
|
||||||
|
super.updateItem(item, empty)
|
||||||
|
setText(item)
|
||||||
|
setGraphic(search)
|
||||||
|
setAlignment(Pos.CENTER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO : find a way to replace these magic numbers.
|
||||||
|
textField.paddingProperty().bind(searchCategory.widthProperty().map {
|
||||||
|
Insets(5.0, 5.0, 5.0, it.toDouble() + 10)
|
||||||
|
})
|
||||||
|
textField.promptTextProperty().bind(searchCategory.valueProperty().map {
|
||||||
|
val category = if (it == ALL) {
|
||||||
|
filterCriteria.map { it.first.toLowerCase() }.joinToString(", ")
|
||||||
|
} else {
|
||||||
|
it.toLowerCase()
|
||||||
|
}
|
||||||
|
"Filter by $category."
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,12 +1,69 @@
|
|||||||
package net.corda.explorer.views
|
package net.corda.explorer.views
|
||||||
|
|
||||||
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
|
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
|
||||||
|
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView
|
||||||
import javafx.scene.Node
|
import javafx.scene.Node
|
||||||
|
import javafx.scene.Parent
|
||||||
|
import javafx.scene.control.CheckBox
|
||||||
|
import javafx.scene.control.ComboBox
|
||||||
|
import javafx.scene.control.Label
|
||||||
|
import javafx.scene.control.TextField
|
||||||
|
import net.corda.client.fxutils.map
|
||||||
|
import net.corda.client.model.objectProperty
|
||||||
|
import net.corda.client.model.observableList
|
||||||
import net.corda.explorer.model.CordaView
|
import net.corda.explorer.model.CordaView
|
||||||
|
import net.corda.explorer.model.ReportingCurrencyModel
|
||||||
|
import net.corda.explorer.model.SettingsModel
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
// TODO : Allow user to configure preferences, e.g Reporting currency, full screen mode etc.
|
// Allow user to configure preferences, e.g Reporting currency, full screen mode etc.
|
||||||
class Settings : CordaView() {
|
class Settings : CordaView() {
|
||||||
override val root = underConstruction()
|
override val root by fxml<Parent>()
|
||||||
override val widget: Node? = null
|
|
||||||
override val icon = FontAwesomeIcon.COGS
|
override val icon = FontAwesomeIcon.COGS
|
||||||
|
|
||||||
|
// Inject Data.
|
||||||
|
private val currencies by observableList(ReportingCurrencyModel::supportedCurrencies)
|
||||||
|
private val reportingCurrencies by objectProperty(SettingsModel::reportingCurrencyProperty)
|
||||||
|
private val rememberMe by objectProperty(SettingsModel::rememberMeProperty)
|
||||||
|
private val fullscreen by objectProperty(SettingsModel::fullscreenProperty)
|
||||||
|
private val host by objectProperty(SettingsModel::hostProperty)
|
||||||
|
private val port by objectProperty(SettingsModel::portProperty)
|
||||||
|
|
||||||
|
// Components.
|
||||||
|
private val reportingCurrenciesComboBox by fxid<ComboBox<Currency>>()
|
||||||
|
private val rememberMeCheckBox by fxid<CheckBox>()
|
||||||
|
private val fullscreenCheckBox by fxid<CheckBox>()
|
||||||
|
private val hostTextField by fxid<TextField>()
|
||||||
|
private val portTextField by fxid<TextField>()
|
||||||
|
private val editCancel by fxid<Label>()
|
||||||
|
private val save by fxid<Label>()
|
||||||
|
private val clientPane by fxid<Node>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
reportingCurrenciesComboBox.items = currencies
|
||||||
|
reportingCurrenciesComboBox.valueProperty().bindBidirectional(reportingCurrencies)
|
||||||
|
rememberMeCheckBox.selectedProperty().bindBidirectional(rememberMe)
|
||||||
|
fullscreenCheckBox.selectedProperty().bindBidirectional(fullscreen)
|
||||||
|
// TODO : Some host name validations.
|
||||||
|
hostTextField.textProperty().bindBidirectional(host)
|
||||||
|
|
||||||
|
portTextField.textFormatter = intFormatter()
|
||||||
|
portTextField.textProperty().bindBidirectional(port)
|
||||||
|
|
||||||
|
editCancel.setOnMouseClicked {
|
||||||
|
if (!clientPane.isDisable) {
|
||||||
|
// Cancel changes and reload properties from disk.
|
||||||
|
getModel<SettingsModel>().load()
|
||||||
|
}
|
||||||
|
clientPane.isDisable = !clientPane.isDisable
|
||||||
|
}
|
||||||
|
save.setOnMouseClicked {
|
||||||
|
getModel<SettingsModel>().commit()
|
||||||
|
clientPane.isDisable = true
|
||||||
|
}
|
||||||
|
save.visibleProperty().bind(clientPane.disableProperty().map { !it })
|
||||||
|
editCancel.textProperty().bind(clientPane.disableProperty().map { if (!it) "Cancel" else "Edit" })
|
||||||
|
editCancel.graphicProperty().bind(clientPane.disableProperty()
|
||||||
|
.map { if (!it) FontAwesomeIconView(FontAwesomeIcon.TIMES) else FontAwesomeIconView(FontAwesomeIcon.EDIT) })
|
||||||
|
}
|
||||||
}
|
}
|
@ -3,7 +3,8 @@ package net.corda.explorer.views
|
|||||||
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
|
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
|
||||||
import javafx.beans.binding.Bindings
|
import javafx.beans.binding.Bindings
|
||||||
import javafx.beans.value.ObservableValue
|
import javafx.beans.value.ObservableValue
|
||||||
import javafx.collections.FXCollections
|
import javafx.collections.ObservableList
|
||||||
|
import javafx.geometry.HPos
|
||||||
import javafx.geometry.Insets
|
import javafx.geometry.Insets
|
||||||
import javafx.geometry.Pos
|
import javafx.geometry.Pos
|
||||||
import javafx.scene.Node
|
import javafx.scene.Node
|
||||||
@ -12,24 +13,24 @@ import javafx.scene.control.Label
|
|||||||
import javafx.scene.control.ListView
|
import javafx.scene.control.ListView
|
||||||
import javafx.scene.control.TableView
|
import javafx.scene.control.TableView
|
||||||
import javafx.scene.control.TitledPane
|
import javafx.scene.control.TitledPane
|
||||||
import javafx.scene.layout.Background
|
|
||||||
import javafx.scene.layout.BackgroundFill
|
|
||||||
import javafx.scene.layout.BorderPane
|
import javafx.scene.layout.BorderPane
|
||||||
import javafx.scene.layout.CornerRadii
|
import javafx.scene.layout.VBox
|
||||||
import javafx.scene.paint.Color
|
import net.corda.client.fxutils.filterNotNull
|
||||||
import net.corda.client.fxutils.*
|
import net.corda.client.fxutils.lift
|
||||||
|
import net.corda.client.fxutils.map
|
||||||
|
import net.corda.client.fxutils.sequence
|
||||||
import net.corda.client.model.*
|
import net.corda.client.model.*
|
||||||
import net.corda.contracts.asset.Cash
|
import net.corda.contracts.asset.Cash
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.CompositeKey
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.composite
|
import net.corda.core.crypto.toStringShort
|
||||||
import net.corda.core.flows.StateMachineRunId
|
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.explorer.AmountDiff
|
import net.corda.explorer.AmountDiff
|
||||||
import net.corda.explorer.formatters.AmountFormatter
|
import net.corda.explorer.formatters.AmountFormatter
|
||||||
import net.corda.explorer.identicon.identicon
|
import net.corda.explorer.identicon.identicon
|
||||||
import net.corda.explorer.identicon.identiconToolTip
|
import net.corda.explorer.identicon.identiconToolTip
|
||||||
import net.corda.explorer.model.CordaView
|
import net.corda.explorer.model.CordaView
|
||||||
|
import net.corda.explorer.model.CordaWidget
|
||||||
import net.corda.explorer.model.ReportingCurrencyModel
|
import net.corda.explorer.model.ReportingCurrencyModel
|
||||||
import net.corda.explorer.sign
|
import net.corda.explorer.sign
|
||||||
import net.corda.explorer.ui.setCustomCellFactory
|
import net.corda.explorer.ui.setCustomCellFactory
|
||||||
@ -40,158 +41,194 @@ class TransactionViewer : CordaView("Transactions") {
|
|||||||
override val root by fxml<BorderPane>()
|
override val root by fxml<BorderPane>()
|
||||||
override val icon = FontAwesomeIcon.EXCHANGE
|
override val icon = FontAwesomeIcon.EXCHANGE
|
||||||
|
|
||||||
private val transactionViewTable by fxid<TableView<ViewerNode>>()
|
private val transactionViewTable by fxid<TableView<Transaction>>()
|
||||||
private val matchingTransactionsLabel by fxid<Label>()
|
private val matchingTransactionsLabel by fxid<Label>()
|
||||||
// Inject data
|
// Inject data
|
||||||
private val gatheredTransactionDataList by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
|
private val transactions by observableListReadOnly(GatheredTransactionDataModel::partiallyResolvedTransactions)
|
||||||
private val reportingExchange by observableValue(ReportingCurrencyModel::reportingExchange)
|
private val reportingExchange by observableValue(ReportingCurrencyModel::reportingExchange)
|
||||||
|
private val reportingCurrency by observableValue(ReportingCurrencyModel::reportingCurrency)
|
||||||
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
|
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
|
||||||
|
|
||||||
override val widget: Node = TransactionWidget()
|
override val widgets = listOf(CordaWidget(title, TransactionWidget())).observable()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is what holds data for a single transaction node. Note how a lot of these are nullable as we often simply don't
|
* This is what holds data for a single transaction node. Note how a lot of these are nullable as we often simply don't
|
||||||
* have the data.
|
* have the data.
|
||||||
*/
|
*/
|
||||||
data class ViewerNode(
|
data class Transaction(
|
||||||
val transaction: PartiallyResolvedTransaction,
|
val tx: PartiallyResolvedTransaction,
|
||||||
val inputContracts: List<Contract>,
|
val id: SecureHash,
|
||||||
val outputContracts: List<Contract>,
|
val inputs: Inputs,
|
||||||
val stateMachineRunId: ObservableValue<StateMachineRunId?>,
|
val outputs: ObservableList<StateAndRef<ContractState>>,
|
||||||
val stateMachineStatus: ObservableValue<out StateMachineStatus?>,
|
val inputParties: ObservableList<List<ObservableValue<NodeInfo?>>>,
|
||||||
val flowStatus: ObservableValue<out FlowStatus?>,
|
val outputParties: ObservableList<List<ObservableValue<NodeInfo?>>>,
|
||||||
val commandTypes: Collection<Class<CommandData>>,
|
val commandTypes: List<Class<CommandData>>,
|
||||||
val totalValueEquiv: ObservableValue<AmountDiff<Currency>>
|
val totalValueEquiv: ObservableValue<AmountDiff<Currency>>
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
data class Inputs(val resolved: ObservableList<StateAndRef<ContractState>>, val unresolved: ObservableList<StateRef>)
|
||||||
* Holds information about a single input/output state, to be displayed in the [contractStatesTitledPane]
|
|
||||||
*/
|
|
||||||
data class StateNode(
|
|
||||||
val state: ObservableValue<PartiallyResolvedTransaction.InputResolution>,
|
|
||||||
val stateRef: StateRef
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We map the gathered data about transactions almost one-to-one to the nodes.
|
* We map the gathered data about transactions almost one-to-one to the nodes.
|
||||||
*/
|
*/
|
||||||
private val viewerNodes = gatheredTransactionDataList.map {
|
|
||||||
// TODO in theory there may be several associated state machines, we should at least give a warning if there are
|
|
||||||
// several, currently we just throw others away
|
|
||||||
val stateMachine = it.stateMachines.first()
|
|
||||||
fun <A> stateMachineProperty(property: (StateMachineData) -> ObservableValue<out A?>): ObservableValue<out A?> {
|
|
||||||
return stateMachine.map { it?.let(property) }.bindOut { it ?: null.lift() }
|
|
||||||
}
|
|
||||||
ViewerNode(
|
|
||||||
transaction = it.transaction,
|
|
||||||
inputContracts = it.transaction.inputs.map { it.value as? PartiallyResolvedTransaction.InputResolution.Resolved }.filterNotNull().map { it.stateAndRef.state.data.contract },
|
|
||||||
outputContracts = it.transaction.transaction.tx.outputs.map { it.data.contract },
|
|
||||||
stateMachineRunId = stateMachine.map { it?.id },
|
|
||||||
flowStatus = stateMachineProperty { it.flowStatus },
|
|
||||||
stateMachineStatus = stateMachineProperty { it.stateMachineStatus },
|
|
||||||
commandTypes = it.transaction.transaction.tx.commands.map { it.value.javaClass },
|
|
||||||
totalValueEquiv = {
|
|
||||||
val resolvedInputs = it.transaction.inputs.sequence()
|
|
||||||
.map { (it as? PartiallyResolvedTransaction.InputResolution.Resolved)?.stateAndRef?.state }
|
|
||||||
.filterNotNull().toList().lift()
|
|
||||||
|
|
||||||
::calculateTotalEquiv.lift(
|
|
||||||
myIdentity,
|
|
||||||
reportingExchange,
|
|
||||||
resolvedInputs,
|
|
||||||
it.transaction.transaction.tx.outputs.lift()
|
|
||||||
)
|
|
||||||
}()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val searchField = SearchField(viewerNodes, { viewerNode, s -> viewerNode.commandTypes.any { it.simpleName.contains(s, true) } })
|
val transactions = transactions.map {
|
||||||
|
val resolved = it.inputs.sequence()
|
||||||
|
.map { it as? PartiallyResolvedTransaction.InputResolution.Resolved }
|
||||||
|
.filterNotNull()
|
||||||
|
.map { it.stateAndRef }
|
||||||
|
val unresolved = it.inputs.sequence()
|
||||||
|
.map { it as? PartiallyResolvedTransaction.InputResolution.Unresolved }
|
||||||
|
.filterNotNull()
|
||||||
|
.map { it.stateRef }
|
||||||
|
val outputs = it.transaction.tx.outputs
|
||||||
|
.mapIndexed { index, transactionState ->
|
||||||
|
val stateRef = StateRef(it.id, index)
|
||||||
|
StateAndRef(transactionState, stateRef)
|
||||||
|
}.observable()
|
||||||
|
Transaction(
|
||||||
|
tx = it,
|
||||||
|
id = it.id,
|
||||||
|
inputs = Inputs(resolved, unresolved),
|
||||||
|
outputs = outputs,
|
||||||
|
inputParties = resolved.getParties(),
|
||||||
|
outputParties = outputs.getParties(),
|
||||||
|
commandTypes = it.transaction.tx.commands.map { it.value.javaClass },
|
||||||
|
totalValueEquiv = ::calculateTotalEquiv.lift(myIdentity,
|
||||||
|
reportingExchange,
|
||||||
|
resolved.map { it.state.data }.lift(),
|
||||||
|
it.transaction.tx.outputs.map { it.data }.lift())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val searchField = SearchField(transactions,
|
||||||
|
"Transaction ID" to { tx, s -> "${tx.id}".contains(s, true) },
|
||||||
|
"Input" to { tx, s -> tx.inputs.resolved.any { it.state.data.contract.javaClass.simpleName.contains(s, true) } },
|
||||||
|
"Output" to { tx, s -> tx.outputs.any { it.state.data.contract.javaClass.simpleName.contains(s, true) } },
|
||||||
|
"Input Party" to { tx, s -> tx.inputParties.any { it.any { it.value?.legalIdentity?.name?.contains(s, true) ?: false } } },
|
||||||
|
"Output Party" to { tx, s -> tx.outputParties.any { it.any { it.value?.legalIdentity?.name?.contains(s, true) ?: false } } },
|
||||||
|
"Command Type" to { tx, s -> tx.commandTypes.any { it.simpleName.contains(s, true) } }
|
||||||
|
)
|
||||||
root.top = searchField.root
|
root.top = searchField.root
|
||||||
// Transaction table
|
// Transaction table
|
||||||
transactionViewTable.apply {
|
transactionViewTable.apply {
|
||||||
items = searchField.filteredData
|
items = searchField.filteredData
|
||||||
column("Transaction ID", ViewerNode::transaction).setCustomCellFactory {
|
column("Transaction ID", Transaction::id) { maxWidth = 200.0 }.setCustomCellFactory {
|
||||||
label("${it.id}") {
|
label("$it") {
|
||||||
graphic = imageview {
|
graphic = identicon(it, 15.0)
|
||||||
image = identicon(it.id, 5.0)
|
tooltip = identiconToolTip(it)
|
||||||
}
|
|
||||||
tooltip = identiconToolTip(it.id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
column("Input Contract Type(s)", ViewerNode::inputContracts).cellFormat { text = (it.map { it.javaClass.simpleName }.toSet().joinToString(", ")) }
|
column("Input", Transaction::inputs).cellFormat {
|
||||||
column("Output Contract Type(s)", ViewerNode::outputContracts).cellFormat { text = it.map { it.javaClass.simpleName }.toSet().joinToString(", ") }
|
text = it.resolved.toText()
|
||||||
column("State Machine ID", ViewerNode::stateMachineRunId).cellFormat { text = "${it?.uuid ?: ""}" }
|
if (!it.unresolved.isEmpty()) {
|
||||||
column("Flow status", ViewerNode::flowStatus).cellFormat { text = "${it.value ?: ""}" }
|
if (!text.isBlank()) {
|
||||||
column("SM Status", ViewerNode::stateMachineStatus).cellFormat { text = "${it.value ?: ""}" }
|
text += ", "
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
text += "Unresolved(${it.unresolved.size})"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
column("Output", Transaction::outputs).cellFormat { text = it.toText() }
|
||||||
|
column("Input Party", Transaction::inputParties).cellFormat { text = it.flatten().map { it.value?.legalIdentity?.name }.filterNotNull().toSet().joinToString() }
|
||||||
|
column("Output Party", Transaction::outputParties).cellFormat { text = it.flatten().map { it.value?.legalIdentity?.name }.filterNotNull().toSet().joinToString() }
|
||||||
|
column("Command type", Transaction::commandTypes).cellFormat { text = it.map { it.simpleName }.joinToString() }
|
||||||
|
column("Total value", Transaction::totalValueEquiv).cellFormat {
|
||||||
|
text = "${it.positivity.sign}${AmountFormatter.boring.format(it.amount)}"
|
||||||
|
titleProperty.bind(reportingCurrency.map { "Total value ($it equiv)" })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rowExpander {
|
||||||
|
add(ContractStatesView(it).root)
|
||||||
|
prefHeight = 400.0
|
||||||
|
}.apply {
|
||||||
|
prefWidth = 26.0
|
||||||
|
isResizable = false
|
||||||
|
}
|
||||||
|
setColumnResizePolicy { true }
|
||||||
|
}
|
||||||
matchingTransactionsLabel.textProperty().bind(Bindings.size(transactionViewTable.items).map {
|
matchingTransactionsLabel.textProperty().bind(Bindings.size(transactionViewTable.items).map {
|
||||||
"$it matching transaction${if (it == 1) "" else "s"}"
|
"$it matching transaction${if (it == 1) "" else "s"}"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ContractStatesView(val transaction: PartiallyResolvedTransaction) : View() {
|
private fun ObservableList<StateAndRef<ContractState>>.getParties() = map { it.state.data.participants.map { getModel<NetworkIdentityModel>().lookup(it) } }
|
||||||
override val root: Parent by fxml()
|
private fun ObservableList<StateAndRef<ContractState>>.toText() = map { it.contract().javaClass.simpleName }.groupBy { it }.map { "${it.key} (${it.value.size})" }.joinToString()
|
||||||
private val inputs: ListView<StateNode> by fxid()
|
|
||||||
private val outputs: ListView<StateNode> by fxid()
|
private class TransactionWidget() : BorderPane() {
|
||||||
private val signatures: ListView<CompositeKey> by fxid()
|
private val partiallyResolvedTransactions by observableListReadOnly(GatheredTransactionDataModel::partiallyResolvedTransactions)
|
||||||
private val inputPane: TitledPane by fxid()
|
|
||||||
private val outputPane: TitledPane by fxid()
|
// TODO : Add a scrolling table to show latest transaction.
|
||||||
private val signaturesPane: TitledPane by fxid()
|
// TODO : Add a chart to show types of transactions.
|
||||||
|
init {
|
||||||
|
right {
|
||||||
|
label {
|
||||||
|
textProperty().bind(Bindings.size(partiallyResolvedTransactions).map(Number::toString))
|
||||||
|
BorderPane.setAlignment(this, Pos.BOTTOM_RIGHT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ContractStatesView(transaction: Transaction) : Fragment() {
|
||||||
|
override val root by fxml<Parent>()
|
||||||
|
private val inputs by fxid<ListView<StateAndRef<ContractState>>>()
|
||||||
|
private val outputs by fxid<ListView<StateAndRef<ContractState>>>()
|
||||||
|
private val signatures by fxid<VBox>()
|
||||||
|
private val inputPane by fxid<TitledPane>()
|
||||||
|
private val outputPane by fxid<TitledPane>()
|
||||||
|
private val signaturesPane by fxid<TitledPane>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val inputStates = transaction.inputs.map { StateNode(it, it.value.stateRef) }
|
val signatureData = transaction.tx.transaction.sigs.map { it.by }
|
||||||
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.composite }
|
|
||||||
// Bind count to TitlePane
|
// Bind count to TitlePane
|
||||||
inputPane.textProperty().bind(inputStates.lift().map { "Input (${it.count()})" })
|
inputPane.text = "Input (${transaction.inputs.resolved.count()})"
|
||||||
outputPane.textProperty().bind(outputStates.lift().map { "Output (${it.count()})" })
|
outputPane.text = "Output (${transaction.outputs.count()})"
|
||||||
signaturesPane.textProperty().bind(signatureData.lift().map { "Signatures (${it.count()})" })
|
signaturesPane.text = "Signatures (${signatureData.count()})"
|
||||||
|
|
||||||
val cellFactory = { node: StateNode ->
|
inputs.cellCache { getCell(it) }
|
||||||
(node.state.value as? PartiallyResolvedTransaction.InputResolution.Resolved)?.run {
|
outputs.cellCache { getCell(it) }
|
||||||
val data = stateAndRef.state.data
|
|
||||||
form {
|
inputs.items = transaction.inputs.resolved
|
||||||
label("${data.contract.javaClass.simpleName} (${stateAndRef.ref.toString().substring(0, 16)}...)[${stateAndRef.ref.index}]") {
|
outputs.items = transaction.outputs.observable()
|
||||||
graphic = imageview {
|
|
||||||
image = identicon(stateAndRef.ref.txhash, 10.0)
|
signatures.children.addAll(signatureData.map { signature ->
|
||||||
|
val nodeInfo = getModel<NetworkIdentityModel>().lookup(signature)
|
||||||
|
copyableLabel(nodeInfo.map { "${signature.toStringShort()} (${it?.legalIdentity?.name ?: "???"})" })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
tooltip = identiconToolTip(stateAndRef.ref.txhash)
|
|
||||||
|
private fun getCell(contractState: StateAndRef<ContractState>): Node {
|
||||||
|
return {
|
||||||
|
gridpane {
|
||||||
|
padding = Insets(0.0, 5.0, 10.0, 10.0)
|
||||||
|
vgap = 10.0
|
||||||
|
hgap = 10.0
|
||||||
|
row {
|
||||||
|
label("${contractState.contract().javaClass.simpleName} (${contractState.ref.toString().substring(0, 16)}...)[${contractState.ref.index}]") {
|
||||||
|
graphic = identicon(contractState.ref.txhash, 30.0)
|
||||||
|
tooltip = identiconToolTip(contractState.ref.txhash)
|
||||||
|
gridpaneConstraints { columnSpan = 2 }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
val data = contractState.state.data
|
||||||
when (data) {
|
when (data) {
|
||||||
is Cash.State -> form {
|
is Cash.State -> {
|
||||||
fieldset {
|
row {
|
||||||
field("Amount :") {
|
label("Amount :") { gridpaneConstraints { hAlignment = HPos.RIGHT } }
|
||||||
label(AmountFormatter.boring.format(data.amount.withoutIssuer()))
|
label(AmountFormatter.boring.format(data.amount.withoutIssuer()))
|
||||||
}
|
}
|
||||||
field("Issuer :") {
|
row {
|
||||||
|
label("Issuer :") { gridpaneConstraints { hAlignment = HPos.RIGHT } }
|
||||||
label("${data.amount.token.issuer}") {
|
label("${data.amount.token.issuer}") {
|
||||||
tooltip(data.amount.token.issuer.party.owningKey.toString())
|
tooltip(data.amount.token.issuer.party.owningKey.toBase58String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
field("Owner :") {
|
row {
|
||||||
|
label("Owner :") { gridpaneConstraints { hAlignment = HPos.RIGHT } }
|
||||||
val owner = data.owner
|
val owner = data.owner
|
||||||
val nodeInfo = Models.get<NetworkIdentityModel>(TransactionViewer::class).lookup(owner)
|
val nodeInfo = getModel<NetworkIdentityModel>().lookup(owner)
|
||||||
label(nodeInfo?.legalIdentity?.name ?: "???") {
|
label(nodeInfo.map { it?.legalIdentity?.name ?: "???" }) {
|
||||||
tooltip(data.owner.toString())
|
tooltip(data.owner.toBase58String())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -199,40 +236,11 @@ class TransactionViewer : CordaView("Transactions") {
|
|||||||
else -> label {}
|
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 (${nodeInfo?.legalIdentity?.name ?: "???"})"
|
|
||||||
}
|
|
||||||
prefHeight = 185.0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TransactionWidget() : BorderPane() {
|
private fun StateAndRef<ContractState>.contract() = this.state.data.contract
|
||||||
private val gatheredTransactionDataList by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
|
|
||||||
|
|
||||||
// TODO : Add a scrolling table to show latest transaction.
|
|
||||||
// TODO : Add a chart to show types of transactions.
|
|
||||||
init {
|
|
||||||
right {
|
|
||||||
label {
|
|
||||||
textProperty().bind(Bindings.size(gatheredTransactionDataList).map { it.toString() })
|
|
||||||
BorderPane.setAlignment(this, Pos.BOTTOM_RIGHT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -240,14 +248,22 @@ class TransactionViewer : CordaView("Transactions") {
|
|||||||
*/
|
*/
|
||||||
private fun calculateTotalEquiv(identity: NodeInfo?,
|
private fun calculateTotalEquiv(identity: NodeInfo?,
|
||||||
reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>,
|
reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>,
|
||||||
inputs: List<TransactionState<ContractState>>,
|
inputs: List<ContractState>,
|
||||||
outputs: List<TransactionState<ContractState>>): AmountDiff<Currency> {
|
outputs: List<ContractState>): AmountDiff<Currency> {
|
||||||
val (reportingCurrency, exchange) = reportingCurrencyExchange
|
val (reportingCurrency, exchange) = reportingCurrencyExchange
|
||||||
val publicKey = identity?.legalIdentity?.owningKey
|
val publicKey = identity?.legalIdentity?.owningKey
|
||||||
fun List<TransactionState<ContractState>>.sum() = this.map { it.data as? Cash.State }
|
fun List<ContractState>.sum() = this.map { it as? Cash.State }
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
.filter { publicKey == it.owner }
|
.filter { publicKey == it.owner }
|
||||||
.map { exchange(it.amount.withoutIssuer()).quantity }
|
.map { exchange(it.amount.withoutIssuer()).quantity }
|
||||||
.sum()
|
.sum()
|
||||||
return AmountDiff.fromLong(outputs.sum() - inputs.sum(), reportingCurrency)
|
|
||||||
|
// For issuing cash, if I am the issuer and not the owner (e.g. issuing cash to other party), count it as negative.
|
||||||
|
val issuedAmount = if (inputs.isEmpty()) outputs.map { it as? Cash.State }
|
||||||
|
.filterNotNull()
|
||||||
|
.filter { publicKey == it.amount.token.issuer.party.owningKey && publicKey != it.owner }
|
||||||
|
.map { exchange(it.amount.withoutIssuer()).quantity }
|
||||||
|
.sum() else 0
|
||||||
|
|
||||||
|
return AmountDiff.fromLong(outputs.sum() - inputs.sum() - issuedAmount, reportingCurrency)
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package net.corda.explorer.views.cordapps
|
package net.corda.explorer.views.cordapps.cash
|
||||||
|
|
||||||
import com.sun.javafx.collections.ObservableListWrapper
|
import com.sun.javafx.collections.ObservableListWrapper
|
||||||
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
|
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
|
||||||
@ -8,11 +8,9 @@ import javafx.beans.value.ObservableValue
|
|||||||
import javafx.collections.FXCollections
|
import javafx.collections.FXCollections
|
||||||
import javafx.collections.ObservableList
|
import javafx.collections.ObservableList
|
||||||
import javafx.geometry.Insets
|
import javafx.geometry.Insets
|
||||||
import javafx.scene.Node
|
|
||||||
import javafx.scene.Parent
|
import javafx.scene.Parent
|
||||||
import javafx.scene.chart.NumberAxis
|
import javafx.scene.chart.NumberAxis
|
||||||
import javafx.scene.control.*
|
import javafx.scene.control.*
|
||||||
import javafx.scene.image.ImageView
|
|
||||||
import javafx.scene.input.MouseButton
|
import javafx.scene.input.MouseButton
|
||||||
import javafx.scene.layout.BorderPane
|
import javafx.scene.layout.BorderPane
|
||||||
import javafx.scene.layout.HBox
|
import javafx.scene.layout.HBox
|
||||||
@ -29,10 +27,14 @@ import net.corda.explorer.formatters.AmountFormatter
|
|||||||
import net.corda.explorer.identicon.identicon
|
import net.corda.explorer.identicon.identicon
|
||||||
import net.corda.explorer.identicon.identiconToolTip
|
import net.corda.explorer.identicon.identiconToolTip
|
||||||
import net.corda.explorer.model.CordaView
|
import net.corda.explorer.model.CordaView
|
||||||
|
import net.corda.explorer.model.CordaWidget
|
||||||
import net.corda.explorer.model.ReportingCurrencyModel
|
import net.corda.explorer.model.ReportingCurrencyModel
|
||||||
import net.corda.explorer.model.SettingsModel
|
import net.corda.explorer.model.SettingsModel
|
||||||
import net.corda.explorer.ui.*
|
import net.corda.explorer.ui.*
|
||||||
import net.corda.explorer.views.*
|
import net.corda.explorer.views.SearchField
|
||||||
|
import net.corda.explorer.views.runInFxApplicationThread
|
||||||
|
import net.corda.explorer.views.stringConverter
|
||||||
|
import net.corda.explorer.views.toStringWithSuffix
|
||||||
import org.fxmisc.easybind.EasyBind
|
import org.fxmisc.easybind.EasyBind
|
||||||
import tornadofx.*
|
import tornadofx.*
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@ -44,7 +46,7 @@ class CashViewer : CordaView("Cash") {
|
|||||||
override val root: BorderPane by fxml()
|
override val root: BorderPane by fxml()
|
||||||
override val icon: FontAwesomeIcon = FontAwesomeIcon.MONEY
|
override val icon: FontAwesomeIcon = FontAwesomeIcon.MONEY
|
||||||
// View's widget.
|
// View's widget.
|
||||||
override val widget: Node = CashWidget()
|
override val widgets = listOf(CordaWidget("Treasury", CashWidget())).observable()
|
||||||
// Left pane
|
// Left pane
|
||||||
private val leftPane: VBox by fxid()
|
private val leftPane: VBox by fxid()
|
||||||
private val splitPane: SplitPane by fxid()
|
private val splitPane: SplitPane by fxid()
|
||||||
@ -60,7 +62,7 @@ class CashViewer : CordaView("Cash") {
|
|||||||
private val toggleButton by fxid<Button>()
|
private val toggleButton by fxid<Button>()
|
||||||
// Inject observables
|
// Inject observables
|
||||||
private val cashStates by observableList(ContractStateModel::cashStates)
|
private val cashStates by observableList(ContractStateModel::cashStates)
|
||||||
private val reportingCurrency by observableValue(SettingsModel::reportingCurrency)
|
private val reportingCurrency by observableValue(SettingsModel::reportingCurrencyProperty)
|
||||||
private val reportingExchange by observableValue(ReportingCurrencyModel::reportingExchange)
|
private val reportingExchange by observableValue(ReportingCurrencyModel::reportingExchange)
|
||||||
|
|
||||||
private val selectedNode = cashViewerTable.singleRowSelection().map {
|
private val selectedNode = cashViewerTable.singleRowSelection().map {
|
||||||
@ -120,7 +122,7 @@ class CashViewer : CordaView("Cash") {
|
|||||||
|
|
||||||
stateIdValueLabel.apply {
|
stateIdValueLabel.apply {
|
||||||
text = stateRow.stateAndRef.ref.toString().substring(0, 16) + "...[${stateRow.stateAndRef.ref.index}]"
|
text = stateRow.stateAndRef.ref.toString().substring(0, 16) + "...[${stateRow.stateAndRef.ref.index}]"
|
||||||
graphic = ImageView(identicon(stateRow.stateAndRef.ref.txhash, 10.0))
|
graphic = identicon(stateRow.stateAndRef.ref.txhash, 30.0)
|
||||||
tooltip = identiconToolTip(stateRow.stateAndRef.ref.txhash)
|
tooltip = identiconToolTip(stateRow.stateAndRef.ref.txhash)
|
||||||
}
|
}
|
||||||
equivLabel.textProperty().bind(equivAmount.map { it.token.currencyCode.toString() })
|
equivLabel.textProperty().bind(equivAmount.map { it.token.currencyCode.toString() })
|
||||||
@ -140,14 +142,14 @@ class CashViewer : CordaView("Cash") {
|
|||||||
* issuer strings.
|
* issuer strings.
|
||||||
*/
|
*/
|
||||||
val searchField = SearchField(cashStates,
|
val searchField = SearchField(cashStates,
|
||||||
{ state, text -> state.state.data.amount.token.product.toString().contains(text, true) },
|
"Currency" to { 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) }
|
"Issuer" to { state, text -> state.state.data.amount.token.issuer.party.toString().contains(text, true) }
|
||||||
)
|
)
|
||||||
root.top = hbox(5.0) {
|
root.top = hbox(5.0) {
|
||||||
button("New Transaction", FontAwesomeIconView(FontAwesomeIcon.PLUS)) {
|
button("New Transaction", FontAwesomeIconView(FontAwesomeIcon.PLUS)) {
|
||||||
setOnMouseClicked {
|
setOnMouseClicked {
|
||||||
if (it.button == MouseButton.PRIMARY) {
|
if (it.button == MouseButton.PRIMARY) {
|
||||||
NewTransaction().show(this@CashViewer.root.scene.window)
|
find<NewTransaction>().show(this@CashViewer.root.scene.window)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -279,7 +281,7 @@ class CashViewer : CordaView("Cash") {
|
|||||||
|
|
||||||
private class CashWidget() : VBox() {
|
private class CashWidget() : VBox() {
|
||||||
// Inject data.
|
// Inject data.
|
||||||
private val reportingCurrency by observableValue(SettingsModel::reportingCurrency)
|
private val reportingCurrency by observableValue(SettingsModel::reportingCurrencyProperty)
|
||||||
private val cashStates by observableList(ContractStateModel::cashStates)
|
private val cashStates by observableList(ContractStateModel::cashStates)
|
||||||
private val exchangeRate: ObservableValue<ExchangeRate> by observableValue(ExchangeRateModel::exchangeRate)
|
private val exchangeRate: ObservableValue<ExchangeRate> by observableValue(ExchangeRateModel::exchangeRate)
|
||||||
private val sumAmount = AmountBindings.sumAmountExchange(
|
private val sumAmount = AmountBindings.sumAmountExchange(
|
@ -1,12 +1,12 @@
|
|||||||
package net.corda.explorer.views
|
package net.corda.explorer.views.cordapps.cash
|
||||||
|
|
||||||
import javafx.beans.binding.Bindings
|
import javafx.beans.binding.Bindings
|
||||||
import javafx.beans.binding.BooleanBinding
|
import javafx.beans.binding.BooleanBinding
|
||||||
import javafx.beans.property.SimpleObjectProperty
|
import javafx.beans.property.SimpleObjectProperty
|
||||||
import javafx.beans.value.ObservableValue
|
|
||||||
import javafx.collections.FXCollections
|
import javafx.collections.FXCollections
|
||||||
import javafx.scene.control.*
|
import javafx.scene.control.*
|
||||||
import javafx.stage.Window
|
import javafx.stage.Window
|
||||||
|
import net.corda.client.fxutils.isNotNull
|
||||||
import net.corda.client.fxutils.map
|
import net.corda.client.fxutils.map
|
||||||
import net.corda.client.fxutils.unique
|
import net.corda.client.fxutils.unique
|
||||||
import net.corda.client.model.*
|
import net.corda.client.model.*
|
||||||
@ -15,19 +15,22 @@ import net.corda.core.crypto.Party
|
|||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.serialization.OpaqueBytes
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
import net.corda.explorer.model.CashTransaction
|
import net.corda.explorer.model.CashTransaction
|
||||||
|
import net.corda.explorer.views.bigDecimalFormatter
|
||||||
|
import net.corda.explorer.views.byteFormatter
|
||||||
|
import net.corda.explorer.views.stringConverter
|
||||||
import net.corda.flows.CashCommand
|
import net.corda.flows.CashCommand
|
||||||
import net.corda.flows.CashFlow
|
import net.corda.flows.CashFlow
|
||||||
import net.corda.flows.CashFlowResult
|
import net.corda.flows.CashFlowResult
|
||||||
import net.corda.node.services.messaging.startFlow
|
import net.corda.node.services.messaging.startFlow
|
||||||
import org.controlsfx.dialog.ExceptionDialog
|
import org.controlsfx.dialog.ExceptionDialog
|
||||||
import tornadofx.View
|
import tornadofx.Fragment
|
||||||
|
import tornadofx.booleanBinding
|
||||||
import tornadofx.observable
|
import tornadofx.observable
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class NewTransaction : View() {
|
class NewTransaction : Fragment() {
|
||||||
override val root by fxml<DialogPane>()
|
override val root by fxml<DialogPane>()
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
private val transactionTypeCB by fxid<ChoiceBox<CashTransaction>>()
|
private val transactionTypeCB by fxid<ChoiceBox<CashTransaction>>()
|
||||||
private val partyATextField by fxid<TextField>()
|
private val partyATextField by fxid<TextField>()
|
||||||
@ -44,23 +47,16 @@ class NewTransaction : View() {
|
|||||||
private val availableAmount by fxid<Label>()
|
private val availableAmount by fxid<Label>()
|
||||||
private val amountLabel by fxid<Label>()
|
private val amountLabel by fxid<Label>()
|
||||||
private val amountTextField by fxid<TextField>()
|
private val amountTextField by fxid<TextField>()
|
||||||
|
|
||||||
private val amount = SimpleObjectProperty<BigDecimal>()
|
private val amount = SimpleObjectProperty<BigDecimal>()
|
||||||
private val issueRef = SimpleObjectProperty<Byte>()
|
private val issueRef = SimpleObjectProperty<Byte>()
|
||||||
|
|
||||||
// Inject data
|
// Inject data
|
||||||
private val parties by observableList(NetworkIdentityModel::parties)
|
private val parties by observableList(NetworkIdentityModel::parties)
|
||||||
private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable)
|
private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable)
|
||||||
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
|
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
|
||||||
private val notaries by observableList(NetworkIdentityModel::notaries)
|
private val notaries by observableList(NetworkIdentityModel::notaries)
|
||||||
private val cash by observableList(ContractStateModel::cash)
|
private val cash by observableList(ContractStateModel::cash)
|
||||||
|
|
||||||
private val executeButton = ButtonType("Execute", ButtonBar.ButtonData.APPLY)
|
private val executeButton = ButtonType("Execute", ButtonBar.ButtonData.APPLY)
|
||||||
|
|
||||||
private fun ObservableValue<*>.isNotNull(): BooleanBinding {
|
|
||||||
return Bindings.createBooleanBinding({ this.value != null }, arrayOf(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun show(window: Window): Unit {
|
fun show(window: Window): Unit {
|
||||||
dialog(window).showAndWait().ifPresent {
|
dialog(window).showAndWait().ifPresent {
|
||||||
val dialog = Alert(Alert.AlertType.INFORMATION).apply {
|
val dialog = Alert(Alert.AlertType.INFORMATION).apply {
|
||||||
@ -139,11 +135,11 @@ class NewTransaction : View() {
|
|||||||
issuerChoiceBox.apply {
|
issuerChoiceBox.apply {
|
||||||
items = cash.map { it.token.issuer.party }.unique().sorted()
|
items = cash.map { it.token.issuer.party }.unique().sorted()
|
||||||
converter = stringConverter { it.name }
|
converter = stringConverter { it.name }
|
||||||
visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Pay || it == CashTransaction.Exit })
|
visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Pay })
|
||||||
}
|
}
|
||||||
issuerTextField.apply {
|
issuerTextField.apply {
|
||||||
textProperty().bind(myIdentity.map { it?.legalIdentity?.name })
|
textProperty().bind(myIdentity.map { it?.legalIdentity?.name })
|
||||||
visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Issue })
|
visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Issue || it == CashTransaction.Exit })
|
||||||
isEditable = false
|
isEditable = false
|
||||||
}
|
}
|
||||||
// Issue Reference
|
// Issue Reference
|
||||||
@ -158,21 +154,16 @@ class NewTransaction : View() {
|
|||||||
// TODO : Create a currency model to store these values
|
// TODO : Create a currency model to store these values
|
||||||
currencyChoiceBox.items = FXCollections.observableList(setOf(USD, GBP, CHF).toList())
|
currencyChoiceBox.items = FXCollections.observableList(setOf(USD, GBP, CHF).toList())
|
||||||
currencyChoiceBox.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
currencyChoiceBox.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||||
|
val issuer = Bindings.createObjectBinding({ if (issuerChoiceBox.isVisible) issuerChoiceBox.value else myIdentity.value?.legalIdentity }, arrayOf(myIdentity, issuerChoiceBox.visibleProperty(), issuerChoiceBox.valueProperty()))
|
||||||
availableAmount.visibleProperty().bind(
|
availableAmount.visibleProperty().bind(
|
||||||
arrayListOf(issuerChoiceBox, currencyChoiceBox)
|
issuer.isNotNull.and(currencyChoiceBox.valueProperty().isNotNull).and(transactionTypeCB.valueProperty().booleanBinding(transactionTypeCB.valueProperty()) { it != CashTransaction.Issue })
|
||||||
.map { it.valueProperty().isNotNull.and(it.visibleProperty()) }
|
|
||||||
.reduce(BooleanBinding::and)
|
|
||||||
)
|
)
|
||||||
availableAmount.textProperty()
|
availableAmount.textProperty()
|
||||||
.bind(Bindings.createStringBinding({
|
.bind(Bindings.createStringBinding({
|
||||||
val filteredCash = cash.filtered {
|
val filteredCash = cash.filtered { it.token.issuer.party == issuer.value && it.token.product == currencyChoiceBox.value }
|
||||||
it.token.issuer.party == issuerChoiceBox.value &&
|
.map { it.withoutIssuer().quantity }
|
||||||
it.token.product == currencyChoiceBox.value
|
|
||||||
}.map { it.withoutIssuer().quantity }
|
|
||||||
"${filteredCash.sum()} ${currencyChoiceBox.value?.currencyCode} Available"
|
"${filteredCash.sum()} ${currencyChoiceBox.value?.currencyCode} Available"
|
||||||
}, arrayOf(currencyChoiceBox.valueProperty(), issuerChoiceBox.valueProperty())))
|
}, arrayOf(currencyChoiceBox.valueProperty(), issuerChoiceBox.valueProperty())))
|
||||||
|
|
||||||
// Amount
|
// Amount
|
||||||
amountLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
amountLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||||
amountTextField.textFormatter = bigDecimalFormatter().apply { amount.bind(this.valueProperty()) }
|
amountTextField.textFormatter = bigDecimalFormatter().apply { amount.bind(this.valueProperty()) }
|
||||||
@ -183,7 +174,7 @@ class NewTransaction : View() {
|
|||||||
myIdentity.isNotNull(),
|
myIdentity.isNotNull(),
|
||||||
transactionTypeCB.valueProperty().isNotNull,
|
transactionTypeCB.valueProperty().isNotNull,
|
||||||
partyBChoiceBox.visibleProperty().not().or(partyBChoiceBox.valueProperty().isNotNull),
|
partyBChoiceBox.visibleProperty().not().or(partyBChoiceBox.valueProperty().isNotNull),
|
||||||
issuerChoiceBox.visibleProperty().not().or(partyBChoiceBox.valueProperty().isNotNull),
|
issuerChoiceBox.visibleProperty().not().or(issuerChoiceBox.valueProperty().isNotNull),
|
||||||
amountTextField.textProperty().isNotEmpty,
|
amountTextField.textProperty().isNotEmpty,
|
||||||
currencyChoiceBox.valueProperty().isNotNull
|
currencyChoiceBox.valueProperty().isNotNull
|
||||||
).reduce(BooleanBinding::and)
|
).reduce(BooleanBinding::and)
|
@ -2,10 +2,27 @@
|
|||||||
* {
|
* {
|
||||||
-color-0: rgba(0, 0, 0, 1); /* Background color */
|
-color-0: rgba(0, 0, 0, 1); /* Background color */
|
||||||
-color-1: rgba(0, 0, 0, 0.1); /* Background color layer 1*/
|
-color-1: rgba(0, 0, 0, 0.1); /* Background color layer 1*/
|
||||||
-color-2: rgba(255, 255, 255, 1); /* Background color layer 2*/
|
-color-2: white; /* Background color layer 2*/
|
||||||
-color-3: rgba(219, 0, 23, 1); /* Corda logo color */
|
-color-3: rgba(219, 0, 23, 1); /* Corda logo color */
|
||||||
-color-4: rgba(239, 0, 23, 1); /* Corda logo color light */
|
-color-4: rgba(239, 0, 23, 1); /* Corda logo color light */
|
||||||
-color-5: rgba(219, 0, 23, 0.5); /* Corda logo highlight */
|
-color-5: rgb(255, 191, 198); /* Corda logo highlight */
|
||||||
|
-color-6: rgba(219, 0, 23, 0.2); /* Corda logo highlight */
|
||||||
|
}
|
||||||
|
.root{
|
||||||
|
-fx-background: #f4f4f4;
|
||||||
|
-fx-control-inner-background: white;
|
||||||
|
-fx-dark-text-color: black;
|
||||||
|
-fx-mid-text-color: #292929;
|
||||||
|
-fx-light-text-color: white;
|
||||||
|
-fx-accent: -color-5;
|
||||||
|
-fx-focus-color: -color-3;
|
||||||
|
-fx-color: -fx-base;
|
||||||
|
-fx-disabled-opacity: 0.4;
|
||||||
|
-fx-cell-hover-color: -color-6;
|
||||||
|
-fx-cell-focus-inner-border: -color-5;
|
||||||
|
-fx-page-bullet-border: #acacac;
|
||||||
|
-fx-page-indicator-hover-border: -color-6;
|
||||||
|
-fx-faint-focus-color: #d3524422;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Global Style*/
|
/* Global Style*/
|
||||||
|
@ -1,32 +1,15 @@
|
|||||||
@import "corda-dark-color-scheme.css";
|
@import "corda-dark-color-scheme.css";
|
||||||
|
|
||||||
/* Global CSS */
|
/* Global CSS */
|
||||||
.button:selected, .button:focused,
|
|
||||||
.list-view:selected, .list-view:focused,
|
|
||||||
.tree-view:selected, .tree-view:focused,
|
|
||||||
.text-field:selected, .text-field:focused,
|
|
||||||
.table-view:selected, .table-view:focused,
|
|
||||||
.choice-box:selected, .choice-box:focused,
|
|
||||||
.scroll-pane:selected, .scroll-pane:focused,
|
|
||||||
.menu-button:selected, .menu-button:focused,
|
|
||||||
.tree-table-view:selected, .tree-table-view:focused {
|
|
||||||
-fx-focus-color: -color-4;
|
|
||||||
-fx-faint-focus-color: #d3524422;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-cell:focused, .list-cell:selected,
|
|
||||||
.table-row-cell:focused, .table-row-cell:selected,
|
|
||||||
.tree-table-row-cell:focused, .tree-table-row-cell:selected,
|
|
||||||
.choice-box .menu-item:focused, .choice-box .menu-item:selected,
|
|
||||||
.menu-button .menu-item:focused, .menu-button .menu-item:selected,
|
|
||||||
.context-menu .menu-item:focused, .context-menu .menu-item:selected {
|
|
||||||
-fx-background-color: -color-5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-menu .menu-item .label {
|
.context-menu .menu-item .label {
|
||||||
-fx-text-fill: -color-0;
|
-fx-text-fill: -color-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.titled-pane .content, .titled-pane .text {
|
||||||
|
-fx-background-color: -color-2;
|
||||||
|
}
|
||||||
|
|
||||||
.split-pane-divider {
|
.split-pane-divider {
|
||||||
-fx-background-color: transparent;
|
-fx-background-color: transparent;
|
||||||
-fx-border-color: transparent;
|
-fx-border-color: transparent;
|
||||||
@ -55,6 +38,7 @@
|
|||||||
.sidebar-menu-item:hover, .sidebar-menu-item:selected {
|
.sidebar-menu-item:hover, .sidebar-menu-item:selected {
|
||||||
-fx-background-color: -color-3;
|
-fx-background-color: -color-3;
|
||||||
-fx-border-color: -color-3;
|
-fx-border-color: -color-3;
|
||||||
|
-fx-cursor: hand;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-menu-item-arrow {
|
.sidebar-menu-item-arrow {
|
||||||
@ -178,12 +162,9 @@
|
|||||||
-fx-background-color: -color-0;
|
-fx-background-color: -color-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login .text-field {
|
.login .label, .login .check-box .text {
|
||||||
-fx-border-color: -color-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login .label {
|
|
||||||
-fx-text-fill: -color-2;
|
-fx-text-fill: -color-2;
|
||||||
|
-fx-fill: -color-2;
|
||||||
-fx-font-weight: bold;
|
-fx-font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,6 +174,12 @@
|
|||||||
-fx-border-radius: 2px;
|
-fx-border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.searchField .combo-box {
|
||||||
|
-fx-padding: -1px;
|
||||||
|
-fx-border-width: 0;
|
||||||
|
-fx-background-insets: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.searchField .glyph-icon {
|
.searchField .glyph-icon {
|
||||||
-fx-fill: -color-1;
|
-fx-fill: -color-1;
|
||||||
-fx-padding: 0;
|
-fx-padding: 0;
|
||||||
@ -201,3 +188,55 @@
|
|||||||
.searchField .search-clear:hover {
|
.searchField .search-clear:hover {
|
||||||
-fx-fill: -color-4;
|
-fx-fill: -color-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contractStateView {
|
||||||
|
-fx-padding: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyable-label, .copyable-label:focused {
|
||||||
|
-fx-background-color: transparent;
|
||||||
|
-fx-background-insets: 0px;
|
||||||
|
-fx-padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Network View */
|
||||||
|
.networkView .worldMap {
|
||||||
|
-fx-image: url("../images/WorldMapSquare.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
.networkView .map .label:hover .glyph-icon,
|
||||||
|
.networkView .map .label:hover .text {
|
||||||
|
-fx-fill: -color-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.networkView .map .glyph-icon,
|
||||||
|
.networkView .map .text {
|
||||||
|
-fx-fill: -color-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.networkTile .title,
|
||||||
|
.networkTile .content,
|
||||||
|
.networkTile .content .button,
|
||||||
|
.networkTile .content .scroll-pane,
|
||||||
|
.networkTile .content .scroll-pane > .viewport {
|
||||||
|
-fx-background-color: rgba(28, 28, 28, 0.5);
|
||||||
|
-fx-background: rgba(28, 28, 28, 0.5);
|
||||||
|
-fx-border-width: 0;
|
||||||
|
-fx-background-insets: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.networkTile .content .button:hover {
|
||||||
|
-fx-background-color: -color-4;
|
||||||
|
-fx-cursor: hand;
|
||||||
|
}
|
||||||
|
|
||||||
|
.networkTile .title > .text,
|
||||||
|
.networkTile .copyable-label > .text,
|
||||||
|
.networkTile .text-field {
|
||||||
|
-fx-fill: white;
|
||||||
|
-fx-text-fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setting-edit-label:hover .text, #setting-edit-label:hover .glyph-icon {
|
||||||
|
-fx-fill: -color-4;
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 342 KiB |
Binary file not shown.
After Width: | Height: | Size: 469 KiB |
@ -3,7 +3,7 @@
|
|||||||
<?import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView?>
|
<?import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView?>
|
||||||
<?import javafx.scene.control.*?>
|
<?import javafx.scene.control.*?>
|
||||||
<?import javafx.scene.layout.*?>
|
<?import javafx.scene.layout.*?>
|
||||||
<GridPane hgap="10" stylesheets="@../css/corda.css" vgap="10" xmlns="http://javafx.com/javafx/8.0.112-ea" xmlns:fx="http://javafx.com/fxml/1">
|
<GridPane styleClass="contractStateView" hgap="10" stylesheets="@../css/corda.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">
|
<TitledPane fx:id="inputPane" collapsible="false" text="Input" GridPane.fillWidth="true">
|
||||||
<ListView fx:id="inputs"/>
|
<ListView fx:id="inputs"/>
|
||||||
</TitledPane>
|
</TitledPane>
|
||||||
@ -15,7 +15,7 @@
|
|||||||
</TitledPane>
|
</TitledPane>
|
||||||
|
|
||||||
<TitledPane fx:id="signaturesPane" collapsible="false" text="Signatures" GridPane.columnSpan="3" GridPane.rowIndex="1">
|
<TitledPane fx:id="signaturesPane" collapsible="false" text="Signatures" GridPane.columnSpan="3" GridPane.rowIndex="1">
|
||||||
<ListView fx:id="signatures"/>
|
<VBox fx:id="signatures" spacing="5"/>
|
||||||
</TitledPane>
|
</TitledPane>
|
||||||
|
|
||||||
<columnConstraints>
|
<columnConstraints>
|
||||||
@ -25,6 +25,6 @@
|
|||||||
</columnConstraints>
|
</columnConstraints>
|
||||||
<rowConstraints>
|
<rowConstraints>
|
||||||
<RowConstraints vgrow="ALWAYS"/>
|
<RowConstraints vgrow="ALWAYS"/>
|
||||||
<RowConstraints/>
|
<RowConstraints vgrow="NEVER"/>
|
||||||
</rowConstraints>
|
</rowConstraints>
|
||||||
</GridPane>
|
</GridPane>
|
||||||
|
@ -22,14 +22,19 @@
|
|||||||
<center>
|
<center>
|
||||||
<GridPane hgap="10" prefWidth="400" vgap="10">
|
<GridPane hgap="10" prefWidth="400" vgap="10">
|
||||||
<Label text="Corda Node :" GridPane.halignment="RIGHT"/>
|
<Label text="Corda Node :" GridPane.halignment="RIGHT"/>
|
||||||
<TextField fx:id="host" promptText="Host" GridPane.columnIndex="1"/>
|
<TextField fx:id="hostTextField" promptText="Host" GridPane.columnIndex="1"/>
|
||||||
<TextField fx:id="port" prefWidth="100" promptText="Port" GridPane.columnIndex="2"/>
|
<TextField fx:id="portTextField" prefWidth="100" promptText="Port" GridPane.columnIndex="2"/>
|
||||||
|
|
||||||
<Label text="Username :" GridPane.rowIndex="1" GridPane.halignment="RIGHT"/>
|
<Label text="Username :" GridPane.rowIndex="1" GridPane.halignment="RIGHT"/>
|
||||||
<TextField fx:id="username" promptText="Username" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="1"/>
|
<TextField fx:id="usernameTextField" promptText="Username" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="1"/>
|
||||||
|
|
||||||
<Label text="Password :" GridPane.rowIndex="2" GridPane.halignment="RIGHT"/>
|
<Label text="Password :" GridPane.rowIndex="2" GridPane.halignment="RIGHT"/>
|
||||||
<PasswordField fx:id="password" promptText="Password" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="2"/>
|
<PasswordField fx:id="passwordTextField" promptText="Password" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="2"/>
|
||||||
|
|
||||||
|
<HBox spacing="20" GridPane.columnIndex="1" GridPane.rowIndex="3" GridPane.columnSpan="2">
|
||||||
|
<CheckBox fx:id="rememberMeCheckBox" text="Remember me" />
|
||||||
|
<CheckBox fx:id="fullscreenCheckBox" text="Fullscreen mode"/>
|
||||||
|
</HBox>
|
||||||
</GridPane>
|
</GridPane>
|
||||||
</center>
|
</center>
|
||||||
</BorderPane>
|
</BorderPane>
|
||||||
|
@ -14,8 +14,7 @@
|
|||||||
<!-- User account menu -->
|
<!-- User account menu -->
|
||||||
<MenuButton fx:id="userButton" mnemonicParsing="false" GridPane.columnIndex="3">
|
<MenuButton fx:id="userButton" mnemonicParsing="false" GridPane.columnIndex="3">
|
||||||
<items>
|
<items>
|
||||||
<MenuItem mnemonicParsing="false" text="Sign out"/>
|
<MenuItem fx:id="exit" mnemonicParsing="false" text="Exit Corda Explorer"/>
|
||||||
<MenuItem mnemonicParsing="false" text="Account settings..."/>
|
|
||||||
</items>
|
</items>
|
||||||
<graphic>
|
<graphic>
|
||||||
<FontAwesomeIconView glyphName="USER" glyphSize="20"/>
|
<FontAwesomeIconView glyphName="USER" glyphSize="20"/>
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<BorderPane styleClass="networkView" stylesheets="@../css/corda.css" xmlns="http://javafx.com/javafx/8.0.112-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||||
|
<center>
|
||||||
|
<StackPane>
|
||||||
|
<ScrollPane fx:id="mapScrollPane" hbarPolicy="ALWAYS" pannable="true" vbarPolicy="ALWAYS">
|
||||||
|
<Pane fx:id="mapPane" styleClass="map">
|
||||||
|
<ImageView fx:id="mapImageView" styleClass="worldMap"/>
|
||||||
|
</Pane>
|
||||||
|
</ScrollPane>
|
||||||
|
<VBox spacing="5" StackPane.alignment="TOP_LEFT" maxWidth="-Infinity" maxHeight="-Infinity">
|
||||||
|
<StackPane.margin>
|
||||||
|
<Insets bottom="25" left="5" right="5" top="5"/>
|
||||||
|
</StackPane.margin>
|
||||||
|
<TitledPane styleClass="networkTile" text="My Identity">
|
||||||
|
<BorderPane fx:id="myIdentityPane"/>
|
||||||
|
</TitledPane>
|
||||||
|
<TitledPane styleClass="networkTile" text="Notaries">
|
||||||
|
<BorderPane>
|
||||||
|
<center>
|
||||||
|
<ScrollPane hbarPolicy="NEVER">
|
||||||
|
<VBox fx:id="notaryList" maxWidth="-Infinity"/>
|
||||||
|
</ScrollPane>
|
||||||
|
</center>
|
||||||
|
</BorderPane>
|
||||||
|
</TitledPane>
|
||||||
|
<TitledPane styleClass="networkTile" text="Peers" VBox.vgrow="ALWAYS">
|
||||||
|
<BorderPane>
|
||||||
|
<center>
|
||||||
|
<ScrollPane hbarPolicy="NEVER">
|
||||||
|
<VBox fx:id="peerList" maxWidth="-Infinity">
|
||||||
|
<Button text="Template" prefHeight="100" prefWidth="200"/>
|
||||||
|
</VBox>
|
||||||
|
</ScrollPane>
|
||||||
|
</center>
|
||||||
|
</BorderPane>
|
||||||
|
</TitledPane>
|
||||||
|
</VBox>
|
||||||
|
</StackPane>
|
||||||
|
</center>
|
||||||
|
</BorderPane>
|
@ -24,7 +24,7 @@
|
|||||||
<ChoiceBox fx:id="issuerChoiceBox" maxWidth="Infinity"/>
|
<ChoiceBox fx:id="issuerChoiceBox" maxWidth="Infinity"/>
|
||||||
<TextField fx:id="issuerTextField" maxWidth="Infinity" prefWidth="100" visible="false"/>
|
<TextField fx:id="issuerTextField" maxWidth="Infinity" prefWidth="100" visible="false"/>
|
||||||
</StackPane>
|
</StackPane>
|
||||||
<Label fx:id="issueRefLabel" text="Issue Reference : " GridPane.halignment="RIGHT" GridPane.columnIndex="3" GridPane.rowIndex="3"/>
|
<Label fx:id="issueRefLabel" text="Issuer Reference : " GridPane.halignment="RIGHT" GridPane.columnIndex="3" GridPane.rowIndex="3"/>
|
||||||
|
|
||||||
<TextField fx:id="issueRefTextField" prefWidth="50" GridPane.columnIndex="4" GridPane.rowIndex="3"/>
|
<TextField fx:id="issueRefTextField" prefWidth="50" GridPane.columnIndex="4" GridPane.rowIndex="3"/>
|
||||||
|
|
||||||
|
@ -2,25 +2,24 @@
|
|||||||
|
|
||||||
<?import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView?>
|
<?import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView?>
|
||||||
<?import javafx.geometry.Insets?>
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.ComboBox?>
|
||||||
<?import javafx.scene.control.TextField?>
|
<?import javafx.scene.control.TextField?>
|
||||||
<?import javafx.scene.layout.*?>
|
<?import javafx.scene.layout.*?>
|
||||||
<StackPane styleClass="searchField" stylesheets="@../css/corda.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
<StackPane styleClass="searchField" stylesheets="@../css/corda.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||||
<padding>
|
<padding>
|
||||||
<Insets bottom="5"/>
|
<Insets bottom="5"/>
|
||||||
</padding>
|
</padding>
|
||||||
<TextField fx:id="textField" promptText="Filter transactions by originator, contract type...">
|
<TextField fx:id="textField" promptText="Filter transactions by originator, contract type..."/>
|
||||||
<padding>
|
|
||||||
<Insets left="35.0" right="5.0"/>
|
|
||||||
</padding>
|
|
||||||
</TextField>
|
|
||||||
<FontAwesomeIconView glyphName="SEARCH" StackPane.alignment="CENTER_LEFT">
|
|
||||||
<StackPane.margin>
|
|
||||||
<Insets left="10.0"/>
|
|
||||||
</StackPane.margin>
|
|
||||||
</FontAwesomeIconView>
|
|
||||||
<FontAwesomeIconView fx:id="clearButton" glyphName="TIMES_CIRCLE" styleClass="search-clear" StackPane.alignment="CENTER_RIGHT">
|
<FontAwesomeIconView fx:id="clearButton" glyphName="TIMES_CIRCLE" styleClass="search-clear" StackPane.alignment="CENTER_RIGHT">
|
||||||
<StackPane.margin>
|
<StackPane.margin>
|
||||||
<Insets right="10.0"/>
|
<Insets right="10.0"/>
|
||||||
</StackPane.margin>
|
</StackPane.margin>
|
||||||
</FontAwesomeIconView>
|
</FontAwesomeIconView>
|
||||||
|
|
||||||
|
<ComboBox fx:id="searchCategory" StackPane.alignment="CENTER_LEFT">
|
||||||
|
<StackPane.margin>
|
||||||
|
<Insets left="1.0"/>
|
||||||
|
</StackPane.margin>
|
||||||
|
</ComboBox>
|
||||||
</StackPane>
|
</StackPane>
|
@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<ScrollPane fitToWidth="true" stylesheets="@../css/corda.css" xmlns="http://javafx.com/javafx/8.0.112-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||||
|
<VBox alignment="CENTER">
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="10" left="10" right="10" top="10"/>
|
||||||
|
</padding>
|
||||||
|
<StackPane>
|
||||||
|
<TitledPane text="Client Setting">
|
||||||
|
<GridPane fx:id="clientPane" disable="true" hgap="50" vgap="20">
|
||||||
|
<padding>
|
||||||
|
<Insets top="30" right="30" bottom="30" left="30"/>
|
||||||
|
</padding>
|
||||||
|
<Label text="Reporting Currency :"/>
|
||||||
|
<ComboBox fx:id="reportingCurrenciesComboBox" GridPane.columnIndex="1"/>
|
||||||
|
|
||||||
|
<Label text="Fullscreen :" GridPane.rowIndex="1"/>
|
||||||
|
<CheckBox fx:id="fullscreenCheckBox" GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.rowIndex="1"/>
|
||||||
|
|
||||||
|
<Label text="Remember me :" GridPane.rowIndex="2"/>
|
||||||
|
<CheckBox fx:id="rememberMeCheckBox" GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.rowIndex="2"/>
|
||||||
|
|
||||||
|
<Label text="Corda Node :" GridPane.rowIndex="3" GridPane.valignment="TOP"/>
|
||||||
|
|
||||||
|
<HBox spacing="3" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="3">
|
||||||
|
<TextField fx:id="hostTextField" promptText="Host"/>
|
||||||
|
<TextField fx:id="portTextField" prefWidth="100" promptText="Port"/>
|
||||||
|
</HBox>
|
||||||
|
</GridPane>
|
||||||
|
</TitledPane>
|
||||||
|
|
||||||
|
<HBox alignment="TOP_RIGHT" maxWidth="-Infinity" maxHeight="-Infinity" StackPane.alignment="TOP_RIGHT">
|
||||||
|
<Label id="setting-edit-label" fx:id="save" text="Save" visible="false">
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="5" left="10" right="10" top="5"/>
|
||||||
|
</padding>
|
||||||
|
<graphic>
|
||||||
|
<FontAwesomeIconView glyphName="SAVE"/>
|
||||||
|
</graphic>
|
||||||
|
</Label>
|
||||||
|
<Label id="setting-edit-label" fx:id="editCancel" text="Edit">
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="5" left="10" right="10" top="5"/>
|
||||||
|
</padding>
|
||||||
|
<graphic>
|
||||||
|
<FontAwesomeIconView glyphName="EDIT"/>
|
||||||
|
</graphic>
|
||||||
|
</Label>
|
||||||
|
</HBox>
|
||||||
|
</StackPane>
|
||||||
|
</VBox>
|
||||||
|
</ScrollPane>
|
@ -3,12 +3,12 @@
|
|||||||
<?import javafx.geometry.Insets?>
|
<?import javafx.geometry.Insets?>
|
||||||
<?import javafx.scene.control.*?>
|
<?import javafx.scene.control.*?>
|
||||||
<?import javafx.scene.layout.*?>
|
<?import javafx.scene.layout.*?>
|
||||||
<BorderPane stylesheets="@../../css/corda.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
<BorderPane stylesheets="@../../../css/corda.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||||
<padding>
|
<padding>
|
||||||
<Insets right="5" left="5" bottom="5" top="5"/>
|
<Insets right="5" left="5" bottom="5" top="5"/>
|
||||||
</padding>
|
</padding>
|
||||||
<top>
|
<top>
|
||||||
<fx:include source="../SearchField.fxml"/>
|
<fx:include source="../../SearchField.fxml"/>
|
||||||
</top>
|
</top>
|
||||||
<center>
|
<center>
|
||||||
<SplitPane fx:id="splitPane" dividerPositions="0.5">
|
<SplitPane fx:id="splitPane" dividerPositions="0.5">
|
@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<DialogPane stylesheets="@../../../css/corda.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||||
|
<content>
|
||||||
|
<GridPane hgap="10" vgap="10">
|
||||||
|
<!-- Row 0 -->
|
||||||
|
<Label text="Transaction Type : " GridPane.halignment="RIGHT"/>
|
||||||
|
<ChoiceBox fx:id="transactionTypeCB" maxWidth="Infinity" GridPane.columnIndex="1" GridPane.columnSpan="4" GridPane.hgrow="ALWAYS"/>
|
||||||
|
|
||||||
|
<!-- Row 1 -->
|
||||||
|
<Label fx:id="partyALabel" GridPane.halignment="RIGHT" GridPane.rowIndex="1"/>
|
||||||
|
<TextField fx:id="partyATextField" GridPane.columnIndex="1" GridPane.columnSpan="4" GridPane.rowIndex="1"/>
|
||||||
|
|
||||||
|
<!-- Row 2 -->
|
||||||
|
<Label fx:id="partyBLabel" GridPane.halignment="RIGHT" GridPane.rowIndex="2"/>
|
||||||
|
<ChoiceBox fx:id="partyBChoiceBox" maxWidth="Infinity" GridPane.columnIndex="1" GridPane.columnSpan="4" GridPane.fillWidth="true" GridPane.hgrow="ALWAYS" GridPane.rowIndex="2"/>
|
||||||
|
|
||||||
|
<!-- Row 3 -->
|
||||||
|
<Label fx:id="issuerLabel" text="Issuer : " GridPane.halignment="RIGHT" GridPane.rowIndex="3"/>
|
||||||
|
<StackPane GridPane.columnIndex="1" GridPane.rowIndex="3" GridPane.columnSpan="1">
|
||||||
|
<ChoiceBox fx:id="issuerChoiceBox" maxWidth="Infinity"/>
|
||||||
|
<TextField fx:id="issuerTextField" maxWidth="Infinity" prefWidth="100" visible="false"/>
|
||||||
|
</StackPane>
|
||||||
|
<Label fx:id="issueRefLabel" text="Issue Reference : " GridPane.halignment="RIGHT" GridPane.columnIndex="3" GridPane.rowIndex="3"/>
|
||||||
|
|
||||||
|
<TextField fx:id="issueRefTextField" prefWidth="50" GridPane.columnIndex="4" GridPane.rowIndex="3"/>
|
||||||
|
|
||||||
|
<!-- Row 4 -->
|
||||||
|
<Label fx:id="currencyLabel" text="Currency : " GridPane.halignment="RIGHT" GridPane.rowIndex="4"/>
|
||||||
|
<ChoiceBox fx:id="currencyChoiceBox" GridPane.columnIndex="1" GridPane.rowIndex="4" maxWidth="Infinity"/>
|
||||||
|
<Label fx:id="availableAmount" text="100000 USD available" GridPane.rowIndex="4" GridPane.columnIndex="3" GridPane.columnSpan="2" styleClass="availableAmountLabel"/>
|
||||||
|
|
||||||
|
<!-- Row 5 -->
|
||||||
|
<Label fx:id="amountLabel" text="Amount : " GridPane.halignment="RIGHT" GridPane.rowIndex="5"/>
|
||||||
|
<TextField fx:id="amountTextField" maxWidth="Infinity" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" GridPane.rowIndex="5" GridPane.columnSpan="4"/>
|
||||||
|
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="20.0" left="30.0" right="30.0" top="30.0"/>
|
||||||
|
</padding>
|
||||||
|
</GridPane>
|
||||||
|
</content>
|
||||||
|
</DialogPane>
|
@ -1,21 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
|
|
||||||
|
|
||||||
<?import javafx.scene.control.*?>
|
|
||||||
<?import javafx.scene.layout.AnchorPane?>
|
|
||||||
<DialogPane expanded="true" headerText="Settings" scaleShape="false" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1">
|
|
||||||
<content>
|
|
||||||
<AnchorPane>
|
|
||||||
<children>
|
|
||||||
<Label layoutX="6.0" layoutY="6.0" text="We are" />
|
|
||||||
<ChoiceBox layoutX="146.0" layoutY="2.0" prefHeight="24.0" prefWidth="360.0" AnchorPane.leftAnchor="146.0" />
|
|
||||||
<Label layoutX="6.0" layoutY="37.0" text="Reporting currency" />
|
|
||||||
<ChoiceBox layoutX="156.0" layoutY="33.0" prefWidth="150.0" />
|
|
||||||
</children>
|
|
||||||
</AnchorPane>
|
|
||||||
</content>
|
|
||||||
<buttonTypes>
|
|
||||||
<ButtonType fx:constant="APPLY" />
|
|
||||||
<ButtonType fx:constant="CLOSE" />
|
|
||||||
</buttonTypes>
|
|
||||||
</DialogPane>
|
|
@ -0,0 +1,41 @@
|
|||||||
|
package net.corda.explorer.model
|
||||||
|
|
||||||
|
import net.corda.core.div
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
import java.nio.file.Files
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class SettingsModelTest {
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val tempFolder: TemporaryFolder = TemporaryFolder()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test save config and rollback`() {
|
||||||
|
val path = tempFolder.root.toPath() / "conf"
|
||||||
|
val config = path / "CordaExplorer.properties"
|
||||||
|
|
||||||
|
val setting = SettingsModel(path)
|
||||||
|
assertEquals("", setting.hostProperty.value)
|
||||||
|
assertEquals("", setting.portProperty.value)
|
||||||
|
setting.hostProperty.value = "host"
|
||||||
|
setting.portProperty.value = "100"
|
||||||
|
assertEquals("host", setting.hostProperty.value)
|
||||||
|
assertEquals("100", setting.portProperty.value)
|
||||||
|
assertFalse(Files.exists(config))
|
||||||
|
setting.commit()
|
||||||
|
assertTrue(Files.exists(config))
|
||||||
|
setting.hostProperty.value = "host2"
|
||||||
|
setting.portProperty.value = "200"
|
||||||
|
assertEquals("host2", setting.hostProperty.value)
|
||||||
|
assertEquals("200", setting.portProperty.value)
|
||||||
|
// Rollback discarding all in memory data.
|
||||||
|
setting.load()
|
||||||
|
assertEquals("host", setting.hostProperty.value)
|
||||||
|
assertEquals("100", setting.portProperty.value)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user