Explorer corda branding

This commit is contained in:
Patrick Kuo 2016-11-11 09:31:52 +00:00
parent 37ca651ace
commit af899a98f4
44 changed files with 991 additions and 657 deletions

View File

@ -6,7 +6,7 @@
<option name="LIVE_STYLESHEETS" value="false" /> <option name="LIVE_STYLESHEETS" value="false" />
<option name="DUMP_STYLESHEETS" value="false" /> <option name="DUMP_STYLESHEETS" value="false" />
<option name="LIVE_VIEWS" value="false" /> <option name="LIVE_VIEWS" value="false" />
<option name="MAIN_CLASS_NAME" value="com.r3corda.explorer.Main" /> <option name="MAIN_CLASS_NAME" value="net.corda.explorer.Main" />
<option name="VM_PARAMETERS" value="" /> <option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="" /> <option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="file://$PROJECT_DIR$" /> <option name="WORKING_DIRECTORY" value="file://$PROJECT_DIR$" />

View File

@ -275,3 +275,7 @@ fun <A> ObservableList<A>.last(): ObservableValue<A?> {
} }
}, arrayOf(this)) }, arrayOf(this))
} }
fun <T : Any> ObservableList<T>.unique(): ObservableList<T> {
return associateByAggregation { it }.getObservableValues().map { Bindings.valueAt(it, 0) }.flatten()
}

View File

@ -1,14 +1,14 @@
package net.corda.client.model package net.corda.client.model
import javafx.collections.ObservableList
import kotlinx.support.jdk8.collections.removeIf
import net.corda.client.fxutils.foldToObservableList import net.corda.client.fxutils.foldToObservableList
import net.corda.client.fxutils.recordInSequence import net.corda.client.fxutils.map
import net.corda.contracts.asset.Cash import net.corda.contracts.asset.Cash
import net.corda.core.contracts.ContractState import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
import javafx.collections.ObservableList
import kotlinx.support.jdk8.collections.removeIf
import rx.Observable import rx.Observable
data class Diff<out T : ContractState>( data class Diff<out T : ContractState>(
@ -22,10 +22,10 @@ data class Diff<out T : ContractState>(
class ContractStateModel { class ContractStateModel {
private val vaultUpdates: Observable<Vault.Update> by observable(NodeMonitorModel::vaultUpdates) private val vaultUpdates: Observable<Vault.Update> by observable(NodeMonitorModel::vaultUpdates)
val contractStatesDiff: Observable<Diff<ContractState>> = vaultUpdates.map { private val contractStatesDiff: Observable<Diff<ContractState>> = vaultUpdates.map {
Diff(it.produced, it.consumed) Diff(it.produced, it.consumed)
} }
val cashStatesDiff: Observable<Diff<Cash.State>> = contractStatesDiff.map { private val cashStatesDiff: Observable<Diff<Cash.State>> = contractStatesDiff.map {
// We can't filter removed hashes here as we don't have type info // We can't filter removed hashes here as we don't have type info
Diff(it.added.filterCashStateAndRefs(), it.removed) Diff(it.added.filterCashStateAndRefs(), it.removed)
} }
@ -35,6 +35,7 @@ class ContractStateModel {
observableList.addAll(statesDiff.added) observableList.addAll(statesDiff.added)
} }
val cash = cashStates.map { it.state.data.amount }
companion object { companion object {
private fun Collection<StateAndRef<ContractState>>.filterCashStateAndRefs(): List<StateAndRef<Cash.State>> { private fun Collection<StateAndRef<ContractState>>.filterCashStateAndRefs(): List<StateAndRef<Cash.State>> {

View File

@ -72,6 +72,9 @@ dependencies {
// Controls FX: more java FX components http://fxexperience.com/controlsfx/ // Controls FX: more java FX components http://fxexperience.com/controlsfx/
compile 'org.controlsfx:controlsfx:8.40.12' compile 'org.controlsfx:controlsfx:8.40.12'
compile 'commons-lang:commons-lang:2.6'
// This provide com.apple.eawt stub for non-mac system.
compile 'com.yuvimasory:orange-extensions:1.3.0'
} }
task(runDemoNodes, dependsOn: 'classes', type: JavaExec) { task(runDemoNodes, dependsOn: 'classes', type: JavaExec) {

View File

@ -1,41 +1,98 @@
package net.corda.explorer package net.corda.explorer
import com.apple.eawt.Application
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory
import javafx.embed.swing.SwingFXUtils
import javafx.scene.control.Alert
import javafx.scene.control.ButtonType
import javafx.scene.image.Image
import javafx.stage.Stage import javafx.stage.Stage
import jfxtras.resources.JFXtrasFontRoboto
import net.corda.client.mock.EventGenerator import net.corda.client.mock.EventGenerator
import net.corda.client.model.Models import net.corda.client.model.Models
import net.corda.client.model.NodeMonitorModel import net.corda.client.model.NodeMonitorModel
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.explorer.views.runInFxApplicationThread import net.corda.explorer.model.CordaViewModel
import net.corda.explorer.views.*
import net.corda.explorer.views.cordapps.CashViewer
import net.corda.node.driver.PortAllocation import net.corda.node.driver.PortAllocation
import net.corda.node.driver.driver import net.corda.node.driver.driver
import net.corda.node.services.User import net.corda.node.services.User
import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.config.FullNodeConfiguration
import net.corda.node.services.config.configureTestSSL
import net.corda.node.services.messaging.ArtemisMessagingComponent import net.corda.node.services.messaging.ArtemisMessagingComponent
import net.corda.node.services.messaging.startProtocol import net.corda.node.services.messaging.startProtocol
import net.corda.node.services.startProtocolPermission import net.corda.node.services.startProtocolPermission
import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.protocols.CashProtocol import net.corda.protocols.CashProtocol
import org.apache.commons.lang.SystemUtils
import org.controlsfx.dialog.ExceptionDialog import org.controlsfx.dialog.ExceptionDialog
import tornadofx.App import tornadofx.App
import tornadofx.addStageIcon
import tornadofx.find
import java.util.* import java.util.*
/** /**
* Main class for Explorer, you will need Tornado FX to run the explorer. * Main class for Explorer, you will need Tornado FX to run the explorer.
*/ */
class Main : App() { class Main : App() {
override val primaryView = MainWindow::class override val primaryView = MainView::class
private val loginView by inject<LoginView>()
override fun start(stage: Stage) { 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.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()
}
}
init {
// Shows any uncaught exception in exception dialog.
Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
throwable.printStackTrace() throwable.printStackTrace()
// Show exceptions in exception dialog. // Show exceptions in exception dialog. Ensure this runs in application thread.
runInFxApplicationThread { runInFxApplicationThread {
// [showAndWait] need to be in the FX thread // [showAndWait] need to be in the FX thread.
ExceptionDialog(throwable).showAndWait() ExceptionDialog(throwable).showAndWait()
System.exit(1) System.exit(1)
} }
} }
super.start(stage) // Do this first before creating the notification bar, so it can autosize itself properly.
loadFontsAndStyles()
// Add Corda logo to OSX dock and windows icon.
val cordaLogo = Image(javaClass.getResourceAsStream("images/Logo-03.png"))
if (SystemUtils.IS_OS_MAC_OSX) {
Application.getApplication().dockIconImage = SwingFXUtils.fromFXImage(cordaLogo, null)
}
addStageIcon(cordaLogo)
// Register views.
Models.get<CordaViewModel>(Main::class).apply {
// TODO : This could block the UI thread when number of views increase, maybe we can make this async and display a loading screen.
// Stock Views.
registerView<Dashboard>()
registerView<TransactionViewer>()
// CordApps Views.
registerView<CashViewer>()
// Tools.
registerView<Network>()
registerView<Settings>()
// Default view to Dashboard.
selectedView.set(find<Dashboard>())
}
}
private fun loadFontsAndStyles() {
JFXtrasFontRoboto.loadAll()
FontAwesomeIconFactory.get() // Force initialisation.
} }
} }
@ -57,12 +114,11 @@ fun main(args: Array<String>) {
arrayOf(notaryNode, aliceNode, bobNode).forEach { arrayOf(notaryNode, aliceNode, bobNode).forEach {
println("${it.nodeInfo.legalIdentity} started on ${ArtemisMessagingComponent.toHostAndPort(it.nodeInfo.address)}") println("${it.nodeInfo.legalIdentity} started on ${ArtemisMessagingComponent.toHostAndPort(it.nodeInfo.address)}")
} }
// Register with alice to use alice's RPC proxy to create random events. // Register with alice to use alice's RPC proxy to create random events.
Models.get<NodeMonitorModel>(Main::class).register(ArtemisMessagingComponent.toHostAndPort(aliceNode.nodeInfo.address), FullNodeConfiguration(aliceNode.config), user.username, user.password) 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 rpcProxy = Models.get<NodeMonitorModel>(Main::class).proxyObservable.get()
for (i in 0..10000) { for (i in 0..10) {
Thread.sleep(500) Thread.sleep(500)
val eventGenerator = EventGenerator( val eventGenerator = EventGenerator(
parties = listOf(aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity), parties = listOf(aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity),

View File

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

View File

@ -0,0 +1,33 @@
package net.corda.explorer.model
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import javafx.beans.property.SimpleObjectProperty
import javafx.scene.Node
import tornadofx.View
import tornadofx.find
import tornadofx.observable
class CordaViewModel {
val selectedView = SimpleObjectProperty<CordaView>()
val registeredViews = mutableListOf<CordaView>().observable()
inline fun <reified T> registerView() where T : CordaView {
// Note: this is weirdly very important, as it forces the initialisation of Views. Therefore this is the entry
// point to the top level observable/stream wiring! Any events sent before this init may be lost!
registeredViews.add(find<T>().apply { root })
}
}
/**
* Contain methods to construct various UI component used by the explorer UI framework.
* TODO : Implement views with this interface and register in [CordaViewModel] when UI start up. We can use the [CordaViewModel] to dynamically create sidebar and dashboard without manual wiring.
* TODO : "goto" functionality?
*/
abstract class CordaView(title: String? = null) : View(title) {
abstract val widget: Node?
abstract val icon: FontAwesomeIcon
init {
if (title == null) super.title = javaClass.simpleName
}
}

View File

@ -1,24 +0,0 @@
package net.corda.explorer.model
import javafx.beans.property.SimpleObjectProperty
import javafx.scene.image.Image
enum class SelectedView(val displayableName: String, val image: Image, val subviews: Array<SelectedView> = emptyArray()) {
Home("Home", getImage("home.png")),
Transaction("Transaction", getImage("tx.png")),
Setting("Setting", getImage("settings_lrg.png")),
NewTransaction("New Transaction", getImage("cash.png")),
Cash("Cash", getImage("cash.png"), arrayOf(Transaction, NewTransaction)),
NetworkMap("Network Map", getImage("cash.png")),
Vault("Vault", getImage("cash.png"), arrayOf(Cash)),
Network("Network", getImage("inst.png"), arrayOf(NetworkMap, Transaction))
}
private fun getImage(imageName: String): Image {
val basePath = "/net/corda/explorer/images"
return Image("$basePath/$imageName")
}
class TopLevelModel {
val selectedView = SimpleObjectProperty<SelectedView>(SelectedView.Home)
}

View File

@ -1,13 +0,0 @@
package net.corda.explorer.views
import javafx.scene.Node
/**
* Corda view interface, provides methods to construct various UI component used by the explorer UI framework.
* TODO : Implement this interface on all views and register the views with ViewModel when UI start up, then we can use the ViewModel to dynamically create sidebar and dashboard without manual wiring.
* TODO : Sidebar icons.
*/
interface CordaView {
val widget: Node?
val viewName: String
}

View File

@ -0,0 +1,51 @@
package net.corda.explorer.views
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import javafx.beans.binding.Bindings
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.map
import net.corda.client.model.observableList
import net.corda.client.model.writableValue
import net.corda.explorer.model.CordaView
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)
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
val columns: Int = ((tilePane.width - 10) / prefWidth).toInt()
tilePane.children.forEach { (it as? TitledPane)?.prefWidth = (tilePane.width - 10) / columns }
}
}
}

View File

@ -0,0 +1,33 @@
package net.corda.explorer.views
import javafx.scene.control.TextFormatter
import javafx.util.converter.BigDecimalStringConverter
import javafx.util.converter.ByteStringConverter
import javafx.util.converter.IntegerStringConverter
import java.math.BigDecimal
import java.util.regex.Pattern
// BigDecimal text Formatter, restricting text box input to decimal values.
fun bigDecimalFormatter(): TextFormatter<BigDecimal> = Pattern.compile("-?((\\d*)|(\\d+\\.\\d*))").run {
TextFormatter<BigDecimal>(BigDecimalStringConverter(), null) { change ->
val newText = change.controlNewText
if (matcher(newText).matches()) change else null
}
}
// Byte text Formatter, restricting text box input to decimal values.
fun byteFormatter(): TextFormatter<Byte> = Pattern.compile("\\d*").run {
TextFormatter<Byte>(ByteStringConverter(), null) { change ->
val newText = change.controlNewText
if (matcher(newText).matches()) change else null
}
}
// Short text Formatter, restricting text box input to decimal values.
fun intFormatter(): TextFormatter<Int> = Pattern.compile("\\d*").run {
TextFormatter<Int>(IntegerStringConverter(), null) { change ->
val newText = change.controlNewText
if (matcher(newText).matches()) change else null
}
}

View File

@ -1,7 +1,15 @@
package net.corda.explorer.views package net.corda.explorer.views
import javafx.application.Platform import javafx.application.Platform
import javafx.event.EventTarget
import javafx.geometry.Pos
import javafx.scene.Parent
import javafx.scene.layout.GridPane
import javafx.scene.layout.Priority
import javafx.scene.text.TextAlignment
import javafx.util.StringConverter import javafx.util.StringConverter
import tornadofx.gridpane
import tornadofx.label
/** /**
* Helper method to reduce boiler plate code * Helper method to reduce boiler plate code
@ -24,8 +32,9 @@ fun <T> stringConverter(fromStringFunction: ((String?) -> T)? = null, toStringFu
*/ */
fun Number.toStringWithSuffix(precision: Int = 1): String { fun Number.toStringWithSuffix(precision: Int = 1): String {
if (this.toDouble() < 1000) return "$this" if (this.toDouble() < 1000) return "$this"
val exp = (Math.log(this.toDouble()) / Math.log(1000.0)).toInt() val scales = "kMBT"
return "${(this.toDouble() / Math.pow(1000.0, exp.toDouble())).format(precision)} ${"kMGTPE"[exp - 1]}" val exp = Math.min(scales.length, (Math.log(this.toDouble()) / Math.log(1000.0)).toInt())
return "${(this.toDouble() / Math.pow(1000.0, exp.toDouble())).format(precision)}${scales[exp - 1]}"
} }
fun Double.format(precision: Int) = String.format("%.${precision}f", this) fun Double.format(precision: Int) = String.format("%.${precision}f", this)
@ -40,3 +49,15 @@ fun runInFxApplicationThread(block: () -> Unit) {
Platform.runLater(block) Platform.runLater(block)
} }
} }
fun EventTarget.underConstruction(): Parent {
return gridpane {
label("Under Construction...") {
maxWidth = Double.MAX_VALUE
textAlignment = TextAlignment.CENTER
alignment = Pos.CENTER
GridPane.setVgrow(this, Priority.ALWAYS)
GridPane.setHgrow(this, Priority.ALWAYS)
}
}
}

View File

@ -1,31 +0,0 @@
package net.corda.explorer.views
import net.corda.client.fxutils.map
import net.corda.client.model.NetworkIdentityModel
import net.corda.client.model.observableValue
import net.corda.explorer.model.TopLevelModel
import javafx.scene.control.Label
import javafx.scene.control.SplitMenuButton
import javafx.scene.image.ImageView
import javafx.scene.layout.GridPane
import tornadofx.View
class Header : View() {
override val root: GridPane by fxml()
private val sectionLabel: Label by fxid()
private val userButton: SplitMenuButton by fxid()
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
private val selectedView by observableValue(TopLevelModel::selectedView)
init {
sectionLabel.textProperty().bind(selectedView.map { it.displayableName })
sectionLabel.graphicProperty().bind(selectedView.map {
ImageView(it.image).apply {
fitHeight = 30.0
fitWidth = 30.0
}
})
userButton.textProperty().bind(myIdentity.map { it?.legalIdentity?.name })
}
}

View File

@ -1,55 +0,0 @@
package net.corda.explorer.views
import net.corda.client.fxutils.map
import net.corda.client.model.GatheredTransactionData
import net.corda.client.model.GatheredTransactionDataModel
import net.corda.client.model.observableListReadOnly
import net.corda.client.model.writableValue
import net.corda.explorer.model.SelectedView
import net.corda.explorer.model.TopLevelModel
import javafx.beans.binding.Bindings
import javafx.beans.value.WritableValue
import javafx.collections.ObservableList
import javafx.scene.Node
import javafx.scene.Parent
import javafx.scene.control.Label
import javafx.scene.control.TitledPane
import javafx.scene.input.MouseButton
import javafx.scene.input.MouseEvent
import javafx.scene.layout.TilePane
import tornadofx.View
import tornadofx.find
class Home : View() {
override val root: Parent by fxml()
private val tilePane: TilePane by fxid()
private val ourCashPane: TitledPane by fxid()
private val ourTransactionsLabel: Label by fxid()
private val selectedView: WritableValue<SelectedView> by writableValue(TopLevelModel::selectedView)
private val gatheredTransactionDataList: ObservableList<out GatheredTransactionData>
by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
init {
// TODO: register views in view model and populate the dashboard dynamically.
ourTransactionsLabel.textProperty().bind(
Bindings.size(gatheredTransactionDataList).map { it.toString() }
)
ourCashPane.apply {
content = find(CashViewer::class).widget
}
tilePane.widthProperty().addListener { e ->
val prefWidth = 350
val columns: Int = ((tilePane.width - 10) / prefWidth).toInt()
tilePane.children.forEach { (it as? TitledPane)?.prefWidth = (tilePane.width - 10) / columns }
}
}
fun changeView(event: MouseEvent) {
if (event.button == MouseButton.PRIMARY) {
selectedView.value = SelectedView.valueOf((event.source as Node).id)
}
}
}

View File

@ -3,14 +3,12 @@ package net.corda.explorer.views
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import javafx.beans.property.SimpleIntegerProperty import javafx.beans.property.SimpleIntegerProperty
import javafx.scene.control.* import javafx.scene.control.*
import javafx.util.converter.IntegerStringConverter
import org.controlsfx.dialog.ExceptionDialog import org.controlsfx.dialog.ExceptionDialog
import tornadofx.View import tornadofx.View
import java.util.regex.Pattern
import kotlin.system.exitProcess import kotlin.system.exitProcess
class LoginView : View() { class LoginView : View() {
override val root: DialogPane by fxml() override val root by fxml<DialogPane>()
private val host by fxid<TextField>() private val host by fxid<TextField>()
private val port by fxid<TextField>() private val port by fxid<TextField>()
@ -19,46 +17,42 @@ class LoginView : View() {
private val portProperty = SimpleIntegerProperty() private val portProperty = SimpleIntegerProperty()
fun login(loginFunction: (HostAndPort, String, String) -> Unit) { fun login(loginFunction: (HostAndPort, String, String) -> Unit) {
val loggedIn = Dialog<Boolean>().apply { val status = Dialog<LoginStatus>().apply {
dialogPane = root dialogPane = root
var exception = false
setResultConverter { setResultConverter {
exception = false
when (it?.buttonData) { when (it?.buttonData) {
ButtonBar.ButtonData.OK_DONE -> try { ButtonBar.ButtonData.OK_DONE -> try {
// TODO : Run this async to avoid UI lockup. // TODO : Run this async to avoid UI lockup.
loginFunction(HostAndPort.fromParts(host.text, portProperty.value), username.text, password.text) loginFunction(HostAndPort.fromParts(host.text, portProperty.value), username.text, password.text)
true LoginStatus.loggedIn
} catch (e: Exception) { } catch (e: Exception) {
ExceptionDialog(e).showAndWait() // TODO : Handle this in a more user friendly way.
exception = true ExceptionDialog(e).apply { initOwner(root.scene.window) }.showAndWait()
false LoginStatus.exception
} }
else -> false else -> LoginStatus.exited
} }
} }
setOnCloseRequest { setOnCloseRequest {
if (!result && !exception) { if (result == LoginStatus.exited) {
when (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 {
}.showAndWait().get()) { initOwner(root.scene.window)
ButtonType.OK -> exitProcess(0) }.showAndWait().get()
if (button == ButtonType.OK) {
exitProcess(0)
} }
} }
} }
}.showAndWait().get() }.showAndWait().get()
if (status != LoginStatus.loggedIn) login(loginFunction)
if (!loggedIn) login(loginFunction)
} }
init { init {
// Restrict text field to Integer only. // Restrict text field to Integer only.
val integerFormat = Pattern.compile("-?(\\d*)").run { port.textFormatter = intFormatter().apply { portProperty.bind(this.valueProperty()) }
TextFormatter<Int>(IntegerStringConverter(), null) { change -> }
val newText = change.controlNewText
if (matcher(newText).matches()) change else null private enum class LoginStatus {
} loggedIn, exited, exception
}
port.textFormatter = integerFormat
portProperty.bind(integerFormat.valueProperty())
} }
} }

View File

@ -0,0 +1,89 @@
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.geometry.Insets
import javafx.geometry.Pos
import javafx.scene.Parent
import javafx.scene.control.ContentDisplay
import javafx.scene.control.MenuButton
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 net.corda.client.fxutils.ChosenList
import net.corda.client.fxutils.map
import net.corda.client.model.NetworkIdentityModel
import net.corda.client.model.objectProperty
import net.corda.client.model.observableList
import net.corda.client.model.observableValue
import net.corda.explorer.model.CordaViewModel
import tornadofx.*
/**
* The root view embeds the [Shell] and provides support for the status bar, and modal dialogs.
*/
class MainView : View() {
override val root by fxml<Parent>()
// Inject components.
private val userButton by fxid<MenuButton>()
private val sidebar by fxid<VBox>()
private val selectionBorderPane by fxid<BorderPane>()
// Inject data.
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
private val selectedView by objectProperty(CordaViewModel::selectedView)
private val registeredViews by observableList(CordaViewModel::registeredViews)
private val menuItemCSS = "sidebar-menu-item"
private val menuItemArrowCSS = "sidebar-menu-item-arrow"
private val menuItemSelectedCSS = "$menuItemCSS-selected"
init {
// Header
userButton.textProperty().bind(myIdentity.map { it?.legalIdentity?.name })
// 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()
})
stackpane {
button(it.title) {
graphic = FontAwesomeIconView(it.icon).apply {
glyphSize = 30
textAlignment = TextAlignment.CENTER
fillProperty().bind(this@button.textFillProperty())
}
Bindings.bindContent(styleClass, buttonStyle)
setOnMouseClicked { e ->
if (e.button == MouseButton.PRIMARY) {
selectedView.value = it
}
}
// Transform to smaller icon layout when sidebar width is below 150.
val smallIconProperty = widthProperty().map { (it.toDouble() < 150) }
contentDisplayProperty().bind(smallIconProperty.map { if (it) ContentDisplay.TOP else ContentDisplay.LEFT })
textAlignmentProperty().bind(smallIconProperty.map { if (it) TextAlignment.CENTER else TextAlignment.LEFT })
alignmentProperty().bind(smallIconProperty.map { if (it) Pos.CENTER else Pos.CENTER_LEFT })
fontProperty().bind(smallIconProperty.map { if (it) Font.font(10.0) else Font.font(12.0) })
wrapTextProperty().bind(smallIconProperty)
}
// Small triangle indicator to make selected view more obvious.
add(FontAwesomeIconView(FontAwesomeIcon.CARET_LEFT).apply {
StackPane.setAlignment(this, Pos.CENTER_RIGHT)
StackPane.setMargin(this, Insets(0.0, -5.0, 0.0, 0.0))
styleClass.add(menuItemArrowCSS)
visibleProperty().bind(selectedView.map { selected -> selected == it })
})
}
}
Bindings.bindContent(sidebar.children, menuItems)
// Main view
selectionBorderPane.centerProperty().bind(selectedView.map { it?.root })
}
}

View File

@ -0,0 +1,13 @@
package net.corda.explorer.views
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import javafx.scene.Node
import net.corda.explorer.model.CordaView
// TODO : Construct a node map using node info and display hem 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 icon = FontAwesomeIcon.GLOBE
}

View File

@ -2,65 +2,114 @@ package net.corda.explorer.views
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
import javafx.beans.binding.BooleanBinding import javafx.beans.binding.BooleanBinding
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.value.ObservableValue import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.scene.Node
import javafx.scene.Parent
import javafx.scene.control.* import javafx.scene.control.*
import javafx.util.converter.BigDecimalStringConverter import javafx.stage.Window
import net.corda.client.fxutils.map import net.corda.client.fxutils.map
import net.corda.client.model.NetworkIdentityModel import net.corda.client.fxutils.unique
import net.corda.client.model.NodeMonitorModel import net.corda.client.model.*
import net.corda.client.model.observableList
import net.corda.client.model.observableValue
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.Party
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.explorer.model.CashTransaction import net.corda.explorer.model.CashTransaction
import net.corda.node.services.messaging.CordaRPCOps
import net.corda.node.services.messaging.startProtocol import net.corda.node.services.messaging.startProtocol
import net.corda.protocols.CashCommand import net.corda.protocols.CashCommand
import net.corda.protocols.CashProtocol import net.corda.protocols.CashProtocol
import net.corda.protocols.CashProtocolResult import net.corda.protocols.CashProtocolResult
import org.controlsfx.dialog.ExceptionDialog import org.controlsfx.dialog.ExceptionDialog
import tornadofx.View import tornadofx.View
import tornadofx.observable
import java.math.BigDecimal import java.math.BigDecimal
import java.util.* import java.util.*
import java.util.regex.Pattern
class NewTransaction : View() { class NewTransaction : View() {
override val root: Parent by fxml() override val root by fxml<DialogPane>()
private val partyATextField: TextField by fxid() // Components
private val partyBChoiceBox: ChoiceBox<NodeInfo> by fxid() private val transactionTypeCB by fxid<ChoiceBox<CashTransaction>>()
private val partyALabel: Label by fxid() private val partyATextField by fxid<TextField>()
private val partyBLabel: Label by fxid() private val partyALabel by fxid<Label>()
private val amountLabel: Label by fxid() private val partyBChoiceBox by fxid<ChoiceBox<NodeInfo>>()
private val partyBLabel by fxid<Label>()
private val issuerLabel by fxid<Label>()
private val issuerTextField by fxid<TextField>()
private val issuerChoiceBox by fxid<ChoiceBox<Party>>()
private val issueRefLabel by fxid<Label>()
private val issueRefTextField by fxid<TextField>()
private val currencyLabel by fxid<Label>()
private val currencyChoiceBox by fxid<ChoiceBox<Currency>>()
private val availableAmount by fxid<Label>()
private val amountLabel by fxid<Label>()
private val amountTextField by fxid<TextField>()
private val executeButton: Button by fxid() private val amount = SimpleObjectProperty<BigDecimal>()
private val issueRef = SimpleObjectProperty<Byte>()
private val transactionTypeCB: ChoiceBox<CashTransaction> by fxid()
private val amount: TextField by fxid()
private val currency: ChoiceBox<Currency> by fxid()
private val issueRefLabel: Label by fxid()
private val issueRefTextField: TextField by fxid()
// Inject data // Inject data
private val parties: ObservableList<NodeInfo> by observableList(NetworkIdentityModel::parties) private val parties by observableList(NetworkIdentityModel::parties)
private val rpcProxy: ObservableValue<CordaRPCOps?> by observableValue(NodeMonitorModel::proxyObservable) private val rpcProxy by observableValue(NodeMonitorModel::proxyObservable)
private val myIdentity: ObservableValue<NodeInfo?> by observableValue(NetworkIdentityModel::myIdentity) private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
private val notaries: ObservableList<NodeInfo> by observableList(NetworkIdentityModel::notaries) 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 { private fun ObservableValue<*>.isNotNull(): BooleanBinding {
return Bindings.createBooleanBinding({ this.value != null }, arrayOf(this)) return Bindings.createBooleanBinding({ this.value != null }, arrayOf(this))
} }
private fun resetScreen() { fun show(window: Window): Unit {
partyBChoiceBox.valueProperty().set(null) dialog(window).showAndWait().ifPresent {
transactionTypeCB.valueProperty().set(null) val dialog = Alert(Alert.AlertType.INFORMATION).apply {
currency.valueProperty().set(null) headerText = null
amount.clear() contentText = "Transaction Started."
dialogPane.isDisable = true
initOwner(window)
}
dialog.show()
runAsync {
rpcProxy.value!!.startProtocol(::CashProtocol, it).returnValue.toBlocking().first()
}.ui {
dialog.contentText = when (it) {
is CashProtocolResult.Success -> {
dialog.alertType = Alert.AlertType.INFORMATION
"Transaction Started \nTransaction ID : ${it.transaction?.id} \nMessage : ${it.message}"
}
is CashProtocolResult.Failed -> {
dialog.alertType = Alert.AlertType.ERROR
it.toString()
}
}
dialog.dialogPane.isDisable = false
dialog.dialogPane.scene.window.sizeToScene()
}.setOnFailed {
dialog.close()
ExceptionDialog(it.source.exception).apply { initOwner(window) }.showAndWait()
}
}
}
private fun dialog(window: Window) = Dialog<CashCommand>().apply {
dialogPane = root
initOwner(window)
setResultConverter {
val defaultRef = OpaqueBytes(ByteArray(1, { 1 }))
when (it) {
executeButton -> when (transactionTypeCB.value) {
CashTransaction.Issue -> {
val issueRef = if (issueRef.value != null) OpaqueBytes(ByteArray(1, { issueRef.value })) else defaultRef
CashCommand.IssueCash(Amount(amount.value, currencyChoiceBox.value), issueRef, partyBChoiceBox.value.legalIdentity, notaries.first().notaryIdentity)
}
CashTransaction.Pay -> CashCommand.PayCash(Amount(amount.value, Issued(PartyAndReference(issuerChoiceBox.value, defaultRef), currencyChoiceBox.value)), partyBChoiceBox.value.legalIdentity)
CashTransaction.Exit -> CashCommand.ExitCash(Amount(amount.value, currencyChoiceBox.value), defaultRef)
else -> null
}
else -> null
}
}
} }
init { init {
@ -68,7 +117,9 @@ class NewTransaction : View() {
val notariesNotNullBinding = Bindings.createBooleanBinding({ notaries.isNotEmpty() }, arrayOf(notaries)) val notariesNotNullBinding = Bindings.createBooleanBinding({ notaries.isNotEmpty() }, arrayOf(notaries))
val enableProperty = myIdentity.isNotNull().and(rpcProxy.isNotNull()).and(notariesNotNullBinding) val enableProperty = myIdentity.isNotNull().and(rpcProxy.isNotNull()).and(notariesNotNullBinding)
root.disableProperty().bind(enableProperty.not()) root.disableProperty().bind(enableProperty.not())
transactionTypeCB.items = FXCollections.observableArrayList(CashTransaction.values().asList())
// Transaction Types Choice Box
transactionTypeCB.items = CashTransaction.values().asList().observable()
// Party A textfield always display my identity name, not editable. // Party A textfield always display my identity name, not editable.
partyATextField.isEditable = false partyATextField.isEditable = false
@ -76,96 +127,69 @@ class NewTransaction : View() {
partyALabel.textProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameA?.let { "$it : " } }) partyALabel.textProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameA?.let { "$it : " } })
partyATextField.visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameA }.isNotNull()) partyATextField.visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameA }.isNotNull())
// Party B
partyBLabel.textProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB?.let { "$it : " } }) partyBLabel.textProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB?.let { "$it : " } })
partyBChoiceBox.apply { partyBChoiceBox.apply {
visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB }.isNotNull()) visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB }.isNotNull())
partyBChoiceBox.items = parties items = parties.sorted()
converter = stringConverter { it?.legalIdentity?.name ?: "" } converter = stringConverter { it?.legalIdentity?.name ?: "" }
} }
// Issuer
// BigDecimal text Formatter, restricting text box input to decimal values. issuerLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
val textFormatter = Pattern.compile("-?((\\d*)|(\\d+\\.\\d*))").run { issuerChoiceBox.apply {
TextFormatter<BigDecimal>(BigDecimalStringConverter(), null) { change -> items = cash.map { it.token.issuer.party }.unique().sorted()
val newText = change.controlNewText converter = stringConverter { it.name }
if (matcher(newText).matches()) change else null visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Pay || it == CashTransaction.Exit })
}
} }
amount.textFormatter = textFormatter issuerTextField.apply {
textProperty().bind(myIdentity.map { it?.legalIdentity?.name })
visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Issue })
isEditable = false
}
// Issue Reference
issueRefLabel.visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Issue })
// Hide currency and amount fields when transaction type is not specified. issueRefTextField.apply {
textFormatter = byteFormatter().apply { issueRef.bind(this.valueProperty()) }
visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Issue })
}
// Currency
currencyLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
// TODO : Create a currency model to store these values // TODO : Create a currency model to store these values
currency.items = FXCollections.observableList(setOf(USD, GBP, CHF).toList()) currencyChoiceBox.items = FXCollections.observableList(setOf(USD, GBP, CHF).toList())
currency.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull) currencyChoiceBox.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
amount.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
availableAmount.visibleProperty().bind(
arrayListOf(issuerChoiceBox, currencyChoiceBox)
.map { it.valueProperty().isNotNull.and(it.visibleProperty()) }
.reduce(BooleanBinding::and)
)
availableAmount.textProperty()
.bind(Bindings.createStringBinding({
val filteredCash = cash.filtered {
it.token.issuer.party == issuerChoiceBox.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) amountLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
issueRefLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull) amountTextField.textFormatter = bigDecimalFormatter().apply { amount.bind(this.valueProperty()) }
issueRefTextField.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull) amountTextField.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
// Validate inputs. // Validate inputs.
val formValidCondition = arrayOf( val formValidCondition = arrayOf(
myIdentity.isNotNull(), myIdentity.isNotNull(),
transactionTypeCB.valueProperty().isNotNull, transactionTypeCB.valueProperty().isNotNull,
partyBChoiceBox.visibleProperty().not().or(partyBChoiceBox.valueProperty().isNotNull), partyBChoiceBox.visibleProperty().not().or(partyBChoiceBox.valueProperty().isNotNull),
textFormatter.valueProperty().isNotNull, issuerChoiceBox.visibleProperty().not().or(partyBChoiceBox.valueProperty().isNotNull),
textFormatter.valueProperty().isNotEqualTo(BigDecimal.ZERO), amountTextField.textProperty().isNotEmpty,
currency.valueProperty().isNotNull currencyChoiceBox.valueProperty().isNotNull
).reduce(BooleanBinding::and) ).reduce(BooleanBinding::and)
// Enable execute button when form is valid. // Enable execute button when form is valid.
executeButton.disableProperty().bind(formValidCondition.not()) root.buttonTypes.add(executeButton)
executeButton.setOnAction { event -> root.lookupButton(executeButton).disableProperty().bind(formValidCondition.not())
// Null checks to ensure these observable values are set, execute button should be disabled if any of these value are null, this extra checks are for precaution and getting non-nullable values without using !!.
myIdentity.value?.let { myIdentity ->
// TODO : Allow user to chose which notary to use?
notaries.first()?.let { notary ->
rpcProxy.value?.let { rpcProxy ->
Triple(myIdentity, notary, rpcProxy)
}
}
}?.let {
val (myIdentity, notary, rpcProxy) = it
transactionTypeCB.value?.let {
// Default issuer reference to 1 if not specified.
val issueRef = OpaqueBytes(if (issueRefTextField.text.trim().isNotBlank()) issueRefTextField.text.toByteArray() else ByteArray(1, { 1 }))
// TODO : Change these commands into individual RPC methods instead of using executeCommand.
val command = when (it) {
CashTransaction.Issue -> CashCommand.IssueCash(Amount(textFormatter.value, currency.value), issueRef, partyBChoiceBox.value.legalIdentity, notary.notaryIdentity)
CashTransaction.Pay -> CashCommand.PayCash(Amount(textFormatter.value, Issued(PartyAndReference(myIdentity.legalIdentity, issueRef), currency.value)), partyBChoiceBox.value.legalIdentity)
CashTransaction.Exit -> CashCommand.ExitCash(Amount(textFormatter.value, currency.value), issueRef)
}
val dialog = Alert(Alert.AlertType.INFORMATION).apply {
headerText = null
contentText = "Transaction Started."
dialogPane.isDisable = true
initOwner((event.target as Node).scene.window)
}
dialog.show()
runAsync {
rpcProxy.startProtocol(::CashProtocol, command).returnValue.toBlocking().first()
}.ui {
dialog.contentText = when (it) {
is CashProtocolResult.Success -> {
dialog.alertType = Alert.AlertType.INFORMATION
dialog.setOnCloseRequest { resetScreen() }
"Transaction Started \nTransaction ID : ${it.transaction?.id} \nMessage : ${it.message}"
}
is CashProtocolResult.Failed -> {
dialog.alertType = Alert.AlertType.ERROR
it.toString()
}
}
dialog.dialogPane.isDisable = false
dialog.dialogPane.scene.window.sizeToScene()
}.setOnFailed {
dialog.close()
ExceptionDialog(it.source.exception).apply {
initOwner((event.target as Node).scene.window)
}.showAndWait()
}
}
}
}
// Remove focus from textfield when click on the blank area.
root.setOnMouseClicked { e -> root.requestFocus() }
} }
} }

View File

@ -1,23 +1,23 @@
package net.corda.explorer.views package net.corda.explorer.views
import javafx.collections.ObservableList
import javafx.scene.Node
import javafx.scene.Parent
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.ChosenList
import net.corda.client.fxutils.filter import net.corda.client.fxutils.filter
import net.corda.client.fxutils.lift import net.corda.client.fxutils.lift
import net.corda.client.fxutils.map import net.corda.client.fxutils.map
import javafx.collections.ObservableList
import javafx.scene.Parent
import javafx.scene.control.TextField
import javafx.scene.image.ImageView
import javafx.scene.input.MouseButton
import javafx.scene.input.MouseEvent
import tornadofx.UIComponent import tornadofx.UIComponent
import tornadofx.observable import tornadofx.observable
class SearchField<T>(private val data: ObservableList<T>, filterCriteria: Array<(T, String) -> Boolean>) : UIComponent() { class SearchField<T>(private val data: ObservableList<T>, vararg filterCriteria: (T, String) -> Boolean) : UIComponent() {
override val root: Parent by fxml() override val root: Parent by fxml()
private val textField by fxid<TextField>() private val textField by fxid<TextField>()
private val clearButton by fxid<ImageView>() private val clearButton by fxid<Node>()
// Currently this method apply each filter to the collection and return the collection with most matches. // 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. // TODO : Allow user to chose if there are matches in multiple category.
@ -30,7 +30,7 @@ class SearchField<T>(private val data: ObservableList<T>, filterCriteria: Array<
init { init {
clearButton.setOnMouseClicked { event: MouseEvent -> clearButton.setOnMouseClicked { event: MouseEvent ->
if (event.button == MouseButton.PRIMARY) { if (event.button == MouseButton.PRIMARY) {
textField.text = "" textField.clear()
} }
} }
} }

View File

@ -0,0 +1,12 @@
package net.corda.explorer.views
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import javafx.scene.Node
import net.corda.explorer.model.CordaView
// TODO : 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 icon = FontAwesomeIcon.COGS
}

View File

@ -1,51 +0,0 @@
package net.corda.explorer.views
import net.corda.client.fxutils.map
import net.corda.client.model.writableValue
import net.corda.explorer.model.SelectedView
import net.corda.explorer.model.TopLevelModel
import javafx.beans.value.WritableValue
import javafx.geometry.Pos
import javafx.scene.control.ContentDisplay
import javafx.scene.input.MouseButton
import javafx.scene.layout.VBox
import javafx.scene.text.Font
import javafx.scene.text.TextAlignment
import tornadofx.View
import tornadofx.button
import tornadofx.imageview
class Sidebar : View() {
override val root: VBox by fxml()
private val selectedView: WritableValue<SelectedView> by writableValue(TopLevelModel::selectedView)
init {
// TODO: Obtain views from ViewModel.
arrayOf(SelectedView.Home, SelectedView.Cash, SelectedView.Transaction, SelectedView.NewTransaction, SelectedView.Network, SelectedView.Setting).forEach { view ->
root.apply {
button(view.displayableName) {
graphic = imageview {
image = view.image
// TODO : Use CSS instead.
fitWidth = 35.0
fitHeight = 35.0
}
styleClass.add("sidebar-menu-item")
setOnMouseClicked { e ->
if (e.button == MouseButton.PRIMARY) {
selectedView.value = view
}
}
// Transform to smaller icon layout when sidebar width is below 150.
val smallIconProperty = widthProperty().map { (it.toDouble() < 150) }
contentDisplayProperty().bind(smallIconProperty.map { if (it) ContentDisplay.TOP else ContentDisplay.LEFT })
textAlignmentProperty().bind(smallIconProperty.map { if (it) TextAlignment.CENTER else TextAlignment.LEFT })
alignmentProperty().bind(smallIconProperty.map { if (it) Pos.CENTER else Pos.CENTER_LEFT })
fontProperty().bind(smallIconProperty.map { if (it) Font.font(9.0) else Font.font(13.0) })
wrapTextProperty().bind(smallIconProperty)
}
}
}
}
}

View File

@ -1,63 +0,0 @@
package net.corda.explorer.views
import net.corda.client.fxutils.map
import net.corda.client.model.objectProperty
import net.corda.explorer.model.SelectedView
import net.corda.explorer.model.TopLevelModel
import javafx.beans.property.ObjectProperty
import javafx.geometry.Pos
import javafx.scene.Parent
import javafx.scene.layout.BorderPane
import javafx.scene.layout.GridPane
import javafx.scene.layout.Pane
import javafx.scene.layout.Priority
import javafx.scene.text.TextAlignment
import tornadofx.View
import tornadofx.add
import tornadofx.gridpane
import tornadofx.label
class TopLevel : View() {
override val root: Parent by fxml()
val selectionBorderPane: BorderPane by fxid()
val sidebarPane: Pane by fxid()
private val header: Header by inject()
private val sidebar: Sidebar by inject()
private val home: Home by inject()
private val cash: CashViewer by inject()
private val transaction: TransactionViewer by inject()
private val newTransaction: NewTransaction by inject()
// Note: this is weirdly very important, as it forces the initialisation of Views. Therefore this is the entry
// point to the top level observable/stream wiring! Any events sent before this init may be lost!
private val homeRoot = home.root
private val cashRoot = cash.root
private val transactionRoot = transaction.root
private val newTransactionRoot = newTransaction.root
val selectedView: ObjectProperty<SelectedView> by objectProperty(TopLevelModel::selectedView)
init {
selectionBorderPane.centerProperty().bind(selectedView.map {
when (it) {
SelectedView.Home -> homeRoot
SelectedView.Cash -> cashRoot
SelectedView.Transaction -> transactionRoot
SelectedView.NewTransaction -> newTransactionRoot
else -> gridpane {
label("Under Construction...") {
maxWidth = Double.MAX_VALUE
textAlignment = TextAlignment.CENTER
alignment = Pos.CENTER
GridPane.setVgrow(this, Priority.ALWAYS)
GridPane.setHgrow(this, Priority.ALWAYS)
}
}
}
})
selectionBorderPane.center.styleClass.add("no-padding")
sidebarPane.add(sidebar.root)
selectionBorderPane.top = header.root
}
}

View File

@ -1,9 +1,12 @@
package net.corda.explorer.views package net.corda.explorer.views
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
import javafx.beans.value.ObservableValue import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections import javafx.collections.FXCollections
import javafx.geometry.Insets import javafx.geometry.Insets
import javafx.geometry.Pos
import javafx.scene.Node
import javafx.scene.Parent import javafx.scene.Parent
import javafx.scene.control.Label import javafx.scene.control.Label
import javafx.scene.control.ListView import javafx.scene.control.ListView
@ -19,7 +22,6 @@ import net.corda.client.model.*
import net.corda.contracts.asset.Cash import net.corda.contracts.asset.Cash
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.PublicKeyTree import net.corda.core.crypto.PublicKeyTree
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.tree import net.corda.core.crypto.tree
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.protocols.StateMachineRunId import net.corda.core.protocols.StateMachineRunId
@ -27,14 +29,17 @@ import net.corda.explorer.AmountDiff
import net.corda.explorer.formatters.AmountFormatter import net.corda.explorer.formatters.AmountFormatter
import net.corda.explorer.identicon.identicon import net.corda.explorer.identicon.identicon
import net.corda.explorer.identicon.identiconToolTip import net.corda.explorer.identicon.identiconToolTip
import net.corda.explorer.model.CordaView
import net.corda.explorer.model.ReportingCurrencyModel import net.corda.explorer.model.ReportingCurrencyModel
import net.corda.explorer.sign import net.corda.explorer.sign
import net.corda.explorer.ui.setCustomCellFactory import net.corda.explorer.ui.setCustomCellFactory
import tornadofx.* import tornadofx.*
import java.util.* import java.util.*
class TransactionViewer : View() { class TransactionViewer : CordaView("Transactions") {
override val root by fxml<BorderPane>() override val root by fxml<BorderPane>()
override val icon = FontAwesomeIcon.EXCHANGE
private val transactionViewTable by fxid<TableView<ViewerNode>>() private val transactionViewTable by fxid<TableView<ViewerNode>>()
private val matchingTransactionsLabel by fxid<Label>() private val matchingTransactionsLabel by fxid<Label>()
// Inject data // Inject data
@ -42,13 +47,16 @@ class TransactionViewer : View() {
private val reportingExchange by observableValue(ReportingCurrencyModel::reportingExchange) private val reportingExchange by observableValue(ReportingCurrencyModel::reportingExchange)
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity) private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
override val widget: Node = TransactionWidget()
/** /**
* This is what holds data for a single transaction node. Note how a lot of these are nullable as we often simply don't * This is what holds data for a single transaction node. Note how a lot of these are nullable as we often simply don't
* have the data. * have the data.
*/ */
data class ViewerNode( data class ViewerNode(
val transaction: PartiallyResolvedTransaction, val transaction: PartiallyResolvedTransaction,
val transactionId: SecureHash, val inputContracts: List<Contract>,
val outputContracts: List<Contract>,
val stateMachineRunId: ObservableValue<StateMachineRunId?>, val stateMachineRunId: ObservableValue<StateMachineRunId?>,
val stateMachineStatus: ObservableValue<out StateMachineStatus?>, val stateMachineStatus: ObservableValue<out StateMachineStatus?>,
val protocolStatus: ObservableValue<out ProtocolStatus?>, val protocolStatus: ObservableValue<out ProtocolStatus?>,
@ -76,7 +84,8 @@ class TransactionViewer : View() {
} }
ViewerNode( ViewerNode(
transaction = it.transaction, transaction = it.transaction,
transactionId = it.transaction.id, 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 }, stateMachineRunId = stateMachine.map { it?.id },
protocolStatus = stateMachineProperty { it.protocolStatus }, protocolStatus = stateMachineProperty { it.protocolStatus },
stateMachineStatus = stateMachineProperty { it.stateMachineStatus }, stateMachineStatus = stateMachineProperty { it.stateMachineStatus },
@ -97,19 +106,21 @@ class TransactionViewer : View() {
} }
init { init {
val searchField = SearchField(viewerNodes, arrayOf({ viewerNode, s -> viewerNode.commandTypes.any { it.simpleName.contains(s, true) } })) val searchField = SearchField(viewerNodes, { viewerNode, s -> viewerNode.commandTypes.any { it.simpleName.contains(s, true) } })
root.top = searchField.root root.top = searchField.root
// Transaction table // Transaction table
transactionViewTable.apply { transactionViewTable.apply {
items = searchField.filteredData items = searchField.filteredData
column("Transaction ID", ViewerNode::transactionId).setCustomCellFactory { column("Transaction ID", ViewerNode::transaction).setCustomCellFactory {
label("$it".substring(0, 16) + "...") { label("${it.id}") {
graphic = imageview { graphic = imageview {
image = identicon(it, 5.0) image = identicon(it.id, 5.0)
} }
tooltip = identiconToolTip(it) tooltip = identiconToolTip(it.id)
} }
} }
column("Input Contract Type(s)", ViewerNode::inputContracts).cellFormat { text = (it.map { it.javaClass.simpleName }.toSet().joinToString(", ")) }
column("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("State Machine ID", ViewerNode::stateMachineRunId).cellFormat { text = "${it?.uuid ?: ""}" }
column("Protocol status", ViewerNode::protocolStatus).cellFormat { text = "${it.value ?: ""}" } column("Protocol status", ViewerNode::protocolStatus).cellFormat { text = "${it.value ?: ""}" }
column("SM Status", ViewerNode::stateMachineStatus).cellFormat { text = "${it.value ?: ""}" } column("SM Status", ViewerNode::stateMachineStatus).cellFormat { text = "${it.value ?: ""}" }
@ -207,6 +218,21 @@ class TransactionViewer : View() {
} }
} }
} }
private class TransactionWidget() : BorderPane() {
private val gatheredTransactionDataList by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
// TODO : Add a scrolling table to show latest transaction.
// TODO : Add a chart to show types of transactions.
init {
right {
label {
textProperty().bind(Bindings.size(gatheredTransactionDataList).map { it.toString() })
BorderPane.setAlignment(this, Pos.BOTTOM_RIGHT)
}
}
}
}
} }
/** /**
@ -218,12 +244,10 @@ private fun calculateTotalEquiv(identity: NodeInfo?,
outputs: List<TransactionState<ContractState>>): AmountDiff<Currency> { outputs: List<TransactionState<ContractState>>): AmountDiff<Currency> {
val (reportingCurrency, exchange) = reportingCurrencyExchange val (reportingCurrency, exchange) = reportingCurrencyExchange
val publicKey = identity?.legalIdentity?.owningKey val publicKey = identity?.legalIdentity?.owningKey
fun List<TransactionState<ContractState>>.sum(): Long { fun List<TransactionState<ContractState>>.sum() = this.map { it.data as? Cash.State }
return this.map { it.data as? Cash.State } .filterNotNull()
.filterNotNull() .filter { publicKey == it.owner }
.filter { publicKey == it.owner } .map { exchange(it.amount.withoutIssuer()).quantity }
.map { exchange(it.amount.withoutIssuer()).quantity } .sum()
.sum()
}
return AmountDiff.fromLong(outputs.sum() - inputs.sum(), reportingCurrency) return AmountDiff.fromLong(outputs.sum() - inputs.sum(), reportingCurrency)
} }

View File

@ -1,20 +1,8 @@
package net.corda.explorer.views package net.corda.explorer.views.cordapps
import net.corda.client.fxutils.*
import net.corda.client.model.*
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.Amount
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.withoutIssuer
import net.corda.core.crypto.Party
import net.corda.explorer.formatters.AmountFormatter
import net.corda.explorer.identicon.identicon
import net.corda.explorer.identicon.identiconToolTip
import net.corda.explorer.model.ReportingCurrencyModel
import net.corda.explorer.model.SettingsModel
import net.corda.explorer.ui.*
import com.sun.javafx.collections.ObservableListWrapper import com.sun.javafx.collections.ObservableListWrapper
import javafx.application.Platform import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
import javafx.beans.value.ObservableValue import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections import javafx.collections.FXCollections
@ -25,54 +13,38 @@ import javafx.scene.Parent
import javafx.scene.chart.NumberAxis import javafx.scene.chart.NumberAxis
import javafx.scene.control.* import javafx.scene.control.*
import javafx.scene.image.ImageView import javafx.scene.image.ImageView
import javafx.scene.input.MouseButton
import javafx.scene.layout.BorderPane import javafx.scene.layout.BorderPane
import javafx.scene.layout.HBox
import javafx.scene.layout.Priority
import javafx.scene.layout.VBox import javafx.scene.layout.VBox
import net.corda.client.fxutils.*
import net.corda.client.model.*
import net.corda.contracts.asset.Cash
import net.corda.core.contracts.Amount
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.withoutIssuer
import net.corda.core.crypto.Party
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.ReportingCurrencyModel
import net.corda.explorer.model.SettingsModel
import net.corda.explorer.ui.*
import net.corda.explorer.views.*
import org.fxmisc.easybind.EasyBind import org.fxmisc.easybind.EasyBind
import tornadofx.* import tornadofx.*
import java.time.Instant import java.time.Instant
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.* import java.util.*
class CashViewer : View(), CordaView { class CashViewer : CordaView("Cash") {
// Inject UI elements. // Inject UI elements.
override val root: BorderPane by fxml() override val root: BorderPane by fxml()
override val icon: FontAwesomeIcon = FontAwesomeIcon.MONEY
// View's widget. // View's widget.
override val viewName = "Cash" override val widget: Node = CashWidget()
override val widget: Node = vbox {
padding = Insets(0.0, 10.0, 0.0, 0.0)
val xAxis = NumberAxis().apply {
//isAutoRanging = true
isMinorTickVisible = false
isForceZeroInRange = false
tickLabelFormatter = stringConverter {
Instant.ofEpochMilli(it.toLong()).atZone(TimeZone.getDefault().toZoneId()).toLocalTime().toString()
}
}
val yAxis = NumberAxis().apply {
isAutoRanging = true
isMinorTickVisible = false
isForceZeroInRange = false
tickLabelFormatter = stringConverter { it.toStringWithSuffix(0) }
}
linechart(null, xAxis, yAxis) {
series("USD") {
runAsync {
while (true) {
Thread.sleep(1000)
Platform.runLater {
// Modify data in UI thread.
if (data.size > 300) data.remove(0, 1)
data(System.currentTimeMillis(), sumAmount.value.quantity)
}
}
}
}
createSymbols = false
animated = false
}
}
// Left pane // Left pane
private val leftPane: VBox by fxid() private val leftPane: VBox by fxid()
private val splitPane: SplitPane by fxid() private val splitPane: SplitPane by fxid()
@ -81,23 +53,15 @@ class CashViewer : View(), CordaView {
private val cashViewerTableIssuerCurrency: TreeTableColumn<ViewerNode, String> by fxid() private val cashViewerTableIssuerCurrency: TreeTableColumn<ViewerNode, String> by fxid()
private val cashViewerTableLocalCurrency: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid() private val cashViewerTableLocalCurrency: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid()
private val cashViewerTableEquiv: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid() private val cashViewerTableEquiv: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid()
// Right pane // Right pane
private val rightPane: VBox by fxid() private val rightPane: VBox by fxid()
private val totalPositionsLabel: Label by fxid() private val totalPositionsLabel: Label by fxid()
private val cashStatesList: ListView<StateRow> by fxid() private val cashStatesList: ListView<StateRow> by fxid()
private val toggleButton by fxid<Button>() private val toggleButton by fxid<Button>()
// Inject observables // Inject observables
private val cashStates by observableList(ContractStateModel::cashStates) private val cashStates by observableList(ContractStateModel::cashStates)
private val reportingCurrency by observableValue(SettingsModel::reportingCurrency) private val reportingCurrency by observableValue(SettingsModel::reportingCurrency)
private val reportingExchange by observableValue(ReportingCurrencyModel::reportingExchange) private val reportingExchange by observableValue(ReportingCurrencyModel::reportingExchange)
private val exchangeRate: ObservableValue<ExchangeRate> by observableValue(ExchangeRateModel::exchangeRate)
private val sumAmount = AmountBindings.sumAmountExchange(
cashStates.map { it.state.data.amount.withoutIssuer() },
reportingCurrency,
exchangeRate
)
private val selectedNode = cashViewerTable.singleRowSelection().map { private val selectedNode = cashViewerTable.singleRowSelection().map {
when (it) { when (it) {
@ -175,11 +139,21 @@ class CashViewer : View(), CordaView {
* one which produces more results, which seems to work, as the set of currency strings don't really overlap with * one which produces more results, which seems to work, as the set of currency strings don't really overlap with
* issuer strings. * issuer strings.
*/ */
val searchField = SearchField(cashStates, arrayOf( val searchField = SearchField(cashStates,
{ state, text -> state.state.data.amount.token.product.toString().contains(text, true) }, { 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) } { state, text -> state.state.data.amount.token.issuer.party.toString().contains(text, true) }
)) )
root.top = searchField.root root.top = hbox(5.0) {
button("New Transaction", FontAwesomeIconView(FontAwesomeIcon.PLUS)) {
setOnMouseClicked {
if (it.button == MouseButton.PRIMARY) {
NewTransaction().show(this@CashViewer.root.scene.window)
}
}
}
HBox.setHgrow(searchField.root, Priority.ALWAYS)
add(searchField.root)
}
/** /**
* This is where we aggregate the list of cash states into the TreeTable structure. * This is where we aggregate the list of cash states into the TreeTable structure.
@ -280,7 +254,6 @@ class CashViewer : View(), CordaView {
textProperty().bind(reportingCurrency.map { "$it Equiv" }) textProperty().bind(reportingCurrency.map { "$it Equiv" })
} }
// Right Pane. // Right Pane.
totalPositionsLabel.textProperty().bind(cashStatesList.itemsProperty().map { totalPositionsLabel.textProperty().bind(cashStatesList.itemsProperty().map {
val plural = if (it.size == 1) "" else "s" val plural = if (it.size == 1) "" else "s"
@ -303,4 +276,49 @@ class CashViewer : View(), CordaView {
cashViewerTable.selectionModel.clearSelection() cashViewerTable.selectionModel.clearSelection()
} }
} }
private class CashWidget() : VBox() {
// Inject data.
private val reportingCurrency by observableValue(SettingsModel::reportingCurrency)
private val cashStates by observableList(ContractStateModel::cashStates)
private val exchangeRate: ObservableValue<ExchangeRate> by observableValue(ExchangeRateModel::exchangeRate)
private val sumAmount = AmountBindings.sumAmountExchange(
cashStates.map { it.state.data.amount.withoutIssuer() },
reportingCurrency,
exchangeRate)
init {
padding = Insets(0.0, 10.0, 0.0, 0.0)
val xAxis = NumberAxis().apply {
//isAutoRanging = true
isMinorTickVisible = false
isForceZeroInRange = false
tickLabelFormatter = stringConverter {
Instant.ofEpochMilli(it.toLong()).atZone(TimeZone.getDefault().toZoneId()).toLocalTime().toString()
}
}
val yAxis = NumberAxis().apply {
isAutoRanging = true
isMinorTickVisible = false
isForceZeroInRange = false
tickLabelFormatter = stringConverter { it.toStringWithSuffix() }
}
linechart(null, xAxis, yAxis) {
series("USD") {
sumAmount.addListener { observableValue, old, new ->
val lastTimeStamp = data.last().value?.xValue
if (lastTimeStamp == null || System.currentTimeMillis() - lastTimeStamp.toLong() > 1.seconds.toMillis()) {
data(System.currentTimeMillis(), sumAmount.value.quantity)
runInFxApplicationThread {
// Modify data in UI thread.
if (data.size > 300) data.remove(0, 1)
}
}
}
}
createSymbols = false
animated = false
}
}
}
} }

View File

@ -0,0 +1,18 @@
/* Corda dark color theme */
* {
-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-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 */
}
/* Global Style*/
.corda-logo {
-fx-image: url("../images/Logo-03.png");
}
.corda-text-logo {
-fx-image: url("../images/Logo-04.png");
}

View File

@ -0,0 +1,203 @@
@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;
}
.split-pane-divider {
-fx-background-color: transparent;
-fx-border-color: transparent;
-fx-border-width: 0;
-fx-padding: 0 0 0 2;
}
/* Sidebar */
.sidebar {
-fx-background-color: -color-0;
-fx-max-width: 200px;
}
.sidebar-menu-item {
-fx-background-color: transparent;
-fx-max-width: infinity;
-fx-min-width: 85px;
-fx-border-width: 0;
-fx-padding: 10, 10, 10, 10;
-fx-text-fill: white;
-fx-font-weight: bold;
-fx-background-radius: 0;
}
.sidebar-menu-item-selected,
.sidebar-menu-item:hover, .sidebar-menu-item:selected {
-fx-background-color: -color-3;
-fx-border-color: -color-3;
}
.sidebar-menu-item-arrow {
-fx-text-fill: -color-0;
-fx-font-size: 30pt;
}
/* Top level split panes */
#mainSplitPane > .split-pane-divider {
-fx-background-color: -color-0;
-fx-border-color: -color-0;
-fx-border-width: 0;
-fx-padding: 0 0 0 3;
}
/* Header */
.header {
-fx-background-color: -color-0;
-fx-padding: 5;
}
.header .split-menu-button {
-fx-min-width: 100px;
}
.header .split-menu-button .label {
-fx-text-fill: -color-0;
}
.mainView .split-pane {
-fx-background-color: -fx-box-border, -fx-control-inner-background;
-fx-background-insets: 0;
-fx-padding: 0;
}
/* Chart */
.chart {
-fx-padding: 0;
}
.chart-series-line {
-fx-stroke-width: 2px;
-fx-effect: null;
}
.default-color0.chart-series-line {
-fx-stroke: -color-3;
}
.chart-plot-background {
-fx-background-color: rgba(255, 255, 255, 0.5);
}
.chart-horizontal-grid-lines, .chart-vertical-grid-lines {
-fx-stroke: transparent;
}
.chart-alternative-row-fill {
-fx-fill: transparent;
-fx-stroke: transparent;
-fx-stroke-width: 0;
}
/* Home view*/
.tile .title, .tile:expanded .title {
-fx-alignment: center-left;
-fx-font-size: 1.4em;
-fx-font-weight: bold;
-fx-cursor: hand;
-fx-background-color: -color-1;
-fx-border-color: transparent;
}
.tile .title .text, .tile:expanded .title .text {
-fx-fill: -color-0;
}
.tile .content {
-fx-background-color: -color-1;
-fx-background-size: Auto 90%;
-fx-background-repeat: no-repeat;
-fx-background-position: center center;
-fx-cursor: hand;
-fx-padding: 0px;
-fx-alignment: bottom-right;
-fx-border-color: transparent; /*t r b l */
}
.tile .label {
-fx-font-size: 2.4em;
-fx-padding: 20px;
-fx-text-fill: -color-0;
-fx-font-weight: normal;
-fx-text-alignment: right;
}
.tile:hover {
-fx-border-color: -color-3;
-fx-border-width: 2;
-fx-border-radius: 2;
}
.tile, .tile-user {
-fx-padding: 10px;
-fx-pref-height: 250px;
}
/* Login View */
.login .button {
-fx-background-color: -color-3;
-fx-font-weight: bold;
-fx-text-fill: -color-2;
}
.login .button:hover {
-fx-background-color: -color-4;
}
.login {
-fx-background-color: -color-0;
}
.login .text-field {
-fx-border-color: -color-1;
}
.login .label {
-fx-text-fill: -color-2;
-fx-font-weight: bold;
}
.searchField .text-field {
-fx-padding: 5px 5px 5px 30px;
-fx-background-radius: 2px;
-fx-border-radius: 2px;
}
.searchField .glyph-icon {
-fx-fill: -color-1;
-fx-padding: 0;
}
.searchField .search-clear:hover {
-fx-fill: -color-4;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -3,16 +3,12 @@
<?import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView?> <?import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView?>
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<GridPane hgap="10" styleClass="expand-row" stylesheets="@../css/wallet.css" vgap="10" xmlns="http://javafx.com/javafx/8.0.112-ea" xmlns:fx="http://javafx.com/fxml/1"> <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">
<TitledPane fx:id="inputPane" collapsible="false" text="Input" GridPane.fillWidth="true"> <TitledPane fx:id="inputPane" collapsible="false" text="Input" GridPane.fillWidth="true">
<ListView fx:id="inputs"/> <ListView fx:id="inputs"/>
</TitledPane> </TitledPane>
<Label GridPane.columnIndex="1"> <FontAwesomeIconView glyphName="PLAY" glyphSize="40" GridPane.columnIndex="1"/>
<graphic>
<FontAwesomeIconView glyphName="PLAY" glyphSize="40" style="-fx-fill: rgb(20, 136, 204);"/>
</graphic>
</Label>
<TitledPane fx:id="outputPane" collapsible="false" text="Outputs" GridPane.columnIndex="2"> <TitledPane fx:id="outputPane" collapsible="false" text="Outputs" GridPane.columnIndex="2">
<ListView fx:id="outputs" maxWidth="Infinity"/> <ListView fx:id="outputs" maxWidth="Infinity"/>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.TitledPane?>
<?import javafx.scene.layout.TilePane?>
<ScrollPane hbarPolicy="NEVER" fitToWidth="true" stylesheets="@../css/corda.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<TilePane fx:id="tilePane" tileAlignment="TOP_LEFT">
<TitledPane fx:id="template" text="Template" collapsible="false" styleClass="tile">
<Label text="USD 186.7m" textAlignment="CENTER" wrapText="true"/>
</TitledPane>
</TilePane>
</ScrollPane>

View File

@ -1,45 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.SplitMenuButton?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.GridPane?>
<GridPane hgap="10" stylesheets="@../css/wallet.css" styleClass="header-panel" vgap="5" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<padding>
<Insets left="10.0" right="10.0" top="5.0" bottom="5.0"/>
</padding>
<!-- Row 1 -->
<Label id="headline" fx:id="sectionLabel" alignment="BOTTOM_CENTER" maxHeight="Infinity" text="Home" GridPane.columnIndex="0" GridPane.hgrow="ALWAYS"/>
<SplitMenuButton fx:id="userButton" maxHeight="Infinity" mnemonicParsing="false" text="DRUTTER" GridPane.columnIndex="3">
<items>
<MenuItem mnemonicParsing="false" text="Sign out"/>
<MenuItem mnemonicParsing="false" text="Account settings..."/>
</items>
<graphic>
<ImageView fitHeight="20.0" fitWidth="20.0" pickOnBounds="true" preserveRatio="true">
<Image url="@../images/user_w.png"/>
</ImageView>
</graphic>
</SplitMenuButton>
<!--<Button fx:id="settingsButton" maxHeight="Infinity" mnemonicParsing="false" text="Settings" GridPane.columnIndex="4">
<graphic>
<ImageView fitHeight="20.0" fitWidth="20.0" pickOnBounds="true" preserveRatio="true">
<Image url="@../images/settings_w.png"/>
</ImageView>
</graphic>
</Button>-->
<!--&lt;!&ndash; Row 2 &ndash;&gt;
<StackPane alignment="CENTER_RIGHT" GridPane.columnSpan="5" GridPane.rowIndex="1">
<TextField fx:id="search_main" promptText="Search for states, transactions, counterparties etc." styleClass="search"/>
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
<StackPane.margin>
<Insets right="10.0"/>
</StackPane.margin>
</ImageView>
</StackPane>-->
</GridPane>

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.TilePane?>
<ScrollPane hbarPolicy="NEVER" fitToWidth="true" stylesheets="@../css/wallet.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<TilePane fx:id="tilePane" tileAlignment="TOP_LEFT" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<TitledPane id="Cash" fx:id="ourCashPane" collapsible="false" onMouseClicked="#changeView" styleClass="tile" text="Our cash">
<Label fx:id="ourCashLabel" text="USD 186.7m" textAlignment="CENTER" wrapText="true" />
</TitledPane>
<TitledPane id="tile_debtors" fx:id="ourDebtorsPane" collapsible="false" styleClass="tile" text="Our debtors">
<Label text="USD 71.3m" textAlignment="CENTER" wrapText="true" />
</TitledPane>
<TitledPane id="tile_creditors" fx:id="ourCreditorsPane" collapsible="false" styleClass="tile" text="Our creditors">
<Label text="USD (29.4m)" textAlignment="CENTER" wrapText="true" />
</TitledPane>
<TitledPane id="Transaction" fx:id="ourTransactionsPane" collapsible="false" onMouseClicked="#changeView" styleClass="tile" text="Our transactions">
<Label fx:id="ourTransactionsLabel" textAlignment="CENTER" wrapText="true" />
</TitledPane>
<TitledPane id="NewTransaction" fx:id="newTransaction" collapsible="false" onMouseClicked="#changeView" styleClass="tile" text="New Transaction">
</TitledPane>
</TilePane>
</ScrollPane>

View File

@ -2,23 +2,38 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<DialogPane stylesheets="@../css/wallet.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1"> <DialogPane styleClass="login" stylesheets="@../css/corda.css" xmlns="http://javafx.com/javafx/8.0.112-ea" xmlns:fx="http://javafx.com/fxml/1">
<padding> <padding>
<Insets top="10" bottom="10" left="50" right="50"/> <Insets bottom="30" left="50" right="50" top="10"/>
</padding> </padding>
<content> <content>
<GridPane hgap="10" vgap="10" prefWidth="400"> <BorderPane>
<Label text="Corda Node :"/> <top>
<TextField fx:id="host" promptText="Host" GridPane.columnIndex="1"/> <VBox alignment="CENTER" spacing="-60" maxWidth="Infinity">
<TextField fx:id="port" promptText="Port" prefWidth="100" GridPane.columnIndex="2"/> <ImageView fitWidth="250" preserveRatio="true" styleClass="corda-logo"/>
<ImageView fitWidth="280" preserveRatio="true" styleClass="corda-text-logo"/>
<padding>
<Insets bottom="50" top="20"/>
</padding>
</VBox>
</top>
<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"/>
<Label text="Username :" GridPane.rowIndex="1"/> <Label text="Username :" GridPane.rowIndex="1" GridPane.halignment="RIGHT"/>
<TextField fx:id="username" promptText="Username" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.columnSpan="2"/> <TextField fx:id="username" 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"/>
</GridPane>
</center>
</BorderPane>
<Label text="Password:" GridPane.rowIndex="2"/>
<PasswordField fx:id="password" promptText="Password" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.columnSpan="2"/>
</GridPane>
</content> </content>
<ButtonType fx:id="connectButton" text="Connect" buttonData="OK_DONE"/> <ButtonType buttonData="OK_DONE" text="Connect"/>
</DialogPane> </DialogPane>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView?>
<?import javafx.scene.control.*?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.*?>
<StackPane styleClass="mainView" stylesheets="@../css/corda.css" prefHeight="650" prefWidth="900" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<BorderPane maxHeight="Infinity">
<top>
<GridPane styleClass="header" vgap="5">
<!-- Corda logo -->
<ImageView styleClass="corda-text-logo" fitHeight="35" preserveRatio="true" GridPane.hgrow="ALWAYS" fx:id="cordaLogo"/>
<!-- 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..."/>
</items>
<graphic>
<FontAwesomeIconView glyphName="USER" glyphSize="20"/>
</graphic>
</MenuButton>
</GridPane>
</top>
<center>
<SplitPane id="mainSplitPane" dividerPositions="0.0">
<VBox styleClass="sidebar" fx:id="sidebar" SplitPane.resizableWithParent="false">
<StackPane>
<Button fx:id="template" text="Template" styleClass="sidebar-menu-item"/>
<FontAwesomeIconView glyphName="CARET_LEFT" visible="false"/>
</StackPane>
<StackPane>
<Button fx:id="selectedTemplate" text="Selected" styleClass="sidebar-menu-item, sidebar-menu-item-selected"/>
<FontAwesomeIconView glyphName="CARET_LEFT" StackPane.alignment="CENTER_RIGHT" styleClass="sidebar-menu-item-arrow"/>
</StackPane>
</VBox>
<BorderPane fx:id="selectionBorderPane"/>
</SplitPane>
</center>
</BorderPane>
</StackPane>

View File

@ -3,34 +3,43 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<GridPane hgap="10" vgap="10" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1"> <DialogPane stylesheets="@../css/corda.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<!-- Row 1 --> <content>
<Label text="Transaction Type : " GridPane.halignment="RIGHT"/> <GridPane hgap="10" vgap="10">
<ChoiceBox fx:id="transactionTypeCB" maxWidth="Infinity" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.hgrow="ALWAYS"/> <!-- 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 2 --> <!-- Row 1 -->
<Label fx:id="partyALabel" GridPane.halignment="RIGHT" GridPane.rowIndex="1"/> <Label fx:id="partyALabel" GridPane.halignment="RIGHT" GridPane.rowIndex="1"/>
<TextField fx:id="partyATextField" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="1"/> <TextField fx:id="partyATextField" GridPane.columnIndex="1" GridPane.columnSpan="4" GridPane.rowIndex="1"/>
<!-- Row 3 --> <!-- Row 2 -->
<Label fx:id="partyBLabel" GridPane.halignment="RIGHT" GridPane.rowIndex="2"/> <Label fx:id="partyBLabel" GridPane.halignment="RIGHT" GridPane.rowIndex="2"/>
<ChoiceBox fx:id="partyBChoiceBox" maxWidth="Infinity" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.fillWidth="true" GridPane.hgrow="ALWAYS" 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 4 --> <!-- Row 3 -->
<Label fx:id="amountLabel" text="Amount : " GridPane.halignment="RIGHT" GridPane.rowIndex="3"/> <Label fx:id="issuerLabel" text="Issuer : " GridPane.halignment="RIGHT" GridPane.rowIndex="3"/>
<ChoiceBox fx:id="currency" GridPane.columnIndex="1" GridPane.rowIndex="3"/> <StackPane GridPane.columnIndex="1" GridPane.rowIndex="3" GridPane.columnSpan="1">
<TextField fx:id="amount" maxWidth="Infinity" GridPane.columnIndex="2" GridPane.hgrow="ALWAYS" GridPane.rowIndex="3"/> <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"/>
<!-- Row 5 --> <TextField fx:id="issueRefTextField" prefWidth="50" GridPane.columnIndex="4" GridPane.rowIndex="3"/>
<Label fx:id="issueRefLabel" text="Issue Reference : " GridPane.halignment="RIGHT" GridPane.rowIndex="4"/>
<TextField fx:id="issueRefTextField" GridPane.columnIndex="1" GridPane.rowIndex="4" GridPane.columnSpan="2"/>
<!-- Row 6 --> <!-- Row 4 -->
<Button fx:id="executeButton" text="Execute" GridPane.columnIndex="2" GridPane.halignment="RIGHT" GridPane.rowIndex="5"/> <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"/>
<Pane fx:id="mainPane" prefHeight="0.0" prefWidth="0.0"/> <!-- 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> <padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/> <Insets bottom="20.0" left="30.0" right="30.0" top="30.0"/>
</padding> </padding>
</GridPane> </GridPane>
</content>
</DialogPane>

View File

@ -1,23 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView?>
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.TextField?> <?import javafx.scene.control.TextField?>
<?import javafx.scene.image.Image?> <?import javafx.scene.layout.*?>
<?import javafx.scene.image.ImageView?> <StackPane styleClass="searchField" stylesheets="@../css/corda.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<?import javafx.scene.layout.StackPane?> <padding>
<StackPane alignment="CENTER_RIGHT" stylesheets="@../css/wallet.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1"> <Insets bottom="5"/>
<TextField fx:id="textField" promptText="Filter transactions by originator, contract type..." styleClass="search"> </padding>
<TextField fx:id="textField" promptText="Filter transactions by originator, contract type...">
<padding> <padding>
<Insets bottom="5.0" left="30.0" right="5.0"/> <Insets left="35.0" right="5.0"/>
</padding> </padding>
<StackPane.margin>
<Insets bottom="5.0" top="5.0"/>
</StackPane.margin>
</TextField> </TextField>
<ImageView fx:id="clearButton" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear"> <FontAwesomeIconView glyphName="SEARCH" StackPane.alignment="CENTER_LEFT">
<StackPane.margin> <StackPane.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/> <Insets left="10.0"/>
</StackPane.margin> </StackPane.margin>
<Image url="@../images/clear_inactive.png"/> </FontAwesomeIconView>
</ImageView> <FontAwesomeIconView fx:id="clearButton" glyphName="TIMES_CIRCLE" styleClass="search-clear" StackPane.alignment="CENTER_RIGHT">
<StackPane.margin>
<Insets right="10.0"/>
</StackPane.margin>
</FontAwesomeIconView>
</StackPane> </StackPane>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<Label styleClass="corda-logo"/>
<Separator/>
</VBox>

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<StackPane stylesheets="@../css/wallet.css" prefHeight="650" prefWidth="900" styleClass="root, no-padding" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<SplitPane dividerPositions="0.0" styleClass="no-padding, split-pane-divider">
<VBox fx:id="sidebarPane" maxWidth="200.0" minWidth="80" styleClass="sidebar" SplitPane.resizableWithParent="false"/>
<BorderPane fx:id="selectionBorderPane" maxHeight="Infinity" minWidth="400"/>
</SplitPane>
</StackPane>

View File

@ -5,9 +5,9 @@
<?import javafx.scene.control.TableView?> <?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.BorderPane?> <?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<BorderPane stylesheets="@../css/wallet.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1"> <BorderPane stylesheets="@../css/corda.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<padding> <padding>
<Insets top="5" left="5" right="10" bottom="5"/> <Insets top="5" left="5" right="5" bottom="5"/>
</padding> </padding>
<center> <center>
<TableView fx:id="transactionViewTable" VBox.vgrow="ALWAYS"> <TableView fx:id="transactionViewTable" VBox.vgrow="ALWAYS">

View File

@ -3,10 +3,13 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<BorderPane stylesheets="@../css/wallet.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1"> <BorderPane stylesheets="@../../css/corda.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
<padding> <padding>
<Insets right="10" left="5" bottom="5" top="5"/> <Insets right="5" left="5" bottom="5" top="5"/>
</padding> </padding>
<top>
<fx:include source="../SearchField.fxml"/>
</top>
<center> <center>
<SplitPane fx:id="splitPane" dividerPositions="0.5"> <SplitPane fx:id="splitPane" dividerPositions="0.5">
<VBox fx:id="leftPane" spacing="5.0"> <VBox fx:id="leftPane" spacing="5.0">

View File

@ -0,0 +1,16 @@
package net.corda.explorer.views
import org.junit.Assert.assertEquals
import org.junit.Test
class GuiUtilitiesKtTest {
@Test
fun `test to string with suffix`() {
assertEquals("10.5k", 10500.toStringWithSuffix())
assertEquals("100", 100.toStringWithSuffix())
assertEquals("5.0M", 5000000.toStringWithSuffix())
assertEquals("1.0B", 1000000000.toStringWithSuffix())
assertEquals("1.5T", 1500000000000.toStringWithSuffix())
assertEquals("1000.0T", 1000000000000000.toStringWithSuffix())
}
}