diff --git a/.gitignore b/.gitignore index 9bb37b3..902e9c1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /dist /tmp node_modules +build/** diff --git a/package.json b/package.json index aafc0e4..5d721ea 100644 --- a/package.json +++ b/package.json @@ -12,16 +12,26 @@ "@oclif/config": "^1", "@oclif/plugin-help": "^2", "@terrastack/hcl2json-wasm": "^1", + "@terrastack/ink": "^2.0.0", "chalk": "^2.4.1", "es6-template-strings": "^2.0.1", "eventemitter2": "^5.0.1", "lodash": "^4.17.11", "log4js": "^3.0.6", "mustache": "^3.0.0", + "prop-types": "^15.6.2", + "react": "^16.6.3", + "react-redux": "^5.1.1", "recursive-copy": "^2.0.9", + "redux": "^4.0.1", + "terraform-plan-parser": "^1.5.0", "underscore.string": "^3.3.5" }, "devDependencies": { + "@babel/cli": "^7.1.5", + "@babel/core": "^7.1.6", + "@babel/plugin-transform-modules-commonjs": "^7.1.0", + "@babel/preset-react": "^7.0.0", "@oclif/dev-cli": "^1", "eslint": "^5.5", "eslint-config-oclif": "^3.1", @@ -46,7 +56,7 @@ "license": "MPL-2.0", "main": "src/index.js", "oclif": { - "commands": "./src/commands", + "commands": "./build/commands", "bin": "terrastack", "plugins": [ "@oclif/plugin-help" @@ -57,8 +67,18 @@ } } }, + "babel": { + "plugins": [ + "@babel/plugin-transform-modules-commonjs" + ], + "presets": [ + "@babel/preset-react" + ] + }, "repository": "terrastackio/terrastack-cli", "scripts": { + "build": "babel src --out-dir build --copy-files", + "build:watch": "babel src --watch --out-dir build --copy-files", "postpack": "rm -f oclif.manifest.json npm-shrinkwrap.json", "prepack": "oclif-dev manifest && oclif-dev readme && npm shrinkwrap", "test": "jest", diff --git a/src/commands/stack/apply.js b/src/commands/stack/apply.js index 97db796..448a644 100644 --- a/src/commands/stack/apply.js +++ b/src/commands/stack/apply.js @@ -7,6 +7,7 @@ const { Command, flags } = require("@oclif/command"); const { Terrastack } = require("../../orchestration/terrastack"); const applyLogging = require("../../logging.js"); +const { applyVisualization } = require("../../ui"); class ApplyCommand extends Command { async run() { @@ -14,6 +15,7 @@ class ApplyCommand extends Command { const stack = require(process.cwd() + "/stack.js"); const terrastack = new Terrastack(stack); applyLogging(terrastack); + applyVisualization(terrastack); (async () => { await terrastack.apply(); })(); diff --git a/src/commands/stack/destroy.js b/src/commands/stack/destroy.js index 7149caf..388fd77 100644 --- a/src/commands/stack/destroy.js +++ b/src/commands/stack/destroy.js @@ -7,6 +7,7 @@ const { Command, flags } = require("@oclif/command"); const { Terrastack } = require("../../orchestration/terrastack"); const applyLogging = require("../../logging.js"); +const { applyVisualization } = require("../../ui"); class DestroyCommand extends Command { async run() { @@ -14,6 +15,7 @@ class DestroyCommand extends Command { const stack = require(process.cwd() + "/stack.js"); const terrastack = new Terrastack(stack); applyLogging(terrastack); + applyVisualization(terrastack); (async () => { await terrastack.destroy(); })(); diff --git a/src/commands/stack/plan.js b/src/commands/stack/plan.js index 13e0081..8006d0e 100644 --- a/src/commands/stack/plan.js +++ b/src/commands/stack/plan.js @@ -7,6 +7,7 @@ const { Command, flags } = require("@oclif/command"); const { Terrastack } = require("../../orchestration/terrastack"); const applyLogging = require("../../logging.js"); +const { applyVisualization } = require("../../ui"); class PlanCommand extends Command { async run() { @@ -14,6 +15,7 @@ class PlanCommand extends Command { const stack = require(process.cwd() + "/stack.js"); const terrastack = new Terrastack(stack); applyLogging(terrastack); + applyVisualization(terrastack); (async () => { await terrastack.plan(); })(); diff --git a/src/component/index.js b/src/component/index.js index 33b4ccb..ec15708 100644 --- a/src/component/index.js +++ b/src/component/index.js @@ -18,13 +18,13 @@ const initComponent = (name, version, description) => { fs.writeFileSync(".terrastack/component/index.js", files.compononentJs); fs.writeFileSync(".terrastack/@types/index.d.ts", files.compononentTypes); - const package = Object.assign( + const packageJSON = Object.assign( {}, { name, version, description }, packageDefaults ); - fs.writeFileSync("package.json", JSON.stringify(package, null, 2)); + fs.writeFileSync("package.json", JSON.stringify(packageJSON, null, 2)); console.log("Successfully wrapped component"); }; diff --git a/src/logging.js b/src/logging.js index d88f9b0..c6ba6c3 100644 --- a/src/logging.js +++ b/src/logging.js @@ -29,14 +29,14 @@ const applyLogger = base => { buffer.push(output); }); - base.events.on("component:**", function(component) { - console.log(`${component.name}: ${this.event}`); - }); + // base.events.on("component:**", function(component) { + // console.log(`${component.name}: ${this.event}`); + // }); - base.events.on("error", function(component) { - console.log(chalk.red.bold.underline(`Error: ${component.name}`)); - console.log(`Recent output: ${_.takeRight(buffer, 50).join("")}`); - }); + // base.events.on("error", function(component) { + // console.log(chalk.red.bold.underline(`Error: ${component.name}`)); + // console.log(`Recent output: ${_.takeRight(buffer, 50).join("")}`); + // }); }; module.exports = applyLogger; diff --git a/src/orchestration/terraform.js b/src/orchestration/terraform.js index 11583fa..a2d2689 100644 --- a/src/orchestration/terraform.js +++ b/src/orchestration/terraform.js @@ -6,6 +6,7 @@ const { spawn } = require("child_process"); const eventbus = require("./eventbus"); +const parser = require("terraform-plan-parser"); class Terraform { constructor(component) { @@ -55,9 +56,11 @@ class Terraform { () => { eventbus.emit("component:plan:success", this.component); }, - code => { + ({ code, stdout }) => { // Code 2 means: Succeeded, but there is a diff if (code == 2) { + const result = parser.parseStdout(stdout); + this.component._diff = result; eventbus.emit("component:plan:diff", this.component); } else { eventbus.emit("component:plan:failed", this.component); @@ -87,7 +90,7 @@ class Terraform { this.component.setOutput(JSON.parse(output)); eventbus.emit(`component:output:success`, this.component); }, - code => { + ({ code }) => { // eventbus.emit(`component:output:failed`, this.component, code); // eventbus.emit("error", this.component); // throw "failed"; @@ -124,7 +127,7 @@ class Terraform { proc.on("close", function(code) { if (code !== 0) { - reject(code); + reject({ code, stdout }); } else { resolve(stdout); } @@ -137,7 +140,7 @@ class Terraform { () => { eventbus.emit(`component:${command}:success`, this.component); }, - code => { + ({ code }) => { eventbus.emit(`component:${command}:failed`, this.component, code); eventbus.emit("error", this.component); throw "failed"; diff --git a/src/ui/components/spinner.js b/src/ui/components/spinner.js new file mode 100644 index 0000000..f48d540 --- /dev/null +++ b/src/ui/components/spinner.js @@ -0,0 +1,44 @@ +import { Component } from "react"; +import spinners from "cli-spinners"; + +class Spinner extends Component { + constructor(props) { + super(props); + + this.state = { frame: 0 }; + this.switchFrame = this.switchFrame.bind(this); + } + + getSpinner() { + return spinners.dots; + } + + render() { + const spinner = this.getSpinner(); + return spinner.frames[this.state.frame]; + } + + componentDidMount() { + const spinner = this.getSpinner(); + + this.timer = setInterval(this.switchFrame, spinner.interval); + } + + componentWillUnmount() { + clearInterval(this.timer); + } + + switchFrame() { + const { frame } = this.state; + + const spinner = this.getSpinner(); + const isLastFrame = frame === spinner.frames.length - 1; + const nextFrame = isLastFrame ? 0 : frame + 1; + + this.setState({ + frame: nextFrame + }); + } +} + +module.exports = { Spinner }; diff --git a/src/ui/components/stack-component.js b/src/ui/components/stack-component.js new file mode 100644 index 0000000..1839988 --- /dev/null +++ b/src/ui/components/stack-component.js @@ -0,0 +1,34 @@ +import React, { Component } from "react"; +import { Box, Text } from "@terrastack/ink"; + +class StackComponent extends Component { + render() { + return ( + + {this.props.item.name} + + ); + } + + color() { + switch (this.props.item.status) { + case "start": + return "cyan"; + case "success": + return "green"; + case "diff": + return "yellow"; + case "failed": + return "red"; + default: + return "white"; + } + } +} + +module.exports = { StackComponent }; diff --git a/src/ui/components/stack.js b/src/ui/components/stack.js new file mode 100644 index 0000000..0fc3090 --- /dev/null +++ b/src/ui/components/stack.js @@ -0,0 +1,100 @@ +import React, { Component } from "react"; +import { StackComponent } from "./stack-component"; +import { connect } from "react-redux"; +import { Box, Text } from "@terrastack/ink"; +import _ from "lodash"; + +class Row extends Component { + render() { + return ( + + {this.props.children} + + ); + } +} + +class Header extends Component { + render() { + return ( + + Stack: {this.props.stack.name} + + ); + } +} + +class Footer extends Component { + render() { + if (_.isEmpty(this.props.issues)) return " "; + const issues = this.props.issues.map(issue => { + return `${issue.component.name}: ${JSON.stringify(issue.component._diff, null, 2)}`; + }); + return ( + + + Note: {issues.join(",")} + + + ); + } +} + +class Logs extends Component { + render() { + if (_.isEmpty(this.props.issues)) return " "; + const issues = this.props.issues.map(issue => { + return `${issue.component.name}: ${issue.reason}`; + }); + return ( + + + Note: {issues.join(",")} + + + ); + } +} + +class Stack extends Component { + render() { + if (_.isEmpty(this.props.rows)) { + return "Nothing to see"; + } else { + const elements = this.props.rows.map((row, index) => ( + + + Step: {index} + + {row.map(item => ( + + ))} + + )); + + return ( + +
+ {elements} +