├── .babelrc ├── test ├── mocha.opts ├── domain │ ├── environmentVariable.spec.js │ ├── service.restartPolicy.spec.js │ ├── baseImage.spec.js │ ├── service.environmentVariables.spec.js │ ├── service.portMapping.spec.js │ └── service.image.spec.js ├── compose.loader │ ├── ComposeLoaderV2.spec.js │ └── parsedYaml.json ├── exporter │ └── shell │ │ └── docker-service.js └── utils │ └── environmentVariable.spec.js ├── .travis.yml ├── .gitignore ├── icon.png ├── src ├── css │ ├── _variables.scss │ ├── _statusbar.scss │ ├── _main.panel.scss │ ├── _buttons.scss │ ├── _content.panel.scss │ ├── _typeahead.scss │ ├── _service-list.scss │ ├── _left.panel.scss │ ├── editor.main.scss │ └── _form.scss ├── utils │ ├── index.js │ ├── uuid.js │ ├── environmentVariable.js │ └── randomNameGenerator.js ├── domain │ ├── exception │ │ └── internalPortAlreadyInUseException.js │ ├── index.js │ ├── restartPolicy.js │ ├── environmentVariable.js │ ├── baseImage.js │ ├── portMapping.js │ └── service.js ├── constants │ └── index.js ├── reducers │ ├── reducerRegistry.js │ └── index.js ├── containers │ ├── StatusBarPanel.js │ ├── App.js │ ├── ContentPanel.js │ ├── GlobalEnvVariables.js │ └── LeftPanel.js ├── components │ ├── ServiceNameInputField.js │ ├── ServiceListItem.js │ ├── ServiceList.js │ ├── RestartPolicyInputField.js │ ├── ServiceDetails.js │ ├── GlobalEnvInputFields.js │ ├── ServiceEnvInputFields.js │ ├── ImageInputField.js │ ├── EnvInputField.js │ └── PortsInputField.js ├── actions │ └── index.js ├── index.js ├── js │ ├── compose.loader.v2.js │ └── compose.loader.js ├── ipc │ └── index.js ├── exporter │ └── shell │ │ └── docker-service.js └── index.html ├── doc ├── screenshot_env.png └── screenshot_service.png ├── menu ├── index.js ├── help.js ├── edit.js └── file.js ├── i18n.js ├── locales └── de.json ├── main.js ├── README.md ├── package.json ├── ipc.js └── gulpfile.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: ["react", "es2015"] 3 | } -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-core/register 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "6" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | node_modules/ 3 | .idea/ 4 | dist/ 5 | releases/ -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-arcs/docker-compose-editor/HEAD/icon.png -------------------------------------------------------------------------------- /src/css/_variables.scss: -------------------------------------------------------------------------------- 1 | $color1: #1E1E1E; 2 | $color2: #252526; 3 | $color3: #333333; -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export * from './randomNameGenerator'; 2 | export * from './uuid'; -------------------------------------------------------------------------------- /doc/screenshot_env.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-arcs/docker-compose-editor/HEAD/doc/screenshot_env.png -------------------------------------------------------------------------------- /doc/screenshot_service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-arcs/docker-compose-editor/HEAD/doc/screenshot_service.png -------------------------------------------------------------------------------- /src/domain/exception/internalPortAlreadyInUseException.js: -------------------------------------------------------------------------------- 1 | export class InternalPortAlreadyInUseException { 2 | } 3 | export class ExternalPortAlreadyInUseException { 4 | } -------------------------------------------------------------------------------- /src/css/_statusbar.scss: -------------------------------------------------------------------------------- 1 | .statusbar { 2 | background: #68217a; 3 | padding: 2px 7px; 4 | text-align: right; 5 | } 6 | 7 | .statusbar > span { 8 | margin: 0 0 0 15px; 9 | } -------------------------------------------------------------------------------- /src/domain/index.js: -------------------------------------------------------------------------------- 1 | export * from './service'; 2 | export * from './restartPolicy'; 3 | export * from './portMapping'; 4 | export * from './baseImage'; 5 | export * from './environmentVariable'; 6 | -------------------------------------------------------------------------------- /menu/index.js: -------------------------------------------------------------------------------- 1 | const {Menu} = require('electron'); 2 | 3 | const template = [ 4 | require('./file'), 5 | require('./help') 6 | ]; 7 | 8 | const menu = Menu.buildFromTemplate(template); 9 | Menu.setApplicationMenu(menu); -------------------------------------------------------------------------------- /src/utils/uuid.js: -------------------------------------------------------------------------------- 1 | export function generateUUID() { 2 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 3 | var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 4 | return v.toString(16); 5 | }); 6 | } -------------------------------------------------------------------------------- /src/css/_main.panel.scss: -------------------------------------------------------------------------------- 1 | .main-panel { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100%; 5 | } 6 | 7 | .main-panel > main { 8 | display: flex; 9 | flex: 1 0 0; 10 | } 11 | 12 | .main-panel { 13 | background-color: $color1; 14 | flex: 1 0 0; 15 | } -------------------------------------------------------------------------------- /src/css/_buttons.scss: -------------------------------------------------------------------------------- 1 | .btn { 2 | display: block; 3 | padding: 4px 8px; 4 | text-align: center; 5 | background-color: $color1; 6 | color: #fff; 7 | text-decoration: none; 8 | margin: 8px 0; 9 | 10 | &.btn-primary { 11 | background-color: #0e639c; 12 | 13 | &:hover { 14 | background-color: #006bb3; 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/css/_content.panel.scss: -------------------------------------------------------------------------------- 1 | .content-panel { 2 | display: flex; 3 | flex: 1 0 0; 4 | overflow: hidden; 5 | } 6 | 7 | .content { 8 | flex: 1 0 0; 9 | padding: 5px 15px; 10 | overflow: auto; 11 | 12 | background-position: center center; 13 | background-repeat: no-repeat; 14 | 15 | h1 { 16 | margin: 0 0 16px; 17 | padding: 0; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /menu/help.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | const _ = require('../i18n'); 3 | 4 | module.exports = { 5 | label: _('menu.help.label'), 6 | submenu: [ 7 | { 8 | label: _('menu.help.info.label') + '...', 9 | click () { 10 | electron.shell.openExternal('http://www.codearcs.de') 11 | } 12 | } 13 | ] 14 | }; 15 | -------------------------------------------------------------------------------- /test/domain/environmentVariable.spec.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | import {EnvironmentVariable} from "../../src/domain"; 4 | 5 | describe('EnvironmentVariable', function () { 6 | it('should override toJSON.', () => { 7 | const envVar = EnvironmentVariable.create("A", 1); 8 | expect(JSON.stringify(envVar)).to.equal('{"_key":"A","_value":1}'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/css/_typeahead.scss: -------------------------------------------------------------------------------- 1 | .twitter-typeahead { 2 | display: flex !important; 3 | flex: 1 0 0; 4 | } 5 | 6 | .tt-menu { 7 | width: 100%; 8 | background-color: $color3; 9 | color: #fff; 10 | border: 1px solid #fff; 11 | margin-top: -1px; 12 | } 13 | 14 | .tt-selectable { 15 | padding: 5px; 16 | cursor: pointer; 17 | 18 | &:hover { 19 | background-color: rgba(255, 255, 255, .2); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /i18n.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | const app = electron.app; 3 | 4 | module.exports = function(what) { 5 | let strings; 6 | try { 7 | strings = require(`./locales/${locale}.json`); 8 | } catch(e) { 9 | strings = require(`./locales/de.json`); 10 | } 11 | 12 | what.split('.').forEach(p => strings = (strings[p] || {})); 13 | return (typeof strings === 'string') ? strings : what; 14 | }; -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | export const ADD_SERVICE = 'ADD_SERVICE'; 2 | export const SHOW_SERVICE_DETAILS = 'SHOW_SERVICE_DETAILS'; 3 | export const OPEN_FILE = 'OPEN_FILE'; 4 | export const UPDATE_SERVICE = 'UPDATE_SERVICE'; 5 | export const UPDATE_ENV_VARIABLE = 'UPDATE_ENV_VARIABLE'; 6 | export const ADD_ENV_VARIABLE = 'ADD_ENV_VARIABLE'; 7 | export const DELETE_ENV_VARIABLE = 'DELETE_ENV_VARIABLE'; 8 | export const IMPORT_COMPOSE_FILE = 'IMPORT_COMPOSE_FILE'; 9 | -------------------------------------------------------------------------------- /src/reducers/reducerRegistry.js: -------------------------------------------------------------------------------- 1 | export class ReducerRegistry { 2 | constructor() { 3 | this.reducer = {}; 4 | } 5 | 6 | register(actionType, fn) { 7 | this.reducer[actionType] = fn; 8 | } 9 | 10 | getReducer(actionType) { 11 | return this.reducer[actionType] || ((state) => state); 12 | } 13 | 14 | execute(actionType, state, action) { 15 | return this.getReducer(actionType)(state, action); 16 | } 17 | } -------------------------------------------------------------------------------- /menu/edit.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | label: 'Edit', 3 | submenu: [ 4 | { 5 | role: 'undo' 6 | }, 7 | { 8 | role: 'redo' 9 | }, 10 | { 11 | type: 'separator' 12 | }, 13 | { 14 | role: 'cut' 15 | }, 16 | { 17 | role: 'copy' 18 | }, 19 | { 20 | role: 'paste' 21 | }, 22 | { 23 | role: 'pasteandmatchstyle' 24 | }, 25 | { 26 | role: 'delete' 27 | }, 28 | { 29 | role: 'selectall' 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /src/css/_service-list.scss: -------------------------------------------------------------------------------- 1 | .service-list-panel { 2 | flex: 1 0 0; 3 | overflow-y: auto; 4 | } 5 | 6 | .service-list { 7 | list-style: none; 8 | padding: 0; 9 | margin: 0; 10 | width: 100%; 11 | } 12 | 13 | .service-list-item { 14 | display: flex; 15 | background-color: rgba(0, 0, 0, 0.4); 16 | margin-bottom: 1px; 17 | padding: 3px 8px; 18 | 19 | &.inactive { 20 | font-style: italic; 21 | color: rgba(#ffffff, 0.2); 22 | } 23 | 24 | a { 25 | flex: 1 0 0; 26 | color: inherit; 27 | text-decoration: none; 28 | } 29 | 30 | .icon { 31 | padding-right: 8px; 32 | } 33 | 34 | input { 35 | align-self: flex-end; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/domain/restartPolicy.js: -------------------------------------------------------------------------------- 1 | export const RestartPolicy = {}; 2 | RestartPolicy.NO = "no"; 3 | RestartPolicy.ON_FAILURE = "on-failure"; 4 | RestartPolicy.ALWAYS = "always"; 5 | RestartPolicy.UNLESS_STOPPED = "unless-stopped"; 6 | 7 | /** 8 | * @param key 9 | * @returns {string} 10 | */ 11 | RestartPolicy.get = function (key) { 12 | let value = RestartPolicy[key]; 13 | if (!value) { 14 | for (let a in RestartPolicy) { 15 | const policy = RestartPolicy[a]; 16 | if (typeof policy === 'string' && key === policy) { 17 | value = policy; 18 | break; 19 | } 20 | } 21 | } 22 | 23 | return value || RestartPolicy.NO; 24 | }; 25 | -------------------------------------------------------------------------------- /src/containers/StatusBarPanel.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {connect} from "react-redux"; 3 | 4 | class StatusBarPanel extends React.Component { 5 | render() { 6 | return ( 7 |
8 | {this.props.globalEnvVars} Environment Variables 9 | {this.props.activeServices} Services 10 |
11 | ) 12 | } 13 | } 14 | 15 | function mapStateToProps(state) { 16 | return { 17 | activeServices: (state.app.docker.services || []).length, 18 | globalEnvVars: (state.app.docker.envVars || []).length 19 | } 20 | } 21 | 22 | export default connect(mapStateToProps)(StatusBarPanel) 23 | -------------------------------------------------------------------------------- /src/css/_left.panel.scss: -------------------------------------------------------------------------------- 1 | .left-panel { 2 | background-color: $color3; 3 | flex-basis: 50px; 4 | flex-shrink: 0; 5 | display: flex; 6 | flex-direction: column; 7 | } 8 | 9 | .left-nav-panel { 10 | display: flex; 11 | width: 100%; 12 | 13 | ul { 14 | margin: 0; 15 | padding: 0; 16 | width: 100%; 17 | } 18 | 19 | li { 20 | text-align: center; 21 | 22 | .icon-nav { 23 | margin: 8px 0 8px -10px; 24 | height: 24px; 25 | width: 24px; 26 | fill: rgba(255, 255, 255, 0.5); 27 | 28 | &:hover { 29 | fill: rgba(255, 255, 255, 1); 30 | } 31 | } 32 | } 33 | } 34 | 35 | .left-panel-title { 36 | padding: 3px 8px; 37 | margin: 0; 38 | font-size: 1em; 39 | } -------------------------------------------------------------------------------- /src/components/ServiceNameInputField.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from "react"; 2 | import * as Action from "../actions"; 3 | import {connect} from "react-redux"; 4 | 5 | class ServiceNameInputField extends React.Component { 6 | render() { 7 | return ( 8 | 12 | ) 13 | } 14 | 15 | onChange(event) { 16 | const service = this.props.service; 17 | service.setName(event.target.value); 18 | this.props.dispatch(Action.updateService(service)); 19 | } 20 | } 21 | 22 | export default connect()(ServiceNameInputField); 23 | -------------------------------------------------------------------------------- /src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {connect} from "react-redux"; 3 | import LeftPanel from "./LeftPanel"; 4 | import ContentPanel from "./ContentPanel"; 5 | import StatusBarPanel from "./StatusBarPanel"; 6 | import {IPC} from "../ipc"; 7 | 8 | class App extends React.Component { 9 | render() { 10 | IPC.register(this.props); 11 | return ( 12 |
13 |
14 | 15 | 16 |
17 | 18 |
19 | ) 20 | } 21 | } 22 | 23 | function mapStateToProps(state) { 24 | return state.app; 25 | } 26 | export default connect(mapStateToProps)(App) 27 | -------------------------------------------------------------------------------- /src/containers/ContentPanel.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {connect} from "react-redux"; 3 | 4 | class ContentPanel extends React.Component { 5 | render() { 6 | let sidebar; 7 | if (this.props.sidebar) { 8 | sidebar = ( 9 |
10 | {this.props.sidebar} 11 |
12 | ); 13 | } 14 | 15 | return ( 16 |
17 | {sidebar} 18 |
19 | {this.props.content} 20 |
21 |
22 | ) 23 | } 24 | } 25 | 26 | function mapStateToProps(state) { 27 | return state.app; 28 | 29 | } 30 | export default connect(mapStateToProps)(ContentPanel); 31 | -------------------------------------------------------------------------------- /locales/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu": { 3 | "file": { 4 | "label": "Datei", 5 | "open": { 6 | "label": "Öffnen" 7 | }, 8 | "save": { 9 | "label": "Speichern" 10 | }, 11 | "save_as": { 12 | "label": "Speichern als" 13 | }, 14 | "export": { 15 | "label": "Exportieren", 16 | "compose": { 17 | "label": "Als Compose-Datei exportieren..." 18 | }, 19 | "docker-run": { 20 | "label": "Als docker run commands exportieren..." 21 | }, 22 | "docker-service": { 23 | "label": "Als docker service commands exportieren..." 24 | } 25 | }, 26 | "quit": { 27 | "label": "Beenden" 28 | }, 29 | "import": { 30 | "label": "Importieren..." 31 | } 32 | }, 33 | "help": { 34 | "label": "Hilfe", 35 | "info": { 36 | "label": "Info" 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/domain/environmentVariable.js: -------------------------------------------------------------------------------- 1 | export class EnvironmentVariable { 2 | constructor(args) { 3 | if (args && args.length === 2) { 4 | this._key = args[0]; 5 | this._value = args[1]; 6 | } 7 | } 8 | 9 | static create() { 10 | if (arguments.length === 2) { 11 | const environmentVariable = new EnvironmentVariable(); 12 | environmentVariable.setKey(arguments[0]); 13 | environmentVariable.setValue(arguments[1]); 14 | return environmentVariable; 15 | } 16 | } 17 | 18 | static fromJSON(json) { 19 | return Object.assign(new EnvironmentVariable(), json); 20 | } 21 | 22 | getKey() { 23 | return this._key; 24 | } 25 | 26 | setKey(key) { 27 | this._key = key; 28 | } 29 | 30 | getValue() { 31 | return this._value; 32 | } 33 | 34 | setValue(value) { 35 | this._value = value; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/domain/service.restartPolicy.spec.js: -------------------------------------------------------------------------------- 1 | const should = require('chai').should(); 2 | 3 | import {Service, RestartPolicy} from "../../src/domain"; 4 | 5 | describe('Service: Restart Policy', function () { 6 | beforeEach(() => { 7 | this.service = new Service(); 8 | }); 9 | 10 | it('should set restart policy based on string.', () => { 11 | this.service.setRestartPolicy('ON_FAILURE'); 12 | this.service.getRestartPolicy().should.equal(RestartPolicy.ON_FAILURE); 13 | }); 14 | 15 | it('should set restart policy based on constant.', () => { 16 | this.service.setRestartPolicy(RestartPolicy.ON_FAILURE); 17 | this.service.getRestartPolicy().should.equal(RestartPolicy.ON_FAILURE); 18 | }); 19 | 20 | it('should set default restart policy when invalid key is provided.', () => { 21 | this.service.setRestartPolicy('invalid'); 22 | this.service.getRestartPolicy().should.equal(RestartPolicy.NO); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/components/ServiceListItem.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Link} from "react-router"; 3 | import {connect} from "react-redux"; 4 | import * as Actions from "../actions"; 5 | 6 | class ServiceListItem extends React.Component { 7 | handleChange(event) { 8 | const service = this.props.service; 9 | service.setActive(event.target.checked); 10 | this.props.dispatch(Actions.updateService(service)); 11 | } 12 | 13 | render() { 14 | const clazzName = ["service-list-item"]; 15 | if (!this.props.service.isActive()) { 16 | clazzName.push("inactive") 17 | } 18 | 19 | return ( 20 |
  • 21 | {this.props.service._name} 22 | 23 |
  • 24 | ) 25 | } 26 | } 27 | export default connect()(ServiceListItem) 28 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | const app = electron.app; 3 | 4 | const client = require('electron-connect').client; 5 | const BrowserWindow = electron.BrowserWindow; 6 | let mainWindow; 7 | 8 | function createWindow () { 9 | require('./ipc'); 10 | require('./menu'); 11 | 12 | mainWindow = new BrowserWindow({width: 1024, height: 768, icon: __dirname + '/icon.png'}); 13 | mainWindow.loadURL(`file://${__dirname}/dist/index.html`); 14 | mainWindow.on('closed', function () { 15 | mainWindow = null 16 | }); 17 | 18 | if(process.env.DCE_DEBUG === "true") { 19 | mainWindow.webContents.openDevTools(); 20 | // Connect to server process 21 | client.create(mainWindow); 22 | } 23 | } 24 | 25 | app.on('ready', createWindow); 26 | app.on('window-all-closed', () => { 27 | if (process.platform !== 'darwin') { 28 | app.quit() 29 | } 30 | }); 31 | 32 | app.on('activate', () => { 33 | if (mainWindow === null) { 34 | createWindow() 35 | } 36 | }); 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Compose Editor 2 | 3 | [![Build Status](https://travis-ci.org/code-arcs/docker-compose-editor.svg?branch=master)](https://travis-ci.org/code-arcs/docker-compose-editor) 4 | [![dependencies Status](https://david-dm.org/code-arcs/docker-compose-editor/status.svg)](https://david-dm.org/code-arcs/docker-compose-editor) 5 | 6 | Editing Docker Compose files may be very frustrating when havin a lot of services bundled together. 7 | In the name of simplicity, this Docker Compose Editor is implemented. 8 | Since it is not ready to ship, yet, here is a screenshot for you. 9 | 10 | ## Screenshots 11 | ![View of a single service](./doc/screenshot_service.png) 12 | ![View of global variables](./doc/screenshot_env.png) 13 | 14 | ## 3rd party products used 15 | This project makes use of 3rd party software products which have been implemented 16 | by other great and awesome development teams. 17 | Hence, we want to thank the girls and boys who created the following modules which 18 | are an important part of this tool! 19 | 20 | * ipc 21 | * jQuery 22 | * lodash 23 | * node-zip 24 | * React.js 25 | * Redux 26 | * typeahead.js 27 | * yaml.js 28 | -------------------------------------------------------------------------------- /src/containers/GlobalEnvVariables.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from "react"; 2 | import {connect} from "react-redux"; 3 | import GlobalEnvInputFields from "../components/GlobalEnvInputFields"; 4 | import * as pkg from "../../package.json"; 5 | 6 | class GlobalEnvVariables extends React.Component { 7 | onChange(action) { 8 | this.props.dispatch(action); 9 | } 10 | 11 | render() { 12 | document.title = `${pkg.productName}`; 13 | return ( 14 |
    15 |

    Global Environment Variables

    16 |

    17 | Define environment variables here which you can reuse in services later. E.g. when multiple services 18 | share common environment variables, you can change them here for all services at once. 19 |

    20 | 21 |
    22 | ) 23 | } 24 | } 25 | 26 | function mapStateToProps(state) { 27 | return { 28 | envVars: state.app.docker.envVars 29 | } 30 | } 31 | export default connect(mapStateToProps)(GlobalEnvVariables) 32 | -------------------------------------------------------------------------------- /test/domain/baseImage.spec.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | import {BaseImage} from "../../src/domain"; 4 | 5 | describe('BaseImage', function () { 6 | beforeEach(() => { 7 | this.baseImage = new BaseImage(); 8 | }); 9 | 10 | it('should return type for tag.', () => { 11 | this.baseImage.setImage("ab"); 12 | this.baseImage.setTag("tag"); 13 | expect(this.baseImage.getType()).to.equal(':'); 14 | }); 15 | 16 | it('should return type for digest.', () => { 17 | this.baseImage.setImage("ab"); 18 | this.baseImage.setDigest("digest"); 19 | expect(this.baseImage.getType()).to.equal('@'); 20 | }); 21 | 22 | it('should clean digest or tag when setting the counterpart.', () => { 23 | this.baseImage.setImage("ab"); 24 | this.baseImage.setTag("tag"); 25 | this.baseImage.setDigest("digest"); 26 | expect(this.baseImage.getTag()).to.equal(undefined); 27 | 28 | this.baseImage.setImage("ab"); 29 | this.baseImage.setDigest("digest"); 30 | this.baseImage.setTag("tag"); 31 | expect(this.baseImage.getDigest()).to.equal(undefined); 32 | }) 33 | }); 34 | -------------------------------------------------------------------------------- /src/containers/LeftPanel.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {connect} from "react-redux"; 3 | import {Link} from "react-router"; 4 | 5 | class LeftPanel extends React.Component { 6 | render() { 7 | return ( 8 |
    9 |
    10 | 26 |
    27 |
    28 | ) 29 | } 30 | } 31 | 32 | export default connect()(LeftPanel) 33 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as C from "../constants"; 2 | 3 | export function addService(service) { 4 | return { 5 | type: C.ADD_SERVICE, 6 | payload: service || {} 7 | }; 8 | } 9 | 10 | export function showServiceDetails(service) { 11 | return { 12 | type: C.SHOW_SERVICE_DETAILS, 13 | payload: service 14 | } 15 | } 16 | 17 | export function openFile(data) { 18 | return { 19 | type: C.OPEN_FILE, 20 | payload: data 21 | } 22 | } 23 | 24 | export function updateService(service) { 25 | return { 26 | type: C.UPDATE_SERVICE, 27 | payload: service 28 | } 29 | } 30 | 31 | export function updateEnvVariable(payload) { 32 | return { 33 | type: C.UPDATE_ENV_VARIABLE, 34 | payload: payload 35 | } 36 | } 37 | 38 | export function addEnvVariable() { 39 | return { 40 | type: C.ADD_ENV_VARIABLE 41 | } 42 | } 43 | 44 | export function deleteEnvVariable(idx) { 45 | return { 46 | type: C.DELETE_ENV_VARIABLE, 47 | payload: { 48 | idx: idx 49 | } 50 | } 51 | } 52 | 53 | export function importComposeFile(composeFile) { 54 | return { 55 | type: C.IMPORT_COMPOSE_FILE, 56 | payload: composeFile 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/css/editor.main.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | @import "buttons"; 3 | @import "service-list"; 4 | @import "statusbar"; 5 | @import "left.panel"; 6 | @import "main.panel"; 7 | @import "typeahead"; 8 | @import "content.panel"; 9 | @import "form"; 10 | @import "../../node_modules/react-select/dist/react-select.css"; 11 | 12 | html, body { 13 | height: 100%; 14 | font-family: -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe UI, HelveticaNeue-Light, Ubuntu, Droid Sans, sans-serif; 15 | font-size: 13px; 16 | color: #ffffff; 17 | } 18 | 19 | body { 20 | padding: 0; 21 | margin: 0; 22 | } 23 | 24 | .dce-app { 25 | height: 100%; 26 | } 27 | 28 | .icon { 29 | width: 16px; 30 | height: 16px; 31 | position: relative; 32 | top: 4px; 33 | left: 5px; 34 | fill: #fff; 35 | 36 | &.icon-delete { 37 | flex-basis: 64px; 38 | } 39 | } 40 | 41 | .sidebar { 42 | flex-basis: 200px; 43 | height: 100%; 44 | background-color: $color2; 45 | 46 | h2 { 47 | font-size: 1em; 48 | font-weight: normal; 49 | padding: 0 16px; 50 | } 51 | } 52 | 53 | .active .icon { 54 | fill: #fff !important; 55 | } 56 | 57 | a .icon-delete { 58 | transition-property: translate; 59 | transition-duration: 100ms; 60 | 61 | &:hover { 62 | transform: rotate(90deg); 63 | } 64 | } -------------------------------------------------------------------------------- /src/components/ServiceList.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from "react"; 2 | import ServiceListItem from "./ServiceListItem"; 3 | import {connect} from "react-redux"; 4 | import * as Actions from "../actions"; 5 | import {Service} from "../domain"; 6 | 7 | class ServiceList extends React.Component { 8 | render() { 9 | let services; 10 | if (Array.isArray(this.props.services)) { 11 | 12 | services = this.props.services.map((service, idx) => { 13 | return ( 14 | 15 | ) 16 | }); 17 | } 18 | 19 | return ( 20 |
    21 |

    Services

    22 | 23 | Add Service 24 | 25 | 28 |
    29 | ) 30 | } 31 | 32 | addService() { 33 | this.props.dispatch(Actions.addService(new Service())); 34 | } 35 | } 36 | 37 | function mapStateToProps(state) { 38 | return { 39 | services: state.app.docker.services 40 | }; 41 | } 42 | export default connect(mapStateToProps)(ServiceList) 43 | -------------------------------------------------------------------------------- /src/components/RestartPolicyInputField.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from "react"; 2 | import {connect} from "react-redux"; 3 | import * as Actions from "../actions"; 4 | import {RestartPolicy} from "../domain"; 5 | 6 | class RestartPolicyInputField extends React.Component { 7 | render() { 8 | const policies = Object.keys(RestartPolicy) 9 | .filter(name => typeof RestartPolicy[name] === 'string') 10 | .map((restartPolicyName, idx) => { 11 | return ( 12 | 13 | ) 14 | }); 15 | 16 | return ( 17 |
    18 | 19 | 23 |
    24 | ) 25 | } 26 | 27 | onChange(event) { 28 | const service = this.props.service; 29 | console.log(service); 30 | service.setRestartPolicy(event.target.value); 31 | this.props.dispatch(Actions.updateService(service)); 32 | } 33 | } 34 | export default connect()(RestartPolicyInputField) 35 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {render} from "react-dom"; 3 | import {createStore, combineReducers} from "redux"; 4 | import {Provider} from "react-redux"; 5 | import {Router, Route, hashHistory} from "react-router"; 6 | import {syncHistoryWithStore, routerReducer} from "react-router-redux"; 7 | import {appReducer as app} from "./reducers"; 8 | import App from "./containers/App"; 9 | import ServiceDetails from "./components/ServiceDetails"; 10 | import GlobalEnvVariables from "./containers/GlobalEnvVariables"; 11 | import ServiceList from "./components/ServiceList"; 12 | 13 | const store = createStore( 14 | combineReducers({ 15 | app, 16 | routing: routerReducer 17 | }) 18 | ); 19 | const history = syncHistoryWithStore(hashHistory, store); 20 | 21 | render( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | , 32 | document.getElementById("dce-app-root") 33 | ); 34 | -------------------------------------------------------------------------------- /test/domain/service.environmentVariables.spec.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | import {Service} from "../../src/domain"; 4 | 5 | describe('Service: EnvVars', function () { 6 | beforeEach(() => { 7 | this.service = new Service(); 8 | }); 9 | 10 | it('should set environment variable by key, value.', () => { 11 | this.service.addEnvironmentVariable('a', 1); 12 | expect(this.service.getEnvironmentVariables()[0]).to.eql({_key: 'a', _value: 1}); 13 | }); 14 | 15 | it('should replace environment variable in value.', () => { 16 | this.service.addEnvironmentVariable('A', 'env A!'); 17 | this.service.addEnvironmentVariable('B', 'Value from $A'); 18 | expect(this.service.getEnvironmentVariable('B', true)).to.eql({_key: 'B', _value: 'Value from env A!'}); 19 | }); 20 | 21 | it('should replace environment variables in value.', () => { 22 | this.service.addEnvironmentVariable('A', 'Hello,'); 23 | this.service.addEnvironmentVariable('B', 'I am'); 24 | this.service.addEnvironmentVariable('C', 'a string!'); 25 | this.service.addEnvironmentVariable('D', '$A $B $C'); 26 | expect(this.service.getEnvironmentVariable('D', true)).to.eql({_key: 'D', _value: 'Hello, I am a string!'}); 27 | expect(this.service.getEnvironmentVariable('D')).to.eql({_key: 'D', _value: '$A $B $C'}); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/compose.loader/ComposeLoaderV2.spec.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const ComposeLoaderV2 = require('../../src/js/compose.loader.v2'); 4 | const yaml = require('./parsedYaml.json'); 5 | import {Service, RestartPolicy, BaseImage, PortMapping, EnvironmentVariable} from "../../src/domain"; 6 | 7 | describe('ComposeLoaderV2', function () { 8 | beforeEach(() => { 9 | this.compose = new ComposeLoaderV2(yaml); 10 | }); 11 | 12 | it('should load services.', () => { 13 | const services = this.compose.getServices(); 14 | const firstService = services[0]; 15 | 16 | expect(services.length).to.eql(6); 17 | expect(firstService).to.be.instanceOf(Service); 18 | expect(firstService.getName()).to.eql("apigateway"); 19 | expect(firstService.getBaseImage()).to.eql(new BaseImage("quay.io/gbtec/biccloud-apigateway-sidecar-service")); 20 | expect(firstService.getRestartPolicy()).to.eql(RestartPolicy.UNLESS_STOPPED); 21 | expect(firstService.getPortMappings().length).to.eql(2); 22 | expect(firstService.getPortMappings()[0]).to.eql(new PortMapping(8087, 8080)); 23 | expect(firstService.getPortMappings()[1]).to.eql(new PortMapping(8000, 8000)); 24 | expect(firstService.getEnvironmentVariables().length).to.eql(11); 25 | expect(firstService.getEnvironmentVariables()[0]).to.eql(EnvironmentVariable.create('NODE_ENV', 'production')); 26 | }); 27 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "de.codearcs.docker-compose-editor", 3 | "productName": "Docker Compose Editor", 4 | "version": "0.1.0-alpha.1", 5 | "main": "main.js", 6 | "author": "info@codearcs.de", 7 | "scripts": { 8 | "start": "electron .", 9 | "test": "mocha --recursive", 10 | "serve": "gulp serve", 11 | "release:win": "electron-packager ./dist --platform=win32" 12 | }, 13 | "devDependencies": { 14 | "babel-core": "^6.17.0", 15 | "babel-loader": "^6.2.5", 16 | "babel-plugin-transform-react-jsx": "^6.8.0", 17 | "babel-preset-es2015": "^6.14.0", 18 | "babel-preset-react": "^6.11.1", 19 | "babelify": "^7.3.0", 20 | "chai": "*", 21 | "electron": "^1.3.6", 22 | "electron-connect": "^0.6.0", 23 | "electron-packager": "^8.1.0", 24 | "gulp": "^3.9.1", 25 | "gulp-babel": "^6.1.2", 26 | "gulp-react": "^3.1.0", 27 | "gulp-sass": "^3.1.0", 28 | "gulp-sourcemaps": "^2.2.3", 29 | "mocha": "*", 30 | "run-sequence": "^1.2.2", 31 | "xtend": "^4.0.1" 32 | }, 33 | "dependencies": { 34 | "ipc": "0.0.1", 35 | "jquery": "^3.1.1", 36 | "lodash": "^4.15.0", 37 | "node-zip": "^1.1.1", 38 | "react": "^15.3.1", 39 | "react-dom": "^15.3.1", 40 | "react-redux": "^5.0.1", 41 | "react-router": "^3.0.0", 42 | "react-router-redux": "^4.0.5", 43 | "react-select": "^1.0.0-rc.1", 44 | "redux": "^3.6.0", 45 | "typeahead.js": "^0.11.1", 46 | "yamljs": "^0.2.8" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/js/compose.loader.v2.js: -------------------------------------------------------------------------------- 1 | import {Service} from "../domain/service"; 2 | 'use strict'; 3 | module.exports = class ComposeLoaderV2 { 4 | constructor(yaml, content) { 5 | this.yaml = yaml; 6 | 7 | this.services = []; 8 | this._processServices(); 9 | } 10 | 11 | _processServices() { 12 | for (let name in this.yaml.services) { 13 | const yamlService = this.yaml.services[name]; 14 | const service = new Service(name); 15 | this._processService(service, yamlService); 16 | } 17 | } 18 | 19 | _processService(service, yamlService) { 20 | service.setBaseImage(yamlService.image); 21 | service.setRestartPolicy(yamlService.restart); 22 | this._processServicePorts(service, yamlService); 23 | this._processEnvironmentVariables(service, yamlService); 24 | this.services.push(service); 25 | } 26 | 27 | _processServicePorts(service, yamlService) { 28 | const ports = Array.isArray(yamlService.ports) ? yamlService.ports : []; 29 | ports.forEach(port => service.addPortMapping(port)); 30 | } 31 | 32 | _processEnvironmentVariables(service, yamlService) { 33 | for (let envVarKey in yamlService.environment) { 34 | service.addEnvironmentVariable(envVarKey, yamlService.environment[envVarKey]); 35 | } 36 | } 37 | 38 | getVersion() { 39 | return this.yaml.version; 40 | } 41 | 42 | getServices() { 43 | return this.services; 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /ipc.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const Zip = require('node-zip'); 3 | const electron = require('electron'); 4 | const ipcMain = electron.ipcMain; 5 | const dialog = electron.dialog; 6 | 7 | ipcMain.on('export', function (event, yamlContent) { 8 | const dialogOpts = { 9 | filters: [ 10 | {name: 'Docker-Compose-File', extensions: ['yml', 'yaml']} 11 | ] 12 | }; 13 | const file = dialog.showSaveDialog(dialogOpts); 14 | if (file) { 15 | fs.writeFileSync(file, yamlContent, 'utf8'); 16 | } 17 | }); 18 | 19 | ipcMain.on('export.docker-service', function (event, data) { 20 | const dialogOpts = { 21 | filters: [ 22 | {name: 'Bash-Script', extensions: ['sh']} 23 | ] 24 | }; 25 | const file = dialog.showSaveDialog(dialogOpts); 26 | if (file) { 27 | fs.writeFileSync(file, data, 'utf8'); 28 | } 29 | }); 30 | 31 | ipcMain.on('save', function (event, data) { 32 | const dialogOpts = { 33 | filters: [ 34 | {name: 'DCE-Project', extensions: ['dce']} 35 | ] 36 | }; 37 | 38 | if (!electron.app.currentProjectFile) { 39 | electron.app.currentProjectFile = dialog.showSaveDialog(dialogOpts); 40 | } 41 | 42 | if (electron.app.currentProjectFile) { 43 | const zip = new Zip(); 44 | zip.file('data.json', data); 45 | const zippedData = zip.generate({base64: false, compression: 'DEFLATE'}); 46 | fs.writeFileSync(electron.app.currentProjectFile, zippedData, 'binary'); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /src/css/_form.scss: -------------------------------------------------------------------------------- 1 | .form-group { 2 | display: flex; 3 | flex-direction: column; 4 | margin-bottom: 20px; 5 | 6 | .no-values-panel { 7 | padding: 14px 0; 8 | text-align: center; 9 | border-radius: 7px; 10 | background-color: #252526; 11 | } 12 | 13 | label { 14 | display: block; 15 | font-weight: bold; 16 | padding: 6px 0; 17 | font-size: 1.1em; 18 | } 19 | 20 | input[type=text] { 21 | border: none; 22 | border-bottom: 1px solid #fff; 23 | } 24 | 25 | .separator { 26 | padding: 6px 12px; 27 | } 28 | 29 | &.docker-image { 30 | display: flex; 31 | 32 | .docker-image-tag { 33 | width: 100px; 34 | } 35 | } 36 | } 37 | 38 | .form-control-wrapper { 39 | flex-direction: row; 40 | display: flex; 41 | margin-bottom: 5px; 42 | } 43 | 44 | .form-control { 45 | outline: none; 46 | color: #fff; 47 | background-color: #343434; 48 | display: block; 49 | padding: 6px 12px; 50 | font-size: 14px; 51 | line-height: 1.42857143; 52 | border: none; 53 | border-bottom: 1px solid #ccc; 54 | flex: 1 0 0; 55 | 56 | &.error { 57 | background-color: rgba(#a94442, 0.2); 58 | border-color: #a94442; 59 | } 60 | 61 | &.service-name { 62 | font-size: 20px; 63 | font-weight: bold; 64 | margin: 16px 0; 65 | } 66 | 67 | &.image-type { 68 | flex-basis: 0; 69 | flex-grow: 0; 70 | flex-shrink: 0; 71 | margin: 0 4px; 72 | text-align: center; 73 | padding: 6px 4px; 74 | } 75 | } 76 | 77 | select.form-control { 78 | flex: auto; 79 | } -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const electron = require('electron-connect').server.create(); 3 | const babel = require('gulp-babel'); 4 | const sourcemaps = require('gulp-sourcemaps'); 5 | const runSequence = require('run-sequence'); 6 | const sass = require('gulp-sass'); 7 | 8 | process.env.DCE_DEBUG=true; 9 | 10 | gulp.task('serve', function (callback) { 11 | gulp.watch('main.js', ['electron:restart']); 12 | gulp.watch(['src/**/*.scss', 'src/**/*.jsx', 'src/**/*.js', 'src/**/*.html', 'src/**/*.css', 'src/**/*.png'], ['build:dev']); 13 | 14 | runSequence('electron:start', 'build:dev'); 15 | }); 16 | gulp.task('build:dev', function (callback) { 17 | runSequence('copy:static', 18 | ['jsx2js', 'sass'], 19 | 'electron:reload', 20 | callback); 21 | }); 22 | gulp.task('electron:start', () => electron.start()); 23 | gulp.task('electron:reload', () => electron.reload()); 24 | gulp.task('electron:restart', () => electron.restart()); 25 | gulp.task('copy:static', () => { 26 | gulp.src(['src/**/*.html', 'src/**/*.png']) 27 | .pipe(gulp.dest('dist')); 28 | }); 29 | gulp.task('jsx2js', function () { 30 | return gulp.src(['src/**/*.js']) 31 | .pipe(sourcemaps.init()) 32 | .pipe(babel({ 33 | plugins: ['transform-react-jsx'] 34 | })) 35 | .pipe(sourcemaps.write('.')) 36 | .pipe(gulp.dest('dist')); 37 | }); 38 | gulp.task('sass', function () { 39 | return gulp.src('src/**/*.scss') 40 | .pipe(sass().on('error', sass.logError)) 41 | .pipe(gulp.dest('dist')); 42 | }); -------------------------------------------------------------------------------- /src/components/ServiceDetails.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from "react"; 2 | import {connect} from "react-redux"; 3 | import ImageInputField from "./ImageInputField"; 4 | import RestartPolicyInputField from "./RestartPolicyInputField"; 5 | import PortsInputField from "./PortsInputField"; 6 | import ServiceEnvInputFields from "./ServiceEnvInputFields"; 7 | import ServiceNameInputField from "./ServiceNameInputField"; 8 | import * as pkg from "../../package.json"; 9 | 10 | class ServiceDetails extends React.Component { 11 | render() { 12 | this.service = this.getService(); 13 | document.title = `${pkg.productName} [${this.service._name}]`; 14 | 15 | const style = { 16 | position: 'absolute', 17 | top: 0, 18 | right: 0, 19 | fontSize: 10 20 | }; 21 | 22 | return ( 23 |
    24 | {/*
    */} 25 | {/*
    {JSON.stringify(this.service, null, 2)}
    */} 26 | {/*
    */} 27 | 28 | 29 | 30 | 31 | 32 |
    33 | ) 34 | } 35 | 36 | getService() { 37 | return this.props.services.find(service => service._id === this.props.params.id) || {}; 38 | } 39 | } 40 | 41 | function mapStateToProps(state) { 42 | return { 43 | services: state.app.docker.services 44 | } 45 | } 46 | export default connect(mapStateToProps)(ServiceDetails) 47 | -------------------------------------------------------------------------------- /src/ipc/index.js: -------------------------------------------------------------------------------- 1 | import ComposeLoader from "../js/compose.loader"; 2 | import {ipcRenderer} from "electron"; 3 | import * as Actions from "../actions"; 4 | import * as pkg from "../../package.json"; 5 | import {Service} from "../domain/service"; 6 | import {ShellDockerServiceExporter} from "../exporter/shell/docker-service"; 7 | 8 | export class IPC { 9 | static register(props) { 10 | [ 11 | 'open-file', 12 | 'export', 13 | 'export.docker-service', 14 | 'save', 15 | 'import' 16 | ].forEach(l => ipcRenderer.removeAllListeners(l)) 17 | 18 | ipcRenderer.on('open-file', (event, data) => { 19 | props.dispatch(Actions.openFile(data)); 20 | document.title = pkg.productName; 21 | }); 22 | 23 | ipcRenderer.on('export', () => { 24 | console.log(props); 25 | ipcRenderer.send('export', ComposeLoader.toYaml(props.docker)); 26 | }); 27 | 28 | ipcRenderer.on('export.docker-service', () => { 29 | const s = (props.docker.services) 30 | .filter(s => s.isActive()) 31 | .map(s => Service.fromJSON(s)) 32 | .map(s => ShellDockerServiceExporter.getShellCommand(s, props.docker.envVars, true)) 33 | .join('\n\n'); 34 | ipcRenderer.send('export.docker-service', s); 35 | }); 36 | 37 | ipcRenderer.on('save', () => { 38 | ipcRenderer.send('save', JSON.stringify({ 39 | envVars: props.docker.envVars, 40 | services: props.docker.services 41 | })); 42 | }); 43 | 44 | ipcRenderer.on('import', (event, filename) => { 45 | props.dispatch(Actions.importComposeFile(filename)); 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/GlobalEnvInputFields.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from "react"; 2 | import {connect} from "react-redux"; 3 | import * as Action from "../actions"; 4 | import EnvInputField from "./EnvInputField"; 5 | 6 | class GlobalEnvInputFields extends React.Component { 7 | render() { 8 | const environmentVariables = this.props.envVars; 9 | let envInputs; 10 | if (Array.isArray(environmentVariables) && environmentVariables.length > 0) { 11 | envInputs = environmentVariables.map((variable, idx) => { 12 | return ( 13 | 18 | ); 19 | }); 20 | } else { 21 | envInputs = ( 22 |
    No global environment variables declared.
    23 | ) 24 | } 25 | 26 | return ( 27 |
    28 | 36 | {envInputs} 37 |
    38 | ) 39 | } 40 | 41 | onDelete(idx) { 42 | this.props.dispatch(Action.deleteEnvVariable(idx)); 43 | } 44 | 45 | onChange(idx, variable) { 46 | this.props.dispatch(Action.updateEnvVariable({ 47 | idx: idx, 48 | variable 49 | })); 50 | } 51 | 52 | addEnv() { 53 | this.props.dispatch(Action.addEnvVariable()); 54 | } 55 | } 56 | 57 | function mapStateToScope(state) { 58 | return { 59 | envVars: state.app.docker.envVars 60 | } 61 | } 62 | 63 | export default connect(mapStateToScope)(GlobalEnvInputFields); 64 | -------------------------------------------------------------------------------- /src/domain/baseImage.js: -------------------------------------------------------------------------------- 1 | export class BaseImage { 2 | constructor(image) { 3 | if (image) { 4 | if (image.indexOf(':') !== -1) { 5 | const imageName = image.split(':'); 6 | this._image = imageName[0]; 7 | this._tag = imageName[1] || 'latest'; 8 | } 9 | else if (image.indexOf('@') !== -1) { 10 | const imageName = image.split('@'); 11 | this._image = imageName[0]; 12 | this._digest = imageName[1]; 13 | if (this._digest.trim() === '') { 14 | this._tag = 'latest'; 15 | this._digest = undefined; 16 | } 17 | } 18 | else { 19 | this._image = image; 20 | this._tag = 'latest'; 21 | } 22 | } 23 | } 24 | 25 | getImage() { 26 | return this._image; 27 | } 28 | 29 | setImage(image) { 30 | this._image = image; 31 | } 32 | 33 | getTag() { 34 | return this._tag; 35 | } 36 | 37 | setTag(tag) { 38 | this._digest = undefined; 39 | this._tag = tag; 40 | } 41 | 42 | getDigest() { 43 | return this._digest; 44 | } 45 | 46 | setDigest(digest) { 47 | this._tag = undefined; 48 | this._digest = digest; 49 | } 50 | 51 | getType() { 52 | return this._digest !== undefined ? '@' : ':'; 53 | } 54 | 55 | toString(dropLatest = false) { 56 | const toString = [this._image]; 57 | if (this._tag) { 58 | if (this._tag !== 'latest') { 59 | toString.push(':'); 60 | toString.push(this._tag); 61 | } else if (!dropLatest) { 62 | toString.push(':'); 63 | toString.push(this._tag); 64 | } 65 | } 66 | if (this._digest) { 67 | toString.push('@'); 68 | toString.push(this._digest); 69 | } 70 | 71 | return toString.join(''); 72 | } 73 | 74 | static fromJSON(json) { 75 | return Object.assign(new BaseImage(), json); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/components/ServiceEnvInputFields.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from "react"; 2 | import {connect} from "react-redux"; 3 | import * as Action from "../actions"; 4 | import EnvInputField from "./EnvInputField"; 5 | 6 | class ServiceEnvInputFields extends React.Component { 7 | render() { 8 | const environmentVariables = this.props.service.getEnvironmentVariables(); 9 | let envInputs; 10 | if (Array.isArray(environmentVariables) && environmentVariables.length > 0) { 11 | envInputs = environmentVariables.map((variable, idx) => { 12 | return ( 13 | 18 | ); 19 | }); 20 | } else { 21 | envInputs = ( 22 |
    No environment variables declared.
    23 | ) 24 | } 25 | 26 | return ( 27 |
    28 | 36 | {envInputs} 37 |
    38 | ) 39 | } 40 | 41 | onChange() { 42 | this.props.dispatch(Action.updateService(this.props.service)); 43 | } 44 | 45 | onDelete(idx) { 46 | const filter = this.props.service.getEnvironmentVariables().filter((env, envIdx) => envIdx !== idx); 47 | this.props.service.setEnvironmentVariables(filter); 48 | this.onChange(); 49 | } 50 | 51 | addEnv() { 52 | const service = this.props.service; 53 | service.addEnvironmentVariable(); 54 | this.props.dispatch(Action.updateService(service)); 55 | } 56 | } 57 | 58 | export default connect()(ServiceEnvInputFields); 59 | -------------------------------------------------------------------------------- /src/js/compose.loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import {RestartPolicy} from "../domain/restartPolicy"; 3 | import {EnvironmentVariableHelper} from "../utils/environmentVariable"; 4 | import lodash from "lodash"; 5 | 6 | const YAML = require('yamljs'); 7 | const fs = require('fs'); 8 | const ComposeLoaderV2 = require('./compose.loader.v2'); 9 | 10 | 11 | export default class ComposeLoader { 12 | static createFromFile(file) { 13 | const content = fs.readFileSync(file, 'utf8'); 14 | const yaml = YAML.parse(content); 15 | 16 | if (ComposeLoader._isVersion(yaml, '2')) { 17 | return new ComposeLoaderV2(yaml, content); 18 | } 19 | 20 | throw "Unrecognized Docker Compose file."; 21 | } 22 | 23 | static toYaml(state) { 24 | if (state.version === '2') { 25 | const services = {}; 26 | 27 | lodash.cloneDeep(state.services) 28 | .filter(s => s.isActive()) 29 | .forEach(service => { 30 | const ts = {}; 31 | ts.image = service.getBaseImage().toString(); 32 | 33 | if (service.getRestartPolicy() !== RestartPolicy.NO) { 34 | ts.restart = service.getRestartPolicy(); 35 | } 36 | if (service.getPortMappings().length > 0) { 37 | ts.ports = service.getPortMappings().map(portMapping => portMapping.toString()); 38 | } 39 | if (service.getEnvironmentVariables().length > 0) { 40 | ts.environment = {}; 41 | EnvironmentVariableHelper.replaceEnvWithGlobalEnv(service, state.envVars); 42 | service.getEnvironmentVariables().forEach(e => { 43 | ts.environment[e.getKey()] = e.getValue(); 44 | }) 45 | } 46 | 47 | services[service.getName()] = ts; 48 | }); 49 | 50 | return YAML.stringify({ 51 | version: '2', 52 | services: services 53 | }, 10); 54 | } 55 | } 56 | 57 | static _isVersion(yaml, version) { 58 | return yaml.version && yaml.version === version || yaml.services; 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /src/domain/portMapping.js: -------------------------------------------------------------------------------- 1 | export class PortMapping { 2 | constructor(externalPort, internalPort) { 3 | if (internalPort === undefined && typeof externalPort === 'string') { 4 | const splittedPorts = externalPort.split(':'); 5 | if (splittedPorts.length === 2) { 6 | this._externalPort = String(splittedPorts[0] || ""); 7 | this._internalPort = String(splittedPorts[1] || ""); 8 | } else if (splittedPorts.length === 1) { 9 | this._externalPort = ""; 10 | this._internalPort = String(splittedPorts[0] || ""); 11 | } 12 | } else { 13 | this._externalPort = String(externalPort || ""); 14 | this._internalPort = String(internalPort || ""); 15 | } 16 | 17 | const isExternalPortRange = (this.getExternalPort() || "").indexOf('-') !== -1; 18 | const isInternalPortRange = (this.getInternalPort() || "").indexOf('-') !== -1; 19 | if (this.getInternalPort() && this.getExternalPort() && (isExternalPortRange && !isInternalPortRange || !isExternalPortRange && isInternalPortRange)) { 20 | throw new Error(); 21 | } 22 | 23 | if (isExternalPortRange) { 24 | const split = this.getExternalPort().split("-"); 25 | if (+split[0] >= +split[1]) { 26 | throw new Error(`External port range is malformed! ${this.getExternalPort()}`); 27 | } 28 | } 29 | 30 | if (isInternalPortRange) { 31 | const split = this.getInternalPort().split("-"); 32 | if (+split[0] >= +split[1]) { 33 | throw new Error(`External port range is malformed! ${this.getInternalPort()}`); 34 | } 35 | } 36 | } 37 | 38 | getExternalPort() { 39 | return this._externalPort; 40 | } 41 | 42 | setExternalPort(port) { 43 | this._externalPort = port; 44 | } 45 | 46 | getInternalPort() { 47 | return this._internalPort; 48 | } 49 | 50 | setInternalPort(port) { 51 | this._internalPort = port; 52 | } 53 | 54 | toString() { 55 | return [this.getExternalPort(), this.getInternalPort()].filter(p => p.length > 0).join(':'); 56 | } 57 | 58 | static fromJSON(json) { 59 | return Object.assign(new PortMapping(), json); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/domain/service.portMapping.spec.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | import {Service} from "../../src/domain"; 4 | 5 | describe('Service: Port Mapping', function () { 6 | beforeEach(() => { 7 | this.service = new Service(); 8 | }); 9 | 10 | it('should set port mapping based on numbers.', () => { 11 | this.service.addPortMapping('1337', '8080'); 12 | expect(this.service.getPortMappings()[0].getExternalPort()).to.equal('1337'); 13 | expect(this.service.getPortMappings()[0].getInternalPort()).to.equal('8080'); 14 | }); 15 | 16 | it('should set port mapping based on string.', () => { 17 | this.service.addPortMapping('1337:8080'); 18 | expect(this.service.getPortMappings()[0].getExternalPort()).to.equal('1337'); 19 | expect(this.service.getPortMappings()[0].getInternalPort()).to.equal('8080'); 20 | }); 21 | 22 | it('should set port mapping based on string.', () => { 23 | this.service.addPortMapping('8080'); 24 | expect(this.service.getPortMappings()[0].getExternalPort()).to.equal(''); 25 | expect(this.service.getPortMappings()[0].getInternalPort()).to.equal('8080'); 26 | }); 27 | 28 | it('should set port mapping based on string with range.', () => { 29 | this.service.addPortMapping('8080-8090'); 30 | expect(this.service.getPortMappings()[0].getExternalPort()).to.equal(''); 31 | expect(this.service.getPortMappings()[0].getInternalPort()).to.equal('8080-8090'); 32 | }); 33 | 34 | it('should set port mapping based on string with range internal / external.', () => { 35 | this.service.addPortMapping('7070-7080:8080-8090'); 36 | expect(this.service.getPortMappings()[0].getExternalPort()).to.equal('7070-7080'); 37 | expect(this.service.getPortMappings()[0].getInternalPort()).to.equal('8080-8090'); 38 | }); 39 | 40 | it('should set port mapping based on string with range internal / external having different range sizes.', () => { 41 | expect(() => this.service.addPortMapping('7070:8080-8090')).to.throw(Error); 42 | expect(() => this.service.addPortMapping('7070-7071:8080')).to.throw(Error); 43 | expect(() => this.service.addPortMapping('7071-7070:8080-8081')).to.throw(Error); 44 | expect(() => this.service.addPortMapping('7071-7070')).to.throw(Error); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/exporter/shell/docker-service.js: -------------------------------------------------------------------------------- 1 | import lodash from "lodash"; 2 | import {EnvironmentVariableHelper} from "../../utils/environmentVariable"; 3 | 4 | export class ShellDockerServiceExporter { 5 | /** 6 | * @param {Service} service 7 | * @returns {string} 8 | */ 9 | static getShellCommand(service, globalEnvs, mode) { 10 | return new ShellDockerServiceExporter(service, globalEnvs).generate(mode); 11 | } 12 | 13 | /** 14 | * @param {Service} service 15 | */ 16 | constructor(service, globalEnvs) { 17 | this.service = lodash.cloneDeep(service); 18 | this.globalEnvs = lodash.cloneDeep(globalEnvs); 19 | this.cmd = []; 20 | } 21 | 22 | generate(mode) { 23 | const service = EnvironmentVariableHelper.replaceEnvWithGlobalEnv(this.service, this.globalEnvs); 24 | 25 | this.cmd.push(`docker service create`); 26 | this.cmd.push(`--name ${service.getName()}`); 27 | 28 | this._processEnvVars(service.getEnvironmentVariables()); 29 | this._processPorts(service.getPortMappings()); 30 | this._processRestartPolicy(service.getRestartPolicy()); 31 | 32 | this._processBaseImage(service.getBaseImage()); 33 | 34 | return this.cmd.join(mode === true ? ' \\\n ' : ' '); 35 | } 36 | 37 | /** 38 | * @param {BaseImage} baseimage 39 | */ 40 | _processBaseImage(baseimage) { 41 | this.cmd.push(baseimage); 42 | } 43 | 44 | /** 45 | * @param {Array} envVars 46 | * @private 47 | */ 48 | _processEnvVars(envVars) { 49 | envVars.forEach(ev => { 50 | this.cmd.push(`--env ${ev.getKey()}=${ev.getValue()}`) 51 | }); 52 | } 53 | 54 | /** 55 | * @param {Array} portMappings 56 | * @private 57 | */ 58 | _processPorts(portMappings) { 59 | portMappings.forEach(portMapping => { 60 | this.cmd.push(`--publish ${portMapping}`) 61 | }) 62 | } 63 | 64 | /** 65 | * @param {RestartPolicy} restartPolicy 66 | * @private 67 | */ 68 | _processRestartPolicy(restartPolicy) { 69 | switch (restartPolicy) { 70 | case 'on-failure': 71 | this.cmd.push(`--restart-condition on-failure`); 72 | break; 73 | case 'always': 74 | this.cmd.push(`--restart-condition any`); 75 | break; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/components/ImageInputField.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from "react"; 2 | import {connect} from "react-redux"; 3 | import * as Action from "../actions"; 4 | const _ = require('../../i18n'); 5 | 6 | class ImageInputField extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = {}; 10 | } 11 | 12 | render() { 13 | const image = this.props.service.getBaseImage(); 14 | this.state.imageType = image.getType(); 15 | 16 | return ( 17 |
    18 | 19 |
    20 | 24 | 30 | 34 |
    35 |
    36 | ) 37 | } 38 | 39 | onChange(what, event) { 40 | const baseImage = this.props.service.getBaseImage(); 41 | if (what === 'type') { 42 | const val = baseImage.getTag() || baseImage.getDigest() || ""; 43 | if (event.target.value === ':') { 44 | baseImage.setTag(val); 45 | } else { 46 | baseImage.setDigest(val); 47 | } 48 | } 49 | if (what === 'image') { 50 | baseImage.setImage(event.target.value); 51 | } 52 | if (what === 'tag') { 53 | if (this.state.imageType === ':') { 54 | baseImage.setTag(event.target.value); 55 | } else { 56 | baseImage.setDigest(event.target.value); 57 | } 58 | } 59 | this.props.dispatch(Action.updateService(this.props.service)); 60 | } 61 | } 62 | export default connect()(ImageInputField); 63 | -------------------------------------------------------------------------------- /src/utils/environmentVariable.js: -------------------------------------------------------------------------------- 1 | import {EnvironmentVariable} from "../domain/environmentVariable"; 2 | import lodash from "lodash"; 3 | 4 | export class EnvironmentVariableHelper { 5 | /** 6 | * Replaces variable references in the environment variables of a service by their concrete value of global 7 | * environment variable. 8 | * 9 | * @param {Service} service 10 | * @param {Array} globalEnvs 11 | */ 12 | static replaceEnvWithGlobalEnv(service, globalEnvs) { 13 | globalEnvs = EnvironmentVariableHelper._resolveVarsInGlobalEnv(globalEnvs); 14 | service = lodash.cloneDeep(service); 15 | const serviceEnvVars = service.getEnvironmentVariables(); 16 | const envVars = (serviceEnvVars || []).map(EnvironmentVariableHelper._replaceVarsCallback(globalEnvs)); 17 | service.setEnvironmentVariables(envVars); 18 | return service; 19 | } 20 | 21 | /** 22 | * Replaces all variables of global environment variables with their respective concrete values. 23 | * 24 | * @param {Array} globalEnvs 25 | * @returns {Array} replaced global environment variables 26 | * @private 27 | */ 28 | static _resolveVarsInGlobalEnv(globalEnvs) { 29 | globalEnvs = lodash.cloneDeep(globalEnvs); 30 | // FIXME: This is ugly as fuck... We continue replacing until no "$" is found in the values.... 31 | // Don't kill me please! We need to build a tree here which is used to resolve vars. 32 | while ((globalEnvs || []).map(e => e.getValue()).some(v => v.indexOf("$") !== -1)) { 33 | globalEnvs = (globalEnvs || []).map(EnvironmentVariableHelper._replaceVarsCallback(globalEnvs)); 34 | } 35 | return globalEnvs; 36 | } 37 | 38 | /** 39 | * This is private property! 40 | * 41 | * This function is used to replace the variables of type $ABC.. etc. 42 | * 43 | * @param {Array}globalEnvs 44 | * @returns {Function} callback used by Array.map function 45 | * @private 46 | */ 47 | static _replaceVarsCallback(globalEnvs) { 48 | const regexForVariables = /\$([A-Z_]*)/gi; 49 | 50 | return function (e) { 51 | let value = e.getValue(); 52 | if (typeof value === 'string' && value.indexOf("$") !== -1) { 53 | value = value.replace(regexForVariables, match => { 54 | const envVar = globalEnvs.find(env => env.getKey() === match.substr(1)); 55 | return envVar ? envVar.getValue() : match; 56 | }); 57 | 58 | return EnvironmentVariable.create(e.getKey(), value); 59 | } 60 | return e; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import lodash from "lodash"; 2 | import * as C from "../constants"; 3 | import ComposeLoader from "../js/compose.loader"; 4 | import {ReducerRegistry} from "./reducerRegistry"; 5 | import {EnvironmentVariable, Service} from "../domain"; 6 | import {generateUUID} from "../utils"; 7 | 8 | const initialState = { 9 | docker: { 10 | envVars: [], 11 | services: [], 12 | version: '2' 13 | }, 14 | activeService: {}, 15 | }; 16 | 17 | const reducerRegistry = new ReducerRegistry(); 18 | 19 | export function appReducer(state = initialState, action) { 20 | const newState = lodash.cloneDeep(state); 21 | return reducerRegistry.execute(action.type, newState, action); 22 | } 23 | 24 | reducerRegistry.register(C.UPDATE_SERVICE, (state, action) => { 25 | let service = state.docker.services.find(service => service._id === action.payload._id); 26 | service = action.payload; 27 | return state; 28 | }); 29 | 30 | reducerRegistry.register(C.ADD_SERVICE, (state, action) => { 31 | action.payload._id = generateUUID(); 32 | state.docker.services.push(action.payload); 33 | return state; 34 | }); 35 | 36 | reducerRegistry.register(C.SHOW_SERVICE_DETAILS, (state, action) => { 37 | state.activeService = action.payload; 38 | return state; 39 | }); 40 | 41 | reducerRegistry.register(C.OPEN_FILE, (state, action) => { 42 | state.docker = { 43 | envVars: action.payload.envVars.map(env => EnvironmentVariable.create(env._key, env._value)), 44 | services: action.payload.services.map(service => Service.fromJSON(service)) 45 | }; 46 | 47 | return state; 48 | }); 49 | 50 | reducerRegistry.register(C.UPDATE_ENV_VARIABLE, (state, action) => { 51 | if (action.payload.serviceName) { 52 | const service = state.docker.services.find(s => s._name === action.payload.serviceName); 53 | service.environment[action.payload.idx] = { 54 | key: action.payload.key, 55 | value: action.payload.value, 56 | }; 57 | } else { 58 | state.docker.envVars[action.payload.idx] = action.payload.variable; 59 | } 60 | return state; 61 | }); 62 | 63 | reducerRegistry.register(C.DELETE_ENV_VARIABLE, (state, action) => { 64 | if (action.payload.serviceName) { 65 | const service = state.docker.services[action.payload.serviceName]; 66 | service.environment = service.environment.filter((val, idx) => action.payload.idx !== idx); 67 | } else { 68 | state.docker.envVars = state.docker.envVars.filter((val, idx) => action.payload.idx !== idx); 69 | } 70 | return state; 71 | }); 72 | 73 | reducerRegistry.register(C.ADD_ENV_VARIABLE, (state, action) => { 74 | state.docker.envVars.push(new EnvironmentVariable()); 75 | return state; 76 | }); 77 | 78 | reducerRegistry.register(C.IMPORT_COMPOSE_FILE, (state, action) => { 79 | const Compose = ComposeLoader.createFromFile(action.payload); 80 | Array.prototype.push.apply(state.docker.services, Compose.getServices()); 81 | return state; 82 | }); 83 | -------------------------------------------------------------------------------- /test/domain/service.image.spec.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | import {Service} from "../../src/domain"; 4 | 5 | describe('Service: Image', function () { 6 | beforeEach(() => { 7 | this.service = new Service(); 8 | }); 9 | 10 | it('should set image with tag based on string.', () => { 11 | this.service.setBaseImage('image:tag'); 12 | expect(this.service.getBaseImage().getImage()).to.equal('image'); 13 | expect(this.service.getBaseImage().getTag()).to.equal('tag'); 14 | expect(this.service.getBaseImage().getDigest()).to.equal(undefined); 15 | }); 16 | 17 | it('should set image with empty tag based on string.', () => { 18 | this.service.setBaseImage('image:'); 19 | expect(this.service.getBaseImage().getImage()).to.equal('image'); 20 | expect(this.service.getBaseImage().getTag()).to.equal('latest'); 21 | expect(this.service.getBaseImage().getDigest()).to.equal(undefined); 22 | }); 23 | 24 | it('should set image without tag based on string.', () => { 25 | this.service.setBaseImage('image'); 26 | expect(this.service.getBaseImage().getImage()).to.equal('image'); 27 | expect(this.service.getBaseImage().getTag()).to.equal('latest'); 28 | expect(this.service.getBaseImage().getDigest()).to.equal(undefined); 29 | }); 30 | 31 | it('should set image with digest based on string.', () => { 32 | this.service.setBaseImage('image@123'); 33 | expect(this.service.getBaseImage().getImage()).to.equal('image'); 34 | expect(this.service.getBaseImage().getTag()).to.equal(undefined); 35 | expect(this.service.getBaseImage().getDigest()).to.equal('123'); 36 | }); 37 | 38 | it('should set image with empty digest based on string.', () => { 39 | this.service.setBaseImage('image@'); 40 | expect(this.service.getBaseImage().getImage()).to.equal('image'); 41 | expect(this.service.getBaseImage().getTag()).to.equal('latest'); 42 | expect(this.service.getBaseImage().getDigest()).to.equal(undefined); 43 | }); 44 | 45 | it('should not fail when providing empty image name.', () => { 46 | this.service.setBaseImage(); 47 | expect(this.service.getBaseImage().getImage()).to.equal(undefined); 48 | expect(this.service.getBaseImage().getTag()).to.equal(undefined); 49 | expect(this.service.getBaseImage().getDigest()).to.equal(undefined); 50 | }); 51 | 52 | it('should return image as string.', () => { 53 | this.service.setBaseImage('image:123'); 54 | expect(this.service.getBaseImage().toString()).to.equal('image:123'); 55 | 56 | this.service.setBaseImage('image'); 57 | expect(this.service.getBaseImage().toString()).to.equal('image:latest'); 58 | 59 | this.service.setBaseImage('image'); 60 | expect(this.service.getBaseImage().toString(true)).to.equal('image'); 61 | 62 | this.service.setBaseImage('image@123'); 63 | expect(this.service.getBaseImage().toString()).to.equal('image@123'); 64 | }) 65 | }); 66 | -------------------------------------------------------------------------------- /src/components/EnvInputField.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from "react"; 2 | import {connect} from "react-redux"; 3 | 4 | const jQuery = require('jquery'); 5 | const typeahead = require('../../node_modules/typeahead.js/dist/typeahead.jquery'); 6 | 7 | class EnvInputField extends React.Component { 8 | render() { 9 | const key = this.props.variable.key; 10 | const value = this.props.variable.value; 11 | 12 | return ( 13 |
    14 | 18 | : 19 | 24 | 25 | 26 | 27 | 28 | 29 |
    30 | ) 31 | } 32 | 33 | 34 | handleDelete() { 35 | this.props.onDelete(this.props.index); 36 | } 37 | 38 | componentDidMount() { 39 | const opts = { 40 | hint: false, 41 | highlight: true, 42 | minLength: 1, 43 | }; 44 | jQuery(`#env_${this.props.index}`).typeahead(opts, 45 | { 46 | source: this.substringMatcher(this.props.envVars), 47 | limit: 15, 48 | display: (s) => `\$${s.getKey()}`, 49 | templates: { 50 | suggestion: (res) => `
    \$${res.getKey()}: ${res.getValue()}
    ` 51 | } 52 | }); 53 | } 54 | 55 | /** 56 | * @param {Array} environmentVariables 57 | * @returns {findMatches} 58 | */ 59 | substringMatcher(environmentVariables) { 60 | return function findMatches(q, cb) { 61 | const matches = environmentVariables 62 | .sort((a, b) => a.getKey() > b.getKey() ? 1 : -1) 63 | .filter(envVar => { 64 | let key = "$" + envVar.getKey(); 65 | let keyMatched = key.indexOf(q.toLowerCase()) !== -1; 66 | 67 | let value = envVar.getValue(); 68 | let valueMatched = (typeof value === "string") ? value.toLowerCase().indexOf(q.toLowerCase()) !== -1 : false; 69 | 70 | return keyMatched || valueMatched; 71 | }); 72 | cb(matches); 73 | }; 74 | } 75 | 76 | onChange(what, event) { 77 | const environmentVariable = this.props.variable; 78 | 79 | if (what === "key") { 80 | environmentVariable.setKey(event.target.value); 81 | } 82 | if (what === "value") { 83 | environmentVariable.setValue(event.target.value); 84 | } 85 | 86 | if (this.props.onChange) { 87 | this.props.onChange(this.props.index, environmentVariable); 88 | } 89 | } 90 | } 91 | function mapStateToScope(state) { 92 | return { 93 | envVars: state.app.docker.envVars 94 | } 95 | } 96 | export default connect(mapStateToScope)(EnvInputField) 97 | -------------------------------------------------------------------------------- /test/exporter/shell/docker-service.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | import {Service, RestartPolicy} from "../../../src/domain"; 4 | import {ShellDockerServiceExporter} from "../../../src/exporter/shell/docker-service"; 5 | import {EnvironmentVariable} from "../../../src/domain/environmentVariable"; 6 | 7 | describe('ShellDockerServiceExporter', function () { 8 | it('should convert image.', () => { 9 | const service = new Service("database"); 10 | service.setBaseImage("mysql:5.6"); 11 | 12 | const shellCommand = ShellDockerServiceExporter.getShellCommand(service); 13 | 14 | expect(shellCommand).to.equal('docker service create --name database mysql:5.6'); 15 | }); 16 | 17 | it('should convert env vars.', () => { 18 | const service = new Service("database"); 19 | service.setBaseImage("mysql:5.6"); 20 | service.addEnvironmentVariable("NODE_ENV", "development"); 21 | 22 | const shellCommand = ShellDockerServiceExporter.getShellCommand(service); 23 | 24 | expect(shellCommand).to.equal('docker service create --name database --env NODE_ENV=development mysql:5.6'); 25 | }); 26 | 27 | it('should resolve env values.', () => { 28 | const service = new Service("database"); 29 | service.setBaseImage("mysql:5.6"); 30 | service.addEnvironmentVariable("SPRING_RABBITMQ_HOST", "$IP"); 31 | 32 | const globalEnvVars = [ 33 | EnvironmentVariable.create("IP", "127.0.0.1") 34 | ]; 35 | 36 | const shellCommand = ShellDockerServiceExporter.getShellCommand(service, globalEnvVars); 37 | 38 | expect(shellCommand).to.equal('docker service create --name database --env SPRING_RABBITMQ_HOST=127.0.0.1 mysql:5.6'); 39 | }); 40 | 41 | it('should convert ports.', () => { 42 | const service = new Service("database"); 43 | service.setBaseImage("mysql:5.6"); 44 | service.addPortMapping(1337, 3360); 45 | service.addPortMapping(8080); 46 | 47 | const shellCommand = ShellDockerServiceExporter.getShellCommand(service); 48 | 49 | expect(shellCommand).to.equal('docker service create --name database --publish 1337:3360 --publish 8080 mysql:5.6'); 50 | }); 51 | 52 | it('should convert restart-policy.', () => { 53 | const service = new Service("database"); 54 | service.setBaseImage("mysql:5.6"); 55 | 56 | service.setRestartPolicy(RestartPolicy.ON_FAILURE); 57 | let shellCommand = ShellDockerServiceExporter.getShellCommand(service); 58 | expect(shellCommand).to.equal('docker service create --name database --restart-condition on-failure mysql:5.6'); 59 | 60 | service.setRestartPolicy(RestartPolicy.NO); 61 | shellCommand = ShellDockerServiceExporter.getShellCommand(service); 62 | expect(shellCommand).to.equal('docker service create --name database mysql:5.6'); 63 | 64 | service.setRestartPolicy(RestartPolicy.ALWAYS); 65 | shellCommand = ShellDockerServiceExporter.getShellCommand(service); 66 | expect(shellCommand).to.equal('docker service create --name database --restart-condition any mysql:5.6'); 67 | }); 68 | 69 | it('should pretty print.', () => { 70 | const service = new Service("database"); 71 | service.setBaseImage("mysql:5.6"); 72 | service.addEnvironmentVariable("NODE_ENV", "development"); 73 | 74 | const shellCommand = ShellDockerServiceExporter.getShellCommand(service, [], true); 75 | 76 | const actual = 'docker service create \\\n --name database \\\n --env NODE_ENV=development \\\n mysql:5.6'; 77 | expect(shellCommand).to.equal(actual); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/domain/service.js: -------------------------------------------------------------------------------- 1 | import {generateUUID, RandomNameGenerator} from "../utils"; 2 | import {PortMapping, RestartPolicy, BaseImage, EnvironmentVariable} from "./"; 3 | 4 | export class Service { 5 | constructor(name) { 6 | this._id = generateUUID(); 7 | this._name = name || RandomNameGenerator.getRandomName(); 8 | this._baseImage = new BaseImage(); 9 | this._ports = []; 10 | this._environment = []; 11 | this._restart = RestartPolicy.NO; 12 | this._active = true; 13 | } 14 | 15 | setName(name) { 16 | this._name = name; 17 | } 18 | 19 | getName() { 20 | return this._name; 21 | } 22 | 23 | setBaseImage(image) { 24 | this._baseImage = new BaseImage(image); 25 | } 26 | 27 | /** 28 | * @returns {BaseImage} 29 | */ 30 | getBaseImage() { 31 | return this._baseImage; 32 | } 33 | 34 | setRestartPolicy(policy) { 35 | this._restart = RestartPolicy.get(policy); 36 | } 37 | 38 | getRestartPolicy() { 39 | return this._restart; 40 | } 41 | 42 | addPortMapping(externalPort, internalPort) { 43 | const portMapping = new PortMapping(externalPort, internalPort); 44 | const externalPortAlreadyUsed = this._ports.some(portMapping => portMapping.externalPort === portMapping.getExternalPort()); 45 | const internalPortAlreadyUsed = this._ports.some(portMapping => portMapping.internalPort === portMapping.getInternalPort()); 46 | if (externalPortAlreadyUsed || internalPortAlreadyUsed) { 47 | // TODO: what to do, when some of the desired ports are already in use? 48 | } else { 49 | this._ports.push(portMapping); 50 | } 51 | } 52 | 53 | setPortMappings(portMappings) { 54 | this._ports = portMappings; 55 | } 56 | 57 | /** 58 | * @returns {Array} 59 | */ 60 | getPortMappings() { 61 | return this._ports; 62 | } 63 | 64 | addEnvironmentVariable() { 65 | this._environment.push(new EnvironmentVariable(arguments)); 66 | } 67 | 68 | getEnvironmentVariables() { 69 | return this._environment; 70 | } 71 | 72 | /** 73 | * @param key 74 | * @param resolveValue If true, all variables in value are resolved. 75 | * @returns {EnvironmentVariable} 76 | */ 77 | getEnvironmentVariable(key, resolveValue) { 78 | const environmentVariable = this._environment.find(env => env.getKey() === key); 79 | if (environmentVariable && resolveValue === true) { 80 | let value = environmentVariable.getValue(); 81 | if (typeof value === 'string' && value.indexOf("$") !== -1) { 82 | value = value.replace(/\$([A-Za-z_]*)/gi, match => { 83 | const envVar = this.getEnvironmentVariable(match.substr(1), true); 84 | return envVar ? envVar.getValue() : match; 85 | }); 86 | 87 | return EnvironmentVariable.create(key, value); 88 | } 89 | } 90 | return environmentVariable; 91 | } 92 | 93 | setEnvironmentVariables(envVars) { 94 | this._environment = envVars; 95 | } 96 | 97 | setActive(active) { 98 | this._active = active; 99 | } 100 | 101 | isActive() { 102 | return this._active; 103 | } 104 | 105 | static fromJSON(json) { 106 | const service = Object.assign(new Service(), json); 107 | service._baseImage = BaseImage.fromJSON(service._baseImage); 108 | service._environment = service._environment.map(EnvironmentVariable.fromJSON); 109 | service._ports = service._ports.map(PortMapping.fromJSON); 110 | return service; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /menu/file.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | const dialog = electron.dialog; 3 | const _ = require('../i18n'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const Zip = require('node-zip'); 7 | 8 | module.exports = { 9 | label: _('menu.file.label'), 10 | submenu: [ 11 | { 12 | label: _('menu.file.open.label') + '...', 13 | accelerator: 'Ctrl+O', 14 | click (item, focusedWindow) { 15 | const dialogOpts = { 16 | properties: ['openFile'], 17 | filters: [ 18 | {name: 'Docker-Compose-File', extensions: ['dce']} 19 | ] 20 | }; 21 | 22 | const files = dialog.showOpenDialog(dialogOpts); 23 | if(files && files.length === 1) { 24 | const data = fs.readFileSync(files[0], 'binary'); 25 | var zip = new Zip(data, {base64: false, checkCRC32: true}); 26 | const file = zip.files['data.json']; 27 | electron.app.currentProjectFile = files[0]; 28 | focusedWindow.webContents.send('open-file', JSON.parse(file.asText())); 29 | } 30 | } 31 | }, 32 | { 33 | type: 'separator' 34 | }, 35 | { 36 | label: _('menu.file.import.label'), 37 | accelerator: 'Ctrl+I', 38 | click(item, focusedWindow) { 39 | const dialogOpts = { 40 | properties: ['openFile'], 41 | filters: [ 42 | {name: 'Docker-Compose-File', extensions: ['yml', 'yaml']} 43 | ] 44 | }; 45 | 46 | const files = dialog.showOpenDialog(dialogOpts); 47 | if(files && files.length === 1) { 48 | focusedWindow.webContents.send('import', files[0]); 49 | } 50 | } 51 | }, 52 | { 53 | type: 'separator' 54 | }, 55 | { 56 | label: _('menu.file.save.label'), 57 | accelerator: 'Ctrl+S', 58 | click(item, focusedWindow) { 59 | focusedWindow.webContents.send('save'); 60 | } 61 | }, 62 | // { 63 | // label: _('menu.file.save_as.label') + '...', 64 | // accelerator: 'Ctrl+Shift+S', 65 | // click(item, focusedWindow) { 66 | // focusedWindow.webContents.send('save-as'); 67 | // } 68 | // }, 69 | { 70 | type: 'separator' 71 | }, 72 | { 73 | label: _('menu.file.export.label'), 74 | submenu: [ 75 | { 76 | label: _('menu.file.export.compose.label'), 77 | accelerator: 'Ctrl+E', 78 | click(item, focusedWindow) { 79 | focusedWindow.webContents.send('export'); 80 | } 81 | }, 82 | // { 83 | // label: _('menu.file.export.docker-run.label'), 84 | // click(item, focusedWindow) { 85 | // focusedWindow.webContents.send('export.docker-run'); 86 | // } 87 | // }, 88 | { 89 | label: _('menu.file.export.docker-service.label'), 90 | accelerator: 'Ctrl+Shift+E', 91 | click(item, focusedWindow) { 92 | focusedWindow.webContents.send('export.docker-service'); 93 | } 94 | } 95 | ] 96 | }, 97 | { 98 | type: 'separator' 99 | }, 100 | { 101 | label: _('menu.file.quit.label'), 102 | role: 'quit', 103 | accelerator: 'Ctrl+Q' 104 | } 105 | ] 106 | }; 107 | -------------------------------------------------------------------------------- /test/utils/environmentVariable.spec.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | import {Service} from "../../src/domain"; 4 | import {EnvironmentVariableHelper} from "../../src/utils/environmentVariable"; 5 | import {EnvironmentVariable} from "../../src/domain/environmentVariable"; 6 | 7 | describe('EnvironmentVariableHelper', function () { 8 | const ENV_IP = EnvironmentVariable.create("IP", "127.0.0.1"); 9 | const ENV_PORT = EnvironmentVariable.create("PORT", "8080"); 10 | const ENV_HOST = EnvironmentVariable.create("HOST", "http://$IP/"); 11 | const ENV_HOST_W_PORT = EnvironmentVariable.create("HOST_W_PORT", "http://$IP:$PORT/"); 12 | 13 | it('should do nothing for env vars without vars.', () => { 14 | const service = new Service(); 15 | service.addEnvironmentVariable("A", "A value"); 16 | 17 | const serviceWithReplacements = EnvironmentVariableHelper.replaceEnvWithGlobalEnv(service); 18 | expect(service.getEnvironmentVariables()[0]).to.eql(EnvironmentVariable.create("A", "A value")); 19 | expect(serviceWithReplacements.getEnvironmentVariables()[0]).to.eql(EnvironmentVariable.create("A", "A value")); 20 | }); 21 | 22 | it('should replace one var in global env vars.', () => { 23 | const service = new Service(); 24 | service.addEnvironmentVariable("A", "$HOST"); 25 | 26 | const globalEnvs = EnvironmentVariableHelper._resolveVarsInGlobalEnv([ENV_HOST, ENV_IP]); 27 | expect(globalEnvs[0]).to.eql(EnvironmentVariable.create("HOST", "http://127.0.0.1/")); 28 | expect(ENV_HOST).to.eql(EnvironmentVariable.create("HOST", "http://$IP/")); 29 | }); 30 | 31 | it('should replace multiple vars in global env vars.', () => { 32 | const service = new Service(); 33 | service.addEnvironmentVariable("A", "$HOST"); 34 | 35 | const globalEnvs = EnvironmentVariableHelper._resolveVarsInGlobalEnv([ENV_HOST_W_PORT, ENV_IP, ENV_PORT]); 36 | expect(globalEnvs[0]).to.eql(EnvironmentVariable.create("HOST_W_PORT", "http://127.0.0.1:8080/")); 37 | expect(ENV_HOST_W_PORT).to.eql(EnvironmentVariable.create("HOST_W_PORT", "http://$IP:$PORT/")); 38 | }); 39 | 40 | it('should replace env vars having vars.', () => { 41 | const service = new Service(); 42 | service.addEnvironmentVariable("A", "$IP"); 43 | 44 | const serviceWithReplacements = EnvironmentVariableHelper.replaceEnvWithGlobalEnv(service, [ENV_HOST, ENV_IP]); 45 | expect(service.getEnvironmentVariables()[0]).to.eql(EnvironmentVariable.create("A", "$IP")); 46 | expect(serviceWithReplacements.getEnvironmentVariables()[0]).to.eql(EnvironmentVariable.create("A", "127.0.0.1")); 47 | }); 48 | 49 | it('should replace env vars having global vars containing vars.', () => { 50 | const service = new Service(); 51 | service.addEnvironmentVariable("A", "$HOST_W_PORT"); 52 | 53 | const serviceWithReplacements = EnvironmentVariableHelper.replaceEnvWithGlobalEnv(service, [ENV_HOST_W_PORT, ENV_IP, ENV_PORT]); 54 | expect(service.getEnvironmentVariables()[0]).to.eql(EnvironmentVariable.create("A", "$HOST_W_PORT")); 55 | expect(serviceWithReplacements.getEnvironmentVariables()[0]).to.eql(EnvironmentVariable.create("A", "http://127.0.0.1:8080/")); 56 | }); 57 | 58 | it('should replace complex 1.', () => { 59 | const globalEnvs = [ 60 | EnvironmentVariable.create("IP", "127.0.0.1"), 61 | EnvironmentVariable.create("MIDDLEWARE_URL", "$IP"), 62 | EnvironmentVariable.create("MIDDLEWARE_API_URL", "$MIDDLEWARE_URL/api"), 63 | ]; 64 | 65 | const service = new Service(); 66 | service.addEnvironmentVariable("middleware_url", "$MIDDLEWARE_URL"); 67 | service.addEnvironmentVariable("middleware_apiUrl", "$MIDDLEWARE_API_URL"); 68 | 69 | const serviceWithReplacements = EnvironmentVariableHelper.replaceEnvWithGlobalEnv(service, globalEnvs); 70 | expect(serviceWithReplacements.getEnvironmentVariable("middleware_url").getValue()).to.eql("127.0.0.1"); 71 | expect(serviceWithReplacements.getEnvironmentVariable("middleware_apiUrl").getValue()).to.eql("127.0.0.1/api"); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/components/PortsInputField.js: -------------------------------------------------------------------------------- 1 | import React, {PropTypes} from "react"; 2 | import {connect} from "react-redux"; 3 | import * as Action from "../actions"; 4 | 5 | class PortsInputField extends React.Component { 6 | render() { 7 | const portMappings = this.props.service.getPortMappings(); 8 | let portsInputs; 9 | if (Array.isArray(portMappings) && portMappings.length > 0) { 10 | portsInputs = portMappings.map((portMapping, idx) => { 11 | return ( 12 | 19 | ); 20 | }); 21 | } else { 22 | portsInputs = ( 23 |
    No port mappings defined.
    24 | ) 25 | } 26 | 27 | return ( 28 |
    29 | 37 | {portsInputs} 38 |
    39 | ) 40 | } 41 | 42 | addPortMapping() { 43 | const service = this.props.service; 44 | service.addPortMapping(0, 0); 45 | this.props.dispatch(Action.updateService(service)); 46 | } 47 | 48 | onChange() { 49 | this.props.dispatch(Action.updateService(this.props.service)); 50 | } 51 | 52 | onDelete(idx) { 53 | const service = this.props.service; 54 | const portMappings = service.getPortMappings().filter((p, pidx) => pidx !== idx); 55 | service.setPortMappings(portMappings); 56 | this.props.dispatch(Action.updateService(service)); 57 | } 58 | 59 | validate(value) { 60 | const ports = { 61 | externalPort: [], 62 | internalPort: [] 63 | }; 64 | 65 | this.props.values.forEach((val, idx) => { 66 | if (idx != value.index) { 67 | const __ret = this.splitPortString(val); 68 | ports.externalPort.push(__ret.externalPort); 69 | ports.internalPort.push(__ret.internalPort); 70 | } 71 | }); 72 | 73 | const externalPortInUse = ports.externalPort.indexOf(value.externalPort) !== -1; 74 | const internalPortInUse = ports.internalPort.indexOf(value.internalPort) !== -1; 75 | return { 76 | externalPortInUse: externalPortInUse, 77 | internalPortInUse: internalPortInUse, 78 | hasErrors() { 79 | return !externalPortInUse && !internalPortInUse; 80 | } 81 | } 82 | } 83 | 84 | } 85 | export default connect()(PortsInputField); 86 | 87 | /** 88 | * 89 | */ 90 | class PortInputField extends React.Component { 91 | render() { 92 | return ( 93 |
    94 | 97 | : 98 | 101 | 102 | 103 | 104 | 105 | 106 |
    107 | ) 108 | } 109 | 110 | onChange(portType, event) { 111 | const portMapping = this.props.portMapping; 112 | const value = event.target.value; 113 | if (portType === 'external') { 114 | portMapping.setExternalPort(value); 115 | } 116 | if (portType === 'internal') { 117 | portMapping.setInternalPort(value); 118 | } 119 | 120 | this.props.onChange(portMapping); 121 | } 122 | } -------------------------------------------------------------------------------- /test/compose.loader/parsedYaml.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2", 3 | "services": { 4 | "apigateway": { 5 | "image": "quay.io/gbtec/biccloud-apigateway-sidecar-service", 6 | "restart": "unless-stopped", 7 | "ports": [ 8 | "8087:8080", 9 | "8000:8000" 10 | ], 11 | "environment": { 12 | "NODE_ENV": "production", 13 | "SERVER_PORT": "8080", 14 | "LOGGING_LEVEL_ROOT": "$LOGGING_LEVEL_ROOT", 15 | "JAVA_TOOL_OPTIONS": "-Xmx128m", 16 | "EUREKA_CLIENT_SERVICEURL_DEFAULTZONE": "$EUREKA_CLIENT_SERVICEURL", 17 | "SPRING_CLOUD_CONFIG_DISCOVERY_SERVICE_ID": "apigateway", 18 | "SPRING_RABBITMQ_HOST": "$SPRING_RABBITMQ_HOST", 19 | "SPRING_RABBITMQ_USERNAME": "$SPRING_RABBITMQ_USERNAME", 20 | "SPRING_RABBITMQ_PASSWORD": "$SPRING_RABBITMQ_PASSWORD", 21 | "middleware_url": "$MIDDLEWARE_URL", 22 | "middleware_apiUrl": "$MIDDLEWARE_API_URL" 23 | } 24 | }, 25 | "message-bus": { 26 | "image": "rabbitmq:3.6.1-management", 27 | "restart": "unless-stopped", 28 | "ports": [ 29 | "15672:15672" 30 | ], 31 | "environment": { 32 | "LOGGING_LEVEL_ROOT": "$LOGGING_LEVEL_ROOT", 33 | "JAVA_TOOL_OPTIONS": "-Xmx512m", 34 | "RABBITMQ_DEFAULT_USER": "admin", 35 | "RABBITMQ_DEFAULT_PASS": "secret", 36 | "CELERY_AMQP_TASK_RESULT_EXPIRES": 10800 37 | } 38 | }, 39 | "domain-service": { 40 | "image": "quay.io/gbtec/biccloud-domain-service", 41 | "restart": "unless-stopped", 42 | "ports": [ 43 | "8080" 44 | ], 45 | "environment": { 46 | "JAVA_TOOL_OPTIONS": "-Xmx256m", 47 | "SERVER_PORT": "8080", 48 | "EUREKA_CLIENT_SERVICEURL_DEFAULTZONE": "$EUREKA_CLIENT_SERVICEURL", 49 | "SPRING_CLOUD_CONFIG_DISCOVERY_SERVICE_ID": "domain-service", 50 | "SPRING_RABBITMQ_HOST": "$SPRING_RABBITMQ_HOST", 51 | "SPRING_RABBITMQ_USERNAME": "$SPRING_RABBITMQ_USERNAME", 52 | "SPRING_RABBITMQ_PASSWORD": "$SPRING_RABBITMQ_PASSWORD", 53 | "SPRING_PROFILES_ACTIVE": "postgres,dataimport", 54 | "LOGGING_LEVEL_ROOT": "$LOGGING_LEVEL_ROOT" 55 | } 56 | }, 57 | "eureka-service": { 58 | "image": "quay.io/gbtec/biccloud-eureka-service", 59 | "restart": "unless-stopped", 60 | "links": [ 61 | "message-bus" 62 | ], 63 | "ports": [ 64 | "8080:8761" 65 | ], 66 | "environment": { 67 | "SERVER_PORT": "8761", 68 | "LOGGING_LEVEL_ROOT": "$LOGGING_LEVEL_ROOT", 69 | "JAVA_TOOL_OPTIONS": "-Xmx1g", 70 | "SERVICE_8080_TAGS": "proxytcp", 71 | "SPRING_RABBITMQ_HOST": "$SPRING_RABBITMQ_HOST", 72 | "SPRING_RABBITMQ_USERNAME": "$SPRING_RABBITMQ_USERNAME", 73 | "SPRING_RABBITMQ_PASSWORD": "$SPRING_RABBITMQ_PASSWORD" 74 | } 75 | }, 76 | "user-service": { 77 | "image": "quay.io/gbtec/biccloud-user-service", 78 | "restart": "unless-stopped", 79 | "ports": [ 80 | "8080" 81 | ], 82 | "environment": { 83 | "SERVER_PORT": "8080", 84 | "LOGGING_LEVEL_ROOT": "$LOGGING_LEVEL_ROOT", 85 | "JAVA_TOOL_OPTIONS": "-Xmx256m", 86 | "EUREKA_CLIENT_SERVICEURL_DEFAULTZONE": "$EUREKA_CLIENT_SERVICEURL", 87 | "SPRING_CLOUD_CONFIG_DISCOVERY_SERVICE_ID": "user-service", 88 | "SPRING_RABBITMQ_HOST": "$SPRING_RABBITMQ_HOST", 89 | "SPRING_RABBITMQ_USERNAME": "$SPRING_RABBITMQ_USERNAME", 90 | "SPRING_RABBITMQ_PASSWORD": "$SPRING_RABBITMQ_PASSWORD", 91 | "SPRING_MAIL_HOST": "stekoe.kasserver.com", 92 | "SPRING_MAIL_PORT": 587, 93 | "SPRING_MAIL_USERNAME": "admin", 94 | "SPRING_MAIL_PASSWORD": "secret", 95 | "BIC_CLOUD_USER_PASSWORD_RESET_EMAIL_FROM": "biccloud@stekoe.de", 96 | "SPRING_PROFILES_ACTIVE": "postgres" 97 | } 98 | }, 99 | "method-service": { 100 | "image": "quay.io/gbtec/biccloud-method-service", 101 | "restart": "unless-stopped", 102 | "ports": [ 103 | "8080" 104 | ], 105 | "environment": { 106 | "SERVER_PORT": "8080", 107 | "LOGGING_LEVEL_ROOT": "$LOGGING_LEVEL_ROOT", 108 | "JAVA_TOOL_OPTIONS": "-Xmx512m", 109 | "EUREKA_CLIENT_SERVICEURL_DEFAULTZONE": "$EUREKA_CLIENT_SERVICEURL", 110 | "SPRING_CLOUD_CONFIG_DISCOVERY_SERVICE_ID": "method-service", 111 | "SPRING_RABBITMQ_HOST": "$SPRING_RABBITMQ_HOST", 112 | "SPRING_RABBITMQ_USERNAME": "$SPRING_RABBITMQ_USERNAME", 113 | "SPRING_RABBITMQ_PASSWORD": "$SPRING_RABBITMQ_PASSWORD", 114 | "SPRING_PROFILES_ACTIVE": "postgres,dataimport" 115 | } 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /src/utils/randomNameGenerator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This class generates random names based on the same algorithm as docker does. The parts arrays are 3 | * taken directly from docker source code: https://github.com/docker/docker/blob/master/pkg/namesgenerator/names-generator.go 4 | */ 5 | export class RandomNameGenerator { 6 | static getRandomName() { 7 | const left = RandomNameGenerator.getRandomPart(RandomNameGenerator.left); 8 | const right = RandomNameGenerator.getRandomPart(RandomNameGenerator.right); 9 | return `${left}_${right}`; 10 | } 11 | 12 | static getRandomPart(parts) { 13 | const randomIndex = Math.floor(Math.random() * 999999 % parts.length); 14 | return parts[randomIndex]; 15 | } 16 | } 17 | 18 | RandomNameGenerator.left = [ 19 | "admiring", 20 | "adoring", 21 | "affectionate", 22 | "agitated", 23 | "amazing", 24 | "angry", 25 | "awesome", 26 | "backstabbing", 27 | "berserk", 28 | "big", 29 | "boring", 30 | "clever", 31 | "cocky", 32 | "compassionate", 33 | "condescending", 34 | "cranky", 35 | "desperate", 36 | "determined", 37 | "distracted", 38 | "dreamy", 39 | "drunk", 40 | "eager", 41 | "ecstatic", 42 | "elastic", 43 | "elated", 44 | "elegant", 45 | "evil", 46 | "fervent", 47 | "focused", 48 | "furious", 49 | "gigantic", 50 | "gloomy", 51 | "goofy", 52 | "grave", 53 | "happy", 54 | "high", 55 | "hopeful", 56 | "hungry", 57 | "infallible", 58 | "jolly", 59 | "jovial", 60 | "kickass", 61 | "lonely", 62 | "loving", 63 | "mad", 64 | "modest", 65 | "naughty", 66 | "nauseous", 67 | "nostalgic", 68 | "peaceful", 69 | "pedantic", 70 | "pensive", 71 | "prickly", 72 | "reverent", 73 | "romantic", 74 | "sad", 75 | "serene", 76 | "sharp", 77 | "sick", 78 | "silly", 79 | "sleepy", 80 | "small", 81 | "stoic", 82 | "stupefied", 83 | "suspicious", 84 | "tender", 85 | "thirsty", 86 | "tiny", 87 | "trusting", 88 | "zen" 89 | ]; 90 | RandomNameGenerator.right = [ 91 | "albattani", 92 | "allen", 93 | "almeida", 94 | "agnesi", 95 | "archimedes", 96 | "ardinghelli", 97 | "aryabhata", 98 | "austin", 99 | "babbage", 100 | "banach", 101 | "bardeen", 102 | "bartik", 103 | "bassi", 104 | "beaver", 105 | "bell", 106 | "bhabha", 107 | "bhaskara", 108 | "blackwell", 109 | "bohr", 110 | "booth", 111 | "borg", 112 | "bose", 113 | "boyd", 114 | "brahmagupta", 115 | "brattain", 116 | "brown", 117 | "carson", 118 | "chandrasekhar", 119 | "shannon", 120 | "clarke", 121 | "colden", 122 | "cori", 123 | "cray", 124 | "curran", 125 | "curie", 126 | "darwin", 127 | "davinci", 128 | "dijkstra", 129 | "dubinsky", 130 | "easley", 131 | "edison", 132 | "einstein", 133 | "elion", 134 | "engelbart", 135 | "euclid", 136 | "euler", 137 | "fermat", 138 | "fermi", 139 | "feynman", 140 | "franklin", 141 | "galileo", 142 | "gates", 143 | "goldberg", 144 | "goldstine", 145 | "goldwasser", 146 | "golick", 147 | "goodall", 148 | "haibt", 149 | "hamilton", 150 | "hawking", 151 | "heisenberg", 152 | "heyrovsky", 153 | "hodgkin", 154 | "hoover", 155 | "hopper", 156 | "hugle", 157 | "hypatia", 158 | "jang", 159 | "jennings", 160 | "jepsen", 161 | "joliot", 162 | "jones", 163 | "kalam", 164 | "kare", 165 | "keller", 166 | "khorana", 167 | "kilby", 168 | "kirch", 169 | "knuth", 170 | "kowalevski", 171 | "lalande", 172 | "lamarr", 173 | "lamport", 174 | "leakey", 175 | "leavitt", 176 | "lewin", 177 | "lichterman", 178 | "liskov", 179 | "lovelace", 180 | "lumiere", 181 | "mahavira", 182 | "mayer", 183 | "mccarthy", 184 | "mcclintock", 185 | "mclean", 186 | "mcnulty", 187 | "meitner", 188 | "meninsky", 189 | "mestorf", 190 | "minsky", 191 | "mirzakhani", 192 | "morse", 193 | "murdock", 194 | "newton", 195 | "nightingale", 196 | "nobel", 197 | "noether", 198 | "northcutt", 199 | "noyce", 200 | "panini", 201 | "pare", 202 | "pasteur", 203 | "payne", 204 | "perlman", 205 | "pike", 206 | "poincare", 207 | "poitras", 208 | "ptolemy", 209 | "raman", 210 | "ramanujan", 211 | "ride", 212 | "montalcini", 213 | "ritchie", 214 | "roentgen", 215 | "rosalind", 216 | "saha", 217 | "sammet", 218 | "shaw", 219 | "shirley", 220 | "shockley", 221 | "sinoussi", 222 | "snyder", 223 | "spence", 224 | "stallman", 225 | "stonebraker", 226 | "swanson", 227 | "swartz", 228 | "swirles", 229 | "tesla", 230 | "thompson", 231 | "torvalds", 232 | "turing", 233 | "varahamihira", 234 | "visvesvaraya", 235 | "volhard", 236 | "wescoff", 237 | "wiles", 238 | "williams", 239 | "wilson", 240 | "wing", 241 | "wozniak", 242 | "wright", 243 | "yalow", 244 | "yonath" 245 | ]; 246 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | require('../package.json').productName 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
    27 | 28 | 35 | 36 | 37 | 38 | 39 | 40 | --------------------------------------------------------------------------------