V2 gui for the bootstrapper (#3373)

* V2 gui

* Misc small fixes and tweaks

* V2 gui

* More tweaks

* fix horizontal resize issue
This commit is contained in:
Stefano Franz 2018-06-28 12:05:36 +01:00 committed by GitHub
parent a768904e4e
commit de6e78b4a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 413 additions and 286 deletions

View File

@ -31,7 +31,6 @@ apply plugin: 'application'
apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'com.github.johnrengelman.shadow'
dependencies { dependencies {
compile "com.microsoft.azure:azure:1.8.0" compile "com.microsoft.azure:azure:1.8.0"
compile "com.github.docker-java:docker-java:3.0.6" compile "com.github.docker-java:docker-java:3.0.6"
@ -50,8 +49,8 @@ dependencies {
// TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's. // TornadoFX: A lightweight Kotlin framework for working with JavaFX UI's.
compile "no.tornado:tornadofx:$tornadofx_version" compile "no.tornado:tornadofx:$tornadofx_version"
// ControlsFX: Extra controls for JavaFX.
compile "org.controlsfx:controlsfx:$controlsfx_version" compile "org.controlsfx:controlsfx:$controlsfx_version"
} }
shadowJar { shadowJar {

View File

@ -1,44 +1,34 @@
@file:JvmName("Main") @file:JvmName("Main")
package net.corda.bootstrapper package net.corda.bootstrapper
import javafx.application.Application
import net.corda.bootstrapper.backends.Backend import net.corda.bootstrapper.backends.Backend
import net.corda.bootstrapper.backends.Backend.BackendType.AZURE import net.corda.bootstrapper.backends.Backend.BackendType.AZURE
import net.corda.bootstrapper.cli.AzureParser import net.corda.bootstrapper.cli.AzureParser
import net.corda.bootstrapper.cli.CliParser import net.corda.bootstrapper.cli.CliParser
import net.corda.bootstrapper.cli.CommandLineInterface import net.corda.bootstrapper.cli.CommandLineInterface
import net.corda.bootstrapper.cli.GuiSwitch
import net.corda.bootstrapper.gui.Gui import net.corda.bootstrapper.gui.Gui
import net.corda.bootstrapper.serialization.SerializationEngine import net.corda.bootstrapper.serialization.SerializationEngine
import picocli.CommandLine import picocli.CommandLine
val baseArgs = CliParser()
fun main(args: Array<String>) { fun main(args: Array<String>) {
SerializationEngine.init() SerializationEngine.init()
CommandLine(baseArgs).parse(*args)
val entryPointArgs = GuiSwitch(); if (baseArgs.gui) {
CommandLine(entryPointArgs).parse(*args) Application.launch(Gui::class.java)
if (entryPointArgs.usageHelpRequested) {
CommandLine.usage(AzureParser(), System.out)
return return
} }
val argParser: CliParser = when (baseArgs.backendType) {
if (entryPointArgs.gui) { AZURE -> {
Gui.main(args) val azureArgs = AzureParser()
} else { CommandLine(azureArgs).parse(*args)
val baseArgs = CliParser() azureArgs
CommandLine(baseArgs).parse(*args)
val argParser: CliParser = when (baseArgs.backendType) {
AZURE -> {
val azureArgs = AzureParser()
CommandLine(azureArgs).parse(*args)
azureArgs
}
Backend.BackendType.LOCAL_DOCKER -> baseArgs
} }
CommandLineInterface().run(argParser) Backend.BackendType.LOCAL_DOCKER -> baseArgs
} }
CommandLineInterface().run(argParser)
} }

View File

@ -30,6 +30,10 @@ interface NetworkBuilder {
fun withBackendOptions(options: Map<String, String>): NetworkBuilder fun withBackendOptions(options: Map<String, String>): NetworkBuilder
fun build(): CompletableFuture<Pair<List<NodeInstance>, Context>> fun build(): CompletableFuture<Pair<List<NodeInstance>, Context>>
fun onNodeStartBuild(callback: (FoundNode) -> Unit): NetworkBuilder
fun onNodePushStart(callback: (BuiltNode) -> Unit): NetworkBuilder
fun onNodeInstancesRequested(callback: (List<NodeInstanceRequest>) -> Unit): NetworkBuilder
} }
private class NetworkBuilderImpl : NetworkBuilder { private class NetworkBuilderImpl : NetworkBuilder {
@ -40,11 +44,18 @@ private class NetworkBuilderImpl : NetworkBuilder {
@Volatile @Volatile
private var onNodeCopiedCallback: ((CopiedNode) -> Unit) = {} private var onNodeCopiedCallback: ((CopiedNode) -> Unit) = {}
@Volatile @Volatile
private var onNodeBuildStartCallback: (FoundNode) -> Unit = {}
@Volatile
private var onNodeBuiltCallback: ((BuiltNode) -> Unit) = {} private var onNodeBuiltCallback: ((BuiltNode) -> Unit) = {}
@Volatile @Volatile
private var onNodePushStartCallback: ((BuiltNode) -> Unit) = {}
@Volatile
private var onNodePushedCallback: ((PushedNode) -> Unit) = {} private var onNodePushedCallback: ((PushedNode) -> Unit) = {}
@Volatile @Volatile
private var onNodeInstanceRequestedCallback: (List<NodeInstanceRequest>) -> Unit = {}
@Volatile
private var onNodeInstanceCallback: ((NodeInstance) -> Unit) = {} private var onNodeInstanceCallback: ((NodeInstance) -> Unit) = {}
@Volatile @Volatile
private var nodeCounts = mapOf<String, Int>() private var nodeCounts = mapOf<String, Int>()
@Volatile @Volatile
@ -67,6 +78,12 @@ private class NetworkBuilderImpl : NetworkBuilder {
return this return this
} }
override fun onNodeStartBuild(callback: (FoundNode) -> Unit): NetworkBuilder {
this.onNodeBuildStartCallback = callback
return this;
}
override fun onNodeBuild(callback: (BuiltNode) -> Unit): NetworkBuilder { override fun onNodeBuild(callback: (BuiltNode) -> Unit): NetworkBuilder {
this.onNodeBuiltCallback = callback this.onNodeBuiltCallback = callback
return this return this
@ -77,6 +94,11 @@ private class NetworkBuilderImpl : NetworkBuilder {
return this return this
} }
override fun onNodeInstancesRequested(callback: (List<NodeInstanceRequest>) -> Unit): NetworkBuilder {
this.onNodeInstanceRequestedCallback = callback
return this
}
override fun onNodeInstance(callback: (NodeInstance) -> Unit): NetworkBuilder { override fun onNodeInstance(callback: (NodeInstance) -> Unit): NetworkBuilder {
this.onNodeInstanceCallback = callback; this.onNodeInstanceCallback = callback;
return this return this
@ -107,6 +129,12 @@ private class NetworkBuilderImpl : NetworkBuilder {
return this return this
} }
override fun onNodePushStart(callback: (BuiltNode) -> Unit): NetworkBuilder {
this.onNodePushStartCallback = callback;
return this;
}
override fun build(): CompletableFuture<Pair<List<NodeInstance>, Context>> { override fun build(): CompletableFuture<Pair<List<NodeInstance>, Context>> {
val cacheDir = File(workingDir, cacheDirName) val cacheDir = File(workingDir, cacheDirName)
val baseDir = workingDir!! val baseDir = workingDir!!
@ -132,6 +160,7 @@ private class NetworkBuilderImpl : NetworkBuilder {
val notaryDiscoveryFuture = CompletableFuture.supplyAsync { val notaryDiscoveryFuture = CompletableFuture.supplyAsync {
val copiedNotaries = notaryFinder.findNotaries() val copiedNotaries = notaryFinder.findNotaries()
.map { foundNode: FoundNode -> .map { foundNode: FoundNode ->
onNodeBuildStartCallback.invoke(foundNode)
notaryCopier.copyNotary(foundNode) notaryCopier.copyNotary(foundNode)
} }
volume.notariesForNetworkParams(copiedNotaries) volume.notariesForNetworkParams(copiedNotaries)
@ -141,14 +170,15 @@ private class NetworkBuilderImpl : NetworkBuilder {
val notariesFuture = notaryDiscoveryFuture.thenCompose { copiedNotaries -> val notariesFuture = notaryDiscoveryFuture.thenCompose { copiedNotaries ->
copiedNotaries copiedNotaries
.map { copiedNotary -> .map { copiedNotary ->
nodeBuilder.buildNode(copiedNotary) nodeBuilder.buildNode(copiedNotary).also(onNodeBuiltCallback)
}.map { builtNotary -> }.map { builtNotary ->
nodePusher.pushNode(builtNotary) onNodePushStartCallback(builtNotary)
nodePusher.pushNode(builtNotary).thenApply { it.also(onNodePushedCallback) }
}.map { pushedNotary -> }.map { pushedNotary ->
pushedNotary.thenApplyAsync { nodeInstantiator.createInstanceRequest(it) } pushedNotary.thenApplyAsync { nodeInstantiator.createInstanceRequest(it).also { onNodeInstanceRequestedCallback.invoke(listOf(it)) } }
}.map { instanceRequest -> }.map { instanceRequest ->
instanceRequest.thenComposeAsync { request -> instanceRequest.thenComposeAsync { request ->
nodeInstantiator.instantiateNotaryInstance(request) nodeInstantiator.instantiateNotaryInstance(request).thenApply { it.also(onNodeInstanceCallback) }
} }
}.toSingleFuture() }.toSingleFuture()
} }
@ -161,6 +191,7 @@ private class NetworkBuilderImpl : NetworkBuilder {
it it
} }
}.map { copiedNode: CopiedNode -> }.map { copiedNode: CopiedNode ->
onNodeBuildStartCallback.invoke(copiedNode)
nodeBuilder.buildNode(copiedNode).let { nodeBuilder.buildNode(copiedNode).let {
onNodeBuiltCallback.invoke(it) onNodeBuiltCallback.invoke(it)
it it
@ -172,7 +203,8 @@ private class NetworkBuilderImpl : NetworkBuilder {
} }
}.map { pushedNode -> }.map { pushedNode ->
pushedNode.thenApplyAsync { pushedNode.thenApplyAsync {
nodeInstantiator.createInstanceRequests(it, nodeCount) nodeInstantiator.createInstanceRequests(it, nodeCount).also(onNodeInstanceRequestedCallback)
} }
}.map { instanceRequests -> }.map { instanceRequests ->
instanceRequests.thenComposeAsync { requests -> instanceRequests.thenComposeAsync { requests ->

View File

@ -22,8 +22,16 @@ interface Backend {
val instantiator: Instantiator val instantiator: Instantiator
val volume: Volume val volume: Volume
enum class BackendType { enum class BackendType(val displayName: String) {
AZURE, LOCAL_DOCKER
AZURE("Azure Containers"), LOCAL_DOCKER("Local Docker");
override fun toString(): String {
return this.displayName
}
} }
operator fun component1(): ContainerPusher { operator fun component1(): ContainerPusher {

View File

@ -17,7 +17,7 @@ class CommandLineInterface {
fun run(parsedArgs: CliParser) { fun run(parsedArgs: CliParser) {
val baseDir = parsedArgs.baseDirectory val baseDir = parsedArgs.baseDirectory
val cacheDir = File(baseDir, Constants.BOOTSTRAPPER_DIR_NAME) val cacheDir = File(baseDir, Constants.BOOTSTRAPPER_DIR_NAME)
val networkName = parsedArgs.name val networkName = parsedArgs.name ?: "corda-network"
val objectMapper = Constants.getContextMapper() val objectMapper = Constants.getContextMapper()
val contextFile = File(cacheDir, "$networkName.yaml") val contextFile = File(cacheDir, "$networkName.yaml")
if (parsedArgs.isNew()) { if (parsedArgs.isNew()) {

View File

@ -7,30 +7,20 @@ import picocli.CommandLine
import picocli.CommandLine.Option import picocli.CommandLine.Option
import java.io.File import java.io.File
open class GuiSwitch { open class CliParser {
@Option(names = ["-n", "--network-name"], description = ["The resource grouping to use"])
var name: String? = null
@Option(names = ["-h", "--help"], usageHelp = true, description = ["display this help message"]) @Option(names = ["-g", "--gui"], description = ["Run the graphical user interface"])
var usageHelpRequested: Boolean = false
@Option(names = ["-g", "--gui"], description = ["Run in Gui Mode"])
var gui = false var gui = false
@CommandLine.Unmatched
var unmatched = arrayListOf<String>()
}
open class CliParser : GuiSwitch() {
@Option(names = ["-n", "--network-name"], description = ["The resource grouping to use"], required = true)
lateinit var name: String
@Option(names = ["-d", "--nodes-directory"], description = ["The directory to search for nodes in"]) @Option(names = ["-d", "--nodes-directory"], description = ["The directory to search for nodes in"])
var baseDirectory = File(System.getProperty("user.dir")) var baseDirectory = File(System.getProperty("user.dir"))
@Option(names = ["-b", "--backend"], description = ["The backend to use when instantiating nodes"]) @Option(names = ["-b", "--backend"], description = ["The backend to use when instantiating nodes"])
var backendType: Backend.BackendType = Backend.BackendType.LOCAL_DOCKER var backendType: Backend.BackendType = Backend.BackendType.LOCAL_DOCKER
@Option(names = ["-nodes"], split = ":", description = ["The number of each node to create NodeX:2 will create two instances of NodeX"]) @Option(names = ["--nodes"], split = ":", description = ["The number of each node to create. NodeX:2 will create two instances of NodeX"])
var nodes: MutableMap<String, Int> = hashMapOf() var nodes: MutableMap<String, Int> = hashMapOf()
@Option(names = ["--add", "-a"]) @Option(names = ["--add", "-a"])
@ -43,11 +33,9 @@ open class CliParser : GuiSwitch() {
open fun backendOptions(): Map<String, String> { open fun backendOptions(): Map<String, String> {
return emptyMap() return emptyMap()
} }
} }
class AzureParser : CliParser() { class AzureParser : CliParser() {
companion object { companion object {
val regions = Region.values().map { it.name() to it }.toMap() val regions = Region.values().map { it.name() to it }.toMap()
} }
@ -64,5 +52,4 @@ class AzureParser : CliParser() {
override fun backendOptions(): Map<String, String> { override fun backendOptions(): Map<String, String> {
return mapOf(Constants.REGION_ARG_NAME to region.name()) return mapOf(Constants.REGION_ARG_NAME to region.name())
} }
} }

View File

@ -2,128 +2,143 @@ package net.corda.bootstrapper.gui
import com.microsoft.azure.management.resources.fluentcore.arm.Region import com.microsoft.azure.management.resources.fluentcore.arm.Region
import javafx.beans.property.SimpleObjectProperty import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.SimpleStringProperty
import javafx.collections.ObservableListBase
import javafx.collections.transformation.SortedList import javafx.collections.transformation.SortedList
import javafx.event.EventHandler import javafx.event.EventHandler
import javafx.scene.control.ChoiceDialog import javafx.fxml.FXML
import javafx.scene.control.TableView.CONSTRAINED_RESIZE_POLICY import javafx.scene.control.*
import javafx.scene.control.TextInputDialog
import javafx.scene.input.MouseEvent import javafx.scene.input.MouseEvent
import javafx.scene.layout.HBox
import javafx.scene.layout.Priority import javafx.scene.layout.Priority
import javafx.scene.layout.VBox
import javafx.stage.DirectoryChooser import javafx.stage.DirectoryChooser
import net.corda.bootstrapper.Constants import net.corda.bootstrapper.Constants
import net.corda.bootstrapper.GuiUtils import net.corda.bootstrapper.GuiUtils
import net.corda.bootstrapper.NetworkBuilder import net.corda.bootstrapper.NetworkBuilder
import net.corda.bootstrapper.backends.Backend import net.corda.bootstrapper.backends.Backend
import net.corda.bootstrapper.baseArgs
import net.corda.bootstrapper.context.Context import net.corda.bootstrapper.context.Context
import net.corda.bootstrapper.nodes.* import net.corda.bootstrapper.nodes.*
import net.corda.bootstrapper.notaries.NotaryFinder import net.corda.bootstrapper.notaries.NotaryFinder
import org.apache.commons.lang3.RandomStringUtils import org.apache.commons.lang3.RandomStringUtils
import org.controlsfx.control.SegmentedButton
import tornadofx.* import tornadofx.*
import java.io.File import java.io.File
import java.util.* import java.util.*
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.atomic.AtomicInteger
import kotlin.Comparator
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
class BootstrapperView : View("Network Bootstrapper") { class BootstrapperView : View("Corda Network Builder") {
val YAML_MAPPER = Constants.getContextMapper() val YAML_MAPPER = Constants.getContextMapper()
override val root: VBox by fxml("/views/mainPane.fxml")
val controller: State by inject() val controller: State by inject()
val textarea = textarea { val localDockerBtn: ToggleButton by fxid()
maxWidth = Double.MAX_VALUE val azureBtn: ToggleButton by fxid()
maxHeight = Double.MAX_VALUE val nodeTableView: TableView<NodeTemplateInfo> by fxid()
} val templateChoiceBox: ChoiceBox<String> by fxid()
val buildButton: Button by fxid()
val addInstanceButton: Button by fxid()
val infoTextArea: TextArea by fxid()
override val root = vbox { init {
visuallyTweakBackendSelector()
menubar { buildButton.run {
menu("File") { enableWhen { controller.baseDir.isNotNull }
item("Open") { action {
action { var networkName = "corda-network"
selectNodeDirectory().thenAcceptAsync({ (notaries: List<FoundNode>, nodes: List<FoundNode>) ->
controller.nodes(nodes) val selectedBackEnd = when {
controller.notaries(notaries) azureBtn.isSelected -> Backend.BackendType.AZURE
}) localDockerBtn.isSelected -> Backend.BackendType.LOCAL_DOCKER
else -> kotlin.error("Unknown backend selected")
}
val backendParams = when (selectedBackEnd) {
Backend.BackendType.LOCAL_DOCKER -> {
emptyMap()
}
Backend.BackendType.AZURE -> {
val pair = setupAzureRegionOptions()
networkName = pair.second
pair.first
} }
} }
item("Build") { val nodeCount = controller.foundNodes.map { it.id to it.count }.toMap()
enableWhen(controller.baseDir.isNotNull) val result = NetworkBuilder.instance()
action { .withBasedir(controller.baseDir.get())
controller.clear() .withNetworkName(networkName)
val availableBackends = getAvailableBackends() .onNodeStartBuild(controller::onBuild)
val backend = ChoiceDialog<Backend.BackendType>(availableBackends.first(), availableBackends).showAndWait() .onNodeBuild(controller::addBuiltNode)
var networkName = "gui-network" .onNodePushStart(controller::addBuiltNode)
backend.ifPresent { selectedBackEnd -> .onNodePushed(controller::addPushedNode)
.onNodeInstancesRequested(controller::addInstanceRequests)
.onNodeInstance(controller::addInstance)
.withBackend(selectedBackEnd)
.withNodeCounts(nodeCount)
.withBackendOptions(backendParams)
.build()
val backendParams = when (selectedBackEnd) { result.handle { v, t ->
Backend.BackendType.LOCAL_DOCKER -> { runLater {
if (t != null) {
GuiUtils.showException("Failed to build network", "Failure due to", t)
} else {
controller.networkContext.set(v.second)
}
}
}
}
}
emptyMap<String, String>() templateChoiceBox.run {
} enableWhen { controller.networkContext.isNotNull }
Backend.BackendType.AZURE -> { controller.networkContext.addListener { _, _, newValue ->
val defaultName = RandomStringUtils.randomAlphabetic(4) + "-network" if (newValue != null) {
val textInputDialog = TextInputDialog(defaultName) items = object : ObservableListBase<String>() {
textInputDialog.title = "Choose Network Name" override fun get(index: Int): String {
networkName = textInputDialog.showAndWait().orElseGet { defaultName } return controller.foundNodes[index].id
mapOf(Constants.REGION_ARG_NAME to ChoiceDialog<Region>(Region.EUROPE_WEST, Region.values().toList().sortedBy { it.name() }).showAndWait().get().name()) }
}
override val size: Int
get() = controller.foundNodes.size
}
selectionModel.select(controller.foundNodes[0].id)
}
}
}
addInstanceButton.run {
enableWhen { controller.networkContext.isNotNull }
action {
templateChoiceBox.selectionModel.selectedItem?.let { nodeToAdd ->
val context = controller.networkContext.value
runLater {
val (_, instantiator, _) = Backend.fromContext(
context,
File(controller.baseDir.get(), Constants.BOOTSTRAPPER_DIR_NAME))
val nodeAdder = NodeAdder(context, NodeInstantiator(instantiator, context))
controller.addInstanceRequest(nodeToAdd)
nodeAdder.addNode(context, nodeToAdd).handleAsync { instanceInfo, t ->
t?.let {
GuiUtils.showException("Failed", "Failed to add node", it)
} }
instanceInfo?.let {
val nodeCount = controller.foundNodes.map { it.id to it.count }.toMap()
val result = NetworkBuilder.instance()
.withBasedir(controller.baseDir.get())
.withNetworkName(networkName)
.onNodeBuild(controller::addBuiltNode)
.onNodePushed(controller::addPushedNode)
.onNodeInstance(controller::addInstance)
.withBackend(selectedBackEnd)
.withNodeCounts(nodeCount)
.withBackendOptions(backendParams)
.build()
result.handle { v, t ->
runLater { runLater {
if (t != null) { controller.addInstance(NodeInstanceEntry(
GuiUtils.showException("Failed to build network", "Failure due to", t) it.groupId,
} else { it.instanceName,
controller.networkContext.set(v.second) it.instanceAddress,
} it.reachableAddress,
} it.portMapping[Constants.NODE_P2P_PORT] ?: Constants.NODE_P2P_PORT,
} it.portMapping[Constants.NODE_SSHD_PORT]
} ?: Constants.NODE_SSHD_PORT))
}
}
item("Add Node") {
enableWhen(controller.networkContext.isNotNull)
action {
val foundNodes = controller.foundNodes.map { it.id }
val nodeToAdd = ChoiceDialog<String>(foundNodes.first(), *foundNodes.toTypedArray()).showAndWait()
val context = controller.networkContext.value
nodeToAdd.ifPresent { node ->
runLater {
val (_, instantiator, _) = Backend.fromContext(
context,
File(controller.baseDir.get(), Constants.BOOTSTRAPPER_DIR_NAME))
val nodeAdder = NodeAdder(context, NodeInstantiator(instantiator, context))
nodeAdder.addNode(context, node).handleAsync { instanceInfo, t ->
t?.let {
GuiUtils.showException("Failed", "Failed to add node", it)
}
instanceInfo?.let {
runLater {
controller.addInstance(NodeInstanceTableEntry(
it.groupId,
it.instanceName,
it.instanceAddress,
it.reachableAddress,
it.portMapping[Constants.NODE_P2P_PORT] ?: Constants.NODE_P2P_PORT,
it.portMapping[Constants.NODE_SSHD_PORT]
?: Constants.NODE_SSHD_PORT))
}
}
} }
} }
} }
@ -132,89 +147,72 @@ class BootstrapperView : View("Network Bootstrapper") {
} }
} }
hbox { nodeTableView.run {
vbox { items = controller.sortedNodes
label("Nodes to build") column("ID", NodeTemplateInfo::templateId)
val foundNodesTable = tableview(controller.foundNodes) { column("Type", NodeTemplateInfo::nodeType)
readonlyColumn("ID", FoundNodeTableEntry::id) column("Local Docker Image", NodeTemplateInfo::localDockerImageId)
column("Count", FoundNodeTableEntry::count).makeEditable() column("Repository Image", NodeTemplateInfo::repositoryImageId)
vgrow = Priority.ALWAYS column("Status", NodeTemplateInfo::status)
hgrow = Priority.ALWAYS columnResizePolicy = TableView.CONSTRAINED_RESIZE_POLICY
}
foundNodesTable.columnResizePolicy = CONSTRAINED_RESIZE_POLICY
label("Notaries to build")
val notaryListView = listview(controller.foundNotaries) {
vgrow = Priority.ALWAYS
hgrow = Priority.ALWAYS
}
notaryListView.cellFormat { text = it.name }
vgrow = Priority.ALWAYS
hgrow = Priority.ALWAYS
}
vbox {
label("Built Nodes")
tableview(controller.builtNodes) {
readonlyColumn("ID", BuiltNodeTableEntry::id)
readonlyColumn("LocalImageId", BuiltNodeTableEntry::localImageId)
columnResizePolicy = CONSTRAINED_RESIZE_POLICY
vgrow = Priority.ALWAYS
hgrow = Priority.ALWAYS
}
label("Pushed Nodes")
tableview(controller.pushedNodes) {
readonlyColumn("ID", PushedNode::name)
readonlyColumn("RemoteImageId", PushedNode::remoteImageName)
columnResizePolicy = CONSTRAINED_RESIZE_POLICY
vgrow = Priority.ALWAYS
hgrow = Priority.ALWAYS
}
vgrow = Priority.ALWAYS
hgrow = Priority.ALWAYS
}
borderpane {
top = vbox {
label("Instances")
tableview(controller.nodeInstances) {
onMouseClicked = EventHandler<MouseEvent> { _ ->
textarea.text = YAML_MAPPER.writeValueAsString(selectionModel.selectedItem)
}
readonlyColumn("ID", NodeInstanceTableEntry::id)
readonlyColumn("InstanceId", NodeInstanceTableEntry::nodeInstanceName)
readonlyColumn("Address", NodeInstanceTableEntry::address)
columnResizePolicy = CONSTRAINED_RESIZE_POLICY
}
}
center = textarea
vgrow = Priority.ALWAYS
hgrow = Priority.ALWAYS
}
vgrow = Priority.ALWAYS
hgrow = Priority.ALWAYS hgrow = Priority.ALWAYS
onMouseClicked = EventHandler<MouseEvent> { _ ->
val selectedItem: NodeTemplateInfo = selectionModel.selectedItem ?: return@EventHandler
infoTextArea.text = YAML_MAPPER.writeValueAsString(translateForPrinting(selectedItem))
}
} }
try {
processSelectedDirectory(baseArgs.baseDirectory)
} catch (e: Exception) {
e.printStackTrace()
}
} }
private fun getAvailableBackends(): List<Backend.BackendType> { private fun visuallyTweakBackendSelector() {
return Backend.BackendType.values().toMutableList(); // The SegmentedButton will jam together the two toggle buttons in a way
// that looks more modern.
val hBox = localDockerBtn.parent as HBox
val idx = hBox.children.indexOf(localDockerBtn)
// Adding this to the hbox will re-parent the two toggle buttons into the
// SegmentedButton control, so we have to put it in the same position as
// the original buttons. Unfortunately it's not so Scene Builder friendly.
hBox.children.add(idx, SegmentedButton(localDockerBtn, azureBtn).apply {
styleClass.add(SegmentedButton.STYLE_CLASS_DARK)
})
} }
private fun setupAzureRegionOptions(): Pair<Map<String, String>, String> {
fun selectNodeDirectory(): CompletableFuture<Pair<List<FoundNode>, List<FoundNode>>> { var networkName1 = RandomStringUtils.randomAlphabetic(4) + "-network"
val fileChooser = DirectoryChooser(); val textInputDialog = TextInputDialog(networkName1)
fileChooser.initialDirectory = File(System.getProperty("user.home")) textInputDialog.title = "Azure Resource Group"
val file = fileChooser.showDialog(null) networkName1 = textInputDialog.showAndWait().orElseGet { networkName1 }
controller.baseDir.set(file) return Pair(mapOf(Constants.REGION_ARG_NAME to ChoiceDialog<Region>(Region.EUROPE_WEST, Region.values().toList().sortedBy { it.name() }).showAndWait().get().name()), networkName1)
return processSelectedDirectory(file)
} }
private fun translateForPrinting(selectedItem: NodeTemplateInfo): Any {
return object {
val templateId = selectedItem.templateId.get()
val nodeType = selectedItem.nodeType.get()
val localDockerImageId = selectedItem.localDockerImageId.get()
val repositoryImageId = selectedItem.repositoryImageId.get()
val status = selectedItem.status.get()
val instances = selectedItem.instances.map { it }
}
}
fun processSelectedDirectory(dir: File): CompletableFuture<Pair<List<FoundNode>, List<FoundNode>>> { @FXML
fun onOpenClicked() {
val chooser = DirectoryChooser()
chooser.initialDirectory = File(System.getProperty("user.home"))
val file: File = chooser.showDialog(null) ?: return // Null means user cancelled.
processSelectedDirectory(file)
}
private fun processSelectedDirectory(dir: File) {
controller.clearAll()
controller.baseDir.set(dir)
val foundNodes = CompletableFuture.supplyAsync { val foundNodes = CompletableFuture.supplyAsync {
val nodeFinder = NodeFinder(dir) val nodeFinder = NodeFinder(dir)
nodeFinder.findNodes() nodeFinder.findNodes()
@ -223,101 +221,148 @@ class BootstrapperView : View("Network Bootstrapper") {
val notaryFinder = NotaryFinder(dir) val notaryFinder = NotaryFinder(dir)
notaryFinder.findNotaries() notaryFinder.findNotaries()
} }
return foundNodes.thenCombine(foundNotaries) { nodes, notaries -> foundNodes.thenCombine(foundNotaries) { nodes, notaries ->
notaries to nodes notaries to nodes
}.thenAcceptAsync({ (notaries: List<FoundNode>, nodes: List<FoundNode>) ->
runLater {
controller.foundNodes(nodes)
controller.notaries(notaries)
}
})
}
class NodeTemplateInfo(templateId: String, type: NodeType) {
val templateId: SimpleStringProperty = object : SimpleStringProperty(templateId) {
override fun toString(): String {
return this.get()?.toString() ?: "null"
}
} }
} val nodeType: SimpleObjectProperty<NodeType> = SimpleObjectProperty(type)
} val localDockerImageId: SimpleStringProperty = SimpleStringProperty()
val repositoryImageId: SimpleStringProperty = SimpleStringProperty()
class State : Controller() { val status: SimpleObjectProperty<NodeBuildStatus> = SimpleObjectProperty(NodeBuildStatus.DISCOVERED)
val instances: MutableList<NodeInstanceEntry> = ArrayList()
val foundNodes = Collections.synchronizedList(ArrayList<FoundNodeTableEntry>()).observable() val numberOfInstancesWaiting: AtomicInteger = AtomicInteger(-1)
val builtNodes = Collections.synchronizedList(ArrayList<BuiltNodeTableEntry>()).observable()
val pushedNodes = Collections.synchronizedList(ArrayList<PushedNode>()).observable()
private val backingUnsortedInstances = Collections.synchronizedList(ArrayList<NodeInstanceTableEntry>()).observable()
val nodeInstances = SortedList(backingUnsortedInstances, COMPARATOR)
val foundNotaries = Collections.synchronizedList(ArrayList<FoundNode>()).observable()
val networkContext = SimpleObjectProperty<Context>(null)
fun clear() {
builtNodes.clear()
pushedNodes.clear()
backingUnsortedInstances.clear()
networkContext.set(null)
} }
fun nodes(nodes: List<FoundNode>) { enum class NodeBuildStatus {
foundNodes.clear() DISCOVERED, LOCALLY_BUILDING, LOCALLY_BUILT, REMOTE_PUSHING, REMOTE_PUSHED, INSTANTIATING, INSTANTIATED,
nodes.forEach { addFoundNode(it) }
} }
fun notaries(notaries: List<FoundNode>) { enum class NodeType {
foundNotaries.clear() NODE, NOTARY
notaries.forEach { runLater { foundNotaries.add(it) } }
} }
var baseDir = SimpleObjectProperty<File>(null) class State : Controller() {
val foundNodes = Collections.synchronizedList(ArrayList<FoundNodeTableEntry>()).observable()
val foundNotaries = Collections.synchronizedList(ArrayList<FoundNode>()).observable()
val networkContext = SimpleObjectProperty<Context>(null)
val unsortedNodes = Collections.synchronizedList(ArrayList<NodeTemplateInfo>()).observable()
val sortedNodes = SortedList(unsortedNodes, Comparator<NodeTemplateInfo> { o1, o2 ->
compareValues(o1.nodeType.toString() + o1.templateId, o2.nodeType.toString() + o2.templateId) * -1
})
fun addFoundNode(foundNode: FoundNode) { fun clear() {
runLater { networkContext.set(null)
foundNodes.add(FoundNodeTableEntry(foundNode.name))
} }
}
fun addBuiltNode(builtNode: BuiltNode) { fun clearAll() {
runLater { networkContext.set(null)
builtNodes.add(BuiltNodeTableEntry(builtNode.name, builtNode.localImageId)) foundNodes.clear()
foundNotaries.clear()
unsortedNodes.clear()
} }
}
fun addPushedNode(pushedNode: PushedNode) { fun foundNodes(nodesToAdd: List<FoundNode>) {
runLater { foundNodes.clear()
pushedNodes.add(pushedNode) nodesToAdd.forEach {
runLater {
foundNodes.add(FoundNodeTableEntry(it.name))
unsortedNodes.add(NodeTemplateInfo(it.name, NodeType.NODE))
}
}
} }
}
fun addInstance(nodeInstance: NodeInstance) { fun notaries(notaries: List<FoundNode>) {
runLater { foundNotaries.clear()
backingUnsortedInstances.add(NodeInstanceTableEntry( notaries.forEach {
runLater {
foundNotaries.add(it)
unsortedNodes.add(NodeTemplateInfo(it.name, NodeType.NOTARY))
}
}
}
var baseDir = SimpleObjectProperty<File>(null)
fun addBuiltNode(builtNode: BuiltNode) {
runLater {
val foundNode = unsortedNodes.find { it.templateId.get() == builtNode.name }
foundNode?.status?.set(NodeBuildStatus.LOCALLY_BUILT)
foundNode?.localDockerImageId?.set(builtNode.localImageId)
}
}
fun addPushedNode(pushedNode: PushedNode) {
runLater {
val foundNode = unsortedNodes.find { it.templateId.get() == pushedNode.name }
foundNode?.status?.set(NodeBuildStatus.REMOTE_PUSHED)
foundNode?.repositoryImageId?.set(pushedNode.remoteImageName)
}
}
fun onBuild(nodeBuilding: FoundNode) {
val foundNode = unsortedNodes.find { it.templateId.get() == nodeBuilding.name }
foundNode?.status?.set(NodeBuildStatus.LOCALLY_BUILDING)
}
fun addInstance(nodeInstance: NodeInstance) {
addInstance(NodeInstanceEntry(
nodeInstance.name, nodeInstance.name,
nodeInstance.nodeInstanceName, nodeInstance.nodeInstanceName,
nodeInstance.expectedFqName, nodeInstance.expectedFqName,
nodeInstance.reachableAddress, nodeInstance.reachableAddress,
nodeInstance.portMapping[Constants.NODE_P2P_PORT] ?: Constants.NODE_P2P_PORT, nodeInstance.portMapping[Constants.NODE_P2P_PORT] ?: Constants.NODE_P2P_PORT,
nodeInstance.portMapping[Constants.NODE_SSHD_PORT] ?: Constants.NODE_SSHD_PORT)) nodeInstance.portMapping[Constants.NODE_SSHD_PORT] ?: Constants.NODE_SSHD_PORT)
)
} }
}
fun addInstance(nodeInstance: NodeInstanceTableEntry) { fun addInstanceRequests(requests: List<NodeInstanceRequest>) {
runLater { requests.firstOrNull()?.let { request ->
backingUnsortedInstances.add(nodeInstance) unsortedNodes.find { it.templateId.get() == request.name }?.let {
} it.numberOfInstancesWaiting.set(requests.size)
} it.status.set(NodeBuildStatus.INSTANTIATING)
}
companion object {
val COMPARATOR: (NodeInstanceTableEntry, NodeInstanceTableEntry) -> Int = { o1, o2 ->
if (o1.id == (o2.id)) {
o1.nodeInstanceName.compareTo(o2.nodeInstanceName)
} else {
o1.id.compareTo(o2.id)
} }
} }
fun addInstance(nodeInstance: NodeInstanceEntry) {
runLater {
val foundNode = unsortedNodes.find { it.templateId.get() == nodeInstance.id }
foundNode?.instances?.add(nodeInstance)
if (foundNode != null && foundNode.instances.size == foundNode.numberOfInstancesWaiting.get()) {
foundNode.status.set(NodeBuildStatus.INSTANTIATED)
}
}
}
fun addInstanceRequest(nodeToAdd: String) {
val foundNode = unsortedNodes.find { it.templateId.get() == nodeToAdd }
foundNode?.numberOfInstancesWaiting?.incrementAndGet()
foundNode?.status?.set(NodeBuildStatus.INSTANTIATING)
}
} }
data class NodeInstanceEntry(val id: String,
val nodeInstanceName: String,
val address: String,
val locallyReachableAddress: String,
val rpcPort: Int,
val sshPort: Int)
} }
data class FoundNodeTableEntry(val id: String, data class FoundNodeTableEntry(val id: String, @Volatile var count: Int = 1)
@Volatile var count: Int = 1)
data class BuiltNodeTableEntry(val id: String, val localImageId: String)
data class NodeInstanceTableEntry(val id: String,
val nodeInstanceName: String,
val address: String,
val locallyReachableAddress: String,
val rpcPort: Int,
val sshPort: Int)

View File

@ -1,11 +1,11 @@
package net.corda.bootstrapper.gui package net.corda.bootstrapper.gui
import javafx.application.Application import javafx.stage.Stage
import tornadofx.App import tornadofx.*
class Gui : App(BootstrapperView::class) { class Gui : App(BootstrapperView::class) {
companion object { override fun start(stage: Stage) {
@JvmStatic super.start(stage)
fun main(args: Array<String>) = Application.launch(Gui::class.java, *args) stage.scene.stylesheets.add("/views/bootstrapper.css")
} }
} }

View File

@ -0,0 +1,13 @@
.top-pane {
-fx-background-color: white;
}
.top-pane > .button, .top-pane .toggle-button {
-fx-padding: 15px;
-fx-base: white;
}
.top-pane > .choice-box {
-fx-padding: 10px;
-fx-base: white;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.*?>
<VBox prefHeight="768.0" prefWidth="1024.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1">
<children>
<HBox alignment="CENTER_LEFT" minHeight="-Infinity" prefHeight="75.0" prefWidth="1073.0" styleClass="top-pane">
<children>
<ImageView fitHeight="150.0" fitWidth="100.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@cordalogo.png" />
</image>
</ImageView>
<Pane prefHeight="75.0" prefWidth="21.0" />
<Button mnemonicParsing="false" onAction="#onOpenClicked" text="Open nodes ..." />
<Button fx:id="buildButton" mnemonicParsing="false" text="Build">
<HBox.margin>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</HBox.margin>
</Button>
<ToggleButton fx:id="localDockerBtn" mnemonicParsing="false" selected="true" text="Local Docker">
<toggleGroup>
<ToggleGroup fx:id="target" />
</toggleGroup>
</ToggleButton>
<ToggleButton fx:id="azureBtn" mnemonicParsing="false" text="Azure" toggleGroup="$target" />
<ChoiceBox fx:id="templateChoiceBox" prefWidth="150.0">
<HBox.margin>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</HBox.margin>
</ChoiceBox>
<Button fx:id="addInstanceButton" mnemonicParsing="false" text="Add Instance">
<HBox.margin>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</HBox.margin>
</Button>
</children>
</HBox>
<HBox maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="461.0" prefWidth="1014.0" VBox.vgrow="ALWAYS">
<children>
<SplitPane dividerPositions="0.6940371456500489" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" HBox.hgrow="ALWAYS">
<items>
<TableView fx:id="nodeTableView" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="458.0" prefWidth="662.0" />
<TextArea fx:id="infoTextArea" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="458.0" prefWidth="353.0" />
</items>
</SplitPane>
</children>
</HBox>
</children>
</VBox>