Explorer corda branding

This commit is contained in:
Patrick Kuo 2016-11-11 09:31:52 +00:00
parent 95c404bb67
commit fbf952a1ab
37 changed files with 1046 additions and 422 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
myLegalName = "Vast Global MegaCorp, Ltd"
nearestCity = "The Moon"
nearestCity = "London"
emailAddress = "admin@company.com"
exportJMXto = "http"
keyStorePassword = "cordacadevpass"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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