mirror of
https://github.com/corda/corda.git
synced 2024-12-20 05:28:21 +00:00
Implement a simple transaction graph visualiser tool.
This commit is contained in:
parent
31ca78533b
commit
a95cd056ea
@ -33,9 +33,9 @@ dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
compile "com.google.guava:guava:18.0"
|
||||
compile "com.esotericsoftware:kryo:3.0.3"
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src'
|
||||
test.java.srcDirs += 'tests'
|
||||
// For visualisation
|
||||
compile "org.graphstream:gs-core:1.3"
|
||||
compile "org.graphstream:gs-ui:1.3"
|
||||
compile "com.intellij:forms_rt:7.0.3"
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ class CommercialPaperTests {
|
||||
// Generate a trade lifecycle with various parameters.
|
||||
private fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days,
|
||||
aliceGetsBack: Amount = 1000.DOLLARS,
|
||||
destroyPaperAtRedemption: Boolean = true): TransactionGroupForTest<CommercialPaper.State> {
|
||||
destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL<CommercialPaper.State> {
|
||||
val someProfits = 1200.DOLLARS
|
||||
return transactionGroupFor() {
|
||||
roots {
|
@ -50,7 +50,7 @@ class CrowdFundTests {
|
||||
raiseFunds().verify()
|
||||
}
|
||||
|
||||
private fun raiseFunds(): TransactionGroupForTest<CrowdFund.State> {
|
||||
private fun raiseFunds(): TransactionGroupDSL<CrowdFund.State> {
|
||||
return transactionGroupFor<CrowdFund.State> {
|
||||
roots {
|
||||
transaction(1000.DOLLARS.CASH `owned by` ALICE label "alice's $1000")
|
||||
@ -79,7 +79,7 @@ class CrowdFundTests {
|
||||
}
|
||||
|
||||
// 3. Close the opportunity, assuming the target has been met
|
||||
transaction(TEST_TX_TIME + 8.days) {
|
||||
transaction(time = TEST_TX_TIME + 8.days) {
|
||||
input ("pledged opportunity")
|
||||
output ("funded and closed") { "pledged opportunity".output.copy(closed = true) }
|
||||
arg(MINI_CORP_PUBKEY) { CrowdFund.Commands.Funded() }
|
@ -210,8 +210,8 @@ open class TransactionForTest : AbstractTransactionForTest() {
|
||||
|
||||
fun transaction(body: TransactionForTest.() -> Unit) = TransactionForTest().apply { body() }
|
||||
|
||||
class TransactionGroupForTest<out T : ContractState>(private val stateType: Class<T>) {
|
||||
open inner class LedgerTransactionForTest : AbstractTransactionForTest() {
|
||||
class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
|
||||
open inner class LedgerTransactionDSL : AbstractTransactionForTest() {
|
||||
private val inStates = ArrayList<ContractStateRef>()
|
||||
|
||||
fun input(label: String) {
|
||||
@ -231,7 +231,7 @@ class TransactionGroupForTest<out T : ContractState>(private val stateType: Clas
|
||||
|
||||
val String.output: T get() = labelToOutputs[this] ?: throw IllegalArgumentException("State with label '$this' was not found")
|
||||
|
||||
private inner class InternalLedgerTransactionForTest : LedgerTransactionForTest() {
|
||||
private inner class InternalLedgerTransactionDSL : LedgerTransactionDSL() {
|
||||
fun finaliseAndInsertLabels(time: Instant): LedgerTransaction {
|
||||
val ltx = toLedgerTransaction(time)
|
||||
for ((index, labelledState) in outStates.withIndex()) {
|
||||
@ -240,6 +240,7 @@ class TransactionGroupForTest<out T : ContractState>(private val stateType: Clas
|
||||
if (stateType.isInstance(labelledState.state)) {
|
||||
labelToOutputs[labelledState.label] = labelledState.state as T
|
||||
}
|
||||
outputsToLabels[labelledState.state] = labelledState.label
|
||||
}
|
||||
}
|
||||
return ltx
|
||||
@ -249,34 +250,47 @@ class TransactionGroupForTest<out T : ContractState>(private val stateType: Clas
|
||||
private val rootTxns = ArrayList<LedgerTransaction>()
|
||||
private val labelToRefs = HashMap<String, ContractStateRef>()
|
||||
private val labelToOutputs = HashMap<String, T>()
|
||||
private val outputsToLabels = HashMap<ContractState, String>()
|
||||
|
||||
fun labelForState(state: T): String? = outputsToLabels[state]
|
||||
|
||||
inner class Roots {
|
||||
fun transaction(vararg outputStates: LabeledOutput) {
|
||||
val outs = outputStates.map { it.state }
|
||||
val wtx = WireTransaction(emptyList(), outs, emptyList())
|
||||
val ltx = wtx.toLedgerTransaction(TEST_TX_TIME, TEST_KEYS_TO_CORP_MAP, SecureHash.randomSHA256())
|
||||
outputStates.forEachIndexed { index, labeledOutput -> labelToRefs[labeledOutput.label!!] = ContractStateRef(ltx.hash, index) }
|
||||
for ((index, state) in outputStates.withIndex()) {
|
||||
val label = state.label!!
|
||||
labelToRefs[label] = ContractStateRef(ltx.hash, index)
|
||||
outputsToLabels[state.state] = label
|
||||
}
|
||||
rootTxns.add(ltx)
|
||||
}
|
||||
|
||||
@Deprecated("Does not nest ", level = DeprecationLevel.ERROR)
|
||||
fun roots(body: Roots.() -> Unit) {}
|
||||
@Deprecated("Use the vararg form of transaction inside roots", level = DeprecationLevel.ERROR)
|
||||
fun transaction(time: Instant = TEST_TX_TIME, body: LedgerTransactionForTest.() -> Unit) {}
|
||||
fun transaction(time: Instant = TEST_TX_TIME, body: LedgerTransactionDSL.() -> Unit) {}
|
||||
}
|
||||
fun roots(body: Roots.() -> Unit) = Roots().apply { body() }
|
||||
|
||||
val txns = ArrayList<LedgerTransaction>()
|
||||
private val txnToLabelMap = HashMap<LedgerTransaction, String>()
|
||||
|
||||
fun transaction(time: Instant = TEST_TX_TIME, body: LedgerTransactionForTest.() -> Unit): LedgerTransaction {
|
||||
val forTest = InternalLedgerTransactionForTest()
|
||||
fun transaction(label: String? = null, time: Instant = TEST_TX_TIME, body: LedgerTransactionDSL.() -> Unit): LedgerTransaction {
|
||||
val forTest = InternalLedgerTransactionDSL()
|
||||
forTest.body()
|
||||
val ltx = forTest.finaliseAndInsertLabels(time)
|
||||
txns.add(ltx)
|
||||
if (label != null)
|
||||
txnToLabelMap[ltx] = label
|
||||
return ltx
|
||||
}
|
||||
|
||||
fun labelForTransaction(ltx: LedgerTransaction): String? = txnToLabelMap[ltx]
|
||||
|
||||
@Deprecated("Does not nest ", level = DeprecationLevel.ERROR)
|
||||
fun transactionGroup(body: TransactionGroupForTest<T>.() -> Unit) {}
|
||||
fun transactionGroup(body: TransactionGroupDSL<T>.() -> Unit) {}
|
||||
|
||||
fun toTransactionGroup() = TransactionGroup(txns.map { it }.toSet(), rootTxns.toSet())
|
||||
|
||||
@ -304,5 +318,5 @@ class TransactionGroupForTest<out T : ContractState>(private val stateType: Clas
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T : ContractState> transactionGroupFor(body: TransactionGroupForTest<T>.() -> Unit) = TransactionGroupForTest<T>(T::class.java).apply { this.body() }
|
||||
fun transactionGroup(body: TransactionGroupForTest<ContractState>.() -> Unit) = TransactionGroupForTest(ContractState::class.java).apply { this.body() }
|
||||
inline fun <reified T : ContractState> transactionGroupFor(body: TransactionGroupDSL<T>.() -> Unit) = TransactionGroupDSL<T>(T::class.java).apply { this.body() }
|
||||
fun transactionGroup(body: TransactionGroupDSL<ContractState>.() -> Unit) = TransactionGroupDSL(ContractState::class.java).apply { this.body() }
|
95
src/test/kotlin/core/visualiser/GraphStream.kt
Normal file
95
src/test/kotlin/core/visualiser/GraphStream.kt
Normal file
@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2015, R3 CEV. All rights reserved.
|
||||
*/
|
||||
|
||||
package core.visualiser
|
||||
|
||||
import org.graphstream.graph.Edge
|
||||
import org.graphstream.graph.Element
|
||||
import org.graphstream.graph.Graph
|
||||
import org.graphstream.graph.Node
|
||||
import org.graphstream.graph.implementations.SingleGraph
|
||||
import org.graphstream.ui.layout.Layout
|
||||
import org.graphstream.ui.layout.springbox.implementations.SpringBox
|
||||
import org.graphstream.ui.swingViewer.DefaultView
|
||||
import org.graphstream.ui.view.Viewer
|
||||
import org.graphstream.ui.view.ViewerListener
|
||||
import java.util.*
|
||||
import javax.swing.JFrame
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
// Some utilities to make the GraphStream API a bit nicer to work with. For some reason GS likes to use a non-type safe
|
||||
// string->value map type API for configuring common things. We fix it up here:
|
||||
|
||||
class GSPropertyDelegate<T>(private val prefix: String) {
|
||||
operator fun getValue(thisRef: Element, property: KProperty<*>): T = thisRef.getAttribute("$prefix.${property.name}")
|
||||
operator fun setValue(thisRef: Element, property: KProperty<*>, value: T) = thisRef.setAttribute("$prefix.${property.name}", value)
|
||||
}
|
||||
|
||||
var Node.label: String by GSPropertyDelegate<String>("ui")
|
||||
var Graph.stylesheet: String by GSPropertyDelegate<String>("ui")
|
||||
var Edge.weight: Double by GSPropertyDelegate<Double>("layout")
|
||||
|
||||
// Do this one by hand as 'class' is a reserved word.
|
||||
var Node.styleClass: String
|
||||
set(value) = setAttribute("ui.class", value)
|
||||
get() = getAttribute("ui.class")
|
||||
|
||||
fun createGraph(name: String, styles: String): SingleGraph {
|
||||
System.setProperty("org.graphstream.ui.renderer", "org.graphstream.ui.j2dviewer.J2DGraphRenderer");
|
||||
return SingleGraph(name).apply {
|
||||
stylesheet = styles
|
||||
setAttribute("ui.quality")
|
||||
setAttribute("ui.antialias")
|
||||
setAttribute("layout.quality", 0)
|
||||
setAttribute("layout.force", 0.9)
|
||||
}
|
||||
}
|
||||
|
||||
class MyViewer(graph: Graph) : Viewer(graph, Viewer.ThreadingModel.GRAPH_IN_ANOTHER_THREAD) {
|
||||
override fun enableAutoLayout(layoutAlgorithm: Layout) {
|
||||
super.enableAutoLayout(layoutAlgorithm)
|
||||
|
||||
// Setting shortNap to 1 stops things bouncing around horribly at the start.
|
||||
optLayout.setNaps(50, 1)
|
||||
}
|
||||
}
|
||||
|
||||
fun runGraph(graph: SingleGraph, nodeOnClick: (Node) -> Unit) {
|
||||
// Use a bit of custom code here instead of calling graph.display() so we can maximize the window.
|
||||
val viewer = MyViewer(graph)
|
||||
val view: DefaultView = object : DefaultView(viewer, Viewer.DEFAULT_VIEW_ID, Viewer.newGraphRenderer()) {
|
||||
override fun openInAFrame(on: Boolean) {
|
||||
super.openInAFrame(on)
|
||||
if (frame != null) {
|
||||
frame.extendedState = frame.extendedState or JFrame.MAXIMIZED_BOTH
|
||||
}
|
||||
}
|
||||
}
|
||||
viewer.addView(view)
|
||||
|
||||
var loop: Boolean = true
|
||||
val viewerPipe = viewer.newViewerPipe()
|
||||
viewerPipe.addViewerListener(object : ViewerListener {
|
||||
override fun buttonPushed(id: String?) {
|
||||
}
|
||||
|
||||
override fun buttonReleased(id: String?) {
|
||||
val node = graph.getNode<Node>(id)
|
||||
nodeOnClick(node)
|
||||
}
|
||||
|
||||
override fun viewClosed(viewName: String?) {
|
||||
loop = false
|
||||
}
|
||||
})
|
||||
|
||||
view.openInAFrame(true)
|
||||
// Seed determined through trial and error: it gives a reasonable layout for the Wednesday demo.
|
||||
val springBox = SpringBox(false, Random(-103468310429824593L))
|
||||
viewer.enableAutoLayout(springBox)
|
||||
|
||||
while (loop) {
|
||||
viewerPipe.blockingPump()
|
||||
}
|
||||
}
|
82
src/test/kotlin/core/visualiser/GraphStreamVisualiser.kt
Normal file
82
src/test/kotlin/core/visualiser/GraphStreamVisualiser.kt
Normal file
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2015, R3 CEV. All rights reserved.
|
||||
*/
|
||||
@file:Suppress("CAST_NEVER_SUCCEEDS")
|
||||
|
||||
package core.visualiser
|
||||
|
||||
import contracts.Cash
|
||||
import contracts.CommercialPaper
|
||||
import core.Amount
|
||||
import core.ContractState
|
||||
import core.DOLLARS
|
||||
import core.days
|
||||
import core.testutils.*
|
||||
import java.time.Instant
|
||||
import kotlin.reflect.memberProperties
|
||||
|
||||
|
||||
val PAPER_1 = CommercialPaper.State(
|
||||
issuance = MEGA_CORP.ref(123),
|
||||
owner = MEGA_CORP_PUBKEY,
|
||||
faceValue = 1000.DOLLARS,
|
||||
maturityDate = TEST_TX_TIME + 7.days
|
||||
)
|
||||
|
||||
private fun trade(redemptionTime: Instant = TEST_TX_TIME + 8.days,
|
||||
aliceGetsBack: Amount = 1000.DOLLARS,
|
||||
destroyPaperAtRedemption: Boolean = true): TransactionGroupDSL<ContractState> {
|
||||
val someProfits = 1200.DOLLARS
|
||||
return transactionGroupFor<CommercialPaper.State>() {
|
||||
roots {
|
||||
transaction(900.DOLLARS.CASH `owned by` ALICE label "alice's $900")
|
||||
transaction(someProfits.CASH `owned by` MEGA_CORP_PUBKEY label "some profits")
|
||||
}
|
||||
|
||||
// Some CP is issued onto the ledger by MegaCorp.
|
||||
transaction("Issuance") {
|
||||
output("paper") { PAPER_1 }
|
||||
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() }
|
||||
}
|
||||
|
||||
// The CP is sold to alice for her $900, $100 less than the face value. At 10% interest after only 7 days,
|
||||
// that sounds a bit too good to be true!
|
||||
transaction("Trade") {
|
||||
input("paper")
|
||||
input("alice's $900")
|
||||
output("borrowed $900") { 900.DOLLARS.CASH `owned by` MEGA_CORP_PUBKEY }
|
||||
output("alice's paper") { "paper".output `owned by` ALICE }
|
||||
arg(ALICE) { Cash.Commands.Move() }
|
||||
arg(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Move() }
|
||||
}
|
||||
|
||||
// Time passes, and Alice redeem's her CP for $1000, netting a $100 profit. MegaCorp has received $1200
|
||||
// as a single payment from somewhere and uses it to pay Alice off, keeping the remaining $200 as change.
|
||||
transaction("Redemption", redemptionTime) {
|
||||
input("alice's paper")
|
||||
input("some profits")
|
||||
|
||||
output("Alice's profit") { aliceGetsBack.CASH `owned by` ALICE }
|
||||
output("Change") { (someProfits - aliceGetsBack).CASH `owned by` MEGA_CORP_PUBKEY }
|
||||
if (!destroyPaperAtRedemption)
|
||||
output { "paper".output }
|
||||
|
||||
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
|
||||
arg(ALICE) { CommercialPaper.Commands.Redeem() }
|
||||
}
|
||||
} as TransactionGroupDSL<ContractState>
|
||||
}
|
||||
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val tg = trade()
|
||||
val graph = GraphConverter(tg).convert()
|
||||
runGraph(graph, nodeOnClick = { node ->
|
||||
val state: ContractState? = node.getAttribute("state")
|
||||
if (state != null) {
|
||||
val props: List<Pair<String, Any?>> = state.javaClass.kotlin.memberProperties.map { it.name to it.getter.call(state) }
|
||||
StateViewer.show(props)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
68
src/test/kotlin/core/visualiser/GroupToGraphConversion.kt
Normal file
68
src/test/kotlin/core/visualiser/GroupToGraphConversion.kt
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2015, R3 CEV. All rights reserved.
|
||||
*/
|
||||
|
||||
package core.visualiser
|
||||
|
||||
import core.Command
|
||||
import core.ContractState
|
||||
import core.SecureHash
|
||||
import core.testutils.TransactionGroupDSL
|
||||
import org.graphstream.graph.Edge
|
||||
import org.graphstream.graph.Node
|
||||
import org.graphstream.graph.implementations.SingleGraph
|
||||
|
||||
class GraphConverter(val dsl: TransactionGroupDSL<in ContractState>) {
|
||||
companion object {
|
||||
val css = GraphConverter::class.java.getResourceAsStream("graph.css").bufferedReader().readText()
|
||||
}
|
||||
|
||||
fun convert(): SingleGraph {
|
||||
val tg = dsl.toTransactionGroup()
|
||||
val graph = createGraph("Transaction group", css)
|
||||
|
||||
// Map all the transactions, including the bogus non-verified ones (with no inputs) to graph nodes.
|
||||
for ((txIndex, tx) in (tg.transactions + tg.nonVerifiedRoots).withIndex()) {
|
||||
val txNode = graph.addNode<Node>("tx$txIndex")
|
||||
if (tx !in tg.nonVerifiedRoots)
|
||||
txNode.label = dsl.labelForTransaction(tx).let { it ?: "TX ${tx.hash.prefixChars()}" }
|
||||
txNode.styleClass = "tx"
|
||||
|
||||
// Now create a vertex for each output state.
|
||||
for (outIndex in tx.outStates.indices) {
|
||||
val node = graph.addNode<Node>(tx.outRef<ContractState>(outIndex).ref.toString())
|
||||
val state = tx.outStates[outIndex]
|
||||
node.label = stateToLabel(state)
|
||||
node.styleClass = stateToCSSClass(state) + ",state"
|
||||
node.setAttribute("state", state)
|
||||
val edge = graph.addEdge<Edge>("tx$txIndex-out$outIndex", txNode, node, true)
|
||||
edge.weight = 0.7
|
||||
}
|
||||
|
||||
// And a vertex for each command.
|
||||
for ((index, cmd) in tx.commands.withIndex()) {
|
||||
val node = graph.addNode<Node>(SecureHash.randomSHA256().prefixChars())
|
||||
node.label = commandToTypeName(cmd.value)
|
||||
node.styleClass = "command"
|
||||
val edge = graph.addEdge<Edge>("tx$txIndex-cmd-$index", node, txNode)
|
||||
edge.weight = 0.4
|
||||
}
|
||||
}
|
||||
// And now all states and transactions were mapped to graph nodes, hook up the input edges.
|
||||
for ((txIndex, tx) in tg.transactions.withIndex()) {
|
||||
for ((inputIndex, ref) in tx.inStateRefs.withIndex()) {
|
||||
val edge = graph.addEdge<Edge>("tx$txIndex-in$inputIndex", ref.toString(), "tx$txIndex", true)
|
||||
edge.weight = 1.2
|
||||
}
|
||||
}
|
||||
return graph
|
||||
}
|
||||
|
||||
private fun stateToLabel(state: ContractState): String {
|
||||
return dsl.labelForState(state) ?: stateToTypeName(state)
|
||||
}
|
||||
|
||||
private fun commandToTypeName(state: Command) = state.javaClass.canonicalName.removePrefix("contracts.").replace('$', '.')
|
||||
private fun stateToTypeName(state: ContractState) = state.javaClass.canonicalName.removePrefix("contracts.").removeSuffix(".State")
|
||||
private fun stateToCSSClass(state: ContractState) = stateToTypeName(state).replace('.', '_').toLowerCase()
|
||||
}
|
37
src/test/kotlin/core/visualiser/StateViewer.form
Normal file
37
src/test/kotlin/core/visualiser/StateViewer.form
Normal file
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="core.visualiser.StateViewer">
|
||||
<grid id="27dc6" binding="root" layout-manager="BorderLayout" hgap="15" vgap="15">
|
||||
<constraints>
|
||||
<xy x="20" y="20" width="500" height="400"/>
|
||||
</constraints>
|
||||
<properties>
|
||||
<background color="-1"/>
|
||||
</properties>
|
||||
<border type="empty">
|
||||
<size top="15" left="15" bottom="15" right="15"/>
|
||||
</border>
|
||||
<children>
|
||||
<component id="c1614" class="javax.swing.JLabel">
|
||||
<constraints border-constraint="North"/>
|
||||
<properties>
|
||||
<font style="1"/>
|
||||
<text value="State viewer"/>
|
||||
</properties>
|
||||
</component>
|
||||
<scrollpane id="2974d">
|
||||
<constraints border-constraint="Center"/>
|
||||
<properties/>
|
||||
<border type="none"/>
|
||||
<children>
|
||||
<component id="8f1af" class="javax.swing.JTable" binding="propsTable">
|
||||
<constraints/>
|
||||
<properties>
|
||||
<autoResizeMode value="3"/>
|
||||
<showHorizontalLines value="false"/>
|
||||
</properties>
|
||||
</component>
|
||||
</children>
|
||||
</scrollpane>
|
||||
</children>
|
||||
</grid>
|
||||
</form>
|
113
src/test/kotlin/core/visualiser/StateViewer.java
Normal file
113
src/test/kotlin/core/visualiser/StateViewer.java
Normal file
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2015, R3 CEV. All rights reserved.
|
||||
*/
|
||||
|
||||
package core.visualiser;
|
||||
|
||||
import kotlin.*;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.*;
|
||||
import java.awt.*;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
public class StateViewer {
|
||||
private JPanel root;
|
||||
private JTable propsTable;
|
||||
|
||||
public static void main(String[] args) {
|
||||
JFrame frame = new JFrame("StateViewer");
|
||||
List<Pair<String, Object>> props = new ArrayList<>();
|
||||
props.add(new Pair<>("a", 123));
|
||||
props.add(new Pair<>("things", "bar"));
|
||||
frame.setContentPane(new StateViewer(props).root);
|
||||
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
frame.pack();
|
||||
frame.setVisible(true);
|
||||
frame.setSize(800, 600);
|
||||
}
|
||||
|
||||
public static void show(List<Pair<String, Object>> props) {
|
||||
JFrame frame = new JFrame("StateViewer");
|
||||
StateViewer viewer = new StateViewer(props);
|
||||
frame.setContentPane(viewer.root);
|
||||
frame.pack();
|
||||
frame.setSize(600, 300);
|
||||
|
||||
viewer.propsTable.getColumnModel().getColumn(0).setMinWidth(150);
|
||||
viewer.propsTable.getColumnModel().getColumn(0).setMaxWidth(150);
|
||||
|
||||
frame.setVisible(true);
|
||||
}
|
||||
|
||||
public StateViewer(List<Pair<String, Object>> props) {
|
||||
propsTable.setModel(new AbstractTableModel() {
|
||||
@Override
|
||||
public int getRowCount() {
|
||||
return props.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnName(int column) {
|
||||
if (column == 0)
|
||||
return "Attribute";
|
||||
else if (column == 1)
|
||||
return "Value";
|
||||
else
|
||||
return "?";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValueAt(int rowIndex, int columnIndex) {
|
||||
if (columnIndex == 0)
|
||||
return props.get(rowIndex).getFirst();
|
||||
else
|
||||
return props.get(rowIndex).getSecond();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
// GUI initializer generated by IntelliJ IDEA GUI Designer
|
||||
// >>> IMPORTANT!! <<<
|
||||
// DO NOT EDIT OR ADD ANY CODE HERE!
|
||||
$$$setupUI$$$();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method generated by IntelliJ IDEA GUI Designer
|
||||
* >>> IMPORTANT!! <<<
|
||||
* DO NOT edit this method OR call it in your code!
|
||||
*
|
||||
* @noinspection ALL
|
||||
*/
|
||||
private void $$$setupUI$$$() {
|
||||
root = new JPanel();
|
||||
root.setLayout(new BorderLayout(15, 15));
|
||||
root.setBackground(new Color(-1));
|
||||
root.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15), null));
|
||||
final JLabel label1 = new JLabel();
|
||||
label1.setFont(new Font(label1.getFont().getName(), Font.BOLD, label1.getFont().getSize()));
|
||||
label1.setText("State viewer");
|
||||
root.add(label1, BorderLayout.NORTH);
|
||||
final JScrollPane scrollPane1 = new JScrollPane();
|
||||
root.add(scrollPane1, BorderLayout.CENTER);
|
||||
propsTable = new JTable();
|
||||
propsTable.setAutoResizeMode(3);
|
||||
propsTable.setShowHorizontalLines(false);
|
||||
scrollPane1.setViewportView(propsTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @noinspection ALL
|
||||
*/
|
||||
public JComponent $$$getRootComponent$$$() {
|
||||
return root;
|
||||
}
|
||||
}
|
41
src/test/resources/core/visualiser/graph.css
Normal file
41
src/test/resources/core/visualiser/graph.css
Normal file
@ -0,0 +1,41 @@
|
||||
node.tx {
|
||||
size: 10px;
|
||||
fill-color: blue;
|
||||
shape: rounded-box;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
node.state {
|
||||
size: 25px;
|
||||
fill-color: beige;
|
||||
stroke-width: 2px;
|
||||
stroke-color: black;
|
||||
stroke-mode: plain;
|
||||
}
|
||||
|
||||
node {
|
||||
text-background-mode: rounded-box;
|
||||
text-background-color: darkslategrey;
|
||||
text-padding: 5px;
|
||||
text-offset: 10px;
|
||||
text-color: white;
|
||||
text-alignment: under;
|
||||
text-size: 16;
|
||||
}
|
||||
|
||||
node.command {
|
||||
text-size: 12;
|
||||
size: 8px;
|
||||
fill-color: white;
|
||||
stroke-width: 2px;
|
||||
stroke-color: black;
|
||||
stroke-mode: plain;
|
||||
}
|
||||
|
||||
node.cash {
|
||||
fill-color: red;
|
||||
}
|
||||
|
||||
graph {
|
||||
padding: 100px;
|
||||
}
|
Loading…
Reference in New Issue
Block a user