mirror of
https://github.com/corda/corda.git
synced 2024-12-19 04:57:58 +00:00
Explorer corda branding
This commit is contained in:
parent
95c404bb67
commit
fbf952a1ab
@ -1,6 +1,7 @@
|
||||
package net.corda.client.fxutils
|
||||
|
||||
import javafx.beans.binding.Bindings
|
||||
import javafx.beans.binding.BooleanBinding
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.beans.value.ObservableValue
|
||||
@ -277,5 +278,25 @@ fun <A> ObservableList<A>.last(): ObservableValue<A?> {
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
@ -48,7 +48,7 @@ class EventGenerator(
|
||||
}
|
||||
)
|
||||
val producedGenerator: Generator<Set<StateAndRef<ContractState>>> = Generator.frequency(
|
||||
// 0.1 to Generator.pure(setOf())
|
||||
// 0.1 to Generator.pure(setOf())
|
||||
0.9 to Generator.impure { vault }.bind { states ->
|
||||
Generator.replicate(2, cashStateGenerator).map {
|
||||
vault = states + it
|
||||
@ -89,9 +89,12 @@ class EventGenerator(
|
||||
)
|
||||
}
|
||||
|
||||
val clientToServiceCommandGenerator = Generator.frequency(
|
||||
0.4 to issueCashGenerator,
|
||||
0.5 to moveCashGenerator,
|
||||
0.1 to exitCashGenerator
|
||||
val clientCommandGenerator = Generator.frequency(
|
||||
1.0 to moveCashGenerator
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
class GatheredTransactionDataModel {
|
||||
|
||||
private val transactions by observable(NodeMonitorModel::transactions)
|
||||
private val stateMachineUpdates by observable(NodeMonitorModel::stateMachineUpdates)
|
||||
private val progressTracking by observable(NodeMonitorModel::progressTracking)
|
||||
@ -89,41 +88,26 @@ class GatheredTransactionDataModel {
|
||||
private val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
|
||||
private val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
|
||||
private val stateMachineStatus = stateMachineUpdates.foldToObservableMap(Unit) { update, _unit, map: ObservableMap<StateMachineRunId, SimpleObjectProperty<StateMachineStatus>> ->
|
||||
when (update) {
|
||||
is StateMachineUpdate.Added -> {
|
||||
val added: SimpleObjectProperty<StateMachineStatus> =
|
||||
SimpleObjectProperty(StateMachineStatus.Added(update.stateMachineInfo.flowLogicClassName))
|
||||
map[update.id] = added
|
||||
}
|
||||
is StateMachineUpdate.Removed -> {
|
||||
val added = map[update.id]
|
||||
added ?: throw Exception("State machine removed with unknown id ${update.id}")
|
||||
added.set(StateMachineStatus.Removed(added.value.stateMachineName))
|
||||
}
|
||||
}
|
||||
when (update) {
|
||||
is StateMachineUpdate.Added -> {
|
||||
val added: SimpleObjectProperty<StateMachineStatus> =
|
||||
SimpleObjectProperty(StateMachineStatus.Added(update.stateMachineInfo.flowLogicClassName))
|
||||
map[update.id] = added
|
||||
}
|
||||
is StateMachineUpdate.Removed -> {
|
||||
val added = map[update.id]
|
||||
added ?: throw Exception("State machine removed with unknown id ${update.id}")
|
||||
added.set(StateMachineStatus.Removed(added.value.stateMachineName))
|
||||
}
|
||||
}
|
||||
}
|
||||
private val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
|
||||
StateMachineData(id, progress.map { it?.let { FlowStatus(it.message) } }, status)
|
||||
}.getObservableValues()
|
||||
}.getObservableValues()
|
||||
// TODO : Create a new screen for state machines.
|
||||
private val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
|
||||
private val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
|
||||
private val partiallyResolvedTransactions = collectedTransactions.map {
|
||||
val partiallyResolvedTransactions = collectedTransactions.map {
|
||||
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
|
||||
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.ObservableList
|
||||
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.map
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.NetworkMapCache
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import java.security.PublicKey
|
||||
|
||||
class NetworkIdentityModel {
|
||||
private val networkIdentityObservable by observable(NodeMonitorModel::networkMap)
|
||||
|
||||
private val networkIdentities: ObservableList<NodeInfo> =
|
||||
val networkIdentities: ObservableList<NodeInfo> =
|
||||
networkIdentityObservable.foldToObservableList(Unit) { update, _accumulator, observableList ->
|
||||
observableList.removeIf {
|
||||
when (update.type) {
|
||||
@ -31,10 +35,15 @@ class NetworkIdentityModel {
|
||||
val myIdentity = rpcProxy.map { it?.nodeIdentity() }
|
||||
|
||||
private fun NodeInfo.isCordaService(): Boolean {
|
||||
// TODO: better way to identify Corda service?
|
||||
return advertisedServices.any { it.info.type == NetworkMapService.type || it.info.type.isNotary() }
|
||||
}
|
||||
|
||||
fun lookup(compositeKey: CompositeKey): NodeInfo? {
|
||||
return parties.firstOrNull { it.legalIdentity.owningKey == compositeKey } ?: notaries.firstOrNull { it.notaryIdentity.owningKey == compositeKey }
|
||||
fun lookup(compositeKey: CompositeKey): ObservableValue<NodeInfo?> = parties.firstOrDefault(notaries.firstOrNullObservable { 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"
|
||||
nearestCity = "The Moon"
|
||||
nearestCity = "London"
|
||||
emailAddress = "admin@company.com"
|
||||
exportJMXto = "http"
|
||||
keyStorePassword = "cordacadevpass"
|
||||
|
@ -48,7 +48,7 @@ dependencies {
|
||||
testCompile group: 'junit', name: 'junit', version: '4.11'
|
||||
|
||||
// TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's.
|
||||
compile 'no.tornado:tornadofx:1.5.6'
|
||||
compile 'no.tornado:tornadofx:1.5.7'
|
||||
|
||||
// Corda Core: Data structures and basic types needed to work with Corda.
|
||||
compile project(':core')
|
||||
|
@ -8,19 +8,21 @@ import javafx.scene.control.ButtonType
|
||||
import javafx.scene.image.Image
|
||||
import javafx.stage.Stage
|
||||
import jfxtras.resources.JFXtrasFontRoboto
|
||||
import net.corda.client.CordaRPCClient
|
||||
import net.corda.client.mock.EventGenerator
|
||||
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.ServiceType
|
||||
import net.corda.explorer.model.CordaViewModel
|
||||
import net.corda.explorer.model.SettingsModel
|
||||
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.node.driver.PortAllocation
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.User
|
||||
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.startFlow
|
||||
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.
|
||||
*/
|
||||
class Main : App() {
|
||||
override val primaryView = MainView::class
|
||||
class Main : App(MainView::class) {
|
||||
private val loginView by inject<LoginView>()
|
||||
private val fullscreen by observableValue(SettingsModel::fullscreenProperty)
|
||||
|
||||
override fun start(stage: Stage) {
|
||||
// Login to Corda node
|
||||
loginView.login { hostAndPort, username, password ->
|
||||
Models.get<NodeMonitorModel>(MainView::class).register(hostAndPort, configureTestSSL(), username, password)
|
||||
}
|
||||
super.start(stage)
|
||||
stage.minHeight = 600.0
|
||||
stage.minWidth = 800.0
|
||||
stage.isFullScreen = fullscreen.value
|
||||
stage.setOnCloseRequest {
|
||||
val button = Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to exit Corda explorer?").apply {
|
||||
initOwner(stage.scene.window)
|
||||
}.showAndWait().get()
|
||||
if (button != ButtonType.OK) it.consume()
|
||||
}
|
||||
stage.hide()
|
||||
loginView.login()
|
||||
stage.show()
|
||||
}
|
||||
|
||||
init {
|
||||
@ -103,32 +106,55 @@ fun main(args: Array<String>) {
|
||||
val portAllocation = PortAllocation.Incremental(20000)
|
||||
driver(portAllocation = portAllocation) {
|
||||
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 alice = startNode("Alice", rpcUsers = arrayListOf(user))
|
||||
val bob = startNode("Bob", rpcUsers = arrayListOf(user))
|
||||
val alice = startNode("Alice", rpcUsers = arrayListOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))))
|
||||
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 aliceNode = alice.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)}")
|
||||
}
|
||||
// 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 rpcProxy = Models.get<NodeMonitorModel>(Main::class).proxyObservable.get()
|
||||
val aliceClient = CordaRPCClient(ArtemisMessagingComponent.toHostAndPort(aliceNode.nodeInfo.address), FullNodeConfiguration(aliceNode.config))
|
||||
aliceClient.start(user.username, user.password)
|
||||
val aliceRPC = aliceClient.proxy()
|
||||
|
||||
for (i in 0..10) {
|
||||
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()
|
||||
|
||||
val eventGenerator = EventGenerator(
|
||||
parties = listOf(aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity, issuerNode.nodeInfo.legalIdentity),
|
||||
notary = notaryNode.nodeInfo.notaryIdentity
|
||||
)
|
||||
|
||||
for (i in 0..1000) {
|
||||
Thread.sleep(500)
|
||||
val eventGenerator = EventGenerator(
|
||||
parties = listOf(aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity),
|
||||
notary = notaryNode.nodeInfo.notaryIdentity
|
||||
)
|
||||
eventGenerator.clientToServiceCommandGenerator.map { command ->
|
||||
rpcProxy?.startFlow(::CashFlow, command)
|
||||
listOf(aliceRPC, bobRPC).forEach {
|
||||
eventGenerator.clientCommandGenerator.map { command ->
|
||||
it.startFlow(::CashFlow, command)
|
||||
Unit
|
||||
}.generate(SplittableRandom())
|
||||
}
|
||||
eventGenerator.bankOfCordaCommandGenerator.map { command ->
|
||||
bocRPC.startFlow(::CashFlow, command)
|
||||
Unit
|
||||
}.generate(SplittableRandom())
|
||||
}
|
||||
|
||||
aliceClient.close()
|
||||
bobClient.close()
|
||||
issuerClient.close()
|
||||
waitForAllNodesToFinish()
|
||||
}
|
||||
}
|
@ -1,16 +1,19 @@
|
||||
package net.corda.explorer.identicon
|
||||
|
||||
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.canvas.Canvas
|
||||
import javafx.scene.canvas.GraphicsContext
|
||||
import javafx.scene.control.ContentDisplay
|
||||
import javafx.scene.control.Tooltip
|
||||
import javafx.scene.image.Image
|
||||
import javafx.scene.image.ImageView
|
||||
import javafx.scene.image.WritableImage
|
||||
import javafx.scene.paint.Color
|
||||
import javafx.scene.text.TextAlignment
|
||||
import net.corda.core.crypto.SecureHash
|
||||
|
||||
/**
|
||||
* (The MIT License)
|
||||
@ -39,39 +42,42 @@ import javafx.scene.text.TextAlignment
|
||||
* And has been modified to Kotlin and JavaFX instead of Java code using AWT
|
||||
*/
|
||||
|
||||
class IdenticonRenderer {
|
||||
object IdenticonRenderer {
|
||||
/**
|
||||
* Each patch is a polygon created from a list of vertices on a 5 by 5 grid.
|
||||
* Vertices are numbered from 0 to 24, starting from top-left corner of the
|
||||
* grid, moving left to right and top to bottom.
|
||||
*/
|
||||
private val patchTypes = arrayOf(
|
||||
byteArrayOf(0, 4, 24, 20, 0),
|
||||
byteArrayOf(0, 4, 20, 0),
|
||||
byteArrayOf(2, 24, 20, 2),
|
||||
byteArrayOf(0, 2, 20, 22, 0),
|
||||
byteArrayOf(2, 14, 22, 10, 2),
|
||||
byteArrayOf(0, 14, 24, 22, 0),
|
||||
byteArrayOf(2, 24, 22, 13, 11, 22, 20, 2),
|
||||
byteArrayOf(0, 14, 22, 0),
|
||||
byteArrayOf(6, 8, 18, 16, 6),
|
||||
byteArrayOf(4, 20, 10, 12, 2, 4),
|
||||
byteArrayOf(0, 2, 12, 10, 0),
|
||||
byteArrayOf(10, 14, 22, 10),
|
||||
byteArrayOf(20, 12, 24, 20),
|
||||
byteArrayOf(10, 2, 12, 10),
|
||||
byteArrayOf(0, 2, 10, 0),
|
||||
byteArrayOf(0, 4, 24, 20, 0)).map(::Patch)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Each patch is a polygon created from a list of vertices on a 5 by 5 grid.
|
||||
* Vertices are numbered from 0 to 24, starting from top-left corner of the
|
||||
* grid, moving left to right and top to bottom.
|
||||
*/
|
||||
private val patchTypes = arrayOf(
|
||||
byteArrayOf(0, 4, 24, 20, 0),
|
||||
byteArrayOf(0, 4, 20, 0),
|
||||
byteArrayOf(2, 24, 20, 2),
|
||||
byteArrayOf(0, 2, 20, 22, 0),
|
||||
byteArrayOf(2, 14, 22, 10, 2),
|
||||
byteArrayOf(0, 14, 24, 22, 0),
|
||||
byteArrayOf(2, 24, 22, 13, 11, 22, 20, 2),
|
||||
byteArrayOf(0, 14, 22, 0),
|
||||
byteArrayOf(6, 8, 18, 16, 6),
|
||||
byteArrayOf(4, 20, 10, 12, 2, 4),
|
||||
byteArrayOf(0, 2, 12, 10, 0),
|
||||
byteArrayOf(10, 14, 22, 10),
|
||||
byteArrayOf(20, 12, 24, 20),
|
||||
byteArrayOf(10, 2, 12, 10),
|
||||
byteArrayOf(0, 2, 10, 0),
|
||||
byteArrayOf(0, 4, 24, 20, 0)).map(::Patch)
|
||||
private val PATCH_CELLS = 4
|
||||
private val PATCH_GRIDS = PATCH_CELLS + 1
|
||||
private val PATCH_SYMMETRIC: Byte = 1
|
||||
private val PATCH_INVERTED: Byte = 2
|
||||
|
||||
private val PATCH_CELLS = 4
|
||||
private val PATCH_GRIDS = PATCH_CELLS + 1
|
||||
private val PATCH_SYMMETRIC: Byte = 1
|
||||
private val PATCH_INVERTED: Byte = 2
|
||||
private val patchFlags = byteArrayOf(PATCH_SYMMETRIC, 0, 0, 0, PATCH_SYMMETRIC, 0, 0, 0, PATCH_SYMMETRIC, 0, 0, 0, 0, 0, 0, (PATCH_SYMMETRIC + PATCH_INVERTED).toByte())
|
||||
|
||||
private 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) {
|
||||
fun x(patchSize: Double): DoubleArray {
|
||||
@ -85,13 +91,17 @@ class IdenticonRenderer {
|
||||
val size = byteArray.size
|
||||
}
|
||||
|
||||
fun getIdenticon(hash: SecureHash): Image {
|
||||
return cache.get(hash)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns rendered identicon image for given identicon code.
|
||||
* Size of the returned identicon image is determined by patchSize set using
|
||||
* [setPatchSize]. Since a 9-block identicon consists of 3x3 patches,
|
||||
* width and height will be 3 times the patch size.
|
||||
*/
|
||||
fun render(code: Int, patchSize: Double, backgroundColor: Color = Color.WHITE): WritableImage {
|
||||
private fun render(code: Int, patchSize: Double, backgroundColor: Color = Color.WHITE): Image {
|
||||
// decode the code into parts
|
||||
val middleType = intArrayOf(0, 4, 8, 15)[code and 0x3] // bit 0-1: middle patch type
|
||||
val middleInvert = code shr 2 and 0x1 != 0 // bit 2: middle invert
|
||||
@ -177,18 +187,22 @@ class IdenticonRenderer {
|
||||
val dx = (c1.red - c2.red) * 256
|
||||
val dy = (c1.green - c2.green) * 256
|
||||
val dz = (c1.blue - c2.blue) * 256
|
||||
return Math.sqrt(dx * dx + dy * dy + dz * dz.toDouble()).toFloat()
|
||||
return Math.sqrt(dx * dx + dy * dy + dz * dz).toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
fun identicon(secureHash: SecureHash, size: Double): WritableImage {
|
||||
return IdenticonRenderer().render(secureHash.hashCode(), size)
|
||||
fun identicon(secureHash: SecureHash, size: Double): ImageView {
|
||||
return ImageView(IdenticonRenderer.getIdenticon(secureHash)).apply {
|
||||
isPreserveRatio = true
|
||||
fitWidth = size
|
||||
}
|
||||
}
|
||||
|
||||
fun identiconToolTip(secureHash: SecureHash): Tooltip {
|
||||
return Tooltip(Splitter.fixedLength(16).split("$secureHash").joinToString("\n")).apply {
|
||||
contentDisplay = ContentDisplay.TOP
|
||||
textAlignment = TextAlignment.CENTER
|
||||
graphic = ImageView(identicon(secureHash, 30.0))
|
||||
graphic = identicon(secureHash, 90.0)
|
||||
isAutoHide = false
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package net.corda.explorer.model
|
||||
|
||||
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.scene.Node
|
||||
import tornadofx.View
|
||||
import tornadofx.find
|
||||
@ -24,10 +25,12 @@ class CordaViewModel {
|
||||
* TODO : "goto" functionality?
|
||||
*/
|
||||
abstract class CordaView(title: String? = null) : View(title) {
|
||||
abstract val widget: Node?
|
||||
open val widgets: ObservableList<CordaWidget> = emptyList<CordaWidget>().observable()
|
||||
abstract val icon: FontAwesomeIcon
|
||||
|
||||
init {
|
||||
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
|
||||
|
||||
import javafx.beans.value.ObservableValue
|
||||
import net.corda.client.fxutils.AmountBindings
|
||||
import net.corda.client.model.ExchangeRate
|
||||
import net.corda.client.model.ExchangeRateModel
|
||||
import net.corda.client.model.observableValue
|
||||
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 tornadofx.observable
|
||||
import java.util.*
|
||||
|
||||
class ReportingCurrencyModel {
|
||||
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
|
||||
* exchange rates change
|
||||
|
@ -1,11 +1,94 @@
|
||||
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 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 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 javafx.beans.binding.Bindings
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.scene.Node
|
||||
import javafx.scene.Parent
|
||||
import javafx.scene.control.TitledPane
|
||||
import javafx.scene.input.MouseButton
|
||||
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.model.observableList
|
||||
import net.corda.client.model.writableValue
|
||||
@ -17,30 +18,16 @@ import net.corda.explorer.model.CordaViewModel
|
||||
class Dashboard : CordaView() {
|
||||
override val root: Parent by fxml()
|
||||
override val icon = FontAwesomeIcon.DASHBOARD
|
||||
override val widget: Node? = null
|
||||
private val tilePane: TilePane by fxid()
|
||||
private val template: TitledPane by fxid()
|
||||
|
||||
private val selectedView by writableValue(CordaViewModel::selectedView)
|
||||
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 {
|
||||
val widgetPanes = registeredViews.map { view ->
|
||||
view.widget?.let {
|
||||
TitledPane(view.title, it).apply {
|
||||
styleClass.addAll(template.styleClass)
|
||||
collapsibleProperty().bind(template.collapsibleProperty())
|
||||
setOnMouseClicked {
|
||||
if (it.button == MouseButton.PRIMARY) {
|
||||
selectedView.value = view
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.filterNotNull()
|
||||
|
||||
Bindings.bindContent(tilePane.children, widgetPanes)
|
||||
|
||||
// Dynamically change column count and width according to the window size.
|
||||
tilePane.widthProperty().addListener { e ->
|
||||
val prefWidth = 350
|
||||
@ -48,4 +35,19 @@ class Dashboard : CordaView() {
|
||||
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)
|
||||
collapsibleProperty().bind(template.collapsibleProperty())
|
||||
setOnMouseClicked {
|
||||
if (it.button == MouseButton.PRIMARY) {
|
||||
selectedView.value = view
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,15 +1,20 @@
|
||||
package net.corda.explorer.views
|
||||
|
||||
import javafx.application.Platform
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.event.EventTarget
|
||||
import javafx.geometry.Pos
|
||||
import javafx.scene.Parent
|
||||
import javafx.scene.control.TextField
|
||||
import javafx.scene.layout.GridPane
|
||||
import javafx.scene.layout.Priority
|
||||
import javafx.scene.text.TextAlignment
|
||||
import javafx.util.StringConverter
|
||||
import net.corda.client.model.Models
|
||||
import tornadofx.View
|
||||
import tornadofx.gridpane
|
||||
import tornadofx.label
|
||||
import tornadofx.textfield
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
return gridpane {
|
||||
label("Under Construction...") {
|
||||
@ -60,4 +68,16 @@ fun EventTarget.underConstruction(): Parent {
|
||||
GridPane.setHgrow(this, Priority.ALWAYS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 javafx.beans.property.SimpleIntegerProperty
|
||||
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 tornadofx.View
|
||||
import kotlin.system.exitProcess
|
||||
@ -10,32 +14,51 @@ import kotlin.system.exitProcess
|
||||
class LoginView : View() {
|
||||
override val root by fxml<DialogPane>()
|
||||
|
||||
private val host by fxid<TextField>()
|
||||
private val port by fxid<TextField>()
|
||||
private val username by fxid<TextField>()
|
||||
private val password by fxid<PasswordField>()
|
||||
private val hostTextField by fxid<TextField>()
|
||||
private val portTextField by fxid<TextField>()
|
||||
private val usernameTextField by fxid<TextField>()
|
||||
private val passwordTextField by fxid<PasswordField>()
|
||||
private val rememberMeCheckBox by fxid<CheckBox>()
|
||||
private val fullscreenCheckBox by fxid<CheckBox>()
|
||||
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 {
|
||||
dialogPane = root
|
||||
setResultConverter {
|
||||
when (it?.buttonData) {
|
||||
ButtonBar.ButtonData.OK_DONE -> try {
|
||||
root.isDisable = true
|
||||
// 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
|
||||
} catch (e: Exception) {
|
||||
// TODO : Handle this in a more user friendly way.
|
||||
e.printStackTrace()
|
||||
ExceptionDialog(e).apply { initOwner(root.scene.window) }.showAndWait()
|
||||
LoginStatus.exception
|
||||
} finally {
|
||||
root.isDisable = false
|
||||
}
|
||||
else -> LoginStatus.exited
|
||||
}
|
||||
}
|
||||
setOnCloseRequest {
|
||||
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)
|
||||
}.showAndWait().get()
|
||||
if (button == ButtonType.OK) {
|
||||
@ -44,12 +67,17 @@ class LoginView : View() {
|
||||
}
|
||||
}
|
||||
}.showAndWait().get()
|
||||
if (status != LoginStatus.loggedIn) login(loginFunction)
|
||||
if (status != LoginStatus.loggedIn) login()
|
||||
}
|
||||
|
||||
init {
|
||||
// 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 {
|
||||
|
@ -8,12 +8,15 @@ import javafx.geometry.Pos
|
||||
import javafx.scene.Parent
|
||||
import javafx.scene.control.ContentDisplay
|
||||
import javafx.scene.control.MenuButton
|
||||
import javafx.scene.control.MenuItem
|
||||
import javafx.scene.input.MouseButton
|
||||
import javafx.scene.layout.BorderPane
|
||||
import javafx.scene.layout.StackPane
|
||||
import javafx.scene.layout.VBox
|
||||
import javafx.scene.text.Font
|
||||
import javafx.scene.text.TextAlignment
|
||||
import javafx.stage.Stage
|
||||
import javafx.stage.WindowEvent
|
||||
import net.corda.client.fxutils.ChosenList
|
||||
import net.corda.client.fxutils.map
|
||||
import net.corda.client.model.NetworkIdentityModel
|
||||
@ -31,6 +34,7 @@ class MainView : View() {
|
||||
|
||||
// Inject components.
|
||||
private val userButton by fxid<MenuButton>()
|
||||
private val exit by fxid<MenuItem>()
|
||||
private val sidebar by fxid<VBox>()
|
||||
private val selectionBorderPane by fxid<BorderPane>()
|
||||
|
||||
@ -46,11 +50,14 @@ class MainView : View() {
|
||||
init {
|
||||
// Header
|
||||
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
|
||||
val menuItems = registeredViews.map {
|
||||
// This needed to be declared val or else it will get GCed and listener unregistered.
|
||||
val buttonStyle = ChosenList(selectedView.map { selected->
|
||||
if(selected == it) listOf(menuItemCSS, menuItemSelectedCSS).observable() else listOf(menuItemCSS).observable()
|
||||
val buttonStyle = ChosenList(selectedView.map { selected ->
|
||||
if (selected == it) listOf(menuItemCSS, menuItemSelectedCSS).observable() else listOf(menuItemCSS).observable()
|
||||
})
|
||||
stackpane {
|
||||
button(it.title) {
|
||||
|
@ -1,13 +1,100 @@
|
||||
package net.corda.explorer.views
|
||||
|
||||
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
|
||||
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView
|
||||
import javafx.beans.binding.Bindings
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
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 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.
|
||||
class Network : CordaView() {
|
||||
override val root = underConstruction()
|
||||
override val widget: Node? = null
|
||||
override val root by fxml<Parent>()
|
||||
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
|
||||
|
||||
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.geometry.Insets
|
||||
import javafx.geometry.Pos
|
||||
import javafx.scene.Node
|
||||
import javafx.scene.Parent
|
||||
import javafx.scene.control.ComboBox
|
||||
import javafx.scene.control.ListCell
|
||||
import javafx.scene.control.TextField
|
||||
import javafx.scene.input.MouseButton
|
||||
import javafx.scene.input.MouseEvent
|
||||
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 tornadofx.UIComponent
|
||||
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()
|
||||
private val textField by fxid<TextField>()
|
||||
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.
|
||||
// TODO : Allow user to chose if there are matches in multiple category.
|
||||
val filteredData = ChosenList(textField.textProperty().map { text ->
|
||||
if (text.isBlank()) data else filterCriteria.map { criterion ->
|
||||
data.filter({ state: T -> criterion(state, text) }.lift())
|
||||
}.maxBy { it.size } ?: emptyList<T>().observable()
|
||||
})
|
||||
val filteredData = ChosenList(Bindings.createObjectBinding({
|
||||
val text = textField.text
|
||||
val category = searchCategory.value
|
||||
data.filtered { data ->
|
||||
text.isNullOrBlank() || if (category == ALL) {
|
||||
filterCriteria.any { it.second(data, text) }
|
||||
} else {
|
||||
filterCriteria.toMap()[category]?.invoke(data, text) ?: false
|
||||
}
|
||||
}
|
||||
}, arrayOf<Observable>(textField.textProperty(), searchCategory.valueProperty())))
|
||||
|
||||
init {
|
||||
clearButton.setOnMouseClicked { event: MouseEvent ->
|
||||
@ -33,5 +49,30 @@ class SearchField<T>(private val data: ObservableList<T>, vararg filterCriteria:
|
||||
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
|
||||
|
||||
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
|
||||
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView
|
||||
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.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() {
|
||||
override val root = underConstruction()
|
||||
override val widget: Node? = null
|
||||
override val root by fxml<Parent>()
|
||||
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 javafx.beans.binding.Bindings
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.geometry.HPos
|
||||
import javafx.geometry.Insets
|
||||
import javafx.geometry.Pos
|
||||
import javafx.scene.Node
|
||||
@ -12,24 +13,24 @@ import javafx.scene.control.Label
|
||||
import javafx.scene.control.ListView
|
||||
import javafx.scene.control.TableView
|
||||
import javafx.scene.control.TitledPane
|
||||
import javafx.scene.layout.Background
|
||||
import javafx.scene.layout.BackgroundFill
|
||||
import javafx.scene.layout.BorderPane
|
||||
import javafx.scene.layout.CornerRadii
|
||||
import javafx.scene.paint.Color
|
||||
import net.corda.client.fxutils.*
|
||||
import javafx.scene.layout.VBox
|
||||
import net.corda.client.fxutils.filterNotNull
|
||||
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.contracts.asset.Cash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.composite
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.explorer.AmountDiff
|
||||
import net.corda.explorer.formatters.AmountFormatter
|
||||
import net.corda.explorer.identicon.identicon
|
||||
import net.corda.explorer.identicon.identiconToolTip
|
||||
import net.corda.explorer.model.CordaView
|
||||
import net.corda.explorer.model.CordaWidget
|
||||
import net.corda.explorer.model.ReportingCurrencyModel
|
||||
import net.corda.explorer.sign
|
||||
import net.corda.explorer.ui.setCustomCellFactory
|
||||
@ -40,199 +41,206 @@ class TransactionViewer : CordaView("Transactions") {
|
||||
override val root by fxml<BorderPane>()
|
||||
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>()
|
||||
// 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 reportingCurrency by observableValue(ReportingCurrencyModel::reportingCurrency)
|
||||
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
|
||||
* have the data.
|
||||
*/
|
||||
data class ViewerNode(
|
||||
val transaction: PartiallyResolvedTransaction,
|
||||
val inputContracts: List<Contract>,
|
||||
val outputContracts: List<Contract>,
|
||||
val stateMachineRunId: ObservableValue<StateMachineRunId?>,
|
||||
val stateMachineStatus: ObservableValue<out StateMachineStatus?>,
|
||||
val flowStatus: ObservableValue<out FlowStatus?>,
|
||||
val commandTypes: Collection<Class<CommandData>>,
|
||||
data class Transaction(
|
||||
val tx: PartiallyResolvedTransaction,
|
||||
val id: SecureHash,
|
||||
val inputs: Inputs,
|
||||
val outputs: ObservableList<StateAndRef<ContractState>>,
|
||||
val inputParties: ObservableList<List<ObservableValue<NodeInfo?>>>,
|
||||
val outputParties: ObservableList<List<ObservableValue<NodeInfo?>>>,
|
||||
val commandTypes: List<Class<CommandData>>,
|
||||
val totalValueEquiv: ObservableValue<AmountDiff<Currency>>
|
||||
)
|
||||
|
||||
/**
|
||||
* 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
|
||||
)
|
||||
data class Inputs(val resolved: ObservableList<StateAndRef<ContractState>>, val unresolved: ObservableList<StateRef>)
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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
|
||||
// Transaction table
|
||||
transactionViewTable.apply {
|
||||
items = searchField.filteredData
|
||||
column("Transaction ID", ViewerNode::transaction).setCustomCellFactory {
|
||||
label("${it.id}") {
|
||||
graphic = imageview {
|
||||
image = identicon(it.id, 5.0)
|
||||
}
|
||||
tooltip = identiconToolTip(it.id)
|
||||
column("Transaction ID", Transaction::id) { maxWidth = 200.0 }.setCustomCellFactory {
|
||||
label("$it") {
|
||||
graphic = identicon(it, 15.0)
|
||||
tooltip = identiconToolTip(it)
|
||||
}
|
||||
}
|
||||
column("Input Contract Type(s)", ViewerNode::inputContracts).cellFormat { text = (it.map { it.javaClass.simpleName }.toSet().joinToString(", ")) }
|
||||
column("Output Contract Type(s)", ViewerNode::outputContracts).cellFormat { text = it.map { it.javaClass.simpleName }.toSet().joinToString(", ") }
|
||||
column("State Machine ID", ViewerNode::stateMachineRunId).cellFormat { text = "${it?.uuid ?: ""}" }
|
||||
column("Flow status", ViewerNode::flowStatus).cellFormat { text = "${it.value ?: ""}" }
|
||||
column("SM Status", ViewerNode::stateMachineStatus).cellFormat { text = "${it.value ?: ""}" }
|
||||
column("Command type(s)", ViewerNode::commandTypes).cellFormat { text = it.map { it.simpleName }.joinToString(",") }
|
||||
column("Total value (USD equiv)", ViewerNode::totalValueEquiv)
|
||||
.cellFormat { text = "${it.positivity.sign}${AmountFormatter.boring.format(it.amount)}" }
|
||||
rowExpander(true) {
|
||||
add(ContractStatesView(it.transaction).root)
|
||||
background = Background(BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY))
|
||||
column("Input", Transaction::inputs).cellFormat {
|
||||
text = it.resolved.toText()
|
||||
if (!it.unresolved.isEmpty()) {
|
||||
if (!text.isBlank()) {
|
||||
text += ", "
|
||||
}
|
||||
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 {
|
||||
// Hide the expander column.
|
||||
isVisible = false
|
||||
prefWidth = 0.0
|
||||
prefWidth = 26.0
|
||||
isResizable = false
|
||||
}
|
||||
setColumnResizePolicy { true }
|
||||
}
|
||||
|
||||
matchingTransactionsLabel.textProperty().bind(Bindings.size(transactionViewTable.items).map {
|
||||
"$it matching transaction${if (it == 1) "" else "s"}"
|
||||
})
|
||||
}
|
||||
|
||||
private class ContractStatesView(val transaction: PartiallyResolvedTransaction) : View() {
|
||||
override val root: Parent by fxml()
|
||||
private val inputs: ListView<StateNode> by fxid()
|
||||
private val outputs: ListView<StateNode> by fxid()
|
||||
private val signatures: ListView<CompositeKey> by fxid()
|
||||
private val inputPane: TitledPane by fxid()
|
||||
private val outputPane: TitledPane by fxid()
|
||||
private val signaturesPane: TitledPane by fxid()
|
||||
|
||||
init {
|
||||
val inputStates = transaction.inputs.map { StateNode(it, it.value.stateRef) }
|
||||
val outputStates = transaction.transaction.tx.outputs.mapIndexed { index, transactionState ->
|
||||
val stateRef = StateRef(transaction.id, index)
|
||||
StateNode(PartiallyResolvedTransaction.InputResolution.Resolved(StateAndRef(transactionState, stateRef)).lift(), stateRef)
|
||||
}
|
||||
|
||||
val signatureData = transaction.transaction.sigs.map { it.by.composite }
|
||||
// Bind count to TitlePane
|
||||
inputPane.textProperty().bind(inputStates.lift().map { "Input (${it.count()})" })
|
||||
outputPane.textProperty().bind(outputStates.lift().map { "Output (${it.count()})" })
|
||||
signaturesPane.textProperty().bind(signatureData.lift().map { "Signatures (${it.count()})" })
|
||||
|
||||
val cellFactory = { node: StateNode ->
|
||||
(node.state.value as? PartiallyResolvedTransaction.InputResolution.Resolved)?.run {
|
||||
val data = stateAndRef.state.data
|
||||
form {
|
||||
label("${data.contract.javaClass.simpleName} (${stateAndRef.ref.toString().substring(0, 16)}...)[${stateAndRef.ref.index}]") {
|
||||
graphic = imageview {
|
||||
image = identicon(stateAndRef.ref.txhash, 10.0)
|
||||
}
|
||||
tooltip = identiconToolTip(stateAndRef.ref.txhash)
|
||||
}
|
||||
when (data) {
|
||||
is Cash.State -> form {
|
||||
fieldset {
|
||||
field("Amount :") {
|
||||
label(AmountFormatter.boring.format(data.amount.withoutIssuer()))
|
||||
}
|
||||
field("Issuer :") {
|
||||
label("${data.amount.token.issuer}") {
|
||||
tooltip(data.amount.token.issuer.party.owningKey.toString())
|
||||
}
|
||||
}
|
||||
field("Owner :") {
|
||||
val owner = data.owner
|
||||
val nodeInfo = Models.get<NetworkIdentityModel>(TransactionViewer::class).lookup(owner)
|
||||
label(nodeInfo?.legalIdentity?.name ?: "???") {
|
||||
tooltip(data.owner.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO : Generic view using reflection?
|
||||
else -> label {}
|
||||
}
|
||||
}
|
||||
} ?: label { text = "???" }
|
||||
}
|
||||
|
||||
inputs.setCustomCellFactory(cellFactory)
|
||||
outputs.setCustomCellFactory(cellFactory)
|
||||
|
||||
inputs.items = FXCollections.observableList(inputStates)
|
||||
outputs.items = FXCollections.observableList(outputStates)
|
||||
signatures.items = FXCollections.observableList(signatureData)
|
||||
|
||||
signatures.apply {
|
||||
cellFormat { key ->
|
||||
val nodeInfo = Models.get<NetworkIdentityModel>(TransactionViewer::class).lookup(key)
|
||||
text = "$key (${nodeInfo?.legalIdentity?.name ?: "???"})"
|
||||
}
|
||||
prefHeight = 185.0
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun ObservableList<StateAndRef<ContractState>>.getParties() = map { it.state.data.participants.map { getModel<NetworkIdentityModel>().lookup(it) } }
|
||||
private fun ObservableList<StateAndRef<ContractState>>.toText() = map { it.contract().javaClass.simpleName }.groupBy { it }.map { "${it.key} (${it.value.size})" }.joinToString()
|
||||
|
||||
private class TransactionWidget() : BorderPane() {
|
||||
private val gatheredTransactionDataList by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
|
||||
private val partiallyResolvedTransactions by observableListReadOnly(GatheredTransactionDataModel::partiallyResolvedTransactions)
|
||||
|
||||
// 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() })
|
||||
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 {
|
||||
val signatureData = transaction.tx.transaction.sigs.map { it.by }
|
||||
// Bind count to TitlePane
|
||||
inputPane.text = "Input (${transaction.inputs.resolved.count()})"
|
||||
outputPane.text = "Output (${transaction.outputs.count()})"
|
||||
signaturesPane.text = "Signatures (${signatureData.count()})"
|
||||
|
||||
inputs.cellCache { getCell(it) }
|
||||
outputs.cellCache { getCell(it) }
|
||||
|
||||
inputs.items = transaction.inputs.resolved
|
||||
outputs.items = transaction.outputs.observable()
|
||||
|
||||
signatures.children.addAll(signatureData.map { signature ->
|
||||
val nodeInfo = getModel<NetworkIdentityModel>().lookup(signature)
|
||||
copyableLabel(nodeInfo.map { "${signature.toStringShort()} (${it?.legalIdentity?.name ?: "???"})" })
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
is Cash.State -> {
|
||||
row {
|
||||
label("Amount :") { gridpaneConstraints { hAlignment = HPos.RIGHT } }
|
||||
label(AmountFormatter.boring.format(data.amount.withoutIssuer()))
|
||||
}
|
||||
row {
|
||||
label("Issuer :") { gridpaneConstraints { hAlignment = HPos.RIGHT } }
|
||||
label("${data.amount.token.issuer}") {
|
||||
tooltip(data.amount.token.issuer.party.owningKey.toBase58String())
|
||||
}
|
||||
}
|
||||
row {
|
||||
label("Owner :") { gridpaneConstraints { hAlignment = HPos.RIGHT } }
|
||||
val owner = data.owner
|
||||
val nodeInfo = getModel<NetworkIdentityModel>().lookup(owner)
|
||||
label(nodeInfo.map { it?.legalIdentity?.name ?: "???" }) {
|
||||
tooltip(data.owner.toBase58String())
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO : Generic view using reflection?
|
||||
else -> label {}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
private fun StateAndRef<ContractState>.contract() = this.state.data.contract
|
||||
}
|
||||
|
||||
/**
|
||||
@ -240,14 +248,22 @@ class TransactionViewer : CordaView("Transactions") {
|
||||
*/
|
||||
private fun calculateTotalEquiv(identity: NodeInfo?,
|
||||
reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>,
|
||||
inputs: List<TransactionState<ContractState>>,
|
||||
outputs: List<TransactionState<ContractState>>): AmountDiff<Currency> {
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>): AmountDiff<Currency> {
|
||||
val (reportingCurrency, exchange) = reportingCurrencyExchange
|
||||
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()
|
||||
.filter { publicKey == it.owner }
|
||||
.map { exchange(it.amount.withoutIssuer()).quantity }
|
||||
.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 de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
|
||||
@ -8,11 +8,9 @@ import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.geometry.Insets
|
||||
import javafx.scene.Node
|
||||
import javafx.scene.Parent
|
||||
import javafx.scene.chart.NumberAxis
|
||||
import javafx.scene.control.*
|
||||
import javafx.scene.image.ImageView
|
||||
import javafx.scene.input.MouseButton
|
||||
import javafx.scene.layout.BorderPane
|
||||
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.identiconToolTip
|
||||
import net.corda.explorer.model.CordaView
|
||||
import net.corda.explorer.model.CordaWidget
|
||||
import net.corda.explorer.model.ReportingCurrencyModel
|
||||
import net.corda.explorer.model.SettingsModel
|
||||
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 tornadofx.*
|
||||
import java.time.Instant
|
||||
@ -44,7 +46,7 @@ class CashViewer : CordaView("Cash") {
|
||||
override val root: BorderPane by fxml()
|
||||
override val icon: FontAwesomeIcon = FontAwesomeIcon.MONEY
|
||||
// View's widget.
|
||||
override val widget: Node = CashWidget()
|
||||
override val widgets = listOf(CordaWidget("Treasury", CashWidget())).observable()
|
||||
// Left pane
|
||||
private val leftPane: VBox by fxid()
|
||||
private val splitPane: SplitPane by fxid()
|
||||
@ -60,7 +62,7 @@ class CashViewer : CordaView("Cash") {
|
||||
private val toggleButton by fxid<Button>()
|
||||
// Inject observables
|
||||
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 selectedNode = cashViewerTable.singleRowSelection().map {
|
||||
@ -120,7 +122,7 @@ class CashViewer : CordaView("Cash") {
|
||||
|
||||
stateIdValueLabel.apply {
|
||||
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)
|
||||
}
|
||||
equivLabel.textProperty().bind(equivAmount.map { it.token.currencyCode.toString() })
|
||||
@ -140,14 +142,14 @@ class CashViewer : CordaView("Cash") {
|
||||
* issuer strings.
|
||||
*/
|
||||
val searchField = SearchField(cashStates,
|
||||
{ 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) }
|
||||
"Currency" to { state, text -> state.state.data.amount.token.product.toString().contains(text, true) },
|
||||
"Issuer" to { state, text -> state.state.data.amount.token.issuer.party.toString().contains(text, true) }
|
||||
)
|
||||
root.top = hbox(5.0) {
|
||||
button("New Transaction", FontAwesomeIconView(FontAwesomeIcon.PLUS)) {
|
||||
setOnMouseClicked {
|
||||
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() {
|
||||
// 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 exchangeRate: ObservableValue<ExchangeRate> by observableValue(ExchangeRateModel::exchangeRate)
|
||||
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.BooleanBinding
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.scene.control.*
|
||||
import javafx.stage.Window
|
||||
import net.corda.client.fxutils.isNotNull
|
||||
import net.corda.client.fxutils.map
|
||||
import net.corda.client.fxutils.unique
|
||||
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.serialization.OpaqueBytes
|
||||
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.CashFlow
|
||||
import net.corda.flows.CashFlowResult
|
||||
import net.corda.node.services.messaging.startFlow
|
||||
import org.controlsfx.dialog.ExceptionDialog
|
||||
import tornadofx.View
|
||||
import tornadofx.Fragment
|
||||
import tornadofx.booleanBinding
|
||||
import tornadofx.observable
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
|
||||
class NewTransaction : View() {
|
||||
class NewTransaction : Fragment() {
|
||||
override val root by fxml<DialogPane>()
|
||||
|
||||
// Components
|
||||
private val transactionTypeCB by fxid<ChoiceBox<CashTransaction>>()
|
||||
private val partyATextField by fxid<TextField>()
|
||||
@ -44,23 +47,16 @@ class NewTransaction : View() {
|
||||
private val availableAmount by fxid<Label>()
|
||||
private val amountLabel by fxid<Label>()
|
||||
private val amountTextField by fxid<TextField>()
|
||||
|
||||
private val amount = SimpleObjectProperty<BigDecimal>()
|
||||
private val issueRef = SimpleObjectProperty<Byte>()
|
||||
|
||||
// Inject data
|
||||
private val parties by observableList(NetworkIdentityModel::parties)
|
||||
private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable)
|
||||
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
|
||||
private val notaries by observableList(NetworkIdentityModel::notaries)
|
||||
private val cash by observableList(ContractStateModel::cash)
|
||||
|
||||
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 {
|
||||
dialog(window).showAndWait().ifPresent {
|
||||
val dialog = Alert(Alert.AlertType.INFORMATION).apply {
|
||||
@ -139,11 +135,11 @@ class NewTransaction : View() {
|
||||
issuerChoiceBox.apply {
|
||||
items = cash.map { it.token.issuer.party }.unique().sorted()
|
||||
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 {
|
||||
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
|
||||
}
|
||||
// Issue Reference
|
||||
@ -158,21 +154,16 @@ class NewTransaction : View() {
|
||||
// TODO : Create a currency model to store these values
|
||||
currencyChoiceBox.items = FXCollections.observableList(setOf(USD, GBP, CHF).toList())
|
||||
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(
|
||||
arrayListOf(issuerChoiceBox, currencyChoiceBox)
|
||||
.map { it.valueProperty().isNotNull.and(it.visibleProperty()) }
|
||||
.reduce(BooleanBinding::and)
|
||||
issuer.isNotNull.and(currencyChoiceBox.valueProperty().isNotNull).and(transactionTypeCB.valueProperty().booleanBinding(transactionTypeCB.valueProperty()) { it != CashTransaction.Issue })
|
||||
)
|
||||
availableAmount.textProperty()
|
||||
.bind(Bindings.createStringBinding({
|
||||
val filteredCash = cash.filtered {
|
||||
it.token.issuer.party == issuerChoiceBox.value &&
|
||||
it.token.product == currencyChoiceBox.value
|
||||
}.map { it.withoutIssuer().quantity }
|
||||
val filteredCash = cash.filtered { it.token.issuer.party == issuer.value && it.token.product == currencyChoiceBox.value }
|
||||
.map { it.withoutIssuer().quantity }
|
||||
"${filteredCash.sum()} ${currencyChoiceBox.value?.currencyCode} Available"
|
||||
}, arrayOf(currencyChoiceBox.valueProperty(), issuerChoiceBox.valueProperty())))
|
||||
|
||||
// Amount
|
||||
amountLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||
amountTextField.textFormatter = bigDecimalFormatter().apply { amount.bind(this.valueProperty()) }
|
||||
@ -183,7 +174,7 @@ class NewTransaction : View() {
|
||||
myIdentity.isNotNull(),
|
||||
transactionTypeCB.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,
|
||||
currencyChoiceBox.valueProperty().isNotNull
|
||||
).reduce(BooleanBinding::and)
|
@ -2,10 +2,27 @@
|
||||
* {
|
||||
-color-0: rgba(0, 0, 0, 1); /* Background color */
|
||||
-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-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*/
|
||||
|
@ -1,32 +1,15 @@
|
||||
@import "corda-dark-color-scheme.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 {
|
||||
-fx-text-fill: -color-0;
|
||||
}
|
||||
|
||||
.titled-pane .content, .titled-pane .text {
|
||||
-fx-background-color: -color-2;
|
||||
}
|
||||
|
||||
.split-pane-divider {
|
||||
-fx-background-color: transparent;
|
||||
-fx-border-color: transparent;
|
||||
@ -55,6 +38,7 @@
|
||||
.sidebar-menu-item:hover, .sidebar-menu-item:selected {
|
||||
-fx-background-color: -color-3;
|
||||
-fx-border-color: -color-3;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.sidebar-menu-item-arrow {
|
||||
@ -178,12 +162,9 @@
|
||||
-fx-background-color: -color-0;
|
||||
}
|
||||
|
||||
.login .text-field {
|
||||
-fx-border-color: -color-1;
|
||||
}
|
||||
|
||||
.login .label {
|
||||
.login .label, .login .check-box .text {
|
||||
-fx-text-fill: -color-2;
|
||||
-fx-fill: -color-2;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
@ -193,6 +174,12 @@
|
||||
-fx-border-radius: 2px;
|
||||
}
|
||||
|
||||
.searchField .combo-box {
|
||||
-fx-padding: -1px;
|
||||
-fx-border-width: 0;
|
||||
-fx-background-insets: 0px;
|
||||
}
|
||||
|
||||
.searchField .glyph-icon {
|
||||
-fx-fill: -color-1;
|
||||
-fx-padding: 0;
|
||||
@ -201,3 +188,55 @@
|
||||
.searchField .search-clear:hover {
|
||||
-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 javafx.scene.control.*?>
|
||||
<?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">
|
||||
<ListView fx:id="inputs"/>
|
||||
</TitledPane>
|
||||
@ -15,7 +15,7 @@
|
||||
</TitledPane>
|
||||
|
||||
<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>
|
||||
|
||||
<columnConstraints>
|
||||
@ -25,6 +25,6 @@
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints vgrow="ALWAYS"/>
|
||||
<RowConstraints/>
|
||||
<RowConstraints vgrow="NEVER"/>
|
||||
</rowConstraints>
|
||||
</GridPane>
|
||||
|
@ -22,14 +22,19 @@
|
||||
<center>
|
||||
<GridPane hgap="10" prefWidth="400" vgap="10">
|
||||
<Label text="Corda Node :" GridPane.halignment="RIGHT"/>
|
||||
<TextField fx:id="host" promptText="Host" GridPane.columnIndex="1"/>
|
||||
<TextField fx:id="port" prefWidth="100" promptText="Port" GridPane.columnIndex="2"/>
|
||||
<TextField fx:id="hostTextField" promptText="Host" GridPane.columnIndex="1"/>
|
||||
<TextField fx:id="portTextField" prefWidth="100" promptText="Port" GridPane.columnIndex="2"/>
|
||||
|
||||
<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"/>
|
||||
<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>
|
||||
</center>
|
||||
</BorderPane>
|
||||
|
@ -14,8 +14,7 @@
|
||||
<!-- User account menu -->
|
||||
<MenuButton fx:id="userButton" mnemonicParsing="false" GridPane.columnIndex="3">
|
||||
<items>
|
||||
<MenuItem mnemonicParsing="false" text="Sign out"/>
|
||||
<MenuItem mnemonicParsing="false" text="Account settings..."/>
|
||||
<MenuItem fx:id="exit" mnemonicParsing="false" text="Exit Corda Explorer"/>
|
||||
</items>
|
||||
<graphic>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
|
||||
|
@ -2,25 +2,24 @@
|
||||
|
||||
<?import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.ComboBox?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?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">
|
||||
<padding>
|
||||
<Insets bottom="5"/>
|
||||
</padding>
|
||||
<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>
|
||||
<TextField fx:id="textField" promptText="Filter transactions by originator, contract type..."/>
|
||||
|
||||
<FontAwesomeIconView fx:id="clearButton" glyphName="TIMES_CIRCLE" styleClass="search-clear" StackPane.alignment="CENTER_RIGHT">
|
||||
<StackPane.margin>
|
||||
<Insets right="10.0"/>
|
||||
</StackPane.margin>
|
||||
</FontAwesomeIconView>
|
||||
|
||||
<ComboBox fx:id="searchCategory" StackPane.alignment="CENTER_LEFT">
|
||||
<StackPane.margin>
|
||||
<Insets left="1.0"/>
|
||||
</StackPane.margin>
|
||||
</ComboBox>
|
||||
</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.scene.control.*?>
|
||||
<?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>
|
||||
<Insets right="5" left="5" bottom="5" top="5"/>
|
||||
</padding>
|
||||
<top>
|
||||
<fx:include source="../SearchField.fxml"/>
|
||||
<fx:include source="../../SearchField.fxml"/>
|
||||
</top>
|
||||
<center>
|
||||
<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…
Reference in New Issue
Block a user