}
39 | title="Title"
40 | />
41 | ))
42 | .add( 'with children', () => (
43 |
46 | ))
47 | .add( 'with everything', () => (
48 |
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 | onRemove( index ) }
37 | >Remove
38 | : null }
39 |
40 | )}
41 |
Add
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 | Download and Install
42 |
;
43 |
44 | default:
45 | return ;
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 | Text
28 |
29 |
30 | Text
31 |
32 |
33 | ))
34 | .add( 'noborder', () => (
35 |
36 |
37 | Text
38 |
39 |
40 | Text
41 |
42 |
43 | ))
44 | .add( 'tiny', () => (
45 |
46 |
47 | Text
48 |
49 |
50 | Text
51 |
52 |
53 | ))
54 | .add( 'icon', () => (
55 |
56 |
57 | Text
58 |
59 |
60 | Text
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 |
48 | Name:
49 | props.onChange({ name: e.target.value }) }
53 | />
54 |
55 |
56 |
Chassis Directory:
57 |
60 |
61 |
62 |
63 | { buttonText }
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 |
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 |
62 |
63 | { selected.length > 0 ?
64 |
65 | Importing { selected.length } { selected.length === 1 ? "box" : "boxes" }.
66 | {}}
70 | >Import
71 |
72 | :
73 |
74 | Skip Import
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 | dispatch( ui.undo.action ) }
23 | >Undo
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 | dispatch(showModal('create'))}
53 | >Add your first box
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 | dispatch(reset())}
38 | >Reset Chassis Desktop
39 | this.setState({ showRealReset: false }) }
42 | >Cancel
43 |
44 |
;
45 | }
46 |
47 | return
48 |
56 |
57 |
58 |
59 | Enable keyboard shortcut overlay
60 | dispatch( setPreference( 'showShortcuts', e.target.checked ) ) }
65 | />
66 |
67 |
68 |
69 |
70 | this.setState({ showRealReset: true })}
73 | >Reset Chassis Desktop
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 | openBrowser( 'https://github.com/Chassis/Desktop/issues' ) }
55 | >
56 | Feedback
57 |
58 | dispatch(showModal('settings')) }
64 | >
65 | Settings
66 |
67 | dispatch(showModal('create')) }
73 | >
74 | Add…
75 |
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 | this.setState({ showingConsole: !showingConsole }) }
99 | >{ showingConsole ? "Collapse" : "Expand" }
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; // props.onRefresh()}>Refresh ;
12 | switch (props.status) {
13 | case 'not_created':
14 | case 'poweroff':
15 | status = Status: Off { refreshButton }
16 | mainActions =
17 | props.onLaunch()}>Launch
18 |
;
19 | break;
20 |
21 | case 'running':
22 | status = Status: Running { refreshButton }
;
23 | mainActions =
24 | props.onLaunch()}>Launch
25 | props.onHalt()}>Stop
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 = Cancel
;
37 | break;
38 |
39 | case 'halting':
40 | status = Status: Halting
;
41 | // mainActions = Cancel
;
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 | props.onEdit()}
64 | >Settings
65 |
66 |
67 | { props.status === 'running' && domain ?
68 | openBrowser(`http://${domain}/`)}
73 | >Open
74 | : null }
75 |
76 | props.onFinder()}
81 | >Reveal
82 | props.onTerminal()}
87 | >Terminal
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 ;
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 | Back
124 | :
125 | Cancel
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 | this.onSave() }
155 | >{ empty( this.getChanges() ) ? "Done" : "Save" }
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 |
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 | Next
167 | :
168 |
169 |
These will be downloaded to { formatPath( downloadPath ) }.
170 |
Skip
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