Merged in aslemmer-node-explorer (pull request #311)

explorer ui v0.2
This commit is contained in:
Andras Slemmer 2016-09-20 12:02:09 +01:00
commit 2640e600c8
75 changed files with 3394 additions and 112 deletions

1
.gitignore vendored
View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View 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"
}
}

View File

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

View File

@ -0,0 +1,6 @@
package com.r3corda.explorer.formatters
interface Formatter<in T> {
fun format(value: T): String
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View 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;
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

View File

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

View File

@ -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="&gt;&gt;" />
<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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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="&gt;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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,3 +7,4 @@ include 'client'
include 'experimental'
include 'test-utils'
include 'network-simulator'
include 'explorer'