├── .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 | [](https://travis-ci.org/code-arcs/docker-compose-editor)
4 | [](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 | 
12 | 
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 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
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 |
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 | {RestartPolicy[restartPolicyName]}
13 | )
14 | });
15 |
16 | return (
17 |
18 | Restart
19 |
21 | {policies}
22 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------