1
.gitignore
vendored
@ -18,6 +18,7 @@ tags
|
||||
/docs/build/doctrees
|
||||
/test-utils/build
|
||||
/client/build
|
||||
/explorer/build
|
||||
|
||||
# gradle's buildSrc build/
|
||||
/buildSrc/build/
|
||||
|
3
.idea/modules.xml
generated
@ -18,6 +18,9 @@
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental.iml" group="experimental" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental_main.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental_main.iml" group="experimental" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/experimental/experimental_test.iml" filepath="$PROJECT_DIR$/.idea/modules/experimental/experimental_test.iml" group="experimental" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/explorer/explorer.iml" filepath="$PROJECT_DIR$/.idea/modules/explorer/explorer.iml" group="explorer" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/explorer/explorer_main.iml" filepath="$PROJECT_DIR$/.idea/modules/explorer/explorer_main.iml" group="explorer" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/explorer/explorer_test.iml" filepath="$PROJECT_DIR$/.idea/modules/explorer/explorer_test.iml" group="explorer" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated.iml" group="contracts/isolated" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_main.iml" group="contracts/isolated" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" filepath="$PROJECT_DIR$/.idea/modules/contracts/isolated/isolated_test.iml" group="contracts/isolated" />
|
||||
|
@ -105,9 +105,9 @@ class WalletMonitorClientTests {
|
||||
}
|
||||
),
|
||||
expect { tx: ServiceToClientEvent.Transaction ->
|
||||
require(tx.transaction.tx.inputs.isEmpty())
|
||||
require(tx.transaction.tx.outputs.size == 1)
|
||||
val signaturePubKeys = tx.transaction.sigs.map { it.by }.toSet()
|
||||
require(tx.transaction.inputs.isEmpty())
|
||||
require(tx.transaction.outputs.size == 1)
|
||||
val signaturePubKeys = tx.transaction.mustSign.toSet()
|
||||
// Only Alice signed
|
||||
require(signaturePubKeys.size == 1)
|
||||
require(signaturePubKeys.contains(aliceNode.identity.owningKey))
|
||||
@ -137,9 +137,9 @@ class WalletMonitorClientTests {
|
||||
}
|
||||
),
|
||||
expect { tx: ServiceToClientEvent.Transaction ->
|
||||
require(tx.transaction.tx.inputs.size == 1)
|
||||
require(tx.transaction.tx.outputs.size == 1)
|
||||
val signaturePubKeys = tx.transaction.sigs.map { it.by }.toSet()
|
||||
require(tx.transaction.inputs.size == 1)
|
||||
require(tx.transaction.outputs.size == 1)
|
||||
val signaturePubKeys = tx.transaction.mustSign.toSet()
|
||||
// Alice and Notary signed
|
||||
require(signaturePubKeys.size == 2)
|
||||
require(signaturePubKeys.contains(aliceNode.identity.owningKey))
|
||||
|
@ -21,7 +21,7 @@ import javafx.collections.ObservableListBase
|
||||
* The above will create a list that chooses and delegates to the appropriate filtered list based on the type of filter.
|
||||
*/
|
||||
class ChosenList<E>(
|
||||
private val chosenListObservable: ObservableValue<ObservableList<E>>
|
||||
private val chosenListObservable: ObservableValue<out ObservableList<out E>>
|
||||
): ObservableListBase<E>() {
|
||||
|
||||
private var currentList = chosenListObservable.value
|
||||
@ -48,7 +48,7 @@ class ChosenList<E>(
|
||||
}
|
||||
}
|
||||
|
||||
private fun pick(list: ObservableList<E>) {
|
||||
private fun pick(list: ObservableList<out E>) {
|
||||
currentList.removeListener(listener)
|
||||
list.addListener(listener)
|
||||
beginChange()
|
||||
|
@ -0,0 +1,117 @@
|
||||
package com.r3corda.client.fxutils
|
||||
|
||||
import javafx.beans.InvalidationListener
|
||||
import javafx.beans.value.ChangeListener
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.ListChangeListener
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.collections.transformation.TransformationList
|
||||
import org.eclipse.jetty.server.Authentication
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* [FlattenedList] flattens the passed in list of [ObservableValue]s so that changes in individual updates to the values
|
||||
* are reflected in the exposed list as expected.
|
||||
*/
|
||||
class FlattenedList<A>(val sourceList: ObservableList<out ObservableValue<out A>>) : TransformationList<A, ObservableValue<out A>>(sourceList) {
|
||||
|
||||
/**
|
||||
* We maintain an ObservableValue->index map. This is needed because we need the ObservableValue's index in order to
|
||||
* propagate a change and if the listener closure captures the index at the time of the call to
|
||||
* [ObservableValue.addListener] it will become incorrect if the indices shift around later.
|
||||
*
|
||||
* Note that because of the bookkeeping required for this map, any remove operation and any add operation that
|
||||
* inserts to the middle of the list will be O(N) as we need to scan the map and shift indices accordingly.
|
||||
*
|
||||
* Note also that we're wrapping each ObservableValue, this is required because we want to support reusing of
|
||||
* ObservableValues and we need each to have a different hash.
|
||||
*/
|
||||
class WrappedObservableValue<A>(
|
||||
val observableValue: ObservableValue<A>
|
||||
)
|
||||
val indexMap = HashMap<WrappedObservableValue<out A>, Pair<Int, ChangeListener<A>>>()
|
||||
init {
|
||||
sourceList.forEachIndexed { index, observableValue ->
|
||||
val wrappedObservableValue = WrappedObservableValue(observableValue)
|
||||
indexMap[wrappedObservableValue] = Pair(index, createListener(wrappedObservableValue))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createListener(wrapped: WrappedObservableValue<out A>): ChangeListener<A> {
|
||||
val listener = ChangeListener<A> { _observableValue, oldValue, newValue ->
|
||||
val currentIndex = indexMap[wrapped]!!.first
|
||||
beginChange()
|
||||
nextReplace(currentIndex, currentIndex + 1, listOf(oldValue))
|
||||
endChange()
|
||||
}
|
||||
wrapped.observableValue.addListener(listener)
|
||||
return listener
|
||||
}
|
||||
|
||||
override fun sourceChanged(c: ListChangeListener.Change<out ObservableValue<out A>>) {
|
||||
beginChange()
|
||||
while (c.next()) {
|
||||
if (c.wasPermutated()) {
|
||||
val from = c.from
|
||||
val to = c.to
|
||||
val permutation = IntArray(to, { c.getPermutation(it) })
|
||||
indexMap.replaceAll { _observableValue, pair -> Pair(permutation[pair.first], pair.second) }
|
||||
nextPermutation(from, to, permutation)
|
||||
} else if (c.wasUpdated()) {
|
||||
throw UnsupportedOperationException("FlattenedList doesn't support Update changes")
|
||||
} else {
|
||||
val removed = c.removed
|
||||
if (removed.size != 0) {
|
||||
// TODO this assumes that if wasAdded() == true then we are adding elements to the getFrom() position
|
||||
val removeStart = c.from
|
||||
val removeRange = c.removed.size
|
||||
val removeEnd = c.from + removeRange
|
||||
val iterator = indexMap.iterator()
|
||||
for (entry in iterator) {
|
||||
val (wrapped, pair) = entry
|
||||
val (index, listener) = pair
|
||||
if (index >= removeStart) {
|
||||
if (index < removeEnd) {
|
||||
wrapped.observableValue.removeListener(listener)
|
||||
iterator.remove()
|
||||
} else {
|
||||
// Shift indices
|
||||
entry.setValue(Pair(index - removeRange, listener))
|
||||
}
|
||||
}
|
||||
}
|
||||
nextRemove(removeStart, removed.map { it.value })
|
||||
}
|
||||
if (c.wasAdded()) {
|
||||
val addStart = c.from
|
||||
val addEnd = c.to
|
||||
val addRange = addEnd - addStart
|
||||
// If it was a push to the end we don't need to shift indices
|
||||
if (addStart != indexMap.size) {
|
||||
val iterator = indexMap.iterator()
|
||||
for (entry in iterator) {
|
||||
val (index, listener) = entry.value
|
||||
if (index >= addStart) {
|
||||
// Shift indices
|
||||
entry.setValue(Pair(index + addRange, listener))
|
||||
}
|
||||
}
|
||||
}
|
||||
c.addedSubList.forEachIndexed { sublistIndex, observableValue ->
|
||||
val wrapped = WrappedObservableValue(observableValue)
|
||||
indexMap[wrapped] = Pair(addStart + sublistIndex, createListener(wrapped))
|
||||
}
|
||||
nextAdd(addStart, addEnd)
|
||||
}
|
||||
}
|
||||
}
|
||||
endChange()
|
||||
require(sourceList.size == indexMap.size)
|
||||
}
|
||||
|
||||
override fun get(index: Int) = sourceList.get(index).value
|
||||
|
||||
override fun getSourceIndex(index: Int) = index
|
||||
|
||||
override val size: Int get() = sourceList.size
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
package com.r3corda.client.fxutils
|
||||
|
||||
import javafx.beans.binding.Bindings
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.collections.transformation.FilteredList
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
import java.util.function.Predicate
|
||||
|
||||
/**
|
||||
* Here follows utility extension functions that help reduce the visual load when developing RX code. Each function should
|
||||
* have a short accompanying example code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* val person: ObservableValue<Person> = (..)
|
||||
* val personName: ObservableValue<String> = person.map { it.name }
|
||||
*/
|
||||
fun <A, B> ObservableValue<out A>.map(function: (A) -> B): ObservableValue<B> = EasyBind.map(this, function)
|
||||
|
||||
/**
|
||||
* val dogs: ObservableList<Dog> = (..)
|
||||
* val dogOwners: ObservableList<Person> = dogs.map { it.owner }
|
||||
*/
|
||||
fun <A, B> ObservableList<out A>.map(function: (A) -> B): ObservableList<B> = EasyBind.map(this, function)
|
||||
|
||||
/**
|
||||
* val aliceHeight: ObservableValue<Long> = (..)
|
||||
* val bobHeight: ObservableValue<Long> = (..)
|
||||
* fun sumHeight(a: Long, b: Long): Long { .. }
|
||||
*
|
||||
* val aliceBobSumHeight = ::sumHeight.lift(aliceHeight, bobHeight)
|
||||
* val aliceHeightPlus2 = ::sumHeight.lift(aliceHeight, 2L.lift())
|
||||
*/
|
||||
fun <A> A.lift(): ObservableValue<A> = ReadOnlyObjectWrapper(this)
|
||||
fun <A, R> ((A) -> R).lift(
|
||||
arg0: ObservableValue<A>
|
||||
): ObservableValue<R> = EasyBind.map(arg0, this)
|
||||
fun <A, B, R> ((A, B) -> R).lift(
|
||||
arg0: ObservableValue<A>,
|
||||
arg1: ObservableValue<B>
|
||||
): ObservableValue<R> = EasyBind.combine(arg0, arg1, this)
|
||||
fun <A, B, C, R> ((A, B, C) -> R).lift(
|
||||
arg0: ObservableValue<A>,
|
||||
arg1: ObservableValue<B>,
|
||||
arg2: ObservableValue<C>
|
||||
): ObservableValue<R> = EasyBind.combine(arg0, arg1, arg2, this)
|
||||
fun <A, B, C, D, R> ((A, B, C, D) -> R).lift(
|
||||
arg0: ObservableValue<A>,
|
||||
arg1: ObservableValue<B>,
|
||||
arg2: ObservableValue<C>,
|
||||
arg3: ObservableValue<D>
|
||||
): ObservableValue<R> = EasyBind.combine(arg0, arg1, arg2, arg3, this)
|
||||
|
||||
/**
|
||||
* data class Person(val height: ObservableValue<Long>)
|
||||
* val person: ObservableValue<Person> = (..)
|
||||
* val personHeight: ObservableValue<Long> = person.bind { it.height }
|
||||
*/
|
||||
fun <A, B> ObservableValue<out A>.bind(function: (A) -> ObservableValue<out B>): ObservableValue<out B> =
|
||||
// We cast here to enforce variance, flatMap should be covariant
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
EasyBind.monadic(this).flatMap(function as (A) -> ObservableValue<B>)
|
||||
|
||||
/**
|
||||
* enum class FilterCriterion { HEIGHT, NAME }
|
||||
* val filterCriterion: ObservableValue<FilterCriterion> = (..)
|
||||
* val people: ObservableList<Person> = (..)
|
||||
* fun filterFunction(filterCriterion: FilterCriterion): (Person) -> Boolean { .. }
|
||||
*
|
||||
* val filteredPeople: ObservableList<Person> = people.filter(filterCriterion.map(filterFunction))
|
||||
*/
|
||||
fun <A> ObservableList<out A>.filter(predicate: ObservableValue<out (A) -> Boolean>): ObservableList<out A> {
|
||||
// We cast here to enforce variance, FilteredList should be covariant
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return FilteredList<A>(this as ObservableList<A>).apply {
|
||||
predicateProperty().bind(predicate.map { predicateFunction ->
|
||||
Predicate<A> { predicateFunction(it) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* val people: ObservableList<Person> = (..)
|
||||
* val concatenatedNames = people.fold("", { names, person -> names + person.name })
|
||||
* val concatenatedNames2 = people.map(Person::name).fold("", String::plus)
|
||||
*/
|
||||
fun <A, B> ObservableList<out A>.fold(initial: B, folderFunction: (B, A) -> B): ObservableValue<B> {
|
||||
return Bindings.createObjectBinding({
|
||||
var current = initial
|
||||
forEach {
|
||||
current = folderFunction(current, it)
|
||||
}
|
||||
current
|
||||
}, arrayOf(this))
|
||||
}
|
||||
|
||||
/**
|
||||
* data class Person(val height: ObservableValue<Long>)
|
||||
* val people: ObservableList<Person> = (..)
|
||||
* val heights: ObservableList<Long> = people.map(Person::height).flatten()
|
||||
*/
|
||||
fun <A> ObservableList<out ObservableValue<out A>>.flatten(): ObservableList<out A> = FlattenedList(this)
|
@ -88,12 +88,21 @@ class EventGenerator(
|
||||
)
|
||||
}
|
||||
|
||||
val exitCashGenerator =
|
||||
amountIssuedGenerator.map {
|
||||
ClientToServiceCommand.ExitCash(
|
||||
it.withoutIssuer(),
|
||||
it.token.issuer.reference
|
||||
)
|
||||
}
|
||||
|
||||
val serviceToClientEventGenerator = Generator.frequency<ServiceToClientEvent>(
|
||||
1.0 to outputStateGenerator
|
||||
)
|
||||
|
||||
val clientToServiceCommandGenerator = Generator.frequency(
|
||||
0.33 to issueCashGenerator,
|
||||
0.33 to moveCashGenerator
|
||||
0.4 to issueCashGenerator,
|
||||
0.5 to moveCashGenerator,
|
||||
0.1 to exitCashGenerator
|
||||
)
|
||||
}
|
||||
|
@ -1,20 +1,23 @@
|
||||
package com.r3corda.client.model
|
||||
|
||||
import com.r3corda.client.fxutils.foldToObservableList
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.core.contracts.ContractState
|
||||
import com.r3corda.core.contracts.StateAndRef
|
||||
import com.r3corda.core.contracts.StateRef
|
||||
import com.r3corda.client.fxutils.foldToObservableList
|
||||
import com.r3corda.node.services.monitor.ServiceToClientEvent
|
||||
import com.r3corda.node.services.monitor.StateSnapshotMessage
|
||||
import javafx.collections.ObservableList
|
||||
import kotlinx.support.jdk8.collections.removeIf
|
||||
import rx.Observable
|
||||
|
||||
class StatesDiff<out T : ContractState>(
|
||||
val added: Collection<StateAndRef<T>>,
|
||||
val removed: Collection<StateRef>
|
||||
)
|
||||
sealed class StatesModification<out T : ContractState>{
|
||||
class Diff<out T : ContractState>(
|
||||
val added: Collection<StateAndRef<T>>,
|
||||
val removed: Collection<StateRef>
|
||||
) : StatesModification<T>()
|
||||
class Reset<out T : ContractState>(val states: Collection<StateAndRef<T>>) : StatesModification<T>()
|
||||
}
|
||||
|
||||
/**
|
||||
* This model exposes the list of owned contract states.
|
||||
@ -24,16 +27,45 @@ class ContractStateModel {
|
||||
private val snapshot: Observable<StateSnapshotMessage> by observable(WalletMonitorModel::snapshot)
|
||||
private val outputStates = serviceToClient.ofType(ServiceToClientEvent.OutputState::class.java)
|
||||
|
||||
val contractStatesDiff = outputStates.map { StatesDiff(it.produced, it.consumed) }
|
||||
val contractStatesDiff: Observable<StatesModification.Diff<ContractState>> =
|
||||
outputStates.map { StatesModification.Diff(it.produced, it.consumed) }
|
||||
// We filter the diff first rather than the complete contract state list.
|
||||
// TODO wire up snapshot once it holds StateAndRefs
|
||||
val cashStatesDiff = contractStatesDiff.map {
|
||||
StatesDiff(it.added.filterIsInstance<StateAndRef<Cash.State>>(), it.removed)
|
||||
}
|
||||
val cashStatesModification: Observable<StatesModification<Cash.State>> = Observable.merge(
|
||||
arrayOf(
|
||||
contractStatesDiff.map {
|
||||
StatesModification.Diff(it.added.filterCashStateAndRefs(), it.removed)
|
||||
},
|
||||
snapshot.map {
|
||||
StatesModification.Reset(it.contractStates.filterCashStateAndRefs())
|
||||
}
|
||||
)
|
||||
)
|
||||
val cashStates: ObservableList<StateAndRef<Cash.State>> =
|
||||
cashStatesDiff.foldToObservableList(Unit) { statesDiff, _accumulator, observableList ->
|
||||
observableList.removeIf { it.ref in statesDiff.removed }
|
||||
observableList.addAll(statesDiff.added)
|
||||
cashStatesModification.foldToObservableList(Unit) { statesDiff, _accumulator, observableList ->
|
||||
when (statesDiff) {
|
||||
is StatesModification.Diff -> {
|
||||
observableList.removeIf { it.ref in statesDiff.removed }
|
||||
observableList.addAll(statesDiff.added)
|
||||
}
|
||||
is StatesModification.Reset -> {
|
||||
observableList.setAll(statesDiff.states)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private fun Collection<StateAndRef<ContractState>>.filterCashStateAndRefs(): List<StateAndRef<Cash.State>> {
|
||||
return this.map { stateAndRef ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
if (stateAndRef.state.data is Cash.State) {
|
||||
// Kotlin doesn't unify here for some reason
|
||||
stateAndRef as StateAndRef<Cash.State>
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.filterNotNull()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,13 +1,16 @@
|
||||
package com.r3corda.client.model
|
||||
|
||||
import com.r3corda.client.fxutils.foldToObservableList
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.transactions.LedgerTransaction
|
||||
import com.r3corda.node.services.monitor.ServiceToClientEvent
|
||||
import com.r3corda.node.services.monitor.TransactionBuildResult
|
||||
import com.r3corda.node.utilities.AddOrRemove
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import rx.Observable
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
@ -15,11 +18,12 @@ import java.util.UUID
|
||||
interface GatheredTransactionData {
|
||||
val fiberId: ObservableValue<Long?>
|
||||
val uuid: ObservableValue<UUID?>
|
||||
val protocolName: ObservableValue<String?>
|
||||
val protocolStatus: ObservableValue<ProtocolStatus?>
|
||||
val transaction: ObservableValue<SignedTransaction?>
|
||||
val stateMachineStatus: ObservableValue<StateMachineStatus?>
|
||||
val transaction: ObservableValue<LedgerTransaction?>
|
||||
val status: ObservableValue<TransactionCreateStatus?>
|
||||
val lastUpdate: ObservableValue<Instant>
|
||||
val allEvents: ObservableList<out ServiceToClientEvent>
|
||||
}
|
||||
|
||||
sealed class TransactionCreateStatus(val message: String?) {
|
||||
@ -28,21 +32,24 @@ sealed class TransactionCreateStatus(val message: String?) {
|
||||
override fun toString(): String = message ?: javaClass.simpleName
|
||||
}
|
||||
|
||||
sealed class ProtocolStatus(val status: String?) {
|
||||
object Added: ProtocolStatus(null)
|
||||
object Removed: ProtocolStatus(null)
|
||||
class InProgress(status: String): ProtocolStatus(status)
|
||||
override fun toString(): String = status ?: javaClass.simpleName
|
||||
data class ProtocolStatus(
|
||||
val status: String
|
||||
)
|
||||
sealed class StateMachineStatus(val stateMachineName: String) {
|
||||
class Added(stateMachineName: String): StateMachineStatus(stateMachineName)
|
||||
class Removed(stateMachineName: String): StateMachineStatus(stateMachineName)
|
||||
override fun toString(): String = "${javaClass.simpleName}($stateMachineName)"
|
||||
}
|
||||
|
||||
data class GatheredTransactionDataWritable(
|
||||
override val fiberId: SimpleObjectProperty<Long?> = SimpleObjectProperty(null),
|
||||
override val uuid: SimpleObjectProperty<UUID?> = SimpleObjectProperty(null),
|
||||
override val protocolName: SimpleObjectProperty<String?> = SimpleObjectProperty(null),
|
||||
override val stateMachineStatus: SimpleObjectProperty<StateMachineStatus?> = SimpleObjectProperty(null),
|
||||
override val protocolStatus: SimpleObjectProperty<ProtocolStatus?> = SimpleObjectProperty(null),
|
||||
override val transaction: SimpleObjectProperty<SignedTransaction?> = SimpleObjectProperty(null),
|
||||
override val transaction: SimpleObjectProperty<LedgerTransaction?> = SimpleObjectProperty(null),
|
||||
override val status: SimpleObjectProperty<TransactionCreateStatus?> = SimpleObjectProperty(null),
|
||||
override val lastUpdate: SimpleObjectProperty<Instant>
|
||||
override val lastUpdate: SimpleObjectProperty<Instant>,
|
||||
override val allEvents: ObservableList<ServiceToClientEvent> = FXCollections.observableArrayList()
|
||||
) : GatheredTransactionData
|
||||
|
||||
/**
|
||||
@ -54,7 +61,7 @@ class GatheredTransactionDataModel {
|
||||
|
||||
/**
|
||||
* Aggregation of updates to transactions. We use the observable list as the only container and do linear search for
|
||||
* matching transactions because we have two keys(fiber ID and UUID) and this way it's easier to avoid syncing issues.
|
||||
* matching transactions because we have three keys(fiber ID, UUID, tx id) and this way it's easier to avoid syncing issues.
|
||||
*
|
||||
* The Fiber ID is used to identify events that relate to the same transaction server-side, whereas the UUID is
|
||||
* generated on the UI and is used to identify events with the UI action that triggered them. Currently a UUID is
|
||||
@ -64,46 +71,49 @@ class GatheredTransactionDataModel {
|
||||
* (Note that a transaction may be mapped by one or both)
|
||||
* TODO: Expose a writable stream to combine [serviceToClient] with to allow recording of transactions made locally(UUID)
|
||||
*/
|
||||
val gatheredGatheredTransactionDataList: ObservableList<out GatheredTransactionData> =
|
||||
val gatheredTransactionDataList: ObservableList<out GatheredTransactionData> =
|
||||
serviceToClient.foldToObservableList<ServiceToClientEvent, GatheredTransactionDataWritable, Unit>(
|
||||
initialAccumulator = Unit,
|
||||
folderFun = { serviceToClientEvent, _unit, transactionStates ->
|
||||
return@foldToObservableList when (serviceToClientEvent) {
|
||||
is ServiceToClientEvent.Transaction -> {
|
||||
// TODO handle this once we have some id to associate the tx with
|
||||
newTransactionIdTransactionStateOrModify(transactionStates, serviceToClientEvent,
|
||||
transaction = serviceToClientEvent.transaction,
|
||||
tweak = {}
|
||||
)
|
||||
}
|
||||
is ServiceToClientEvent.OutputState -> {}
|
||||
is ServiceToClientEvent.StateMachine -> {
|
||||
newFiberIdTransactionStateOrModify(transactionStates,
|
||||
newFiberIdTransactionStateOrModify(transactionStates, serviceToClientEvent,
|
||||
fiberId = serviceToClientEvent.fiberId,
|
||||
lastUpdate = serviceToClientEvent.time,
|
||||
tweak = {
|
||||
protocolName.set(serviceToClientEvent.label)
|
||||
protocolStatus.set(when (serviceToClientEvent.addOrRemove) {
|
||||
AddOrRemove.ADD -> ProtocolStatus.Added
|
||||
AddOrRemove.REMOVE -> ProtocolStatus.Removed
|
||||
stateMachineStatus.set(when (serviceToClientEvent.addOrRemove) {
|
||||
AddOrRemove.ADD -> StateMachineStatus.Added(serviceToClientEvent.label)
|
||||
AddOrRemove.REMOVE -> StateMachineStatus.Removed(serviceToClientEvent.label)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
is ServiceToClientEvent.Progress -> {
|
||||
newFiberIdTransactionStateOrModify(transactionStates,
|
||||
newFiberIdTransactionStateOrModify(transactionStates, serviceToClientEvent,
|
||||
fiberId = serviceToClientEvent.fiberId,
|
||||
lastUpdate = serviceToClientEvent.time,
|
||||
tweak = {
|
||||
protocolStatus.set(ProtocolStatus.InProgress(serviceToClientEvent.message))
|
||||
protocolStatus.set(ProtocolStatus(serviceToClientEvent.message))
|
||||
}
|
||||
)
|
||||
}
|
||||
is ServiceToClientEvent.TransactionBuild -> {
|
||||
val state = serviceToClientEvent.state
|
||||
newUuidTransactionStateOrModify(transactionStates,
|
||||
newUuidTransactionStateOrModify(transactionStates, serviceToClientEvent,
|
||||
uuid = serviceToClientEvent.id,
|
||||
fiberId = when (state) {
|
||||
is TransactionBuildResult.ProtocolStarted -> state.fiberId
|
||||
is TransactionBuildResult.Failed -> null
|
||||
},
|
||||
lastUpdate = serviceToClientEvent.time,
|
||||
transactionId = when (state) {
|
||||
is TransactionBuildResult.ProtocolStarted -> state.transaction?.id
|
||||
is TransactionBuildResult.Failed -> null
|
||||
},
|
||||
tweak = {
|
||||
return@newUuidTransactionStateOrModify when (state) {
|
||||
is TransactionBuildResult.ProtocolStarted -> {
|
||||
@ -122,50 +132,84 @@ class GatheredTransactionDataModel {
|
||||
)
|
||||
|
||||
companion object {
|
||||
private fun newFiberIdTransactionStateOrModify(
|
||||
|
||||
private fun newTransactionIdTransactionStateOrModify(
|
||||
transactionStates: ObservableList<GatheredTransactionDataWritable>,
|
||||
fiberId: Long,
|
||||
lastUpdate: Instant,
|
||||
event: ServiceToClientEvent,
|
||||
transaction: LedgerTransaction,
|
||||
tweak: GatheredTransactionDataWritable.() -> Unit
|
||||
) {
|
||||
val index = transactionStates.indexOfFirst { it.fiberId.value == fiberId }
|
||||
if (index < 0) {
|
||||
val index = transactionStates.indexOfFirst { transaction.id == it.transaction.value?.id }
|
||||
val state = if (index < 0) {
|
||||
val newState = GatheredTransactionDataWritable(
|
||||
fiberId = SimpleObjectProperty(fiberId),
|
||||
lastUpdate = SimpleObjectProperty(lastUpdate)
|
||||
transaction = SimpleObjectProperty(transaction),
|
||||
lastUpdate = SimpleObjectProperty(event.time)
|
||||
)
|
||||
tweak(newState)
|
||||
transactionStates.add(newState)
|
||||
newState
|
||||
} else {
|
||||
val existingState = transactionStates[index]
|
||||
existingState.lastUpdate.set(lastUpdate)
|
||||
existingState.lastUpdate.set(event.time)
|
||||
tweak(existingState)
|
||||
existingState
|
||||
}
|
||||
state.allEvents.add(event)
|
||||
}
|
||||
|
||||
private fun newFiberIdTransactionStateOrModify(
|
||||
transactionStates: ObservableList<GatheredTransactionDataWritable>,
|
||||
event: ServiceToClientEvent,
|
||||
fiberId: Long,
|
||||
tweak: GatheredTransactionDataWritable.() -> Unit
|
||||
) {
|
||||
val index = transactionStates.indexOfFirst { it.fiberId.value == fiberId }
|
||||
val state = if (index < 0) {
|
||||
val newState = GatheredTransactionDataWritable(
|
||||
fiberId = SimpleObjectProperty(fiberId),
|
||||
lastUpdate = SimpleObjectProperty(event.time)
|
||||
)
|
||||
tweak(newState)
|
||||
transactionStates.add(newState)
|
||||
newState
|
||||
} else {
|
||||
val existingState = transactionStates[index]
|
||||
existingState.lastUpdate.set(event.time)
|
||||
tweak(existingState)
|
||||
existingState
|
||||
}
|
||||
state.allEvents.add(event)
|
||||
}
|
||||
|
||||
private fun newUuidTransactionStateOrModify(
|
||||
transactionStates: ObservableList<GatheredTransactionDataWritable>,
|
||||
event: ServiceToClientEvent,
|
||||
uuid: UUID,
|
||||
fiberId: Long?,
|
||||
lastUpdate: Instant,
|
||||
transactionId: SecureHash?,
|
||||
tweak: GatheredTransactionDataWritable.() -> Unit
|
||||
) {
|
||||
val index = transactionStates.indexOfFirst {
|
||||
it.uuid.value == uuid || (fiberId != null && it.fiberId.value == fiberId)
|
||||
it.uuid.value == uuid ||
|
||||
(fiberId != null && it.fiberId.value == fiberId) ||
|
||||
(transactionId != null && it.transaction.value?.id == transactionId)
|
||||
}
|
||||
if (index < 0) {
|
||||
val state = if (index < 0) {
|
||||
val newState = GatheredTransactionDataWritable(
|
||||
uuid = SimpleObjectProperty(uuid),
|
||||
fiberId = SimpleObjectProperty(fiberId),
|
||||
lastUpdate = SimpleObjectProperty(lastUpdate)
|
||||
lastUpdate = SimpleObjectProperty(event.time)
|
||||
)
|
||||
tweak(newState)
|
||||
transactionStates.add(newState)
|
||||
newState
|
||||
} else {
|
||||
val existingState = transactionStates[index]
|
||||
existingState.lastUpdate.set(lastUpdate)
|
||||
existingState.lastUpdate.set(event.time)
|
||||
tweak(existingState)
|
||||
existingState
|
||||
}
|
||||
state.allEvents.add(event)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,28 +8,31 @@ import kotlin.test.fail
|
||||
class AggregatedListTest {
|
||||
|
||||
var sourceList = FXCollections.observableArrayList<Int>()
|
||||
var aggregatedList = AggregatedList(sourceList, { it % 3 }) { mod3, group -> Pair(mod3, group) }
|
||||
var replayedList = ReplayedList(aggregatedList)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
sourceList = FXCollections.observableArrayList<Int>()
|
||||
aggregatedList = AggregatedList(sourceList, { it % 3 }) { mod3, group -> Pair(mod3, group) }
|
||||
replayedList = ReplayedList(aggregatedList)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addWorks() {
|
||||
val aggregatedList = AggregatedList(sourceList, { it % 3 }) { mod3, group -> Pair(mod3, group) }
|
||||
require(aggregatedList.size == 0) { "Aggregation is empty is source list is" }
|
||||
require(replayedList.size == 0) { "Aggregation is empty is source list is" }
|
||||
|
||||
sourceList.add(9)
|
||||
require(aggregatedList.size == 1) { "Aggregation list has one element if one was added to source list" }
|
||||
require(aggregatedList[0]!!.first == 0)
|
||||
require(replayedList.size == 1) { "Aggregation list has one element if one was added to source list" }
|
||||
require(replayedList[0]!!.first == 0)
|
||||
|
||||
sourceList.add(8)
|
||||
require(aggregatedList.size == 2) { "Aggregation list has two elements if two were added to source list with different keys" }
|
||||
require(replayedList.size == 2) { "Aggregation list has two elements if two were added to source list with different keys" }
|
||||
|
||||
sourceList.add(6)
|
||||
require(aggregatedList.size == 2) { "Aggregation list's size doesn't change if element with existing key is added" }
|
||||
require(replayedList.size == 2) { "Aggregation list's size doesn't change if element with existing key is added" }
|
||||
|
||||
aggregatedList.forEach {
|
||||
replayedList.forEach {
|
||||
when (it.first) {
|
||||
0 -> require(it.second.toSet() == setOf(6, 9))
|
||||
2 -> require(it.second.size == 1)
|
||||
@ -40,11 +43,10 @@ class AggregatedListTest {
|
||||
|
||||
@Test
|
||||
fun removeWorks() {
|
||||
val aggregatedList = AggregatedList(sourceList, { it % 3 }) { mod3, group -> Pair(mod3, group) }
|
||||
sourceList.addAll(0, 1, 2, 3, 4)
|
||||
|
||||
require(aggregatedList.size == 3)
|
||||
aggregatedList.forEach {
|
||||
require(replayedList.size == 3)
|
||||
replayedList.forEach {
|
||||
when (it.first) {
|
||||
0 -> require(it.second.toSet() == setOf(0, 3))
|
||||
1 -> require(it.second.toSet() == setOf(1, 4))
|
||||
@ -54,8 +56,8 @@ class AggregatedListTest {
|
||||
}
|
||||
|
||||
sourceList.remove(4)
|
||||
require(aggregatedList.size == 3)
|
||||
aggregatedList.forEach {
|
||||
require(replayedList.size == 3)
|
||||
replayedList.forEach {
|
||||
when (it.first) {
|
||||
0 -> require(it.second.toSet() == setOf(0, 3))
|
||||
1 -> require(it.second.toSet() == setOf(1))
|
||||
@ -65,8 +67,8 @@ class AggregatedListTest {
|
||||
}
|
||||
|
||||
sourceList.remove(2, 4)
|
||||
require(aggregatedList.size == 2)
|
||||
aggregatedList.forEach {
|
||||
require(replayedList.size == 2)
|
||||
replayedList.forEach {
|
||||
when (it.first) {
|
||||
0 -> require(it.second.toSet() == setOf(0))
|
||||
1 -> require(it.second.toSet() == setOf(1))
|
||||
@ -75,7 +77,7 @@ class AggregatedListTest {
|
||||
}
|
||||
|
||||
sourceList.removeAll(0, 1)
|
||||
require(aggregatedList.size == 0)
|
||||
require(replayedList.size == 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,113 @@
|
||||
package com.r3corda.client.fxutils
|
||||
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import javafx.collections.FXCollections
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class FlattenedListTest {
|
||||
|
||||
var sourceList = FXCollections.observableArrayList(SimpleObjectProperty(1234))
|
||||
var flattenedList = FlattenedList(sourceList)
|
||||
var replayedList = ReplayedList(flattenedList)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
sourceList = FXCollections.observableArrayList(SimpleObjectProperty(1234))
|
||||
flattenedList = FlattenedList(sourceList)
|
||||
replayedList = ReplayedList(flattenedList)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addWorks() {
|
||||
require(replayedList.size == 1)
|
||||
require(replayedList[0] == 1234)
|
||||
|
||||
sourceList.add(SimpleObjectProperty(12))
|
||||
require(replayedList.size == 2)
|
||||
require(replayedList[0] == 1234)
|
||||
require(replayedList[1] == 12)
|
||||
|
||||
sourceList.add(SimpleObjectProperty(34))
|
||||
require(replayedList.size == 3)
|
||||
require(replayedList[0] == 1234)
|
||||
require(replayedList[1] == 12)
|
||||
require(replayedList[2] == 34)
|
||||
|
||||
sourceList.add(0, SimpleObjectProperty(56))
|
||||
require(replayedList.size == 4)
|
||||
require(replayedList[0] == 56)
|
||||
require(replayedList[1] == 1234)
|
||||
require(replayedList[2] == 12)
|
||||
require(replayedList[3] == 34)
|
||||
|
||||
sourceList.addAll(2, listOf(SimpleObjectProperty(78), SimpleObjectProperty(910)))
|
||||
require(replayedList.size == 6)
|
||||
require(replayedList[0] == 56)
|
||||
require(replayedList[1] == 1234)
|
||||
require(replayedList[2] == 78)
|
||||
require(replayedList[3] == 910)
|
||||
require(replayedList[4] == 12)
|
||||
require(replayedList[5] == 34)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeWorks() {
|
||||
val firstRemoved = sourceList.removeAt(0)
|
||||
require(firstRemoved.get() == 1234)
|
||||
require(replayedList.size == 0)
|
||||
firstRemoved.set(123)
|
||||
|
||||
sourceList.add(SimpleObjectProperty(12))
|
||||
sourceList.add(SimpleObjectProperty(34))
|
||||
sourceList.add(SimpleObjectProperty(56))
|
||||
require(replayedList.size == 3)
|
||||
val secondRemoved = sourceList.removeAt(1)
|
||||
require(secondRemoved.get() == 34)
|
||||
require(replayedList.size == 2)
|
||||
require(replayedList[0] == 12)
|
||||
require(replayedList[1] == 56)
|
||||
secondRemoved.set(123)
|
||||
|
||||
sourceList.clear()
|
||||
require(replayedList.size == 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updatingObservableWorks() {
|
||||
require(replayedList[0] == 1234)
|
||||
sourceList[0].set(4321)
|
||||
require(replayedList[0] == 4321)
|
||||
|
||||
sourceList.add(0, SimpleObjectProperty(12))
|
||||
sourceList[1].set(8765)
|
||||
require(replayedList[0] == 12)
|
||||
require(replayedList[1] == 8765)
|
||||
|
||||
sourceList[0].set(34)
|
||||
require(replayedList[0] == 34)
|
||||
require(replayedList[1] == 8765)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reusingObservableWorks() {
|
||||
val observable = SimpleObjectProperty(12)
|
||||
sourceList.add(observable)
|
||||
sourceList.add(observable)
|
||||
require(replayedList.size == 3)
|
||||
require(replayedList[0] == 1234)
|
||||
require(replayedList[1] == 12)
|
||||
require(replayedList[2] == 12)
|
||||
|
||||
observable.set(34)
|
||||
require(replayedList.size == 3)
|
||||
require(replayedList[0] == 1234)
|
||||
require(replayedList[1] == 34)
|
||||
require(replayedList[2] == 34)
|
||||
|
||||
sourceList.removeAt(1)
|
||||
require(replayedList.size == 2)
|
||||
require(replayedList[0] == 1234)
|
||||
require(replayedList[1] == 34)
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package com.r3corda.client.fxutils
|
||||
|
||||
import javafx.collections.ListChangeListener
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.collections.transformation.TransformationList
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* This list type just replays changes propagated from the underlying source list. Used for testing changes.
|
||||
*/
|
||||
class ReplayedList<A>(sourceList: ObservableList<A>) : TransformationList<A, A>(sourceList) {
|
||||
|
||||
val replayedList = ArrayList<A>(sourceList)
|
||||
|
||||
override val size: Int get() = replayedList.size
|
||||
|
||||
override fun sourceChanged(c: ListChangeListener.Change<out A>) {
|
||||
|
||||
beginChange()
|
||||
while (c.next()) {
|
||||
if (c.wasPermutated()) {
|
||||
val from = c.from
|
||||
val to = c.to
|
||||
val permutation = IntArray(to, { c.getPermutation(it) })
|
||||
val permutedSubList = ArrayList<A>(to - from)
|
||||
for (i in 0 .. (to - from - 1)) {
|
||||
permutedSubList.add(replayedList[permutation[from + i]])
|
||||
}
|
||||
permutedSubList.forEachIndexed { i, element ->
|
||||
replayedList[from + i] = element
|
||||
}
|
||||
nextPermutation(from, to, permutation)
|
||||
} else if (c.wasUpdated()) {
|
||||
for (i in c.from .. c.to - 1) {
|
||||
replayedList[i] = c.list[i]
|
||||
nextUpdate(i)
|
||||
}
|
||||
} else {
|
||||
if (c.wasRemoved()) {
|
||||
// TODO this assumes that if wasAdded() == true then we are adding elements to the getFrom() position
|
||||
val removePosition = c.from
|
||||
for (i in 0 .. c.removedSize - 1) {
|
||||
replayedList.removeAt(removePosition)
|
||||
}
|
||||
nextRemove(c.from, c.removed)
|
||||
}
|
||||
if (c.wasAdded()) {
|
||||
val addStart = c.from
|
||||
val addEnd = c.to
|
||||
for (i in addStart .. addEnd - 1) {
|
||||
replayedList.add(i, c.list[i])
|
||||
}
|
||||
nextAdd(addStart, addEnd)
|
||||
}
|
||||
}
|
||||
}
|
||||
endChange()
|
||||
}
|
||||
|
||||
override fun getSourceIndex(index: Int) = index
|
||||
|
||||
override fun get(index: Int) = replayedList[index]
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
package com.r3corda.client.fxutils
|
||||
|
||||
import javafx.collections.FXCollections
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class ReplayedListTest {
|
||||
|
||||
var sourceList = FXCollections.observableArrayList(1234)
|
||||
var replayedList = ReplayedList(sourceList)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
sourceList = FXCollections.observableArrayList(1234)
|
||||
replayedList = ReplayedList(sourceList)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addWorks() {
|
||||
require(replayedList.size == 1)
|
||||
require(replayedList[0] == 1234)
|
||||
|
||||
sourceList.add(12)
|
||||
require(replayedList.size == 2)
|
||||
require(replayedList[0] == 1234)
|
||||
require(replayedList[1] == 12)
|
||||
|
||||
sourceList.add(34)
|
||||
require(replayedList.size == 3)
|
||||
require(replayedList[0] == 1234)
|
||||
require(replayedList[1] == 12)
|
||||
require(replayedList[2] == 34)
|
||||
|
||||
sourceList.add(0, 56)
|
||||
require(replayedList.size == 4)
|
||||
require(replayedList[0] == 56)
|
||||
require(replayedList[1] == 1234)
|
||||
require(replayedList[2] == 12)
|
||||
require(replayedList[3] == 34)
|
||||
|
||||
sourceList.addAll(2, listOf(78, 910))
|
||||
require(replayedList.size == 6)
|
||||
require(replayedList[0] == 56)
|
||||
require(replayedList[1] == 1234)
|
||||
require(replayedList[2] == 78)
|
||||
require(replayedList[3] == 910)
|
||||
require(replayedList[4] == 12)
|
||||
require(replayedList[5] == 34)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeWorks() {
|
||||
val firstRemoved = sourceList.removeAt(0)
|
||||
require(firstRemoved == 1234)
|
||||
require(replayedList.size == 0)
|
||||
|
||||
sourceList.add(12)
|
||||
sourceList.add(34)
|
||||
sourceList.add(56)
|
||||
require(replayedList.size == 3)
|
||||
val secondRemoved = sourceList.removeAt(1)
|
||||
require(secondRemoved == 34)
|
||||
require(replayedList.size == 2)
|
||||
require(replayedList[0] == 12)
|
||||
require(replayedList[1] == 56)
|
||||
|
||||
sourceList.clear()
|
||||
require(replayedList.size == 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateWorks() {
|
||||
require(replayedList[0] == 1234)
|
||||
sourceList[0] = 4321
|
||||
require(replayedList[0] == 4321)
|
||||
|
||||
sourceList.add(0, 12)
|
||||
sourceList[1] = 8765
|
||||
require(replayedList[0] == 12)
|
||||
require(replayedList[1] == 8765)
|
||||
|
||||
sourceList[0] = 34
|
||||
require(replayedList[0] == 34)
|
||||
require(replayedList[1] == 8765)
|
||||
}
|
||||
}
|
77
explorer/build.gradle
Normal file
@ -0,0 +1,77 @@
|
||||
group 'com.r3corda'
|
||||
version '1.0-SNAPSHOT'
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.0.3'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url 'https://dl.bintray.com/kotlin/exposed'
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'application'
|
||||
|
||||
sourceCompatibility = 1.8
|
||||
|
||||
applicationDefaultJvmArgs = ["-javaagent:${rootProject.configurations.quasar.singleFile}"]
|
||||
mainClassName = 'com.r3corda.explorer.Main'
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
resources {
|
||||
srcDir "../config/dev"
|
||||
}
|
||||
}
|
||||
test {
|
||||
resources {
|
||||
srcDir "../config/test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
testCompile group: 'junit', name: 'junit', version: '4.11'
|
||||
|
||||
// TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's.
|
||||
compile 'no.tornado:tornadofx:1.5.1'
|
||||
|
||||
// Corda Core: Data structures and basic types needed to work with Corda.
|
||||
compile project(':core')
|
||||
compile project(':client')
|
||||
compile project(':node')
|
||||
compile project(':contracts')
|
||||
|
||||
// FontAwesomeFX: The "FontAwesome" icon library.
|
||||
compile 'de.jensd:fontawesomefx-fontawesome:4.6.1-2'
|
||||
|
||||
// ReactFX: Functional reactive UI programming.
|
||||
compile 'org.reactfx:reactfx:2.0-M5'
|
||||
compile 'org.fxmisc.easybind:easybind:1.0.3'
|
||||
|
||||
// JFXtras: useful widgets including a calendar control.
|
||||
compile 'org.jfxtras:jfxtras-agenda:8.0-r5'
|
||||
compile 'org.jfxtras:jfxtras-font-roboto:8.0-r5'
|
||||
|
||||
// Humanize: formatting
|
||||
compile 'com.github.mfornos:humanize-icu:1.2.2'
|
||||
}
|
26
explorer/src/main/kotlin/com/r3corda/explorer/AmountDiff.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package com.r3corda.explorer
|
||||
|
||||
import com.r3corda.core.contracts.Amount
|
||||
|
||||
enum class Positivity {
|
||||
Positive,
|
||||
Negative
|
||||
}
|
||||
|
||||
val Positivity.sign: String get() = when (this) {
|
||||
Positivity.Positive -> ""
|
||||
Positivity.Negative -> "-"
|
||||
}
|
||||
|
||||
data class AmountDiff<T>(
|
||||
val positivity: Positivity,
|
||||
val amount: Amount<T>
|
||||
) {
|
||||
companion object {
|
||||
fun <T> fromLong(quantity: Long, token: T) =
|
||||
AmountDiff(
|
||||
positivity = if (quantity < 0) Positivity.Negative else Positivity.Positive,
|
||||
amount = Amount(Math.abs(quantity), token)
|
||||
)
|
||||
}
|
||||
}
|
82
explorer/src/main/kotlin/com/r3corda/explorer/Main.kt
Normal file
@ -0,0 +1,82 @@
|
||||
package com.r3corda.explorer
|
||||
|
||||
import com.r3corda.client.WalletMonitorClient
|
||||
import com.r3corda.client.mock.EventGenerator
|
||||
import com.r3corda.client.mock.Generator
|
||||
import com.r3corda.client.mock.oneOf
|
||||
import com.r3corda.client.model.Models
|
||||
import com.r3corda.client.model.WalletMonitorModel
|
||||
import com.r3corda.client.model.observer
|
||||
import com.r3corda.core.contracts.ClientToServiceCommand
|
||||
import com.r3corda.explorer.model.IdentityModel
|
||||
import com.r3corda.node.driver.PortAllocation
|
||||
import com.r3corda.node.driver.driver
|
||||
import com.r3corda.node.driver.startClient
|
||||
import com.r3corda.node.services.monitor.ServiceToClientEvent
|
||||
import com.r3corda.node.services.transactions.SimpleNotaryService
|
||||
import javafx.stage.Stage
|
||||
import rx.Observer
|
||||
import rx.subjects.PublishSubject
|
||||
import tornadofx.App
|
||||
import java.util.*
|
||||
|
||||
class Main : App() {
|
||||
override val primaryView = MainWindow::class
|
||||
val aliceOutStream: Observer<ClientToServiceCommand> by observer(WalletMonitorModel::clientToService)
|
||||
|
||||
override fun start(stage: Stage) {
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||
throwable.printStackTrace()
|
||||
System.exit(1)
|
||||
}
|
||||
|
||||
super.start(stage)
|
||||
|
||||
// start the driver on another thread
|
||||
// TODO Change this to connecting to an actual node (specified on cli/in a config) once we're happy with the code
|
||||
Thread({
|
||||
|
||||
val portAllocation = PortAllocation.Incremental(20000)
|
||||
driver(portAllocation = portAllocation) {
|
||||
|
||||
val aliceNodeFuture = startNode("Alice")
|
||||
val bobNodeFuture = startNode("Bob")
|
||||
val notaryNodeFuture = startNode("Notary", advertisedServices = setOf(SimpleNotaryService.Type))
|
||||
|
||||
val aliceNode = aliceNodeFuture.get()
|
||||
val bobNode = bobNodeFuture.get()
|
||||
val notaryNode = notaryNodeFuture.get()
|
||||
|
||||
val aliceClient = startClient(aliceNode).get()
|
||||
|
||||
Models.get<IdentityModel>(Main::class).myIdentity.set(aliceNode.identity)
|
||||
Models.get<WalletMonitorModel>(Main::class).register(aliceClient, aliceNode)
|
||||
|
||||
val bobInStream = PublishSubject.create<ServiceToClientEvent>()
|
||||
val bobOutStream = PublishSubject.create<ClientToServiceCommand>()
|
||||
|
||||
val bobClient = startClient(bobNode).get()
|
||||
val bobMonitorClient = WalletMonitorClient(bobClient, bobNode, bobOutStream, bobInStream, PublishSubject.create())
|
||||
assert(bobMonitorClient.register().get())
|
||||
|
||||
for (i in 0 .. 10000) {
|
||||
Thread.sleep(500)
|
||||
|
||||
val eventGenerator = EventGenerator(
|
||||
parties = listOf(aliceNode.identity, bobNode.identity),
|
||||
notary = notaryNode.identity
|
||||
)
|
||||
|
||||
eventGenerator.clientToServiceCommandGenerator.combine(Generator.oneOf(listOf(aliceOutStream, bobOutStream))) {
|
||||
command, stream -> stream.onNext(command)
|
||||
}.generate(Random())
|
||||
}
|
||||
|
||||
waitForAllNodesToFinish()
|
||||
}
|
||||
|
||||
}).start()
|
||||
}
|
||||
}
|
||||
|
26
explorer/src/main/kotlin/com/r3corda/explorer/MainWindow.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package com.r3corda.explorer
|
||||
|
||||
import com.r3corda.explorer.views.TopLevel
|
||||
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory
|
||||
import jfxtras.resources.JFXtrasFontRoboto
|
||||
import tornadofx.*
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
init {
|
||||
// Do this first before creating the notification bar, so it can autosize itself properly.
|
||||
loadFontsAndStyles()
|
||||
}
|
||||
|
||||
private fun loadFontsAndStyles() {
|
||||
JFXtrasFontRoboto.loadAll()
|
||||
importStylesheet("/com/r3corda/explorer/css/wallet.css")
|
||||
FontAwesomeIconFactory.get() // Force initialisation.
|
||||
root.styleClass += "root"
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.r3corda.explorer.formatters
|
||||
|
||||
import com.r3corda.core.contracts.Amount
|
||||
import java.util.Currency
|
||||
|
||||
/**
|
||||
* A note on formatting: Currently we don't have any fancy locale/use-case-specific formatting of amounts. This is a
|
||||
* non-trivial problem that requires substantial work.
|
||||
* Libraries to evaluate: IBM ICU currency library, github.com/mfornos/humanize, JSR 354 ref. implementation
|
||||
*/
|
||||
|
||||
object AmountFormatter {
|
||||
// TODO replace this once we settled on how we do formatting
|
||||
val boring = object : Formatter<Amount<Currency>> {
|
||||
override fun format(value: Amount<Currency>) = "${value.quantity} ${value.token}"
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package com.r3corda.explorer.formatters
|
||||
|
||||
|
||||
interface Formatter<in T> {
|
||||
fun format(value: T): String
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package com.r3corda.explorer.formatters
|
||||
|
||||
object NumberFormatter {
|
||||
// TODO replace this once we settled on how we do formatting
|
||||
val boring = object : Formatter<Any> {
|
||||
override fun format(value: Any) = value.toString()
|
||||
}
|
||||
|
||||
val boringLong: Formatter<Long> = boring
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.r3corda.explorer.model
|
||||
|
||||
import com.r3corda.core.crypto.Party
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
|
||||
|
||||
class IdentityModel {
|
||||
val myIdentity = SimpleObjectProperty<Party>()
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package com.r3corda.explorer.model
|
||||
|
||||
import com.r3corda.core.contracts.Amount
|
||||
import com.r3corda.client.fxutils.AmountBindings
|
||||
import com.r3corda.client.model.ExchangeRate
|
||||
import com.r3corda.client.model.ExchangeRateModel
|
||||
import com.r3corda.client.model.observableValue
|
||||
import javafx.beans.value.ObservableValue
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
import java.util.*
|
||||
|
||||
class ReportingCurrencyModel {
|
||||
private val exchangeRate: ObservableValue<ExchangeRate> by observableValue(ExchangeRateModel::exchangeRate)
|
||||
val reportingCurrency: ObservableValue<Currency> by observableValue(SettingsModel::reportingCurrency)
|
||||
/**
|
||||
* This stream provides a stream of exchange() functions that updates when either the reporting currency or the
|
||||
* exchange rates change
|
||||
*/
|
||||
val reportingExchange: ObservableValue<Pair<Currency, (Amount<Currency>) -> Amount<Currency>>> =
|
||||
EasyBind.map(AmountBindings.exchange(reportingCurrency, exchangeRate)) { Pair(it.first) { amount: Amount<Currency> ->
|
||||
Amount(it.second(amount), it.first)
|
||||
}}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package com.r3corda.explorer.model
|
||||
|
||||
import com.r3corda.core.contracts.USD
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
import java.util.*
|
||||
|
||||
class SettingsModel {
|
||||
|
||||
val reportingCurrency: SimpleObjectProperty<Currency> = SimpleObjectProperty(USD)
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.r3corda.explorer.model
|
||||
|
||||
import javafx.beans.property.SimpleObjectProperty
|
||||
|
||||
enum class SelectedView {
|
||||
Home,
|
||||
Cash,
|
||||
Transaction
|
||||
}
|
||||
|
||||
class TopLevelModel {
|
||||
val selectedView = SimpleObjectProperty<SelectedView>(SelectedView.Home)
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package com.r3corda.explorer.ui
|
||||
|
||||
import com.r3corda.explorer.formatters.Formatter
|
||||
import javafx.scene.Node
|
||||
import javafx.scene.control.ListCell
|
||||
import javafx.scene.control.ListView
|
||||
import javafx.util.Callback
|
||||
|
||||
fun <T> Formatter<T>.toListCellFactory() = Callback<ListView<T?>, ListCell<T?>> {
|
||||
object : ListCell<T?>() {
|
||||
override fun updateItem(value: T?, empty: Boolean) {
|
||||
super.updateItem(value, empty)
|
||||
text = if (value == null || empty) {
|
||||
""
|
||||
} else {
|
||||
format(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> ListView<T>.setCustomCellFactory(toNode: (T) -> Node) {
|
||||
setCellFactory {
|
||||
object : ListCell<T>() {
|
||||
init {
|
||||
text = null
|
||||
}
|
||||
override fun updateItem(value: T?, empty: Boolean) {
|
||||
super.updateItem(value, empty)
|
||||
graphic = if (value != null && !empty) {
|
||||
toNode(value)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package com.r3corda.explorer.ui
|
||||
|
||||
sealed class SingleRowSelection<out A> {
|
||||
class None<out A> : SingleRowSelection<A>()
|
||||
class Selected<out A>(val node: A) : SingleRowSelection<A>()
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
package com.r3corda.explorer.ui
|
||||
|
||||
import com.r3corda.explorer.formatters.Formatter
|
||||
import javafx.beans.binding.Bindings
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.scene.Node
|
||||
import javafx.scene.control.ListCell
|
||||
import javafx.scene.control.TableCell
|
||||
import javafx.scene.control.TableColumn
|
||||
import javafx.scene.control.TableView
|
||||
import javafx.util.Callback
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
|
||||
fun <S> TableView<S>.setColumnPrefWidthPolicy(
|
||||
getColumnWidth: (tableWidthWithoutPaddingAndBorder: Number, column: TableColumn<S, *>) -> Number
|
||||
) {
|
||||
val tableWidthWithoutPaddingAndBorder = Bindings.createDoubleBinding({
|
||||
val padding = padding
|
||||
val borderInsets = border?.insets
|
||||
width -
|
||||
(if (padding != null) padding.left + padding.right else 0.0) -
|
||||
(if (borderInsets != null) borderInsets.left + borderInsets.right else 0.0)
|
||||
}, arrayOf(columns, widthProperty(), paddingProperty(), borderProperty()))
|
||||
|
||||
columns.forEach {
|
||||
it.setPrefWidthPolicy(tableWidthWithoutPaddingAndBorder, getColumnWidth)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <S> TableColumn<S, *>.setPrefWidthPolicy(
|
||||
widthWithoutPaddingAndBorder: ObservableValue<Number>,
|
||||
getColumnWidth: (tableWidthWithoutPaddingAndBorder: Number, column: TableColumn<S, *>) -> Number
|
||||
) {
|
||||
prefWidthProperty().bind(EasyBind.map(widthWithoutPaddingAndBorder) {
|
||||
getColumnWidth(it, this)
|
||||
})
|
||||
}
|
||||
|
||||
fun <S, T> Formatter<T>.toTableCellFactory() = Callback<TableColumn<S, T?>, TableCell<S, T?>> {
|
||||
object : TableCell<S, T?>() {
|
||||
override fun updateItem(value: T?, empty: Boolean) {
|
||||
super.updateItem(value, empty)
|
||||
text = if (value == null || empty) {
|
||||
""
|
||||
} else {
|
||||
format(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <S> TableView<S>.singleRowSelection() = Bindings.createObjectBinding({
|
||||
if (selectionModel.selectedItems.size == 0) {
|
||||
SingleRowSelection.None<S>()
|
||||
} else {
|
||||
SingleRowSelection.Selected(selectionModel.selectedItems[0])
|
||||
}
|
||||
}, arrayOf(selectionModel.selectedItems))
|
||||
|
||||
fun <S, T> TableColumn<S, T>.setCustomCellFactory(toNode: (T) -> Node) {
|
||||
setCellFactory {
|
||||
object : TableCell<S, T>() {
|
||||
init {
|
||||
text = null
|
||||
}
|
||||
override fun updateItem(value: T?, empty: Boolean) {
|
||||
super.updateItem(value, empty)
|
||||
graphic = if (value != null && !empty) {
|
||||
toNode(value)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package com.r3corda.explorer.ui
|
||||
|
||||
import com.r3corda.explorer.formatters.Formatter
|
||||
import javafx.beans.binding.Bindings
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.scene.control.TreeTableCell
|
||||
import javafx.scene.control.TreeTableColumn
|
||||
import javafx.scene.control.TreeTableView
|
||||
import javafx.util.Callback
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
|
||||
|
||||
fun <S> TreeTableView<S>.setColumnPrefWidthPolicy(
|
||||
getColumnWidth: (tableWidthWithoutPaddingAndBorder: Number, column: TreeTableColumn<S, *>) -> Number
|
||||
) {
|
||||
val tableWidthWithoutPaddingAndBorder = Bindings.createDoubleBinding({
|
||||
val padding = padding
|
||||
val borderInsets = border?.insets
|
||||
width -
|
||||
(if (padding != null) padding.left + padding.right else 0.0) -
|
||||
(if (borderInsets != null) borderInsets.left + borderInsets.right else 0.0)
|
||||
}, arrayOf(columns, widthProperty(), paddingProperty(), borderProperty()))
|
||||
|
||||
columns.forEach {
|
||||
it.setPrefWidthPolicy(tableWidthWithoutPaddingAndBorder, getColumnWidth)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <S> TreeTableColumn<S, *>.setPrefWidthPolicy(
|
||||
widthWithoutPaddingAndBorder: ObservableValue<Number>,
|
||||
getColumnWidth: (tableWidthWithoutPaddingAndBorder: Number, column: TreeTableColumn<S, *>) -> Number
|
||||
) {
|
||||
prefWidthProperty().bind(EasyBind.map(widthWithoutPaddingAndBorder) {
|
||||
getColumnWidth(it, this)
|
||||
})
|
||||
}
|
||||
|
||||
fun <S, T> Formatter<T>.toTreeTableCellFactory() = Callback<TreeTableColumn<S, T?>, TreeTableCell<S, T?>> {
|
||||
object : TreeTableCell<S, T?>() {
|
||||
override fun updateItem(value: T?, empty: Boolean) {
|
||||
super.updateItem(value, empty)
|
||||
text = if (value == null || empty) {
|
||||
""
|
||||
} else {
|
||||
format(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <S> TreeTableView<S>.singleRowSelection(): ObservableValue<out SingleRowSelection<S>> =
|
||||
Bindings.createObjectBinding({
|
||||
if (selectionModel.selectedItems.size == 0) {
|
||||
SingleRowSelection.None<S>()
|
||||
} else {
|
||||
SingleRowSelection.Selected(selectionModel.selectedItems[0].value)
|
||||
}
|
||||
}, arrayOf(selectionModel.selectedItems))
|
@ -0,0 +1,364 @@
|
||||
package com.r3corda.explorer.views
|
||||
|
||||
import com.r3corda.client.fxutils.*
|
||||
import com.r3corda.client.model.ContractStateModel
|
||||
import com.r3corda.client.model.observableList
|
||||
import com.r3corda.client.model.observableValue
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.core.contracts.Amount
|
||||
import com.r3corda.core.contracts.StateAndRef
|
||||
import com.r3corda.core.contracts.withoutIssuer
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.explorer.formatters.AmountFormatter
|
||||
import com.r3corda.explorer.model.ReportingCurrencyModel
|
||||
import com.r3corda.explorer.model.SettingsModel
|
||||
import com.r3corda.explorer.ui.*
|
||||
import javafx.beans.binding.Bindings
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.scene.Node
|
||||
import javafx.scene.control.*
|
||||
import javafx.scene.image.ImageView
|
||||
import javafx.scene.input.MouseButton
|
||||
import javafx.scene.input.MouseEvent
|
||||
import javafx.scene.layout.HBox
|
||||
import javafx.scene.layout.VBox
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
import tornadofx.UIComponent
|
||||
import tornadofx.View
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
|
||||
sealed class FilterCriteria {
|
||||
abstract fun matches(string: String): Boolean
|
||||
|
||||
object All : FilterCriteria() {
|
||||
override fun matches(string: String) = true
|
||||
}
|
||||
|
||||
class FilterString(val filterString: String) : FilterCriteria() {
|
||||
override fun matches(string: String) = string.contains(filterString)
|
||||
}
|
||||
}
|
||||
|
||||
class CashViewer : View() {
|
||||
// Inject UI elements
|
||||
override val root: SplitPane by fxml()
|
||||
|
||||
val topSplitPane: SplitPane by fxid()
|
||||
// Left pane
|
||||
val leftPane: VBox by fxid()
|
||||
val searchCriteriaTextField: TextField by fxid()
|
||||
val searchCancelImageView: ImageView by fxid()
|
||||
val totalMatchingLabel: Label by fxid()
|
||||
val cashViewerTable: TreeTableView<ViewerNode> by fxid()
|
||||
val cashViewerTableIssuerCurrency: TreeTableColumn<ViewerNode, String> by fxid()
|
||||
val cashViewerTableLocalCurrency: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid()
|
||||
val cashViewerTableEquiv: TreeTableColumn<ViewerNode, Amount<Currency>?> by fxid()
|
||||
|
||||
// Right pane
|
||||
val rightPane: VBox by fxid()
|
||||
val totalPositionsLabel: Label by fxid()
|
||||
val equivSumLabel: Label by fxid()
|
||||
val cashStatesList: ListView<StateRow> by fxid()
|
||||
|
||||
// Inject observables
|
||||
val cashStates by observableList(ContractStateModel::cashStates)
|
||||
val reportingCurrency: ObservableValue<Currency> by observableValue(SettingsModel::reportingCurrency)
|
||||
val reportingExchange: ObservableValue<Pair<Currency, (Amount<Currency>) -> Amount<Currency>>>
|
||||
by observableValue(ReportingCurrencyModel::reportingExchange)
|
||||
|
||||
/**
|
||||
* This holds the data for each row in the TreeTable.
|
||||
*/
|
||||
sealed class ViewerNode {
|
||||
object Root : ViewerNode()
|
||||
class IssuerNode(
|
||||
val issuer: Party,
|
||||
val sumEquivAmount: ObservableValue<out Amount<Currency>>,
|
||||
val states: ObservableList<StateAndRef<Cash.State>>
|
||||
) : ViewerNode()
|
||||
class CurrencyNode(
|
||||
val amount: ObservableValue<Amount<Currency>>,
|
||||
val equivAmount: ObservableValue<Amount<Currency>>,
|
||||
val states: ObservableList<StateAndRef<Cash.State>>
|
||||
) : ViewerNode()
|
||||
}
|
||||
|
||||
/**
|
||||
* We allow filtering by both issuer and currency. We do this by filtering by both at the same time and picking the
|
||||
* one which produces more results, which seems to work, as the set of currency strings don't really overlap with
|
||||
* issuer strings.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Holds the filtering criterion based on the input text
|
||||
*/
|
||||
private val filterCriteria = searchCriteriaTextField.textProperty().map { text ->
|
||||
if (text.isBlank()) {
|
||||
FilterCriteria.All
|
||||
} else {
|
||||
FilterCriteria.FilterString(text)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter cash states based on issuer.
|
||||
*/
|
||||
private val issueFilteredCashStates = cashStates.filter(filterCriteria.map { criteria ->
|
||||
{ state: StateAndRef<Cash.State> ->
|
||||
criteria.matches(state.state.data.amount.token.issuer.party.toString())
|
||||
}
|
||||
})
|
||||
/**
|
||||
* Now filter cash states based on currency.
|
||||
*/
|
||||
private val currencyFilteredCashStates = cashStates.filter(filterCriteria.map { criteria ->
|
||||
{ state: StateAndRef<Cash.State> ->
|
||||
criteria.matches(state.state.data.amount.token.product.toString())
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Now we pick which one to use.
|
||||
*/
|
||||
private val filteredCashStates = ChosenList(filterCriteria.map {
|
||||
if (issueFilteredCashStates.size > currencyFilteredCashStates.size) {
|
||||
issueFilteredCashStates
|
||||
} else {
|
||||
currencyFilteredCashStates
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* This is where we aggregate the list of cash states into the TreeTable structure.
|
||||
*/
|
||||
val cashViewerIssueNodes: ObservableList<TreeItem<out ViewerNode.IssuerNode>> =
|
||||
/**
|
||||
* First we group the states based on the issuer. [memberStates] is all states holding currency issued by [issuer]
|
||||
*/
|
||||
AggregatedList(filteredCashStates, { it.state.data.amount.token.issuer.party }) { issuer, memberStates ->
|
||||
/**
|
||||
* Next we create subgroups based on currency. [memberStates] here is all states holding currency [currency] issued by [issuer] above.
|
||||
* Note that these states will not be displayed in the TreeTable, but rather in the side pane if the user clicks on the row.
|
||||
*/
|
||||
val currencyNodes = AggregatedList(memberStates, { it.state.data.amount.token.product }) { currency, memberStates ->
|
||||
/**
|
||||
* We sum the states in the subgroup, to be displayed in the "Local Currency" column
|
||||
*/
|
||||
val amounts = memberStates.map { it.state.data.amount.withoutIssuer() }
|
||||
val sumAmount = amounts.fold(Amount(0, currency), Amount<Currency>::plus)
|
||||
|
||||
/**
|
||||
* We exchange the sum to the reporting currency, to be displayed in the "<currency> Equiv" column.
|
||||
*/
|
||||
val equivSumAmount = EasyBind.combine(sumAmount, reportingExchange) { sum, exchange ->
|
||||
exchange.second(sum)
|
||||
}
|
||||
/**
|
||||
* Finally assemble the actual TreeTable Currency node.
|
||||
*/
|
||||
TreeItem(ViewerNode.CurrencyNode(sumAmount, equivSumAmount, memberStates))
|
||||
}
|
||||
|
||||
/**
|
||||
* Now that we have all nodes per currency, we sum the exchanged amounts, to be displayed in the
|
||||
* "<currency> Equiv" column, this time on the issuer level.
|
||||
*/
|
||||
val equivAmounts = currencyNodes.map { it.value.equivAmount }.flatten()
|
||||
val equivSumAmount = reportingCurrency.bind { currency ->
|
||||
equivAmounts.fold(Amount(0, currency), Amount<Currency>::plus)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble the Issuer node.
|
||||
*/
|
||||
val treeItem = TreeItem(ViewerNode.IssuerNode(issuer, equivSumAmount, memberStates))
|
||||
|
||||
/**
|
||||
* Bind the children in the TreeTable structure.
|
||||
*
|
||||
* TODO Perhaps we shouldn't do this here, but rather have a generic way of binding nodes to the treetable once.
|
||||
*/
|
||||
treeItem.isExpanded = true
|
||||
val children: List<TreeItem<out ViewerNode.IssuerNode>> = treeItem.children
|
||||
Bindings.bindContent(children, currencyNodes)
|
||||
treeItem
|
||||
}
|
||||
|
||||
/**
|
||||
* Now we build up the Observables needed for the side pane, given that the user clicks on a row.
|
||||
*/
|
||||
val selectedViewerNode = cashViewerTable.singleRowSelection()
|
||||
|
||||
/**
|
||||
* Holds data for a single state, to be displayed in the list in the side pane.
|
||||
*/
|
||||
data class StateRow (
|
||||
val originated: LocalDateTime,
|
||||
val stateAndRef: StateAndRef<Cash.State>
|
||||
)
|
||||
|
||||
/**
|
||||
* A small class describing the graphics of a single state.
|
||||
*/
|
||||
inner class StateRowGraphic(
|
||||
val stateRow: StateRow
|
||||
) : UIComponent() {
|
||||
override val root: HBox by fxml("CashStateViewer.fxml")
|
||||
|
||||
val equivLabel: Label by fxid()
|
||||
val stateIdValueLabel: Label by fxid()
|
||||
val issuerValueLabel: Label by fxid()
|
||||
val originatedValueLabel: Label by fxid()
|
||||
val amountValueLabel: Label by fxid()
|
||||
val equivValueLabel: Label by fxid()
|
||||
|
||||
val equivAmount: ObservableValue<out Amount<Currency>> = reportingExchange.map {
|
||||
it.second(stateRow.stateAndRef.state.data.amount.withoutIssuer())
|
||||
}
|
||||
|
||||
init {
|
||||
val amountNoIssuer = stateRow.stateAndRef.state.data.amount.withoutIssuer()
|
||||
val amountFormatter = AmountFormatter.boring
|
||||
val equivFormatter = AmountFormatter.boring
|
||||
|
||||
equivLabel.textProperty().bind(equivAmount.map { it.token.currencyCode.toString() })
|
||||
stateIdValueLabel.text = stateRow.stateAndRef.ref.toString()
|
||||
issuerValueLabel.text = stateRow.stateAndRef.state.data.amount.token.issuer.toString()
|
||||
originatedValueLabel.text = stateRow.originated.toString()
|
||||
amountValueLabel.text = amountFormatter.format(amountNoIssuer)
|
||||
equivValueLabel.textProperty().bind(equivAmount.map { equivFormatter.format(it) })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of states related to the current selection. If none or the root is selected it's empty, if an issuer or
|
||||
* currency node is selected it's the relevant states.
|
||||
*/
|
||||
private val noSelectionStates = FXCollections.observableArrayList<StateAndRef<Cash.State>>()
|
||||
private val selectedViewerNodeStates = ChosenList(selectedViewerNode.map { selection ->
|
||||
when (selection) {
|
||||
is SingleRowSelection.None -> noSelectionStates
|
||||
is SingleRowSelection.Selected ->
|
||||
when (selection.node) {
|
||||
CashViewer.ViewerNode.Root -> noSelectionStates
|
||||
is CashViewer.ViewerNode.IssuerNode -> selection.node.states
|
||||
is CashViewer.ViewerNode.CurrencyNode -> selection.node.states
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* We re-display the exchanged sum amount, if we have a selection.
|
||||
*/
|
||||
private val noSelectionSumEquiv = reportingCurrency.map { Amount(0, it) }
|
||||
private val selectedViewerNodeSumEquiv = selectedViewerNode.bind { selection ->
|
||||
when (selection) {
|
||||
is SingleRowSelection.None -> noSelectionSumEquiv
|
||||
is SingleRowSelection.Selected ->
|
||||
when (selection.node) {
|
||||
ViewerNode.Root -> noSelectionSumEquiv
|
||||
is ViewerNode.IssuerNode -> selection.node.sumEquivAmount
|
||||
is ViewerNode.CurrencyNode -> selection.node.equivAmount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We add some extra timestamp data here to the selected states.
|
||||
*
|
||||
* TODO update this once we have actual timestamps.
|
||||
*/
|
||||
private val stateRows = selectedViewerNodeStates.map { StateRow(LocalDateTime.now(), it) }
|
||||
|
||||
/**
|
||||
* We only display the right pane if a node is selected in the TreeTable.
|
||||
*/
|
||||
private val onlyLeftPaneShown = FXCollections.observableArrayList<Node>(leftPane)
|
||||
private val bothPanesShown = FXCollections.observableArrayList<Node>(leftPane, rightPane)
|
||||
private val panesShown = ChosenList<Node>(selectedViewerNode.map {
|
||||
when (it) {
|
||||
is SingleRowSelection.None -> onlyLeftPaneShown
|
||||
is SingleRowSelection.Selected -> bothPanesShown
|
||||
}
|
||||
})
|
||||
|
||||
// Wire up UI
|
||||
init {
|
||||
searchCancelImageView.setOnMouseClicked { event: MouseEvent ->
|
||||
if (event.button == MouseButton.PRIMARY) {
|
||||
searchCriteriaTextField.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
Bindings.bindContent(topSplitPane.items, panesShown)
|
||||
|
||||
totalPositionsLabel.textProperty().bind(Bindings.size(selectedViewerNodeStates).map {
|
||||
val plural = if (it == 1) "" else "s"
|
||||
"Total $it position$plural"
|
||||
})
|
||||
|
||||
val equivSumLabelFormatter = AmountFormatter.boring
|
||||
equivSumLabel.textProperty().bind(selectedViewerNodeSumEquiv.map {
|
||||
equivSumLabelFormatter.format(it)
|
||||
})
|
||||
|
||||
Bindings.bindContent(cashStatesList.items, stateRows)
|
||||
|
||||
cashStatesList.setCustomCellFactory { StateRowGraphic(it).root }
|
||||
|
||||
val cellFactory = AmountFormatter.boring.toTreeTableCellFactory<ViewerNode, Amount<Currency>>()
|
||||
|
||||
// TODO use smart resize
|
||||
cashViewerTable.setColumnPrefWidthPolicy { tableWidthWithoutPaddingAndBorder, column ->
|
||||
Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / cashViewerTable.columns.size).toInt()
|
||||
}
|
||||
|
||||
cashViewerTableIssuerCurrency.setCellValueFactory {
|
||||
val node = it.value.value
|
||||
when (node) {
|
||||
ViewerNode.Root -> "".lift()
|
||||
is ViewerNode.IssuerNode -> node.issuer.toString().lift()
|
||||
is ViewerNode.CurrencyNode -> node.amount.map { it.token.toString() }
|
||||
}
|
||||
}
|
||||
cashViewerTableLocalCurrency.setCellValueFactory {
|
||||
val node = it.value.value
|
||||
when (node) {
|
||||
ViewerNode.Root -> null.lift()
|
||||
is ViewerNode.IssuerNode -> null.lift()
|
||||
is ViewerNode.CurrencyNode -> node.amount.map { it }
|
||||
}
|
||||
}
|
||||
cashViewerTableLocalCurrency.cellFactory = cellFactory
|
||||
/**
|
||||
* We must set this, otherwise on sort an exception will be thrown, as it will try to compare Amounts of differing currency
|
||||
*/
|
||||
cashViewerTableLocalCurrency.isSortable = false
|
||||
cashViewerTableEquiv.setCellValueFactory {
|
||||
val node = it.value.value
|
||||
when (node) {
|
||||
ViewerNode.Root -> null.lift()
|
||||
is ViewerNode.IssuerNode -> node.sumEquivAmount.map { it }
|
||||
is ViewerNode.CurrencyNode -> node.equivAmount.map { it }
|
||||
}
|
||||
}
|
||||
cashViewerTableEquiv.cellFactory = cellFactory
|
||||
cashViewerTableEquiv.textProperty().bind(reportingCurrency.map { "$it Equiv" })
|
||||
|
||||
cashViewerTable.root = TreeItem(ViewerNode.Root)
|
||||
val children: List<TreeItem<out ViewerNode>> = cashViewerTable.root.children
|
||||
Bindings.bindContent(children, cashViewerIssueNodes)
|
||||
|
||||
cashViewerTable.root.isExpanded = true
|
||||
cashViewerTable.isShowRoot = false
|
||||
|
||||
// TODO Think about i18n!
|
||||
totalMatchingLabel.textProperty().bind(Bindings.size(cashViewerIssueNodes).map {
|
||||
val plural = if (it == 1) "" else "s"
|
||||
"Total $it matching issuer$plural"
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package com.r3corda.explorer.views
|
||||
|
||||
import com.r3corda.client.model.observableValue
|
||||
import com.r3corda.explorer.model.SelectedView
|
||||
import com.r3corda.explorer.model.TopLevelModel
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.scene.control.Button
|
||||
import javafx.scene.control.Label
|
||||
import javafx.scene.image.Image
|
||||
import javafx.scene.image.ImageView
|
||||
import javafx.scene.layout.VBox
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
import tornadofx.View
|
||||
|
||||
class Header : View() {
|
||||
override val root: VBox by fxml()
|
||||
|
||||
private val sectionIcon: ImageView by fxid()
|
||||
private val sectionIconContainer: VBox by fxid()
|
||||
private val sectionLabel: Label by fxid()
|
||||
private val debugNextButton: Button by fxid()
|
||||
private val debugGoStopButton: Button by fxid()
|
||||
|
||||
private val selectedView: ObservableValue<SelectedView> by observableValue(TopLevelModel::selectedView)
|
||||
|
||||
private val homeImage = Image("/com/r3corda/explorer/images/home.png")
|
||||
private val cashImage = Image("/com/r3corda/explorer/images/cash.png")
|
||||
private val transactionImage = Image("/com/r3corda/explorer/images/tx.png")
|
||||
|
||||
init {
|
||||
sectionLabel.textProperty().bind(EasyBind.map(selectedView) {
|
||||
when (it) {
|
||||
SelectedView.Home -> "Home"
|
||||
SelectedView.Cash -> "Cash"
|
||||
SelectedView.Transaction -> "Transactions"
|
||||
null -> "Home"
|
||||
}
|
||||
})
|
||||
|
||||
sectionIcon.imageProperty().bind(EasyBind.map(selectedView) {
|
||||
when (it) {
|
||||
SelectedView.Home -> homeImage
|
||||
SelectedView.Cash -> cashImage
|
||||
SelectedView.Transaction -> transactionImage
|
||||
null -> homeImage
|
||||
}
|
||||
})
|
||||
|
||||
// JavaFX bugs and doesn't invalidate the wrapping Box's height if the icon fit height is first set to
|
||||
// unbounded (0.0) - which is what the label's height is initially, so we set it to 1.0 instead
|
||||
val secionLabelHeightNonZero = EasyBind.map(sectionLabel.heightProperty()) {
|
||||
if (it == 0.0) {
|
||||
1.0
|
||||
} else {
|
||||
it.toDouble()
|
||||
}
|
||||
}
|
||||
|
||||
sectionIconContainer.minWidthProperty().bind(secionLabelHeightNonZero)
|
||||
sectionIcon.fitWidthProperty().bind(secionLabelHeightNonZero)
|
||||
sectionIcon.fitHeightProperty().bind(sectionIcon.fitWidthProperty())
|
||||
}
|
||||
}
|
68
explorer/src/main/kotlin/com/r3corda/explorer/views/Home.kt
Normal file
@ -0,0 +1,68 @@
|
||||
package com.r3corda.explorer.views
|
||||
|
||||
import com.r3corda.client.fxutils.AmountBindings
|
||||
import com.r3corda.client.fxutils.map
|
||||
import com.r3corda.client.model.*
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.core.contracts.StateAndRef
|
||||
import com.r3corda.core.contracts.withoutIssuer
|
||||
import com.r3corda.explorer.formatters.AmountFormatter
|
||||
import com.r3corda.explorer.model.SelectedView
|
||||
import com.r3corda.explorer.model.SettingsModel
|
||||
import com.r3corda.explorer.model.TopLevelModel
|
||||
import javafx.beans.binding.Bindings
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.beans.value.WritableValue
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.scene.control.Label
|
||||
import javafx.scene.control.TitledPane
|
||||
import javafx.scene.input.MouseButton
|
||||
import javafx.scene.layout.TilePane
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
import tornadofx.View
|
||||
import java.util.*
|
||||
|
||||
|
||||
class Home : View() {
|
||||
override val root: TilePane by fxml()
|
||||
|
||||
private val ourCashPane: TitledPane by fxid()
|
||||
private val ourCashLabel: Label by fxid()
|
||||
|
||||
private val ourTransactionsPane: TitledPane by fxid()
|
||||
private val ourTransactionsLabel: Label by fxid()
|
||||
|
||||
private val selectedView: WritableValue<SelectedView> by writableValue(TopLevelModel::selectedView)
|
||||
private val cashStates: ObservableList<StateAndRef<Cash.State>> by observableList(ContractStateModel::cashStates)
|
||||
private val gatheredTransactionDataList: ObservableList<out GatheredTransactionData>
|
||||
by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
|
||||
private val reportingCurrency: ObservableValue<Currency> by observableValue(SettingsModel::reportingCurrency)
|
||||
private val exchangeRate: ObservableValue<ExchangeRate> by observableValue(ExchangeRateModel::exchangeRate)
|
||||
|
||||
private val sumAmount = AmountBindings.sumAmountExchange(
|
||||
cashStates.map { it.state.data.amount.withoutIssuer() },
|
||||
reportingCurrency,
|
||||
exchangeRate
|
||||
)
|
||||
|
||||
init {
|
||||
val formatter = AmountFormatter.boring
|
||||
|
||||
ourCashLabel.textProperty().bind(sumAmount.map { formatter.format(it) })
|
||||
ourCashPane.setOnMouseClicked { clickEvent ->
|
||||
if (clickEvent.button == MouseButton.PRIMARY) {
|
||||
selectedView.value = SelectedView.Cash
|
||||
}
|
||||
}
|
||||
|
||||
ourTransactionsLabel.textProperty().bind(
|
||||
Bindings.size(gatheredTransactionDataList).map { it.toString() }
|
||||
)
|
||||
ourTransactionsPane.setOnMouseClicked { clickEvent ->
|
||||
if (clickEvent.button == MouseButton.PRIMARY) {
|
||||
selectedView.value = SelectedView.Transaction
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package com.r3corda.explorer.views
|
||||
|
||||
import com.r3corda.client.model.objectProperty
|
||||
import com.r3corda.explorer.model.SelectedView
|
||||
import com.r3corda.explorer.model.TopLevelModel
|
||||
import javafx.beans.property.ObjectProperty
|
||||
import javafx.scene.input.KeyCode
|
||||
import javafx.scene.input.KeyEvent
|
||||
import javafx.scene.layout.BorderPane
|
||||
import javafx.scene.layout.Priority
|
||||
import javafx.scene.layout.VBox
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
import tornadofx.View
|
||||
|
||||
class TopLevel : View() {
|
||||
override val root: VBox by fxml()
|
||||
val selectionBorderPane: BorderPane by fxid()
|
||||
|
||||
private val header: Header by inject()
|
||||
private val home: Home by inject()
|
||||
private val cash: CashViewer by inject()
|
||||
private val transaction: TransactionViewer 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 fun getView(selection: SelectedView) = when (selection) {
|
||||
SelectedView.Home -> homeRoot
|
||||
SelectedView.Cash -> cashRoot
|
||||
SelectedView.Transaction -> transactionRoot
|
||||
}
|
||||
val selectedView: ObjectProperty<SelectedView> by objectProperty(TopLevelModel::selectedView)
|
||||
|
||||
init {
|
||||
VBox.setVgrow(selectionBorderPane, Priority.ALWAYS)
|
||||
selectionBorderPane.centerProperty().bind(EasyBind.map(selectedView) { getView(it) })
|
||||
|
||||
primaryStage.addEventHandler(KeyEvent.KEY_RELEASED) { keyEvent ->
|
||||
if (keyEvent.code == KeyCode.ESCAPE) {
|
||||
selectedView.value = SelectedView.Home
|
||||
}
|
||||
}
|
||||
|
||||
root.children.add(0, header.root)
|
||||
}
|
||||
}
|
@ -0,0 +1,399 @@
|
||||
package com.r3corda.explorer.views
|
||||
|
||||
import com.r3corda.client.fxutils.ChosenList
|
||||
import com.r3corda.client.fxutils.bind
|
||||
import com.r3corda.client.fxutils.lift
|
||||
import com.r3corda.client.fxutils.map
|
||||
import com.r3corda.client.model.*
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.crypto.toStringShort
|
||||
import com.r3corda.core.transactions.LedgerTransaction
|
||||
import com.r3corda.explorer.AmountDiff
|
||||
import com.r3corda.explorer.formatters.AmountFormatter
|
||||
import com.r3corda.explorer.formatters.Formatter
|
||||
import com.r3corda.explorer.formatters.NumberFormatter
|
||||
import com.r3corda.explorer.model.IdentityModel
|
||||
import com.r3corda.explorer.model.ReportingCurrencyModel
|
||||
import com.r3corda.explorer.sign
|
||||
import com.r3corda.explorer.ui.*
|
||||
import com.r3corda.node.services.monitor.ServiceToClientEvent
|
||||
import javafx.beans.binding.Bindings
|
||||
import javafx.beans.value.ObservableValue
|
||||
import javafx.collections.FXCollections
|
||||
import javafx.collections.ObservableList
|
||||
import javafx.geometry.Insets
|
||||
import javafx.scene.Node
|
||||
import javafx.scene.control.*
|
||||
import javafx.scene.layout.Background
|
||||
import javafx.scene.layout.BackgroundFill
|
||||
import javafx.scene.layout.CornerRadii
|
||||
import javafx.scene.layout.VBox
|
||||
import javafx.scene.paint.Color
|
||||
import org.fxmisc.easybind.EasyBind
|
||||
import tornadofx.View
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
class TransactionViewer: View() {
|
||||
override val root: VBox by fxml()
|
||||
|
||||
val topSplitPane: SplitPane by fxid()
|
||||
|
||||
// Top half (transactions table)
|
||||
private val transactionViewTable: TableView<ViewerNode> by fxid()
|
||||
private val transactionViewTransactionId: TableColumn<ViewerNode, String> by fxid()
|
||||
private val transactionViewFiberId: TableColumn<ViewerNode, String> by fxid()
|
||||
private val transactionViewClientUuid: TableColumn<ViewerNode, String> by fxid()
|
||||
private val transactionViewTransactionStatus: TableColumn<ViewerNode, TransactionCreateStatus?> by fxid()
|
||||
private val transactionViewProtocolStatus: TableColumn<ViewerNode, String> by fxid()
|
||||
private val transactionViewStateMachineStatus: TableColumn<ViewerNode, StateMachineStatus?> by fxid()
|
||||
private val transactionViewCommandTypes: TableColumn<ViewerNode, String> by fxid()
|
||||
private val transactionViewTotalValueEquiv: TableColumn<ViewerNode, AmountDiff<Currency>> by fxid()
|
||||
|
||||
// Bottom half (details)
|
||||
private val contractStatesTitledPane: TitledPane by fxid()
|
||||
|
||||
private val contractStatesInputsCountLabel: Label by fxid()
|
||||
private val contractStatesInputStatesTable: TableView<StateNode> by fxid()
|
||||
private val contractStatesInputStatesId: TableColumn<StateNode, String> by fxid()
|
||||
private val contractStatesInputStatesType: TableColumn<StateNode, Class<out ContractState>> by fxid()
|
||||
private val contractStatesInputStatesOwner: TableColumn<StateNode, String> by fxid()
|
||||
private val contractStatesInputStatesLocalCurrency: TableColumn<StateNode, Currency?> by fxid()
|
||||
private val contractStatesInputStatesAmount: TableColumn<StateNode, Long> by fxid()
|
||||
private val contractStatesInputStatesEquiv: TableColumn<StateNode, Amount<Currency>> by fxid()
|
||||
|
||||
private val contractStatesOutputsCountLabel: Label by fxid()
|
||||
private val contractStatesOutputStatesTable: TableView<StateNode> by fxid()
|
||||
private val contractStatesOutputStatesId: TableColumn<StateNode, String> by fxid()
|
||||
private val contractStatesOutputStatesType: TableColumn<StateNode, Class<out ContractState>> by fxid()
|
||||
private val contractStatesOutputStatesOwner: TableColumn<StateNode, String> by fxid()
|
||||
private val contractStatesOutputStatesLocalCurrency: TableColumn<StateNode, Currency?> by fxid()
|
||||
private val contractStatesOutputStatesAmount: TableColumn<StateNode, Long> by fxid()
|
||||
private val contractStatesOutputStatesEquiv: TableColumn<StateNode, Amount<Currency>> by fxid()
|
||||
|
||||
private val signaturesTitledPane: TitledPane by fxid()
|
||||
private val signaturesList: ListView<PublicKey> by fxid()
|
||||
|
||||
private val lowLevelEventsTitledPane: TitledPane by fxid()
|
||||
private val lowLevelEventsTable: TableView<ServiceToClientEvent> by fxid()
|
||||
private val lowLevelEventsTimestamp: TableColumn<ServiceToClientEvent, Instant> by fxid()
|
||||
private val lowLevelEventsEvent: TableColumn<ServiceToClientEvent, ServiceToClientEvent> by fxid()
|
||||
|
||||
private val matchingTransactionsLabel: Label by fxid()
|
||||
|
||||
// Inject data
|
||||
private val gatheredTransactionDataList: ObservableList<out GatheredTransactionData>
|
||||
by observableListReadOnly(GatheredTransactionDataModel::gatheredTransactionDataList)
|
||||
private val reportingExchange: ObservableValue<Pair<Currency, (Amount<Currency>) -> Amount<Currency>>>
|
||||
by observableValue(ReportingCurrencyModel::reportingExchange)
|
||||
private val myIdentity: ObservableValue<Party> by observableValue(IdentityModel::myIdentity)
|
||||
|
||||
/**
|
||||
* 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 transactionId: ObservableValue<SecureHash?>,
|
||||
val fiberId: ObservableValue<Long?>,
|
||||
val clientUuid: ObservableValue<UUID?>,
|
||||
val originator: ObservableValue<String>,
|
||||
val transactionStatus: ObservableValue<TransactionCreateStatus?>,
|
||||
val stateMachineStatus: ObservableValue<StateMachineStatus?>,
|
||||
val protocolStatus: ObservableValue<ProtocolStatus?>,
|
||||
val statusUpdated: ObservableValue<Instant>,
|
||||
val commandTypes: ObservableValue<Collection<Class<CommandData>>>,
|
||||
val totalValueEquiv: ObservableValue<AmountDiff<Currency>?>,
|
||||
val transaction: ObservableValue<LedgerTransaction?>,
|
||||
val allEvents: ObservableList<out ServiceToClientEvent>
|
||||
)
|
||||
|
||||
/**
|
||||
* Holds information about a single input/output state, to be displayed in the [contractStatesTitledPane]
|
||||
*/
|
||||
data class StateNode(
|
||||
val transactionState: TransactionState<*>,
|
||||
val stateRef: StateRef
|
||||
)
|
||||
|
||||
/**
|
||||
* We map the gathered data about transactions almost one-to-one to the nodes.
|
||||
*/
|
||||
private val viewerNodes = gatheredTransactionDataList.map {
|
||||
ViewerNode(
|
||||
transactionId = it.transaction.map { it?.id },
|
||||
fiberId = it.fiberId,
|
||||
clientUuid = it.uuid,
|
||||
/**
|
||||
* We can't really do any better based on uuid, we need to store explicit data for this TODO
|
||||
*/
|
||||
originator = it.uuid.map { uuid ->
|
||||
if (uuid == null) {
|
||||
"Someone"
|
||||
} else {
|
||||
"Us"
|
||||
}
|
||||
},
|
||||
transactionStatus = it.status,
|
||||
protocolStatus = it.protocolStatus,
|
||||
stateMachineStatus = it.stateMachineStatus,
|
||||
statusUpdated = it.lastUpdate,
|
||||
commandTypes = it.transaction.map {
|
||||
val commands = mutableSetOf<Class<CommandData>>()
|
||||
it?.commands?.forEach {
|
||||
commands.add(it.value.javaClass)
|
||||
}
|
||||
commands
|
||||
},
|
||||
totalValueEquiv = ::calculateTotalEquiv.lift(myIdentity, reportingExchange, it.transaction),
|
||||
transaction = it.transaction,
|
||||
allEvents = it.allEvents
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The detail panes are only filled out if a transaction is selected
|
||||
*/
|
||||
private val selectedViewerNode = transactionViewTable.singleRowSelection()
|
||||
private val selectedTransaction = selectedViewerNode.bind {
|
||||
when (it) {
|
||||
is SingleRowSelection.None -> null.lift()
|
||||
is SingleRowSelection.Selected -> it.node.transaction
|
||||
}
|
||||
}
|
||||
|
||||
private val inputStateNodes = ChosenList(selectedTransaction.map {
|
||||
if (it == null) {
|
||||
FXCollections.emptyObservableList<StateNode>()
|
||||
} else {
|
||||
FXCollections.observableArrayList(it.inputs.map { StateNode(it.state, it.ref) })
|
||||
}
|
||||
})
|
||||
|
||||
private val outputStateNodes = ChosenList(selectedTransaction.map {
|
||||
if (it == null) {
|
||||
FXCollections.emptyObservableList<StateNode>()
|
||||
} else {
|
||||
FXCollections.observableArrayList(it.outputs.mapIndexed { index, transactionState ->
|
||||
StateNode(transactionState, StateRef(it.id, index))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
private val signatures = ChosenList(selectedTransaction.map {
|
||||
if (it == null) {
|
||||
FXCollections.emptyObservableList<PublicKey>()
|
||||
} else {
|
||||
FXCollections.observableArrayList(it.mustSign)
|
||||
}
|
||||
})
|
||||
|
||||
private val lowLevelEvents = ChosenList(selectedViewerNode.map {
|
||||
when (it) {
|
||||
is SingleRowSelection.None -> FXCollections.emptyObservableList<ServiceToClientEvent>()
|
||||
is SingleRowSelection.Selected -> it.node.allEvents
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* We only display the detail panes if there is a node selected.
|
||||
*/
|
||||
private val allNodesShown = FXCollections.observableArrayList<Node>(
|
||||
transactionViewTable,
|
||||
contractStatesTitledPane,
|
||||
signaturesTitledPane,
|
||||
lowLevelEventsTitledPane
|
||||
)
|
||||
private val onlyTransactionsTableShown = FXCollections.observableArrayList<Node>(
|
||||
transactionViewTable
|
||||
)
|
||||
private val topSplitPaneNodesShown = ChosenList(
|
||||
selectedViewerNode.map { selection ->
|
||||
if (selection is SingleRowSelection.None<*>) {
|
||||
onlyTransactionsTableShown
|
||||
} else {
|
||||
allNodesShown
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Both input and output state tables look the same, so we each up with [wireUpStatesTable]
|
||||
*/
|
||||
private fun wireUpStatesTable(
|
||||
states: ObservableList<StateNode>,
|
||||
statesCountLabel: Label,
|
||||
statesTable: TableView<StateNode>,
|
||||
statesId: TableColumn<StateNode, String>,
|
||||
statesType: TableColumn<StateNode, Class<out ContractState>>,
|
||||
statesOwner: TableColumn<StateNode, String>,
|
||||
statesLocalCurrency: TableColumn<StateNode, Currency?>,
|
||||
statesAmount: TableColumn<StateNode, Long>,
|
||||
statesEquiv: TableColumn<StateNode, Amount<Currency>>
|
||||
) {
|
||||
statesCountLabel.textProperty().bind(Bindings.size(states).map { "$it" })
|
||||
|
||||
Bindings.bindContent(statesTable.items, states)
|
||||
|
||||
statesId.setCellValueFactory { it.value.stateRef.toString().lift() }
|
||||
statesType.setCellValueFactory { it.value.transactionState.data.javaClass.lift() }
|
||||
statesOwner.setCellValueFactory {
|
||||
val state = it.value.transactionState.data
|
||||
if (state is OwnableState) {
|
||||
state.owner.toStringShort().lift()
|
||||
} else {
|
||||
"???".lift()
|
||||
}
|
||||
}
|
||||
statesLocalCurrency.setCellValueFactory {
|
||||
val state = it.value.transactionState.data
|
||||
if (state is Cash.State) {
|
||||
state.amount.token.product.lift()
|
||||
} else {
|
||||
null.lift()
|
||||
}
|
||||
}
|
||||
statesAmount.setCellValueFactory {
|
||||
val state = it.value.transactionState.data
|
||||
if (state is Cash.State) {
|
||||
state.amount.quantity.lift()
|
||||
} else {
|
||||
null.lift()
|
||||
}
|
||||
}
|
||||
statesAmount.cellFactory = NumberFormatter.boringLong.toTableCellFactory()
|
||||
statesEquiv.setCellValueFactory {
|
||||
val state = it.value.transactionState.data
|
||||
if (state is Cash.State) {
|
||||
reportingExchange.map { exchange ->
|
||||
exchange.second(state.amount.withoutIssuer())
|
||||
}
|
||||
} else {
|
||||
null.lift()
|
||||
}
|
||||
}
|
||||
statesEquiv.cellFactory = AmountFormatter.boring.toTableCellFactory()
|
||||
}
|
||||
|
||||
init {
|
||||
Bindings.bindContent(topSplitPane.items, topSplitPaneNodesShown)
|
||||
|
||||
// Transaction table
|
||||
Bindings.bindContent(transactionViewTable.items, viewerNodes)
|
||||
|
||||
transactionViewTable.setColumnPrefWidthPolicy { tableWidthWithoutPaddingAndBorder, column ->
|
||||
Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / transactionViewTable.columns.size).toInt()
|
||||
}
|
||||
|
||||
transactionViewTransactionId.setCellValueFactory { it.value.transactionId.map { "${it ?: ""}" } }
|
||||
transactionViewFiberId.setCellValueFactory { it.value.fiberId.map { "${it?: ""}" } }
|
||||
transactionViewClientUuid.setCellValueFactory { it.value.clientUuid.map { "${it ?: ""}" } }
|
||||
transactionViewProtocolStatus.setCellValueFactory { it.value.protocolStatus.map { "${it ?: ""}" } }
|
||||
transactionViewTransactionStatus.setCellValueFactory { it.value.transactionStatus }
|
||||
transactionViewTransactionStatus.setCustomCellFactory {
|
||||
val label = Label()
|
||||
val backgroundFill = when (it) {
|
||||
is TransactionCreateStatus.Started -> BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)
|
||||
is TransactionCreateStatus.Failed -> BackgroundFill(Color.SALMON, CornerRadii.EMPTY, Insets.EMPTY)
|
||||
null -> BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)
|
||||
}
|
||||
label.background = Background(backgroundFill)
|
||||
label.text = "$it"
|
||||
label
|
||||
}
|
||||
transactionViewStateMachineStatus.setCellValueFactory { it.value.stateMachineStatus }
|
||||
transactionViewStateMachineStatus.setCustomCellFactory {
|
||||
val label = Label()
|
||||
val backgroundFill = when (it) {
|
||||
is StateMachineStatus.Added -> BackgroundFill(Color.LIGHTYELLOW, CornerRadii.EMPTY, Insets.EMPTY)
|
||||
is StateMachineStatus.Removed -> BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)
|
||||
null -> BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)
|
||||
}
|
||||
label.background = Background(backgroundFill)
|
||||
label.text = "$it"
|
||||
label
|
||||
}
|
||||
|
||||
transactionViewCommandTypes.setCellValueFactory {
|
||||
it.value.commandTypes.map { it.map { it.simpleName }.joinToString(",") }
|
||||
}
|
||||
transactionViewTotalValueEquiv.setCellValueFactory<ViewerNode, AmountDiff<Currency>> { it.value.totalValueEquiv }
|
||||
transactionViewTotalValueEquiv.cellFactory = object : Formatter<AmountDiff<Currency>> {
|
||||
override fun format(value: AmountDiff<Currency>) =
|
||||
"${value.positivity.sign}${AmountFormatter.boring.format(value.amount)}"
|
||||
}.toTableCellFactory()
|
||||
|
||||
// Contract states
|
||||
wireUpStatesTable(
|
||||
inputStateNodes,
|
||||
contractStatesInputsCountLabel,
|
||||
contractStatesInputStatesTable,
|
||||
contractStatesInputStatesId,
|
||||
contractStatesInputStatesType,
|
||||
contractStatesInputStatesOwner,
|
||||
contractStatesInputStatesLocalCurrency,
|
||||
contractStatesInputStatesAmount,
|
||||
contractStatesInputStatesEquiv
|
||||
)
|
||||
wireUpStatesTable(
|
||||
outputStateNodes,
|
||||
contractStatesOutputsCountLabel,
|
||||
contractStatesOutputStatesTable,
|
||||
contractStatesOutputStatesId,
|
||||
contractStatesOutputStatesType,
|
||||
contractStatesOutputStatesOwner,
|
||||
contractStatesOutputStatesLocalCurrency,
|
||||
contractStatesOutputStatesAmount,
|
||||
contractStatesOutputStatesEquiv
|
||||
)
|
||||
|
||||
// Signatures
|
||||
Bindings.bindContent(signaturesList.items, signatures)
|
||||
signaturesList.cellFactory = object : Formatter<PublicKey> {
|
||||
override fun format(value: PublicKey) = value.toStringShort()
|
||||
}.toListCellFactory()
|
||||
|
||||
// Low level events
|
||||
Bindings.bindContent(lowLevelEventsTable.items, lowLevelEvents)
|
||||
lowLevelEventsTimestamp.setCellValueFactory { it.value.time.lift() }
|
||||
lowLevelEventsEvent.setCellValueFactory { it.value.lift() }
|
||||
lowLevelEventsTable.setColumnPrefWidthPolicy { tableWidthWithoutPaddingAndBorder, column ->
|
||||
Math.floor(tableWidthWithoutPaddingAndBorder.toDouble() / lowLevelEventsTable.columns.size).toInt()
|
||||
}
|
||||
|
||||
matchingTransactionsLabel.textProperty().bind(EasyBind.map(Bindings.size(viewerNodes)) {
|
||||
"$it matching transaction${if (it == 1) "" else "s"}"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We calculate the total value by subtracting relevant input states and adding relevant output states, as long as they're cash
|
||||
*/
|
||||
private fun calculateTotalEquiv(
|
||||
identity: Party,
|
||||
reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>,
|
||||
transaction: LedgerTransaction?): AmountDiff<Currency>? {
|
||||
if (transaction == null) {
|
||||
return null
|
||||
}
|
||||
var sum = 0L
|
||||
val (reportingCurrency, exchange) = reportingCurrencyExchange
|
||||
val publicKey = identity.owningKey
|
||||
transaction.inputs.forEach {
|
||||
val contractState = it.state.data
|
||||
if (contractState is Cash.State && publicKey == contractState.owner) {
|
||||
sum -= exchange(contractState.amount.withoutIssuer()).quantity
|
||||
}
|
||||
}
|
||||
transaction.outputs.forEach {
|
||||
val contractState = it.data
|
||||
if (contractState is Cash.State && publicKey == contractState.owner) {
|
||||
sum += exchange(contractState.amount.withoutIssuer()).quantity
|
||||
}
|
||||
}
|
||||
return AmountDiff.fromLong(sum, reportingCurrency)
|
||||
}
|
||||
|
@ -0,0 +1,42 @@
|
||||
.notification-bar {
|
||||
-fx-background-color: linear-gradient(to bottom, black, darkslategray);
|
||||
}
|
||||
|
||||
.notification-bar > .notification-bar-item {
|
||||
-fx-padding: 10;
|
||||
}
|
||||
|
||||
.notification-bar > .notification-bar-item > Label {
|
||||
-fx-text-fill: white;
|
||||
-fx-font-weight: bold;
|
||||
-fx-font-size: 13;
|
||||
}
|
||||
|
||||
.notification-bar > .notification-bar-item > .progress-bar > .bar {
|
||||
-fx-padding: 8;
|
||||
}
|
||||
|
||||
.notification-bar > .notification-bar-item > .progress-bar > .track {
|
||||
-fx-opacity: 0.0;
|
||||
}
|
||||
|
||||
.notification-bar > .notification-bar-item > .button {
|
||||
-fx-base: orange;
|
||||
-fx-font-weight: bold;
|
||||
-fx-font-size: 12;
|
||||
-fx-text-fill: white;
|
||||
-fx-background-insets: 1;
|
||||
-fx-background-radius: 5;
|
||||
}
|
||||
|
||||
.thin-progress-bar > .bar {
|
||||
-fx-padding: 8;
|
||||
}
|
||||
|
||||
.thin-progress-bar > .track {
|
||||
-fx-background-color: #bce7f5;
|
||||
-fx-background-insets: 3 3 4 3;
|
||||
/*-fx-background-radius: 0.583em; *//* 7 */
|
||||
-fx-background-radius: 2;
|
||||
-fx-padding: 8;
|
||||
}
|
143
explorer/src/main/resources/com/r3corda/explorer/css/shell.css
Normal file
@ -0,0 +1,143 @@
|
||||
.root {
|
||||
-fx-font-family: "Roboto Light", sans-serif;
|
||||
-fx-font-size: 12pt;
|
||||
/* Setting the background colour explicitly is required for a correct fade/blur animation. */
|
||||
-fx-background-color: white;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
-fx-background-color: #494949;
|
||||
}
|
||||
|
||||
.sidebar-right-shadow {
|
||||
-fx-background-color: linear-gradient(to left, #1c1c1c, #494949);
|
||||
-fx-border-color: black;
|
||||
-fx-border-width: 0 0.1em 0 0;
|
||||
-fx-padding: 0 0.2em 0 0.2em;
|
||||
}
|
||||
|
||||
.sidebar-button {
|
||||
-fx-base: transparent;
|
||||
-fx-background-color: transparent;
|
||||
-fx-text-fill: white;
|
||||
-fx-graphic-text-gap: 1em;
|
||||
-fx-cursor: hand;
|
||||
-fx-effect: dropshadow(three-pass-box, black, 6, 0.0, 0, 0);
|
||||
}
|
||||
|
||||
.sidebar-button:selected {
|
||||
-fx-background-color: grey;
|
||||
}
|
||||
|
||||
.sidebar-icon {
|
||||
-fx-font-size: 25pt;
|
||||
}
|
||||
|
||||
.modal-window {
|
||||
-fx-background-color: white;
|
||||
-fx-background-radius: 5;
|
||||
-fx-effect: dropshadow(three-pass-box, black, 10, 0.0, 0, 0);
|
||||
}
|
||||
|
||||
.modal-window > .title-bar {
|
||||
-fx-background-radius: 5 5 0 0;
|
||||
-fx-background-color: #3c777b;
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.modal-window > .title-bar > Button {
|
||||
-fx-base: transparent;
|
||||
-fx-background-color: transparent;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.modal-window > .title-bar > Button:hover {
|
||||
-fx-base: #3c777b;
|
||||
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
|
||||
.modal-window > .title-bar > Label {
|
||||
-fx-text-fill: white;
|
||||
-fx-font-size: 120%;
|
||||
}
|
||||
|
||||
.large-font {
|
||||
-fx-font-size: 200%;
|
||||
}
|
||||
|
||||
/********************************************************************************************************************
|
||||
*
|
||||
* Buttons
|
||||
*
|
||||
*/
|
||||
|
||||
.flat-button {
|
||||
-fx-background-color: white;
|
||||
-fx-padding: 0 0 0 0;
|
||||
-fx-font-size: 10pt;
|
||||
}
|
||||
|
||||
.flat-button:hover {
|
||||
-fx-underline: true;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
.flat-button:focused {
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.fat-buttons {
|
||||
-fx-spacing: 15.0;
|
||||
}
|
||||
|
||||
.fat-buttons Button {
|
||||
-fx-padding: 10 15 10 15;
|
||||
-fx-min-width: 100;
|
||||
-fx-font-weight: bold;
|
||||
-fx-base: whitesmoke;
|
||||
}
|
||||
|
||||
.fat-buttons Button:default {
|
||||
-fx-base: orange;
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
|
||||
.fat-buttons Button:cancel {
|
||||
-fx-background-color: white;
|
||||
-fx-background-insets: 1;
|
||||
-fx-border-color: lightgray;
|
||||
-fx-border-radius: 3;
|
||||
-fx-text-fill: black;
|
||||
}
|
||||
|
||||
.fat-buttons Button:cancel:hover {
|
||||
-fx-base: white;
|
||||
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
|
||||
-fx-text-fill: black;
|
||||
}
|
||||
|
||||
/** take out the focus ring */
|
||||
.no-focus-button:focused {
|
||||
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
|
||||
-fx-background-insets: 0 0 -1 0, 0, 1, 2;
|
||||
-fx-background-radius: 3px, 3px, 2px, 1px;
|
||||
}
|
||||
|
||||
.blue-button {
|
||||
-fx-base: lightblue;
|
||||
-fx-text-fill: darkslategrey;
|
||||
}
|
||||
|
||||
.blue-button:disabled {
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
|
||||
.green-button {
|
||||
-fx-base: #62c462;
|
||||
-fx-text-fill: darkslategrey;
|
||||
}
|
||||
|
||||
.green-button:disabled {
|
||||
-fx-text-fill: white;
|
||||
}
|
400
explorer/src/main/resources/com/r3corda/explorer/css/wallet.css
Normal file
@ -0,0 +1,400 @@
|
||||
#topLevel.root {
|
||||
-fx-background-image:url('../images/r3bg.png');
|
||||
-fx-background-size: cover;
|
||||
-fx-background-repeat:no-repeat;
|
||||
-fx-base:white;
|
||||
}
|
||||
|
||||
#cashViewer {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
#cashViewerTable {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
#cashViewerTable .tree-table-row-cell {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
.root {
|
||||
-fx-padding:5px;
|
||||
}
|
||||
|
||||
.root {
|
||||
-fx-padding:5px;
|
||||
}
|
||||
|
||||
.dialog-pane {
|
||||
-fx-background-color:rgba(255,255,255,0.7);
|
||||
-fx-background-radius:2px;
|
||||
-fx-border-radius: 2px;
|
||||
-fx-border-color: rgb(20,136,204);
|
||||
}
|
||||
|
||||
|
||||
.nested-column-header, .nested-column-header {
|
||||
-fx-background-color:transparent;
|
||||
-fx-wrap-text:true;
|
||||
-fx-border-color:transparent;
|
||||
}
|
||||
|
||||
|
||||
.text-field,
|
||||
.table-column,
|
||||
.label,
|
||||
.title,
|
||||
.combo-box,
|
||||
.button,
|
||||
.split-menu-button,
|
||||
.choice-box {
|
||||
-fx-font-family:Effra;
|
||||
-fx-font-size:1em;
|
||||
-fx-text-fill:rgb(63,63,63);
|
||||
-fx-font-smoothing-type: gray;
|
||||
}
|
||||
|
||||
.text-highlight {
|
||||
-fx-text-fill:rgb(20,136,204);
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
-fx-background-color:rgba(255,255,255,0.9);
|
||||
}
|
||||
|
||||
.titled-pane .content,
|
||||
.split-menu-button .label,
|
||||
.split-menu-button .arrow-button,
|
||||
.titled-pane .split-pane, .scroll-pane {
|
||||
-fx-background-color:transparent;
|
||||
}
|
||||
|
||||
|
||||
.text-field,
|
||||
.tree-table-view,
|
||||
.table-view,
|
||||
.accordion,
|
||||
.combo-box,
|
||||
.context-menu,
|
||||
.button,
|
||||
.split-menu-button,
|
||||
.choice-box,
|
||||
.titled-pane .title {
|
||||
-fx-border-color:rgb(150,150,150);
|
||||
-fx-border-width:1px;
|
||||
|
||||
}
|
||||
|
||||
.text-field:focused,
|
||||
.tree-table-view:focused,
|
||||
.table-view:focused,
|
||||
.accordion:focused,
|
||||
.combo-box:focused,
|
||||
.context-menu:focused,
|
||||
.button:focused,
|
||||
.split-menu-button:focused,
|
||||
.text-field:hover,
|
||||
.button:hover,
|
||||
.split-menu-button:hover,
|
||||
.choice-box:hover,
|
||||
.titled-pane:hover .title {
|
||||
-fx-border-color:rgb(20,136,204);
|
||||
}
|
||||
|
||||
.split-menu-button:pressed,
|
||||
.button:pressed,
|
||||
.choice-box:pressed,
|
||||
.titled-pane:expanded .title {
|
||||
-fx-background-color:rgb(20,136,204);
|
||||
-fx-text-fill:white;
|
||||
}
|
||||
|
||||
.titled-pane:expanded .title .text {
|
||||
-fx-fill:white;
|
||||
}
|
||||
.titled-pane .title,.titled-pane .title:hover {
|
||||
-fx-border-width:0.5px;
|
||||
}
|
||||
|
||||
.text-field, .combo-box, .choice-box, .password-field {
|
||||
-fx-background-color:rgba(255,255,255,0.5);
|
||||
-fx-background-radius:2px;
|
||||
-fx-border-radius: 2px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
/* switch off highlighting for text-field when it's inside a combo-box */
|
||||
.combo-box .text-field, .combo-box .text-field:hover, .combo-box .text-field:focused {
|
||||
-fx-border-color:transparent;
|
||||
}
|
||||
/* table formatting */
|
||||
|
||||
.column-header-background,
|
||||
.table-column,
|
||||
.tree-table-row-cell, .column-header-background .filler {
|
||||
-fx-background-color:transparent;
|
||||
-fx-label-padding:3px;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.nested-column-header .label { -fx-wrap-text:true}
|
||||
|
||||
.table-column { -fx-border-style:solid;
|
||||
-fx-border-color:rgb(216,216,216); /*t r b l */
|
||||
|
||||
-fx-border-width:0.5px;
|
||||
-fx-border-insets: 1.5px;
|
||||
-fx-background-insets: 2px;
|
||||
}
|
||||
|
||||
|
||||
.tree-table-row-cell:even .table-column,
|
||||
.table-row-cell:even .table-column,
|
||||
.title, .split-menu-button, .button {
|
||||
-fx-background-color: rgba(20,136,204,0.2);
|
||||
}
|
||||
|
||||
.table-row-cell:selected, .tree-table-row-cell:selected {
|
||||
-fx-background-color:transparent;
|
||||
}
|
||||
.tree-table-row-cell:selected .table-column,
|
||||
.tree-table-row-cell:focused .table-column,
|
||||
.table-row-cell:selected .table-column,
|
||||
.table-row-cell:focused .table-column {
|
||||
-fx-background-color:rgba(20,136,204,0.8);
|
||||
|
||||
|
||||
}
|
||||
.bad .text {
|
||||
-fx-fill:rgb(236,29,36);
|
||||
}
|
||||
|
||||
.table-row-cell:focused .table-column .text,
|
||||
.tree-table-row-cell:focused .table-column .text {
|
||||
-fx-fill:white;
|
||||
}
|
||||
|
||||
.table-column:hover,
|
||||
.table-row-cell:hover .first-column,
|
||||
.table-row-cell:hover .second-column,
|
||||
.tree-table-row-cell:hover .first-column,
|
||||
.tree-table-row-cell:hover .second-column {
|
||||
-fx-border-color:rgb(20,136,204);
|
||||
}
|
||||
|
||||
.tree-table-view .column-header-background .nested-column-header .table-column,
|
||||
.table-view .column-header-background .nested-column-header .table-column {
|
||||
-fx-border-color:transparent;
|
||||
}
|
||||
|
||||
/* Special formatting - columns to be presented with no join between them */
|
||||
|
||||
.first-column {
|
||||
-fx-border-width:0.5px 0px 0.5px 0.5px;
|
||||
-fx-border-insets: 1.5px 0px 1.5px 1.5px;
|
||||
-fx-background-insets: 2px 0px 2px 2px;
|
||||
}
|
||||
|
||||
|
||||
.second-column {
|
||||
-fx-border-width:0.5px 0.5px 0.5px 0px;
|
||||
-fx-border-insets: 1.5px 1.5px 1.5px 0px;
|
||||
-fx-background-insets: 2px 2px 2px 0px;
|
||||
}
|
||||
|
||||
|
||||
/* highlighting where the user has typed a key */
|
||||
.tree-table-view text-area, .table-view text-area{
|
||||
-fx-font-weight:bold;
|
||||
-fx-fill:rgb(20,136,204);
|
||||
|
||||
}
|
||||
|
||||
.tree-table-row-cell:selected .table-column text-area,
|
||||
.table-row-cell:selected .table-column text-area
|
||||
{
|
||||
-fx-font-weight:bold;
|
||||
-fx-fill:rgb(255,255,255);
|
||||
}
|
||||
|
||||
|
||||
/* labels */
|
||||
.dialog-pane .header-panel .label .text{
|
||||
-fx-font-size:1em;
|
||||
-fx-fill:rgb(20,136,204);
|
||||
}
|
||||
|
||||
#headline, .headline {
|
||||
-fx-font-size:2.4em;
|
||||
}
|
||||
#subline, .subline {
|
||||
-fx-font-size:1.4em;
|
||||
}
|
||||
#headline, #subline {
|
||||
-fx-text-fill:rgb(65,65,65);
|
||||
-fx-padding:0px;
|
||||
|
||||
}
|
||||
|
||||
/* search boxes */
|
||||
.search {
|
||||
-fx-background-image:url('../images/search.png');
|
||||
-fx-background-size:Auto 16px;
|
||||
-fx-background-repeat:no-repeat;
|
||||
-fx-background-position:8px center;
|
||||
-fx-padding:5px 5px 5px 30px;
|
||||
-fx-background-radius: 2px;
|
||||
-fx-border-radius: 2px;
|
||||
}
|
||||
|
||||
.search-clear {
|
||||
-fx-image:url('../images/clear_inactive.png');
|
||||
}
|
||||
|
||||
.search-clear:hover {
|
||||
-fx-image:url('../images/clear.png');
|
||||
}
|
||||
|
||||
.split-menu-button, .button, .choice-box {
|
||||
-fx-background-radius:2px;
|
||||
-fx-border-radius: 2px;
|
||||
-fx-border-insets: 0.5px;
|
||||
-fx-background-insets:0.5px;
|
||||
|
||||
}
|
||||
|
||||
.tree-table-row-cell .monetary-value, .monetary-value .label, .table-row-cell .monetary-value {
|
||||
-fx-alignment:center-right;
|
||||
}
|
||||
|
||||
|
||||
/* split panes */
|
||||
.split-pane-divider {
|
||||
-fx-background-color: transparent;
|
||||
-fx-border-color: rgb(160,160,160);
|
||||
-fx-border-width: 0 0 0 0.5px
|
||||
}
|
||||
|
||||
/* Dashboard tiles */
|
||||
|
||||
.tile,.tile-user {
|
||||
-fx-padding: 10px;
|
||||
-fx-pref-height:200px; -fx-pref-width:200px;
|
||||
|
||||
|
||||
}
|
||||
.tile .title, .tile:expanded .title,
|
||||
.tile-user .title, .tile-user:expanded .title {
|
||||
-fx-alignment:center-right;
|
||||
-fx-font-size:1.4em;
|
||||
-fx-font-weight:bold;
|
||||
-fx-cursor:hand;
|
||||
-fx-background-radius:2px 2px 0 0;
|
||||
-fx-border-radius: 2px 2px 0 0;
|
||||
-fx-border-width:1px 1px 0 1px;
|
||||
-fx-background-color: rgba(255,255,255,0.5);
|
||||
-fx-border-color:rgb(160,160,160); /*t r b l */
|
||||
|
||||
}
|
||||
|
||||
.tile .title .text, .tile:expanded .title .text,
|
||||
.tile-user .title .text, .tile-user:expanded .title .text {
|
||||
-fx-fill:rgb(65,65,65);
|
||||
}
|
||||
.tile .content,
|
||||
.tile-user .content {
|
||||
-fx-background-color: rgba(255,255,255,0.7);
|
||||
-fx-background-size:Auto 90%;
|
||||
-fx-background-repeat:no-repeat;
|
||||
-fx-background-position:center center;
|
||||
-fx-cursor:hand;
|
||||
-fx-background-radius:0 0 2px 2px;
|
||||
-fx-border-radius: 0 0 2px 2px;
|
||||
-fx-padding:0px;
|
||||
-fx-alignment:bottom-right;
|
||||
-fx-border-color:rgb(150,150,150); /*t r b l */
|
||||
}
|
||||
.tile .label,
|
||||
.tile-user .label {
|
||||
-fx-font-size:2.4em;
|
||||
-fx-padding:20px;
|
||||
-fx-text-fill:rgb(65,65,65);
|
||||
-fx-font-weight:normal;
|
||||
-fx-text-alignment:right;
|
||||
|
||||
}
|
||||
|
||||
.tile:hover .label,
|
||||
.tile-user:hover .label {
|
||||
-fx-padding:24px;
|
||||
}
|
||||
|
||||
.tile:hover .content, .tile:hover .title,
|
||||
.tile-user:hover .content, .tile-user:hover .title {
|
||||
-fx-border-color:rgb(20,136,204);
|
||||
-fx-background-color: rgb(20,136,204);
|
||||
|
||||
}
|
||||
.tile:hover, .tile-user:hover {
|
||||
-fx-padding:4px;
|
||||
}
|
||||
|
||||
.tile:hover .label, .tile:hover .label .text, .tile:hover .title .text {
|
||||
-fx-text-fill:rgb(255,255,255);
|
||||
-fx-fill:rgb(255,255,255);
|
||||
-fx-font-weight:bold;
|
||||
-fx-effect:none;
|
||||
}
|
||||
|
||||
#tile_cash .content {
|
||||
-fx-background-image:url('../images/cash_lrg.png');
|
||||
}
|
||||
#tile_debtors .content {
|
||||
-fx-background-image:url('../images/outflow_lrg.png');
|
||||
}
|
||||
#tile_creditors .content {
|
||||
-fx-background-image:url('../images/inflow_lrg.png');
|
||||
}
|
||||
#tile_tx .content {
|
||||
-fx-background-image:url('../images/tx_lrg.png');
|
||||
}
|
||||
|
||||
#tile_cpty .content {
|
||||
-fx-background-image:url('../images/cpty_lrg.png');
|
||||
}
|
||||
|
||||
.tile-user .content {
|
||||
-fx-background-image:url('../images/user_b.png');
|
||||
}
|
||||
.tile-user-test-man .content {
|
||||
-fx-background-image:url('../images/man1.png');
|
||||
-fx-background-size:cover;
|
||||
}
|
||||
.tile-user-test-woman .content {
|
||||
-fx-background-image:url('../images/woman1.png');
|
||||
-fx-background-size:cover;
|
||||
}
|
||||
|
||||
.tile-user .label {
|
||||
-fx-background-color:rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.tile-user:hover .title, .tile-user:hover .content {
|
||||
-fx-background-color:rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.counterparty {
|
||||
-fx-background-image:url('../images/inst_128.png');
|
||||
-fx-background-size:Auto 16px;
|
||||
-fx-background-repeat: no-repeat;
|
||||
-fx-background-position:0px center;
|
||||
-fx-padding:0 0 0 20px;
|
||||
}
|
||||
|
||||
.state-panel{
|
||||
-fx-background-color: rgba(255,255,255,0.7);
|
||||
-fx-border-color:rgb(150,150,150);
|
||||
-fx-insets:5px
|
||||
}
|
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.5 KiB |
BIN
explorer/src/main/resources/com/r3corda/explorer/images/cash.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 18 KiB |
BIN
explorer/src/main/resources/com/r3corda/explorer/images/home.png
Normal file
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 7.0 KiB |
BIN
explorer/src/main/resources/com/r3corda/explorer/images/inst.png
Normal file
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 4.4 KiB |
BIN
explorer/src/main/resources/com/r3corda/explorer/images/man1.png
Normal file
After Width: | Height: | Size: 326 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 3.7 KiB |
BIN
explorer/src/main/resources/com/r3corda/explorer/images/tx.png
Normal file
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 12 KiB |
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg id="svg15261" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="297mm" width="210mm" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" viewBox="0 0 744.09448819 1052.3622047">
|
||||
<metadata id="metadata15266">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title/>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g id="layer1">
|
||||
<g id="g16332" transform="matrix(22.359 0 0 22.359 -19527 -23416)" fill="#1488cc">
|
||||
<g id="g45283" fill="#1488cc">
|
||||
<path id="path6608" d="m891.63 1061.7c0 2.0866-1.695 3.7802-3.7844 3.7802-2.0872 0-3.7822-1.6936-3.7822-3.7802 0-2.0906 1.695-3.7826 3.7822-3.7826 2.0894 0 3.7844 1.692 3.7844 3.7826"/>
|
||||
<path id="path6610" d="m894.18 1076.1c0 2.4219-1.633 2.7474-3.6493 2.7474h-5.3908c-2.0119 0-3.6448-0.3255-3.6448-2.7474l1.0547-6.3223c0-2.02 1.3627-3.6582 3.0422-3.6582h4.4913c1.6795 0 3.04 1.6382 3.04 3.6582l1.0569 6.3223"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 8.8 KiB |
After Width: | Height: | Size: 485 KiB |
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<HBox spacing="5.0" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<VBox HBox.hgrow="SOMETIMES">
|
||||
<children>
|
||||
<Label text="State ID" VBox.vgrow="ALWAYS" />
|
||||
<Label text="Issuer" />
|
||||
<Label text="Originated" wrapText="true" />
|
||||
<Label text="Amount" wrapText="true" />
|
||||
<Label fx:id="equivLabel" text="USD" wrapText="true" />
|
||||
</children>
|
||||
</VBox>
|
||||
<VBox HBox.hgrow="ALWAYS">
|
||||
<children>
|
||||
<Label fx:id="stateIdValueLabel" text="39043-329090-390091" />
|
||||
<Label fx:id="issuerValueLabel" styleClass="counterparty" text="C-03820 HSBC GROUP PLC" />
|
||||
<Label fx:id="originatedValueLabel" text="2018-04-27 11:34 UTC" />
|
||||
<Label fx:id="amountValueLabel" text="GBP 0.00" wrapText="true" />
|
||||
<Label fx:id="equivValueLabel" text="0.00" wrapText="true" />
|
||||
</children>
|
||||
</VBox>
|
||||
</children>
|
||||
</HBox>
|
@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import java.lang.String?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ListView?>
|
||||
<?import javafx.scene.control.SplitPane?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.control.TreeTableColumn?>
|
||||
<?import javafx.scene.control.TreeTableView?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<SplitPane fx:id="topSplitPane" dividerPositions="0.5" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<items>
|
||||
<VBox fx:id="leftPane" spacing="5.0" styleClass="root">
|
||||
<children>
|
||||
<StackPane alignment="CENTER_RIGHT">
|
||||
<VBox.margin>
|
||||
<Insets />
|
||||
</VBox.margin>
|
||||
<children>
|
||||
<TextField id="search" fx:id="searchCriteriaTextField" promptText="Search by issuer/currency" styleClass="search">
|
||||
<opaqueInsets>
|
||||
<Insets />
|
||||
</opaqueInsets>
|
||||
<StackPane.margin>
|
||||
<Insets />
|
||||
</StackPane.margin>
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
</TextField>
|
||||
<ImageView fx:id="searchCancelImageView" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
|
||||
<image>
|
||||
<Image url="@../../images/clear_inactive.png" />
|
||||
</image>
|
||||
<StackPane.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</StackPane.margin>
|
||||
</ImageView>
|
||||
</children>
|
||||
</StackPane>
|
||||
<Label fx:id="totalMatchingLabel" alignment="BOTTOM_LEFT" text="Total 15 matching issuer(s)" wrapText="true">
|
||||
<VBox.margin>
|
||||
<Insets bottom="5.0" right="10.0" top="5.0" />
|
||||
</VBox.margin>
|
||||
</Label>
|
||||
<TreeTableView fx:id="cashViewerTable" showRoot="false" VBox.vgrow="ALWAYS">
|
||||
<columns>
|
||||
<TreeTableColumn fx:id="cashViewerTableIssuerCurrency" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefWidth="100.0" styleClass="first-column" text="Issuer/Currency" />
|
||||
<TreeTableColumn fx:id="cashViewerTableLocalCurrency" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefWidth="132.0" text="Local currency">
|
||||
<styleClass>
|
||||
<String fx:value="monetary-value" />
|
||||
<String fx:value="second-column" />
|
||||
</styleClass>
|
||||
</TreeTableColumn>
|
||||
<TreeTableColumn fx:id="cashViewerTableEquiv" maxWidth="1.7976931348623157E308" minWidth="-1.0" prefWidth="72.0" styleClass="monetary-value" text="Equiv" />
|
||||
</columns>
|
||||
</TreeTableView>
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
</VBox>
|
||||
<VBox fx:id="rightPane" spacing="5.0">
|
||||
<children>
|
||||
<Button mnemonicParsing="false" text=">>" />
|
||||
<Label fx:id="totalPositionsLabel" styleClass="subline" text="Total 18 position(s)" />
|
||||
<Label fx:id="equivSumLabel" styleClass="headline" text="USD 394.6k" />
|
||||
<ListView fx:id="cashStatesList" VBox.vgrow="ALWAYS" />
|
||||
</children>
|
||||
<opaqueInsets>
|
||||
<Insets />
|
||||
</opaqueInsets>
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
</VBox>
|
||||
</items>
|
||||
</SplitPane>
|
@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.MenuItem?>
|
||||
<?import javafx.scene.control.SplitMenuButton?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<VBox spacing="5.0">
|
||||
<children>
|
||||
<HBox>
|
||||
<children>
|
||||
<HBox alignment="CENTER_LEFT" spacing="5.0" HBox.hgrow="ALWAYS">
|
||||
<children>
|
||||
<VBox fx:id="sectionIconContainer" alignment="CENTER">
|
||||
<children>
|
||||
<ImageView fx:id="sectionIcon" fitHeight="30.0" fitWidth="30.0" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../../images/home.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</children>
|
||||
</VBox>
|
||||
<Label id="headline" fx:id="sectionLabel" text="Home" />
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
</HBox>
|
||||
<HBox alignment="TOP_RIGHT" spacing="5.0">
|
||||
<children>
|
||||
<Button fx:id="debugNextButton" mnemonicParsing="false" text="Next" />
|
||||
<Button fx:id="debugGoStopButton" mnemonicParsing="false" text="!!!" />
|
||||
<SplitMenuButton mnemonicParsing="false" text="DRUTTER">
|
||||
<items>
|
||||
<MenuItem mnemonicParsing="false" text="Sign out" />
|
||||
<MenuItem mnemonicParsing="false" text="Account settings..." />
|
||||
</items>
|
||||
<graphic>
|
||||
<ImageView fitHeight="20.0" fitWidth="52.0" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../../images/user_w.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
</SplitMenuButton>
|
||||
<Button fx:id="settingsButton" mnemonicParsing="false" text="Settings">
|
||||
<graphic>
|
||||
<ImageView fitHeight="20.0" fitWidth="20.0" pickOnBounds="true" preserveRatio="true">
|
||||
<image>
|
||||
<Image url="@../../images/settings_w.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</graphic>
|
||||
</Button>
|
||||
</children>
|
||||
</HBox>
|
||||
</children>
|
||||
</HBox>
|
||||
<StackPane alignment="CENTER_RIGHT">
|
||||
<children>
|
||||
<TextField id="search_main" promptText="Search for states, transactions, counterparties etc." styleClass="search">
|
||||
<opaqueInsets>
|
||||
<Insets />
|
||||
</opaqueInsets>
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="30.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
</TextField>
|
||||
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
|
||||
<image>
|
||||
<Image url="@../../images/clear_inactive.png" />
|
||||
</image>
|
||||
<StackPane.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</StackPane.margin>
|
||||
</ImageView>
|
||||
</children>
|
||||
</StackPane>
|
||||
</children>
|
||||
</VBox>
|
||||
<StackPane alignment="CENTER_RIGHT" />
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
</VBox>
|
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.TitledPane?>
|
||||
<?import javafx.scene.layout.TilePane?>
|
||||
|
||||
<TilePane prefHeight="425.0" prefWidth="425.0" tileAlignment="TOP_LEFT" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<TitledPane id="tile_cash" fx:id="ourCashPane" alignment="CENTER" collapsible="false" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our cash">
|
||||
<content>
|
||||
<Label fx:id="ourCashLabel" text="USD 186.7m" textAlignment="CENTER" wrapText="true" />
|
||||
</content>
|
||||
</TitledPane>
|
||||
<TitledPane id="tile_debtors" fx:id="ourDebtorsPane" alignment="CENTER" collapsible="false" layoutX="232.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our debtors">
|
||||
<content>
|
||||
<Label text="USD 71.3m" textAlignment="CENTER" wrapText="true" />
|
||||
</content>
|
||||
</TitledPane>
|
||||
<TitledPane id="tile_creditors" fx:id="ourCreditorsPane" alignment="CENTER" collapsible="false" layoutX="312.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our creditors">
|
||||
<content>
|
||||
<Label text="USD (29.4m)" textAlignment="CENTER" wrapText="true" />
|
||||
</content>
|
||||
</TitledPane>
|
||||
<TitledPane id="tile_tx" fx:id="ourTransactionsPane" alignment="CENTER" collapsible="false" layoutX="392.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile" text="Our transactions">
|
||||
<content>
|
||||
<Label fx:id="ourTransactionsLabel" text="In flight: 1,315" textAlignment="CENTER" wrapText="true" />
|
||||
</content>
|
||||
</TitledPane>
|
||||
</children>
|
||||
</TilePane>
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.layout.BorderPane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<VBox fx:id="topLevel" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<BorderPane fx:id="selectionBorderPane" />
|
||||
</children>
|
||||
</VBox>
|
@ -0,0 +1,147 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import java.lang.String?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ListView?>
|
||||
<?import javafx.scene.control.SplitPane?>
|
||||
<?import javafx.scene.control.TableColumn?>
|
||||
<?import javafx.scene.control.TableView?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.control.TitledPane?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<VBox styleClass="view" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<StackPane alignment="CENTER_RIGHT">
|
||||
<children>
|
||||
<TextField promptText="Filter transactions by originator, contract type..." styleClass="search">
|
||||
<opaqueInsets>
|
||||
<Insets />
|
||||
</opaqueInsets>
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="30.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
<StackPane.margin>
|
||||
<Insets bottom="5.0" top="5.0" />
|
||||
</StackPane.margin>
|
||||
</TextField>
|
||||
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
|
||||
<image>
|
||||
<Image url="@../../../../../../../../../internal/explorer/src/main/resources/images/clear_inactive.png" />
|
||||
</image>
|
||||
<StackPane.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</StackPane.margin>
|
||||
</ImageView>
|
||||
</children>
|
||||
</StackPane>
|
||||
<SplitPane fx:id="topSplitPane" dividerPositions="0.3, 0.6, 0.7" orientation="VERTICAL" prefHeight="562.0" prefWidth="1087.0" VBox.vgrow="ALWAYS">
|
||||
<items>
|
||||
<TableView fx:id="transactionViewTable" prefHeight="200.0" prefWidth="200.0">
|
||||
<columns>
|
||||
<TableColumn fx:id="transactionViewTransactionId" prefWidth="75.0" text="Transaction ID" />
|
||||
<TableColumn fx:id="transactionViewFiberId" prefWidth="187.0" text="Fiber ID" />
|
||||
<TableColumn fx:id="transactionViewClientUuid" prefWidth="75.0" text="Client UUID" />
|
||||
<TableColumn fx:id="transactionViewTransactionStatus" prefWidth="75.0" text="Transaction status" />
|
||||
<TableColumn fx:id="transactionViewProtocolStatus" prefWidth="75.0" text="Protocol status" />
|
||||
<TableColumn fx:id="transactionViewStateMachineStatus" prefWidth="75.0" text="SM Status" />
|
||||
<TableColumn fx:id="transactionViewCommandTypes" prefWidth="75.0" text="Command type(s)" />
|
||||
<TableColumn fx:id="transactionViewTotalValueEquiv" prefWidth="75.0" styleClass="monetary-value" text="Total value (USD equiv)" />
|
||||
</columns>
|
||||
<columnResizePolicy>
|
||||
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
|
||||
</columnResizePolicy>
|
||||
</TableView>
|
||||
<TitledPane fx:id="contractStatesTitledPane" animated="false" text="Contract states">
|
||||
<content>
|
||||
<SplitPane dividerPositions="0.5" prefHeight="160.0" prefWidth="200.0">
|
||||
<items>
|
||||
<VBox prefHeight="200.0" prefWidth="100.0">
|
||||
<children>
|
||||
<HBox spacing="5.0">
|
||||
<children>
|
||||
<Label text="Inputs:" />
|
||||
<Label fx:id="contractStatesInputsCountLabel" text="Label" />
|
||||
</children>
|
||||
</HBox>
|
||||
<TableView fx:id="contractStatesInputStatesTable" prefHeight="200.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
|
||||
<columns>
|
||||
<TableColumn fx:id="contractStatesInputStatesId" prefWidth="75.0" text="ID" />
|
||||
<TableColumn fx:id="contractStatesInputStatesType" prefWidth="75.0" text="Type" />
|
||||
<TableColumn fx:id="contractStatesInputStatesOwner" prefWidth="75.0" text="Owner" />
|
||||
<TableColumn fx:id="contractStatesInputStatesLocalCurrency" prefWidth="75.0" styleClass="first-column" text="Local Ccy" />
|
||||
<TableColumn fx:id="contractStatesInputStatesAmount" prefWidth="75.0" text="Amount">
|
||||
<styleClass>
|
||||
<String fx:value="second-column" />
|
||||
<String fx:value="monetary-value" />
|
||||
</styleClass>
|
||||
</TableColumn>
|
||||
<TableColumn fx:id="contractStatesInputStatesEquiv" prefWidth="75.0" text="USD Equiv" />
|
||||
</columns>
|
||||
<columnResizePolicy>
|
||||
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
|
||||
</columnResizePolicy>
|
||||
</TableView>
|
||||
</children>
|
||||
</VBox>
|
||||
<VBox prefHeight="200.0" prefWidth="100.0">
|
||||
<children>
|
||||
<HBox spacing="5.0">
|
||||
<children>
|
||||
<Label text="Outputs:" />
|
||||
<Label fx:id="contractStatesOutputsCountLabel" text="Label" />
|
||||
</children>
|
||||
</HBox>
|
||||
<TableView fx:id="contractStatesOutputStatesTable" prefHeight="200.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
|
||||
<columns>
|
||||
<TableColumn fx:id="contractStatesOutputStatesId" prefWidth="75.0" text="ID" />
|
||||
<TableColumn fx:id="contractStatesOutputStatesType" prefWidth="75.0" text="Type" />
|
||||
<TableColumn fx:id="contractStatesOutputStatesOwner" prefWidth="75.0" text="Owner" />
|
||||
<TableColumn fx:id="contractStatesOutputStatesLocalCurrency" prefWidth="75.0" styleClass="first-column" text="Local Ccy" />
|
||||
<TableColumn fx:id="contractStatesOutputStatesAmount" prefWidth="75.0" text="Amount">
|
||||
<styleClass>
|
||||
<String fx:value="second-column" />
|
||||
<String fx:value="monetary-value" />
|
||||
</styleClass>
|
||||
</TableColumn>
|
||||
<TableColumn fx:id="contractStatesOutputStatesEquiv" prefWidth="75.0" text="USD Equiv" />
|
||||
</columns>
|
||||
<columnResizePolicy>
|
||||
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
|
||||
</columnResizePolicy>
|
||||
</TableView>
|
||||
</children>
|
||||
</VBox>
|
||||
</items>
|
||||
</SplitPane>
|
||||
</content>
|
||||
</TitledPane>
|
||||
<TitledPane fx:id="signaturesTitledPane" animated="false" text="Required signatures">
|
||||
<content>
|
||||
<ListView fx:id="signaturesList" />
|
||||
</content>
|
||||
</TitledPane>
|
||||
<TitledPane fx:id="lowLevelEventsTitledPane" animated="false" text="Low level events">
|
||||
<content>
|
||||
<TableView fx:id="lowLevelEventsTable">
|
||||
<columns>
|
||||
<TableColumn fx:id="lowLevelEventsTimestamp" prefWidth="102.0" text="Timestamp" />
|
||||
<TableColumn fx:id="lowLevelEventsEvent" prefWidth="138.0" text="Event" />
|
||||
</columns>
|
||||
</TableView>
|
||||
</content>
|
||||
</TitledPane>
|
||||
</items>
|
||||
</SplitPane>
|
||||
<HBox>
|
||||
<children>
|
||||
<Label fx:id="matchingTransactionsLabel" text="matching transaction(s)" />
|
||||
</children>
|
||||
</HBox>
|
||||
</children>
|
||||
</VBox>
|
@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import java.lang.String?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.ComboBox?>
|
||||
<?import javafx.scene.control.DialogPane?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.PasswordField?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.control.TitledPane?>
|
||||
<?import javafx.scene.layout.TilePane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<DialogPane expanded="true" headerText="Sign in to Corda" prefHeight="246.0" prefWidth="648.0" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<content>
|
||||
<TilePane alignment="CENTER">
|
||||
<children>
|
||||
<TitledPane alignment="CENTER" collapsible="false" prefHeight="160.0" prefWidth="160.0" text="Log in">
|
||||
<content>
|
||||
<Label alignment="BOTTOM_RIGHT" prefHeight="200.0" prefWidth="200.0" text="Thomas" textAlignment="CENTER" wrapText="true" />
|
||||
</content>
|
||||
<styleClass>
|
||||
<String fx:value="tile-user" />
|
||||
<String fx:value="tile-user-test-man" />
|
||||
</styleClass>
|
||||
</TitledPane>
|
||||
<TitledPane alignment="CENTER" collapsible="false" layoutX="232.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" text="Log in">
|
||||
<styleClass>
|
||||
<String fx:value="tile-user" />
|
||||
<String fx:value="tile-user-test-woman" />
|
||||
</styleClass>
|
||||
<content>
|
||||
<Label alignment="BOTTOM_RIGHT" prefHeight="200.0" prefWidth="200.0" text="Theresa" textAlignment="CENTER" wrapText="true" />
|
||||
</content>
|
||||
</TitledPane>
|
||||
<TitledPane alignment="CENTER" collapsible="false" layoutX="312.0" layoutY="10.0" prefHeight="160.0" prefWidth="160.0" styleClass="tile-user" text="Log in">
|
||||
<content>
|
||||
<Label alignment="BOTTOM_RIGHT" prefHeight="200.0" prefWidth="200.0" text="Other user" textAlignment="CENTER" wrapText="true" />
|
||||
</content>
|
||||
</TitledPane>
|
||||
</children>
|
||||
</TilePane>
|
||||
</content>
|
||||
<expandableContent>
|
||||
<VBox alignment="TOP_CENTER">
|
||||
<children>
|
||||
<ComboBox editable="true" maxWidth="300.0" prefWidth="300.0" promptText="Server name">
|
||||
<VBox.margin>
|
||||
<Insets top="5.0" />
|
||||
</VBox.margin>
|
||||
</ComboBox>
|
||||
<TextField maxWidth="300.0" promptText="User name">
|
||||
<VBox.margin>
|
||||
<Insets top="5.0" />
|
||||
</VBox.margin>
|
||||
</TextField>
|
||||
<PasswordField maxWidth="300.0" prefWidth="300.0" promptText="Password">
|
||||
<VBox.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</VBox.margin>
|
||||
</PasswordField>
|
||||
</children>
|
||||
</VBox>
|
||||
</expandableContent>
|
||||
</DialogPane>
|
@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import java.lang.String?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
<?import javafx.scene.control.TreeTableColumn?>
|
||||
<?import javafx.scene.control.TreeTableView?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<StackPane alignment="CENTER_RIGHT">
|
||||
<VBox.margin>
|
||||
<Insets bottom="5.0" />
|
||||
</VBox.margin>
|
||||
<children>
|
||||
<TextField promptText="Search by creditor" styleClass="search">
|
||||
<opaqueInsets>
|
||||
<Insets />
|
||||
</opaqueInsets>
|
||||
<StackPane.margin>
|
||||
<Insets />
|
||||
</StackPane.margin>
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="30.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
</TextField>
|
||||
<ImageView id="clear" fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear">
|
||||
<image>
|
||||
<Image url="@Desktop/clear_inactive.png" />
|
||||
</image>
|
||||
<StackPane.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</StackPane.margin>
|
||||
</ImageView>
|
||||
</children>
|
||||
</StackPane>
|
||||
<VBox />
|
||||
<TreeTableView showRoot="false" sortMode="ONLY_FIRST_LEVEL" VBox.vgrow="ALWAYS">
|
||||
<columns>
|
||||
<TreeTableColumn prefWidth="240.0" styleClass="first-column" text="Creditor" />
|
||||
<TreeTableColumn editable="false" minWidth="0.0" prefWidth="122.0" text="Total outstanding">
|
||||
<styleClass>
|
||||
<String fx:value="montetary-value" />
|
||||
<String fx:value="second-column" />
|
||||
</styleClass></TreeTableColumn>
|
||||
<TreeTableColumn editable="false" minWidth="0.0" prefWidth="156.0" styleClass="montetary-value" text="Overdue" />
|
||||
<TreeTableColumn prefWidth="75.0" styleClass="montetary-value" text="1d" />
|
||||
<TreeTableColumn prefWidth="75.0" styleClass="montetary-value" text="2-7d" />
|
||||
<TreeTableColumn prefWidth="75.0" styleClass="montetary-value" text="8-14d" />
|
||||
<TreeTableColumn prefWidth="75.0" styleClass="montetary-value" text="14d-1m" />
|
||||
<TreeTableColumn prefWidth="75.0" styleClass="montetary-value" text="1-3m" />
|
||||
<TreeTableColumn prefWidth="75.0" styleClass="montetary-value" text="3-6m" />
|
||||
<TreeTableColumn prefWidth="75.0" styleClass="montetary-value" text="6m-1yr" />
|
||||
<TreeTableColumn prefWidth="75.0" styleClass="montetary-value" text="1-5yr" />
|
||||
<TreeTableColumn prefWidth="75.0" styleClass="montetary-value" text=">5yr" />
|
||||
</columns>
|
||||
<columnResizePolicy>
|
||||
<TreeTableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
|
||||
</columnResizePolicy>
|
||||
</TreeTableView>
|
||||
<Label alignment="BOTTOM_LEFT" prefHeight="16.0" text="Total 15 matching issuer(s)" wrapText="true">
|
||||
<VBox.margin>
|
||||
<Insets bottom="5.0" right="10.0" top="5.0" />
|
||||
</VBox.margin>
|
||||
</Label>
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
|
||||
</padding>
|
||||
</VBox>
|
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?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" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<TextField fx:id="SearchCriteriaTextField" promptText="Set prompt text" styleClass="search">
|
||||
<opaqueInsets>
|
||||
<Insets />
|
||||
</opaqueInsets>
|
||||
<StackPane.margin>
|
||||
<Insets />
|
||||
</StackPane.margin>
|
||||
</TextField>
|
||||
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear" StackPane.alignment="CENTER_RIGHT">
|
||||
<image>
|
||||
<Image url="@../../images/clear_inactive.png" />
|
||||
</image>
|
||||
<StackPane.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</StackPane.margin>
|
||||
</ImageView>
|
||||
</children>
|
||||
</StackPane>
|
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?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" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<TextField fx:id="SearchCriteriaTextField" promptText="Set prompt text" styleClass="search">
|
||||
<opaqueInsets>
|
||||
<Insets />
|
||||
</opaqueInsets>
|
||||
<StackPane.margin>
|
||||
<Insets />
|
||||
</StackPane.margin>
|
||||
</TextField>
|
||||
<ImageView fitHeight="16.0" fitWidth="16.0" pickOnBounds="true" preserveRatio="true" styleClass="search-clear" StackPane.alignment="CENTER_RIGHT">
|
||||
<image>
|
||||
<Image url="@../../images/clear_inactive.png" />
|
||||
</image>
|
||||
<StackPane.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</StackPane.margin>
|
||||
</ImageView>
|
||||
</children>
|
||||
</StackPane>
|
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.ButtonType?>
|
||||
<?import javafx.scene.control.ChoiceBox?>
|
||||
<?import javafx.scene.control.DialogPane?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.layout.AnchorPane?>
|
||||
|
||||
|
||||
<DialogPane expanded="true" headerText="Settings" scaleShape="false" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<content>
|
||||
<AnchorPane>
|
||||
<children>
|
||||
<Label layoutX="6.0" layoutY="6.0" text="We are" />
|
||||
<ChoiceBox layoutX="146.0" layoutY="2.0" prefHeight="24.0" prefWidth="360.0" AnchorPane.leftAnchor="146.0" />
|
||||
<Label layoutX="6.0" layoutY="37.0" text="Reporting currency" />
|
||||
<ChoiceBox layoutX="156.0" layoutY="33.0" prefWidth="150.0" />
|
||||
</children>
|
||||
</AnchorPane>
|
||||
</content>
|
||||
<buttonTypes>
|
||||
<ButtonType fx:constant="APPLY" />
|
||||
<ButtonType fx:constant="CLOSE" />
|
||||
</buttonTypes>
|
||||
</DialogPane>
|
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import java.lang.String?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.ButtonType?>
|
||||
<?import javafx.scene.control.DialogPane?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.TableColumn?>
|
||||
<?import javafx.scene.control.TableView?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
|
||||
|
||||
<DialogPane headerText="Issuer C039201 HSBC GROUP PLC" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<content>
|
||||
<VBox spacing="5.0">
|
||||
<children>
|
||||
<Label styleClass="subline" text="Total 18 position(s)" />
|
||||
<Label styleClass="headline" text="USD 394.6k" />
|
||||
<TableView prefHeight="284.0" prefWidth="417.0">
|
||||
<columns>
|
||||
<TableColumn prefWidth="155.0" text="Position ID" />
|
||||
<TableColumn prefWidth="123.0" styleClass="first-column" text="Local ccy" />
|
||||
<TableColumn prefWidth="79.0" text="Amount">
|
||||
<styleClass>
|
||||
<String fx:value="second-column" />
|
||||
<String fx:value="monetary-value" />
|
||||
</styleClass>
|
||||
</TableColumn>
|
||||
<TableColumn prefWidth="75.0" styleClass="monetary-value" text="USD equiv" />
|
||||
</columns>
|
||||
<columnResizePolicy>
|
||||
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
|
||||
</columnResizePolicy>
|
||||
</TableView>
|
||||
</children>
|
||||
<opaqueInsets>
|
||||
<Insets />
|
||||
</opaqueInsets>
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
</VBox>
|
||||
</content>
|
||||
<buttonTypes>
|
||||
<ButtonType fx:constant="CLOSE" />
|
||||
</buttonTypes>
|
||||
</DialogPane>
|
@ -1,7 +1,7 @@
|
||||
package com.r3corda.node.services.monitor
|
||||
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.core.transactions.LedgerTransaction
|
||||
import com.r3corda.node.utilities.AddOrRemove
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
@ -10,8 +10,8 @@ import java.util.*
|
||||
* Events triggered by changes in the node, and sent to monitoring client(s).
|
||||
*/
|
||||
sealed class ServiceToClientEvent(val time: Instant) {
|
||||
class Transaction(time: Instant, val transaction: SignedTransaction) : ServiceToClientEvent(time) {
|
||||
override fun toString() = "Transaction(${transaction.tx.commands})"
|
||||
class Transaction(time: Instant, val transaction: LedgerTransaction) : ServiceToClientEvent(time) {
|
||||
override fun toString() = "Transaction(${transaction.commands})"
|
||||
}
|
||||
class OutputState(
|
||||
time: Instant,
|
||||
@ -26,7 +26,7 @@ sealed class ServiceToClientEvent(val time: Instant) {
|
||||
val label: String,
|
||||
val addOrRemove: AddOrRemove
|
||||
) : ServiceToClientEvent(time) {
|
||||
override fun toString() = "StateMachine(${addOrRemove.name})"
|
||||
override fun toString() = "StateMachine($label, ${addOrRemove.name})"
|
||||
}
|
||||
class Progress(time: Instant, val fiberId: Long, val message: String) : ServiceToClientEvent(time) {
|
||||
override fun toString() = "Progress($message)"
|
||||
@ -46,7 +46,7 @@ sealed class TransactionBuildResult {
|
||||
*
|
||||
* @param transaction the transaction created as a result, in the case where the protocol has completed.
|
||||
*/
|
||||
class ProtocolStarted(val fiberId: Long, val transaction: SignedTransaction?, val message: String?) : TransactionBuildResult() {
|
||||
class ProtocolStarted(val fiberId: Long, val transaction: LedgerTransaction?, val message: String?) : TransactionBuildResult() {
|
||||
override fun toString() = "Started($message)"
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ package com.r3corda.node.services.monitor
|
||||
|
||||
import com.r3corda.core.contracts.ClientToServiceCommand
|
||||
import com.r3corda.core.contracts.ContractState
|
||||
import com.r3corda.core.contracts.StateAndRef
|
||||
import com.r3corda.core.messaging.SingleMessageRecipient
|
||||
import com.r3corda.protocols.DirectRequestMessage
|
||||
|
||||
@ -14,6 +15,6 @@ data class DeregisterRequest(override val replyToRecipient: SingleMessageRecipie
|
||||
override val sessionID: Long) : DirectRequestMessage
|
||||
|
||||
data class DeregisterResponse(val success: Boolean)
|
||||
data class StateSnapshotMessage(val contractStates: Collection<ContractState>, val protocolStates: Collection<String>)
|
||||
data class StateSnapshotMessage(val contractStates: Collection<StateAndRef<ContractState>>, val protocolStates: Collection<String>)
|
||||
|
||||
data class ClientToServiceCommandMessage(override val sessionID: Long, override val replyToRecipient: SingleMessageRecipient, val command: ClientToServiceCommand) : DirectRequestMessage
|
||||
|
@ -11,7 +11,7 @@ import com.r3corda.core.node.services.DEFAULT_SESSION_ID
|
||||
import com.r3corda.core.node.services.Wallet
|
||||
import com.r3corda.core.protocols.ProtocolLogic
|
||||
import com.r3corda.core.serialization.serialize
|
||||
import com.r3corda.core.transactions.SignedTransaction
|
||||
import com.r3corda.core.transactions.LedgerTransaction
|
||||
import com.r3corda.core.transactions.TransactionBuilder
|
||||
import com.r3corda.core.utilities.loggerFor
|
||||
import com.r3corda.node.services.api.AbstractNodeService
|
||||
@ -59,7 +59,7 @@ class WalletMonitorService(services: ServiceHubInternal, val smm: StateMachineMa
|
||||
addMessageHandler(OUT_EVENT_TOPIC) { req: ClientToServiceCommandMessage -> processEventRequest(req) }
|
||||
|
||||
// Notify listeners on state changes
|
||||
services.storageService.validatedTransactions.updates.subscribe { tx -> notifyTransaction(tx) }
|
||||
services.storageService.validatedTransactions.updates.subscribe { tx -> notifyTransaction(tx.tx.toLedgerTransaction(services)) }
|
||||
services.walletService.updates.subscribe { update -> notifyWalletUpdate(update) }
|
||||
smm.changes.subscribe { change ->
|
||||
val fiberId: Long = change.third
|
||||
@ -85,7 +85,7 @@ class WalletMonitorService(services: ServiceHubInternal, val smm: StateMachineMa
|
||||
= notifyEvent(ServiceToClientEvent.OutputState(Instant.now(), update.consumed, update.produced))
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun notifyTransaction(transaction: SignedTransaction)
|
||||
internal fun notifyTransaction(transaction: LedgerTransaction)
|
||||
= notifyEvent(ServiceToClientEvent.Transaction(Instant.now(), transaction))
|
||||
|
||||
private fun processEventRequest(reqMessage: ClientToServiceCommandMessage) {
|
||||
@ -94,11 +94,12 @@ class WalletMonitorService(services: ServiceHubInternal, val smm: StateMachineMa
|
||||
try {
|
||||
when (req) {
|
||||
is ClientToServiceCommand.IssueCash -> issueCash(req)
|
||||
is ClientToServiceCommand.PayCash -> initatePayment(req)
|
||||
is ClientToServiceCommand.PayCash -> initiatePayment(req)
|
||||
is ClientToServiceCommand.ExitCash -> exitCash(req)
|
||||
else -> throw IllegalArgumentException("Unknown request type ${req.javaClass.name}")
|
||||
}
|
||||
} catch(ex: Exception) {
|
||||
logger.warn("Exception while processing message of type ${req.javaClass.simpleName}", ex)
|
||||
TransactionBuildResult.Failed(ex.message)
|
||||
}
|
||||
|
||||
@ -134,7 +135,7 @@ class WalletMonitorService(services: ServiceHubInternal, val smm: StateMachineMa
|
||||
fun processRegisterRequest(req: RegisterRequest) {
|
||||
try {
|
||||
listeners.add(RegisteredListener(req.replyToRecipient, req.sessionID))
|
||||
val stateMessage = StateSnapshotMessage(services.walletService.currentWallet.states.map { it.state.data }.toList(),
|
||||
val stateMessage = StateSnapshotMessage(services.walletService.currentWallet.states.toList(),
|
||||
smm.allStateMachines.map { it.javaClass.name })
|
||||
net.send(net.createMessage(STATE_TOPIC, DEFAULT_SESSION_ID, stateMessage.serialize().bits), req.replyToRecipient)
|
||||
|
||||
@ -151,7 +152,7 @@ class WalletMonitorService(services: ServiceHubInternal, val smm: StateMachineMa
|
||||
}
|
||||
|
||||
// TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service
|
||||
private fun initatePayment(req: ClientToServiceCommand.PayCash): TransactionBuildResult {
|
||||
private fun initiatePayment(req: ClientToServiceCommand.PayCash): TransactionBuildResult {
|
||||
val builder: TransactionBuilder = TransactionType.General.Builder(null)
|
||||
// TODO: Have some way of restricting this to states the caller controls
|
||||
try {
|
||||
@ -165,7 +166,11 @@ class WalletMonitorService(services: ServiceHubInternal, val smm: StateMachineMa
|
||||
}
|
||||
val tx = builder.toSignedTransaction(checkSufficientSignatures = false)
|
||||
val protocol = FinalityProtocol(tx, setOf(req), setOf(req.recipient))
|
||||
return TransactionBuildResult.ProtocolStarted(smm.add(BroadcastTransactionProtocol.TOPIC, protocol).machineId, tx, "Cash payment transaction generated")
|
||||
return TransactionBuildResult.ProtocolStarted(
|
||||
smm.add(BroadcastTransactionProtocol.TOPIC, protocol).machineId,
|
||||
tx.tx.toLedgerTransaction(services),
|
||||
"Cash payment transaction generated"
|
||||
)
|
||||
} catch(ex: InsufficientBalanceException) {
|
||||
return TransactionBuildResult.Failed(ex.message ?: "Insufficient balance")
|
||||
}
|
||||
@ -174,27 +179,35 @@ class WalletMonitorService(services: ServiceHubInternal, val smm: StateMachineMa
|
||||
// TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service
|
||||
private fun exitCash(req: ClientToServiceCommand.ExitCash): TransactionBuildResult {
|
||||
val builder: TransactionBuilder = TransactionType.General.Builder(null)
|
||||
val issuer = PartyAndReference(services.storageService.myLegalIdentity, req.issueRef)
|
||||
Cash().generateExit(builder, req.amount.issuedBy(issuer),
|
||||
services.walletService.currentWallet.statesOfType<Cash.State>().filter { it.state.data.owner == issuer.party.owningKey })
|
||||
builder.signWith(services.storageService.myLegalIdentityKey)
|
||||
try {
|
||||
val issuer = PartyAndReference(services.storageService.myLegalIdentity, req.issueRef)
|
||||
Cash().generateExit(builder, req.amount.issuedBy(issuer),
|
||||
services.walletService.currentWallet.statesOfType<Cash.State>().filter { it.state.data.owner == issuer.party.owningKey })
|
||||
builder.signWith(services.storageService.myLegalIdentityKey)
|
||||
|
||||
// Work out who the owners of the burnt states were
|
||||
val inputStatesNullable = services.walletService.statesForRefs(builder.inputStates())
|
||||
val inputStates = inputStatesNullable.values.filterNotNull().map { it.data }
|
||||
if (inputStatesNullable.size != inputStates.size) {
|
||||
val unresolvedStateRefs = inputStatesNullable.filter { it.value == null }.map { it.key }
|
||||
throw InputStateRefResolveFailed(unresolvedStateRefs)
|
||||
// Work out who the owners of the burnt states were
|
||||
val inputStatesNullable = services.walletService.statesForRefs(builder.inputStates())
|
||||
val inputStates = inputStatesNullable.values.filterNotNull().map { it.data }
|
||||
if (inputStatesNullable.size != inputStates.size) {
|
||||
val unresolvedStateRefs = inputStatesNullable.filter { it.value == null }.map { it.key }
|
||||
throw InputStateRefResolveFailed(unresolvedStateRefs)
|
||||
}
|
||||
|
||||
// TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them
|
||||
// count as a reason to fail?
|
||||
val participants: Set<Party> = inputStates.filterIsInstance<Cash.State>().map { services.identityService.partyFromKey(it.owner) }.filterNotNull().toSet()
|
||||
|
||||
// Commit the transaction
|
||||
val tx = builder.toSignedTransaction(checkSufficientSignatures = false)
|
||||
val protocol = FinalityProtocol(tx, setOf(req), participants)
|
||||
return TransactionBuildResult.ProtocolStarted(
|
||||
smm.add(BroadcastTransactionProtocol.TOPIC, protocol).machineId,
|
||||
tx.tx.toLedgerTransaction(services),
|
||||
"Cash destruction transaction generated"
|
||||
)
|
||||
} catch (ex: InsufficientBalanceException) {
|
||||
return TransactionBuildResult.Failed(ex.message ?: "Insufficient balance")
|
||||
}
|
||||
|
||||
// TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them
|
||||
// count as a reason to fail?
|
||||
val participants: Set<Party> = inputStates.filterIsInstance<Cash.State>().map { services.identityService.partyFromKey(it.owner) }.filterNotNull().toSet()
|
||||
|
||||
// Commit the transaction
|
||||
val tx = builder.toSignedTransaction(checkSufficientSignatures = false)
|
||||
val protocol = FinalityProtocol(tx, setOf(req), participants)
|
||||
return TransactionBuildResult.ProtocolStarted(smm.add(BroadcastTransactionProtocol.TOPIC, protocol).machineId, tx, "Cash destruction transaction generated")
|
||||
}
|
||||
|
||||
// TODO: Make a lightweight protocol that manages this workflow, rather than embedding it directly in the service
|
||||
@ -206,7 +219,11 @@ class WalletMonitorService(services: ServiceHubInternal, val smm: StateMachineMa
|
||||
val tx = builder.toSignedTransaction(checkSufficientSignatures = true)
|
||||
// Issuance transactions do not need to be notarised, so we can skip directly to broadcasting it
|
||||
val protocol = BroadcastTransactionProtocol(tx, setOf(req), setOf(req.recipient))
|
||||
return TransactionBuildResult.ProtocolStarted(smm.add(BroadcastTransactionProtocol.TOPIC, protocol).machineId, tx, "Cash issuance completed")
|
||||
return TransactionBuildResult.ProtocolStarted(
|
||||
smm.add(BroadcastTransactionProtocol.TOPIC, protocol).machineId,
|
||||
tx.tx.toLedgerTransaction(services),
|
||||
"Cash issuance completed"
|
||||
)
|
||||
}
|
||||
|
||||
class InputStateRefResolveFailed(stateRefs: List<StateRef>) :
|
||||
|
@ -140,7 +140,7 @@ class WalletMonitorServiceTests {
|
||||
// Check the returned event is correct
|
||||
val tx = (event.state as TransactionBuildResult.ProtocolStarted).transaction
|
||||
assertNotNull(tx)
|
||||
assertEquals(expectedState, tx!!.tx.outputs.single().data)
|
||||
assertEquals(expectedState, tx!!.outputs.single().data)
|
||||
},
|
||||
expect { event: ServiceToClientEvent.OutputState ->
|
||||
// Check the generated state is correct
|
||||
@ -202,8 +202,8 @@ class WalletMonitorServiceTests {
|
||||
}
|
||||
),
|
||||
expect { event: ServiceToClientEvent.Transaction ->
|
||||
require(event.transaction.sigs.size == 1)
|
||||
event.transaction.sigs.map { it.by }.toSet().containsAll(
|
||||
require(event.transaction.mustSign.size == 1)
|
||||
event.transaction.mustSign.containsAll(
|
||||
listOf(
|
||||
monitorServiceNode.services.storageService.myLegalIdentity.owningKey
|
||||
)
|
||||
|
@ -7,3 +7,4 @@ include 'client'
|
||||
include 'experimental'
|
||||
include 'test-utils'
|
||||
include 'network-simulator'
|
||||
include 'explorer'
|
||||
|