Interim checkin

This commit is contained in:
Richard Green 2017-02-15 10:22:50 +00:00 committed by Katarzyna Streich
parent 759cb6da04
commit 70e7a94310
7 changed files with 494 additions and 6 deletions

View File

@ -2,7 +2,7 @@
<configuration default="false" name="Explorer - demo nodes (simulation)" type="JetRunConfigurationType" factoryName="Kotlin" singleton="true">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.explorer.MainKt" />
<option name="VM_PARAMETERS" value="" />
<option name="VM_PARAMETERS" value="-DAMQ_DELIVERY_DELAY_MS=5000" />
<option name="PROGRAM_PARAMETERS" value="-S" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />

View File

@ -16,17 +16,20 @@ import net.corda.core.transactions.SignedTransaction
import rx.Observable
import rx.subjects.PublishSubject
data class ProgressTrackingEvent(val stateMachineId: StateMachineRunId, val message: String) {
data class ProgressTrackingEvent(val stateMachineId: StateMachineRunId, val message: String, val sessionId: Long ) { // TODO: RG Not a string, but a proper tracking object.
companion object {
fun createStreamFromStateMachineInfo(stateMachine: StateMachineInfo): Observable<ProgressTrackingEvent>? {
return stateMachine.progressTrackerStepAndUpdates?.let { pair ->
val (current, future) = pair
future.map { ProgressTrackingEvent(stateMachine.id, it) }.startWith(ProgressTrackingEvent(stateMachine.id, current))
future.map { ProgressTrackingEvent(stateMachine.id, it, stateMachine.sessionId ) }.startWith(ProgressTrackingEvent(stateMachine.id, current, stateMachine.sessionId))
}
}
}
}
/**
* This model exposes raw event streams to and from the node.
*/

View File

@ -1,5 +1,6 @@
package net.corda.client.jfx.model
import javafx.beans.binding.Bindings
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.value.ObservableValue
import javafx.collections.FXCollections
@ -10,6 +11,7 @@ import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StateMachineRunId
import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.transactions.SignedTransaction
@ -59,6 +61,21 @@ data class PartiallyResolvedTransaction(
}
data class FlowStatus(val status: String)
//todo after rebase
//class FlowStatus(
// val status: String,
// pt: ProgressTrackingEvent?,
// val progress: FlowLogic.ProgressTrackerDisplayProxy = convert(pt)
//) {
//
// companion object {
// fun convert(pt: ProgressTrackingEvent?) : FlowLogic.ProgressTrackerDisplayProxy {
// return FlowLogic.ProgressTrackerDisplayProxy(null, false, false)
///* pt?. == ProgressTracker.DONE,
// pt == null)*/
// }
// }
//}
sealed class StateMachineStatus {
abstract val stateMachineName: String
@ -67,11 +84,19 @@ sealed class StateMachineStatus {
data class Removed(override val stateMachineName: String) : StateMachineStatus()
}
data class StateMachineData(
/*data class StateMachineData(
val id: StateMachineRunId,
val flowStatus: ObservableValue<FlowStatus?>,
val stateMachineStatus: ObservableValue<StateMachineStatus>
)
*/
data class StateMachineData(
val id: StateMachineRunId,
val flowStatus: FlowStatus?,
val stateMachineStatus: StateMachineStatus
)
/**
* This model provides an observable list of transactions and what state machines/flows recorded them
@ -99,9 +124,25 @@ class TransactionDataModel {
}
}
}
val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) {
id, status, progress ->
Bindings.createObjectBinding(
{ StateMachineData(id,
// FlowStatus(progress.value?.message!!, null),
progress.value?.message?.let{
FlowStatus(progress.value!!.message!!.toString(),progress.value)
},
status.get()) }
, arrayOf(progress, status)
)
}.getObservableValues().flatten()
/*
private val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
StateMachineData(id, progress.map { it?.let { FlowStatus(it.message) } }, status)
}.getObservableValues()
}.getObservableValues()*/
// TODO : Create a new screen for state machines.
private val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
private val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
@ -109,3 +150,85 @@ class TransactionDataModel {
PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
}
}
class StateMachineDataModel {
private val transactions by observable(NodeMonitorModel::transactions)
private val stateMachineUpdates by observable(NodeMonitorModel::stateMachineUpdates)
private val progressTracking by observable(NodeMonitorModel::progressTracking)
private val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
private val progressList = progressEvents.getObservableValues()
private val stateMachineTransactionMapping by observable(NodeMonitorModel::stateMachineTransactionMapping)
private val collectedTransactions = transactions.recordInSequence()
private val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
private val stateMachineStatus = stateMachineUpdates.fold(FXCollections.observableHashMap<StateMachineRunId, SimpleObjectProperty<StateMachineStatus>>()) { map, update ->
println("**** $update")
when (update) {
is StateMachineUpdate.Added -> {
val added: SimpleObjectProperty<StateMachineStatus> =
SimpleObjectProperty(StateMachineStatus.Added(update.stateMachineInfo.flowLogicClassName))
map[update.id] = added
}
is StateMachineUpdate.Removed -> {
val added = map[update.id]
added ?: throw Exception("State machine removed with unknown id ${update.id}")
added.set(StateMachineStatus.Removed(added.value.stateMachineName))
}
}
}
val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
Bindings.createObjectBinding({
StateMachineData(id,
progress.value?.message?.let{
FlowStatus(progress.value!!.message!!.toString(),null)
},
// progress.value?.message?.let(::FlowStatus),
status.get())
}, arrayOf(progress, status))
}.getObservableValues().flatten()
/*
val stateMachineDataList = LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
StateMachineData(id, progress.map { it?.let { FlowStatus(it.message) } }, status)
}.getObservableValues()*/
// TODO : Create a new screen for state machines.
private val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
private val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
val partiallyResolvedTransactions = collectedTransactions.map {
PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
}
val flowsInProgress = partiallyResolvedTransactions
/*
data class SomeStructure(
val someInt: ObservableValue<StateMachineStatus>,
val otherData: ObservableValue<String>
)
*/
val progressObservableList = progressList.map{ struct -> struct.message }
fun asd() {
progressEvents // .flatMap { it -> it.key}
progressEvents.keys.map{ it -> { if (true) {null }else { it}}}
// val a1 = progressEvents.map{ struct -> struct.key. { if ( true) { null } else {struct }} }
// val a: ObservableList<SomeStructure> = null!!
// val x: ObservableList<SomeStructure> = ObservableList<SomeStructure>();
// val a3: ObservableList<SomeStructure> = listOf(Pair(1,"hey"))
// val x = a.map{ struct -> struct. { if ( it.javaClass == StateMachineStatus::Removed) { null} else {struct }} }
// val b = a.map { struct -> struct.someInt.map { if (it.javaClass == StateMachineStatus::Removed) { null } else { struct } } }
// val c = b.flatten().filterNotNull()
//return c
}
}

View File

@ -30,6 +30,7 @@ import java.util.*
@CordaSerializable
data class StateMachineInfo(
val id: StateMachineRunId,
val sessionId: Long,
val flowLogicClassName: String,
val initiator: FlowInitiator,
val progressTrackerStepAndUpdates: Pair<String, Observable<String>>?

View File

@ -131,6 +131,7 @@ class Main : App(MainView::class) {
// Stock Views.
registerView<Dashboard>()
registerView<TransactionViewer>()
registerView<FlowViewer>()
// CordApps Views.
registerView<CashViewer>()
// Tools.
@ -248,7 +249,7 @@ fun main(args: Array<String>) {
}
for (i in 0..maxIterations) {
Thread.sleep(300)
Thread.sleep(300) //Thread.sleep(5000) -> todo rebase
// Issuer requests.
eventGenerator.issuerGenerator.map { command ->
when (command) {

View File

@ -0,0 +1,331 @@
package net.corda.explorer.views
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import javafx.beans.binding.Bindings
import javafx.beans.property.ReadOnlyObjectWrapper
import javafx.geometry.Pos
import javafx.scene.control.TableColumn
import javafx.scene.control.TableView
import javafx.scene.layout.BorderPane
import net.corda.client.jfx.model.StateMachineDataModel
import net.corda.client.jfx.model.observableListReadOnly
import net.corda.client.jfx.utils.map
//import net.corda.client.fxutils.map
//import net.corda.client.model.StateMachineDataModel
//import net.corda.client.model.observableListReadOnly
import net.corda.core.crypto.SecureHash
import net.corda.explorer.identicon.identicon
import net.corda.explorer.identicon.identiconToolTip
import net.corda.explorer.model.CordaView
import net.corda.explorer.model.CordaWidget
import net.corda.explorer.ui.setCustomCellFactory
import tornadofx.column
import tornadofx.label
import tornadofx.observable
import tornadofx.right
class FlowViewer : CordaView("Flow Triage") {
override val root by fxml<BorderPane>()
override val icon = FontAwesomeIcon.HEARTBEAT
override val widgets = listOf(CordaWidget(title, FlowViewer.StateMachineWidget())).observable()
private val flowViewTable by fxid<TableView<FlowViewer.Flow>>()
private val flowColumnSessionId by fxid<TableColumn<Flow, Int>>()
private val flowColumnInternalId by fxid<TableColumn<Flow, String>>()
private val flowColumnState by fxid<TableColumn<Flow, String>>()
private class StateMachineWidget() : BorderPane() {
private val flows by observableListReadOnly(StateMachineDataModel::flowsInProgress)
init {
right {
label {
textProperty().bind(Bindings.size(flows).map(Number::toString))
BorderPane.setAlignment(this, Pos.BOTTOM_RIGHT)
}
}
}
}
data class Flow(val id: String, val latestProgress: String)
private val stateMachines by observableListReadOnly(StateMachineDataModel::stateMachineDataList)
// private val flows = stateMachines.map { it -> Flow(it.id.toString(), it.flowStatus.map { it.toString() }) }.filtered { ! it.latestProgress.value.contains("Done") }
private val flows = stateMachines.map { it -> Flow(it.id.toString(), it.flowStatus.toString()) }.filtered {
println("--> $it")
println("Status:${it.latestProgress}")
//it.id.startsWith("[3") or it.id.startsWith("[9")
println(it.latestProgress)
println(it.latestProgress.contains("status=Done"))
!it.latestProgress.contains("status=Done") && it.latestProgress != "null"
//it.latestProgress != null
}
init {
flowViewTable.apply {
items = flows
column("ID", Flow::id) { maxWidth = 200.0 }
.setCustomCellFactory {
label("$it") {
val hash = SecureHash.Companion.randomSHA256()
graphic = identicon(hash, 15.0)
tooltip = identiconToolTip(hash)
}
}
}
flowColumnSessionId.apply {
setCellValueFactory {
ReadOnlyObjectWrapper(0)
}
}
flowColumnInternalId.setCellValueFactory {
ReadOnlyObjectWrapper(it.value.id)
}
flowColumnState.setCellValueFactory {
ReadOnlyObjectWrapper(it.value.latestProgress)
// it.value.latestProgress
}
}
}
/*
class StateMachineViewer2 : CordaView("Transactions") {
override val root by fxml<BorderPane>()
override val icon = FontAwesomeIcon.RANDOM
private val transactionViewTable by fxid<TableView<Transaction>>()
private val matchingTransactionsLabel by fxid<Label>()
// Inject data
private val transactions by observableListReadOnly(TransactionDataModel::partiallyResolvedTransactions)
private val reportingExchange by observableValue(ReportingCurrencyModel::reportingExchange)
private val reportingCurrency by observableValue(ReportingCurrencyModel::reportingCurrency)
private val myIdentity by observableValue(NetworkIdentityModel::myIdentity)
override val widgets = listOf(CordaWidget(title, TransactionWidget())).observable()
/**
* This is what holds data for a single transaction node. Note how a lot of these are nullable as we often simply don't
* have the data.
*/
data class Transaction(
val tx: PartiallyResolvedTransaction,
val id: SecureHash,
val inputs: Inputs,
val outputs: ObservableList<StateAndRef<ContractState>>,
val inputParties: ObservableList<List<ObservableValue<NodeInfo?>>>,
val outputParties: ObservableList<List<ObservableValue<NodeInfo?>>>,
val commandTypes: List<Class<CommandData>>,
val totalValueEquiv: ObservableValue<AmountDiff<Currency>>
)
data class Inputs(val resolved: ObservableList<StateAndRef<ContractState>>, val unresolved: ObservableList<StateRef>)
/**
* We map the gathered data about transactions almost one-to-one to the nodes.
*/
init {
val transactions = transactions.map {
val resolved = it.inputs.sequence()
.map { it as? PartiallyResolvedTransaction.InputResolution.Resolved }
.filterNotNull()
.map { it.stateAndRef }
val unresolved = it.inputs.sequence()
.map { it as? PartiallyResolvedTransaction.InputResolution.Unresolved }
.filterNotNull()
.map { it.stateRef }
val outputs = it.transaction.tx.outputs
.mapIndexed { index, transactionState ->
val stateRef = StateRef(it.id, index)
StateAndRef(transactionState, stateRef)
}.observable()
Transaction(
tx = it,
id = it.id,
inputs = Inputs(resolved, unresolved),
outputs = outputs,
inputParties = resolved.getParties(),
outputParties = outputs.getParties(),
commandTypes = it.transaction.tx.commands.map { it.value.javaClass },
totalValueEquiv = ::calculateTotalEquiv.lift(myIdentity,
reportingExchange,
resolved.map { it.state.data }.lift(),
it.transaction.tx.outputs.map { it.data }.lift())
)
}
val searchField = SearchField(transactions,
"Transaction ID" to { tx, s -> "${tx.id}".contains(s, true) },
"Input" to { tx, s -> tx.inputs.resolved.any { it.state.data.contract.javaClass.simpleName.contains(s, true) } },
"Output" to { tx, s -> tx.outputs.any { it.state.data.contract.javaClass.simpleName.contains(s, true) } },
"Input Party" to { tx, s -> tx.inputParties.any { it.any { it.value?.legalIdentity?.name?.contains(s, true) ?: false } } },
"Output Party" to { tx, s -> tx.outputParties.any { it.any { it.value?.legalIdentity?.name?.contains(s, true) ?: false } } },
"Command Type" to { tx, s -> tx.commandTypes.any { it.simpleName.contains(s, true) } }
)
root.top = searchField.root
// Transaction table
transactionViewTable.apply {
items = searchField.filteredData
column("Transaction ID", Transaction::id) { maxWidth = 200.0 }.setCustomCellFactory {
label("$it") {
graphic = identicon(it, 15.0)
tooltip = identiconToolTip(it)
}
}
column("Input", Transaction::inputs).cellFormat {
text = it.resolved.toText()
if (!it.unresolved.isEmpty()) {
if (!text.isBlank()) {
text += ", "
}
text += "Unresolved(${it.unresolved.size})"
}
}
column("Output", Transaction::outputs).cellFormat { text = it.toText() }
column("Input Party", Transaction::inputParties).cellFormat { text = it.flatten().map { it.value?.legalIdentity?.name }.filterNotNull().toSet().joinToString() }
column("Output Party", Transaction::outputParties).cellFormat { text = it.flatten().map { it.value?.legalIdentity?.name }.filterNotNull().toSet().joinToString() }
column("Command type", Transaction::commandTypes).cellFormat { text = it.map { it.simpleName }.joinToString() }
column("Total value", Transaction::totalValueEquiv).cellFormat {
text = "${it.positivity.sign}${AmountFormatter.boring.format(it.amount)}"
titleProperty.bind(reportingCurrency.map { "Total value ($it equiv)" })
}
rowExpander {
add(ContractStatesView(it).root)
prefHeight = 400.0
}.apply {
prefWidth = 26.0
isResizable = false
}
setColumnResizePolicy { true }
}
matchingTransactionsLabel.textProperty().bind(Bindings.size(transactionViewTable.items).map {
"$it matching transaction${if (it == 1) "" else "s"}"
})
}
private fun ObservableList<StateAndRef<ContractState>>.getParties() = map { it.state.data.participants.map { getModel<NetworkIdentityModel>().lookup(it) } }
private fun ObservableList<StateAndRef<ContractState>>.toText() = map { it.contract().javaClass.simpleName }.groupBy { it }.map { "${it.key} (${it.value.size})" }.joinToString()
private class TransactionWidget() : BorderPane() {
private val partiallyResolvedTransactions by observableListReadOnly(TransactionDataModel::partiallyResolvedTransactions)
// TODO : Add a scrolling table to show latest transaction.
// TODO : Add a chart to show types of transactions.
init {
right {
label {
textProperty().bind(Bindings.size(partiallyResolvedTransactions).map(Number::toString))
BorderPane.setAlignment(this, Pos.BOTTOM_RIGHT)
}
}
}
}
private inner class ContractStatesView(transaction: Transaction) : Fragment() {
override val root by fxml<Parent>()
private val inputs by fxid<ListView<StateAndRef<ContractState>>>()
private val outputs by fxid<ListView<StateAndRef<ContractState>>>()
private val signatures by fxid<VBox>()
private val inputPane by fxid<TitledPane>()
private val outputPane by fxid<TitledPane>()
private val signaturesPane by fxid<TitledPane>()
init {
val signatureData = transaction.tx.transaction.sigs.map { it.by }
// Bind count to TitlePane
inputPane.text = "Input (${transaction.inputs.resolved.count()})"
outputPane.text = "Output (${transaction.outputs.count()})"
signaturesPane.text = "Signatures (${signatureData.count()})"
inputs.cellCache { getCell(it) }
outputs.cellCache { getCell(it) }
inputs.items = transaction.inputs.resolved
outputs.items = transaction.outputs.observable()
signatures.children.addAll(signatureData.map { signature ->
val nodeInfo = getModel<NetworkIdentityModel>().lookup(signature)
copyableLabel(nodeInfo.map { "${signature.toStringShort()} (${it?.legalIdentity?.name ?: "???"})" })
})
}
private fun getCell(contractState: StateAndRef<ContractState>): Node {
return {
gridpane {
padding = Insets(0.0, 5.0, 10.0, 10.0)
vgap = 10.0
hgap = 10.0
row {
label("${contractState.contract().javaClass.simpleName} (${contractState.ref.toString().substring(0, 16)}...)[${contractState.ref.index}]") {
graphic = identicon(contractState.ref.txhash, 30.0)
tooltip = identiconToolTip(contractState.ref.txhash)
gridpaneConstraints { columnSpan = 2 }
}
}
val data = contractState.state.data
when (data) {
is Cash.State -> {
row {
label("Amount :") { gridpaneConstraints { hAlignment = HPos.RIGHT } }
label(AmountFormatter.boring.format(data.amount.withoutIssuer()))
}
row {
label("Issuer :") { gridpaneConstraints { hAlignment = HPos.RIGHT } }
label("${data.amount.token.issuer}") {
tooltip(data.amount.token.issuer.party.owningKey.toBase58String())
}
}
row {
label("Owner :") { gridpaneConstraints { hAlignment = HPos.RIGHT } }
val owner = data.owner
val nodeInfo = getModel<NetworkIdentityModel>().lookup(owner)
label(nodeInfo.map { it?.legalIdentity?.name ?: "???" }) {
tooltip(data.owner.toBase58String())
}
}
}
// TODO : Generic view using reflection?
else -> label {}
}
}
}()
}
}
private fun StateAndRef<ContractState>.contract() = this.state.data.contract
}
/**
* 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: NodeInfo?,
reportingCurrencyExchange: Pair<Currency, (Amount<Currency>) -> Amount<Currency>>,
inputs: List<ContractState>,
outputs: List<ContractState>): AmountDiff<Currency> {
val (reportingCurrency, exchange) = reportingCurrencyExchange
val publicKey = identity?.legalIdentity?.owningKey
fun List<ContractState>.sum() = this.map { it as? Cash.State }
.filterNotNull()
.filter { publicKey == it.owner }
.map { exchange(it.amount.withoutIssuer()).quantity }
.sum()
// For issuing cash, if I am the issuer and not the owner (e.g. issuing cash to other party), count it as negative.
val issuedAmount = if (inputs.isEmpty()) outputs.map { it as? Cash.State }
.filterNotNull()
.filter { publicKey == it.amount.token.issuer.party.owningKey && publicKey != it.owner }
.map { exchange(it.amount.withoutIssuer()).quantity }
.sum() else 0
return AmountDiff.fromLong(outputs.sum() - inputs.sum() - issuedAmount, reportingCurrency)
}
*/

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>
<BorderPane stylesheets="@../css/corda.css" xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1">
<padding>
<Insets bottom="5" left="5" right="5" top="5" />
</padding>
<center>
<TableView fx:id="flowViewTable" VBox.vgrow="ALWAYS">
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
</columnResizePolicy>
<columns>
<TableColumn fx:id="flowColumnSessionId" prefWidth="10.0" styleClass="first-column" text="Session ID" />
<TableColumn fx:id="flowColumnInternalId" prefWidth="73.0" styleClass="first-column" text="Flow Internal ID" />
<TableColumn fx:id="flowColumnState" prefWidth="173.0" styleClass="first-column" text="Flow Status" />
</columns>
</TableView>
</center>
<bottom>
<Label fx:id="matchingFlowsLabel" text="matching flow(s)" />
</bottom>
</BorderPane>