├── src ├── lib │ ├── vagrant │ │ ├── Machine.js │ │ └── parser.js │ ├── openBrowser.js │ ├── reducers │ │ ├── vagrant.js │ │ ├── preferences.js │ │ ├── boxes.js │ │ ├── installer.js │ │ ├── ui.js │ │ └── terminal.js │ ├── download.js │ ├── spawn-sync.js │ ├── formatPath.js │ ├── reducers.js │ ├── actions │ │ ├── updateGlobalStatus.js │ │ ├── install.js │ │ ├── cloneChassis.js │ │ ├── updateConfig.js │ │ ├── updateBoxStatus.js │ │ ├── runCommand.js │ │ └── loadConfig.js │ ├── configure.js │ ├── createStore.js │ ├── actions.js │ └── keys.js ├── Form │ ├── FixedValue.css │ ├── Table │ │ ├── index.js │ │ └── index.css │ └── FixedValue.js ├── Installer │ ├── Downloader.css │ ├── Vagrant.png │ ├── VirtualBox.png │ ├── Ready.css │ ├── Downloads.css │ ├── Welcome.js │ ├── Ready.js │ ├── DownloadStatus.js │ ├── ImportBoxes.css │ ├── Downloader.js │ ├── ImportBoxes.js │ └── Downloads.js ├── Step.js ├── ItemList.css ├── Modal.js ├── MachineActions.css ├── Icon.js ├── App.css ├── LoadingIndicator.css ├── LoadingIndicator.js ├── MachineList.css ├── MachineSettings.css ├── CreateModal.css ├── Modal.css ├── Settings.css ├── Toolbar.js ├── Splash.js ├── MachineDetails.scss ├── KeyHandler.css ├── Terminal.scss ├── about-entry.js ├── CreateModal │ ├── Type.css │ ├── Config.js │ └── Type.js ├── Header.js ├── Installer.scss ├── Button.js ├── Root.js ├── Steps.scss ├── index.js ├── Header.css ├── Installer.css ├── Terminal.js ├── Steps.js ├── Button.css ├── ItemList.js ├── Installer.js ├── About.css ├── About.js ├── KeyHandler.js ├── MachineItem.css ├── MachineList.js ├── Settings.js ├── logo.svg ├── App.js ├── MachineDetails.js ├── MachineActions.js ├── MachineSettings.js ├── CreateModal.js └── MachineItem.js ├── config ├── jest │ ├── CSSStub.js │ └── FileStub.js ├── storybook │ ├── dummy-electron.js │ ├── dummy-child_process.js │ ├── config.js │ └── webpack.config.js ├── polyfills.js ├── env.js ├── paths.js ├── webpack.config.dev.js └── webpack.config.prod.js ├── public ├── logo.png ├── spinner.svg ├── index.html ├── about.html └── loader.css ├── resources ├── icon.ico └── icon.icns ├── .gitignore ├── scripts ├── test.js ├── build.js └── start.js ├── stories ├── MachineSettings.js ├── Terminal.js ├── Header.js ├── MachineItem.js ├── Button.js └── MachineActions.js ├── license.md ├── readme.md ├── package.json └── electron.js /src/lib/vagrant/Machine.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/jest/CSSStub.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /config/jest/FileStub.js: -------------------------------------------------------------------------------- 1 | module.exports = "test-file-stub"; 2 | -------------------------------------------------------------------------------- /src/Form/FixedValue.css: -------------------------------------------------------------------------------- 1 | .FixedValue { 2 | opacity: 0.6; 3 | } 4 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chassis/Desktop/HEAD/public/logo.png -------------------------------------------------------------------------------- /src/Installer/Downloader.css: -------------------------------------------------------------------------------- 1 | .Downloader { 2 | // background: #c1e6f6; 3 | } 4 | -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chassis/Desktop/HEAD/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chassis/Desktop/HEAD/resources/icon.icns -------------------------------------------------------------------------------- /src/Step.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default props =>
  • ; 4 | -------------------------------------------------------------------------------- /src/Installer/Vagrant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chassis/Desktop/HEAD/src/Installer/Vagrant.png -------------------------------------------------------------------------------- /src/Installer/VirtualBox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Chassis/Desktop/HEAD/src/Installer/VirtualBox.png -------------------------------------------------------------------------------- /config/storybook/dummy-electron.js: -------------------------------------------------------------------------------- 1 | export const shell = { 2 | openExternal: url => console.log( url ), 3 | }; 4 | -------------------------------------------------------------------------------- /src/lib/openBrowser.js: -------------------------------------------------------------------------------- 1 | import { shell } from 'electron'; 2 | 3 | export default url => shell.openExternal( url ); 4 | -------------------------------------------------------------------------------- /src/ItemList.css: -------------------------------------------------------------------------------- 1 | .ItemList > input { 2 | display: block; 3 | } 4 | 5 | .ItemList > .Button { 6 | margin-left: 0; 7 | } 8 | -------------------------------------------------------------------------------- /config/storybook/dummy-child_process.js: -------------------------------------------------------------------------------- 1 | export const spawn = () => { 2 | throw new Error( 'Not running in Electron context.' ) 3 | }; 4 | -------------------------------------------------------------------------------- /src/Modal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './Modal.css'; 4 | 5 | export default props =>
    { props.children }
    ; 6 | -------------------------------------------------------------------------------- /src/Form/Table/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './index.css'; 4 | 5 | export default props =>
    ; 6 | -------------------------------------------------------------------------------- /src/Form/FixedValue.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './FixedValue.css'; 4 | 5 | export default ({ value }) =>
    { value }
    ; 6 | -------------------------------------------------------------------------------- /src/MachineActions.css: -------------------------------------------------------------------------------- 1 | .MachineActions { 2 | display: flex; 3 | justify-content: space-between; 4 | } 5 | 6 | .MachineActions .secondary { 7 | text-align: right; 8 | } 9 | -------------------------------------------------------------------------------- /src/Icon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default props => { 4 | let className = `fa fa-${props.type}`; 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | flex-grow: 1; 3 | width: 100%; 4 | background: #ececec; 5 | display: flex; 6 | flex-direction: column; 7 | } 8 | 9 | .app-content { 10 | overflow-y: auto; 11 | } 12 | -------------------------------------------------------------------------------- /src/LoadingIndicator.css: -------------------------------------------------------------------------------- 1 | @keyframes loading-spin { 2 | from { transform: rotate(0deg); } 3 | to { transform: rotate(360deg); } 4 | } 5 | 6 | .LoadingIndicator > i { 7 | animation: loading-spin infinite 1.4s linear; 8 | } 9 | -------------------------------------------------------------------------------- /src/LoadingIndicator.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Icon from './Icon'; 3 | 4 | import './LoadingIndicator.css'; 5 | 6 | export default () => ; 7 | -------------------------------------------------------------------------------- /src/MachineList.css: -------------------------------------------------------------------------------- 1 | .MachineList { 2 | background: #fff; 3 | } 4 | 5 | .MachineList .empty-message, .MachineList .undo { 6 | display: flex; 7 | flex-direction: column; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | -------------------------------------------------------------------------------- /src/MachineSettings.css: -------------------------------------------------------------------------------- 1 | .MachineSettings { 2 | padding: 1rem 2rem; 3 | } 4 | 5 | .MachineSettings .machine-actions .description { 6 | font-size: 0.8em; 7 | font-style: italic; 8 | opacity: 0.8; 9 | margin-top: 0.5em; 10 | margin-bottom: 0; 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | dist 12 | 13 | # misc 14 | .DS_Store 15 | .env 16 | npm-debug.log 17 | -------------------------------------------------------------------------------- /src/CreateModal.css: -------------------------------------------------------------------------------- 1 | .CreateModal { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | text-align: center; 6 | } 7 | 8 | .CreateModal .Header { 9 | align-self: stretch; 10 | } 11 | 12 | .CreateModal header .Button { 13 | // float: left; 14 | } 15 | -------------------------------------------------------------------------------- /src/Modal.css: -------------------------------------------------------------------------------- 1 | .Modal { 2 | background: #fff; 3 | position: absolute; 4 | z-index: 1; 5 | top: 0; 6 | left: 0; 7 | right: 0; 8 | bottom: 0; 9 | height: 100%; 10 | width: 100%; 11 | display: flex; 12 | flex-direction: column; 13 | } 14 | 15 | .Modal h1 { 16 | font-size: 1.5rem; 17 | } 18 | -------------------------------------------------------------------------------- /src/Installer/Ready.css: -------------------------------------------------------------------------------- 1 | @keyframes wave-rotate { 2 | 0% { 3 | transform: rotate(20deg); 4 | } 5 | 6 | 100% { 7 | transform: rotate(50deg); 8 | } 9 | } 10 | 11 | .Ready .wave { 12 | font-size: 3rem; 13 | margin-top: 0; 14 | 15 | animation: wave-rotate 1s linear alternate infinite; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/reducers/vagrant.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | export default function vagrant(state = {}, action) { 4 | switch ( action.type ) { 5 | case actions.INIT_VAGRANT: 6 | return { ...state, machines: action.machines }; 7 | 8 | default: 9 | return state; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Settings.css: -------------------------------------------------------------------------------- 1 | .Settings .Header { 2 | margin-bottom: 2rem; 3 | } 4 | .Settings > .FormTable { 5 | width: 100%; 6 | } 7 | .Settings p { 8 | line-height: 1.5; 9 | } 10 | .Settings .actions { 11 | text-align: center; 12 | } 13 | .Settings .will-reset { 14 | padding-left: 3rem; 15 | padding-right: 3rem; 16 | } 17 | -------------------------------------------------------------------------------- /src/Toolbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Toolbar extends React.Component { 4 | render() { 5 | return
    6 |
    7 | 8 |
    9 |
    10 | 11 |
    12 |
    ; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/reducers/preferences.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | export default function preferences(state = {}, action) { 4 | switch ( action.type ) { 5 | case actions.SET_PREFERENCE: 6 | return { ...state, [ action.key ]: action.value }; 7 | 8 | default: 9 | return state; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Splash.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Note: CSS defined in public/loader.css to load before JS does. 4 | 5 | export default class Splash extends React.Component { 6 | render() { 7 | return
    8 | 9 |

    10 | Loading… 11 |

    12 |
    ; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/MachineDetails.scss: -------------------------------------------------------------------------------- 1 | .MachineDetails { 2 | background: #f6f6f6; 3 | padding: 0.5rem; 4 | } 5 | .MachineDetails button { 6 | font-size: 0.857rem; 7 | } 8 | .MachineDetails .terminal-wrap { 9 | position: relative; 10 | background: darken(#29ABE2, 40%); 11 | } 12 | .MachineDetails .terminal-wrap > .Button { 13 | position: absolute; 14 | right: 1rem; 15 | top: 0.5rem; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/download.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import http from 'http'; 3 | import https from 'https'; 4 | 5 | export default function download(path, url, onStart) { 6 | const file = fs.createWriteStream(path); 7 | 8 | const get = url.startsWith('https://') ? https.get : http.get; 9 | get(url, response => { 10 | response.pipe(file); 11 | 12 | onStart( response ); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /src/KeyHandler.css: -------------------------------------------------------------------------------- 1 | .KeyHandler { 2 | position: absolute; 3 | left: 0; 4 | right: 0; 5 | margin-left: auto; 6 | margin-right: auto; 7 | display: inline-block; 8 | opacity: 0; 9 | transition: opacity 140ms; 10 | } 11 | .KeyHandler > span { 12 | padding: 0.4rem; 13 | background: rgba(102, 102, 102, 0.7); 14 | color: #fff; 15 | border-radius: 0.2rem; 16 | } 17 | .KeyHandler.visible { 18 | opacity: 1; 19 | } 20 | -------------------------------------------------------------------------------- /src/Terminal.scss: -------------------------------------------------------------------------------- 1 | .Terminal { 2 | background: darken(#29ABE2, 40%); 3 | color: #fff; 4 | height: 2.2rem; 5 | width: 100%; 6 | margin-top: 1rem; 7 | overflow: auto; 8 | } 9 | .Terminal.full { 10 | height: 10rem; 11 | } 12 | .Terminal pre { 13 | padding: 0.6rem 1rem; 14 | width: 100%; 15 | margin: 0; 16 | font-size: 0.6875rem; 17 | line-height: 1.4; 18 | } 19 | .Terminal .no-content { 20 | opacity: 0.4; 21 | } 22 | -------------------------------------------------------------------------------- /src/Installer/Downloads.css: -------------------------------------------------------------------------------- 1 | .Downloads { 2 | } 3 | 4 | .Downloads ul { 5 | padding: 0; 6 | margin: 0; 7 | list-style: none; 8 | } 9 | 10 | .Downloads li { 11 | display: flex; 12 | align-items: center; 13 | } 14 | 15 | .Downloads li .logo { 16 | width: 55px; 17 | flex-grow: 0; 18 | flex-shrink: 0; 19 | } 20 | 21 | .Downloads .logo img { 22 | width: 48px; 23 | padding-right: 1rem; 24 | height: auto; 25 | } 26 | 27 | .Downloads .details { 28 | flex-grow: 1; 29 | text-align: left; 30 | } 31 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | process.env.PUBLIC_URL = ''; 3 | 4 | // Load environment variables from .env file. Surpress warnings using silent 5 | // if this file is missing. dotenv will never modify any environment variables 6 | // that have already been set. 7 | // https://github.com/motdotla/dotenv 8 | require('dotenv').config({silent: true}); 9 | 10 | const jest = require('jest'); 11 | const argv = process.argv.slice(2); 12 | 13 | // Watch unless on CI 14 | if (!process.env.CI) { 15 | argv.push('--watch'); 16 | } 17 | 18 | 19 | jest.run(argv); 20 | -------------------------------------------------------------------------------- /src/Installer/Welcome.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Button from '../Button'; 4 | import Step from '../Step'; 5 | 6 | export default props => 7 |
    8 |

    Welcome to Chassis Desktop

    9 |
    10 | 11 | 12 |

    Welcome to Chassis Desktop. To help you get set up, we're going to run through some installation checks and make sure everything's installed.

    13 |

    14 | 19 |

    20 |
    ; 21 | -------------------------------------------------------------------------------- /src/lib/reducers/boxes.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | export default function boxes(state = [], action) { 4 | switch ( action.type ) { 5 | case actions.ADD_BOX: 6 | return [ ...state, action.machine ]; 7 | 8 | case actions.UPDATE_BOX: 9 | return state.map(box => { 10 | if (box.path !== action.path) { 11 | return box; 12 | } 13 | 14 | return { ...box, ...action.data }; 15 | }); 16 | 17 | case actions.REMOVE_BOX: 18 | return state.filter( box => box.path !== action.machine.path ); 19 | 20 | default: 21 | return state; 22 | } 23 | }; -------------------------------------------------------------------------------- /src/about-entry.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import About from './About'; 5 | 6 | // Note: Main CSS is defined in public/loader.css to load with the splash screen. 7 | 8 | import 'font-awesome/css/font-awesome.css'; 9 | 10 | const rootEl = document.getElementById('root'); 11 | 12 | const render = Main => { 13 | ReactDOM.render( 14 |
    , 15 | rootEl 16 | ); 17 | }; 18 | 19 | render( About ); 20 | 21 | if (module.hot) { 22 | module.hot.accept('./About', () => { 23 | const NextAbout = require('./About').default 24 | render(NextAbout); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/Installer/Ready.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Button from '../Button'; 4 | import Step from '../Step'; 5 | 6 | import './Ready.css'; 7 | 8 | export default props => 9 |
    10 |

    Hello.

    11 |
    12 | 13 |

    👋

    14 | 15 |

    You're ready to go!

    16 |

    Note: Chassis Desktop is beta software. We'd really appreciate any feedback you have.

    17 |

    Made with ♥ by Bronson and Ryan.

    18 | 19 |

    20 | 25 |

    26 |
    ; 27 | -------------------------------------------------------------------------------- /src/lib/spawn-sync.js: -------------------------------------------------------------------------------- 1 | // fix-path internally uses some modules, which at the end of the day use 2 | // cross-spawn. 3 | // 4 | // Internally, this uses child_process.spawnSync if available (Node 0.11+), but 5 | // conditionally loads the spawn-sync module for compatibility. However, webpack 6 | // complains if the module isn't available. 7 | // 8 | // We fake this via a webpack alias to this module, as we know the Electron 9 | // environment always has this available. This file could actually be left 10 | // blank because it's never loaded, but just in case another module is using it: 11 | 12 | import child from 'child_process'; 13 | export default child.spawnSync; 14 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | if (typeof Promise === 'undefined') { 2 | // Rejection tracking prevents a common issue where React gets into an 3 | // inconsistent state due to an error, but it gets swallowed by a Promise, 4 | // and the user has no idea what causes React's erratic future behavior. 5 | require('promise/lib/rejection-tracking').enable(); 6 | window.Promise = require('promise/lib/es6-extensions.js'); 7 | } 8 | 9 | // fetch() polyfill for making API calls. 10 | require('whatwg-fetch'); 11 | 12 | // Object.assign() is commonly used with React. 13 | // It will use the native implementation if it's present and isn't buggy. 14 | Object.assign = require('object-assign'); 15 | -------------------------------------------------------------------------------- /src/lib/formatPath.js: -------------------------------------------------------------------------------- 1 | import { sep as separator } from 'path'; 2 | import tildify from 'tildify'; 3 | 4 | export default function formatPath(path) { 5 | const tilded = tildify(path); 6 | 7 | // Only shorten if longer than we want. 8 | if ( tilded.length < 40 ) { 9 | return tilded; 10 | } 11 | 12 | // Split into segments, and keep first and last two. 13 | // https://blog.codinghorror.com/shortening-long-file-paths/ 14 | const segments = tilded.split( separator ); 15 | if ( segments.length < 5 ) { 16 | return tilded; 17 | } 18 | 19 | return [ 20 | ...segments.slice( 0, 2 ), 21 | "\u2026", // Horizontal ellipsis 22 | ...segments.slice( -2 ) 23 | ].join( separator ); 24 | } 25 | -------------------------------------------------------------------------------- /src/lib/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import { RESET } from './actions'; 4 | import { DEFAULT_STATE } from './createStore'; 5 | import boxes from './reducers/boxes'; 6 | import installer from './reducers/installer'; 7 | import preferences from './reducers/preferences'; 8 | import terminal from './reducers/terminal'; 9 | import ui from './reducers/ui'; 10 | import vagrant from './reducers/vagrant'; 11 | 12 | const combined = combineReducers({ boxes, installer, preferences, terminal, ui, vagrant }); 13 | 14 | export default (state, action) => { 15 | if ( action.type === RESET ) { 16 | return { ...DEFAULT_STATE }; 17 | } 18 | 19 | return combined( state, action ); 20 | }; 21 | -------------------------------------------------------------------------------- /stories/MachineSettings.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import MachineSettings from '../src/MachineSettings'; 6 | 7 | const dummy_machine = { 8 | config: { 9 | hosts: [ 10 | 'vagrant.local', 11 | ], 12 | }, 13 | path: '/Users/rmccue/path/to/machine', 14 | }; 15 | 16 | const defaults = { 17 | changes: {}, 18 | machine: dummy_machine, 19 | onChange: action( 'change' ), 20 | onDelete: action( 'remove' ), 21 | onRefresh: action( 'refresh' ), 22 | }; 23 | 24 | storiesOf( 'MachineSettings', module ) 25 | .add( 'initial state', () => ( 26 | 27 | )); 28 | -------------------------------------------------------------------------------- /src/CreateModal/Type.css: -------------------------------------------------------------------------------- 1 | .Type .create-type { 2 | text-align: left; 3 | list-style: none; 4 | padding: 0; 5 | margin: 0; 6 | line-height: 1.4; 7 | } 8 | 9 | .Type .create-type a { 10 | position: relative; 11 | display: flex; 12 | align-items: center; 13 | cursor: pointer; 14 | padding: 0.5rem; 15 | margin: 0 -0.5rem; 16 | } 17 | 18 | .Type .create-type a:active { 19 | background: rgba(0, 0, 0, 0.05); 20 | } 21 | 22 | .Type .create-type a > i { 23 | font-size: 2rem; 24 | width: 2rem; 25 | text-align: center; 26 | margin-right: 1rem; 27 | color: var(--color-primary); 28 | } 29 | 30 | .Type .create-type p { 31 | margin: 0 0 0.2rem; 32 | } 33 | 34 | .Type .create-type .name { 35 | font-weight: 600; 36 | } -------------------------------------------------------------------------------- /public/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/lib/reducers/installer.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | export default function installer(state = {}, action) { 4 | switch ( action.type ) { 5 | case actions.install.SET_STATUS: 6 | return { ...state, installed: { ...state.installed, [ action.app ]: action.status } }; 7 | 8 | case actions.install.SET_DOWNLOAD_PROGRESS: 9 | let downloadProgress = state.downloadProgress || {}; 10 | downloadProgress[ action.app ] = action.progress; 11 | console.log( downloadProgress ); 12 | return { ...state, downloadProgress }; 13 | 14 | case actions.install.SET_DOWNLOAD_TOTAL: 15 | let downloadTotal = state.downloadTotal || {}; 16 | downloadTotal[ action.app ] = action.total; 17 | return { ...state, downloadTotal }; 18 | 19 | default: 20 | return state; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Icon from './Icon'; 4 | 5 | import './Header.css'; 6 | 7 | export default class Header extends React.Component { 8 | render() { 9 | const { children, icon, title } = this.props; 10 | 11 | // `icon` can be an element, an icon type, or null 12 | const iconElement = icon ? ( typeof icon === "string" ? : icon ) : null; 13 | 14 | return
    15 |
    16 | { iconElement } 17 | { title } 18 |
    19 |
    { children }
    20 |
    ; 21 | } 22 | } 23 | 24 | Header.propTypes = { 25 | icon: React.PropTypes.oneOfType([ 26 | React.PropTypes.element, 27 | React.PropTypes.string 28 | ]), 29 | title: React.PropTypes.string.isRequired, 30 | }; 31 | -------------------------------------------------------------------------------- /src/Installer.scss: -------------------------------------------------------------------------------- 1 | .Installer { 2 | text-align: center; 3 | 4 | header { 5 | padding: 0.6rem; 6 | 7 | h1 { 8 | font-size: 1.5rem; 9 | } 10 | } 11 | .steps { 12 | display: flex; 13 | font-size: 0.9rem; 14 | line-height: 1.3; 15 | list-style-type: none; 16 | 17 | > li { 18 | width: 100%; 19 | flex-shrink: 0; 20 | padding: 0.3rem; 21 | 22 | position: relative; 23 | padding-bottom: 40px; 24 | 25 | transition: 600ms; 26 | 27 | &:last-child { 28 | margin-right: none; 29 | } 30 | } 31 | 32 | h2 { 33 | font-size: 1rem; 34 | margin-bottom: 1rem; 35 | } 36 | } 37 | 38 | small { 39 | font-size: 0.8rem; 40 | } 41 | 42 | p { 43 | margin-bottom: 1rem; 44 | } 45 | 46 | .tip { 47 | font-size: 0.8rem; 48 | margin: 0.5rem auto; 49 | max-width: 70%; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import KeyHandler from './KeyHandler'; 4 | import Icon from './Icon'; 5 | import './Button.css'; 6 | 7 | export default props => { 8 | let classes = ["Button"]; 9 | if (props.light) { 10 | classes.push("light"); 11 | } 12 | if (props.noborder) { 13 | classes.push("noborder"); 14 | } 15 | if (props.tiny) { 16 | classes.push("tiny"); 17 | } 18 | 19 | let keyHandler = props.shortcut ? : null; 20 | 21 | return ; 31 | }; 32 | -------------------------------------------------------------------------------- /src/Form/Table/index.css: -------------------------------------------------------------------------------- 1 | .FormTable > * { 2 | display: flex; 3 | margin-bottom: 1rem; 4 | align-items: baseline; 5 | width: 80%; 6 | margin-left: auto; 7 | margin-right: auto; 8 | font-size: 14px; 9 | } 10 | 11 | .FormTable > .no-label > :first-child { 12 | display: block; 13 | margin-left: 35%; 14 | padding-left: 1rem; 15 | width: 65%; 16 | flex-shrink: 0; 17 | } 18 | 19 | .FormTable > *:not(.no-label) > :first-child { 20 | display: block; 21 | width: 35%; 22 | flex-shrink: 0; 23 | text-align: right; 24 | margin-right: 1rem; 25 | } 26 | 27 | .FormTable input[type="text"] { 28 | font-size: inherit; 29 | padding: 0.2em; 30 | } 31 | 32 | .FormTable > * > select, .FormTable > * > input { 33 | width: 100%; 34 | } 35 | 36 | .FormTable .description { 37 | font-size: 0.8em; 38 | opacity: 0.6; 39 | margin-top: 0.5em; 40 | margin-bottom: 0; 41 | } 42 | -------------------------------------------------------------------------------- /src/Installer/DownloadStatus.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Button from '../Button'; 4 | import Downloader from './Downloader'; 5 | import Icon from '../Icon'; 6 | 7 | export default props => { 8 | if ( props.installed ) { 9 | return
    Installed
    ; 10 | } 11 | 12 | if ( props.progress ) { 13 | if ( props.progress === props.total ) { 14 | return
    15 | 20 |
    ; 21 | } 22 | 23 | return
    24 | 29 |
    ; 30 | } 31 | 32 | return
    Not Installed
    33 | }; 34 | -------------------------------------------------------------------------------- /config/storybook/config.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { createStore } from 'redux'; 4 | import { addDecorator, configure } from '@storybook/react'; 5 | 6 | // Load CSS 7 | import 'font-awesome/css/font-awesome.css'; 8 | import '../../public/loader.css'; 9 | 10 | const require_story = require.context( '../../stories', true, /\.js$/ ) 11 | 12 | function loadStories() { 13 | require_story.keys().forEach( filename => require_story( filename ) ); 14 | } 15 | 16 | // Add dummy store for KeyHandler 17 | const INITIAL_STATE = { 18 | preferences: {}, 19 | }; 20 | addDecorator( story => ( 21 | state, INITIAL_STATE ) }> 22 | { story() } 23 | 24 | )); 25 | 26 | // Add dummy handler. 27 | window.keyHandler = { 28 | register: () => {}, 29 | unregister: () => {}, 30 | }; 31 | 32 | configure( loadStories, module ); 33 | -------------------------------------------------------------------------------- /src/lib/actions/updateGlobalStatus.js: -------------------------------------------------------------------------------- 1 | import {spawn} from 'child_process'; 2 | 3 | import { UPDATE_BOX } from '../actions'; 4 | import { parseGlobalStatus } from '../vagrant/parser'; 5 | 6 | export default function updateGlobalStatus() { 7 | return (dispatch, getStore) => { 8 | const {boxes} = getStore(); 9 | 10 | const process = spawn('vagrant', ['global-status', '--machine-readable']); 11 | let output = ''; 12 | process.stdout.on('data', data => { output += data }); 13 | process.on('close', () => { 14 | const parsed = parseGlobalStatus(output); 15 | parsed.forEach( machine => { 16 | // Find corresponding box. 17 | let box = boxes.find(box => box.path === machine.directory); 18 | if ( ! box ) { 19 | return; 20 | } 21 | 22 | dispatch({ 23 | type: UPDATE_BOX, 24 | path: box.path, 25 | data: { status: machine.state } 26 | }); 27 | }) 28 | }); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Chassis Desktop 9 | 10 | 11 |
    12 |
    13 |
    14 | 15 |

    Loading…

    16 |
    17 |
    18 |
    19 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Root.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import CSSTransitionGroup from 'react/lib/ReactCSSTransitionGroup'; 3 | 4 | import Splash from './Splash'; 5 | 6 | export default class Root extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | showingSplash: true, 12 | }; 13 | } 14 | 15 | componentDidMount() { 16 | this.setState({ showingSplash: false }); 17 | } 18 | 19 | render() { 20 | let rootComponent = props =>
    { props.children }
    ; 21 | 22 | return 28 | { this.state.showingSplash ? 29 |
    30 | 31 |
    32 | : 33 |
    34 | { this.props.children } 35 |
    36 | } 37 |
    ; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | About Chassis Desktop 9 | 10 | 11 |
    12 |
    13 |
    14 | 15 |

    Loading…

    16 |
    17 |
    18 |
    19 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /config/storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require( 'path' ); 2 | const genDefaultConfig = require('@storybook/react/dist/server/config/defaults/webpack.config.js'); 3 | 4 | module.exports = (baseConfig, env) => { 5 | const config = genDefaultConfig(baseConfig, env); 6 | 7 | // Shim in our fake electron modules. 8 | config.resolve.alias.child_process = path.resolve( __dirname, 'dummy-child_process.js' ); 9 | config.resolve.alias.electron = path.resolve( __dirname, 'dummy-electron.js' ); 10 | 11 | // Add SCSS processing. 12 | // console.log( config ); 13 | config.module.rules.push({ 14 | test: /\.scss$/, 15 | use: [ 16 | { 17 | loader: "style-loader" // creates style nodes from JS strings 18 | }, 19 | { 20 | loader: "css-loader" // translates CSS into CommonJS 21 | }, 22 | { 23 | loader: "sass-loader" // compiles Sass to CSS 24 | }, 25 | ], 26 | // loader: ExtractTextPlugin.extract('style', 'css!sass') 27 | }); 28 | 29 | return config; 30 | }; 31 | -------------------------------------------------------------------------------- /src/Steps.scss: -------------------------------------------------------------------------------- 1 | .Steps { 2 | flex-grow: 1; 3 | display: flex; 4 | font-size: 0.9rem; 5 | line-height: 1.3; 6 | list-style-type: none; 7 | width: 100%; 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | .Steps > li { 13 | width: 100%; 14 | flex-shrink: 0; 15 | padding: 0 1rem 1rem; 16 | 17 | position: relative; 18 | padding-bottom: 40px; 19 | } 20 | 21 | .Steps > li:last-child { 22 | margin-right: none; 23 | } 24 | 25 | @mixin step-transition($from, $to) { 26 | transition: transform 700ms; 27 | transform: translatex($from); 28 | 29 | &-active { 30 | transform: translatex($to); 31 | } 32 | } 33 | 34 | .Steps .step-next-enter { 35 | @include step-transition(100%, 0); 36 | } 37 | 38 | .Steps .step-next-leave { 39 | position: absolute; 40 | @include step-transition(0, -100%); 41 | } 42 | 43 | .Steps .step-back-enter { 44 | @include step-transition(-100%, 0); 45 | } 46 | 47 | .Steps .step-back-leave { 48 | position: absolute; 49 | @include step-transition(0, 100%); 50 | } 51 | -------------------------------------------------------------------------------- /src/lib/actions/install.js: -------------------------------------------------------------------------------- 1 | export const SET_STATUS = 'install.SET_STATUS'; 2 | export const SET_DOWNLOAD_PROGRESS = 'install.SET_DOWNLOAD_PROGRESS'; 3 | export const SET_DOWNLOAD_TOTAL = 'install.SET_DOWNLOAD_TOTAL'; 4 | 5 | export function setStatus( app, status ) { 6 | return { type: SET_STATUS, app, status }; 7 | } 8 | 9 | export function setDownloadTotal( app, total ) { 10 | return { type: SET_DOWNLOAD_TOTAL, app, total }; 11 | } 12 | 13 | export function addDownloadProgress( app, progress ) { 14 | return (dispatch, getStore) => { 15 | const { installer } = getStore(); 16 | const currentProgress = installer.downloadProgress[ app ] || 0; 17 | const nextProgress = currentProgress + progress; 18 | 19 | dispatch({ type: SET_DOWNLOAD_PROGRESS, app, progress: nextProgress }); 20 | }; 21 | } 22 | 23 | export function resetDownload( app ) { 24 | return dispatch => { 25 | dispatch({ type: SET_DOWNLOAD_TOTAL, app, total: 0 }); 26 | dispatch({ type: SET_DOWNLOAD_PROGRESS, app, progress: 0 }); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | 5 | import App from './App'; 6 | import Root from './Root'; 7 | 8 | // Note: Main CSS is defined in public/loader.css to load with the splash screen. 9 | 10 | import 'font-awesome/css/font-awesome.css'; 11 | 12 | // Some modules rely on global state, which needs to be configured. 13 | import configure from './lib/configure'; 14 | import createStore from './lib/createStore'; 15 | 16 | const store = createStore(); 17 | 18 | // Configure window-level handlers. 19 | configure( store ); 20 | 21 | const rootEl = document.getElementById('root'); 22 | 23 | const render = Main => { 24 | ReactDOM.render( 25 | 26 | 27 |
    28 | 29 | , 30 | rootEl 31 | ); 32 | }; 33 | 34 | render( App ); 35 | 36 | if (module.hot) { 37 | module.hot.accept('./App', () => { 38 | const NextApp = require('./App').default 39 | render(NextApp); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/reducers/ui.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | export default function ui(state = {}, action) { 4 | switch ( action.type ) { 5 | case actions.META_KEY_DOWN: 6 | return { ...state, showKeys: true }; 7 | 8 | case actions.WINDOW_BLUR: 9 | case actions.META_KEY_UP: 10 | return { ...state, showKeys: false }; 11 | 12 | case actions.SELECT_BOX: 13 | return { ...state, selectedBox: action.path, editing: false }; 14 | 15 | case actions.SHOW_MODAL: 16 | return { ...state, modal: action.id }; 17 | 18 | case actions.SET_EDITING: 19 | return { ...state, editing: action.editing }; 20 | 21 | case actions.REMOVE_BOX: 22 | return { 23 | ...state, 24 | selectedBox: null, 25 | editing: false, 26 | undo: { 27 | type: actions.REMOVE_BOX, 28 | action: { type: actions.ADD_BOX, machine: action.machine }, 29 | } 30 | }; 31 | 32 | case actions.ADD_BOX: 33 | // Clear undo. 34 | return { ...state, undo: null }; 35 | 36 | default: 37 | return state; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/actions/cloneChassis.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import runCommand from './runCommand'; 4 | 5 | const CHASSIS_REMOTE = 'https://github.com/Chassis/Chassis.git'; 6 | 7 | export default function cloneChassis( path ) { 8 | return (dispatch, getStore) => new Promise((resolve, reject) => { 9 | try { 10 | fs.mkdirSync( path ); 11 | } catch ( e ) { 12 | if ( e.code !== 'EEXIST' ) { 13 | // Actual error, reject. 14 | reject( e ); 15 | return; 16 | } 17 | 18 | // Directory exists, but might be empty. Check. 19 | const contents = fs.readdirSync( path ); 20 | if ( contents.length > 0 ) { 21 | reject( e ); 22 | return; 23 | } 24 | 25 | // Empty dir, allow. 26 | } 27 | 28 | const args = [ 29 | 'clone', 30 | CHASSIS_REMOTE, 31 | path, 32 | 33 | // Options 34 | '--recurse-submodules', 35 | ]; 36 | dispatch( runCommand( path, 'git', args ) ).then(code => { 37 | if ( code === 0 ) { 38 | resolve(); 39 | } else { 40 | reject(); 41 | } 42 | }); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /src/Header.css: -------------------------------------------------------------------------------- 1 | .Header { 2 | flex-shrink: 0; 3 | background: var(--color-primary); 4 | padding: 2rem 12px 1rem; 5 | //padding: 1rem 12px 1rem; 6 | display: flex; 7 | align-items: center; 8 | justify-content: space-between; 9 | -webkit-app-region: drag; 10 | // border-top: 21px solid rgb(193, 230, 246); 11 | border-bottom: 1px solid #fff; 12 | } 13 | 14 | .Header .title { 15 | font-size: 1.25rem; 16 | min-height: 32px; 17 | color: #fff; 18 | display: flex; 19 | align-items: center; 20 | } 21 | 22 | .Header > .title > img { 23 | height: 32px; 24 | vertical-align: middle; 25 | } 26 | .Header > .title > img, .Header > .title > .fa { 27 | margin-right: 0.5rem; 28 | } 29 | 30 | .Header .actions { 31 | -webkit-app-region: no-drag; 32 | } 33 | 34 | .Header .actions .Button { 35 | color: #fff; 36 | } 37 | 38 | .Header .actions button:hover { 39 | background: rgba(255, 255, 255, 0.2); 40 | } 41 | 42 | .Header .actions button:focus { 43 | outline: none; 44 | border: 1px solid #fff; 45 | } 46 | 47 | .Header .actions i { 48 | margin-right: 0.5rem; 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/reducers/terminal.js: -------------------------------------------------------------------------------- 1 | export default function terminal(state = {}, action) { 2 | let next = (key, modifier) => { 3 | return { ...state, [key]: modifier( state[ key ] ) }; 4 | }; 5 | 6 | switch (action.type) { 7 | case 'COMMAND_START': 8 | return next( action.machine, value => { 9 | let nextValue = value ? { ...value } : { output: "" }; 10 | nextValue.command = action.command; 11 | nextValue.args = action.args; 12 | nextValue.output += `$ ${action.command} ${action.args.join(' ')}\n`; 13 | nextValue.running = true; 14 | return nextValue; 15 | }); 16 | 17 | case 'COMMAND_OUTPUT': 18 | return next( action.machine, value => ({ ...value, output: value.output + action.data }) ); 19 | 20 | case 'COMMAND_END': 21 | const code = action.code; 22 | const message = code === 0 ? `(Exited with success code ${code})\n` : `(Exited with error code ${code})\n\n`; 23 | return next( action.machine, value => ({ ...value, output: value.output + message, running: false }) ); 24 | 25 | default: 26 | return state; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/lib/actions/updateConfig.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import yaml from 'js-yaml'; 4 | 5 | import { loadFile } from './loadConfig'; 6 | import { updateBox } from '../actions'; 7 | 8 | const yamlOptions = { 9 | indent: 4, 10 | noRefs: true, 11 | }; 12 | 13 | export function saveConfig( path, data ) { 14 | const serialized = yaml.safeDump( data, yamlOptions ); 15 | 16 | return new Promise( resolve => { 17 | fs.writeFile( path, serialized, err => { 18 | console.log( err ); 19 | resolve(); 20 | }); 21 | }); 22 | } 23 | 24 | export default function updateConfig( path, machinePath, changes ) { 25 | return (dispatch, getStore) => { 26 | loadFile( path ).then( existing => { 27 | const nextConfig = { ...existing, ...changes }; 28 | saveConfig( path, nextConfig ).then( () => dispatch( updateBox( machinePath, { config: nextConfig } ) ) ); 29 | }); 30 | }; 31 | } 32 | 33 | export function updateMachineConfig( machinePath, changes ) { 34 | return updateConfig( path.join( machinePath, 'config.local.yaml' ), machinePath, changes ); 35 | } 36 | -------------------------------------------------------------------------------- /src/Installer.css: -------------------------------------------------------------------------------- 1 | .Installer { 2 | text-align: center; 3 | background: var(--color-primary); 4 | color: #fff; 5 | display: flex; 6 | flex-direction: column; 7 | overflow-x: hidden; 8 | height: 100%; 9 | } 10 | 11 | .Installer header { 12 | padding: 0.6rem; 13 | } 14 | 15 | .Installer h1 { 16 | font-size: 1.5rem; 17 | } 18 | 19 | .Installer .Steps { 20 | line-height: 1.5; 21 | } 22 | 23 | .Installer .steps h2 { 24 | font-size: 1rem; 25 | margin-bottom: 1rem; 26 | } 27 | 28 | .Installer small { 29 | font-size: 0.8rem; 30 | } 31 | 32 | .Installer p { 33 | margin-bottom: 1rem; 34 | } 35 | 36 | .Installer .tip { 37 | font-size: 0.8rem; 38 | margin: 0.5rem auto; 39 | max-width: 70%; 40 | } 41 | 42 | .Installer .toolbar { 43 | display: flex; 44 | justify-content: space-between; 45 | align-items: center; 46 | padding: 1rem; 47 | } 48 | 49 | .Installer .toolbar ul { 50 | display: flex; 51 | align-items: center; 52 | list-style-type: none; 53 | padding: 0; 54 | margin: 0; 55 | } 56 | 57 | .Installer .toolbar li { 58 | margin-right: 1rem; 59 | line-height: 1.7; 60 | } 61 | -------------------------------------------------------------------------------- /src/Terminal.js: -------------------------------------------------------------------------------- 1 | import ansiHTML from 'ansi-html'; 2 | import React from 'react'; 3 | 4 | import './Terminal.scss'; 5 | 6 | export default class Terminal extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | // Store element ref. 11 | this.terminal = null; 12 | } 13 | 14 | componentDidUpdate() { 15 | if (this.terminal) { 16 | this.terminal.scrollTop = this.terminal.scrollHeight; 17 | } 18 | } 19 | 20 | render() { 21 | const { expanded, output } = this.props; 22 | 23 | let formattedOutput; 24 | let last; 25 | if ( output ) { 26 | formattedOutput = ansiHTML(output); 27 | last = ansiHTML(output.trim('\n').split('\n').slice(-1)[0]); 28 | } else { 29 | // Placeholder content for terminal. 30 | formattedOutput = last = 'No output'; 31 | } 32 | 33 | return
    this.terminal = ref } 36 | > 37 |
    40 | 		
    ; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Steps.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CSSTransitionGroup from 'react/lib/ReactCSSTransitionGroup'; 3 | 4 | import './Steps.scss'; 5 | 6 | const transitionProps = { 7 | className: "Steps", 8 | component: "ul", 9 | transitionEnterTimeout: 700, 10 | transitionLeaveTimeout: 700, 11 | }; 12 | 13 | export default class Steps extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | 17 | this.state = { 18 | direction: "next", 19 | }; 20 | } 21 | 22 | componentWillReceiveProps(nextProps) { 23 | if ( nextProps.step > this.props.step ) { 24 | this.setState({ direction: "next" }); 25 | } else if ( nextProps.step < this.props.step ) { 26 | this.setState({ direction: "back" }); 27 | } 28 | } 29 | 30 | render() { 31 | let current = this.props.children.filter(item => !!item).slice( this.props.step, this.props.step + 1 ); 32 | 33 | const transitionName = this.state.direction === "next" ? "step-next" : "step-back"; 34 | 35 | return 36 | { current } 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Installer/ImportBoxes.css: -------------------------------------------------------------------------------- 1 | .ImportBoxes .box-list { 2 | text-align: left; 3 | list-style: none; 4 | padding-left: 0; 5 | padding-right: 0; 6 | margin-left: 0; 7 | margin-right: 0; 8 | 9 | border: 1px solid rgba(255, 255, 255, 0.6); 10 | border-radius: 3px; 11 | overflow-y: scroll; 12 | height: 20rem; 13 | 14 | background: #fff; 15 | // color: #29ABE2; 16 | color: #333; 17 | } 18 | 19 | .ImportBoxes .box-list label { 20 | display: block; 21 | padding: 0.5rem; 22 | cursor: pointer; 23 | } 24 | 25 | .ImportBoxes .box-list li:nth-child(2n) { 26 | background: rgba( 27 | var(--color-primary-red), 28 | var(--color-primary-green), 29 | var(--color-primary-blue), 30 | 0.1 31 | ); 32 | } 33 | 34 | .ImportBoxes .box-list label:hover { 35 | background: rgba(255, 255, 255, 0.2); 36 | background: rgba( 37 | var(--color-primary-red), 38 | var(--color-primary-green), 39 | var(--color-primary-blue), 40 | 0.2 41 | ); 42 | } 43 | 44 | .ImportBoxes input[type=checkbox] { 45 | margin-right: 0.5rem; 46 | font-size: 1rem; 47 | } 48 | 49 | .ImportBoxes .button-hole { 50 | font-size: 1rem; 51 | display: flex; 52 | justify-content: center; 53 | align-items: center; 54 | } 55 | -------------------------------------------------------------------------------- /src/Button.css: -------------------------------------------------------------------------------- 1 | .Button { 2 | background: transparent; 3 | position: relative; 4 | cursor: pointer; 5 | display: inline-block; 6 | border: 1px solid transparent; 7 | font-size: 1rem; 8 | padding: 0.3rem 0.4rem; 9 | border-radius: 4px; 10 | margin-left: 0.5rem; 11 | 12 | color: var(--color-primary); 13 | border-color: rgba( 14 | var(--color-primary-red), 15 | var(--color-primary-green), 16 | var(--color-primary-blue), 17 | 0.4 18 | ); 19 | } 20 | 21 | .Button:first-child { 22 | margin-left: 0; 23 | } 24 | 25 | .Button.light { 26 | color: #fff; 27 | border-color: rgba(255, 255, 255, 0.4); 28 | } 29 | 30 | .Button.noborder { 31 | border-color: transparent;; 32 | } 33 | 34 | .Button.tiny { 35 | font-size: 0.70rem; 36 | padding: 0.16em 0.25em; 37 | vertical-align: top; 38 | } 39 | 40 | .Button:hover { 41 | background: rgba( 42 | var(--color-primary-red), 43 | var(--color-primary-green), 44 | var(--color-primary-blue), 45 | 0.1 46 | ); 47 | } 48 | 49 | .Button.light:hover { 50 | background: rgba(255, 255, 255, 0.2); 51 | } 52 | 53 | .Button:focus { 54 | outline: none; 55 | border-color: currentColor; 56 | } 57 | 58 | .Button i { 59 | margin-right: 0.5em; 60 | } 61 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 2 | // injected into the application via DefinePlugin in Webpack configuration. 3 | 4 | var REACT_APP = /^REACT_APP_/i; 5 | 6 | function getClientEnvironment(publicUrl) { 7 | return Object 8 | .keys(process.env) 9 | .filter(key => REACT_APP.test(key)) 10 | .reduce((env, key) => { 11 | env['process.env.' + key] = JSON.stringify(process.env[key]); 12 | return env; 13 | }, { 14 | // Useful for determining whether we’re running in production mode. 15 | // Most importantly, it switches React into the correct mode. 16 | 'process.env.NODE_ENV': JSON.stringify( 17 | process.env.NODE_ENV || 'development' 18 | ), 19 | // Useful for resolving the correct path to static assets in `public`. 20 | // For example, . 21 | // This should only be used as an escape hatch. Normally you would put 22 | // images into the `src` and `import` them in code to get their paths. 23 | 'process.env.PUBLIC_URL': JSON.stringify(publicUrl) 24 | }); 25 | } 26 | 27 | module.exports = getClientEnvironment; 28 | -------------------------------------------------------------------------------- /src/lib/actions/updateBoxStatus.js: -------------------------------------------------------------------------------- 1 | import {spawn} from 'child_process'; 2 | 3 | import { UPDATE_BOX } from '../actions'; 4 | import parser from '../vagrant/parser'; 5 | 6 | export default function updateBoxStatus(path) { 7 | return (dispatch, getStore) => { 8 | const {boxes} = getStore(); 9 | let machine = boxes.find(box => box.path === path); 10 | if (!machine) { 11 | return; 12 | } 13 | 14 | dispatch({ type: UPDATE_BOX, path, data: { status: 'loading' } }); 15 | 16 | const process = spawn('vagrant', ['status', '--machine-readable'], { 17 | cwd: machine.path, 18 | }); 19 | let output = ''; 20 | let error = ''; 21 | process.stdout.on('data', data => { output += data }); 22 | process.stderr.on('data', data => { error += data }); 23 | process.on('close', code => { 24 | if ( code !== 0 ) { 25 | console.log( error ); 26 | dispatch({ type: UPDATE_BOX, path, data: { status: 'not_created' }}); 27 | return; 28 | } 29 | 30 | const parsed = parser(output); 31 | console.log( parsed ); 32 | const stateItem = parsed.find(item => item.type === 'state' ); 33 | const state = stateItem.data[0]; 34 | 35 | dispatch({ type: UPDATE_BOX, path, data: { status: state } }); 36 | }); 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /stories/Terminal.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import React from 'react'; 3 | import { storiesOf } from '@storybook/react'; 4 | import { action } from '@storybook/addon-actions'; 5 | 6 | import Terminal from '../src/Terminal'; 7 | 8 | const chalker = new chalk.constructor({ 9 | enabled: true, 10 | }); 11 | 12 | const dummy_output = [ 13 | '$ echo This is an example.', 14 | 'This is an example', 15 | ].join( '\n' ); 16 | 17 | const all_colors = Object.keys( chalk.styles ).map( color => chalker[ color ]( color ) ); 18 | const dummy_colored_output = [ 19 | '$ ./output_colors', 20 | `Here is ${chalker.red('some red text')}, followed by ${chalker.blue('blue text')}`, 21 | ...all_colors, 22 | ].join( '\n' ); 23 | 24 | 25 | storiesOf( 'Terminal', module ) 26 | .add( 'default', () => ( 27 | 28 | )) 29 | .add( 'collapsed', () => ( 30 | 31 | )) 32 | .add( 'with output', () => ( 33 | 37 | )) 38 | .add( 'collapsed with output', () => ( 39 | 42 | )) 43 | .add( 'colored output', () => ( 44 | 48 | )); 49 | -------------------------------------------------------------------------------- /stories/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import Button from '../src/Button'; 6 | import Header from '../src/Header'; 7 | import Icon from '../src/Icon'; 8 | 9 | storiesOf( 'Header', module ) 10 | .addDecorator( story => ( 11 |
    17 | { story() } 18 |
    19 | )) 20 | .add( 'with title', () => ( 21 |
    24 | )) 25 | .add( 'with custom title component', () => ( 26 |
    Title inside a Component } 28 | /> 29 | )) 30 | .add( 'with icon (as string)', () => ( 31 |
    35 | )) 36 | .add( 'with icon (as component)', () => ( 37 |
    } 39 | title="Title" 40 | /> 41 | )) 42 | .add( 'with children', () => ( 43 |
    44 | 45 |
    46 | )) 47 | .add( 'with everything', () => ( 48 |
    52 | 53 |
    54 | )); 55 | -------------------------------------------------------------------------------- /src/lib/actions/runCommand.js: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | 3 | window.running = {}; 4 | 5 | const vagrantEnv = { 6 | CLICOLOR_FORCE: 'yes', 7 | GIT_COLOR: 'yes', 8 | 9 | // Only supported in >1.8.3, --color is used for older versions. 10 | VAGRANT_FORCE_COLOR: 'yes', 11 | }; 12 | 13 | export default function runCommand(path, command, args = [], opts = {}) { 14 | return (dispatch, getStore) => new Promise((resolve, reject) => { 15 | if ( path in window.running ) { 16 | return; 17 | } 18 | 19 | let spawnOpts = Object.assign({}, { 20 | cwd: path, 21 | env: { 22 | ...process.env, 23 | ...vagrantEnv, 24 | }, 25 | }, opts); 26 | 27 | const proc = spawn( command, args, spawnOpts ); 28 | window.running[ path ] = proc; 29 | 30 | dispatch({ type: 'COMMAND_START', command, args, machine: path }); 31 | 32 | proc.stdout.on('data', data => { 33 | dispatch({ type: 'COMMAND_OUTPUT', data, machine: path, stream: 'stdout' }); 34 | }); 35 | proc.stderr.on('data', data => { 36 | dispatch({ type: 'COMMAND_OUTPUT', data, machine: path, stream: 'stderr' }); 37 | }); 38 | proc.on('close', code => { 39 | dispatch({ type: 'COMMAND_END', code, machine: path }); 40 | delete window.running[ path ]; 41 | resolve( code ); 42 | }); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /stories/MachineItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import MachineItem from '../src/MachineItem'; 6 | 7 | const dummy_machine = { 8 | config: { 9 | hosts: [ 10 | 'vagrant.local', 11 | ], 12 | }, 13 | path: '/Users/rmccue/path/to/machine', 14 | }; 15 | 16 | const defaults = { 17 | editing: false, 18 | // changes: {}, 19 | isNext: false, 20 | isPrevious: false, 21 | machine: dummy_machine, 22 | selected: true, 23 | terminal: false, 24 | // status: 'not_created', 25 | onStartEditing: action( 'start-editing' ), 26 | onFinishEditing: action( 'finish-editing' ), 27 | onSave: action( 'save' ), 28 | onSelect: action( 'select' ), 29 | onDeselect: action( 'deselect' ), 30 | onDelete: action( '_delete' ), 31 | onRefresh: action( 'refresh' ), 32 | onRun: action( 'run' ), 33 | }; 34 | 35 | storiesOf( 'MachineItem', module ) 36 | .addDecorator( story => ( 37 |
    38 | { story() } 39 |
    40 | )) 41 | .add( 'unselected', () => ( 42 | 46 | )) 47 | .add( 'selected', () => ( 48 | 49 | )); 50 | -------------------------------------------------------------------------------- /src/ItemList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Button from './Button'; 4 | 5 | import './ItemList.css'; 6 | 7 | const ItemList = props => { 8 | let onAdd = () => { 9 | let next = props.value.slice(); 10 | next.push(''); 11 | props.onChange(next); 12 | }; 13 | let onChange = (index, value) => { 14 | let next = props.value.slice(); 15 | next[index] = value; 16 | props.onChange(next); 17 | }; 18 | let onRemove = index => { 19 | let next = props.value.slice(); 20 | next.splice(index, 1); 21 | props.onChange(next); 22 | }; 23 | 24 | return
    25 | {props.value.map((item, index) => 26 |
    27 | onChange( index, e.target.value ) } 32 | /> 33 | { props.value.length > 1 ? 34 | 38 | : null } 39 |
    40 | )} 41 | 45 |
    ; 46 | }; 47 | 48 | ItemList.propTypes = { 49 | value: React.PropTypes.array.isRequired, 50 | onChange: React.PropTypes.func.isRequired, 51 | }; 52 | 53 | export default ItemList; 54 | -------------------------------------------------------------------------------- /src/lib/configure.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Setup for global handlers. 3 | */ 4 | import ansiHTML from 'ansi-html'; 5 | import fixPath from 'fix-path'; 6 | import which from 'which'; 7 | 8 | import * as actions from './actions'; 9 | import { loadAllConfig } from './actions/loadConfig'; 10 | import Keys from './keys'; 11 | 12 | // Refresh every 10 seconds. 13 | const REFRESH_INTERVAL = 10000; 14 | 15 | export default store => { 16 | const state = store.getState(); 17 | 18 | ansiHTML.setColors({ 19 | 'reset': ['fff', 'transparent'], 20 | 'black': 'transparent', 21 | }); 22 | 23 | window.keyHandler = new Keys(); 24 | window.keyHandler.listen( store ); 25 | 26 | // Fix the process.env path, which isn't inherited by the shell on macOS. 27 | fixPath(); 28 | 29 | if ( ! state.installer.installed.chassis ) { 30 | // Search for installed applications. 31 | which( 'vagrant', err => { 32 | store.dispatch( actions.install.setStatus( 'vagrant', !err ) ); 33 | }); 34 | which( 'VirtualBox', err => { 35 | store.dispatch( actions.install.setStatus( 'virtualbox', !err ) ); 36 | }); 37 | } else { 38 | // Refresh machine state constantly. 39 | store.dispatch(actions.updateGlobalStatus()); 40 | window.setInterval(() => store.dispatch(actions.updateGlobalStatus()), REFRESH_INTERVAL); 41 | 42 | // Refresh configuration. 43 | store.dispatch(loadAllConfig()); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/Installer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import './Installer.css'; 5 | 6 | import { setStatus } from './lib/actions/install'; 7 | import Downloads from './Installer/Downloads'; 8 | import ImportBoxes from './Installer/ImportBoxes'; 9 | import Ready from './Installer/Ready'; 10 | import Steps from './Steps'; 11 | import Welcome from './Installer/Welcome'; 12 | 13 | class Installer extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | 17 | this.state = { 18 | step: 0, 19 | }; 20 | } 21 | 22 | onNext() { 23 | this.setState( state => ({ step: state.step + 1 }) ); 24 | } 25 | 26 | onFinish() { 27 | this.props.dispatch( setStatus( 'chassis', true ) ); 28 | } 29 | 30 | render() { 31 | const { vagrant } = this.props; 32 | 33 | return
    34 | 35 | this.onNext()} /> 36 | this.onNext()} /> 37 | 38 | { vagrant.machines.length > 0 ? 39 | this.onNext()} 43 | /> 44 | : null } 45 | 46 | this.onFinish()}/> 47 | 48 |
    ; 49 | } 50 | } 51 | 52 | export default connect(state => state)(Installer); 53 | -------------------------------------------------------------------------------- /src/Installer/Downloader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Button from '../Button'; 4 | 5 | import './Downloader.css'; 6 | 7 | function formatProgress(current, total) { 8 | let percentage = ( current / total ) * 100; 9 | return percentage.toFixed( 1 ); 10 | } 11 | 12 | /*const STATES = [ 13 | 'waiting', 14 | 'starting', 15 | 'downloading', 16 | 'completed', 17 | ];*/ 18 | 19 | export default class Downloader extends React.Component { 20 | render() { 21 | const { status, current, total } = this.props; 22 | 23 | switch ( status ) { 24 | case 'start': 25 | return
    26 |

    Starting download...

    27 |
    ; 28 | 29 | case 'downloading': 30 | return
    31 | 32 |

    Downloading, { formatProgress( current, total ) }% complete.

    33 |
    ; 34 | 35 | case 'waiting': 36 | return
    37 | 42 |
    ; 43 | 44 | default: 45 | return
    46 |

    { status }

    47 |
    ; 48 | } 49 | } 50 | } 51 | 52 | Downloader.propTypes = { 53 | status: React.PropTypes.string.isRequired, 54 | current: React.PropTypes.number, 55 | total: React.PropTypes.number, 56 | onDownload: React.PropTypes.func.isRequired, 57 | }; 58 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | Copyright (c) 2016 Ryan McCue, Bronson Quick, and contributors. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 15 | -------------------------------------------------------------------------------- /stories/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import Button from '../src/Button'; 6 | 7 | const lightStyle = { 8 | padding: 10, 9 | } 10 | const darkStyle = { 11 | // width: '100%', 12 | background: '#66f', 13 | color: 'red', 14 | padding: 10, 15 | }; 16 | 17 | storiesOf( 'Button', module ) 18 | .addDecorator( story => ( 19 |
    { story() }
    23 | )) 24 | .add( 'default', () => ( 25 |
    26 |

    27 | 28 |

    29 |

    30 | 31 |

    32 |
    33 | )) 34 | .add( 'noborder', () => ( 35 |
    36 |

    37 | 38 |

    39 |

    40 | 41 |

    42 |
    43 | )) 44 | .add( 'tiny', () => ( 45 |
    46 |

    47 | 48 |

    49 |

    50 | 51 |

    52 |
    53 | )) 54 | .add( 'icon', () => ( 55 |
    56 |

    57 | 58 |

    59 |

    60 | 61 |

    62 |
    63 | )); 64 | -------------------------------------------------------------------------------- /src/About.css: -------------------------------------------------------------------------------- 1 | html { 2 | /* Ensure the vibrancy shines through. */ 3 | background: transparent; 4 | } 5 | .About { 6 | display: flex; 7 | flex-direction: column; 8 | width: 100%; 9 | height: 100%; 10 | /*background: #ececec;*/ 11 | 12 | font-size: 14px; 13 | } 14 | .About .description { 15 | flex-grow: 1; 16 | display: flex; 17 | flex-direction: row; 18 | align-items: center; 19 | background: #29ABE2; 20 | color: #efefef; 21 | padding: 0 2rem; 22 | font-size: 0.785rem; 23 | } 24 | .About .description a { 25 | color: #fff; 26 | text-decoration: none; 27 | border-bottom: 1px solid rgba(255, 255, 255, 0.4); 28 | } 29 | .About .description p { 30 | margin-top: 0; 31 | margin-bottom: 0.75em; 32 | } 33 | .About .update .fa-spinner { 34 | animation: Splash-loader infinite 2s linear; 35 | } 36 | .About .logo { 37 | width: 200px; 38 | padding: 1rem; 39 | } 40 | .About .logo img { 41 | max-height: 100%; 42 | max-width: 100%; 43 | } 44 | .About footer { 45 | flex-grow: 0; 46 | flex-shrink: 0; 47 | height: 66px; 48 | 49 | display: flex; 50 | flex-direction: column; 51 | align-items: center; 52 | } 53 | .About footer nav { 54 | flex-grow: 1; 55 | flex-shrink: 0; 56 | align-self: stretch; 57 | } 58 | .About footer nav ul { 59 | display: flex; 60 | justify-content: space-around; 61 | list-style: none; 62 | padding: 0.5rem 1rem; 63 | margin: 0; 64 | } 65 | .About footer nav li { 66 | padding: 0; 67 | margin: 0; 68 | } 69 | .About footer a { 70 | color: #29ABE2; 71 | text-decoration: none; 72 | } 73 | .About footer .for-you { 74 | font-size: 0.7rem; 75 | margin: 0; 76 | padding: 0.6em; 77 | /* color: #aaa; */ 78 | opacity: 0.4; 79 | } 80 | -------------------------------------------------------------------------------- /src/lib/vagrant/parser.js: -------------------------------------------------------------------------------- 1 | const COMMA = /%!\(VAGRANT_COMMA\)/g; 2 | const NEWLINE = /\\n/g; 3 | const RETURN = /\\r/g; 4 | 5 | export default function parser(text) { 6 | const lines = text.trim('\n').split('\n'); 7 | const items = lines.map(line => line.split(',')).map(pieces => { 8 | if (pieces.length < 3) { 9 | throw new Error('Invalid machine-readable, minimum number of pieces is 3'); 10 | } 11 | 12 | const [timestamp, target, type, ...data] = pieces; 13 | 14 | return { 15 | timestamp, 16 | target, 17 | type, 18 | data: data.map(item => item.replace(COMMA, ',').replace(NEWLINE, '\n').replace(RETURN, '\r')) 19 | }; 20 | }); 21 | return items; 22 | } 23 | 24 | export function parseGlobalStatus(text) { 25 | const parsed = parser(text); 26 | const parts = parsed.map(item => item.data[1]); 27 | 28 | // Custom parse. 29 | let header = true; 30 | let columns = []; 31 | let rows = []; 32 | let currentRow = {}; 33 | let currentColumn = 0; 34 | for (var i = 0; i < parts.length; i++) { 35 | let part = parts[i].trim(); 36 | if ( header ) { 37 | if ( part.match( /^-+$/ ) ) { 38 | header = false; 39 | continue; 40 | } 41 | 42 | if ( part !== "" ) { 43 | columns.push( part ); 44 | } 45 | continue; 46 | } 47 | 48 | // End of a row? 49 | if ( part === "" ) { 50 | rows.push( currentRow ); 51 | currentRow = {}; 52 | currentColumn = 0; 53 | continue; 54 | } 55 | 56 | let column = columns[ currentColumn ]; 57 | currentRow[ column ] = part; 58 | currentColumn++; 59 | } 60 | 61 | // Note: intentionally ignores the last "row", as it's useless end-user 62 | // information. 63 | 64 | return rows; 65 | } 66 | -------------------------------------------------------------------------------- /stories/MachineActions.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { action } from '@storybook/addon-actions'; 4 | 5 | import MachineActions from '../src/MachineActions'; 6 | 7 | const dummy_machine = { 8 | config: { 9 | hosts: [ 10 | 'vagrant.local', 11 | ], 12 | }, 13 | path: '/Users/rmccue/path/to/machine', 14 | }; 15 | 16 | const defaults = { 17 | changes: {}, 18 | machine: dummy_machine, 19 | status: 'not_created', 20 | onLaunch: action( 'launch' ), 21 | onHalt: action( 'halt' ), 22 | onEdit: action( 'edit' ), 23 | onFinder: action( 'finder' ), 24 | onTerminal: action( 'terminal' ), 25 | onRefresh: action( 'refresh' ), 26 | }; 27 | 28 | storiesOf( 'MachineActions', module ) 29 | .addDecorator( story => ( 30 |
    31 | { story() } 32 |
    33 | )) 34 | .add( 'initial state', () => ( 35 | 36 | )) 37 | .add( 'off', () => ( 38 | 42 | )) 43 | .add( 'launching', () => ( 44 | 48 | )) 49 | .add( 'loading', () => ( 50 | 54 | )) 55 | .add( 'running', () => ( 56 | 60 | )) 61 | .add( 'halting', () => ( 62 | 66 | )) 67 | .add( 'unknown', () => ( 68 | 72 | )); 73 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | 4 | // Make sure any symlinks in the project folder are resolved: 5 | // https://github.com/facebookincubator/create-react-app/issues/637 6 | var appDirectory = fs.realpathSync(process.cwd()); 7 | function resolveApp(relativePath) { 8 | return path.resolve(appDirectory, relativePath); 9 | } 10 | 11 | // We support resolving modules according to `NODE_PATH`. 12 | // This lets you use absolute paths in imports inside large monorepos: 13 | // https://github.com/facebookincubator/create-react-app/issues/253. 14 | 15 | // It works similar to `NODE_PATH` in Node itself: 16 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 17 | 18 | // We will export `nodePaths` as an array of absolute paths. 19 | // It will then be used by Webpack configs. 20 | // Jest doesn’t need this because it already handles `NODE_PATH` out of the box. 21 | 22 | var nodePaths = (process.env.NODE_PATH || '') 23 | .split(process.platform === 'win32' ? ';' : ':') 24 | .filter(Boolean) 25 | .map(resolveApp); 26 | 27 | // config after eject: we're in ./config/ 28 | module.exports = { 29 | appBuild: resolveApp('build'), 30 | appPublic: resolveApp('public'), 31 | appHtml: resolveApp('public/index.html'), 32 | appIndexJs: resolveApp('src/index.js'), 33 | appAboutHtml: resolveApp('public/about.html'), 34 | appAboutJs: resolveApp('src/about-entry.js'), 35 | appPackageJson: resolveApp('package.json'), 36 | appSrc: resolveApp('src'), 37 | testsSetup: resolveApp('src/setupTests.js'), 38 | appNodeModules: resolveApp('node_modules'), 39 | ownNodeModules: resolveApp('node_modules'), 40 | nodePaths: nodePaths 41 | }; 42 | -------------------------------------------------------------------------------- /src/CreateModal/Config.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import tildify from 'tildify'; 3 | 4 | import Button from '../Button'; 5 | import FormTable from '../Form/Table'; 6 | import FixedValue from '../Form/FixedValue'; 7 | import Step from '../Step'; 8 | import { TYPES } from './Type'; 9 | 10 | export default props => { 11 | let message, buttonText, fields = []; 12 | 13 | let chassisDirectory = props.path; 14 | switch (props.type) { 15 | case TYPES.CREATE: 16 | message =

    You are about to create a new Chassis install.

    ; 17 | buttonText = 'Create'; 18 | break; 19 | 20 | case TYPES.RETROFIT: 21 | message =

    You are adding Chassis to an existing WordPress installation.

    ; 22 | fields.push( 23 |
    24 |
    Project Directory:
    25 | 26 |
    27 | ); 28 | chassisDirectory += '/chassis'; 29 | buttonText = 'Create'; 30 | break; 31 | 32 | case TYPES.IMPORT: 33 | message =

    You are adding an existing Chassis box to the list.

    ; 34 | buttonText = 'Add'; 35 | break; 36 | 37 | default: 38 | return

    Unknown type!

    ; 39 | } 40 | 41 | return 42 |

    Ready?

    43 | { message } 44 | 45 | 46 | { fields } 47 | 55 |
    56 |
    Chassis Directory:
    57 | 60 |
    61 |
    62 | 63 | 64 |
    ; 65 | }; 66 | -------------------------------------------------------------------------------- /src/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import packageData from '../package.json'; 4 | 5 | import './About.css'; 6 | 7 | export default class About extends React.Component { 8 | render() { 9 | return
    10 |
    11 |
    12 | 13 |
    14 |
    15 |

    Chassis Desktop

    16 |

    17 | { packageData.version } 18 | {' '} 19 | View releases 20 |

    21 |

    22 | Chassis is produced by {' '} 23 | Bronson Quick, {' '} 24 | Ryan McCue, 25 | and contributors. 26 | 🇦🇺 27 |

    28 |

    29 | Supported by Human Made, 30 | an enterprise WordPress agency. 31 | 😘 32 |

    33 |

    34 | Logo by Sonja Leix. 35 | 🥕 36 |

    37 |
    38 |
    39 | 58 |
    59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/lib/createStore.js: -------------------------------------------------------------------------------- 1 | import deepmerge from 'deepmerge'; 2 | import { applyMiddleware, createStore as createReduxStore } from 'redux'; 3 | import thunk from 'redux-thunk'; 4 | 5 | import reducers from './reducers'; 6 | 7 | export const DEFAULT_STATE = { 8 | boxes: [], 9 | installer: { 10 | installed: { 11 | chassis: false, 12 | vagrant: false, 13 | virtualbox: false, 14 | }, 15 | downloadProgress: {}, 16 | downloadTotal: {}, 17 | }, 18 | terminal: {}, 19 | vagrant: { 20 | machines: [], 21 | }, 22 | ui: { 23 | editing: false, 24 | modal: 'install', 25 | undo: null, 26 | }, 27 | preferences: { 28 | showShortcuts: true, 29 | } 30 | }; 31 | 32 | export default function createStore() { 33 | let initialState = { ...DEFAULT_STATE }; 34 | let storedState = localStorage.getItem( 'store' ); 35 | if ( storedState ) { 36 | try { 37 | initialState = deepmerge( initialState, JSON.parse( storedState ) ); 38 | } catch (e) { 39 | // No-op 40 | } 41 | } 42 | 43 | const middleware = [ thunk ]; 44 | 45 | // Debugging utilities. 46 | if (process.env.NODE_ENV === 'development') { 47 | const createLogger = require( 'redux-logger' ); 48 | const logger = createLogger(); 49 | middleware.push( logger ); 50 | } 51 | 52 | let store = createReduxStore( reducers, initialState, applyMiddleware( ...middleware ) ); 53 | store.subscribe(() => { 54 | let mapper = store => ({ boxes: store.boxes, installer: { installed: store.installer.installed } }); 55 | localStorage.setItem( 'store', JSON.stringify( mapper( store.getState() ) ) ); 56 | }); 57 | 58 | if (module.hot) { 59 | module.hot.accept('./reducers', () => { 60 | const nextReducers = require('./reducers').default; 61 | store.replaceReducer(nextReducers); 62 | }); 63 | } 64 | 65 | return store; 66 | } 67 | -------------------------------------------------------------------------------- /src/KeyHandler.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import './KeyHandler.css'; 5 | 6 | class KeyHandler extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.handler = null 10 | } 11 | 12 | componentDidMount() { 13 | // Register key handler. 14 | this.handler = window.keyHandler.register( this.props.shortcut, e => { 15 | e.preventDefault(); 16 | this.props.onTrigger(); 17 | }); 18 | } 19 | 20 | componentWillReceiveProps(nextProps) { 21 | if (this.props.shortcut !== nextProps.shortcut) { 22 | // Unregister and re-register. 23 | window.keyHandler.unregister( this.props.shortcut, this.handler ); 24 | } 25 | } 26 | 27 | componentWillUnmount() { 28 | // Unregister. 29 | window.keyHandler.unregister( this.props.shortcut, this.handler ); 30 | } 31 | 32 | render() { 33 | if ( ! this.props.enabled ) { 34 | return null; 35 | } 36 | 37 | let shortcut = this.props.shortcut; 38 | let keyText = shortcut.toLowerCase() 39 | .replace('ctrl', '^') 40 | .replace('cmd', '\u2318') 41 | .replace('shift', '\u21E7') 42 | .replace('left', '\u2190') 43 | .replace('up', '\u2191') 44 | .replace('right', '\u2192') 45 | .replace('down', '\u2193') 46 | .toUpperCase(); 47 | 48 | let classes = [ "KeyHandler" ]; 49 | if ( this.props.showKeys ) { 50 | classes.push( "visible" ); 51 | } 52 | 53 | return 54 | { keyText } 55 | ; 56 | } 57 | }; 58 | 59 | KeyHandler.propTypes = { 60 | shortcut: React.PropTypes.string.isRequired, 61 | onTrigger: React.PropTypes.func, 62 | }; 63 | 64 | const mapStateToProps = state => { 65 | return { 66 | ...state.ui, 67 | enabled: state.preferences.showShortcuts, 68 | }; 69 | }; 70 | 71 | export default connect( mapStateToProps )( KeyHandler ); 72 | -------------------------------------------------------------------------------- /public/loader.css: -------------------------------------------------------------------------------- 1 | /* 2 | 660×540 3 | */ 4 | 5 | html, body { 6 | height: 100%; 7 | font-size: 16px; 8 | user-select: none; 9 | } 10 | 11 | html { 12 | background: #fff; 13 | color: #333; 14 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 15 | box-sizing: border-box; 16 | } 17 | 18 | *, *:before, *:after { 19 | box-sizing: inherit; 20 | } 21 | 22 | body { 23 | margin: 0; 24 | display: flex; 25 | } 26 | 27 | #root { 28 | width: 100%; 29 | max-width: 100%; 30 | height: 100%; 31 | display: flex; 32 | 33 | --color-primary: #29ABE2; 34 | --color-primary-red: 41; 35 | --color-primary-green: 171; 36 | --color-primary-blue: 226; 37 | } 38 | 39 | h1, h2, h3, h4, h5, h6 { 40 | font-weight: 400; 41 | } 42 | 43 | pre { 44 | font-family: Input, monospace; 45 | } 46 | 47 | .splash-fade-leave { 48 | opacity: 1; 49 | transition: opacity 400ms; 50 | } 51 | 52 | .splash-fade-leave-active { 53 | opacity: 0; 54 | } 55 | 56 | .splash-fade-enter { 57 | opacity: 0; 58 | transition: opacity 400ms; 59 | } 60 | 61 | .splash-fade-enter-active { 62 | opacity: 1; 63 | } 64 | 65 | .fader { 66 | width: 100%; 67 | height: 100%; 68 | position: relative; 69 | } 70 | 71 | .fader > * { 72 | position: absolute; 73 | top: 0; 74 | left: 0; 75 | right: 0; 76 | bottom: 0; 77 | display: flex; 78 | } 79 | 80 | .Splash { 81 | background: #29ABE2; 82 | color: #fff; 83 | 84 | flex-grow: 1; 85 | flex-shrink: 0; 86 | align-self: stretch; 87 | display: flex; 88 | flex-direction: column; 89 | align-items: center; 90 | justify-content: center; 91 | } 92 | 93 | .Splash-loader { 94 | animation: Splash-loader infinite 2s linear; 95 | } 96 | 97 | @keyframes Splash-loader { 98 | from { transform: rotate(0deg); } 99 | to { transform: rotate(360deg); } 100 | } 101 | -------------------------------------------------------------------------------- /src/MachineItem.css: -------------------------------------------------------------------------------- 1 | .MachineItem { 2 | border-bottom: 1px solid #ccc; 3 | } 4 | 5 | .MachineItem .row { 6 | display: flex; 7 | align-items: center; 8 | padding: 0.4rem 0.5rem; 9 | // transition: background 140ms; 10 | transition: background 120ms, color 120ms; 11 | } 12 | 13 | .MachineItem .row * { 14 | } 15 | 16 | .MachineItem.selected .row { 17 | background: var(--color-primary); 18 | color: #fff; 19 | } 20 | 21 | .MachineItem .status { 22 | color: red; 23 | align-self: center; 24 | margin-right: 0.75em; 25 | width: 10px; 26 | } 27 | .MachineItem.running .status { 28 | color: limegreen; 29 | } 30 | .MachineItem.indeterminate .status { 31 | color: orange; 32 | } 33 | 34 | .MachineItem .status svg { 35 | width: 14px; 36 | height: 14px; 37 | fill: currentColor; 38 | } 39 | 40 | .MachineItem .info { 41 | flex-grow: 1; 42 | 43 | display: flex; 44 | flex-direction: column; 45 | position: relative; 46 | // font-size: 0.875rem; 47 | } 48 | 49 | .MachineItem .info h1 { 50 | margin: 0; 51 | font-size: inherit; 52 | line-height: 1.5; 53 | display: flex; 54 | align-items: baseline; 55 | } 56 | 57 | .MachineItem .info p { 58 | margin: 0; 59 | color: #929292; 60 | font-size: 0.7857rem; 61 | transition: color 120ms; 62 | } 63 | 64 | .MachineItem.selected .info p { 65 | color: #eee; 66 | } 67 | 68 | .MachineItem .domain { 69 | font-size: 0.875rem; 70 | align-self: center; 71 | } 72 | .MachineItem .domain p { 73 | margin: 0; 74 | } 75 | 76 | .MachineItem .MachineDetails { 77 | display: none; 78 | } 79 | .MachineItem.selected .MachineDetails { 80 | display: block; 81 | } 82 | 83 | .MachineItem .info input { 84 | font-size: inherit; 85 | background: transparent; 86 | color: #fff; 87 | border: 1px solid transparent; 88 | border-bottom-color: #fff; 89 | margin: 0 -4px; 90 | padding-left: 3px; 91 | // padding-right: 2px; 92 | height: 24px; 93 | } 94 | .MachineItem .info input:focus { 95 | outline: 0; 96 | border-color: #fff; 97 | } 98 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
    2 |

    Chassis Desktop

    3 |

    Local WordPress development made easy.

    4 | 5 |
    6 |

    Chassis Desktop is an application to manage Chassis development environments, without touching the command line. Create, manage, and configure development environments with a simple UI.

    7 |

    Install Chassis Desktop →

    8 |

    (Chassis Desktop is in beta, and may break. Please let us know if it does!)

    9 |
    10 | 11 | ## Development 12 | 13 | Chassis Desktop is an [Electron application](http://electron.atom.io/), and uses build tools based on [Create React App][create-react-app]. Node.js/npm is required to build Desktop. 14 | 15 | To run the development version: 16 | 17 | ```sh 18 | # Clone this repository 19 | git clone https://github.com/Chassis/Desktop chassis-desktop 20 | cd chassis-desktop 21 | 22 | # Install dependencies 23 | npm install 24 | 25 | # Run. 26 | npm start 27 | ``` 28 | 29 | 30 | ## Building for Release 31 | 32 | Release/production builds have two build stages: building JS for release, and building the full application packages. 33 | 34 | ```sh 35 | # Build the app scripts 36 | npm run build 37 | 38 | # Verify scripts: 39 | electron . 40 | 41 | # Pack for testing 42 | npm run pack 43 | 44 | # Verify app: 45 | open "dist/mac/Chassis Desktop.app" 46 | 47 | # Pack for distribution (into DMG) 48 | npm run dist 49 | ``` 50 | 51 | 52 | ## License 53 | 54 | Chassis Desktop is licensed under the [BSD License](license.md). 55 | 56 | Contains code from the [create-react-app][] project, copyright Facebook, Inc. Used under the [BSD license](https://github.com/facebookincubator/create-react-app/blob/master/LICENSE). 57 | 58 | [create-react-app]: https://github.com/facebookincubator/create-react-app 59 | -------------------------------------------------------------------------------- /src/Installer/ImportBoxes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Button from '../Button'; 4 | import Step from '../Step'; 5 | import formatPath from '../lib/formatPath'; 6 | 7 | import './ImportBoxes.css'; 8 | 9 | export default class ImportBoxes extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | selected: {}, 15 | }; 16 | } 17 | 18 | onCheck(box, value) { 19 | this.setState( state => { 20 | const diff = {}; 21 | diff[ box.directory ] = value; 22 | return { selected: { ...state.selected, ...diff } }; 23 | }); 24 | } 25 | 26 | render() { 27 | let boxes = this.props.boxes.slice(); 28 | boxes.sort((a, b) => { 29 | const left = a.directory.toLowerCase(); 30 | const right = b.directory.toLowerCase(); 31 | if ( left > right ) { 32 | return -1; 33 | } 34 | 35 | return left === right ? 0 : -1; 36 | }); 37 | 38 | const selected = Object.keys(this.state.selected).filter(key => this.state.selected[key]); 39 | 40 | return 41 |
    42 |

    Import Existing Boxes

    43 |
    44 | 45 |

    We've found some existing boxes on your system. Want to import them?

    46 | 47 |
      48 | { boxes.map( box => 49 |
    • 50 | 59 |
    • 60 | )} 61 |
    62 | 63 | { selected.length > 0 ? 64 |

    65 | Importing { selected.length } { selected.length === 1 ? "box" : "boxes" }. 66 | 71 |

    72 | : 73 |

    74 | 79 |

    80 | } 81 |
    ; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/lib/actions/loadConfig.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import yaml from 'js-yaml'; 4 | 5 | import { updateBox } from '../actions'; 6 | 7 | function deepMap(data, callback) { 8 | if (Array.isArray(data)) { 9 | return data.map(item => deepMap(item, callback)); 10 | } else if (typeof data === "object") { 11 | let nextData = {...data}; 12 | for (let key in data) { 13 | if (data.hasOwnProperty(key)) { 14 | nextData[key] = deepMap(data[key], callback); 15 | } 16 | } 17 | return nextData; 18 | } 19 | 20 | return callback(data); 21 | } 22 | 23 | function parseYaml( data ) { 24 | let parsed = yaml.safeLoad( data ); 25 | 26 | // Correctly parse "Yes"/"No" for YAML 1.1 compatibility. 27 | let corrected = deepMap( parsed, item => { 28 | if ( ! ( typeof item === "string" ) ) { 29 | return item; 30 | } 31 | if ( item.search( /^(y|n|yes|no)$/i ) === 0 ) { 32 | return item[0].toLowerCase() === 'y'; 33 | } 34 | return item; 35 | }); 36 | 37 | return corrected; 38 | } 39 | 40 | export function loadFile( path ) { 41 | return new Promise( resolve => { 42 | fs.readFile( path, (err, data) => { 43 | if ( err ) { 44 | return resolve( {} ); 45 | } 46 | 47 | let fileConfig = parseYaml( data ); 48 | return resolve( fileConfig ); 49 | }) 50 | }); 51 | } 52 | 53 | export default function loadConfig(machinePath) { 54 | return dispatch => { 55 | // Use promises to ensure config order is correct. 56 | let promises = [ 57 | 'config.yaml', 58 | 'config.local.yaml', 59 | 'content/config.yaml', 60 | 'content/config.local.yaml', 61 | ].map( configFile => loadFile( path.join( machinePath, configFile ) ) ); 62 | 63 | return Promise.all( promises ).then( parts => { 64 | let config = parts.reduce( ( carry, value ) => ({ ...carry, ...value }), {} ); 65 | dispatch( updateBox( machinePath, { config } ) ); 66 | }); 67 | }; 68 | } 69 | 70 | export function loadAllConfig() { 71 | console.log( 'loading all' ); 72 | return (dispatch, getStore) => { 73 | const store = getStore(); 74 | store.boxes.forEach(box => { 75 | console.log( 'loading for ', box.path ); 76 | dispatch(loadConfig(box.path)); 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/MachineList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { REMOVE_BOX, deselectBox, finishEditingBox, removeBox, runCommand, selectBox, showModal, startEditingBox, saveBoxChanges, updateBoxStatus } from './lib/actions'; 5 | import Button from './Button'; 6 | import MachineItem from './MachineItem'; 7 | 8 | import './MachineList.css'; 9 | 10 | class MachineList extends React.Component { 11 | render() { 12 | const {boxes, dispatch, terminal, ui} = this.props; 13 | const selected = ui.selectedBox; 14 | 15 | const selectedIndex = boxes.findIndex( box => selected === box.path ); 16 | 17 | return
    18 | { ui.undo && ui.undo.type === REMOVE_BOX ? 19 |
    20 |

    Box removed. 21 | 24 |

    25 |
    26 | : null } 27 | 28 | { boxes.map( (machine, index) => 29 | dispatch( removeBox( machine ) ) } 38 | onDeselect={ () => dispatch(deselectBox()) } 39 | onStartEditing={ () => dispatch(startEditingBox())} 40 | onFinishEditing={ () => dispatch(finishEditingBox()) } 41 | onRefresh={ () => dispatch(updateBoxStatus(machine.path)) } 42 | onRun={ (command, args, opts) => dispatch(runCommand(machine.path, command, args, opts)) } 43 | onSave={ data => dispatch(saveBoxChanges(machine.path, data)) } 44 | onSelect={ () => dispatch(selectBox(machine.path)) } 45 | /> 46 | )} 47 | 48 | { boxes.length === 0 ? 49 |
    50 |

    51 | 54 |

    55 |
    56 | : null } 57 |
    ; 58 | } 59 | } 60 | 61 | export default connect(state => state)(MachineList); 62 | -------------------------------------------------------------------------------- /src/Settings.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { setPreference, reset } from './lib/actions'; 5 | import Button from './Button'; 6 | import FormTable from './Form/Table'; 7 | import Header from './Header'; 8 | 9 | import './Settings.css'; 10 | 11 | class Settings extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | showRealReset: false, 17 | }; 18 | } 19 | 20 | render() { 21 | const { dispatch, preferences, onDismiss } = this.props; 22 | const { showRealReset } = this.state; 23 | 24 | if ( showRealReset ) { 25 | return
    26 |
    27 | 28 |
    29 |

    You're about to reset Chassis Desktop, and will need to run through the installer again.

    30 |

    This will remove all settings from the app, including boxes you've added.

    31 |

    This will not delete any boxes or machines, nor will it uninstall Vagrant or VirtualBox.

    32 |
    33 | 34 |

    35 | 39 | 43 |

    44 |
    ; 45 | } 46 | 47 | return
    48 |
    49 | 55 |
    56 | 57 | 58 | 67 | 68 | 69 |

    70 | 74 |

    75 |
    ; 76 | } 77 | } 78 | 79 | export default connect(store => store)(Settings); 80 | -------------------------------------------------------------------------------- /src/lib/actions.js: -------------------------------------------------------------------------------- 1 | import * as install from './actions/install'; 2 | import {updateMachineConfig} from './actions/updateConfig'; 3 | 4 | export {default as cloneChassis} from './actions/cloneChassis'; 5 | export {default as loadConfig} from './actions/loadConfig'; 6 | export {default as runCommand} from './actions/runCommand'; 7 | export {default as updateBoxStatus} from './actions/updateBoxStatus'; 8 | export {default as updateGlobalStatus} from './actions/updateGlobalStatus'; 9 | 10 | export { install, updateMachineConfig }; 11 | 12 | export const INIT_VAGRANT = 'INIT_VAGRANT'; 13 | export const ADD_BOX = 'ADD_BOX'; 14 | export const UPDATE_BOX = 'UPDATE_BOX'; 15 | export const SELECT_BOX = 'SELECT_BOX'; 16 | export const REMOVE_BOX = 'REMOVE_BOX'; 17 | export const SET_EDITING = 'SET_EDITING'; 18 | export const META_KEY_DOWN = 'META_KEY_DOWN'; 19 | export const META_KEY_UP = 'META_KEY_UP'; 20 | export const WINDOW_BLUR = 'WINDOW_BLUR'; 21 | export const SHOW_MODAL = 'SHOW_MODAL'; 22 | export const RESET = 'RESET'; 23 | export const SET_PREFERENCE = 'SET_PREFERENCE'; 24 | 25 | export function addBox(name, path) { 26 | return { type: ADD_BOX, machine: { name, path, domain: "", status: "" } }; 27 | } 28 | 29 | export function createBox(name, path) { 30 | return { type: ADD_BOX, name, path }; 31 | } 32 | 33 | export function updateBox(path, data) { 34 | return { type: UPDATE_BOX, path, data }; 35 | } 36 | 37 | export function removeBox(machine) { 38 | return { type: REMOVE_BOX, machine }; 39 | } 40 | 41 | export function selectBox(path) { 42 | return { type: SELECT_BOX, path }; 43 | } 44 | 45 | export function deselectBox() { 46 | return { type: SELECT_BOX, path: null }; 47 | } 48 | 49 | export function startEditingBox() { 50 | return { type: SET_EDITING, editing: true }; 51 | } 52 | 53 | export function finishEditingBox() { 54 | return { type: SET_EDITING, editing: false }; 55 | } 56 | 57 | export function saveBoxChanges(path, changes) { 58 | return (dispatch, getStore) => { 59 | if ( changes.name ) { 60 | dispatch( updateBox( path, changes ) ); 61 | } 62 | 63 | if ( changes.config ) { 64 | updateMachineConfig( path, changes.config )( dispatch, getStore ); 65 | } 66 | }; 67 | } 68 | 69 | export function showModal(id) { 70 | return { type: SHOW_MODAL, id }; 71 | } 72 | 73 | export function hideModal() { 74 | return { type: SHOW_MODAL, id: null }; 75 | } 76 | 77 | export function reset() { 78 | return { type: RESET }; 79 | } 80 | 81 | export function setPreference( key, value ) { 82 | return { type: 'SET_PREFERENCE', key, value }; 83 | } 84 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import CSSTransitionGroup from 'react/lib/ReactCSSTransitionGroup'; 3 | import { connect } from 'react-redux'; 4 | 5 | import { hideModal, showModal } from './lib/actions'; 6 | import CreateModal from './CreateModal'; 7 | import Header from './Header'; 8 | import Installer from './Installer'; 9 | import MachineList from './MachineList'; 10 | import Modal from './Modal'; 11 | import Settings from './Settings'; 12 | 13 | import openBrowser from './lib/openBrowser'; 14 | import Button from './Button'; 15 | 16 | import './App.css'; 17 | 18 | class App extends Component { 19 | render() { 20 | const { dispatch, installer } = this.props; 21 | const { modal } = this.props.ui; 22 | const installed = installer.installed.chassis; 23 | 24 | let modalComponent = null; 25 | const onDismiss = () => dispatch(hideModal()); 26 | 27 | switch ( modal ) { 28 | case 'create': 29 | modalComponent = ; 30 | break; 31 | 32 | case 'settings': 33 | modalComponent = ; 34 | break; 35 | 36 | default: 37 | // No-op 38 | break; 39 | } 40 | 41 | // Installation override. 42 | if ( ! installed ) { 43 | modalComponent = ; 44 | } 45 | 46 | const logo = ; 47 | return
    48 | { installed ? 49 |
    50 | 58 | 67 | 76 |
    77 | : null } 78 | 79 | 85 | { modalComponent } 86 | 87 | 88 | { installed ? 89 |
    90 | 91 |
    92 | : null } 93 |
    ; 94 | } 95 | } 96 | 97 | export default connect(state => state)(App); 98 | -------------------------------------------------------------------------------- /src/MachineDetails.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {spawn} from 'child_process'; 3 | import { shell } from 'electron'; 4 | // import {AllHtmlEntities} from 'html-entities'; 5 | 6 | import './MachineDetails.scss'; 7 | 8 | import Button from './Button'; 9 | import MachineActions from './MachineActions'; 10 | import Terminal from './Terminal'; 11 | 12 | // const entities = new AllHtmlEntities(); 13 | 14 | const stateForCommand = terminal => { 15 | const command = terminal.running ? `${terminal.command} ${terminal.args[0]}` : false; 16 | switch ( command ) { 17 | case 'vagrant up': 18 | return 'launching'; 19 | 20 | case 'vagrant halt': 21 | return 'halting'; 22 | 23 | default: 24 | return null; 25 | } 26 | }; 27 | 28 | export default class MachineDetails extends React.Component { 29 | constructor(props) { 30 | super(props); 31 | 32 | this.state = { 33 | showingConsole: false, 34 | }; 35 | } 36 | 37 | componentDidUpdate() { 38 | if (this.terminal) { 39 | this.terminal.scrollTop = this.terminal.scrollHeight; 40 | } 41 | } 42 | 43 | componentWillReceiveProps(nextProps) { 44 | if (!this.props.terminal) { 45 | return; 46 | } 47 | 48 | if (this.props.terminal.running && ! nextProps.terminal.running) { 49 | this.props.onRefresh(); 50 | } 51 | } 52 | 53 | onFinder() { 54 | shell.showItemInFolder( this.props.machine.path ); 55 | } 56 | 57 | /*onEditor() { 58 | alert('To-do!'); 59 | return; 60 | //spawn('open', ['-t', '.'], { 61 | // cwd: this.props.machine.path, 62 | //}); 63 | }*/ 64 | 65 | onTerminal() { 66 | spawn('open', ['-a', 'Terminal', '.'], { 67 | cwd: this.props.machine.path, 68 | }); 69 | } 70 | 71 | render() { 72 | let { machine, terminal } = this.props; 73 | let { showingConsole } = this.state; 74 | 75 | let status = stateForCommand( terminal ) || machine.status; 76 | 77 | return
    78 | this.onFinder()} 83 | onHalt={() => this.props.onRun( 'vagrant', [ 'halt' ] )} 84 | onLaunch={() => this.props.onRun( 'vagrant', [ 'up' ] )} 85 | onRefresh={() => this.props.onRefresh()} 86 | onTerminal={() => this.onTerminal()} 87 | /> 88 | 89 |
    90 | 94 | 100 |
    101 |
    ; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/MachineActions.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import openBrowser from './lib/openBrowser'; 4 | import Button from './Button'; 5 | import LoadingIndicator from './LoadingIndicator'; 6 | 7 | import './MachineActions.css'; 8 | 9 | export default props => { 10 | let mainActions, status; 11 | let refreshButton = null; //; 12 | switch (props.status) { 13 | case 'not_created': 14 | case 'poweroff': 15 | status =

    Status: Off { refreshButton }

    16 | mainActions =

    17 | 18 |

    ; 19 | break; 20 | 21 | case 'running': 22 | status =

    Status: Running { refreshButton }

    ; 23 | mainActions =

    24 | 25 | 26 |

    ; 27 | break; 28 | 29 | case 'loading': 30 | status =

    Status: Loading { refreshButton }

    ; 31 | mainActions =

    ; 32 | break; 33 | 34 | case 'launching': 35 | status =

    Status: Launching

    ; 36 | // mainActions =

    ; 37 | break; 38 | 39 | case 'halting': 40 | status =

    Status: Halting

    ; 41 | // mainActions =

    ; 42 | break; 43 | 44 | default: 45 | status =

    Status: Unknown { refreshButton }

    ; 46 | mainActions =

    ; 47 | break; 48 | } 49 | 50 | let domain = ( props.machine.config && "hosts" in props.machine.config ) ? props.machine.config.hosts[0] : null; 51 | 52 | return
    53 |
    54 | { status } 55 | { mainActions } 56 |
    57 | 58 |
    59 |

    60 | 65 |

    66 |

    67 | { props.status === 'running' && domain ? 68 | 74 | : null } 75 | 76 | 82 | 88 |

    89 |
    90 |
    ; 91 | }; 92 | -------------------------------------------------------------------------------- /src/CreateModal/Type.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { remote } from 'electron'; 3 | 4 | import KeyHandler from '../KeyHandler'; 5 | import Icon from '../Icon'; 6 | import Step from '../Step'; 7 | 8 | import './Type.css'; 9 | 10 | const dialog = remote.dialog; 11 | 12 | export const TYPES = { 13 | CREATE: 'CREATE', 14 | RETROFIT: 'RETROFIT', 15 | IMPORT: 'IMPORT', 16 | }; 17 | 18 | export default class Type extends React.Component { 19 | handleSelectCreate() { 20 | const path = dialog.showSaveDialog({ 21 | title: 'Select new folder for Chassis.', 22 | properties: [ 'createDirectory' ], 23 | buttonLabel: 'Create', 24 | defaultPath: this.props.name, 25 | message: 'A new folder will be created with this name, and Chassis will be installed inside it.', 26 | nameFieldLabel: 'Folder Name:', 27 | showsTagField: false, 28 | }); 29 | if ( ! path ) { 30 | return; 31 | } 32 | 33 | this.props.onSelect( TYPES.CREATE, path ); 34 | } 35 | 36 | handleSelectRetrofit() { 37 | const path = dialog.showOpenDialog({ 38 | title: 'Choose an existing WordPress installation.', 39 | message: 'A folder named "chassis" will be created inside the folder you select.', 40 | properties: [ 'openDirectory' ] 41 | }); 42 | if ( ! path ) { 43 | return; 44 | } 45 | 46 | this.props.onSelect( TYPES.RETROFIT, path[0] ); 47 | } 48 | 49 | handleSelectImport() { 50 | const path = dialog.showOpenDialog({ 51 | title: 'Select existing Chassis folder.', 52 | properties: [ 'openDirectory' ] 53 | }); 54 | if ( ! path ) { 55 | return; 56 | } 57 | 58 | this.props.onSelect( TYPES.IMPORT, path[0] ); 59 | } 60 | 61 | render() { 62 | return 63 |

    Select Your Project Type

    64 | 96 |
    ; 97 | } 98 | } 99 | Type.propTypes = { 100 | /** 101 | * ( type, directory ) => void 102 | */ 103 | onSelect: React.PropTypes.func.isRequired 104 | }; 105 | -------------------------------------------------------------------------------- /src/lib/keys.js: -------------------------------------------------------------------------------- 1 | import * as actions from './actions'; 2 | 3 | const toCode = text => { 4 | const keys = text.split('+').map(key => key.trim()); 5 | const code = { 6 | key: null, 7 | ctrlKey: false, 8 | metaKey: false, 9 | shiftKey: false, 10 | }; 11 | for ( let index = 0; index < keys.length; index++ ) { 12 | let key = keys[index]; 13 | switch (key.toLowerCase()) { 14 | case 'cmd': 15 | code.metaKey = true; 16 | break; 17 | 18 | case 'ctrl': 19 | code.ctrlKey = true; 20 | break; 21 | 22 | case 'shift': 23 | code.shiftKey = true; 24 | break; 25 | 26 | case 'escape': 27 | case 'esc': 28 | code.key = 'Escape'; 29 | break; 30 | 31 | case 'up': 32 | case 'down': 33 | case 'left': 34 | case 'right': 35 | code.key = 'Arrow' + key[0].toUpperCase() + key.slice(1).toLowerCase(); 36 | break; 37 | 38 | default: 39 | if (code.key) { 40 | throw new Error('Only a single key is supported'); 41 | } 42 | if (key.length !== 1) { 43 | throw new Error('Only single characters are supported'); 44 | } 45 | code.key = key.toLowerCase(); 46 | break; 47 | } 48 | } 49 | 50 | if (!code.key) { 51 | throw new Error('keyCode is required'); 52 | } 53 | 54 | return code; 55 | }; 56 | 57 | const COMPARE_PROPS = [ 'key', 'metaKey', 'ctrlKey', 'shiftKey' ]; 58 | const compare = (left, right) => { 59 | for ( let index = 0; index < COMPARE_PROPS.length; index++ ) { 60 | let prop = COMPARE_PROPS[ index ]; 61 | if (left[prop] !== right[prop]) { 62 | return false; 63 | } 64 | } 65 | 66 | return true; 67 | }; 68 | 69 | export default class Keys { 70 | constructor() { 71 | this.handlers = {}; 72 | this.nextIndex = 0; 73 | } 74 | 75 | register( key, callback ) { 76 | const code = { ...toCode( key ), callback, id: this.nextIndex++ }; 77 | this.handlers[ code.key ] = this.handlers[ code.key ] || []; 78 | this.handlers[ code.key ].push( code ); 79 | return code.id; 80 | } 81 | 82 | unregister( key, id ) { 83 | const code = toCode( key ); 84 | if ( ! ( code.key in this.handlers ) ) { 85 | return false; 86 | } 87 | let current = this.handlers[ code.key ]; 88 | let next = current.filter(item => { 89 | return ! ( compare( item, code ) && item.id === id ); 90 | }); 91 | this.handlers[code.key] = next; 92 | return current !== next; 93 | } 94 | 95 | trigger( e ) { 96 | if ( ! e.key || ! ( e.key in this.handlers ) ) { 97 | return; 98 | } 99 | 100 | // Slice to ensure handlers doesn't change while firing. 101 | let handlers = this.handlers[ e.key ].slice(); 102 | for ( let index = 0; index < handlers.length; index++ ) { 103 | let handler = handlers[ index ]; 104 | if ( compare( e, handler ) ) { 105 | handler.callback( e ); 106 | } 107 | } 108 | } 109 | 110 | listen( store ) { 111 | window.addEventListener( 'keydown', e => { 112 | if (e.key === "Meta" && e.metaKey) { 113 | store.dispatch({ type: actions.META_KEY_DOWN }); 114 | } 115 | this.trigger( e ); 116 | }); 117 | window.addEventListener( 'keyup', e => { 118 | if (e.key === "Meta" && !e.metaKey) { 119 | store.dispatch({ type: actions.META_KEY_UP }); 120 | } 121 | }); 122 | window.addEventListener( 'blur', e => { 123 | store.dispatch({ type: actions.WINDOW_BLUR }); 124 | }); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/MachineSettings.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import tildify from 'tildify'; 3 | 4 | import Button from './Button'; 5 | import FormTable from './Form/Table'; 6 | import FixedValue from './Form/FixedValue'; 7 | 8 | import './MachineSettings.css'; 9 | 10 | export default class MachineSettings extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = {}; 15 | } 16 | 17 | onChangeHost(index, value) { 18 | let hosts = (this.state.hosts || this.props.machine.config.hosts).slice(); 19 | hosts[ index ] = value; 20 | this.setState({ hosts }); 21 | } 22 | 23 | onChangeIP(value) { 24 | const ip = value ? value : 'dhcp'; 25 | this.props.onChange({ ip }); 26 | } 27 | 28 | render() { 29 | const { changes, machine, onChange, onDelete, onRefresh } = this.props; 30 | const config = { ...machine.config, ...changes }; 31 | 32 | return
    33 | 34 |
    35 |
    Path:
    36 |
    37 | 38 |

    Paths cannot be changed once added.

    39 |
    40 |
    41 | 53 |
    54 |
    IP Address:
    55 | this.onChangeIP( e.target.value ) } 60 | /> 61 |
    62 | 76 |
    77 |
    78 | 85 |

    Only subdirectory-based multisite is currently supported.

    86 |
    87 |
    88 | {/* 89 |
    90 |
    Extensions:
    91 |
    92 | 93 |
    94 |
    95 | */} 96 |
    97 | {/**/} 98 | 99 |
    100 |

    Actions

    101 | 102 |

    103 |

    This will refresh config details from the Vagrant and YAML data.

    104 | 105 |

    106 |

    This will remove this box from the app. It will not delete any files.

    107 |
    108 |
    ; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chassis-desktop", 3 | "version": "0.2.1", 4 | "productName": "Chassis Desktop", 5 | "description": "Manage local WordPress development environments through a handy UI.", 6 | "private": true, 7 | "main": "electron.js", 8 | "devDependencies": { 9 | "@storybook/react": "^3.1.8", 10 | "ansi-html": "0.0.6", 11 | "autoprefixer": "6.4.1", 12 | "babel-core": "6.14.0", 13 | "babel-eslint": "6.1.2", 14 | "babel-jest": "15.0.0", 15 | "babel-loader": "6.2.5", 16 | "babel-preset-react-app": "^0.2.1", 17 | "case-sensitive-paths-webpack-plugin": "1.1.4", 18 | "chalk": "1.1.3", 19 | "connect-history-api-fallback": "1.3.0", 20 | "css-loader": "0.24.0", 21 | "deepmerge": "^1.3.1", 22 | "detect-port": "1.0.0", 23 | "devtron": "^1.4.0", 24 | "dotenv": "2.0.0", 25 | "electron": "^1.6.11", 26 | "electron-builder": "^11.7.0", 27 | "electron-debug": "^1.1.0", 28 | "eslint": "3.5.0", 29 | "eslint-config-react-app": "^0.2.1", 30 | "eslint-loader": "1.5.0", 31 | "eslint-plugin-flowtype": "2.18.1", 32 | "eslint-plugin-import": "1.12.0", 33 | "eslint-plugin-jsx-a11y": "2.2.2", 34 | "eslint-plugin-react": "6.3.0", 35 | "extract-text-webpack-plugin": "1.0.1", 36 | "file-loader": "0.9.0", 37 | "filesize": "3.3.0", 38 | "find-cache-dir": "0.1.1", 39 | "fix-path": "^2.1.0", 40 | "font-awesome": "^4.6.3", 41 | "fs-extra": "0.30.0", 42 | "gzip-size": "3.0.0", 43 | "html-entities": "^1.2.0", 44 | "html-webpack-plugin": "2.22.0", 45 | "http-proxy-middleware": "0.17.1", 46 | "is-equal": "^1.5.3", 47 | "jest": "15.1.1", 48 | "js-yaml": "^3.7.0", 49 | "json-loader": "0.5.4", 50 | "node-sass": "^4.13.0", 51 | "object-assign": "4.1.0", 52 | "path-exists": "2.1.0", 53 | "plist": "^2.0.1", 54 | "postcss-loader": "0.13.0", 55 | "promise": "7.1.1", 56 | "react": "^15.3.2", 57 | "react-dev-utils": "^0.2.1", 58 | "react-dom": "^15.3.2", 59 | "react-redux": "^4.4.5", 60 | "recursive-readdir": "2.1.0", 61 | "redux": "^3.6.0", 62 | "redux-logger": "^2.7.4", 63 | "redux-thunk": "^2.1.0", 64 | "rimraf": "2.5.4", 65 | "sass-loader": "^4.0.2", 66 | "script-ext-html-webpack-plugin": "^1.8.0", 67 | "strip-ansi": "3.0.1", 68 | "style-loader": "0.13.1", 69 | "tildify": "^1.2.0", 70 | "url-loader": "0.5.7", 71 | "webpack": "1.13.2", 72 | "webpack-dev-server": "1.16.1", 73 | "whatwg-fetch": "1.0.0", 74 | "which": "^1.2.12" 75 | }, 76 | "scripts": { 77 | "start": "node scripts/start.js", 78 | "run": "electron .", 79 | "storybook": "start-storybook -p 9001 -c config/storybook", 80 | "build": "node scripts/build.js", 81 | "test": "node scripts/test.js --env=jsdom", 82 | "pack": "build --dir", 83 | "dist": "build" 84 | }, 85 | "author": { 86 | "name": "Chassis Team", 87 | "url": "http://chassis.io/" 88 | }, 89 | "jest": { 90 | "moduleFileExtensions": [ 91 | "jsx", 92 | "js", 93 | "json" 94 | ], 95 | "moduleNameMapper": { 96 | "^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/config/jest/FileStub.js", 97 | "^.+\\.css$": "/config/jest/CSSStub.js" 98 | }, 99 | "setupFiles": [ 100 | "/config/polyfills.js" 101 | ], 102 | "testPathIgnorePatterns": [ 103 | "/(build|docs|node_modules)/" 104 | ], 105 | "testEnvironment": "node", 106 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(js|jsx)$" 107 | }, 108 | "babel": { 109 | "presets": [ 110 | "react-app" 111 | ] 112 | }, 113 | "eslintConfig": { 114 | "extends": "react-app" 115 | }, 116 | "build": { 117 | "appId": "io.chassis.desktop", 118 | "mac": { 119 | "category": "public.app-category.developer-tools" 120 | }, 121 | "files": [ 122 | "build/**/*", 123 | "electron.js" 124 | ], 125 | "directories": { 126 | "buildResources": "resources" 127 | }, 128 | "protocols": { 129 | "name": "Chassis Desktop URL", 130 | "role": "Viewer", 131 | "schemes": [ 132 | "chassis" 133 | ] 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/CreateModal.js: -------------------------------------------------------------------------------- 1 | import { join, sep } from 'path'; 2 | import React from 'react'; 3 | import {connect} from 'react-redux'; 4 | 5 | import { addBox, cloneChassis, loadConfig, selectBox, updateBoxStatus } from './lib/actions'; 6 | import { saveConfig } from './lib/actions/updateConfig'; 7 | import Button from './Button'; 8 | import Config from './CreateModal/Config'; 9 | import Header from './Header'; 10 | import Type, { TYPES } from './CreateModal/Type'; 11 | import Steps from './Steps'; 12 | 13 | import './CreateModal.css'; 14 | 15 | const nameForPath = path => { 16 | const parts = path.split( sep ); 17 | if ( parts[ parts.length - 1 ].toLowerCase() === 'chassis' ) { 18 | // Ignore. 19 | parts.pop(); 20 | } 21 | 22 | return parts[ parts.length - 1 ]; 23 | }; 24 | 25 | class CreateModal extends React.Component { 26 | constructor(props) { 27 | super(props); 28 | 29 | this.state = { 30 | step: 0, 31 | name: null, 32 | type: null, 33 | path: null, 34 | }; 35 | } 36 | 37 | onNext( name ) { 38 | this.setState( state => ({ name, step: state.step + 1 }) ); 39 | } 40 | 41 | onSelect(type, path) { 42 | this.setState( state => ({ 43 | step: state.step + 1, 44 | type, 45 | name: nameForPath( path ), 46 | path 47 | })); 48 | } 49 | 50 | onCreate() { 51 | const { dispatch } = this.props; 52 | const { name, path, type } = this.state; 53 | switch ( type ) { 54 | case TYPES.CREATE: 55 | // Add, clone, and refresh. 56 | dispatch( addBox( name, path ) ); 57 | 58 | dispatch( cloneChassis( path ) ) 59 | .then( () => dispatch( loadConfig( path ) ) ) 60 | .then( () => dispatch( updateBoxStatus( path ) ) ); 61 | 62 | // Select the newly created box. 63 | dispatch( selectBox( path ) ); 64 | 65 | break; 66 | 67 | case TYPES.IMPORT: 68 | // Add, then refresh. 69 | dispatch( addBox( name, path ) ); 70 | dispatch( loadConfig( path ) ) 71 | .then(() => { 72 | console.log('fulfilled'); 73 | dispatch( updateBoxStatus( path ) ) 74 | }); 75 | 76 | // Select the newly created box. 77 | dispatch( selectBox( path ) ); 78 | 79 | break; 80 | 81 | case TYPES.RETROFIT: 82 | // The Chassis directory is one level deeper. 83 | const chassisPath = join( path, 'chassis' ); 84 | const settings = { 85 | paths: { 86 | base: '..', 87 | wp: 'wp', 88 | content: 'content', 89 | } 90 | }; 91 | const configPath = join( chassisPath, 'config.local.yaml' ); 92 | 93 | dispatch( addBox( name, chassisPath ) ); 94 | dispatch( cloneChassis( chassisPath ) ) 95 | .then( () => saveConfig( configPath, settings ) ) 96 | .then( () => dispatch( loadConfig( chassisPath ) ) ) 97 | .then( () => dispatch( updateBoxStatus( chassisPath ) ) ); 98 | 99 | // Select the newly-created box. 100 | dispatch( selectBox( chassisPath ) ); 101 | break; 102 | 103 | default: 104 | // No-op. 105 | break; 106 | } 107 | 108 | this.props.onDismiss(); 109 | } 110 | 111 | render() { 112 | const { step } = this.state; 113 | const onBack = () => this.setState( state => ({ step: state.step - 1 }) ); 114 | 115 | return
    116 |
    117 | { step > 0 ? 118 | 124 | : 125 | 131 | } 132 |
    133 | 134 | 135 | this.onSelect(...args) } 139 | /> 140 | this.setState( data ) } 146 | onSubmit={() => this.onCreate()} 147 | /> 148 | 149 |
    ; 150 | } 151 | } 152 | 153 | CreateModal.propTypes = { 154 | onDismiss: React.PropTypes.func.isRequired, 155 | }; 156 | 157 | export default connect(state => state)(CreateModal); 158 | -------------------------------------------------------------------------------- /electron.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, Menu, protocol, shell } = require('electron'); 2 | const path = require('path'); 3 | 4 | if (process.env.NODE_ENV === 'development') { 5 | require('electron-debug')({ 6 | showDevTools: 'undocked', 7 | }); 8 | } 9 | 10 | // Keep a global reference of the window object, if you don't, the window will 11 | // be closed automatically when the JavaScript object is garbage collected. 12 | let win, aboutWindow 13 | 14 | function createWindow() { 15 | // Create the browser window. 16 | win = new BrowserWindow({ 17 | width: 660, 18 | height: 540, 19 | resizable: false, 20 | fullscreenable: false, 21 | backgroundColor: '#29ABE2', 22 | center: true, 23 | title: 'Chassis', 24 | titleBarStyle: 'hidden', 25 | show: false, 26 | }) 27 | 28 | // and load the index.html of the app. 29 | if (process.env.NODE_ENV === 'development') { 30 | win.loadURL('http://localhost:3000/') 31 | } else { 32 | win.loadURL(`file://${__dirname}/build/index.html`); 33 | } 34 | 35 | win.on('ready-to-show', () => win.show()) 36 | 37 | win.webContents.on('will-navigate', (e, url) => { 38 | // Allow internal navigation 39 | if ( url.startsWith( 'static://' ) ) { 40 | return; 41 | } 42 | 43 | // Allow reloading for local development. 44 | if ( process.env.NODE_ENV === 'development' ) { 45 | if ( url.startsWith( 'http://localhost:3000/' ) ) { 46 | return; 47 | } 48 | } 49 | 50 | e.preventDefault() 51 | shell.openExternal(url) 52 | }) 53 | 54 | // Emitted when the window is closed. 55 | win.on('closed', () => { 56 | // Dereference the window object, usually you would store windows 57 | // in an array if your app supports multi windows, this is the time 58 | // when you should delete the corresponding element. 59 | win = null 60 | }) 61 | } 62 | 63 | function createAboutWindow() { 64 | aboutWindow = new BrowserWindow({ 65 | width: 648, 66 | height: 320, 67 | resizable: false, 68 | center: true, 69 | vibrancy: 'light', 70 | // Non-Mac: 71 | //backgroundColor: '#ececec', 72 | title: 'About Chassis Desktop', 73 | titleBarStyle: 'hidden-inset', 74 | show: false, 75 | }) 76 | 77 | aboutWindow.loadURL('http://localhost:3000/about.html') 78 | 79 | aboutWindow.on('ready-to-show', () => aboutWindow.show()) 80 | 81 | aboutWindow.webContents.on('will-navigate', (e, url) => { 82 | e.preventDefault() 83 | shell.openExternal(url) 84 | }) 85 | } 86 | 87 | // This method will be called when Electron has finished 88 | // initialization and is ready to create browser windows. 89 | // Some APIs can only be used after this event occurs. 90 | app.on('ready', () => { 91 | // Register static:// protocol for access to build directory assets. 92 | protocol.registerFileProtocol( 93 | 'static', 94 | (request, callback) => { 95 | const url = request.url.substr( 9 ); 96 | callback({ path: path.normalize( `${__dirname}/build/${url}` ) }); 97 | }, 98 | (error) => { 99 | if (error) { 100 | console.error( 'Failed to register protocol' ); 101 | } 102 | } 103 | ); 104 | 105 | createWindow(); 106 | 107 | const mainMenu = Menu.buildFromTemplate([ 108 | { 109 | label: app.getName(), 110 | submenu: [ 111 | { 112 | label: 'About ' + app.getName(), 113 | click: () => createAboutWindow(), 114 | }, 115 | { 116 | label: 'Open Debugger', 117 | click: () => BrowserWindow.getFocusedWindow().webContents.openDevTools(), 118 | }, 119 | {type: 'separator'}, 120 | {role: 'services', submenu: []}, 121 | {type: 'separator'}, 122 | {role: 'hide'}, 123 | {role: 'hideothers'}, 124 | {role: 'unhide'}, 125 | {type: 'separator'}, 126 | {role: 'quit'} 127 | ] 128 | }, 129 | { 130 | role: 'window', 131 | submenu: [ 132 | {role: 'minimize'}, 133 | {role: 'close'}, 134 | {role: 'zoom'}, 135 | {type: 'separator'}, 136 | {role: 'front'} 137 | ] 138 | }, 139 | { 140 | role: 'help', 141 | submenu: [ 142 | { 143 | label: 'Feedback', 144 | click: () => shell.openExternal('https://github.com/Chassis/Desktop/issues'), 145 | } 146 | ] 147 | } 148 | ]); 149 | Menu.setApplicationMenu(mainMenu); 150 | }); 151 | 152 | // Quit when all windows are closed. 153 | app.on('window-all-closed', () => { 154 | // Close, even on macOS 155 | app.quit() 156 | }); 157 | -------------------------------------------------------------------------------- /src/MachineItem.js: -------------------------------------------------------------------------------- 1 | import isEqual from 'is-equal'; 2 | import React from 'react'; 3 | 4 | import formatPath from './lib/formatPath'; 5 | import Button from './Button'; 6 | import KeyHandler from './KeyHandler'; 7 | import MachineDetails from './MachineDetails'; 8 | import MachineSettings from './MachineSettings'; 9 | 10 | import './MachineItem.css'; 11 | 12 | const eachObject = (obj, callback) => { 13 | Object.keys(obj).map(key => [key, obj[key]]).forEach(([key, value]) => { 14 | callback( value, key ); 15 | }); 16 | }; 17 | 18 | const empty = obj => Object.keys( obj ).length === 0; 19 | 20 | export default class MachineItem extends React.Component { 21 | constructor(props) { 22 | super(props); 23 | 24 | this.state = { 25 | nextConfig: {}, 26 | editing: false, 27 | name: props.machine.name, 28 | }; 29 | } 30 | 31 | componentWillReceiveProps(nextProps) { 32 | if ( ! nextProps.selected && this.state.editing ) { 33 | this.onDismiss(); 34 | } 35 | } 36 | 37 | getChanges() { 38 | const { nextConfig } = this.state; 39 | const { config } = this.props.machine; 40 | 41 | let nextMachine = {}; 42 | 43 | if (nextConfig.name !== this.props.machine.name) { 44 | nextMachine.name = nextConfig.name; 45 | } 46 | 47 | // Filter out any values that match existing. 48 | let changes = {}; 49 | eachObject( nextConfig.config, (value, key) => { 50 | if ( ! ( key in config ) || isEqual( value, config[key] ) ) { 51 | return; 52 | } 53 | 54 | changes[key] = value; 55 | }); 56 | 57 | if ( ! empty( changes ) ) { 58 | nextMachine.config = changes; 59 | } 60 | 61 | return nextMachine; 62 | } 63 | 64 | onDismiss() { 65 | this.props.onFinishEditing(); 66 | } 67 | 68 | onStartEditing() { 69 | this.setState({ 70 | nextConfig: { 71 | name: this.props.machine.name || "", 72 | config: this.props.machine.config || {}, 73 | }, 74 | }); 75 | this.props.onStartEditing(); 76 | } 77 | 78 | onEdit( data ) { 79 | this.setState( state => ({ nextConfig: { ...state.nextConfig, ...data} }) ); 80 | } 81 | 82 | onSave() { 83 | const nextMachine = this.getChanges(); 84 | 85 | if ( ! empty( nextMachine ) ) { 86 | this.props.onSave( nextMachine ); 87 | } 88 | 89 | this.onDismiss(); 90 | } 91 | 92 | render() { 93 | const props = this.props; 94 | const { editing, machine, terminal } = props; 95 | const { nextConfig } = this.state; 96 | 97 | let classes = [ 'MachineItem' ]; 98 | if ( props.selected ) { 99 | classes.push('selected'); 100 | } 101 | switch ( machine.status ) { 102 | case 'running': 103 | classes.push('running'); 104 | break; 105 | 106 | case 'loading': 107 | classes.push('indeterminate'); 108 | break; 109 | 110 | default: 111 | break; 112 | } 113 | 114 | let className = classes.join(' '); 115 | 116 | let domain = ( machine.config && "hosts" in machine.config ) ? machine.config.hosts[0] : null; 117 | 118 | return
    119 |
    props.selected ? props.onDeselect() : props.onSelect() }> 120 |
    121 | 122 | 123 | 124 |
    125 |
    126 | { editing ? 127 |

    128 | this.onEdit({ name: e.target.value }) } 132 | onClick={ e => e.stopPropagation() } 133 | /> 134 |

    135 | : 136 |

    { machine.name }

    137 | } 138 |

    { domain }

    139 | 140 | { props.isNext ? 141 | props.onSelect()} /> 142 | : null } 143 | { props.isPrevious ? 144 | props.onSelect()} /> 145 | : null } 146 |
    147 | 148 | { editing ? 149 |
    e.stopPropagation()}> 150 | 156 |
    157 | : 158 |
    159 |

    { formatPath( machine.path ) }

    160 |
    161 | } 162 |
    163 | 164 | { props.selected ? ( 165 | editing ? ( 166 | this.onEdit({ config }) } 170 | onDelete={ props.onDelete } 171 | onDismiss={ () => this.onDismiss() } 172 | onRefresh={ () => { 173 | props.onRefresh(); 174 | this.onDismiss(); 175 | }} 176 | onSave={ () => this.onSave() } 177 | /> 178 | ) : ( 179 | this.onStartEditing() } 183 | onRun={ ( ...args ) => props.onRun(...args) } 184 | onRefresh={ props.onRefresh } 185 | /> 186 | ) 187 | ) : null } 188 |
    189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/Installer/Downloads.js: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | import { remote } from 'electron'; 3 | import plist from 'plist'; 4 | import React from 'react'; 5 | import { connect } from 'react-redux'; 6 | 7 | import Button from '../Button'; 8 | import { addDownloadProgress, resetDownload, setDownloadTotal, setStatus } from '../lib/actions/install'; 9 | import download from '../lib/download'; 10 | import formatPath from '../lib/formatPath'; 11 | import Downloader from './Downloader'; 12 | import DownloadStatus from './DownloadStatus'; 13 | 14 | import './Downloads.css'; 15 | 16 | import VagrantLogo from './Vagrant.png'; 17 | import VirtualBoxLogo from './VirtualBox.png'; 18 | 19 | const downloadPath = remote.app.getPath('userData'); 20 | const sum = (a, b) => (a || 0) + (b || 0); 21 | const apps = { 22 | vagrant: { 23 | url: 'https://releases.hashicorp.com/vagrant/1.8.6/vagrant_1.8.6.dmg', 24 | path: 'Vagrant.dmg', 25 | package: 'Vagrant.pkg', 26 | }, 27 | virtualbox: { 28 | url: 'http://download.virtualbox.org/virtualbox/5.1.10/VirtualBox-5.1.10-112026-OSX.dmg', 29 | path: 'VirtualBox.dmg', 30 | package: 'VirtualBox.pkg', 31 | }, 32 | }; 33 | 34 | const runInstaller = (path, pkgName) => { 35 | return new Promise((resolve, reject) => { 36 | const process = spawn( 'hdiutil', [ 'attach', path, '-plist' ], { 37 | cwd: downloadPath, 38 | }); 39 | let data = { stdout: '', stderr: '' }; 40 | process.stdout.on( 'data', bytes => { 41 | data.stdout += bytes; 42 | }); 43 | process.stderr.on( 'data', bytes => { 44 | data.stderr += bytes; 45 | }); 46 | process.on('close', code => { 47 | if (code !== 0) { 48 | console.log( data ); 49 | console.log( code ); 50 | throw code; 51 | } 52 | 53 | const obj = plist.parse( data.stdout ); 54 | 55 | // Find mount. 56 | const entity = obj['system-entities'].find(obj => obj['content-hint'] === 'Apple_HFS'); 57 | const mount = entity['mount-point']; 58 | const process = spawn( 'open', [ '-W', mount + '/' + pkgName ] ); 59 | process.on( 'close', code => { 60 | resolve(); 61 | console.log( path, code ); 62 | }); 63 | }); 64 | }); 65 | } 66 | 67 | class Downloads extends React.Component { 68 | onDownload() { 69 | const { dispatch } = this.props; 70 | const { installed } = this.props.installer; 71 | 72 | Object.keys(apps).forEach(app => { 73 | if ( app in installed && installed[ app ] ) { 74 | console.log( app, 'already installed' ); 75 | return; 76 | } 77 | 78 | let { path, url } = apps[app]; 79 | 80 | dispatch( resetDownload( app ) ); 81 | console.log( 'download', app ); 82 | download( downloadPath + '/' + path, url, response => { 83 | dispatch( setDownloadTotal( app, parseInt( response.headers[ 'content-length'], 10 ) ) ); 84 | response.on( 'data', chunk => dispatch( addDownloadProgress( app, chunk.length ) ) ); 85 | }); 86 | }); 87 | } 88 | 89 | render() { 90 | const { dispatch } = this.props; 91 | const { downloadProgress, downloadTotal, installed } = this.props.installer; 92 | 93 | const downloaded = Object.keys(apps).map(key => downloadProgress[key]).reduce(sum, 0) 94 | const total = Object.keys(apps).map(key => downloadTotal[key]).reduce(sum, 0); 95 | 96 | const completed = total > 0 && total === downloaded; 97 | const status = total > 0 ? 'downloading' : 'waiting'; 98 | const done = installed.vagrant && installed.virtualbox; 99 | 100 | return
  • 101 |
    102 |

    Requirements

    103 |
    104 | 105 |

    Before we can get started, Chassis has some dependencies that 106 | need to be installed on your system. We can download and 107 | install those for you:

    108 | 109 | { ! done && ! completed ? 110 | this.onDownload() } 115 | /> 116 | : null } 117 | 118 |
      119 |
    • 120 |
      121 | 125 |
      126 |
      127 |

      VirtualBox

      128 |

      VirtualBox runs Chassis virtual machines.

      129 | 130 | { 135 | runInstaller('VirtualBox.dmg', 'VirtualBox.pkg') 136 | .then( () => dispatch( setStatus( 'virtualbox', true ) ) ) 137 | }} 138 | /> 139 |
      140 |
    • 141 |
    • 142 |
      143 | 147 |
      148 |
      149 |

      Vagrant

      150 |

      Vagrant is the underlying tool used to manage your Chassis boxes.

      151 | 152 | { 157 | runInstaller( 'Vagrant.dmg', 'Vagrant.pkg' ) 158 | .then( () => dispatch( setStatus( 'vagrant', true ) ) ) 159 | }} 160 | /> 161 |
      162 |
    • 163 |
    164 | 165 | { done ? 166 |

    167 | : 168 |
    169 |

    These will be downloaded to { formatPath( downloadPath ) }.

    170 |

    171 |
    172 | } 173 |
  • ; 174 | } 175 | } 176 | 177 | export default connect(state => state)(Downloads); 178 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | // Do this as the first thing so that any code reading it knows the right env. 2 | process.env.NODE_ENV = 'production'; 3 | 4 | // Load environment variables from .env file. Surpress warnings using silent 5 | // if this file is missing. dotenv will never modify any environment variables 6 | // that have already been set. 7 | // https://github.com/motdotla/dotenv 8 | require('dotenv').config({silent: true}); 9 | 10 | var chalk = require('chalk'); 11 | var fs = require('fs-extra'); 12 | var path = require('path'); 13 | var filesize = require('filesize'); 14 | var gzipSize = require('gzip-size').sync; 15 | var rimrafSync = require('rimraf').sync; 16 | var webpack = require('webpack'); 17 | var config = require('../config/webpack.config.prod'); 18 | var paths = require('../config/paths'); 19 | var checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 20 | var recursive = require('recursive-readdir'); 21 | var stripAnsi = require('strip-ansi'); 22 | 23 | // Warn and crash if required files are missing 24 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 25 | process.exit(1); 26 | } 27 | 28 | // Input: /User/dan/app/build/static/js/main.82be8.js 29 | // Output: /static/js/main.js 30 | function removeFileNameHash(fileName) { 31 | return fileName 32 | .replace(paths.appBuild, '') 33 | .replace(/\/?(.*)(\.\w+)(\.js|\.css)/, (match, p1, p2, p3) => p1 + p3); 34 | } 35 | 36 | // Input: 1024, 2048 37 | // Output: "(+1 KB)" 38 | function getDifferenceLabel(currentSize, previousSize) { 39 | var FIFTY_KILOBYTES = 1024 * 50; 40 | var difference = currentSize - previousSize; 41 | var fileSize = !Number.isNaN(difference) ? filesize(difference) : 0; 42 | if (difference >= FIFTY_KILOBYTES) { 43 | return chalk.red('+' + fileSize); 44 | } else if (difference < FIFTY_KILOBYTES && difference > 0) { 45 | return chalk.yellow('+' + fileSize); 46 | } else if (difference < 0) { 47 | return chalk.green(fileSize); 48 | } else { 49 | return ''; 50 | } 51 | } 52 | 53 | // First, read the current file sizes in build directory. 54 | // This lets us display how much they changed later. 55 | recursive(paths.appBuild, (err, fileNames) => { 56 | var previousSizeMap = (fileNames || []) 57 | .filter(fileName => /\.(js|css)$/.test(fileName)) 58 | .reduce((memo, fileName) => { 59 | var contents = fs.readFileSync(fileName); 60 | var key = removeFileNameHash(fileName); 61 | memo[key] = gzipSize(contents); 62 | return memo; 63 | }, {}); 64 | 65 | // Remove all content but keep the directory so that 66 | // if you're in it, you don't end up in Trash 67 | rimrafSync(paths.appBuild + '/*'); 68 | 69 | // Start the webpack build 70 | build(previousSizeMap); 71 | 72 | // Merge with the public folder 73 | copyPublicFolder(); 74 | }); 75 | 76 | // Print a detailed summary of build files. 77 | function printFileSizes(stats, previousSizeMap) { 78 | var assets = stats.toJson().assets 79 | .filter(asset => /\.(js|css)$/.test(asset.name)) 80 | .map(asset => { 81 | var fileContents = fs.readFileSync(paths.appBuild + '/' + asset.name); 82 | var size = gzipSize(fileContents); 83 | var previousSize = previousSizeMap[removeFileNameHash(asset.name)]; 84 | var difference = getDifferenceLabel(size, previousSize); 85 | return { 86 | folder: path.join('build', path.dirname(asset.name)), 87 | name: path.basename(asset.name), 88 | size: size, 89 | sizeLabel: filesize(size) + (difference ? ' (' + difference + ')' : '') 90 | }; 91 | }); 92 | assets.sort((a, b) => b.size - a.size); 93 | var longestSizeLabelLength = Math.max.apply(null, 94 | assets.map(a => stripAnsi(a.sizeLabel).length) 95 | ); 96 | assets.forEach(asset => { 97 | var sizeLabel = asset.sizeLabel; 98 | var sizeLength = stripAnsi(sizeLabel).length; 99 | if (sizeLength < longestSizeLabelLength) { 100 | var rightPadding = ' '.repeat(longestSizeLabelLength - sizeLength); 101 | sizeLabel += rightPadding; 102 | } 103 | console.log( 104 | ' ' + sizeLabel + 105 | ' ' + chalk.dim(asset.folder + path.sep) + chalk.cyan(asset.name) 106 | ); 107 | }); 108 | } 109 | 110 | // Create the production build and print the deployment instructions. 111 | function build(previousSizeMap) { 112 | console.log('Creating an optimized production build...'); 113 | webpack(config).run((err, stats) => { 114 | if (err) { 115 | console.error('Failed to create a production build. Reason:'); 116 | console.error(err.message || err); 117 | process.exit(1); 118 | } 119 | 120 | console.log(chalk.green('Compiled successfully.')); 121 | console.log(); 122 | 123 | console.log('File sizes after gzip:'); 124 | console.log(); 125 | printFileSizes(stats, previousSizeMap); 126 | console.log(); 127 | 128 | var openCommand = process.platform === 'win32' ? 'start' : 'open'; 129 | var homepagePath = require(paths.appPackageJson).homepage; 130 | var publicPath = config.output.publicPath; 131 | if (homepagePath && homepagePath.indexOf('.github.io/') !== -1) { 132 | // "homepage": "http://user.github.io/project" 133 | console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.'); 134 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); 135 | console.log(); 136 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); 137 | console.log('To publish it at ' + chalk.green(homepagePath) + ', run:'); 138 | console.log(); 139 | console.log(' ' + chalk.cyan('git') + ' commit -am ' + chalk.yellow('"Save local changes"')); 140 | console.log(' ' + chalk.cyan('git') + ' checkout -B gh-pages'); 141 | console.log(' ' + chalk.cyan('git') + ' add -f build'); 142 | console.log(' ' + chalk.cyan('git') + ' commit -am ' + chalk.yellow('"Rebuild website"')); 143 | console.log(' ' + chalk.cyan('git') + ' filter-branch -f --prune-empty --subdirectory-filter build'); 144 | console.log(' ' + chalk.cyan('git') + ' push -f origin gh-pages'); 145 | console.log(' ' + chalk.cyan('git') + ' checkout -'); 146 | console.log(); 147 | } else if (publicPath !== '/') { 148 | // "homepage": "http://mywebsite.com/project" 149 | console.log('The project was built assuming it is hosted at ' + chalk.green(publicPath) + '.'); 150 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); 151 | console.log(); 152 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); 153 | console.log(); 154 | } else { 155 | // no homepage or "homepage": "http://mywebsite.com" 156 | console.log('The project was built assuming it is hosted at the server root.'); 157 | if (homepagePath) { 158 | // "homepage": "http://mywebsite.com" 159 | console.log('You can control this with the ' + chalk.green('homepage') + ' field in your ' + chalk.cyan('package.json') + '.'); 160 | console.log(); 161 | } else { 162 | // no homepage 163 | console.log('To override this, specify the ' + chalk.green('homepage') + ' in your ' + chalk.cyan('package.json') + '.'); 164 | console.log('For example, add this to build it for GitHub Pages:') 165 | console.log(); 166 | console.log(' ' + chalk.green('"homepage"') + chalk.cyan(': ') + chalk.green('"http://myname.github.io/myapp"') + chalk.cyan(',')); 167 | console.log(); 168 | } 169 | console.log('The ' + chalk.cyan('build') + ' folder is ready to be deployed.'); 170 | console.log('You may also serve it locally with a static server:') 171 | console.log(); 172 | console.log(' ' + chalk.cyan('npm') + ' install -g pushstate-server'); 173 | console.log(' ' + chalk.cyan('pushstate-server') + ' build'); 174 | console.log(' ' + chalk.cyan(openCommand) + ' http://localhost:9000'); 175 | console.log(); 176 | } 177 | }); 178 | } 179 | 180 | function copyPublicFolder() { 181 | fs.copySync(paths.appPublic, paths.appBuild, { 182 | dereference: true, 183 | filter: file => file !== paths.appHtml 184 | }); 185 | } 186 | -------------------------------------------------------------------------------- /config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var autoprefixer = require('autoprefixer'); 3 | var webpack = require('webpack'); 4 | var findCacheDir = require('find-cache-dir'); 5 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 | var CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); 8 | var InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); 9 | var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); 10 | var getClientEnvironment = require('./env'); 11 | var paths = require('./paths'); 12 | 13 | // Webpack uses `publicPath` to determine where the app is being served from. 14 | // In development, we always serve from the root. This makes config easier. 15 | var publicPath = '/'; 16 | // `publicUrl` is just like `publicPath`, but we will provide it to our app 17 | // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript. 18 | // Omit trailing shlash as %PUBLIC_PATH%/xyz looks better than %PUBLIC_PATH%xyz. 19 | var publicUrl = ''; 20 | // Get enrivonment variables to inject into our app. 21 | var env = getClientEnvironment(publicUrl); 22 | 23 | // This is the development configuration. 24 | // It is focused on developer experience and fast rebuilds. 25 | // The production configuration is different and lives in a separate file. 26 | module.exports = { 27 | // This makes the bundle appear split into separate modules in the devtools. 28 | // We don't use source maps here because they can be confusing: 29 | // https://github.com/facebookincubator/create-react-app/issues/343#issuecomment-237241875 30 | // You may want 'cheap-module-source-map' instead if you prefer source maps. 31 | devtool: 'eval', 32 | 33 | // Add Electron as the target. 34 | target: 'electron-renderer', 35 | 36 | // These are the "entry points" to our application. 37 | // This means they will be the "root" imports that are included in JS bundle. 38 | // The first two entry points enable "hot" CSS and auto-refreshes for JS. 39 | entry: { 40 | main: [ 41 | // Include an alternative client for WebpackDevServer. A client's job is to 42 | // connect to WebpackDevServer by a socket and get notified about changes. 43 | // When you save a file, the client will either apply hot updates (in case 44 | // of CSS changes), or refresh the page (in case of JS changes). When you 45 | // make a syntax error, this client will display a syntax error overlay. 46 | // Note: instead of the default WebpackDevServer client, we use a custom one 47 | // to bring better experience for Create React App users. You can replace 48 | // the line below with these two lines if you prefer the stock client: 49 | // require.resolve('webpack-dev-server/client') + '?/', 50 | // require.resolve('webpack/hot/dev-server'), 51 | require.resolve('react-dev-utils/webpackHotDevClient'), 52 | // We ship a few polyfills by default: 53 | require.resolve('./polyfills'), 54 | // Finally, this is your app's code: 55 | paths.appIndexJs 56 | // We include the app code last so that if there is a runtime error during 57 | // initialization, it doesn't blow up the WebpackDevServer client, and 58 | // changing JS code would still trigger a refresh. 59 | ], 60 | about: [ 61 | require.resolve('react-dev-utils/webpackHotDevClient'), 62 | require.resolve('./polyfills'), 63 | paths.appAboutJs, 64 | ] 65 | }, 66 | output: { 67 | // Next line is not used in dev but WebpackDevServer crashes without it: 68 | path: paths.appBuild, 69 | // Add /* filename */ comments to generated require()s in the output. 70 | pathinfo: true, 71 | // This does not produce a real file. It's just the virtual path that is 72 | // served by WebpackDevServer in development. This is the JS bundle 73 | // containing code from all our entry points, and the Webpack runtime. 74 | filename: 'static/js/[name].js', 75 | // This is the URL that app is served from. We use "/" in development. 76 | publicPath: publicPath 77 | }, 78 | resolve: { 79 | // This allows you to set a fallback for where Webpack should look for modules. 80 | // We read `NODE_PATH` environment variable in `paths.js` and pass paths here. 81 | // We use `fallback` instead of `root` because we want `node_modules` to "win" 82 | // if there any conflicts. This matches Node resolution mechanism. 83 | // https://github.com/facebookincubator/create-react-app/issues/253 84 | fallback: paths.nodePaths, 85 | // These are the reasonable defaults supported by the Node ecosystem. 86 | // We also include JSX as a common component filename extension to support 87 | // some tools, although we do not recommend using it, see: 88 | // https://github.com/facebookincubator/create-react-app/issues/290 89 | extensions: ['.js', '.json', '.jsx', ''], 90 | alias: { 91 | // Support React Native Web 92 | // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ 93 | 'react-native': 'react-native-web', 94 | 'spawn-sync': path.join( paths.appSrc, 'lib', 'spawn-sync.js' ), 95 | } 96 | }, 97 | 98 | module: { 99 | // First, run the linter. 100 | // It's important to do this before Babel processes the JS. 101 | preLoaders: [ 102 | { 103 | test: /\.(js|jsx)$/, 104 | loader: 'eslint', 105 | include: paths.appSrc, 106 | } 107 | ], 108 | loaders: [ 109 | // Process JS with Babel. 110 | { 111 | test: /\.(js|jsx)$/, 112 | include: paths.appSrc, 113 | loader: 'babel', 114 | query: { 115 | 116 | // This is a feature of `babel-loader` for webpack (not Babel itself). 117 | // It enables caching results in ./node_modules/.cache/react-scripts/ 118 | // directory for faster rebuilds. We use findCacheDir() because of: 119 | // https://github.com/facebookincubator/create-react-app/issues/483 120 | cacheDirectory: findCacheDir({ 121 | name: 'react-scripts' 122 | }) 123 | } 124 | }, 125 | // "postcss" loader applies autoprefixer to our CSS. 126 | // "css" loader resolves paths in CSS and adds assets as dependencies. 127 | // "style" loader turns CSS into JS modules that inject