mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Explorer corda branding
This commit is contained in:
parent
37ca651ace
commit
af899a98f4
2
.idea/runConfigurations/explorer.xml
generated
2
.idea/runConfigurations/explorer.xml
generated
@ -6,7 +6,7 @@
|
||||
<option name="LIVE_STYLESHEETS" value="false" />
|
||||
<option name="DUMP_STYLESHEETS" 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="PROGRAM_PARAMETERS" value="" />
|
||||
<option name="WORKING_DIRECTORY" value="file://$PROJECT_DIR$" />
|
||||
|
@ -275,3 +275,7 @@ fun <A> ObservableList<A>.last(): ObservableValue<A?> {
|
||||
}
|
||||
}, arrayOf(this))
|
||||
}
|
||||
|
||||
fun <T : Any> ObservableList<T>.unique(): ObservableList<T> {
|
||||
return associateByAggregation { it }.getObservableValues().map { Bindings.valueAt(it, 0) }.flatten()
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
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.recordInSequence
|
||||
import net.corda.client.fxutils.map
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.node.services.Vault
|
||||
import javafx.collections.ObservableList
|
||||
import kotlinx.support.jdk8.collections.removeIf
|
||||
import rx.Observable
|
||||
|
||||
data class Diff<out T : ContractState>(
|
||||
@ -22,10 +22,10 @@ data class Diff<out T : ContractState>(
|
||||
class ContractStateModel {
|
||||
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)
|
||||
}
|
||||
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
|
||||
Diff(it.added.filterCashStateAndRefs(), it.removed)
|
||||
}
|
||||
@ -35,6 +35,7 @@ class ContractStateModel {
|
||||
observableList.addAll(statesDiff.added)
|
||||
}
|
||||
|
||||
val cash = cashStates.map { it.state.data.amount }
|
||||
|
||||
companion object {
|
||||
private fun Collection<StateAndRef<ContractState>>.filterCashStateAndRefs(): List<StateAndRef<Cash.State>> {
|
||||
|
@ -72,6 +72,9 @@ dependencies {
|
||||
|
||||
// Controls FX: more java FX components http://fxexperience.com/controlsfx/
|
||||
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) {
|
||||
|
@ -1,41 +1,98 @@
|
||||
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 jfxtras.resources.JFXtrasFontRoboto
|
||||
import net.corda.client.mock.EventGenerator
|
||||
import net.corda.client.model.Models
|
||||
import net.corda.client.model.NodeMonitorModel
|
||||
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.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.startProtocol
|
||||
import net.corda.node.services.startProtocolPermission
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.protocols.CashProtocol
|
||||
import org.apache.commons.lang.SystemUtils
|
||||
import org.controlsfx.dialog.ExceptionDialog
|
||||
import tornadofx.App
|
||||
import tornadofx.addStageIcon
|
||||
import tornadofx.find
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Main class for Explorer, you will need Tornado FX to run the explorer.
|
||||
*/
|
||||
class Main : App() {
|
||||
override val primaryView = MainWindow::class
|
||||
override val primaryView = MainView::class
|
||||
private val loginView by inject<LoginView>()
|
||||
|
||||
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 ->
|
||||
throwable.printStackTrace()
|
||||
// Show exceptions in exception dialog.
|
||||
// Show exceptions in exception dialog. Ensure this runs in application thread.
|
||||
runInFxApplicationThread {
|
||||
// [showAndWait] need to be in the FX thread
|
||||
// [showAndWait] need to be in the FX thread.
|
||||
ExceptionDialog(throwable).showAndWait()
|
||||
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 {
|
||||
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()
|
||||
|
||||
for (i in 0..10000) {
|
||||
for (i in 0..10) {
|
||||
Thread.sleep(500)
|
||||
val eventGenerator = EventGenerator(
|
||||
parties = listOf(aliceNode.nodeInfo.legalIdentity, bobNode.nodeInfo.legalIdentity),
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,15 @@
|
||||
package net.corda.explorer.views
|
||||
|
||||
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 tornadofx.gridpane
|
||||
import tornadofx.label
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (this.toDouble() < 1000) return "$this"
|
||||
val exp = (Math.log(this.toDouble()) / Math.log(1000.0)).toInt()
|
||||
return "${(this.toDouble() / Math.pow(1000.0, exp.toDouble())).format(precision)} ${"kMGTPE"[exp - 1]}"
|
||||
val scales = "kMBT"
|
||||
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)
|
||||
@ -40,3 +49,15 @@ fun runInFxApplicationThread(block: () -> Unit) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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 })
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -3,14 +3,12 @@ package net.corda.explorer.views
|
||||
import com.google.common.net.HostAndPort
|
||||
import javafx.beans.property.SimpleIntegerProperty
|
||||
import javafx.scene.control.*
|
||||
import javafx.util.converter.IntegerStringConverter
|
||||
import org.controlsfx.dialog.ExceptionDialog
|
||||
import tornadofx.View
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class LoginView : View() {
|
||||
override val root: DialogPane by fxml()
|
||||
override val root by fxml<DialogPane>()
|
||||
|
||||
private val host by fxid<TextField>()
|
||||
private val port by fxid<TextField>()
|
||||
@ -19,46 +17,42 @@ class LoginView : View() {
|
||||
private val portProperty = SimpleIntegerProperty()
|
||||
|
||||
fun login(loginFunction: (HostAndPort, String, String) -> Unit) {
|
||||
val loggedIn = Dialog<Boolean>().apply {
|
||||
val status = Dialog<LoginStatus>().apply {
|
||||
dialogPane = root
|
||||
var exception = false
|
||||
setResultConverter {
|
||||
exception = false
|
||||
when (it?.buttonData) {
|
||||
ButtonBar.ButtonData.OK_DONE -> try {
|
||||
// TODO : Run this async to avoid UI lockup.
|
||||
loginFunction(HostAndPort.fromParts(host.text, portProperty.value), username.text, password.text)
|
||||
true
|
||||
LoginStatus.loggedIn
|
||||
} catch (e: Exception) {
|
||||
ExceptionDialog(e).showAndWait()
|
||||
exception = true
|
||||
false
|
||||
// TODO : Handle this in a more user friendly way.
|
||||
ExceptionDialog(e).apply { initOwner(root.scene.window) }.showAndWait()
|
||||
LoginStatus.exception
|
||||
}
|
||||
else -> false
|
||||
else -> LoginStatus.exited
|
||||
}
|
||||
}
|
||||
setOnCloseRequest {
|
||||
if (!result && !exception) {
|
||||
when (Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to exit Corda explorer?").apply {
|
||||
}.showAndWait().get()) {
|
||||
ButtonType.OK -> exitProcess(0)
|
||||
if (result == LoginStatus.exited) {
|
||||
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) {
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.showAndWait().get()
|
||||
|
||||
if (!loggedIn) login(loginFunction)
|
||||
if (status != LoginStatus.loggedIn) login(loginFunction)
|
||||
}
|
||||
|
||||
init {
|
||||
// Restrict text field to Integer only.
|
||||
val integerFormat = Pattern.compile("-?(\\d*)").run {
|
||||
TextFormatter<Int>(IntegerStringConverter(), null) { change ->
|
||||
val newText = change.controlNewText
|
||||
if (matcher(newText).matches()) change else null
|
||||
}
|
||||
}
|
||||
port.textFormatter = integerFormat
|
||||
portProperty.bind(integerFormat.valueProperty())
|
||||
port.textFormatter = intFormatter().apply { portProperty.bind(this.valueProperty()) }
|
||||
}
|
||||
|
||||
private enum class LoginStatus {
|
||||
loggedIn, exited, exception
|
||||
}
|
||||
}
|
@ -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 })
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -2,65 +2,114 @@ package net.corda.explorer.views
|
||||
|
||||
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.collections.ObservableList
|
||||
import javafx.scene.Node
|
||||
import javafx.scene.Parent
|
||||
import javafx.scene.control.*
|
||||
import javafx.util.converter.BigDecimalStringConverter
|
||||
import javafx.stage.Window
|
||||
import net.corda.client.fxutils.map
|
||||
import net.corda.client.model.NetworkIdentityModel
|
||||
import net.corda.client.model.NodeMonitorModel
|
||||
import net.corda.client.model.observableList
|
||||
import net.corda.client.model.observableValue
|
||||
import net.corda.client.fxutils.unique
|
||||
import net.corda.client.model.*
|
||||
import net.corda.core.contracts.*
|
||||
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.node.services.messaging.CordaRPCOps
|
||||
import net.corda.node.services.messaging.startProtocol
|
||||
import net.corda.protocols.CashCommand
|
||||
import net.corda.protocols.CashProtocol
|
||||
import net.corda.protocols.CashProtocolResult
|
||||
import org.controlsfx.dialog.ExceptionDialog
|
||||
import tornadofx.View
|
||||
import tornadofx.observable
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class NewTransaction : View() {
|
||||
override val root: Parent by fxml()
|
||||
override val root by fxml<DialogPane>()
|
||||
|
||||
private val partyATextField: TextField by fxid()
|
||||
private val partyBChoiceBox: ChoiceBox<NodeInfo> by fxid()
|
||||
private val partyALabel: Label by fxid()
|
||||
private val partyBLabel: Label by fxid()
|
||||
private val amountLabel: Label by fxid()
|
||||
// Components
|
||||
private val transactionTypeCB by fxid<ChoiceBox<CashTransaction>>()
|
||||
private val partyATextField by fxid<TextField>()
|
||||
private val partyALabel by fxid<Label>()
|
||||
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 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()
|
||||
private val amount = SimpleObjectProperty<BigDecimal>()
|
||||
private val issueRef = SimpleObjectProperty<Byte>()
|
||||
|
||||
// Inject data
|
||||
private val parties: ObservableList<NodeInfo> by observableList(NetworkIdentityModel::parties)
|
||||
private val rpcProxy: ObservableValue<CordaRPCOps?> by observableValue(NodeMonitorModel::proxyObservable)
|
||||
private val myIdentity: ObservableValue<NodeInfo?> by observableValue(NetworkIdentityModel::myIdentity)
|
||||
private val notaries: ObservableList<NodeInfo> by observableList(NetworkIdentityModel::notaries)
|
||||
private 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))
|
||||
}
|
||||
|
||||
private fun resetScreen() {
|
||||
partyBChoiceBox.valueProperty().set(null)
|
||||
transactionTypeCB.valueProperty().set(null)
|
||||
currency.valueProperty().set(null)
|
||||
amount.clear()
|
||||
fun show(window: Window): Unit {
|
||||
dialog(window).showAndWait().ifPresent {
|
||||
val dialog = Alert(Alert.AlertType.INFORMATION).apply {
|
||||
headerText = null
|
||||
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 {
|
||||
@ -68,7 +117,9 @@ class NewTransaction : View() {
|
||||
val notariesNotNullBinding = Bindings.createBooleanBinding({ notaries.isNotEmpty() }, arrayOf(notaries))
|
||||
val enableProperty = myIdentity.isNotNull().and(rpcProxy.isNotNull()).and(notariesNotNullBinding)
|
||||
root.disableProperty().bind(enableProperty.not())
|
||||
transactionTypeCB.items = FXCollections.observableArrayList(CashTransaction.values().asList())
|
||||
|
||||
// Transaction Types Choice Box
|
||||
transactionTypeCB.items = CashTransaction.values().asList().observable()
|
||||
|
||||
// Party A textfield always display my identity name, not editable.
|
||||
partyATextField.isEditable = false
|
||||
@ -76,96 +127,69 @@ class NewTransaction : View() {
|
||||
partyALabel.textProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameA?.let { "$it : " } })
|
||||
partyATextField.visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameA }.isNotNull())
|
||||
|
||||
// Party B
|
||||
partyBLabel.textProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB?.let { "$it : " } })
|
||||
partyBChoiceBox.apply {
|
||||
visibleProperty().bind(transactionTypeCB.valueProperty().map { it?.partyNameB }.isNotNull())
|
||||
partyBChoiceBox.items = parties
|
||||
items = parties.sorted()
|
||||
converter = stringConverter { it?.legalIdentity?.name ?: "" }
|
||||
}
|
||||
|
||||
// BigDecimal text Formatter, restricting text box input to decimal values.
|
||||
val textFormatter = Pattern.compile("-?((\\d*)|(\\d+\\.\\d*))").run {
|
||||
TextFormatter<BigDecimal>(BigDecimalStringConverter(), null) { change ->
|
||||
val newText = change.controlNewText
|
||||
if (matcher(newText).matches()) change else null
|
||||
}
|
||||
// Issuer
|
||||
issuerLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||
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 })
|
||||
}
|
||||
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
|
||||
currency.items = FXCollections.observableList(setOf(USD, GBP, CHF).toList())
|
||||
currency.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||
amount.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||
currencyChoiceBox.items = FXCollections.observableList(setOf(USD, GBP, CHF).toList())
|
||||
currencyChoiceBox.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)
|
||||
issueRefLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||
issueRefTextField.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||
amountTextField.textFormatter = bigDecimalFormatter().apply { amount.bind(this.valueProperty()) }
|
||||
amountTextField.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull)
|
||||
|
||||
// Validate inputs.
|
||||
val formValidCondition = arrayOf(
|
||||
myIdentity.isNotNull(),
|
||||
transactionTypeCB.valueProperty().isNotNull,
|
||||
partyBChoiceBox.visibleProperty().not().or(partyBChoiceBox.valueProperty().isNotNull),
|
||||
textFormatter.valueProperty().isNotNull,
|
||||
textFormatter.valueProperty().isNotEqualTo(BigDecimal.ZERO),
|
||||
currency.valueProperty().isNotNull
|
||||
issuerChoiceBox.visibleProperty().not().or(partyBChoiceBox.valueProperty().isNotNull),
|
||||
amountTextField.textProperty().isNotEmpty,
|
||||
currencyChoiceBox.valueProperty().isNotNull
|
||||
).reduce(BooleanBinding::and)
|
||||
|
||||
// Enable execute button when form is valid.
|
||||
executeButton.disableProperty().bind(formValidCondition.not())
|
||||
executeButton.setOnAction { event ->
|
||||
// Null checks to ensure these observable values are set, execute button should be disabled if any of these value are null, this extra checks are for precaution and getting non-nullable values without using !!.
|
||||
myIdentity.value?.let { myIdentity ->
|
||||
// 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() }
|
||||
root.buttonTypes.add(executeButton)
|
||||
root.lookupButton(executeButton).disableProperty().bind(formValidCondition.not())
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,23 @@
|
||||
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.filter
|
||||
import net.corda.client.fxutils.lift
|
||||
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.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()
|
||||
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.
|
||||
// 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 {
|
||||
clearButton.setOnMouseClicked { event: MouseEvent ->
|
||||
if (event.button == MouseButton.PRIMARY) {
|
||||
textField.text = ""
|
||||
textField.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -1,9 +1,12 @@
|
||||
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.geometry.Insets
|
||||
import javafx.geometry.Pos
|
||||
import javafx.scene.Node
|
||||
import javafx.scene.Parent
|
||||
import javafx.scene.control.Label
|
||||
import javafx.scene.control.ListView
|
||||
@ -19,7 +22,6 @@ import net.corda.client.model.*
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.PublicKeyTree
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.tree
|
||||
import net.corda.core.node.NodeInfo
|
||||
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.identicon.identicon
|
||||
import net.corda.explorer.identicon.identiconToolTip
|
||||
import net.corda.explorer.model.CordaView
|
||||
import net.corda.explorer.model.ReportingCurrencyModel
|
||||
import net.corda.explorer.sign
|
||||
import net.corda.explorer.ui.setCustomCellFactory
|
||||
import tornadofx.*
|
||||
import java.util.*
|
||||
|
||||
class TransactionViewer : View() {
|
||||
class TransactionViewer : CordaView("Transactions") {
|
||||
override val root by fxml<BorderPane>()
|
||||
override val icon = FontAwesomeIcon.EXCHANGE
|
||||
|
||||
private val transactionViewTable by fxid<TableView<ViewerNode>>()
|
||||
private val matchingTransactionsLabel by fxid<Label>()
|
||||
// Inject data
|
||||
@ -42,13 +47,16 @@ class TransactionViewer : View() {
|
||||
private val reportingExchange by observableValue(ReportingCurrencyModel::reportingExchange)
|
||||
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
|
||||
* have the data.
|
||||
*/
|
||||
data class ViewerNode(
|
||||
val transaction: PartiallyResolvedTransaction,
|
||||
val transactionId: SecureHash,
|
||||
val inputContracts: List<Contract>,
|
||||
val outputContracts: List<Contract>,
|
||||
val stateMachineRunId: ObservableValue<StateMachineRunId?>,
|
||||
val stateMachineStatus: ObservableValue<out StateMachineStatus?>,
|
||||
val protocolStatus: ObservableValue<out ProtocolStatus?>,
|
||||
@ -76,7 +84,8 @@ class TransactionViewer : View() {
|
||||
}
|
||||
ViewerNode(
|
||||
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 },
|
||||
protocolStatus = stateMachineProperty { it.protocolStatus },
|
||||
stateMachineStatus = stateMachineProperty { it.stateMachineStatus },
|
||||
@ -97,19 +106,21 @@ class TransactionViewer : View() {
|
||||
}
|
||||
|
||||
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
|
||||
// Transaction table
|
||||
transactionViewTable.apply {
|
||||
items = searchField.filteredData
|
||||
column("Transaction ID", ViewerNode::transactionId).setCustomCellFactory {
|
||||
label("$it".substring(0, 16) + "...") {
|
||||
column("Transaction ID", ViewerNode::transaction).setCustomCellFactory {
|
||||
label("${it.id}") {
|
||||
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("Protocol status", ViewerNode::protocolStatus).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> {
|
||||
val (reportingCurrency, exchange) = reportingCurrencyExchange
|
||||
val publicKey = identity?.legalIdentity?.owningKey
|
||||
fun List<TransactionState<ContractState>>.sum(): Long {
|
||||
return this.map { it.data as? Cash.State }
|
||||
.filterNotNull()
|
||||
.filter { publicKey == it.owner }
|
||||
.map { exchange(it.amount.withoutIssuer()).quantity }
|
||||
.sum()
|
||||
}
|
||||
fun List<TransactionState<ContractState>>.sum() = this.map { it.data as? Cash.State }
|
||||
.filterNotNull()
|
||||
.filter { publicKey == it.owner }
|
||||
.map { exchange(it.amount.withoutIssuer()).quantity }
|
||||
.sum()
|
||||
return AmountDiff.fromLong(outputs.sum() - inputs.sum(), reportingCurrency)
|
||||
}
|
||||
|
@ -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 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.value.ObservableValue
|
||||
import javafx.collections.FXCollections
|
||||
@ -25,54 +13,38 @@ 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
|
||||
import javafx.scene.layout.Priority
|
||||
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 tornadofx.*
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
|
||||
class CashViewer : View(), CordaView {
|
||||
class CashViewer : CordaView("Cash") {
|
||||
// Inject UI elements.
|
||||
override val root: BorderPane by fxml()
|
||||
|
||||
override val icon: FontAwesomeIcon = FontAwesomeIcon.MONEY
|
||||
// View's widget.
|
||||
override val viewName = "Cash"
|
||||
override val widget: Node = vbox {
|
||||
padding = Insets(0.0, 10.0, 0.0, 0.0)
|
||||
val xAxis = NumberAxis().apply {
|
||||
//isAutoRanging = true
|
||||
isMinorTickVisible = false
|
||||
isForceZeroInRange = false
|
||||
tickLabelFormatter = stringConverter {
|
||||
Instant.ofEpochMilli(it.toLong()).atZone(TimeZone.getDefault().toZoneId()).toLocalTime().toString()
|
||||
}
|
||||
}
|
||||
val yAxis = NumberAxis().apply {
|
||||
isAutoRanging = true
|
||||
isMinorTickVisible = false
|
||||
isForceZeroInRange = false
|
||||
tickLabelFormatter = stringConverter { it.toStringWithSuffix(0) }
|
||||
}
|
||||
linechart(null, xAxis, yAxis) {
|
||||
series("USD") {
|
||||
runAsync {
|
||||
while (true) {
|
||||
Thread.sleep(1000)
|
||||
Platform.runLater {
|
||||
// Modify data in UI thread.
|
||||
if (data.size > 300) data.remove(0, 1)
|
||||
data(System.currentTimeMillis(), sumAmount.value.quantity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
createSymbols = false
|
||||
animated = false
|
||||
}
|
||||
}
|
||||
|
||||
override val widget: Node = CashWidget()
|
||||
// Left pane
|
||||
private val leftPane: VBox 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 cashViewerTableLocalCurrency: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid()
|
||||
private val cashViewerTableEquiv: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid()
|
||||
|
||||
// Right pane
|
||||
private val rightPane: VBox by fxid()
|
||||
private val totalPositionsLabel: Label by fxid()
|
||||
private val cashStatesList: ListView<StateRow> by fxid()
|
||||
private val toggleButton by fxid<Button>()
|
||||
|
||||
// Inject observables
|
||||
private val cashStates by observableList(ContractStateModel::cashStates)
|
||||
private val reportingCurrency by observableValue(SettingsModel::reportingCurrency)
|
||||
private val reportingExchange by observableValue(ReportingCurrencyModel::reportingExchange)
|
||||
private val exchangeRate: ObservableValue<ExchangeRate> by observableValue(ExchangeRateModel::exchangeRate)
|
||||
private val sumAmount = AmountBindings.sumAmountExchange(
|
||||
cashStates.map { it.state.data.amount.withoutIssuer() },
|
||||
reportingCurrency,
|
||||
exchangeRate
|
||||
)
|
||||
|
||||
private val selectedNode = cashViewerTable.singleRowSelection().map {
|
||||
when (it) {
|
||||
@ -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
|
||||
* 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.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.
|
||||
@ -280,7 +254,6 @@ class CashViewer : View(), CordaView {
|
||||
textProperty().bind(reportingCurrency.map { "$it Equiv" })
|
||||
}
|
||||
|
||||
|
||||
// Right Pane.
|
||||
totalPositionsLabel.textProperty().bind(cashStatesList.itemsProperty().map {
|
||||
val plural = if (it.size == 1) "" else "s"
|
||||
@ -303,4 +276,49 @@ class CashViewer : View(), CordaView {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
@ -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 |
@ -3,16 +3,12 @@
|
||||
<?import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<GridPane hgap="10" styleClass="expand-row" stylesheets="@../css/wallet.css" vgap="10" xmlns="http://javafx.com/javafx/8.0.112-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<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">
|
||||
<ListView fx:id="inputs"/>
|
||||
</TitledPane>
|
||||
|
||||
<Label GridPane.columnIndex="1">
|
||||
<graphic>
|
||||
<FontAwesomeIconView glyphName="PLAY" glyphSize="40" style="-fx-fill: rgb(20, 136, 204);"/>
|
||||
</graphic>
|
||||
</Label>
|
||||
<FontAwesomeIconView glyphName="PLAY" glyphSize="40" GridPane.columnIndex="1"/>
|
||||
|
||||
<TitledPane fx:id="outputPane" collapsible="false" text="Outputs" GridPane.columnIndex="2">
|
||||
<ListView fx:id="outputs" maxWidth="Infinity"/>
|
||||
|
@ -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>
|
@ -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>-->
|
||||
|
||||
<!--<!– Row 2 –>
|
||||
<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>
|
@ -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>
|
@ -2,23 +2,38 @@
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?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>
|
||||
<Insets top="10" bottom="10" left="50" right="50"/>
|
||||
<Insets bottom="30" left="50" right="50" top="10"/>
|
||||
</padding>
|
||||
<content>
|
||||
<GridPane hgap="10" vgap="10" prefWidth="400">
|
||||
<Label text="Corda Node :"/>
|
||||
<TextField fx:id="host" promptText="Host" GridPane.columnIndex="1"/>
|
||||
<TextField fx:id="port" promptText="Port" prefWidth="100" GridPane.columnIndex="2"/>
|
||||
<BorderPane>
|
||||
<top>
|
||||
<VBox alignment="CENTER" spacing="-60" maxWidth="Infinity">
|
||||
<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"/>
|
||||
<TextField fx:id="username" promptText="Username" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.columnSpan="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"/>
|
||||
|
||||
<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>
|
||||
<ButtonType fx:id="connectButton" text="Connect" buttonData="OK_DONE"/>
|
||||
<ButtonType buttonData="OK_DONE" text="Connect"/>
|
||||
</DialogPane>
|
@ -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>
|
@ -3,34 +3,43 @@
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?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">
|
||||
<!-- Row 1 -->
|
||||
<Label text="Transaction Type : " GridPane.halignment="RIGHT"/>
|
||||
<ChoiceBox fx:id="transactionTypeCB" maxWidth="Infinity" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.hgrow="ALWAYS"/>
|
||||
<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 2 -->
|
||||
<Label fx:id="partyALabel" GridPane.halignment="RIGHT" GridPane.rowIndex="1"/>
|
||||
<TextField fx:id="partyATextField" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.rowIndex="1"/>
|
||||
<!-- 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 3 -->
|
||||
<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"/>
|
||||
<!-- 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 4 -->
|
||||
<Label fx:id="amountLabel" text="Amount : " GridPane.halignment="RIGHT" GridPane.rowIndex="3"/>
|
||||
<ChoiceBox fx:id="currency" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
|
||||
<TextField fx:id="amount" maxWidth="Infinity" GridPane.columnIndex="2" GridPane.hgrow="ALWAYS" GridPane.rowIndex="3"/>
|
||||
<!-- 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"/>
|
||||
|
||||
<!-- Row 5 -->
|
||||
<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"/>
|
||||
<TextField fx:id="issueRefTextField" prefWidth="50" GridPane.columnIndex="4" GridPane.rowIndex="3"/>
|
||||
|
||||
<!-- Row 6 -->
|
||||
<Button fx:id="executeButton" text="Execute" GridPane.columnIndex="2" GridPane.halignment="RIGHT" GridPane.rowIndex="5"/>
|
||||
<!-- 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"/>
|
||||
|
||||
<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>
|
||||
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
|
||||
</padding>
|
||||
</GridPane>
|
||||
<padding>
|
||||
<Insets bottom="20.0" left="30.0" right="30.0" top="30.0"/>
|
||||
</padding>
|
||||
</GridPane>
|
||||
</content>
|
||||
</DialogPane>
|
||||
|
@ -1,23 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<StackPane alignment="CENTER_RIGHT" stylesheets="@../css/wallet.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<TextField fx:id="textField" promptText="Filter transactions by originator, contract type..." styleClass="search">
|
||||
<?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 bottom="5.0" left="30.0" right="5.0"/>
|
||||
<Insets left="35.0" right="5.0"/>
|
||||
</padding>
|
||||
<StackPane.margin>
|
||||
<Insets bottom="5.0" top="5.0"/>
|
||||
</StackPane.margin>
|
||||
</TextField>
|
||||
<ImageView fx:id="clearButton" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
|
||||
<FontAwesomeIconView glyphName="SEARCH" StackPane.alignment="CENTER_LEFT">
|
||||
<StackPane.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/>
|
||||
<Insets left="10.0"/>
|
||||
</StackPane.margin>
|
||||
<Image url="@../images/clear_inactive.png"/>
|
||||
</ImageView>
|
||||
</FontAwesomeIconView>
|
||||
<FontAwesomeIconView fx:id="clearButton" glyphName="TIMES_CIRCLE" styleClass="search-clear" StackPane.alignment="CENTER_RIGHT">
|
||||
<StackPane.margin>
|
||||
<Insets right="10.0"/>
|
||||
</StackPane.margin>
|
||||
</FontAwesomeIconView>
|
||||
</StackPane>
|
@ -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>
|
@ -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>
|
@ -5,9 +5,9 @@
|
||||
<?import javafx.scene.control.TableView?>
|
||||
<?import javafx.scene.layout.BorderPane?>
|
||||
<?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>
|
||||
<Insets top="5" left="5" right="10" bottom="5"/>
|
||||
<Insets top="5" left="5" right="5" bottom="5"/>
|
||||
</padding>
|
||||
<center>
|
||||
<TableView fx:id="transactionViewTable" VBox.vgrow="ALWAYS">
|
||||
|
@ -3,10 +3,13 @@
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<BorderPane stylesheets="@../css/wallet.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<BorderPane stylesheets="@../../css/corda.css" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<padding>
|
||||
<Insets right="10" left="5" bottom="5" top="5"/>
|
||||
<Insets right="5" left="5" bottom="5" top="5"/>
|
||||
</padding>
|
||||
<top>
|
||||
<fx:include source="../SearchField.fxml"/>
|
||||
</top>
|
||||
<center>
|
||||
<SplitPane fx:id="splitPane" dividerPositions="0.5">
|
||||
<VBox fx:id="leftPane" spacing="5.0">
|
@ -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())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user