├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── __tests__ ├── App.test.tsx ├── BindMounts.test.tsx ├── LeftNav.test.tsx ├── NetworksDropdown.test.tsx ├── OptionBar.test.tsx ├── Volume.test.tsx ├── Volumes.test.tsx ├── __snapshots__ │ ├── App.test.tsx.snap │ ├── LeftNav.test.tsx.snap │ └── NetworksDropdown.test.tsx.snap ├── fileProcessing.ts ├── getSimulationDimensions.ts ├── setD3State.ts └── yamlState.json ├── babel.config.js ├── build ├── background.tiff ├── nautilus_logo.icns ├── nautilus_logo.ico └── nautilus_logo.png ├── electron-webpack.json ├── package.json ├── samples ├── docker-compose.BAD.yml ├── docker-compose.bpc.yml ├── docker-compose.taiga.yml ├── docker-compose1.yml ├── docker-compose2.yml ├── docker-compose3.yml └── docker-composeDEMO.yml ├── scripts ├── deploy.sh └── run-test.sh ├── src ├── common │ ├── dockerComposeValidation.ts │ └── resolveEnvVariables.ts ├── main │ ├── main.ts │ └── menu.ts ├── renderer │ ├── App.d.ts │ ├── App.tsx │ ├── components │ │ ├── BindMounts.tsx │ │ ├── D3Wrapper.tsx │ │ ├── ErrorDisplay.tsx │ │ ├── FileSelector.tsx │ │ ├── LeftNav.tsx │ │ ├── Links.tsx │ │ ├── NetworksDropdown.tsx │ │ ├── NodePorts.tsx │ │ ├── NodeVolumes.tsx │ │ ├── Nodes.tsx │ │ ├── OptionBar.tsx │ │ ├── ServiceInfo.tsx │ │ ├── Title.tsx │ │ ├── View.tsx │ │ ├── Volume.tsx │ │ ├── Volumes.tsx │ │ └── VolumesWrapper.tsx │ ├── helpers │ │ ├── colorSchemeIndex.ts │ │ ├── getSimulationDimensions.ts │ │ ├── parseOpenError.ts │ │ ├── setD3State.ts │ │ ├── static.ts │ │ └── yamlParser.ts │ ├── index.tsx │ └── styles │ │ ├── _variables.scss │ │ ├── app.scss │ │ ├── d3Wrapper.scss │ │ ├── fonts │ │ ├── OFL.txt │ │ ├── Sen-Bold.ttf │ │ ├── Sen-ExtraBold.ttf │ │ └── Sen-Regular.ttf │ │ ├── leftNav.scss │ │ └── optionBar.scss └── setupTests.ts ├── static ├── Nautilus-text-logo2.png ├── arrow.svg ├── container.svg ├── containerPath.ts ├── nautilus-new-ui-mockup.png ├── nautilus-text-logo.png ├── nautilus_logo.svg ├── options.png └── views.png ├── tsconfig-webpack.json ├── tsconfig.json ├── webpack.main.ext.js ├── webpack.renderer.ext.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | # /node_modules/* in the project root is ignored by default 2 | # build artefacts 3 | dist/* -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "plugin:prettier/recommended", "prettier/react"], 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "es6": true, 7 | "jest": true, 8 | "node": true 9 | }, 10 | "rules": { 11 | "jsx-a11y/href-no-hash": ["off"], 12 | "react/jsx-filename-extension": ["warn", { "extensions": [".js", ".jsx"] }], 13 | "max-len": [ 14 | "warn", 15 | { 16 | "code": 80, 17 | "tabWidth": 2, 18 | "comments": 80, 19 | "ignoreComments": false, 20 | "ignoreTrailingComments": true, 21 | "ignoreUrls": true, 22 | "ignoreStrings": true, 23 | "ignoreTemplateLiterals": true, 24 | "ignoreRegExpLiterals": true 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json 4 | .DS_Store 5 | yarn.lock 6 | release 7 | _secrets -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - node 5 | 6 | env: 7 | global: PATH=/opt/python/3.7.1/bin:$PATH 8 | 9 | notifications: 10 | email: false 11 | slack: nautilus27:HZhDaJlZZK80zNaPCmueIiWk 12 | 13 | # cache some files for faster builds 14 | cache: 15 | yarn: true 16 | directories: 17 | - node_modules 18 | 19 | # install dependenices 20 | before-script: 21 | - yarn 22 | 23 | # on PRs and merges to master and prod run tests and build the app 24 | script: 25 | - yarn test 26 | 27 | # only run this script on pull requests and merges into 28 | # the 'master' branches 29 | branches: 30 | only: 31 | - master 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 4 | 5 | ### **Cloning the Repo** 6 | 7 | 1. Fork the Project and clone the repo to your local machine 8 | 2. Install Dependencies 9 | 10 | Using Yarn (highly recommended): 11 | 12 | ``` 13 | yarn 14 | ``` 15 | 16 | Using npm: 17 | 18 | ``` 19 | npm install 20 | ``` 21 | 22 | 3. Make changes 23 | 4. Write tests for changes 24 | 5. Open a Pull Request 25 | 26 | ### **Development** 27 | 28 | When developing, you'll probably want to run the dev version of our application. Nautilus development is fully integrated with webpack HMR, typescript and sass loaders and Electron. To start the development enviorment. 29 | 30 | ``` 31 | yarn dev 32 | ``` 33 | 34 | This will open a new instance of the Nautilus desktop application and will reload automatically as you make changes. Code away!! 35 | 36 | ### **Packaging** 37 | 38 | Nautilus utilizes electron-builder to package the application. If you want to see the changes you've made to Nautilus in a production version of the application, use these scripts: 39 | 40 | _Package for MacOS:_ 41 | 42 | ``` 43 | yarn package-mac 44 | ``` 45 | 46 | _Package for Windows:_ 47 | 48 | ``` 49 | yarn package-win 50 | ``` 51 | 52 | _Package for Linux:_ 53 | 54 | ``` 55 | yarn package-linux 56 | ``` 57 | 58 | OR 59 | 60 | _Package for all three operating systems:_ 61 | 62 | ``` 63 | yarn package-all 64 | ``` 65 | 66 | 67 | 68 | ## Testing 69 | 70 | The Nautilus repo is integrated with Travis CI, so tests will run automatically on all pull requests. But, we highly recommend that you test as you develop--Nautilus is a test driven development team. We have two ways to run tests: 71 | 72 | #### #1 Run Tests for Whole Application 73 | 74 | ``` 75 | yarn test 76 | ``` 77 | 78 | Best use for `yarn test` is right before making a PR to make sure that none of your changes have broken the application. 79 | 80 | #### #2 Run One Test File 81 | 82 | ``` 83 | yarn test-f 84 | ``` 85 | 86 | This command is ideal when working on a particular component to streamline development. No need to run tests for the whole application when only touching one file. 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nautilus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | A Docker Compose Charting Tool

7 | An interactive D3 visualizing tool that dynamically renders essential Docker Compose properties onto an Electron GUI, built to reduce cognitive load and simplify the development environment for engineers.
8 | nautilusdev.com 9 |

10 | 11 |

12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 41 | 42 | 43 | 44 |

45 | 46 |

47 | No nautili were harmed during the making of this application 48 |
49 | - Aris, Danny, Josh, Michael, Tyler 50 |

51 | 52 | 53 | 54 | ## Table of Contents 55 | 56 | - [Features](#features) 57 | - [Getting Started](#getting-started) 58 | - [Prerequisites](#prerequisites) 59 | - [Installation](#installation) 60 | - [Visualizing Your Docker Compose File](#visualizing-your-docker-compose-file) 61 | - [Contributing](#contributing) 62 | - [Cloning The Repo](#cloning-the-Repo) 63 | - [Development](#development) 64 | - [Packaging](#packaging) 65 | - [Testing](#testing) 66 | - [Technologies Used](#technologies-used) 67 | - [License](#license) 68 | 69 | 70 | 71 | ## Features 72 | 73 | ### Open your Docker Compose file 74 | 75 |

76 | 77 |

78 | 79 | ### Display your service's info, ports and volumes 80 | 81 |

82 | 83 |

84 | 85 | ### View your services by a container dependent view 86 | 87 |

88 | 89 |

90 | 91 | ### View your services grouped by networks 92 | 93 |

94 | 95 |

96 | 97 | 98 | 99 | ## Getting Started 100 | 101 | Nautilus comes in a prepackaged application that is ready to run on your preferred operating system. 102 | 103 | ### **Prerequisites** 104 | 105 | Nautilus requires the docker and docker-compose command-line interface tools installed. For Mac and Windows users, this comes included with Docker Desktop. For Linux Users, this comes included with Docker Engine.

106 | For your convenience, we've included links to Docker's documentation to set this up on your preferred operating system below. 107 | 108 | Docker Setup Instructions 109 | 110 | - [Mac](https://docs.docker.com/docker-for-mac/install/) 111 | - [Windows](https://docs.docker.com/docker-for-windows/install/) 112 | - [Linux](https://docs.docker.com/docker-for-mac/install/) 113 | 114 | ### **Installation** 115 | 116 | Once you're sure you have the Docker CLI tools installed, download the Nautilus application from one of the links below. 117 | 118 | Nautilus Download Links 119 | 120 | - [Mac](https://nautilusdev.com/release/Nautilus-1.3.1.dmg) 121 | - [Windows](https://nautilusdev.com/release/Nautilus%20Setup%201.3.1.exe) 122 | - [Linux](https://nautilusdev.com/release/Nautilus-1.3.1.AppImage) 123 | 124 | We are currently in the process of getting appropriate certifications/signatures so you may need to bypass some security warnings to run our application, but rest assured Nautilus does not make any network calls (and the project is 100% open source). 125 | 126 | ### **Visualizing Your Docker Compose File** 127 | 128 | Run the application, open your Docker Compose file, and visualize your Docker Compose setup with the various views and options. 129 | 130 | 131 | 132 | ## Contributing 133 | 134 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 135 | 136 | ### **Cloning the Repo** 137 | 138 | 1. Fork the Project and clone the repo to your local machine 139 | 2. Install Dependencies 140 | 141 | Using Yarn (highly recommended): 142 | 143 | ``` 144 | yarn 145 | ``` 146 | 147 | Using npm: 148 | 149 | ``` 150 | npm install 151 | ``` 152 | 153 | 3. Make changes 154 | 4. Write tests for changes 155 | 5. Open a Pull Request 156 | 157 | ### **Development** 158 | 159 | When developing, you'll probably want to run the dev version of our application. Nautilus development is fully integrated with webpack HMR, typescript and sass loaders and Electron. To start the development environment. 160 | 161 | ``` 162 | yarn dev 163 | ``` 164 | 165 | This will open a new instance of the Nautilus desktop application and will reload automatically as you make changes. Code away!! 166 | 167 | ### **Packaging** 168 | 169 | Nautilus utilizes electron-builder to package the application. If you want to see the changes you've made to Nautilus in a production version of the application, use these scripts: 170 | 171 | _Package for MacOS:_ 172 | 173 | ``` 174 | yarn package-mac 175 | ``` 176 | 177 | _Package for Windows:_ 178 | 179 | ``` 180 | yarn package-win 181 | ``` 182 | 183 | _Package for Linux:_ 184 | 185 | ``` 186 | yarn package-linux 187 | ``` 188 | 189 | OR 190 | 191 | _Package for all three operating systems:_ 192 | 193 | ``` 194 | yarn package-all 195 | ``` 196 | 197 | 198 | 199 | ## Testing 200 | 201 | The Nautilus repo is integrated with Travis CI, so tests will run automatically on all pull requests. But, we highly recommend that you test as you develop--Nautilus is a test driven development team. We have two ways to run tests: 202 | 203 | #### #1 Run Tests for Whole Application 204 | 205 | ``` 206 | yarn test 207 | ``` 208 | 209 | Best use for `yarn test` is right before making a PR to make sure that none of your changes have broken the application. 210 | 211 | #### #2 Run One Test File 212 | 213 | ``` 214 | yarn test-f 215 | ``` 216 | 217 | This command is ideal when working on a particular component to streamline development. No need to run tests for the whole application when only touching one file. 218 | 219 | 220 | 221 | ## Technologies Used 222 | 223 | - [TypeScript](https://www.typescriptlang.org/) 224 | - [Electron](https://www.electronjs.org/) 225 | - [D3](https://d3js.org/) 226 | - [React with Hooks](https://reactjs.org/) 227 | - [Jest](https://jestjs.io/) 228 | - [Enzyme](https://github.com/enzymejs/enzyme) 229 | - [Travis CI](https://travis-ci.org/) 230 | - [SCSS](https://sass-lang.com/) 231 | - [Webpack](https://webpack.js.org/) 232 | - [Babel](https://babeljs.io/) 233 | 234 | 235 | 236 | ## License 237 | 238 | Distributed under the MIT License. See `LICENSE` for more information. 239 | 240 | The Nautilus Devs - [LinkedIn](https://www.linkedin.com/company/nautilusapp) 241 | -------------------------------------------------------------------------------- /__tests__/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, configure, ShallowWrapper } from 'enzyme'; 3 | import renderer from 'react-test-renderer'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | import App from '../src/renderer/App'; 6 | import fs from 'fs'; 7 | import path from 'path'; 8 | 9 | // IMPORT COMPONENTS 10 | import LeftNav from '../src/renderer/components/LeftNav'; 11 | import OptionBar from '../src/renderer/components/OptionBar'; 12 | import D3Wrapper from '../src/renderer/components/D3Wrapper'; 13 | import { State } from '../src/renderer/App.d'; 14 | 15 | configure({ adapter: new Adapter() }); 16 | 17 | describe('Testing App Stateful Component', () => { 18 | let wrapper: ShallowWrapper<{}, State, App>; 19 | beforeEach(() => { 20 | wrapper = shallow(); 21 | }); 22 | 23 | it('renders correctly', () => { 24 | const tree = renderer.create().toJSON(); 25 | expect(tree).toMatchSnapshot(); 26 | }); 27 | 28 | describe('render()', () => { 29 | it('should render child components', () => { 30 | expect(wrapper.find(LeftNav)).toHaveLength(1); 31 | expect(wrapper.find(OptionBar)).toHaveLength(1); 32 | expect(wrapper.find(D3Wrapper)).toHaveLength(1); 33 | }); 34 | }); 35 | 36 | describe('setSelectedContainer()', () => { 37 | it('should updated selectedContainer in state', () => { 38 | wrapper.instance().setSelectedContainer('tester-service'); 39 | expect(wrapper.state().selectedContainer).toBe('tester-service'); 40 | wrapper.instance().setSelectedContainer('api-service'); 41 | expect(wrapper.state().selectedContainer).toBe('api-service'); 42 | }); 43 | }); 44 | 45 | describe('updateView()', () => { 46 | it('should update view in state', () => { 47 | wrapper.instance().updateView('depends_on'); 48 | expect(wrapper.state().view).toBe('depends_on'); 49 | }); 50 | it('should clear selectedNetwork in state', () => { 51 | wrapper.state().selectedNetwork = 'dummy-network'; 52 | wrapper.instance().updateView('depends_on'); 53 | expect(wrapper.state().selectedNetwork).toBe(''); 54 | }); 55 | }); 56 | 57 | describe('updateOption()', () => { 58 | beforeEach(() => { 59 | wrapper.state().options = { 60 | ports: false, 61 | volumes: false, 62 | selectAll: false, 63 | }; 64 | }); 65 | it('should toggle ports option', () => { 66 | wrapper.instance().updateOption('ports'); 67 | expect(wrapper.state().options.ports).toBe(true); 68 | wrapper.instance().updateOption('ports'); 69 | expect(wrapper.state().options.ports).toBe(false); 70 | }); 71 | it('should toggle volumes option', () => { 72 | wrapper.instance().updateOption('volumes'); 73 | expect(wrapper.state().options.volumes).toBe(true); 74 | wrapper.instance().updateOption('volumes'); 75 | expect(wrapper.state().options.volumes).toBe(false); 76 | }); 77 | it('should toggle selectAll option', () => { 78 | wrapper.instance().updateOption('selectAll'); 79 | expect(wrapper.state().options.selectAll).toBe(true); 80 | wrapper.instance().updateOption('selectAll'); 81 | expect(wrapper.state().options.selectAll).toBe(false); 82 | }); 83 | it('selectAll should toggle ports and options', () => { 84 | wrapper.instance().updateOption('selectAll'); 85 | expect(wrapper.state().options.ports).toBe(true); 86 | expect(wrapper.state().options.volumes).toBe(true); 87 | wrapper.instance().updateOption('selectAll'); 88 | expect(wrapper.state().options.ports).toBe(false); 89 | expect(wrapper.state().options.volumes).toBe(false); 90 | }); 91 | it('selectAll should should reflect all being selected or not', () => { 92 | wrapper.instance().updateOption('ports'); 93 | wrapper.instance().updateOption('volumes'); 94 | expect(wrapper.state().options.selectAll).toBe(true); 95 | wrapper.instance().updateOption('ports'); 96 | expect(wrapper.state().options.selectAll).toBe(false); 97 | }); 98 | }); 99 | 100 | describe('selectNetwork()', () => { 101 | it('should update view to "networks"', () => { 102 | wrapper.instance().selectNetwork('dummy-network'); 103 | expect(wrapper.state().view).toBe('networks'); 104 | }); 105 | it('should updated selectedNetwork to passed in string', () => { 106 | expect(wrapper.state().selectedNetwork).toBe('dummy-network'); 107 | }); 108 | }); 109 | 110 | describe('convertAndStoreYamlJSON()', () => { 111 | let yamlText: string; 112 | 113 | beforeAll(() => { 114 | yamlText = fs 115 | .readFileSync(path.resolve(__dirname, '../samples/docker-compose1.yml')) 116 | .toString(); 117 | wrapper.instance().convertAndStoreYamlJSON(yamlText); 118 | }); 119 | 120 | afterAll(() => { 121 | delete window.d3State; 122 | localStorage.removeItem('state'); 123 | }); 124 | 125 | it('should update d3State of window', () => { 126 | expect(window.d3State).toBeTruthy(); 127 | expect(window.d3State.treeDepth).toBe(2); 128 | expect(window.d3State.simulation).toBeTruthy(); 129 | expect(window.d3State.serviceGraph.nodes).toBeTruthy(); 130 | expect(window.d3State.serviceGraph.links).toBeTruthy(); 131 | }); 132 | it('should set the localstorage with item key "state"', () => { 133 | expect(localStorage.getItem('state')).toBeTruthy(); 134 | }); 135 | it('should update services in state', () => { 136 | const serviceNames = Object.keys(wrapper.state().services); 137 | expect(serviceNames.length).toBe(5); 138 | const check = serviceNames.filter( 139 | (n) => 140 | n === 'vote' || 141 | n === 'result' || 142 | n === 'worker' || 143 | n === 'redis' || 144 | n === 'db', 145 | ); 146 | expect(check.length).toBe(5); 147 | }); 148 | it('should update volumes in state', () => { 149 | const volumes = wrapper.state().volumes; 150 | expect(volumes.hasOwnProperty('db-data')).toBe(true); 151 | }); 152 | it('should update bindmounts in state', () => { 153 | const bindMounts = wrapper.state().bindMounts; 154 | expect(bindMounts.length).toBe(2); 155 | const check = bindMounts.filter( 156 | (n) => n === './vote' || n === './result', 157 | ); 158 | expect(check.length).toBe(2); 159 | }); 160 | it('should update networks in state', () => { 161 | const networks = Object.keys(wrapper.state().networks); 162 | expect(networks.length).toBe(2); 163 | const check = networks.filter( 164 | (n) => n === 'front-tier' || n === 'back-tier', 165 | ); 166 | expect(check.length).toBe(2); 167 | }); 168 | it('should set fileOpened to true', () => { 169 | expect(wrapper.state().fileOpened).toBe(true); 170 | }); 171 | }); 172 | 173 | describe('componentDidMount()', () => { 174 | let state = { 175 | fileOpened: true, 176 | services: { 177 | db: { 178 | image: 'postgres', 179 | environment: [ 180 | 'POSTGRES_MULTIPLE_DATABASES=dpc_attribution,dpc_queue,dpc_auth,dpc_consent', 181 | 'POSTGRES_USER=postgres', 182 | 'POSTGRES_PASSWORD=dpc-safe', 183 | ], 184 | ports: ['5432:5432'], 185 | volumes: ['./docker/postgres:/docker-entrypoint-initdb.d'], 186 | }, 187 | aggregation: { 188 | image: 189 | '${ECR_HOST:-755619740999.dkr.ecr.us-east-1.amazonaws.com/dpc-aggregation}:latest', 190 | ports: ['9901:9900'], 191 | env_file: ['./ops/config/decrypted/local.env'], 192 | environment: ['ENV=local', 'JACOCO=${REPORT_COVERAGE}'], 193 | depends_on: ['db'], 194 | volumes: [ 195 | 'export-volume:/app/data', 196 | './jacocoReport/dpc-aggregation:/jacoco-report', 197 | ], 198 | }, 199 | attribution: { 200 | image: 201 | '${ECR_HOST:-755619740999.dkr.ecr.us-east-1.amazonaws.com/dpc-attribution}:latest', 202 | depends_on: ['db'], 203 | environment: ['ENV=local', 'JACOCO=${REPORT_COVERAGE}'], 204 | ports: ['3500:8080', '9902:9900'], 205 | volumes: ['./jacocoReport/dpc-attribution:/jacoco-report'], 206 | }, 207 | api: { 208 | image: 209 | '${ECR_HOST:-755619740999.dkr.ecr.us-east-1.amazonaws.com/dpc-api}:latest', 210 | ports: ['3002:3002', '9903:9900'], 211 | environment: [ 212 | 'attributionURL=http://attribution:8080/v1/', 213 | 'ENV=local', 214 | 'JACOCO=${REPORT_COVERAGE}', 215 | 'exportPath=/app/data', 216 | 'JVM_FLAGS=-Ddpc.api.authenticationDisabled=${AUTH_DISABLED:-false}', 217 | ], 218 | depends_on: ['attribution'], 219 | volumes: [ 220 | 'export-volume:/app/data', 221 | './jacocoReport/dpc-api:/jacoco-report', 222 | ], 223 | }, 224 | consent: { 225 | image: 226 | '${ECR_HOST:-755619740999.dkr.ecr.us-east-1.amazonaws.com/dpc-consent}:latest', 227 | depends_on: ['db'], 228 | environment: ['ENV=local', 'JACOCO=${REPORT_COVERAGE}'], 229 | ports: ['3600:3600', '9004:9900'], 230 | volumes: ['./jacocoReport/dpc-consent:/jacoco-report'], 231 | }, 232 | start_core_dependencies: { 233 | image: 'dadarek/wait-for-dependencies', 234 | depends_on: ['db'], 235 | command: 'db:5432', 236 | }, 237 | start_api_dependencies: { 238 | image: 'dadarek/wait-for-dependencies', 239 | depends_on: ['attribution', 'aggregation'], 240 | command: 'attribution:8080 aggregation:9900', 241 | }, 242 | start_api: { 243 | image: 'dadarek/wait-for-dependencies', 244 | depends_on: ['api'], 245 | command: 'api:3002', 246 | }, 247 | }, 248 | volumes: { 249 | 'export-volume': { 250 | driver: 'local', 251 | driver_opts: { type: 'none', device: '/tmp', o: 'bind' }, 252 | }, 253 | }, 254 | networks: {}, 255 | bindMounts: [ 256 | './docker/postgres', 257 | './jacocoReport/dpc-aggregation', 258 | './jacocoReport/dpc-attribution', 259 | './jacocoReport/dpc-api', 260 | './jacocoReport/dpc-consent', 261 | ], 262 | }; 263 | beforeAll(() => { 264 | localStorage.setItem('state', JSON.stringify(state)); 265 | wrapper = shallow(); 266 | }); 267 | afterAll(() => { 268 | delete window.d3State; 269 | localStorage.removeItem('state'); 270 | }); 271 | 272 | it('should set state with state from local storage', () => { 273 | const appState = wrapper.state(); 274 | expect(appState.services).toEqual(state.services); 275 | expect(appState.volumes).toEqual(state.volumes); 276 | expect(appState.fileOpened).toBe(true); 277 | expect(appState.bindMounts).toEqual(state.bindMounts); 278 | }); 279 | 280 | it('should set d3State on the window', () => { 281 | expect(window.d3State).toBeTruthy(); 282 | expect(window.d3State.treeDepth).toBe(4); 283 | expect(window.d3State.simulation).toBeTruthy(); 284 | expect(window.d3State.serviceGraph.nodes).toBeTruthy(); 285 | expect(window.d3State.serviceGraph.links).toBeTruthy(); 286 | }); 287 | }); 288 | }); 289 | -------------------------------------------------------------------------------- /__tests__/BindMounts.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import BindMounts from '../src/renderer/components/BindMounts'; 4 | 5 | type Props = { 6 | bindMounts: any; 7 | getColor: any; 8 | }; 9 | 10 | describe('', () => { 11 | const props: Props = { 12 | bindMounts: ['test1', 'test2'], 13 | getColor: jest.fn(() => { 14 | return 'hsl(80%,60%,60%)'; 15 | }), 16 | }; 17 | 18 | const wrapper = shallow(); 19 | 20 | //test to check if right number of components / elements are rendered 21 | 22 | it('contains # of Volume components based on bindMounts array length', () => { 23 | const volumesCount = props.bindMounts.length; 24 | expect(wrapper.find('Volume').length).toEqual(volumesCount); 25 | expect(wrapper.find('Volume').length).not.toBe(1); 26 | }); 27 | 28 | it('returns a one instance of class: bind-mounts', () => { 29 | expect(wrapper.find('.bind-mounts')).toHaveLength(1); 30 | expect(wrapper.find('.bind-mounts')).not.toHaveLength(0); 31 | }); 32 | 33 | it('should render only one div', () => { 34 | expect(wrapper.find('div')).toHaveLength(1); 35 | expect(wrapper.find('div')).not.toHaveLength(2); 36 | }); 37 | 38 | //test to see if props are passed down 39 | 40 | it('expect child component to have props of color and volume', () => { 41 | expect(wrapper.find('Volume').get(0).props).toEqual({ 42 | color: 'hsl(80%,60%,60%)', 43 | volume: 'test1', 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /__tests__/LeftNav.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, configure } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import LeftNav from '../src/renderer/components/LeftNav'; 5 | import Title from '../src/renderer/components/Title'; 6 | import FileSelector from '../src/renderer/components/FileSelector'; 7 | import ServiceInfo from '../src/renderer/components/ServiceInfo'; 8 | import renderer from 'react-test-renderer'; 9 | 10 | configure({ adapter: new Adapter() }); 11 | 12 | const props = { 13 | fileOpen: jest.fn(() => {}), 14 | fileOpened: false, 15 | selectedContainer: '', 16 | service: {}, 17 | }; 18 | 19 | describe('test the functionality of LeftNav component', () => { 20 | // Test Snapshot 21 | it('renders correctly', () => { 22 | const snapProps = Object.assign({}, props, { 23 | service: { 24 | image: 'postgres', 25 | environment: [ 26 | 'POSTGRES_MULTIPLE_DATABASES=dpc_attribution,dpc_queue,dpc_auth,dpc_consent', 27 | 'POSTGRES_USER=postgres', 28 | 'POSTGRES_PASSWORD=dpc-safe', 29 | ], 30 | ports: ['5432:5432'], 31 | volumes: ['./docker/postgres:/docker-entrypoint-initdb.d'], 32 | }, 33 | selectedContainer: 'db', 34 | fileOpened: true, 35 | }); 36 | const component = renderer.create().toJSON(); 37 | expect(component).toMatchSnapshot(); 38 | }); 39 | 40 | // Test Outer div 41 | it('Should render a div with the class of `left-nav`', () => { 42 | const wrapper = shallow(); 43 | expect(wrapper.find('div.left-nav')).toHaveLength(1); 44 | }); 45 | 46 | // Test top-half div 47 | it('Should render a div inside of `div.left-nav` with a class of `top-half`', () => { 48 | const wrapper = shallow(); 49 | expect(wrapper.find('div.left-nav').find('div.top-half')).toHaveLength(1); 50 | }); 51 | 52 | // Test Title 53 | it('Should render a Title component inside of `div.top-half`', () => { 54 | const wrapper = shallow(); 55 | expect(wrapper.find('div.top-half').find(Title)).toHaveLength(1); 56 | }); 57 | 58 | // Test FileOpened 59 | it('`div.top-half` should only have one child if fileOpened is false', () => { 60 | const wrapper = shallow(); 61 | expect(wrapper.find('div.top-half').children()).toHaveLength(1); 62 | }); 63 | 64 | it('`div.top-half` should have two children if fileOpened is true', () => { 65 | const wrapper = shallow(); 66 | expect(wrapper.find('div.top-half').children()).toHaveLength(2); 67 | }); 68 | 69 | it('Should render a file selector if fileOpened is true', () => { 70 | const wrapper = shallow(); 71 | expect(wrapper.find('div.top-half').find(FileSelector)).toHaveLength(1); 72 | }); 73 | 74 | // Test ServiceInfo 75 | it('Should render an ServiceInfo component inside of `div.left-nav`', () => { 76 | const wrapper = shallow(); 77 | expect(wrapper.find('div.left-nav').find(ServiceInfo)).toHaveLength(1); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /__tests__/NetworksDropdown.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, configure } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import NetworksDropDown from '../src/renderer/components/NetworksDropdown'; 5 | import renderer from 'react-test-renderer'; 6 | 7 | configure({ adapter: new Adapter() }); 8 | 9 | const props = { 10 | networks: {}, 11 | selectNetwork: jest.fn(() => {}), 12 | selectedNetwork: '', 13 | }; 14 | 15 | describe('Test Networks Dropdown Component', () => { 16 | // Test Select Dropdown 17 | it('renders correctly', () => { 18 | const snapProps = Object.assign({}, props, { 19 | networks: { 20 | snapTester: {}, 21 | dbTester: {}, 22 | }, 23 | }); 24 | const tree = renderer.create().toJSON(); 25 | expect(tree).toMatchSnapshot(); 26 | }); 27 | 28 | it('Should render select dropdown with id of `networks`', () => { 29 | const wrapper = shallow(); 30 | expect(wrapper.find('select#networks')).toHaveLength(1); 31 | }); 32 | 33 | // Test Network Header 34 | it('should have a disabled networks option', () => { 35 | const wrapper = shallow(); 36 | expect(wrapper.find('option#networkHeader')).toBeDisabled(); 37 | }); 38 | 39 | // Test Selected Networks 40 | it('If select value is `` selectedNetwork should be ``', () => { 41 | const wrapper = shallow(); 42 | expect(wrapper.find('select').props().value).toBe(''); 43 | }); 44 | 45 | it('If select value is a network string selectedNetwork should be a string', () => { 46 | const wrapper = shallow( 47 | , 52 | ); 53 | expect(wrapper.find('select').props().value).toBe('a'); 54 | }); 55 | 56 | // Test Options 57 | it('should render one option with className networkOption if networks is empty', () => { 58 | const wrapper = shallow(); 59 | expect(wrapper.find('option.networkOption')).toHaveLength(1); 60 | }); 61 | 62 | it('should render one option with className networkOption if there is only 1 item in networks', () => { 63 | const wrapper = shallow( 64 | , 65 | ); 66 | expect(wrapper.find('option.networkOption')).toHaveLength(1); 67 | }); 68 | 69 | it('Should render one more networkOption than the number of networks if networks has more than 1 item', () => { 70 | const wrapper = shallow( 71 | , 75 | ); 76 | expect(wrapper.find('option.networkOption')).toHaveLength(4); 77 | }); 78 | 79 | // Test Title 80 | it('If there are no networks title should be default', () => { 81 | const wrapper = shallow(); 82 | expect(wrapper.find('option#groupNetworks')).toHaveText('default'); 83 | }); 84 | 85 | it('If there is 1 network title should be the name of that network', () => { 86 | const wrapper = shallow( 87 | , 88 | ); 89 | expect(wrapper.find('option#groupNetworks')).toHaveLength(0); 90 | }); 91 | 92 | it('If there are more than 1 network title should be `group networks`', () => { 93 | const wrapper = shallow( 94 | , 98 | ); 99 | expect(wrapper.find('option#groupNetworks')).toHaveText('group networks'); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /__tests__/OptionBar.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, configure } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | 5 | import OptionBar from '../src/renderer/components/OptionBar'; 6 | import NetworksDropdown from '../src/renderer/components/NetworksDropdown'; 7 | import { ViewT } from '../src/renderer/App.d'; 8 | 9 | configure({ adapter: new Adapter() }); 10 | 11 | const props = { 12 | view: 'depends_on' as ViewT, 13 | options: { 14 | ports: true, 15 | volumes: true, 16 | selectAll: true, 17 | }, 18 | networks: { 19 | a: 'test1', 20 | b: 'test2', 21 | }, 22 | updateView: jest.fn(() => {}), 23 | updateOption: jest.fn(() => {}), 24 | selectNetwork: jest.fn(() => {}), 25 | selectedNetwork: '', 26 | }; 27 | 28 | describe('', () => { 29 | // optionBar div Testing 30 | it('renders a div with a className of `option-bar`', () => { 31 | const wrapper = shallow(); 32 | expect(wrapper.find('div.option-bar')).toHaveLength(1); 33 | }); 34 | 35 | // Views Testing 36 | it('renders a div with a className of `views`', () => { 37 | const wrapper = shallow(); 38 | expect(wrapper.find('div.views')).toHaveLength(1); 39 | }); 40 | 41 | it('renders a component', () => { 42 | const wrapper = shallow(); 43 | expect(wrapper.find('div.views').find(NetworksDropdown)).toHaveLength(1); 44 | }); 45 | 46 | it('renders a span with an id of `depends_on` inside of div.views', () => { 47 | const wrapper = shallow(); 48 | expect(wrapper.find('div.views').find('span#depends_on')).toHaveLength(1); 49 | }); 50 | 51 | it('fires a click event for span#depends_on in div.views', () => { 52 | let fakeState = ''; 53 | const onButtonClick = jest.fn(id => (fakeState = id)); 54 | const wrapper = shallow( 55 | , 56 | ); 57 | wrapper 58 | .find('div.views') 59 | .find('span#depends_on') 60 | .simulate('click', { currentTarget: { id: 'tester' } }); 61 | expect(onButtonClick.mock.calls.length).toBe(1); 62 | expect(fakeState).toBe('tester'); 63 | }); 64 | 65 | it('renders a span with an id of `depends_on`', () => { 66 | const wrapper = shallow(); 67 | expect(wrapper.find('div.views').find('span#depends_on')).toHaveLength(1); 68 | }); 69 | 70 | // Titles Testing 71 | it('renders a div with a className of `titles`', () => { 72 | const wrapper = shallow(); 73 | expect(wrapper.find('div.titles')).toHaveLength(1); 74 | }); 75 | 76 | it('renders two h2 elements inside of div.titles`', () => { 77 | const wrapper = shallow(); 78 | expect(wrapper.find('div.titles').find('h2')).toHaveLength(2); 79 | }); 80 | 81 | it('renders a div with a className of `vl` inside of div.titles', () => { 82 | const wrapper = shallow(); 83 | expect(wrapper.find('div.titles').find('div.vl')).toHaveLength(1); 84 | }); 85 | 86 | // Options Testing 87 | it('renders a div with a className of `options`', () => { 88 | const wrapper = shallow(); 89 | expect(wrapper.find('div.options')).toHaveLength(1); 90 | }); 91 | 92 | it('renders three spans with className of `option` inside of div.options', () => { 93 | const wrapper = shallow(); 94 | expect(wrapper.find('div.options').find('span.option')).toHaveLength(3); 95 | }); 96 | 97 | it('fires a click event for span#ports inside div.options', () => { 98 | let fakeState = ''; 99 | const onButtonClick = jest.fn(id => (fakeState = id)); 100 | const wrapper = shallow( 101 | , 102 | ); 103 | wrapper 104 | .find('div.options') 105 | .find('span#ports') 106 | .simulate('click', { currentTarget: { id: 'tester' } }); 107 | expect(onButtonClick.mock.calls.length).toBe(1); 108 | expect(fakeState).toBe('tester'); 109 | }); 110 | 111 | it('fires a click event for span#volumes inside div.options', () => { 112 | let fakeState = ''; 113 | const onButtonClick = jest.fn(id => (fakeState = id)); 114 | const wrapper = shallow( 115 | , 116 | ); 117 | wrapper 118 | .find('div.options') 119 | .find('span#volumes') 120 | .simulate('click', { currentTarget: { id: 'tester' } }); 121 | expect(onButtonClick.mock.calls.length).toBe(1); 122 | expect(fakeState).toBe('tester'); 123 | }); 124 | 125 | it('fires a click event for span#selectAll inside div.options', () => { 126 | let fakeState = ''; 127 | const onButtonClick = jest.fn(id => (fakeState = id)); 128 | const wrapper = shallow( 129 | , 130 | ); 131 | wrapper 132 | .find('div.options') 133 | .find('span#selectAll') 134 | .simulate('click', { currentTarget: { id: 'tester' } }); 135 | expect(onButtonClick.mock.calls.length).toBe(1); 136 | expect(fakeState).toBe('tester'); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /__tests__/Volume.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import Volume from '../src/renderer/components/Volume'; 4 | 5 | describe('', () => { 6 | const props = { 7 | volume: 'volume', 8 | color: 'blue', 9 | }; 10 | 11 | const wrapper = shallow(); 12 | 13 | //test number of elements 14 | it('should render four specific classNames', () => { 15 | expect(wrapper.find('.volumeLegend')).toHaveLength(1); 16 | expect(wrapper.find('.volumeColorName')).toHaveLength(1); 17 | expect(wrapper.find('.volumeSvgBox')).toHaveLength(1); 18 | expect(wrapper.find('.volumeSquare')).toHaveLength(1); 19 | }); 20 | 21 | it('should render one svg element', () => { 22 | expect(wrapper.find('svg')).toHaveLength(1); 23 | }); 24 | 25 | it('should render one rect element', () => { 26 | expect(wrapper.find('rect')).toHaveLength(1); 27 | }); 28 | 29 | //test if rendering props 30 | it('make sure fill prop set to color, is passed into rect element', () => { 31 | expect(wrapper.find('.volumeSquare').props().fill).toEqual(props.color); 32 | }); 33 | 34 | it('make sure p tag renders props.volume', () => { 35 | expect(wrapper.find('p').html()).toEqual(`

${props.volume}

`); 36 | }); 37 | 38 | //test if parent nodes are correct 39 | it('is svg element the parent of rect element', () => { 40 | expect( 41 | wrapper 42 | .find('rect') 43 | .parent() 44 | .is('svg'), 45 | ).toEqual(true); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /__tests__/Volumes.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import Volumes from '../src/renderer/components/Volumes'; 4 | 5 | type Props = { 6 | volumes: any; 7 | getColor: any; 8 | }; 9 | 10 | describe('', () => { 11 | const props: Props = { 12 | volumes: { test1: '1', test2: '2' }, 13 | getColor: jest.fn(() => { 14 | return 'hsl(80%,60%,60%)'; 15 | }), 16 | }; 17 | 18 | const wrapper = shallow(); 19 | 20 | //test to check if right number of components / elements are rendered 21 | 22 | it('contains # of Volume components based on volumes length', () => { 23 | const volumesCount = Object.keys(props.volumes).length; 24 | expect(wrapper.find('Volume').length).toEqual(volumesCount); 25 | expect(wrapper.find('Volume').length).not.toBe(1); 26 | }); 27 | 28 | it('returns a one instance of class: volumes', () => { 29 | expect(wrapper.find('.volumes')).toHaveLength(1); 30 | expect(wrapper.find('.volumes')).not.toHaveLength(0); 31 | }); 32 | 33 | it('should render only one div', () => { 34 | expect(wrapper.find('div')).toHaveLength(1); 35 | expect(wrapper.find('div')).not.toHaveLength(2); 36 | }); 37 | 38 | //test to see if props are passed down 39 | 40 | it('expect child component to have props of color and volume', () => { 41 | expect(wrapper.find('Volume').get(0).props).toEqual({ 42 | color: 'hsl(80%,60%,60%)', 43 | volume: 'test1', 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/App.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Testing App Stateful Component renders correctly 1`] = ` 4 |
7 |
10 |
13 |
16 |
19 | 22 |

23 | Nautilus 24 |

25 |
26 |
27 |
30 |

31 | 32 |

33 |
36 |
39 |
42 | Please select a container with details to display. 43 |
44 |
45 |
46 |
47 |
48 |
51 |
54 |
57 | 79 | 84 | depends on 85 | 86 |
87 |
90 |

91 | Views 92 |

93 |
96 |

97 | Options 98 |

99 |
100 |
103 | 108 | ports 109 | 110 | 115 | volumes 116 | 117 | 122 | select all 123 | 124 |
125 |
126 |
129 |
132 |
135 | 166 | 178 |
179 |
180 |
181 |
182 |
183 | `; 184 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/LeftNav.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`test the functionality of LeftNav component renders correctly 1`] = ` 4 |
7 |
10 |
13 | 16 |

17 | Nautilus 18 |

19 |
20 |
23 | 54 | 66 |
67 |
68 |
71 |

72 | Db 73 |

74 |
77 |
80 |
83 |
84 | 87 | Image: 88 | 89 | 92 | postgres 93 | 94 |
95 |
96 | 99 | Environment: 100 | 101 | 104 |
    105 |
  • 108 | 109 | POSTGRES_MULTIPLE_DATABASES 110 | : 111 | 112 | 113 | dpc_attribution,dpc_queue,dpc_auth,dpc_consent 114 |
  • 115 |
  • 118 | 119 | POSTGRES_USER 120 | : 121 | 122 | 123 | postgres 124 |
  • 125 |
  • 128 | 129 | POSTGRES_PASSWORD 130 | : 131 | 132 | 133 | dpc-safe 134 |
  • 135 |
136 |
137 |
138 |
139 | 142 | Ports: 143 | 144 | 147 |
    148 |
  • 151 | 5432:5432 152 |
  • 153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 | `; 162 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/NetworksDropdown.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Test Networks Dropdown Component renders correctly 1`] = ` 4 | 40 | `; 41 | -------------------------------------------------------------------------------- /__tests__/fileProcessing.ts: -------------------------------------------------------------------------------- 1 | import runDockerComposeValidation from '../src/common/dockerComposeValidation'; 2 | import convertYamlToState from '../src/renderer/helpers/yamlParser'; 3 | import fs from 'fs'; 4 | import yaml from 'js-yaml'; 5 | import path from 'path'; 6 | 7 | describe('Process Yaml File', () => { 8 | test('should run docker file validation and populate result object with errors', () => { 9 | try { 10 | expect( 11 | runDockerComposeValidation( 12 | path.resolve(__dirname, '../samples/docker-composeBAD.yml'), 13 | ), 14 | ).resolves.toMatchObject({ 15 | out: '', 16 | filePath: path.resolve(__dirname, '../samples/docker-composeBAD.yml'), 17 | }); 18 | } catch (e) { 19 | expect(e.cmd).toBe( 20 | `docker-compose -f ${path.resolve( 21 | __dirname, 22 | '../samples/docker-composeBAD.yml', 23 | )} config`, 24 | ); 25 | expect(e.code).toBe(1); 26 | expect(e.killed).toBe(false); 27 | expect(e.signal).toBe(null); 28 | } 29 | }); 30 | test('should convert yaml file into state', () => { 31 | const yamlText = fs.readFileSync( 32 | path.resolve(__dirname, '../samples/docker-compose.bpc.yml'), 33 | ); 34 | const yamlJS = yaml.safeLoad(yamlText.toString()); 35 | const correctYamlState = JSON.parse( 36 | fs.readFileSync(path.resolve(__dirname, './yamlState.json')).toString(), 37 | ); 38 | const yamlState = convertYamlToState(yamlJS); 39 | expect(yamlState).toEqual(correctYamlState); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /__tests__/getSimulationDimensions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getHorizontalPosition, 3 | getVerticalPosition, 4 | } from '../src/renderer/helpers/getSimulationDimensions'; 5 | 6 | // IMPORT TYPES 7 | import { SNode } from '../src/renderer/App.d'; 8 | 9 | describe('Get Simulation Dimensions', () => { 10 | let node: SNode; 11 | let treeDepth: number; 12 | let width: number; 13 | let height: number; 14 | 15 | beforeEach(() => { 16 | node = { 17 | id: 1, 18 | name: 'test', 19 | ports: [], 20 | volumes: [], 21 | row: 0, 22 | column: 0, 23 | rowLength: 0, 24 | children: {}, 25 | }; 26 | width = 500; 27 | height = 500; 28 | treeDepth = 0; 29 | }); 30 | 31 | describe('getHorizontalPosition()', () => { 32 | it('should return the horizontal position of node', () => { 33 | node.rowLength = 1; 34 | node.column = 1; 35 | const horizontalPosition = getHorizontalPosition(node, width); 36 | expect(horizontalPosition).toBe(235); 37 | }); 38 | }); 39 | 40 | describe('getVerticalPosition()', () => { 41 | it('should return vertical position of node with treeDepth 1, row 1', () => { 42 | treeDepth = 1; 43 | node.row = 1; 44 | const verticalPosition = getVerticalPosition(node, treeDepth, height); 45 | expect(verticalPosition).toBe(500); 46 | }); 47 | 48 | it('should return vertical position of node with treeDepth 2', () => { 49 | treeDepth = 2; 50 | node.row = 1; 51 | let verticalPosition = getVerticalPosition(node, treeDepth, height); 52 | expect(verticalPosition).toBe(250); 53 | node.row = 2; 54 | verticalPosition = getVerticalPosition(node, treeDepth, height); 55 | expect(verticalPosition).toBe(500); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /__tests__/setD3State.ts: -------------------------------------------------------------------------------- 1 | import setD3State, { 2 | extractPorts, 3 | extractVolumes, 4 | extractNetworks, 5 | dagCreator, 6 | } from '../src/renderer/helpers/setD3State'; 7 | import { 8 | Ports, 9 | Volumes, 10 | SNode, 11 | Link, 12 | Services, 13 | D3State, 14 | } from '../src/renderer/App.d'; 15 | 16 | describe('Set D3 State Functions', () => { 17 | describe('extractPorts()', () => { 18 | let portsData: Ports; 19 | 20 | it('should extract ports if using short syntax', () => { 21 | portsData = '3000:3000'; 22 | expect(extractPorts(portsData)).toEqual(['3000:3000']); 23 | portsData = ['3000:3000', '80:80']; 24 | expect(extractPorts(portsData)[1]).toEqual('80:80'); 25 | }); 26 | 27 | it('should extract ports if using long syntax', () => { 28 | portsData = [ 29 | { 30 | mode: 'test', 31 | protocol: 'test', 32 | published: 5050, 33 | target: 5050, 34 | }, 35 | ]; 36 | expect(extractPorts(portsData)[0]).toEqual('5050:5050'); 37 | }); 38 | }); 39 | 40 | describe('extractVolumes()', () => { 41 | let volumesData: Volumes; 42 | 43 | it('should extract volumes if using short syntax', () => { 44 | volumesData = [ 45 | '/opt/data/:/var/lib/mysql', 46 | 'datavolume:/var/lib/datavolume', 47 | ]; 48 | expect(extractVolumes(volumesData)[0]).toEqual( 49 | '/opt/data/:/var/lib/mysql', 50 | ); 51 | expect(extractVolumes(volumesData)[1]).toEqual( 52 | 'datavolume:/var/lib/datavolume', 53 | ); 54 | }); 55 | 56 | it('should extract volumes if using long syntax', () => { 57 | volumesData = [ 58 | { 59 | type: 'test', 60 | source: 'myTestVolume', 61 | target: '/var/src/myTestVolume', 62 | }, 63 | ]; 64 | expect(extractVolumes(volumesData)[0]).toEqual( 65 | 'myTestVolume:/var/src/myTestVolume', 66 | ); 67 | }); 68 | }); 69 | 70 | describe('extractNetworks()', () => { 71 | let networksData: string[] | {}; 72 | it('should handle networks as an array', () => { 73 | networksData = ['main', 'secondary', 'third']; 74 | expect(extractNetworks(networksData)).toEqual([ 75 | 'main', 76 | 'secondary', 77 | 'third', 78 | ]); 79 | }); 80 | 81 | it('should handle networks as an object', () => { 82 | networksData = { 83 | main: {}, 84 | secondary: {}, 85 | third: {}, 86 | }; 87 | expect(extractNetworks(networksData)).toEqual([ 88 | 'main', 89 | 'secondary', 90 | 'third', 91 | ]); 92 | }); 93 | }); 94 | 95 | describe('dagCreator()', () => { 96 | let nodes: SNode[]; 97 | let links: Link[]; 98 | 99 | beforeEach(() => { 100 | nodes = [ 101 | { 102 | id: 0, 103 | name: 'web', 104 | ports: [], 105 | volumes: [], 106 | networks: [], 107 | children: {}, 108 | row: 0, 109 | rowLength: 0, 110 | column: 0, 111 | }, 112 | { 113 | id: 1, 114 | name: 'user-api', 115 | ports: [], 116 | volumes: [], 117 | networks: [], 118 | children: {}, 119 | row: 0, 120 | rowLength: 0, 121 | column: 0, 122 | }, 123 | { 124 | id: 2, 125 | name: 'feed-api', 126 | ports: [], 127 | volumes: [], 128 | networks: [], 129 | children: {}, 130 | row: 0, 131 | rowLength: 0, 132 | column: 0, 133 | }, 134 | { 135 | id: 3, 136 | name: 'db', 137 | ports: [], 138 | volumes: [], 139 | networks: [], 140 | children: {}, 141 | row: 0, 142 | rowLength: 0, 143 | column: 0, 144 | }, 145 | { 146 | id: 4, 147 | name: 'web2', 148 | ports: [], 149 | volumes: [], 150 | networks: [], 151 | children: {}, 152 | row: 0, 153 | rowLength: 0, 154 | column: 0, 155 | }, 156 | ]; 157 | links = [ 158 | { 159 | source: 'db', 160 | target: 'user-api', 161 | }, 162 | { 163 | source: 'db', 164 | target: 'feed-api', 165 | }, 166 | { 167 | source: 'feed-api', 168 | target: 'web', 169 | }, 170 | { 171 | source: 'user-api', 172 | target: 'web', 173 | }, 174 | ]; 175 | dagCreator(nodes, links); 176 | }); 177 | 178 | it('should return correct tree depth', () => { 179 | expect(dagCreator(nodes, links)).toBe(3); 180 | }); 181 | 182 | it('should handle multiple roots', () => { 183 | expect(nodes[3].row).toBe(0); 184 | expect(nodes[4].row).toBe(0); 185 | }); 186 | 187 | it('should place nodes at correct row in tree', () => { 188 | expect(nodes[0].row).toBe(2); 189 | expect(nodes[1].row).toBe(1); 190 | expect(nodes[2].row).toBe(1); 191 | }); 192 | 193 | it('should give nodes correct row length', () => { 194 | /** 195 | * POSSIBLE ERROR 196 | * if child has multiple parents, it creates column for each parent 197 | */ 198 | expect(nodes[0].rowLength).toBe(2); 199 | expect(nodes[1].rowLength).toBe(2); 200 | expect(nodes[3].rowLength).toBe(2); 201 | }); 202 | 203 | it('should give nodes in same row different columns', () => { 204 | expect(nodes[3].column).not.toBe(nodes[4].column); 205 | expect(nodes[1].column).not.toBe(nodes[2].column); 206 | }); 207 | }); 208 | 209 | describe('Main Func: setD3State()', () => { 210 | let d3State: D3State; 211 | let services: Services = { 212 | db: { 213 | image: 'postgres', 214 | environment: [ 215 | 'POSTGRES_MULTIPLE_DATABASES=dpc_attribution,dpc_queue,dpc_auth,dpc_consent', 216 | 'POSTGRES_USER=postgres', 217 | 'POSTGRES_PASSWORD=dpc-safe', 218 | ], 219 | ports: ['5432:5432'], 220 | volumes: ['./docker/postgres:/docker-entrypoint-initdb.d'], 221 | }, 222 | aggregation: { 223 | image: 224 | '${ECR_HOST:-755619740999.dkr.ecr.us-east-1.amazonaws.com/dpc-aggregation}:latest', 225 | ports: ['9901:9900'], 226 | env_file: ['./ops/config/decrypted/local.env'], 227 | environment: ['ENV=local', 'JACOCO=${REPORT_COVERAGE}'], 228 | depends_on: ['db'], 229 | volumes: [ 230 | 'export-volume:/app/data', 231 | './jacocoReport/dpc-aggregation:/jacoco-report', 232 | ], 233 | }, 234 | attribution: { 235 | image: 236 | '${ECR_HOST:-755619740999.dkr.ecr.us-east-1.amazonaws.com/dpc-attribution}:latest', 237 | depends_on: ['db'], 238 | environment: ['ENV=local', 'JACOCO=${REPORT_COVERAGE}'], 239 | ports: ['3500:8080', '9902:9900'], 240 | volumes: ['./jacocoReport/dpc-attribution:/jacoco-report'], 241 | }, 242 | api: { 243 | image: 244 | '${ECR_HOST:-755619740999.dkr.ecr.us-east-1.amazonaws.com/dpc-api}:latest', 245 | ports: ['3002:3002', '9903:9900'], 246 | environment: [ 247 | 'attributionURL=http://attribution:8080/v1/', 248 | 'ENV=local', 249 | 'JACOCO=${REPORT_COVERAGE}', 250 | 'exportPath=/app/data', 251 | 'JVM_FLAGS=-Ddpc.api.authenticationDisabled=${AUTH_DISABLED:-false}', 252 | ], 253 | depends_on: ['attribution'], 254 | volumes: [ 255 | 'export-volume:/app/data', 256 | './jacocoReport/dpc-api:/jacoco-report', 257 | ], 258 | }, 259 | consent: { 260 | image: 261 | '${ECR_HOST:-755619740999.dkr.ecr.us-east-1.amazonaws.com/dpc-consent}:latest', 262 | depends_on: ['db'], 263 | environment: ['ENV=local', 'JACOCO=${REPORT_COVERAGE}'], 264 | ports: ['3600:3600', '9004:9900'], 265 | volumes: ['./jacocoReport/dpc-consent:/jacoco-report'], 266 | }, 267 | start_core_dependencies: { 268 | image: 'dadarek/wait-for-dependencies', 269 | depends_on: ['db'], 270 | command: 'db:5432', 271 | }, 272 | start_api_dependencies: { 273 | image: 'dadarek/wait-for-dependencies', 274 | depends_on: ['attribution', 'aggregation'], 275 | command: 'attribution:8080 aggregation:9900', 276 | }, 277 | start_api: { 278 | image: 'dadarek/wait-for-dependencies', 279 | depends_on: ['api'], 280 | command: 'api:3002', 281 | }, 282 | }; 283 | 284 | beforeAll(() => { 285 | d3State = setD3State(services); 286 | }); 287 | 288 | it('should return an object with the correct properties', () => { 289 | expect(d3State.hasOwnProperty('treeDepth')).toBe(true); 290 | expect(d3State.hasOwnProperty('serviceGraph')).toBe(true); 291 | expect(d3State.hasOwnProperty('simulation')).toBe(true); 292 | }); 293 | 294 | it('should create a node for all services', () => { 295 | expect(d3State.serviceGraph.nodes.length).toBe(8); 296 | }); 297 | 298 | it('should give nodes the correct name', () => { 299 | const db = d3State.serviceGraph.nodes.filter(node => node.name === 'db'); 300 | expect(db.length).toBe(1); 301 | expect(db[0].name).toBe('db'); 302 | }); 303 | 304 | it('should create a link for each depends on connection', () => { 305 | expect(d3State.serviceGraph.links.length).toBe(8); 306 | }); 307 | 308 | it('should give links correct target and source', () => { 309 | const dbLinks = d3State.serviceGraph.links.filter( 310 | link => link.source === 'db', 311 | ); 312 | expect(dbLinks.length).toBe(4); 313 | const targets = dbLinks.filter( 314 | link => 315 | link.target === 'start_core_dependencies' || 316 | link.target === 'consent' || 317 | link.target === 'attribution' || 318 | link.target === 'aggregation', 319 | ); 320 | expect(targets.length).toBe(4); 321 | }); 322 | 323 | it('should contain the correct treeDepth', () => { 324 | expect(d3State.treeDepth).toBe(4); 325 | }); 326 | }); 327 | }); 328 | -------------------------------------------------------------------------------- /__tests__/yamlState.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileOpened": true, 3 | "services": { 4 | "db": { 5 | "image": "postgres", 6 | "environment": [ 7 | "POSTGRES_MULTIPLE_DATABASES=dpc_attribution,dpc_queue,dpc_auth,dpc_consent", 8 | "POSTGRES_USER=postgres", 9 | "POSTGRES_PASSWORD=dpc-safe" 10 | ], 11 | "ports": ["5432:5432"], 12 | "volumes": ["./docker/postgres:/docker-entrypoint-initdb.d"] 13 | }, 14 | "aggregation": { 15 | "image": "${ECR_HOST:-755619740999.dkr.ecr.us-east-1.amazonaws.com/dpc-aggregation}:latest", 16 | "ports": ["9901:9900"], 17 | "env_file": ["./ops/config/decrypted/local.env"], 18 | "environment": ["ENV=local", "JACOCO=${REPORT_COVERAGE}"], 19 | "depends_on": ["db"], 20 | "volumes": [ 21 | "export-volume:/app/data", 22 | "./jacocoReport/dpc-aggregation:/jacoco-report" 23 | ] 24 | }, 25 | "attribution": { 26 | "image": "${ECR_HOST:-755619740999.dkr.ecr.us-east-1.amazonaws.com/dpc-attribution}:latest", 27 | "depends_on": ["db"], 28 | "environment": ["ENV=local", "JACOCO=${REPORT_COVERAGE}"], 29 | "ports": ["3500:8080", "9902:9900"], 30 | "volumes": ["./jacocoReport/dpc-attribution:/jacoco-report"] 31 | }, 32 | "api": { 33 | "image": "${ECR_HOST:-755619740999.dkr.ecr.us-east-1.amazonaws.com/dpc-api}:latest", 34 | "ports": ["3002:3002", "9903:9900"], 35 | "environment": [ 36 | "attributionURL=http://attribution:8080/v1/", 37 | "ENV=local", 38 | "JACOCO=${REPORT_COVERAGE}", 39 | "exportPath=/app/data", 40 | "JVM_FLAGS=-Ddpc.api.authenticationDisabled=${AUTH_DISABLED:-false}" 41 | ], 42 | "depends_on": ["attribution"], 43 | "volumes": [ 44 | "export-volume:/app/data", 45 | "./jacocoReport/dpc-api:/jacoco-report" 46 | ] 47 | }, 48 | "consent": { 49 | "image": "${ECR_HOST:-755619740999.dkr.ecr.us-east-1.amazonaws.com/dpc-consent}:latest", 50 | "depends_on": ["db"], 51 | "environment": ["ENV=local", "JACOCO=${REPORT_COVERAGE}"], 52 | "ports": ["3600:3600", "9004:9900"], 53 | "volumes": ["./jacocoReport/dpc-consent:/jacoco-report"] 54 | }, 55 | "start_core_dependencies": { 56 | "image": "dadarek/wait-for-dependencies", 57 | "depends_on": ["db"], 58 | "command": "db:5432" 59 | }, 60 | "start_api_dependencies": { 61 | "image": "dadarek/wait-for-dependencies", 62 | "depends_on": ["attribution", "aggregation"], 63 | "command": "attribution:8080 aggregation:9900" 64 | }, 65 | "start_api": { 66 | "image": "dadarek/wait-for-dependencies", 67 | "depends_on": ["api"], 68 | "command": "api:3002" 69 | } 70 | }, 71 | "volumes": { 72 | "export-volume": { 73 | "driver": "local", 74 | "driver_opts": { "type": "none", "device": "/tmp", "o": "bind" } 75 | } 76 | }, 77 | "networks": {}, 78 | "bindMounts": [ 79 | "./docker/postgres", 80 | "./jacocoReport/dpc-aggregation", 81 | "./jacocoReport/dpc-attribution", 82 | "./jacocoReport/dpc-api", 83 | "./jacocoReport/dpc-consent" 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | '@babel/preset-react', 5 | '@babel/preset-typescript', 6 | ], 7 | plugins: ['@babel/plugin-proposal-class-properties'], 8 | }; 9 | -------------------------------------------------------------------------------- /build/background.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nautilusapp/nautilus/3dae444d82a9e16262e75f7ed6be9ca0312143b9/build/background.tiff -------------------------------------------------------------------------------- /build/nautilus_logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nautilusapp/nautilus/3dae444d82a9e16262e75f7ed6be9ca0312143b9/build/nautilus_logo.icns -------------------------------------------------------------------------------- /build/nautilus_logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nautilusapp/nautilus/3dae444d82a9e16262e75f7ed6be9ca0312143b9/build/nautilus_logo.ico -------------------------------------------------------------------------------- /build/nautilus_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nautilusapp/nautilus/3dae444d82a9e16262e75f7ed6be9ca0312143b9/build/nautilus_logo.png -------------------------------------------------------------------------------- /electron-webpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "commonSourceDirectory": "src/common", 3 | "staticSourceDirectory": "static", 4 | "title": "Nautilus", 5 | 6 | "main": { 7 | "sourceDirectory": "src/main", 8 | "webpackConfig": "webpack.main.ext.js" 9 | }, 10 | 11 | "renderer": { 12 | "sourceDirectory": "src/renderer", 13 | "webpackConfig": "webpack.renderer.ext.js" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nautilus", 3 | "version": "1.3.1", 4 | "description": "A Docker Compose Charting Tool", 5 | "scripts": { 6 | "start": "cross-env NODE_ENV=production NOT_PACKAGE=true electron ./dist/main/main.js", 7 | "dev": "cross-env NODE_ENV=development electron-webpack dev", 8 | "compile": "cross-env NODE_ENV=production electron-webpack", 9 | "package-all": "yarn compile && electron-builder -mwl", 10 | "package-mac": "yarn compile && electron-builder --mac", 11 | "package-linux": "yarn compile && electron-builder --linux", 12 | "package-win": "yarn compile && electron-builder --win --x64", 13 | "deploy": "yarn package-all && bash scripts/deploy.sh", 14 | "test": "jest", 15 | "test-f": "bash ./scripts/run-test.sh", 16 | "up-snap": "jest --updateSnapshot" 17 | }, 18 | "jest": { 19 | "setupFilesAfterEnv": [ 20 | "./src/setupTests.ts" 21 | ], 22 | "testEnvironment": "enzyme" 23 | }, 24 | "build": { 25 | "appId": "com.nautilus.app", 26 | "productName": "Nautilus", 27 | "files": [ 28 | "dist", 29 | "static", 30 | "package.json" 31 | ], 32 | "linux": { 33 | "category": "WebDevelopment", 34 | "icon": "build/nautilus_logo.icns", 35 | "desktop": { 36 | "Comment": "Nautilus: A Docker Compose Charting Tool", 37 | "Name": "Nautilus", 38 | "StartupNotify": "true", 39 | "Terminal": "false", 40 | "Type": "Application", 41 | "Categories": "WebDevelopment" 42 | }, 43 | "executableName": "nautilus", 44 | "maintainer": "michaelxhdinh@gmail.com", 45 | "target": [ 46 | "AppImage" 47 | ] 48 | }, 49 | "mac": { 50 | "category": "public.app-category.developer-tools", 51 | "icon": "build/nautilus_logo.icns", 52 | "target": [ 53 | "dmg" 54 | ] 55 | }, 56 | "dmg": { 57 | "background": "build/background.tiff", 58 | "contents": [ 59 | { 60 | "x": 100, 61 | "y": 400 62 | }, 63 | { 64 | "x": 550, 65 | "y": 400, 66 | "type": "link", 67 | "path": "/Applications" 68 | } 69 | ] 70 | }, 71 | "win": { 72 | "asar": false, 73 | "icon": "build/nautilus_logo.ico", 74 | "target": [ 75 | "nsis" 76 | ] 77 | }, 78 | "nsis": { 79 | "createStartMenuShortcut": true, 80 | "createDesktopShortcut": true, 81 | "runAfterFinish": true 82 | }, 83 | "directories": { 84 | "output": "release" 85 | } 86 | }, 87 | "repository": { 88 | "type": "git", 89 | "url": "git+https://github.com/oslabs-beta/nautilis.git" 90 | }, 91 | "keywords": [ 92 | "docker", 93 | "compose", 94 | "visualizer" 95 | ], 96 | "author": "team nautilis", 97 | "license": "MIT", 98 | "bugs": { 99 | "url": "https://github.com/oslabs-beta/nautilis/issues" 100 | }, 101 | "homepage": "https://github.com/oslabs-beta/nautilis#readme", 102 | "dependencies": { 103 | "d3": "^5.15.0", 104 | "electron-devtools-installer": "^2.2.4", 105 | "eslint": "^6.8.0", 106 | "fix-path": "^3.0.0", 107 | "html-loader": "1.0.0-alpha.0", 108 | "js-yaml": "^3.13.1", 109 | "react": "^16.13.0", 110 | "react-dom": "^16.13.0", 111 | "react-icons": "^3.9.0" 112 | }, 113 | "devDependencies": { 114 | "@babel/core": "^7.0.0-0", 115 | "@babel/plugin-proposal-class-properties": "^7.8.3", 116 | "@babel/preset-env": "^7.9.0", 117 | "@babel/preset-react": "^7.9.4", 118 | "@babel/preset-typescript": "^7.9.0", 119 | "@types/d3": "^5.7.2", 120 | "@types/electron-devtools-installer": "^2.2.0", 121 | "@types/enzyme": "^3.10.5", 122 | "@types/enzyme-adapter-react-16": "^1.0.6", 123 | "@types/eslint": "^6.1.8", 124 | "@types/express": "^4.17.3", 125 | "@types/jest": "^25.1.4", 126 | "@types/js-yaml": "^3.12.2", 127 | "@types/node": "^13.9.1", 128 | "@types/react": "^16.9.23", 129 | "@types/react-dom": "^16.9.5", 130 | "@types/react-test-renderer": "^16.9.2", 131 | "@typescript-eslint/eslint-plugin": "^2.24.0", 132 | "@typescript-eslint/parser": "^2.24.0", 133 | "babel": "^6.23.0", 134 | "babel-jest": "^25.2.4", 135 | "concurrently": "^5.1.0", 136 | "cross-env": "^7.0.2", 137 | "css-loader": "^3.4.2", 138 | "electron": "^8.1.1", 139 | "electron-builder": "^22.4.1", 140 | "electron-devtools-installer": "^2.2.4", 141 | "electron-webpack": "^2.7.4", 142 | "enzyme": "^3.11.0", 143 | "enzyme-adapter-react-16": "^1.15.2", 144 | "jest": "^25.2.4", 145 | "jest-environment-enzyme": "^7.1.2", 146 | "jest-enzyme": "^7.1.2", 147 | "mini-css-extract-plugin": "^0.9.0", 148 | "node-sass": "^4.13.1", 149 | "nodemon": "^2.0.2", 150 | "react-hot-loader": "^4.12.20", 151 | "react-test-renderer": "^16.13.1", 152 | "sass-loader": "^8.0.2", 153 | "source-map-loader": "^0.2.4", 154 | "style-loader": "^1.1.3", 155 | "ts-loader": "^6.2.1", 156 | "typescript": "^3.8.3", 157 | "url-loader": "^3.0.0", 158 | "webpack": "^4.42.0", 159 | "webpack-cli": "^3.3.11", 160 | "webpack-dev-server": "^3.10.3" 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /samples/docker-compose.bpc.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | db: 5 | image: postgres 6 | environment: 7 | - POSTGRES_MULTIPLE_DATABASES=dpc_attribution,dpc_queue,dpc_auth,dpc_consent 8 | - POSTGRES_USER=postgres 9 | - POSTGRES_PASSWORD=dpc-safe 10 | ports: 11 | - '5432:5432' 12 | volumes: 13 | - ./docker/postgres:/docker-entrypoint-initdb.d 14 | 15 | aggregation: 16 | image: ${ECR_HOST:-755619740999.dkr.ecr.us-east-1.amazonaws.com/dpc-aggregation}:latest 17 | ports: 18 | - '9901:9900' 19 | env_file: 20 | - ./ops/config/decrypted/local.env 21 | environment: 22 | - ENV=local 23 | - JACOCO=${REPORT_COVERAGE} 24 | depends_on: 25 | - db 26 | volumes: 27 | - export-volume:/app/data 28 | - ./jacocoReport/dpc-aggregation:/jacoco-report 29 | 30 | attribution: 31 | image: ${ECR_HOST:-755619740999.dkr.ecr.us-east-1.amazonaws.com/dpc-attribution}:latest 32 | depends_on: 33 | - db 34 | environment: 35 | - ENV=local 36 | - JACOCO=${REPORT_COVERAGE} 37 | ports: 38 | - '3500:8080' 39 | - '9902:9900' 40 | volumes: 41 | - ./jacocoReport/dpc-attribution:/jacoco-report 42 | 43 | api: 44 | image: ${ECR_HOST:-755619740999.dkr.ecr.us-east-1.amazonaws.com/dpc-api}:latest 45 | ports: 46 | - '3002:3002' 47 | - '9903:9900' 48 | environment: 49 | - attributionURL=http://attribution:8080/v1/ 50 | - ENV=local 51 | - JACOCO=${REPORT_COVERAGE} 52 | - exportPath=/app/data 53 | - JVM_FLAGS=-Ddpc.api.authenticationDisabled=${AUTH_DISABLED:-false} 54 | depends_on: 55 | - attribution 56 | volumes: 57 | - export-volume:/app/data 58 | - ./jacocoReport/dpc-api:/jacoco-report 59 | 60 | consent: 61 | image: ${ECR_HOST:-755619740999.dkr.ecr.us-east-1.amazonaws.com/dpc-consent}:latest 62 | depends_on: 63 | - db 64 | environment: 65 | - ENV=local 66 | - JACOCO=${REPORT_COVERAGE} 67 | ports: 68 | - '3600:3600' 69 | - '9004:9900' 70 | volumes: 71 | - ./jacocoReport/dpc-consent:/jacoco-report 72 | 73 | start_core_dependencies: 74 | image: dadarek/wait-for-dependencies 75 | depends_on: 76 | - db 77 | command: db:5432 78 | 79 | start_api_dependencies: 80 | image: dadarek/wait-for-dependencies 81 | depends_on: 82 | - attribution 83 | - aggregation 84 | command: attribution:8080 aggregation:9900 85 | 86 | start_api: 87 | image: dadarek/wait-for-dependencies 88 | depends_on: 89 | - api 90 | command: api:3002 91 | 92 | volumes: 93 | export-volume: 94 | driver: local 95 | driver_opts: 96 | type: none 97 | device: /tmp 98 | o: bind 99 | -------------------------------------------------------------------------------- /samples/docker-compose.taiga.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | back: 5 | image: dockertaiga/back:5.0.7 6 | container_name: taiga-back 7 | restart: unless-stopped 8 | depends_on: 9 | - db 10 | - events 11 | networks: 12 | - taiga 13 | volumes: 14 | - ./data/media:/taiga-media 15 | - ./conf/back:/taiga-conf 16 | env_file: 17 | - variables.env 18 | 19 | front: 20 | image: dockertaiga/front:5.0.7 21 | container_name: taiga-front 22 | restart: unless-stopped 23 | networks: 24 | - taiga 25 | volumes: 26 | - ./conf/front:/taiga-conf 27 | env_file: 28 | - variables.env 29 | 30 | db: 31 | image: postgres:11-alpine 32 | container_name: taiga-db 33 | restart: unless-stopped 34 | networks: 35 | - taiga 36 | env_file: 37 | - variables.env 38 | volumes: 39 | - ./data/db:/var/lib/postgresql/data 40 | 41 | rabbit: 42 | image: dockertaiga/rabbit 43 | container_name: taiga-rabbit 44 | restart: unless-stopped 45 | networks: 46 | - taiga 47 | env_file: 48 | - variables.env 49 | 50 | redis: 51 | image: bitnami/redis:5.0 52 | container_name: taiga-redis 53 | networks: 54 | - taiga 55 | env_file: 56 | - variables.env 57 | 58 | events: 59 | image: dockertaiga/events 60 | container_name: taiga-events 61 | restart: unless-stopped 62 | depends_on: 63 | - rabbit 64 | networks: 65 | - taiga 66 | env_file: 67 | - variables.env 68 | 69 | proxy: 70 | image: dockertaiga/proxy 71 | container_name: taiga-proxy 72 | restart: unless-stopped 73 | depends_on: 74 | - back 75 | - front 76 | - events 77 | networks: 78 | - taiga 79 | ports: 80 | - 80:80 81 | - 443:443 82 | volumes: 83 | #- ./cert:/taiga-cert 84 | - ./conf/proxy:/taiga-conf 85 | env_file: 86 | - variables.env 87 | 88 | networks: 89 | taiga: 90 | -------------------------------------------------------------------------------- /samples/docker-compose1.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | vote: 5 | build: ./vote 6 | command: python app.py 7 | volumes: 8 | - ./vote:/app 9 | ports: 10 | - '5000:80' 11 | networks: 12 | - front-tier 13 | - back-tier 14 | 15 | result: 16 | build: ./result 17 | command: nodemon server.js 18 | volumes: 19 | - ./result:/app 20 | ports: 21 | - '5001:80' 22 | - '5858:5858' 23 | networks: 24 | - front-tier 25 | - back-tier 26 | 27 | worker: 28 | build: 29 | context: ./worker 30 | depends_on: 31 | - 'redis' 32 | - 'db' 33 | networks: 34 | - back-tier 35 | 36 | redis: 37 | image: redis:alpine 38 | container_name: redis 39 | ports: ['6379'] 40 | networks: 41 | - back-tier 42 | 43 | db: 44 | image: postgres:9.4 45 | container_name: db 46 | environment: 47 | POSTGRES_USER: 'postgres' 48 | POSTGRES_PASSWORD: 'postgres' 49 | volumes: 50 | - 'db-data:/var/lib/postgresql/data' 51 | networks: 52 | - back-tier 53 | 54 | volumes: 55 | db-data: 56 | 57 | networks: 58 | front-tier: 59 | back-tier: 60 | -------------------------------------------------------------------------------- /samples/docker-compose2.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | common: 4 | build: 5 | context: ./ 6 | dockerfile: ./net/grpc/gateway/docker/common/Dockerfile 7 | image: grpcweb/common 8 | node-server: 9 | build: 10 | context: ./ 11 | dockerfile: ./net/grpc/gateway/docker/node_server/Dockerfile 12 | depends_on: 13 | - common 14 | image: grpcweb/node-server 15 | ports: 16 | - "9090:9090" 17 | envoy: 18 | build: 19 | context: ./ 20 | dockerfile: ./net/grpc/gateway/docker/envoy/Dockerfile 21 | image: grpcweb/envoy 22 | ports: 23 | - "8080:8080" 24 | links: 25 | - node-server 26 | commonjs-client: 27 | build: 28 | context: ./ 29 | dockerfile: ./net/grpc/gateway/docker/commonjs_client/Dockerfile 30 | depends_on: 31 | - common 32 | image: grpcweb/commonjs-client 33 | ports: 34 | - "8081:8081" 35 | -------------------------------------------------------------------------------- /samples/docker-composeDEMO.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | back: 5 | image: dockertaiga/back:5.0.7 6 | container_name: taiga-back 7 | restart: unless-stopped 8 | depends_on: 9 | - db 10 | - events 11 | networks: 12 | - back-tier 13 | ports: 14 | - 1240:2222 15 | volumes: 16 | - ./data/media:/taiga-media 17 | - ./conf/back:/taiga-conf 18 | env_file: 19 | - variables.env 20 | 21 | front: 22 | image: dockertaiga/front:5.0.7 23 | container_name: taiga-front 24 | restart: unless-stopped 25 | networks: 26 | - front-tier 27 | volumes: 28 | - ./conf/front:/taiga-conf 29 | ports: 30 | - 1020:1022 31 | env_file: 32 | - variables.env 33 | 34 | db: 35 | image: postgres:11-alpine 36 | container_name: taiga-db 37 | restart: unless-stopped 38 | networks: 39 | - back-tier 40 | env_file: 41 | - variables.env 42 | ports: 43 | - 9090:900 44 | - 1020:620 45 | volumes: 46 | - ./data/db:/var/lib/postgresql/data 47 | 48 | rabbit: 49 | image: dockertaiga/rabbit 50 | container_name: taiga-rabbit 51 | restart: unless-stopped 52 | networks: 53 | - extended-tier 54 | env_file: 55 | - variables.env 56 | 57 | redis: 58 | image: bitnami/redis:5.0 59 | container_name: taiga-redis 60 | networks: 61 | - back-tier 62 | ports: 63 | - 120:120 64 | - 1040:445 65 | env_file: 66 | - variables.env 67 | 68 | events: 69 | image: dockertaiga/events 70 | container_name: taiga-events 71 | restart: unless-stopped 72 | depends_on: 73 | - rabbit 74 | networks: 75 | - front-tier 76 | ports: 77 | - 90:90 78 | - 444:444 79 | env_file: 80 | - variables.env 81 | 82 | proxy: 83 | image: dockertaiga/proxy 84 | container_name: taiga-proxy 85 | restart: unless-stopped 86 | depends_on: 87 | - back 88 | - front 89 | - events 90 | networks: 91 | - back-tier 92 | ports: 93 | - 80:80 94 | - 443:443 95 | volumes: 96 | #- ./cert:/taiga-cert 97 | - ./conf/proxy:/taiga-conf 98 | env_file: 99 | - variables.env 100 | 101 | networks: 102 | front-tier: 103 | back-tier: 104 | extended-tier: 105 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | echo "Processing deploy.sh" 2 | # set env variables 3 | source './_secrets/aws_secret.env' 4 | # set s3 bucket as env variable 5 | S3_BUCKET=nautilusdev.com 6 | # set the default region for aws 7 | aws configure set default.region us-west-1 8 | # set aws_access_key id 9 | aws configure set aws_access_key_id $ACCESS_KEY_ID 10 | # set aws_secret_access_key 11 | aws configure set aws_secret_access_key $SECRET_ACCESS_KEY 12 | # sync release build to s3 buckets 13 | aws s3 sync ./release s3://$S3_BUCKET/release --exclude ".icon-set/*" --exclude "linux-unpacked/*" --exclude "mac/*" --exclude "win-unpacked/*" --cache-control max-age=10 --delete -------------------------------------------------------------------------------- /scripts/run-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # check to see if they passed in which hack hour they want to test 4 | if [ -z $1 ]; then 5 | echo -en '\n' 6 | echo -e "\033[0;31mMake sure to format the command properly:\033[0m" 7 | echo -en '\n' 8 | echo -e "\033[01;36mCorrect Syntax:\033[0m 'yarn test-hh ' "; 9 | echo -en '\n' 10 | # check to ensure that the test file being requested exists 11 | elif [ ! -f "./__tests__/$1" ]; then 12 | echo -en '\n' 13 | echo -e "\033[0;31mTest file does not exist for:\033[0m \033[01;36m$1\033[0m" 14 | echo -e "\033[0;31mPlease check your spelling.\033[0m" 15 | echo -e "\033[0;31mIf you think you've gotten this message in error, please speak to a fellow.\033[0m" 16 | echo -en '\n' 17 | # run the test file 18 | else 19 | echo Running tests for $1 20 | echo -en '\n' 21 | # if they passed the hh to test in as .js, then don't add .js to filename 22 | ./node_modules/.bin/jest __tests__/$1 23 | 24 | fi 25 | -------------------------------------------------------------------------------- /src/common/dockerComposeValidation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************ 3 | * @name runDockerComposeValidation 4 | * @input filePath: string, which is the file path to the docker-compose file on host computer 5 | * @output object: storing output of docker-compose shell command 6 | * ************************ 7 | */ 8 | 9 | import child_process from 'child_process'; 10 | import { ValidationResults } from '../renderer/App.d'; 11 | 12 | const dockerComposeValidation = (filePath: string) => 13 | // promise for the electron application 14 | new Promise((resolve, reject) => { 15 | try { 16 | // run docker's validation command in a bash shell 17 | child_process.exec( 18 | `docker-compose -f ${filePath} config`, 19 | // callback function to access output of docker-compose command 20 | (error, stdout, stderr) => { 21 | // add output to object 22 | const validationResult: ValidationResults = { 23 | out: stdout.toString(), 24 | filePath, 25 | envResolutionRequired: false, 26 | }; 27 | // if there is an error, add the error object to validationResult obj 28 | if (error) { 29 | //if docker-compose uses env file to run, store this variable to handle later 30 | if (error.message.includes('variable is not set')) { 31 | validationResult.envResolutionRequired = true; 32 | } 33 | // filter errors we don't care about 34 | if ( 35 | !error.message.includes("Couldn't find env file") && 36 | !error.message.includes( 37 | 'either does not exist, is not accessible', 38 | ) && 39 | !error.message.includes('variable is not set') 40 | ) { 41 | validationResult.error = error; 42 | } 43 | } 44 | // resolve promise when shell command finishes 45 | resolve(validationResult); 46 | }, 47 | ); 48 | } catch {} 49 | }); 50 | 51 | export default dockerComposeValidation; 52 | -------------------------------------------------------------------------------- /src/common/resolveEnvVariables.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | type EnvObject = { 4 | [variableName: string]: string; 5 | }; 6 | 7 | //this function replaces all env variables within the docker compose yaml string with their values in the env file 8 | const resolveEnvVariables = (yamlText: string, filePath: string) => { 9 | const envFileArray = filePath.split('/'); 10 | const envFilePath = 11 | envFileArray.slice(0, envFileArray.length - 1).join('/') + '/.env'; 12 | //read envfile if there is one there 13 | let envString: string; 14 | try { 15 | envString = fs.readFileSync(envFilePath).toString(); 16 | } catch (err) { 17 | return yamlText; 18 | } 19 | let yamlTextCopy = yamlText; 20 | console.log(envString); 21 | //split by line 22 | const envArray = envString.split('\n'); 23 | //remove empty last element 24 | envArray.splice(-1, 1); 25 | //create Object that stores the variable notation to be replace with its value 26 | const envObject: EnvObject = envArray.reduce((acc: EnvObject, el: string) => { 27 | const [variableName, value] = el.split('='); 28 | const variableKey: string = '\\${' + variableName + '}'; 29 | acc[variableKey] = value; 30 | return acc; 31 | }, {}); 32 | //replace the variables 33 | Object.keys(envObject).forEach((variableKey: string) => { 34 | yamlTextCopy = yamlTextCopy.replace( 35 | new RegExp(variableKey, 'g'), 36 | envObject[variableKey], 37 | ); 38 | }); 39 | return yamlTextCopy; 40 | }; 41 | 42 | export default resolveEnvVariables; 43 | -------------------------------------------------------------------------------- /src/main/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************ 3 | * @name main 4 | * @description spins up electron application with specific settings 5 | * ************************ 6 | */ 7 | import { app, BrowserWindow } from 'electron'; 8 | import path from 'path'; 9 | import url from 'url'; 10 | import createMenu from './menu'; 11 | import installExtension, { 12 | REACT_DEVELOPER_TOOLS, 13 | } from 'electron-devtools-installer'; 14 | import fixPath from 'fix-path'; 15 | 16 | // in development, reload with HMR from webpack-dev-server 17 | if (module.hot) { 18 | module.hot.accept(); 19 | } 20 | 21 | // function to find $PATH for bash cli in production, on macOS 22 | // https://github.com/sindresorhus/fix-path 23 | if (process.env.NODE_ENV === 'production') { 24 | fixPath(); 25 | } 26 | 27 | // create the electron application shell 28 | const createWindow = () => { 29 | // set window object 30 | const window = new BrowserWindow({ 31 | width: 1000, 32 | height: 750, 33 | // remove titleBar native to mac, keep stoplights 34 | titleBarStyle: 'hidden', 35 | webPreferences: { 36 | nodeIntegration: true, 37 | }, 38 | }); 39 | 40 | // set electron app size to full screen 41 | window.maximize(); 42 | 43 | // if in development, load content from dev server and install dev tools 44 | if (process.env.NODE_ENV === 'development') { 45 | // load from webpack dev server 46 | window.loadURL(`http://localhost:9080`); 47 | // install non-native dev tools 48 | window.webContents.on('did-frame-finish-load', () => { 49 | installExtension(REACT_DEVELOPER_TOOLS) 50 | .then((name: string) => { 51 | window.webContents.openDevTools(); 52 | console.log(`Added Extension: ${name}`); 53 | }) 54 | .catch((err: Error) => console.log(`An error occurred: ${err}`)); 55 | }); 56 | } else { 57 | // in production, load content from electron application files 58 | const startUrl = process.env.NOT_PACKAGE 59 | ? `file://${app.getAppPath()}/../renderer/index.html` 60 | : url.format({ 61 | pathname: path.join(__dirname, '/dist/renderer/index.html'), 62 | protocol: 'file:', 63 | slashes: true, 64 | }); 65 | window.loadURL(startUrl); 66 | } 67 | return window; 68 | }; 69 | 70 | // when the electron engine is ready, create window and menubar 71 | app.whenReady().then(createWindow).then(createMenu); 72 | 73 | // for every OS but mac, quit the application when windows are closed 74 | app.on('window-all-closed', () => { 75 | if (process.platform !== 'darwin') { 76 | app.quit(); 77 | } 78 | }); 79 | 80 | // open a new window if no windows are opened 81 | app.on('activate', () => { 82 | if (BrowserWindow.getAllWindows().length === 0) { 83 | createMenu(createWindow()); 84 | } 85 | }); 86 | -------------------------------------------------------------------------------- /src/main/menu.ts: -------------------------------------------------------------------------------- 1 | import { dialog, Menu, BrowserWindow, shell } from 'electron'; 2 | import fs from 'fs'; 3 | import dockerComposeValidation from '../common/dockerComposeValidation'; 4 | import resolveEnvVariables from '../common/resolveEnvVariables'; 5 | 6 | const createMenu = (window: BrowserWindow) => { 7 | const menuTemplate: Electron.MenuItemConstructorOptions[] = [ 8 | { 9 | label: 'File', 10 | submenu: [ 11 | { 12 | label: 'Open Docker-Compose File', 13 | accelerator: 'CommandOrControl+O', 14 | //on click for open menu item 15 | click() { 16 | dialog 17 | .showOpenDialog({ 18 | properties: ['openFile'], 19 | filters: [ 20 | { name: 'Docker Compose Files', extensions: ['yml', 'yaml'] }, 21 | { name: 'All Files', extensions: ['*'] }, 22 | ], 23 | }) 24 | .then((result: Electron.OpenDialogReturnValue) => { 25 | // if user exits out of file open prompt 26 | if (!result.filePaths[0]) return; 27 | return dockerComposeValidation(result.filePaths[0]); 28 | }) 29 | .then((validationResults: any) => { 30 | //if validation actually ran and user did not exit out of file open prompt 31 | if (validationResults) { 32 | //if there was an error with the file 33 | if (validationResults.error) { 34 | window.webContents.send( 35 | 'file-open-error-within-electron', 36 | validationResults.error, 37 | ); 38 | //process file and send to front end 39 | } else if (validationResults.filePath) { 40 | let yamlText = fs 41 | .readFileSync(validationResults.filePath) 42 | .toString(); 43 | if (validationResults.envResolutionRequired) { 44 | yamlText = resolveEnvVariables( 45 | yamlText, 46 | validationResults.filePath, 47 | ); 48 | } 49 | window.webContents.send( 50 | 'file-opened-within-electron', 51 | yamlText, 52 | ); 53 | } 54 | } 55 | }) 56 | .catch((err: Error) => console.log('error reading file: ', err)); 57 | }, 58 | }, 59 | { type: 'separator' }, 60 | { role: 'close' }, 61 | { role: 'quit' }, 62 | ], 63 | }, 64 | { 65 | label: 'Edit', 66 | submenu: [ 67 | { role: 'undo' }, 68 | { role: 'redo' }, 69 | { type: 'separator' }, 70 | { role: 'cut' }, 71 | { role: 'copy' }, 72 | { role: 'paste' }, 73 | { role: 'delete' }, 74 | ], 75 | }, 76 | { 77 | label: 'View', 78 | submenu: [ 79 | { role: 'reload' }, 80 | { role: 'forceReload' }, 81 | { role: 'toggleDevTools' }, 82 | { type: 'separator' }, 83 | { role: 'resetZoom' }, 84 | { role: 'zoomIn' }, 85 | { role: 'zoomOut' }, 86 | { type: 'separator' }, 87 | { role: 'togglefullscreen' }, 88 | ], 89 | }, 90 | { role: 'window', submenu: [{ role: 'minimize' }, { role: 'close' }] }, 91 | { 92 | role: 'help', 93 | submenu: [ 94 | { 95 | label: 'Nautilus Homepage', 96 | click() { 97 | shell.openExternal('http://nautilusdev.com'); 98 | }, 99 | }, 100 | { 101 | label: 'Visit Nautilus on GitHub', 102 | click() { 103 | shell.openExternal('https://github.com/oslabs-beta/nautilus'); 104 | }, 105 | }, 106 | { 107 | label: 'Nautilus v1.3.1', 108 | enabled: false, 109 | }, 110 | ], 111 | }, 112 | ]; 113 | const menu = Menu.buildFromTemplate(menuTemplate); 114 | Menu.setApplicationMenu(menu); 115 | }; 116 | 117 | export default createMenu; 118 | -------------------------------------------------------------------------------- /src/renderer/App.d.ts: -------------------------------------------------------------------------------- 1 | import { SimulationNodeDatum, SimulationLinkDatum } from 'd3'; 2 | 3 | /** 4 | * ********************** 5 | * REACT STATE TYPES 6 | * ********************** 7 | */ 8 | export type State = { 9 | bindMounts: Array; 10 | bindMountsClicked: Clicked; 11 | dependsOn: DependsOn; 12 | fileOpened: boolean; 13 | networks: ReadOnlyObj; 14 | options: Options; 15 | selectedContainer: string; 16 | selectedNetwork: string; 17 | services: Services; 18 | openErrors: string[]; 19 | version: string; 20 | view: ViewT; 21 | volumes: ReadOnlyObj; 22 | volumesClicked: Clicked; 23 | }; 24 | 25 | type ReadOnlyObj = { 26 | readonly [prop: string]: ReadOnlyObj | Array | string; 27 | }; 28 | 29 | type Clicked = { 30 | readonly [propName: string]: string; 31 | }; 32 | 33 | type DependsOn = { 34 | readonly name: string; 35 | readonly children?: Array; 36 | }; 37 | 38 | export type Services = { 39 | [service: string]: Service; 40 | }; 41 | 42 | export type Service = { 43 | build?: string; 44 | image?: string; 45 | command?: string; 46 | environment?: ReadOnlyObj | string[]; 47 | env_file?: string[]; 48 | ports?: Ports; 49 | volumes?: Volumes; 50 | depends_on?: string[]; 51 | networks?: string[] | {}; 52 | }; 53 | 54 | export type Ports = string[] | string | Port[]; 55 | 56 | export type Port = { 57 | mode: string; 58 | protocol: string; 59 | published: number; 60 | target: number; 61 | }; 62 | 63 | export type Volumes = VolumeType[]; 64 | 65 | /** Volumes may have different syntax, depending on the version 66 | * 67 | * https://docs.docker.com/compose/compose-file/#long-syntax-3 68 | */ 69 | type LongVolumeSyntax = Partial<{ 70 | type: 'volume' | 'bind' | 'tmpfs' | 'npipe'; 71 | source: string; 72 | target: string; 73 | read_only: boolean; 74 | bind: { 75 | propogation: string; 76 | }; 77 | volume: { 78 | nocopy: boolean; 79 | }; 80 | tmpft: { 81 | size: number; 82 | }; 83 | consistency: 'consistent' | 'cached' | 'delegated'; 84 | }>; 85 | 86 | type VolumeType = string | LongVolumeSyntax; 87 | 88 | type ViewT = 'networks' | 'depends_on'; 89 | 90 | export type Options = { 91 | ports: boolean; 92 | volumes: boolean; 93 | selectAll: boolean; 94 | }; 95 | 96 | /** 97 | * ********************** 98 | * APP METHOD FUNCTION TYPES 99 | * ********************** 100 | */ 101 | export type FileOpen = { 102 | (file: File): void; 103 | }; 104 | 105 | export type UpdateOption = { 106 | (id: 'ports' | 'volumes' | 'selectAll'): void; 107 | }; 108 | 109 | export type Handler = { 110 | ( 111 | e: 112 | | React.ChangeEvent 113 | | React.MouseEvent, 114 | ): void; 115 | }; 116 | 117 | export type UpdateView = { 118 | (view: 'networks' | 'depends_on'): void; 119 | }; 120 | 121 | export type SelectNetwork = { 122 | (network: string): void; 123 | }; 124 | 125 | export type SetSelectedContainer = { 126 | (containerName: string): void; 127 | }; 128 | 129 | /** 130 | * ********************** 131 | * D3 SIMULATION TYPES 132 | * ********************** 133 | */ 134 | type D3State = { 135 | simulation: Simulation; 136 | treeDepth: number; 137 | serviceGraph: SGraph; 138 | }; 139 | 140 | interface SNode extends SimulationNodeDatum { 141 | id: number; 142 | name: string; 143 | ports: string[]; 144 | volumes: string[]; 145 | networks?: string[]; 146 | row: number; 147 | column: number; 148 | rowLength: number; 149 | children: NodesObject; 150 | } 151 | 152 | interface Link extends SimulationLinkDatum { 153 | source: string; 154 | target: string; 155 | } 156 | 157 | type SGraph = { 158 | nodes: SNode[]; 159 | links: Link[]; 160 | }; 161 | 162 | export type NodesObject = { 163 | [service: string]: SNode; 164 | }; 165 | 166 | export type TreeMap = { 167 | [row: string]: string[]; 168 | }; 169 | 170 | export type Simulation = d3.Simulation; 171 | 172 | export type ValidationResults = { 173 | error?: Error; 174 | out: string; 175 | filePath: string; 176 | envResolutionRequired: boolean; 177 | }; 178 | -------------------------------------------------------------------------------- /src/renderer/App.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module App.tsx 5 | * @author Joshua Nordstrom 6 | * @date 3/7/20 7 | * @description start of the application 8 | * 9 | * ************************************ 10 | */ 11 | //IMPORT LIBRARIES 12 | import React, { Component } from 'react'; 13 | import yaml from 'js-yaml'; 14 | import { ipcRenderer } from 'electron'; 15 | 16 | //IMPORT HELPER FUNCTIONS 17 | import convertYamlToState from './helpers/yamlParser'; 18 | import setD3State from './helpers/setD3State'; 19 | import parseOpenError from './helpers/parseOpenError'; 20 | import runDockerComposeValidation from '../common/dockerComposeValidation'; 21 | import resolveEnvVariables from '../common/resolveEnvVariables'; 22 | 23 | // IMPORT REACT CONTAINERS OR COMPONENTS 24 | import LeftNav from './components/LeftNav'; 25 | import OptionBar from './components/OptionBar'; 26 | import D3Wrapper from './components/D3Wrapper'; 27 | 28 | //IMPORT TYPES 29 | import { 30 | State, 31 | FileOpen, 32 | UpdateOption, 33 | UpdateView, 34 | SelectNetwork, 35 | } from './App.d'; 36 | 37 | const initialState: State = { 38 | openErrors: [], 39 | selectedContainer: '', 40 | fileOpened: false, 41 | services: {}, 42 | dependsOn: { 43 | name: 'placeholder', 44 | }, 45 | networks: {}, 46 | selectedNetwork: '', 47 | volumes: {}, 48 | volumesClicked: {}, 49 | bindMounts: [], 50 | bindMountsClicked: {}, 51 | view: 'depends_on', 52 | options: { 53 | ports: false, 54 | volumes: false, 55 | selectAll: false, 56 | }, 57 | version: '', 58 | }; 59 | 60 | class App extends Component<{}, State> { 61 | constructor(props: {}) { 62 | super(props); 63 | this.state = initialState; 64 | } 65 | 66 | setSelectedContainer = (containerName: string) => { 67 | this.setState({ ...this.state, selectedContainer: containerName }); 68 | }; 69 | 70 | updateView: UpdateView = (view) => { 71 | this.setState((state) => { 72 | return { 73 | ...state, 74 | view, 75 | selectedNetwork: '', 76 | }; 77 | }); 78 | }; 79 | 80 | updateOption: UpdateOption = (option) => { 81 | const newState: State = { 82 | ...this.state, 83 | options: { ...this.state.options, [option]: !this.state.options[option] }, 84 | }; 85 | // check if toggling select all on or off 86 | if (option === 'selectAll') { 87 | if (newState.options.selectAll) { 88 | newState.options.ports = true; 89 | newState.options.volumes = true; 90 | } else { 91 | newState.options.ports = false; 92 | newState.options.volumes = false; 93 | } 94 | // check if select all should be on or off 95 | } else { 96 | if (newState.options.ports && newState.options.volumes) { 97 | newState.options.selectAll = true; 98 | } else { 99 | newState.options.selectAll = false; 100 | } 101 | } 102 | 103 | this.setState(newState); 104 | }; 105 | 106 | selectNetwork: SelectNetwork = (network) => { 107 | this.setState({ view: 'networks', selectedNetwork: network }); 108 | }; 109 | 110 | convertAndStoreYamlJSON = (yamlText: string) => { 111 | const yamlJSON = yaml.safeLoad(yamlText); 112 | const yamlState = convertYamlToState(yamlJSON); 113 | // set global variables for d3 simulation 114 | window.d3State = setD3State(yamlState.services); 115 | localStorage.setItem('state', JSON.stringify(yamlState)); 116 | this.setState(Object.assign(initialState, yamlState)); 117 | }; 118 | 119 | /** 120 | * @param file: a File classed object 121 | * @returns void 122 | * @description validates the docker-compose file 123 | * ** if no errors, passes file string along to convert and store yaml method 124 | * ** if errors, passes error string to handle file open errors method 125 | */ 126 | fileOpen: FileOpen = (file: File) => { 127 | const fileReader = new FileReader(); 128 | // check for valid file path 129 | if (file.path) { 130 | runDockerComposeValidation(file.path).then((validationResults: any) => { 131 | if (validationResults.error) { 132 | this.handleFileOpenError(validationResults.error); 133 | } else { 134 | // event listner to run after the file has been read as text 135 | fileReader.onload = () => { 136 | // if successful read, invoke method to convert and store to state 137 | if (fileReader.result) { 138 | let yamlText = fileReader.result.toString(); 139 | //if docker-compose uses env file, replace the variables with value from env file 140 | if (validationResults.envResolutionRequired) { 141 | yamlText = resolveEnvVariables(yamlText, file.path); 142 | } 143 | this.convertAndStoreYamlJSON(yamlText); 144 | } 145 | }; 146 | // read the file 147 | fileReader.readAsText(file); 148 | } 149 | }); 150 | } 151 | }; 152 | 153 | /** 154 | * @param errorText -> string 155 | * @returns void 156 | * @description sets state with array of strings of different errors 157 | */ 158 | handleFileOpenError = (errorText: Error) => { 159 | const openErrors = parseOpenError(errorText); 160 | this.setState({ 161 | ...initialState, 162 | openErrors, 163 | fileOpened: false, 164 | }); 165 | }; 166 | 167 | componentDidMount() { 168 | if (ipcRenderer) { 169 | ipcRenderer.on('file-open-error-within-electron', (event, arg) => { 170 | this.handleFileOpenError(arg); 171 | }); 172 | ipcRenderer.on('file-opened-within-electron', (event, arg) => { 173 | this.convertAndStoreYamlJSON(arg); 174 | }); 175 | } 176 | const stateJSON = localStorage.getItem('state'); 177 | if (stateJSON) { 178 | const stateJS = JSON.parse(stateJSON); 179 | // set d3 state 180 | window.d3State = setD3State(stateJS.services); 181 | this.setState(Object.assign(initialState, stateJS)); 182 | } 183 | } 184 | 185 | componentWillUnmount() { 186 | if (ipcRenderer) { 187 | ipcRenderer.removeAllListeners('file-opened-within-electron'); 188 | ipcRenderer.removeAllListeners('file-open-error-within-electron'); 189 | } 190 | } 191 | 192 | render() { 193 | return ( 194 |
195 | {/* dummy div to create draggable bar at the top of application to replace removed native bar */} 196 |
197 | 203 |
204 | 213 | 226 |
227 |
228 | ); 229 | } 230 | } 231 | 232 | export default App; 233 | -------------------------------------------------------------------------------- /src/renderer/components/BindMounts.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module BindMounts.tsx 5 | * @author 6 | * @date 3/11/20 7 | * @description Display for the BindMounts view 8 | * 9 | * ************************************ 10 | */ 11 | import React from 'react'; 12 | // IMPORT COMPONENTS 13 | import Volume from './Volume'; 14 | 15 | type Props = { 16 | bindMounts: Array; 17 | getColor: (str: string | undefined) => string; 18 | }; 19 | 20 | const BindMounts: React.FC = ({ bindMounts, getColor }) => { 21 | // interate through bindMounts array 22 | // creating an array of jsx Volume components for each bind mount 23 | const bindMountNames = bindMounts.map((volume, i) => { 24 | // assign unique color by invoking the getColor closure function 25 | return ; 26 | }); 27 | 28 | return
{bindMountNames}
; 29 | }; 30 | 31 | export default BindMounts; 32 | -------------------------------------------------------------------------------- /src/renderer/components/D3Wrapper.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module D3Wrapper.tsx 5 | * @author 6 | * @date 3/11/20 7 | * @description Container to hold all the d3 visualation components 8 | * 9 | * ************************************ 10 | */ 11 | import React from 'react'; 12 | // IMPORT COMPONENTS 13 | import FileSelector from './FileSelector'; 14 | import VolumesWrapper from './VolumesWrapper'; 15 | import ErrorDisplay from './ErrorDisplay'; 16 | import View from './View'; 17 | 18 | // IMPORT HELPER FUNCTIONS 19 | import colorSchemeIndex from '../helpers/colorSchemeIndex'; 20 | 21 | // IMPORT TYPES 22 | import { 23 | FileOpen, 24 | Services, 25 | SetSelectedContainer, 26 | Options, 27 | ReadOnlyObj, 28 | ViewT, 29 | } from '../App.d'; 30 | 31 | type Props = { 32 | fileOpen: FileOpen; 33 | setSelectedContainer: SetSelectedContainer; 34 | fileOpened: boolean; 35 | services: Services; 36 | options: Options; 37 | volumes: ReadOnlyObj; 38 | bindMounts: Array; 39 | view: ViewT; 40 | networks: ReadOnlyObj; 41 | selectedNetwork: string; 42 | openErrors: string[]; 43 | }; 44 | 45 | const D3Wrapper: React.FC = ({ 46 | fileOpened, 47 | fileOpen, 48 | services, 49 | setSelectedContainer, 50 | options, 51 | volumes, 52 | bindMounts, 53 | view, 54 | networks, 55 | selectedNetwork, 56 | openErrors, 57 | }) => { 58 | // invoke function that returns a function with the closure object for tracking colors 59 | const getColor = colorSchemeIndex(); 60 | 61 | return ( 62 |
63 | {/** 64 | * if a file hasn't been opened 65 | * ** if errors, display them 66 | * ** always display open button 67 | * else display visualizer 68 | * (yes, this is nested terinary operator) 69 | */} 70 | {!fileOpened ? ( 71 |
72 | {openErrors.length > 0 ? ( 73 | 74 | ) : ( 75 | <> 76 | )} 77 | 78 |
79 | ) : ( 80 | <> 81 |
82 | 91 |
92 | 97 | 98 | )} 99 |
100 | ); 101 | }; 102 | 103 | export default D3Wrapper; 104 | -------------------------------------------------------------------------------- /src/renderer/components/ErrorDisplay.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module ErrorDisplay.tsx 5 | * @author Mike D 6 | * @date 3/11/20 7 | * @description Container to hold all the d3 visualation components 8 | * 9 | * ************************************ 10 | */ 11 | import React from 'react'; 12 | 13 | type Props = { 14 | openErrors: string[]; 15 | }; 16 | 17 | const ErrorDisplay: React.FC = ({ openErrors }) => { 18 | // convert openErrors array into jsx 19 | const formattedError = openErrors.reduce( 20 | (acc: JSX.Element[], error: string, i: number) => { 21 | acc.push(
  • {error}
  • ); 22 | if (i !== openErrors.length - 1) { 23 | acc.push(
    ); 24 | } 25 | return acc; 26 | }, 27 | [], 28 | ); 29 | return ( 30 |
    31 |

    Docker-Compose File Issues

    32 | {formattedError} 33 |
    34 | Please fix your file and reopen it. 35 |
    36 | ); 37 | }; 38 | 39 | export default ErrorDisplay; 40 | -------------------------------------------------------------------------------- /src/renderer/components/FileSelector.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module FileSelector.tsx 5 | * @author Mike D 6 | * @date 3/11/20 7 | * @description Button to allow user to open docker-compose file 8 | * 9 | * ************************************ 10 | */ 11 | import React from 'react'; 12 | import { FaUpload } from 'react-icons/fa'; 13 | 14 | import { FileOpen } from '../App.d'; 15 | 16 | type Props = { 17 | fileOpen: FileOpen; 18 | }; 19 | 20 | const FileSelector: React.FC = ({ fileOpen }) => { 21 | return ( 22 |
    23 | 29 | ) => { 36 | // make sure there was something selected 37 | if (event.currentTarget) { 38 | // make sure user opened a file 39 | if (event.currentTarget.files) { 40 | // fire fileOpen function on first file opened 41 | fileOpen(event.currentTarget.files[0]); 42 | } 43 | } 44 | }} 45 | /> 46 |
    47 | ); 48 | }; 49 | export default FileSelector; 50 | -------------------------------------------------------------------------------- /src/renderer/components/LeftNav.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module LeftNav.tsx 5 | * @author 6 | * @date 3/11/20 7 | * @description container for the title, the service info and the file open 8 | * 9 | * ************************************ 10 | */ 11 | import React from 'react'; 12 | 13 | // IMPORT REACT COMPONENTS 14 | import ServiceInfo from './ServiceInfo'; 15 | import FileSelector from './FileSelector'; 16 | import Title from './Title'; 17 | import { FileOpen, Service } from '../App.d'; 18 | 19 | type Props = { 20 | service: Service; 21 | selectedContainer: string; 22 | fileOpen: FileOpen; 23 | fileOpened: boolean; 24 | }; 25 | 26 | const LeftNav: React.FC = ({ 27 | fileOpen, 28 | fileOpened, 29 | selectedContainer, 30 | service, 31 | }) => { 32 | return ( 33 |
    34 |
    35 | 36 | {fileOpened ? <FileSelector fileOpen={fileOpen} /> : null} 37 | </div> 38 | <ServiceInfo selectedContainer={selectedContainer} service={service} /> 39 | </div> 40 | ); 41 | }; 42 | 43 | export default LeftNav; 44 | -------------------------------------------------------------------------------- /src/renderer/components/Links.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module Links.tsx 5 | * @author 6 | * @date 3/23/20 7 | * @description Rendering of the nodes in d3 simulation 8 | * 9 | * ************************************ 10 | */ 11 | import React, { useEffect } from 'react'; 12 | import * as d3 from 'd3'; 13 | // IMPORT HELPER FUNCTIONS 14 | import { getStatic } from '../helpers/static'; 15 | import { SNode, Link, Services, ViewT } from '../App.d'; 16 | // IMPORT TYPES 17 | 18 | type Props = { 19 | services: Services; 20 | view: ViewT; 21 | }; 22 | 23 | const Links: React.FC<Props> = ({ services, view }) => { 24 | const { 25 | simulation, 26 | serviceGraph: { links }, 27 | } = window.d3State; 28 | useEffect(() => { 29 | simulation.force( 30 | 'link', 31 | d3 32 | .forceLink<SNode, Link>(links) 33 | .distance(1) 34 | .id((node: SNode) => node.name) 35 | .strength(0.01), 36 | ); 37 | 38 | //initialize graph 39 | const arrowsGroup = d3 40 | .select('.graph') 41 | .append('svg:defs') 42 | .attr('class', 'arrowsGroup'); 43 | 44 | const arrowHead = arrowsGroup 45 | .selectAll('marker') 46 | .data(['end']) // Different link/path types can be defined here 47 | .enter() 48 | .append('svg:marker') // This section adds in the arrows 49 | .attr('id', String) 50 | .attr('class', 'arrowHead') 51 | .attr('viewBox', '0 0 9.76 11.1') 52 | .attr('refX', 30) 53 | .attr('refY', 6) 54 | .attr('markerWidth', 100) 55 | .attr('markerHeight', 6) 56 | .attr('orient', 'auto'); 57 | 58 | arrowHead 59 | .append('rect') 60 | .attr('class', 'line-cover') 61 | .attr('fill', 'white') 62 | .attr('width', 30) 63 | .attr('height', 4) 64 | .attr('y', 4) 65 | .attr('x', 1); 66 | 67 | arrowHead.append('svg:image').attr('xlink:href', getStatic('arrow.svg')); 68 | 69 | const linkGroup = d3.select('.links'); 70 | 71 | const linkLines = linkGroup 72 | .selectAll('line') 73 | .data(links) 74 | .enter() 75 | .append('line') 76 | .attr('stroke-width', 3) 77 | .attr('class', 'link') 78 | .attr('marker-end', 'url(#end)'); 79 | 80 | linkGroup.lower(); 81 | 82 | return () => { 83 | linkLines.remove(); 84 | arrowsGroup.remove(); 85 | }; 86 | }, [services]); 87 | 88 | /** 89 | ********************* 90 | * DEPENDS ON OPTION TOGGLE 91 | ********************* 92 | */ 93 | useEffect(() => { 94 | if (view === 'depends_on') { 95 | d3.select('.arrowsGroup').classed('hide', false); 96 | d3.select('.links').classed('hide', false); 97 | } else { 98 | d3.select('.arrowsGroup').classed('hide', true); 99 | d3.select('.links').classed('hide', true); 100 | } 101 | }, [view]); 102 | 103 | return <g className="links"></g>; 104 | }; 105 | 106 | export default Links; 107 | -------------------------------------------------------------------------------- /src/renderer/components/NetworksDropdown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ReadOnlyObj, SelectNetwork, Handler } from '../App.d'; 3 | 4 | type Props = { 5 | networks: ReadOnlyObj; 6 | selectNetwork: SelectNetwork; 7 | selectedNetwork: string; 8 | }; 9 | 10 | const NetworksDropDown: React.FC<Props> = ({ 11 | networks, 12 | selectNetwork, 13 | selectedNetwork, 14 | }) => { 15 | const handleNetworkUpdate: Handler = e => { 16 | const network = (e as React.ChangeEvent<HTMLSelectElement>).currentTarget 17 | .value; 18 | selectNetwork(network); 19 | }; 20 | 21 | const groupNetworks = (): JSX.Element | void => { 22 | if (Object.keys(networks).length === 1) return; 23 | const title: string = 24 | Object.keys(networks).length > 1 ? 'group networks' : 'default'; 25 | return ( 26 | <option 27 | className="networkOption" 28 | key={title} 29 | id="groupNetworks" 30 | value="groupNetworks" 31 | > 32 | {title} 33 | </option> 34 | ); 35 | }; 36 | const networksOptions = Object.keys(networks).map((network, i) => { 37 | return ( 38 | <option 39 | className="networkOption" 40 | key={`networks option: ${network}`} 41 | id={network} 42 | value={network} 43 | > 44 | {network} 45 | </option> 46 | ); 47 | }); 48 | 49 | let selectClass = selectedNetwork ? 'option selected' : 'option'; 50 | return ( 51 | <> 52 | <select 53 | id="networks" 54 | className={selectClass} 55 | name="networks" 56 | onChange={handleNetworkUpdate} 57 | value={selectedNetwork} 58 | > 59 | <option 60 | key="networks option header" 61 | id="networkHeader" 62 | value="" 63 | disabled 64 | > 65 | networks 66 | </option> 67 | {networksOptions} 68 | {groupNetworks()} 69 | </select> 70 | </> 71 | ); 72 | }; 73 | 74 | export default NetworksDropDown; 75 | -------------------------------------------------------------------------------- /src/renderer/components/NodePorts.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module Ports.tsx 5 | * @author 6 | * @date 3/23/20 7 | * @description Appending ports to nodes in d3 8 | * @note Doesn't add a react elemnt to dom 9 | * 10 | * ************************************ 11 | */ 12 | import React, { useEffect } from 'react'; 13 | import * as d3 from 'd3'; 14 | // IMPORT TYPES 15 | import { SNode } from '../App.d'; 16 | 17 | type Props = { 18 | portsOn: boolean; 19 | }; 20 | 21 | const NodePorts: React.FC<Props> = ({ portsOn }) => { 22 | useEffect(() => { 23 | // PORTS SVG VARIABLE 24 | // border radius 25 | const rx = 3; 26 | // size of rectangle 27 | const width = 43; 28 | const height = 10; 29 | // ports location 30 | const x = 7; 31 | const y = 24; 32 | // text location 33 | const dx = x + 21; // center of text element because of text-anchor 34 | const dy = y + 8; 35 | // PORTS VARIABLES 36 | let nodesWithPorts: d3.Selection<SVGGElement, SNode, any, any>; 37 | const ports: d3.Selection<SVGRectElement, SNode, any, any>[] = []; 38 | const portText: d3.Selection<SVGTextElement, SNode, any, any>[] = []; 39 | if (portsOn) { 40 | // select all nodes with ports 41 | nodesWithPorts = d3 42 | .select('.nodes') 43 | .selectAll<SVGGElement, SNode>('.node') 44 | .filter((d: SNode) => d.ports.length > 0); 45 | 46 | // iterate through all nodes with ports 47 | nodesWithPorts.each(function (d: SNode) { 48 | const node = this; 49 | // iterate through all ports of node 50 | d.ports.forEach((pString, i) => { 51 | // set font size based on length of ports text 52 | const textSize = pString.length <= 9 ? '8px' : '7px'; 53 | // add svg port 54 | const port = d3 55 | .select<SVGElement, SNode>(node) 56 | .append('rect') 57 | .attr('class', 'port') 58 | .attr('rx', rx) 59 | .attr('x', x + i * 1.1) 60 | .attr('y', y + i * (height + 1)) 61 | .attr('width', width) 62 | .attr('height', height); 63 | // store d3 object in ports array 64 | ports.push(port); 65 | // add svg port text 66 | const pText = d3 67 | .select<SVGElement, SNode>(node) 68 | .append('text') 69 | .attr('class', 'ports-text') 70 | .attr('color', 'white') 71 | .attr('dx', dx + i * 1.1) 72 | .attr('dy', dy + i * (height + 1)) 73 | .attr('font-size', textSize) 74 | // center the text in the rectangle 75 | .append('tspan') 76 | .text(pString) 77 | .attr('text-anchor', 'middle'); 78 | 79 | // store d3 object in ports text array 80 | portText.push(pText); 81 | }); 82 | }); 83 | } 84 | 85 | return () => { 86 | // before unmounting, if ports option was on, remove the ports 87 | if (portsOn) { 88 | ports.forEach((node) => node.remove()); 89 | portText.forEach((node) => node.remove()); 90 | } 91 | }; 92 | // only fire when options.ports changes 93 | }, [portsOn]); 94 | 95 | return <></>; 96 | }; 97 | 98 | export default NodePorts; 99 | -------------------------------------------------------------------------------- /src/renderer/components/NodeVolumes.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module Ports.tsx 5 | * @author 6 | * @date 3/23/20 7 | * @description Appending volumes to nodes in d3 8 | * @note Doesn't add a react elemnt to dom 9 | * 10 | * ************************************ 11 | */ 12 | import React, { useEffect } from 'react'; 13 | import * as d3 from 'd3'; 14 | // IMPORT HELPERS 15 | //import { colorSchemeIndex } from '../helpers/colorSchemeHash'; 16 | 17 | //IMPORT SVG 18 | import containerPath from '../../../static/containerPath'; 19 | 20 | // IMPORT TYPES 21 | import { SNode } from '../App.d'; 22 | 23 | type Props = { 24 | volumesOn: boolean; 25 | getColor: any; 26 | }; 27 | 28 | const NodeVolumes: React.FC<Props> = ({ volumesOn, getColor }) => { 29 | useEffect(() => { 30 | // VOLUMES LOCATION 31 | const x = 0; 32 | const y = 0; 33 | const width = 132; 34 | const height = 75; 35 | // VOLUMES VARIABLES 36 | let nodesWithVolumes: d3.Selection<SVGGElement, SNode, any, any>; 37 | const volumes: d3.Selection<SVGSVGElement, SNode, any, any>[] = []; 38 | const volumeText: d3.Selection<SVGTextElement, SNode, any, any>[] = []; 39 | if (volumesOn) { 40 | // select all nodes with volumes 41 | nodesWithVolumes = d3 42 | .select('.nodes') 43 | .selectAll<SVGGElement, SNode>('.node') 44 | .filter((d: SNode) => d.volumes.length > 0); 45 | 46 | // iterate through all nodes with volumes 47 | nodesWithVolumes.each(function (d: SNode) { 48 | const node = this; 49 | // iterate through all volumes of node 50 | d.volumes.reverse().forEach((vString, i) => { 51 | let onClick = false; 52 | let onceClicked = false; 53 | // add svg volume 54 | const volume = d3 55 | .select<SVGElement, SNode>(node) 56 | .insert('svg', 'image') 57 | .attr('viewBox', '0 0 215 124') 58 | .html(containerPath) 59 | .attr('class', 'volumeSVG') 60 | .attr('fill', () => { 61 | let slicedVString = vString.slice(0, vString.indexOf(':')); 62 | return vString.includes(':') 63 | ? getColor(slicedVString) 64 | : getColor(vString); 65 | }) 66 | .attr('width', width + (d.volumes.length - i) * 20) 67 | .attr('height', height + (d.volumes.length - i) * 40) 68 | .attr('x', x - (d.volumes.length - i) * 10) 69 | .attr('y', y - (d.volumes.length - i) * 20) 70 | .on('mouseover', () => { 71 | return vText.style('visibility', 'visible'); 72 | }) 73 | .on('mouseout', () => { 74 | !onClick 75 | ? vText.style('visibility', 'hidden') 76 | : vText.style('visibility', 'visible'); 77 | }) 78 | .on('click', () => { 79 | onceClicked = !onceClicked; 80 | onClick = onceClicked; 81 | }); 82 | // store d3 object in volumes array so that they can be removed 83 | volumes.push(volume); 84 | // add svg volume text 85 | const vText = d3 86 | .select<SVGElement, SNode>(node) 87 | .append('text') 88 | .text(vString) 89 | .attr('class', 'volume-text') 90 | .attr('fill', 'black') 91 | .attr('text-anchor', 'end') 92 | .attr('dx', x - 5) 93 | .attr('dy', y + (i + 1) * 11) 94 | .style('visibility', 'hidden'); 95 | // store d3 object in volumes text array 96 | volumeText.push(vText); 97 | }); 98 | }); 99 | } 100 | 101 | return () => { 102 | // before unmounting, if volumes option was on, remove the volumes 103 | if (volumesOn) { 104 | volumes.forEach((node) => node.remove()); 105 | volumeText.forEach((node) => node.remove()); 106 | } 107 | }; 108 | // only fire when options.volumes changes 109 | }, [volumesOn]); 110 | return <></>; 111 | }; 112 | 113 | export default NodeVolumes; 114 | -------------------------------------------------------------------------------- /src/renderer/components/Nodes.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module Nodes.tsx 5 | * @author 6 | * @date 3/23/20 7 | * @description Rendering of the nodes in d3 simulation 8 | * 9 | * ************************************ 10 | */ 11 | import React, { useEffect } from 'react'; 12 | import * as d3 from 'd3'; 13 | 14 | // IMPORT HELPER FUNCTIONS 15 | import { 16 | getHorizontalPosition, 17 | getVerticalPosition, 18 | } from '../helpers/getSimulationDimensions'; 19 | import { getStatic } from '../helpers/static'; 20 | 21 | // IMPORT TYPES 22 | import { SNode, SetSelectedContainer, Services, Options } from '../App.d'; 23 | 24 | // IMPORT COMPONENTS 25 | import NodePorts from './NodePorts'; 26 | import NodeVolumes from './NodeVolumes'; 27 | 28 | type Props = { 29 | services: Services; 30 | setSelectedContainer: SetSelectedContainer; 31 | options: Options; 32 | getColor: any; 33 | }; 34 | 35 | function wrap(text: d3.Selection<SVGTextElement, SNode, d3.BaseType, unknown>) { 36 | text.each(function () { 37 | const text = d3.select(this); 38 | const className = text.attr('class'); 39 | const words = text.text(); 40 | let line = 0; 41 | const lineLength = 8; 42 | const maxLine = 3; 43 | const totalLinesNeeded = Math.ceil(words.length / lineLength); 44 | if (totalLinesNeeded === 2) { 45 | text.attr('y', 67); 46 | } 47 | const x = text.attr('x'); 48 | const y = text.attr('y'); 49 | if (words.length > 8) { 50 | text.text(''); 51 | while (line < maxLine) { 52 | const currentIndex = line * lineLength; 53 | const lineText = text 54 | .append('tspan') 55 | .attr('x', x) 56 | .attr('y', y) 57 | .attr('dx', 0) 58 | .attr('dy', currentIndex * 1.7 - 10) 59 | .attr('class', className); 60 | if (line < 2 || words.length <= 24) { 61 | lineText.text(words.slice(currentIndex, currentIndex + 8)); 62 | } else { 63 | lineText.text(words.slice(currentIndex, currentIndex + 5) + '...'); 64 | } 65 | line++; 66 | } 67 | } 68 | }); 69 | } 70 | 71 | const Nodes: React.FC<Props> = ({ 72 | setSelectedContainer, 73 | services, 74 | options, 75 | getColor, 76 | }) => { 77 | const { simulation, serviceGraph, treeDepth } = window.d3State; 78 | /** 79 | ********************* 80 | * RENDER NODES 81 | ********************* 82 | */ 83 | useEffect(() => { 84 | const container = d3.select('.view-wrapper'); 85 | const width = parseInt(container.style('width'), 10); 86 | const height = parseInt(container.style('height'), 10); 87 | 88 | //sets 'clicked' nodes back to unfixed position 89 | const dblClick = (d: SNode) => { 90 | simulation.alphaTarget(0); 91 | d.fx = null; 92 | d.fy = null; 93 | }; 94 | 95 | // set up drag feature for nodes 96 | let drag = d3 97 | .drag<SVGGElement, SNode>() 98 | .on('start', function dragstarted(d: SNode) { 99 | // if simulation has stopped, restart it 100 | if (!d3.event.active) simulation.alphaTarget(0.3).restart(); 101 | // set the x and y positions to fixed 102 | d.fx = d3.event.x; 103 | d.fy = d3.event.y; 104 | }) 105 | .on('drag', function dragged(d: SNode) { 106 | // raise the current selected node to the highest layer 107 | d3.select(this).raise(); 108 | // change the fx and fy to dragged position 109 | d.fx = d3.event.x; 110 | d.fy = d3.event.y; 111 | }) 112 | .on('end', function dragended(d: SNode) { 113 | // stop simulation when node is done being dragged 114 | if (!d3.event.active) simulation.alphaTarget(0); 115 | // fix the node to the place where the dragging stopped 116 | d.fx = d.x; 117 | d.fy = d.y; 118 | }); 119 | 120 | // create node container svgs 121 | const nodeContainers = d3 122 | .select('.nodes') 123 | .selectAll('g') 124 | .data<SNode>(serviceGraph.nodes) 125 | .enter() 126 | .append('g') 127 | .attr('class', 'node') 128 | .on('click', (node: SNode) => { 129 | setSelectedContainer(node.name); 130 | }) 131 | .on('dblclick', dblClick) 132 | .call(drag) 133 | // initialize nodes in depends on view 134 | .attr('x', (d: SNode) => { 135 | //assign the initial x location to the relative displacement from the left 136 | return (d.x = getHorizontalPosition(d, width)); 137 | }) 138 | .attr('y', (d: SNode) => { 139 | return (d.y = getVerticalPosition(d, treeDepth, height)); 140 | }); 141 | 142 | //add container image to each node 143 | nodeContainers 144 | .append('svg:image') 145 | .attr('xlink:href', (d: SNode) => { 146 | return getStatic('container.svg'); 147 | }) 148 | .attr('height', 75) 149 | .attr('width', 132) 150 | .attr('class', 'containerImage'); 151 | 152 | // add node service name 153 | nodeContainers 154 | .append('text') 155 | .text((d: SNode) => d.name) 156 | .attr('class', 'nodeLabel') 157 | .attr('x', 80) 158 | .attr('y', 60) 159 | .attr('text-anchor', 'middle') 160 | .call(wrap); 161 | //add stroke 162 | nodeContainers 163 | .insert('text', '.nodeLabel') 164 | .text((d: SNode) => d.name) 165 | .attr('class', 'nodeLabelStroke') 166 | .attr('x', 80) 167 | .attr('y', 60) 168 | .attr('text-anchor', 'middle') 169 | .call(wrap); 170 | 171 | return () => { 172 | // remove containers when services change 173 | nodeContainers.remove(); 174 | }; 175 | }, [services]); 176 | 177 | return ( 178 | <g className="nodes"> 179 | <NodePorts portsOn={options.ports} /> 180 | <NodeVolumes volumesOn={options.volumes} getColor={getColor} /> 181 | </g> 182 | ); 183 | }; 184 | 185 | export default Nodes; 186 | -------------------------------------------------------------------------------- /src/renderer/components/OptionBar.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module OptionBar.tsx 5 | 6 | * @author Tyler Hurtt 7 | * @date 3/11/20 8 | * @description Used to display toggle options 9 | * 10 | * ************************************ 11 | */ 12 | import React from 'react'; 13 | 14 | import NetworksDropdown from './NetworksDropdown'; 15 | 16 | import { 17 | ViewT, 18 | Options, 19 | UpdateOption, 20 | UpdateView, 21 | SelectNetwork, 22 | ReadOnlyObj, 23 | Handler, 24 | } from '../App.d'; 25 | 26 | type Props = { 27 | view: ViewT; 28 | options: Options; 29 | networks: ReadOnlyObj; 30 | updateView: UpdateView; 31 | updateOption: UpdateOption; 32 | selectNetwork: SelectNetwork; 33 | selectedNetwork: string; 34 | }; 35 | 36 | const OptionBar: React.FC<Props> = ({ 37 | view, 38 | options, 39 | networks, 40 | updateView, 41 | updateOption, 42 | selectNetwork, 43 | selectedNetwork, 44 | }) => { 45 | const dependsOnClass = view === 'depends_on' ? 'option selected' : 'option'; 46 | 47 | // calls update function with the specified view from the click event 48 | const handleViewUpdate: Handler = (e) => { 49 | const view = e.currentTarget.id as 'networks' | 'depends_on'; 50 | updateView(view); 51 | }; 52 | 53 | // calls the update option function with the specificed option from the click event 54 | const handleOptionUpdate: Handler = (e) => { 55 | const option = e.currentTarget.id as 'ports' | 'volumes' | 'selectAll'; 56 | updateOption(option); 57 | }; 58 | 59 | // creates an array of jsx elements for each option 60 | const optionsDisplay = Object.keys(options).map((opt, i) => { 61 | let title = ''; 62 | // format select all title 63 | if (opt === 'selectAll') title = 'select all'; 64 | // otherwise set title to option name 65 | else title = opt; 66 | 67 | return ( 68 | <span 69 | key={`opt${i}`} 70 | // if the current option is selected, give it the 'selected' class 71 | className={ 72 | options[opt as 'selectAll' | 'ports' | 'volumes'] 73 | ? 'option selected' 74 | : 'option' 75 | } 76 | id={opt} 77 | onClick={handleOptionUpdate} 78 | > 79 | {title} 80 | </span> 81 | ); 82 | }); 83 | 84 | return ( 85 | <div className="option-bar"> 86 | <div className="views flex"> 87 | <NetworksDropdown 88 | networks={networks} 89 | selectNetwork={selectNetwork} 90 | selectedNetwork={selectedNetwork} 91 | /> 92 | <span 93 | className={dependsOnClass} 94 | id="depends_on" 95 | onClick={handleViewUpdate} 96 | > 97 | depends on 98 | </span> 99 | </div> 100 | <div className="titles flex"> 101 | <h2>Views</h2> 102 | <div className="vl"></div> 103 | <h2>Options</h2> 104 | </div> 105 | <div className="options flex">{optionsDisplay}</div> 106 | </div> 107 | ); 108 | }; 109 | 110 | export default OptionBar; 111 | -------------------------------------------------------------------------------- /src/renderer/components/ServiceInfo.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module ServiceInfo.tsx 5 | * @author Danny Scheiner & Josh Nordstrom 6 | * @date 3/11/20 7 | * @description Dropdown display to show categories of service info 8 | * 9 | * ************************************ 10 | */ 11 | import React from 'react'; 12 | 13 | type ReactProps = { 14 | service?: any; 15 | selectedContainer: string; 16 | }; 17 | 18 | type DockerComposeCommands = { 19 | [prop: string]: string; 20 | }; 21 | 22 | type ServiceOverview = { 23 | [prop: string]: any; 24 | }; 25 | 26 | type TwoDimension = { 27 | [prop: string]: any; 28 | }; 29 | 30 | const ServiceInfo: React.FC<ReactProps> = ({ service, selectedContainer }) => { 31 | // Create an object to house text intros for each docker-compose property 32 | const dockerComposeCommands: DockerComposeCommands = { 33 | build: 'Build: ', 34 | image: 'Image: ', 35 | command: 'Command: ', 36 | environment: 'Environment: ', 37 | env_file: 'Env_file: ', 38 | ports: 'Ports: ', 39 | }; 40 | 41 | // Objects to hold filtered 1D service Commands 42 | const serviceOverview: ServiceOverview = {}; 43 | 44 | // Arrays/Objects to hold filtered 2D service Commands 45 | const environmentVariables: TwoDimension = {}; 46 | const env_file: string[] = []; 47 | const portsArray: string[] = []; 48 | 49 | // loop through each command in the selected container, 50 | // if the command exists in the DC properties, correspond it to an empty string in the serviceOverview object 51 | // also, do the following for each of the specific commands with 2D values, as well as the "build" command because it has options: 52 | // ENVIRONMENT: we want to take an environment variable such as: "JACOCO=${REPORT_COVERAGE}" and set the 2d line to look like - JACOCO: $(REPORT_COVERAGE) 53 | // Thus, if its value is an array, loop through it, split the value at the the '=', and set the key in the environmentVariables cache to the first half, and the value to the second half 54 | // Otherwise, if it's an object, it's already split for you, so set the serviceOverview key to the environment variable's key, and the serviceOverview value to the environment's value 55 | // ENV_FILE: an env file can have a 1D string, so if it does, just set the key in serviceOverview equal to its value as passed down from state 56 | // If it's an array, loop through the env_file values, and push them into the env_file "cache" on line 46 57 | // PORTS: if ports incorrectly is given a string, just set the key in serviceOverview equal to its value as passed down from state 58 | // Finally, for all commands with 1D values and no options (image and command), just set the key in serviceOverview equal to its value as passed down from state 59 | if (service) { 60 | Object.keys(service).forEach((command) => { 61 | if (dockerComposeCommands[command]) { 62 | serviceOverview[command] = ''; 63 | // ********************* 64 | // * Environment 65 | // ********************* 66 | if (command === 'environment') { 67 | if (Array.isArray(service[command])) { 68 | service[command].forEach((value: string) => { 69 | const valueArray = value.split('='); 70 | environmentVariables[valueArray[0]] = valueArray[1]; 71 | }); 72 | } else { 73 | const environment = service[command]; 74 | Object.keys(environment).forEach((key) => { 75 | environmentVariables[key] = environment[key]; 76 | }); 77 | } 78 | // ********************* 79 | // Env_File 80 | // ********************* 81 | } else if (command === 'env_file') { 82 | if (typeof service[command] === 'string') { 83 | serviceOverview[command] = service[command]; 84 | } else { 85 | for (let i = 0; i < service[command].length; i += 1) { 86 | env_file.push(service[command][i]); 87 | } 88 | } 89 | // ********************* 90 | // Ports 91 | // ********************* 92 | } else if (command === 'ports') { 93 | for (let i = 0; i < service[command].length; i += 1) { 94 | if (typeof service[command][i] === 'object') { 95 | portsArray.push( 96 | `${service[command][i].published}:${ 97 | service[command][i].target 98 | } - ${service[command][i].protocol.toUpperCase()} : ${ 99 | service[command][i].mode 100 | }`, 101 | ); 102 | } else if (!service[command][i].includes(':')) { 103 | portsArray.push(`Auto-assigned:${service[command][i]}`); 104 | } else portsArray.push(service[command][i]); 105 | } 106 | // ********************* 107 | // * Build 108 | // ********************* 109 | } else if ( 110 | command === 'build' && 111 | typeof service[command] !== 'string' 112 | ) { 113 | // ********************* 114 | // * Command 115 | // ********************* 116 | } else if (command === 'command' && Array.isArray(service[command])) { 117 | // ********************* 118 | // * General 1D Objects 119 | // ********************* 120 | } else { 121 | serviceOverview[command] = service[command]; 122 | } 123 | } 124 | }); 125 | } 126 | 127 | const commandToJSX = (command: any) => { 128 | const optionJSX = Object.keys(command).map((option) => { 129 | if (typeof command[option] === 'string') { 130 | return ( 131 | <div> 132 | <span className="option-key">{option}:</span> 133 | {command[option]} 134 | </div> 135 | ); 136 | } else if (Array.isArray(command[option])) { 137 | const optionJSX = (command[option] as []).map((element: string, i) => { 138 | const valueArray = element.split('='); 139 | return ( 140 | <li key={i}> 141 | <span>{valueArray[0]}:</span> {valueArray[1]} 142 | </li> 143 | ); 144 | }); 145 | return ( 146 | <div> 147 | <span className="option-key">{option}:</span> 148 | <ul>{optionJSX}</ul> 149 | </div> 150 | ); 151 | } else { 152 | const optionJSX = Object.keys(command[option]).map((key: string, i) => { 153 | return ( 154 | <li key={i}> 155 | <span>{key}:</span> {command[option][key]} 156 | </li> 157 | ); 158 | }); 159 | return ( 160 | <div> 161 | <span className="option-key">{option}:</span> 162 | <ul>{optionJSX}</ul> 163 | </div> 164 | ); 165 | } 166 | }); 167 | return <div className="options">{optionJSX}</div>; 168 | }; 169 | 170 | const infoToJsx = ( 171 | serviceOverview: ServiceOverview, 172 | dockerComposeCommands: DockerComposeCommands, 173 | environmentVariables: TwoDimension, 174 | env_file: string[], 175 | portsArray: string[], 176 | service: any, 177 | ) => { 178 | return Object.keys(serviceOverview).length === 0 179 | ? 'Please select a container with details to display.' 180 | : Object.keys(serviceOverview).map((command, i) => { 181 | let commandJSX = ( 182 | <span className="command">{dockerComposeCommands[command]}</span> 183 | ); 184 | let valueJSX: JSX.Element = <div></div>; 185 | // ********************* 186 | // * Environment 187 | // ********************* 188 | if (command === 'environment' && !serviceOverview[command].length) { 189 | const environment: JSX.Element[] = []; 190 | Object.keys(environmentVariables).forEach((key) => { 191 | environment.push( 192 | <li className="second-level" key={key}> 193 | <span>{key}:</span> {environmentVariables[key]} 194 | </li>, 195 | ); 196 | }); 197 | valueJSX = ( 198 | <span className="command-values"> 199 | <ul>{environment}</ul> 200 | </span> 201 | ); 202 | // ********************* 203 | // Env_File 204 | // ********************* 205 | } else if (command === 'env_file' && env_file.length) { 206 | let envFileArray: JSX.Element[] = []; 207 | env_file.forEach((el) => { 208 | envFileArray.push( 209 | <li className="second-level" key={el}> 210 | {el} 211 | </li>, 212 | ); 213 | }); 214 | valueJSX = ( 215 | <span className="command-values"> 216 | <ul>{envFileArray}</ul> 217 | </span> 218 | ); 219 | // ********************* 220 | // Build 221 | // ********************* 222 | } else if (command === 'build' && !serviceOverview[command].length) { 223 | valueJSX = commandToJSX(service[command]); 224 | // ********************* 225 | // Command 226 | // ********************* 227 | } else if ( 228 | command === 'command' && 229 | !serviceOverview[command].length 230 | ) { 231 | valueJSX = ( 232 | <span className="command-values"> 233 | {service[command].join(', ')} 234 | </span> 235 | ); 236 | // ********************* 237 | // Ports 238 | // ********************* 239 | } else if (command === 'ports' && !serviceOverview[command].length) { 240 | if (portsArray.length) { 241 | let portsArraySquared: JSX.Element[] = []; 242 | portsArray.forEach((el) => { 243 | portsArraySquared.push( 244 | <li className="second-level" key={el}> 245 | {el} 246 | </li>, 247 | ); 248 | }); 249 | valueJSX = ( 250 | <span className="command-values"> 251 | <ul>{portsArraySquared}</ul> 252 | </span> 253 | ); 254 | } 255 | } else { 256 | valueJSX = ( 257 | <span className="command-values">{serviceOverview[command]}</span> 258 | ); 259 | } 260 | 261 | return ( 262 | <div key={`command${i}`}> 263 | {commandJSX} 264 | {valueJSX} 265 | </div> 266 | ); 267 | }); 268 | }; 269 | 270 | return ( 271 | <div className="info-dropdown"> 272 | <h3> 273 | {selectedContainer !== '' 274 | ? selectedContainer[0].toUpperCase() + selectedContainer.slice(1) 275 | : selectedContainer} 276 | </h3> 277 | <div className="content-wrapper"> 278 | <div className="overflow-container"> 279 | <div className="overview"> 280 | {infoToJsx( 281 | serviceOverview, 282 | dockerComposeCommands, 283 | environmentVariables, 284 | env_file, 285 | portsArray, 286 | service, 287 | )} 288 | </div> 289 | </div> 290 | </div> 291 | </div> 292 | ); 293 | }; 294 | 295 | export default ServiceInfo; 296 | -------------------------------------------------------------------------------- /src/renderer/components/Title.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | //import helpers 4 | import { getStatic } from '../helpers/static'; 5 | 6 | const Title: React.FC<{}> = (props) => ( 7 | <div className="title"> 8 | <img src={getStatic('nautilus_logo.svg')} /> 9 | <h1>Nautilus</h1> 10 | </div> 11 | ); 12 | 13 | export default Title; 14 | -------------------------------------------------------------------------------- /src/renderer/components/View.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module DependsOnView.tsx 5 | * @author 6 | * @date 3/11/20 7 | * @description Display area for services containers in Depends_On view : force-graph 8 | * 9 | * ************************************ 10 | */ 11 | import React, { useEffect } from 'react'; 12 | import * as d3 from 'd3'; 13 | 14 | // IMPORT COMPONENTS 15 | import Nodes from './Nodes'; 16 | import Links from './Links'; 17 | 18 | //IMPORT HELPER FNS 19 | import { 20 | getHorizontalPosition, 21 | getVerticalPosition, 22 | } from '../helpers/getSimulationDimensions'; 23 | 24 | // IMPORT STYLES 25 | import { 26 | Services, 27 | SNode, 28 | SetSelectedContainer, 29 | Options, 30 | ReadOnlyObj, 31 | ViewT, 32 | } from '../App.d'; 33 | 34 | type Props = { 35 | services: Services; 36 | setSelectedContainer: SetSelectedContainer; 37 | options: Options; 38 | networks: ReadOnlyObj; 39 | view: ViewT; 40 | selectedNetwork: string; 41 | getColor: any; 42 | }; 43 | 44 | const View: React.FC<Props> = ({ 45 | services, 46 | setSelectedContainer, 47 | options, 48 | view, 49 | networks, 50 | selectedNetwork, 51 | getColor, 52 | }) => { 53 | const { treeDepth, simulation } = window.d3State; 54 | 55 | /** 56 | ********************* 57 | * Depends On View 58 | ********************* 59 | */ 60 | useEffect(() => { 61 | // calculate dimensions of the view wrapper DOM element 62 | const container = d3.select('.view-wrapper'); 63 | const width = parseInt(container.style('width')); 64 | const height = parseInt(container.style('height')); 65 | const topMargin = 20; 66 | const sideMargin = 20; 67 | // Used to determine the size of each container for border enforcement 68 | const radius = 120; 69 | 70 | // selecting the d3 svg nodes and links to manipulate via d3 simulation 71 | const d3Nodes = d3.select('.nodes').selectAll('.node'); 72 | const linkLines = d3.select('.links').selectAll('line'); 73 | 74 | // TICK FUNCTION 75 | // Function to be called every tick of the d3 simulation 76 | // used in both networks and depends view 77 | function ticked() { 78 | // calculate height and width for border reinforcement 79 | const w = parseInt(container.style('width')); 80 | const h = parseInt(container.style('height')); 81 | 82 | // reinforce borders and move nodes in accord with the simulation 83 | d3Nodes 84 | .attr('cx', (d: any) => { 85 | return (d.x = Math.max( 86 | sideMargin, 87 | Math.min(w - sideMargin - radius, d.x as number), 88 | )); 89 | }) 90 | .attr('cy', (d: any) => { 91 | return (d.y = Math.max( 92 | 15 + topMargin, 93 | Math.min(h - topMargin - radius, d.y as number), 94 | )); 95 | }) 96 | .attr('transform', (d: any) => { 97 | return 'translate(' + d.x + ',' + d.y + ')'; 98 | }); 99 | 100 | // link position 101 | const x = 66; 102 | const y = 10; 103 | // move links as the nodes move 104 | linkLines 105 | .attr('x1', (l: any) => l.source.x + x) 106 | .attr('y1', (l: any) => l.source.y + y) 107 | .attr('x2', (l: any) => l.target.x + x) 108 | .attr('y2', (l: any) => l.target.y + y); 109 | } 110 | 111 | // SET DEPENDS ON VIEW 112 | if (view === 'depends_on') { 113 | // setting force simulation for the x position of the nodes 114 | const dependsForceX = (w: number) => 115 | d3 116 | .forceX((d: SNode) => { 117 | return getHorizontalPosition(d, w); 118 | }) 119 | .strength(0.3); 120 | 121 | // setting force simulation for the y position of the nodes 122 | const dependsForceY = (h: number) => 123 | d3 124 | .forceY((d: SNode) => { 125 | return getVerticalPosition(d, treeDepth, h); 126 | }) 127 | .strength(0.3); 128 | 129 | // initialize the simulation for the depends on view 130 | simulation 131 | .alpha(0.8) 132 | // set how strogly nodes are forced into postion 133 | .force('charge', d3.forceManyBody<SNode>().strength(-400)) 134 | // removes repelling force set in networks view 135 | .force('collide', null) 136 | // set the x force into given position of each node 137 | .force('x', dependsForceX(width)) 138 | // set the y force into given position of each node 139 | .force('y', dependsForceY(height)) 140 | // fired every step of the simulation 141 | .on('tick', ticked) 142 | // restart the simulation when switching so it keeps running 143 | .restart(); 144 | 145 | // reposition the simulation when the window size changes 146 | window.onresize = () => { 147 | // recalculate height and width of view 148 | const width = parseInt(container.style('width')); 149 | const height = parseInt(container.style('height')); 150 | // restart the simulation based on new dimensions 151 | simulation 152 | .alpha(0.5) 153 | .force('x', dependsForceX(width)) 154 | .force('y', dependsForceY(height)) 155 | .restart(); 156 | }; 157 | 158 | /** 159 | ********************* 160 | * Networks View 161 | ********************* 162 | */ 163 | } else { 164 | // declaring inital forces 165 | let forceX = d3.forceX<SNode>(0); 166 | let forceY = d3.forceY<SNode>(height / 2); 167 | 168 | /** 169 | * @type GROUP BY NETWORKS 170 | * @description Groups nodes by seperate networks or shared networks 171 | */ 172 | if (selectedNetwork === 'groupNetworks') { 173 | // initialize an object to store network names / groups 174 | const networkHolder: { [networkString: string]: boolean } = {}; 175 | /** 176 | * @params none 177 | * @output number : spacing between each network grouping for simluation 178 | * @description function to determine spacing of newtork groups based on total number of groups 179 | */ 180 | const getSpacing = (): number => { 181 | // iterate through each node 182 | d3Nodes.each((d: any) => { 183 | // if the node is part of a network 184 | if (d.networks) { 185 | // create one string of all networks sorted that node is a part 186 | let networkString = ''; 187 | d.networks.sort(); 188 | d.networks.forEach((network: string) => { 189 | networkString += network; 190 | }); 191 | // add network name or group to network holder 192 | networkHolder[networkString] = true; 193 | } 194 | }); 195 | // divide view container width by the total number of network groups 196 | return width / (Object.keys(networkHolder).length + 1); 197 | }; 198 | // invoke getSpacing function 199 | const spacing = getSpacing(); 200 | // determine the force of each node on the x axis 201 | forceX = d3 202 | .forceX((d: SNode): any => { 203 | // if node has a network 204 | if (d.networks) { 205 | // if no networks in networks object, set networks in center 206 | if (d.networks.length === 0) return width / 2; 207 | // create one string of all networks sorted that node is a part 208 | let networkString = ''; 209 | d.networks.sort(); 210 | d.networks.forEach((network) => { 211 | networkString += network; 212 | }); 213 | // find the location of node along x axis based on networks string 214 | const place = Object.keys(networkHolder).indexOf(networkString); 215 | return (place + 1) * spacing; 216 | } 217 | return width / 2; 218 | }) 219 | .strength(1); 220 | /** 221 | * @type INDIVIDUAL NETWORK VIEW 222 | * @description Center nodes from one network and push all other nodes to left 223 | */ 224 | } else { 225 | // set the force of all nodes that have that network to the center 226 | forceX = d3 227 | .forceX((d: SNode): number => { 228 | if (d.networks) { 229 | for (let n = 0; n < d.networks.length; n++) { 230 | if (d.networks[n] === selectedNetwork) { 231 | return width / 2; 232 | } 233 | } 234 | } 235 | // all other nodes are set to the left side of the creen 236 | return 0; 237 | }) 238 | // set high strength for nodes not part of selected network 239 | // make sure they end up on the left 240 | // allows center group to be more of a circle 241 | .strength((d: SNode): number => { 242 | if (d.networks) { 243 | for (let n = 0; n < d.networks.length; n++) { 244 | if (d.networks[n] === selectedNetwork) { 245 | return 0.5; 246 | } 247 | } 248 | } 249 | return 1; 250 | }); 251 | // set low strength y force for nodes not part of selected network 252 | // so they can spread out along the left side 253 | forceY = d3 254 | .forceY((d: SNode) => height / 2) 255 | .strength((d: SNode): number => { 256 | if (d.networks) { 257 | for (let n = 0; n < d.networks.length; n++) { 258 | if (d.networks[n] === selectedNetwork) { 259 | return 0.3; 260 | } 261 | } 262 | return 0.025; 263 | } 264 | return 0.025; 265 | }); 266 | } 267 | // } 268 | // } 269 | 270 | // create force simulation 271 | simulation 272 | .alpha(1) 273 | .force('x', forceX) 274 | .force('y', forceY) 275 | .force('charge', d3.forceManyBody<SNode>().strength(-150)) 276 | .force('collide', d3.forceCollide(radius / 1.3)) 277 | .on('tick', ticked) 278 | .restart(); 279 | } 280 | 281 | return () => { 282 | // clear window resize if changing away from depends view 283 | if (view === 'depends_on') { 284 | window.onresize = null; 285 | } 286 | }; 287 | }, [view, services, selectedNetwork]); 288 | 289 | return ( 290 | <> 291 | <div className="view-wrapper"> 292 | <svg className="graph"> 293 | <Nodes 294 | setSelectedContainer={setSelectedContainer} 295 | services={services} 296 | options={options} 297 | getColor={getColor} 298 | /> 299 | <Links services={services} view={view} /> 300 | </svg> 301 | </div> 302 | </> 303 | ); 304 | }; 305 | 306 | export default View; 307 | -------------------------------------------------------------------------------- /src/renderer/components/Volume.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module Volume.tsx 5 | * @author 6 | * @date 3/11/20 7 | * @description Show individual volumes or bind mounts 8 | * 9 | * ************************************ 10 | */ 11 | import React from 'react'; 12 | 13 | type Props = { 14 | volume: string; 15 | color: string; 16 | }; 17 | 18 | const Volume: React.FC<Props> = ({ volume, color }) => { 19 | return ( 20 | <div className="volumeLegend"> 21 | <div className="volumeColorName"> 22 | <svg className="volumeSvgBox"> 23 | <rect className="volumeSquare" rx={5} ry={5} fill={color} /> 24 | </svg> 25 | </div> 26 | <div> 27 | <p>{volume}</p> 28 | </div> 29 | </div> 30 | ); 31 | }; 32 | 33 | export default Volume; 34 | -------------------------------------------------------------------------------- /src/renderer/components/Volumes.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module DockerEngine.tsx 5 | * @author 6 | * @date 3/11/20 7 | * @description DockerEngine for bind mounts 8 | * 9 | * ************************************ 10 | */ 11 | import React from 'react'; 12 | import Volume from './Volume'; 13 | import { ReadOnlyObj } from '../App.d'; 14 | 15 | type Props = { 16 | volumes: ReadOnlyObj; 17 | getColor: (str: string | undefined) => string; 18 | }; 19 | 20 | const Volumes: React.FC<Props> = ({ volumes, getColor }) => { 21 | // interate through volumes object via the keys 22 | // creating an array of jsx Volume components for each volume 23 | const volumeNames = Object.keys(volumes).map((volume, i) => { 24 | // assign unique color by invoking the getColor closure function 25 | return <Volume key={'vol' + i} volume={volume} color={getColor(volume)} />; 26 | }); 27 | 28 | return <div className="volumes">{volumeNames}</div>; 29 | }; 30 | 31 | export default Volumes; 32 | -------------------------------------------------------------------------------- /src/renderer/components/VolumesWrapper.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module VolumesWrapper.tsx 5 | * @author 6 | * @date 3/17/20 7 | * @description Display area for the volumes and bindmounts 8 | * ************************************ 9 | */ 10 | import React from 'react'; 11 | // IMPORT COMPONENTS 12 | import Volumes from './Volumes'; 13 | import BindMounts from './BindMounts'; 14 | 15 | // IMPORT TYPES 16 | import { ReadOnlyObj } from '../App.d'; 17 | 18 | type Props = { 19 | volumes: ReadOnlyObj; 20 | bindMounts: Array<string>; 21 | getColor: (str: string | undefined) => string; 22 | }; 23 | 24 | const VolumesWrapper: React.FC<Props> = ({ volumes, bindMounts, getColor }) => { 25 | return ( 26 | <div className="volumes-wrapper"> 27 | <div className="container"> 28 | <div className="half"> 29 | <h2>Bind Mounts</h2> 30 | <hr /> 31 | <div className="scroll"> 32 | <BindMounts bindMounts={bindMounts} getColor={getColor} /> 33 | </div> 34 | </div> 35 | <div className="half"> 36 | <h2>Volumes</h2> 37 | <hr /> 38 | <div className="scroll"> 39 | <Volumes volumes={volumes} getColor={getColor} /> 40 | </div> 41 | </div> 42 | </div> 43 | </div> 44 | ); 45 | }; 46 | 47 | export default VolumesWrapper; 48 | -------------------------------------------------------------------------------- /src/renderer/helpers/colorSchemeIndex.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module colorSchemeIndex.tsx 5 | * @author Aris 6 | * @date 3/20/20 7 | * @description color function for volumes utilizing closure 8 | * ************************************ 9 | */ 10 | 11 | const colorSchemeIndex = () => { 12 | // keeps track of what colors have been used 13 | let currentIndex = 0; 14 | // keeping track of hue and lightness from hsl by volume name key 15 | const cachedColorObj: { 16 | [key: string]: { color: number; light: number }; 17 | } = {}; 18 | 19 | // returned function with closure values 20 | return (str: string | undefined) => { 21 | if (typeof str !== 'string') 22 | throw Error('Must pass string to colorSchemeIndex Closure'); 23 | 24 | // initialize color variables 25 | let currentColor: number; 26 | let currentLightness: number = 60; 27 | 28 | // if volume has already been assigned a color, return that colors 29 | if (cachedColorObj[str] !== undefined) { 30 | currentColor = cachedColorObj[str].color; 31 | currentLightness = cachedColorObj[str].light; 32 | return `hsl(${currentColor},80%,${currentLightness}%)`; 33 | } 34 | 35 | // if volume has not been assigned a color yet 36 | // calculate a new color hue (changes based on currentIndex) 37 | // by nine to keep good distance between colors 38 | // up to 360, which is max hue value 39 | currentColor = (40 * currentIndex + Math.floor(currentIndex / 9)) % 360; 40 | 41 | // first 9, lightness will be 60 42 | // third 9, lightness will be 30 43 | if (currentIndex >= 18 && currentIndex <= 27) { 44 | currentLightness = 30; 45 | // second 9, lightness will be 80 46 | } else if (currentIndex >= 9) { 47 | currentLightness = 80; 48 | } 49 | 50 | // increase the current index so next volume has a different color 51 | currentIndex++; 52 | 53 | // cache color assoicated with volume 54 | cachedColorObj[str] = { 55 | color: currentColor, 56 | light: currentLightness, 57 | }; 58 | 59 | // return the color 60 | return `hsl(${currentColor},80%,${currentLightness}%)`; 61 | }; 62 | }; 63 | 64 | export default colorSchemeIndex; 65 | -------------------------------------------------------------------------------- /src/renderer/helpers/getSimulationDimensions.ts: -------------------------------------------------------------------------------- 1 | import { SNode } from '../App.d'; 2 | 3 | export const getHorizontalPosition = (node: SNode, width: number) => { 4 | return (node.column / (node.rowLength + 1)) * width - 15; 5 | }; 6 | 7 | export const getVerticalPosition = ( 8 | node: SNode, 9 | treeDepth: number, 10 | height: number, 11 | ) => { 12 | return (height / treeDepth) * node.row; 13 | }; 14 | -------------------------------------------------------------------------------- /src/renderer/helpers/parseOpenError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************ 3 | * @name parseOpenError 4 | * @input errorText: Error object 5 | * @output array of strings of error descriptions 6 | * ************************ 7 | */ 8 | 9 | const parseOpenError = (errorText: Error) => { 10 | //split string into an array from line breaks 11 | const splitErrorMessage = errorText.message.split('\n'); 12 | //find the index where there is no information so we only get parts of the error message we want 13 | let startIndex = splitErrorMessage.findIndex((line: string) => { 14 | return line.includes('is invalid because'); 15 | }); 16 | 17 | if (startIndex === -1) { 18 | startIndex = splitErrorMessage.findIndex((line: string) => { 19 | return line.includes('Command failed'); 20 | }); 21 | } 22 | startIndex += 1; 23 | const paragraphIndex = splitErrorMessage.findIndex((line: string) => { 24 | return line === ''; 25 | }); 26 | const displayedErrorMessage = splitErrorMessage.slice( 27 | startIndex, 28 | paragraphIndex, 29 | ); 30 | return displayedErrorMessage; 31 | }; 32 | 33 | export default parseOpenError; 34 | -------------------------------------------------------------------------------- /src/renderer/helpers/setD3State.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module setGlobalVars.ts 5 | * @author 6 | * @date 3/24/20 7 | * @description algorithm to set global vars for forcegraph simulation 8 | * 9 | * ************************************ 10 | */ 11 | import { 12 | Services, 13 | NodesObject, 14 | TreeMap, 15 | Link, 16 | SNode, 17 | Port, 18 | D3State, 19 | Volumes, 20 | Ports, 21 | VolumeType, 22 | } from '../App.d'; 23 | import * as d3 from 'd3'; 24 | 25 | interface SetD3State { 26 | (services: Services): D3State; 27 | } 28 | 29 | /** 30 | * ******************** 31 | * EXTRACTOR FUNCTIONS 32 | * ******************** 33 | */ 34 | 35 | // PORTS: https://docs.docker.com/compose/compose-file/#ports 36 | interface ExtractPorts { 37 | (portsData: Ports): string[]; 38 | } 39 | export const extractPorts: ExtractPorts = (portsData) => { 40 | const ports: string[] = []; 41 | // short syntax string 42 | if (typeof portsData === 'string') { 43 | ports.push(portsData); 44 | // short or long syntax 45 | } else if (Array.isArray(portsData)) { 46 | portsData.forEach((port: string | Port) => { 47 | // short syntax 48 | if (typeof port === 'string') { 49 | const end = port.indexOf('/') !== -1 ? port.indexOf('/') : port.length; 50 | ports.push(port.slice(0, end)); 51 | // long syntax 52 | } else if (typeof port === 'object') { 53 | ports.push(port.published + ':' + port.target); 54 | } 55 | }); 56 | } 57 | 58 | return ports; 59 | }; 60 | 61 | // VOLUMES: https://docs.docker.com/compose/compose-file/#volumes 62 | interface ExtractVolumes { 63 | (VolumesData: Volumes): string[]; 64 | } 65 | export const extractVolumes: ExtractVolumes = (volumesData) => { 66 | const volumes: string[] = []; 67 | // short syntax string 68 | volumesData!.forEach((vol: VolumeType) => { 69 | // short syntax 70 | if (typeof vol === 'string') { 71 | volumes.push(vol); 72 | // long syntax 73 | } else if (typeof vol === 'object') { 74 | volumes.push(vol.source + ':' + vol.target); 75 | } 76 | }); 77 | return volumes; 78 | }; 79 | 80 | // NETWORKS: https://docs.docker.com/compose/compose-file/#networks 81 | interface ExtractNetworks { 82 | (networksData: string[] | {}): string[]; 83 | } 84 | export const extractNetworks: ExtractNetworks = (networksData) => { 85 | const networks = Array.isArray(networksData) 86 | ? networksData 87 | : Object.keys(networksData); 88 | return networks; 89 | }; 90 | 91 | // DEPENDS_ON: https://docs.docker.com/compose/compose-file/#depends_on 92 | interface ExtractDependsOn { 93 | (services: Services): Link[]; 94 | } 95 | export const extractDependsOn: ExtractDependsOn = (services) => { 96 | const links: Link[] = []; 97 | 98 | Object.keys(services).forEach((sName: string) => { 99 | if (services[sName].hasOwnProperty('depends_on')) { 100 | services[sName].depends_on!.forEach((el) => { 101 | links.push({ source: el, target: sName }); 102 | }); 103 | } 104 | }); 105 | 106 | return links; 107 | }; 108 | 109 | /** 110 | * ************* 111 | * DAG CREATOR 112 | * ************* 113 | * adds dag properties a d3 array of nodes passed in and returns depth of tree 114 | */ 115 | interface DagCreator { 116 | (nodesObject: SNode[], Links: Link[]): number; 117 | } 118 | export const dagCreator: DagCreator = (nodes, links) => { 119 | //roots object creation, needs to be a deep copy or else deletion of non-roots will remove from nodesObject 120 | const nodesObject: NodesObject = {}; 121 | nodes.forEach((node) => { 122 | nodesObject[node.name] = node; 123 | }); 124 | 125 | const roots = JSON.parse(JSON.stringify(nodesObject)); 126 | //iterate through links and find if the roots object contains any of the link targets 127 | links.forEach((link: Link) => { 128 | if (roots[link.target]) { 129 | //filter the roots 130 | delete roots[link.target]; 131 | } 132 | }); 133 | 134 | //create Tree 135 | const createTree = (node: NodesObject) => { 136 | Object.keys(node).forEach((root: string) => { 137 | links.forEach((link: Link) => { 138 | if (link.source === root) { 139 | node[root].children[link.target] = nodesObject[link.target]; 140 | } 141 | }); 142 | createTree(node[root].children); 143 | }); 144 | }; 145 | createTree(roots); 146 | 147 | //traverse tree and create object outlining the rows/columns in each tree 148 | const treeMap: TreeMap = {}; 149 | const createTreeMap = (node: NodesObject, height: number = 0) => { 150 | if (!treeMap[height] && Object.keys(node).length > 0) treeMap[height] = []; 151 | Object.keys(node).forEach((sName: string) => { 152 | treeMap[height].push(sName); 153 | createTreeMap(node[sName].children, height + 1); 154 | }); 155 | }; 156 | createTreeMap(roots); 157 | 158 | // populate nodesObject with column, row, and rowLength 159 | const storePositionLocation = (treeHierarchy: TreeMap) => { 160 | Object.keys(treeHierarchy).forEach((row: string) => { 161 | treeHierarchy[row].forEach((sName: string, column: number) => { 162 | nodesObject[sName].row = Number(row); 163 | if (!nodesObject[sName].column) nodesObject[sName].column = column + 1; 164 | nodesObject[sName].rowLength = treeHierarchy[row].length; 165 | }); 166 | }); 167 | }; 168 | storePositionLocation(treeMap); 169 | 170 | return Object.keys(treeMap).length; 171 | }; 172 | 173 | /** 174 | * ******************** 175 | * @param services 176 | * @returns an object with serviceGraph, simulation and treeDepth properties 177 | * ******************** 178 | */ 179 | const setD3State: SetD3State = (services) => { 180 | const links: Link[] = []; 181 | Object.keys(services).forEach((sName: string) => { 182 | if (services[sName].hasOwnProperty('depends_on')) { 183 | services[sName].depends_on!.forEach((el) => { 184 | links.push({ source: el, target: sName }); 185 | }); 186 | } 187 | }); 188 | 189 | const nodes = Object.keys(services).map((sName: string, i) => { 190 | // extract ports data if available 191 | const ports = services[sName].hasOwnProperty('ports') 192 | ? extractPorts(services[sName].ports as Ports) 193 | : []; 194 | // extract volumes data if available 195 | const volumes: string[] = services[sName].hasOwnProperty('volumes') 196 | ? extractVolumes(services[sName].volumes as Volumes) 197 | : []; 198 | // extract networks data if available 199 | const networks: string[] = services[sName].hasOwnProperty('networks') 200 | ? extractNetworks(services[sName].networks as string[]) 201 | : []; 202 | const node: SNode = { 203 | id: i, 204 | name: sName, 205 | ports, 206 | volumes, 207 | networks, 208 | children: {}, 209 | row: 0, 210 | rowLength: 0, 211 | column: 0, 212 | }; 213 | return node; 214 | }); 215 | 216 | const treeDepth = dagCreator(nodes, links); 217 | /** 218 | ********************* 219 | * Variables for d3 visualizer 220 | ********************* 221 | */ 222 | const d3State: D3State = { 223 | treeDepth, 224 | serviceGraph: { 225 | nodes, 226 | links, 227 | }, 228 | simulation: d3.forceSimulation<SNode>(nodes), 229 | }; 230 | 231 | return d3State; 232 | }; 233 | 234 | export default setD3State; 235 | -------------------------------------------------------------------------------- /src/renderer/helpers/static.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import * as url from 'url'; 3 | 4 | const isDevelopment = process.env.NODE_ENV !== 'production'; 5 | 6 | interface GetStatic { 7 | (val: string): string; 8 | } 9 | 10 | declare const __static: string; 11 | 12 | // see https://github.com/electron-userland/electron-/issues/99#issuecomment-459251702 13 | export const getStatic: GetStatic = val => { 14 | if (isDevelopment) { 15 | return url.resolve(window.location.origin, val); 16 | } 17 | if (process.env.NOT_PACKAGE) { 18 | return path.resolve(__dirname, '../../static/', val); 19 | } 20 | return path.resolve(__static, val); 21 | }; 22 | -------------------------------------------------------------------------------- /src/renderer/helpers/yamlParser.ts: -------------------------------------------------------------------------------- 1 | import { ReadOnlyObj, DependsOn, Services, VolumeType } from '../App.d'; 2 | 3 | type YamlState = { 4 | fileOpened: boolean; 5 | services: Services; 6 | dependsOn?: DependsOn; 7 | networks?: ReadOnlyObj; 8 | volumes?: ReadOnlyObj; 9 | bindMounts?: Array<string>; 10 | }; 11 | 12 | const convertYamlToState = (file: any) => { 13 | const services = file.services; 14 | const volumes = file.volumes ? file.volumes : {}; 15 | const networks = file.networks ? file.networks : {}; 16 | const state: YamlState = Object.assign( 17 | {}, 18 | { fileOpened: true, services, volumes, networks }, 19 | ); 20 | const bindMounts: string[] = []; 21 | // iterate through each service 22 | Object.keys(services).forEach((name): void => { 23 | // IF SERVICE HAS VOLUMES PROPERTY 24 | if (services[name].volumes) { 25 | // iterate from all the volumes 26 | services[name].volumes.forEach((volume: VolumeType): void => { 27 | let v = ''; 28 | if (typeof volume === 'string') { 29 | // if its a bind mount, capture it 30 | v = volume.split(':')[0]; 31 | } else if ( 32 | 'source' in volume && 33 | volume.source && 34 | volume.type === 'bind' 35 | ) { 36 | v = volume.source; 37 | } 38 | if (!!v && !volumes.hasOwnProperty(v)) { 39 | bindMounts.push(v); 40 | } 41 | }); 42 | } 43 | }); 44 | state.bindMounts = bindMounts; 45 | return state; 46 | }; 47 | 48 | export default convertYamlToState; 49 | -------------------------------------------------------------------------------- /src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module index.tsx 5 | * @author Joshua Nordstrom 6 | * @date 3/7/20 7 | * @description entry point for application. Hangs React app off of #root in index.html 8 | * 9 | * ************************************ 10 | */ 11 | 12 | import * as React from 'react'; 13 | import { render } from 'react-dom'; 14 | import App from './App'; 15 | import { D3State } from '../renderer/App.d'; 16 | 17 | // IMPORT STYLES 18 | import './styles/app.scss'; 19 | 20 | if (module.hot) { 21 | module.hot.accept(); 22 | } 23 | 24 | declare global { 25 | interface Window { 26 | d3State: D3State; 27 | } 28 | } 29 | 30 | render( 31 | <> 32 | <App /> 33 | </>, 34 | document.getElementById('app'), 35 | ); 36 | -------------------------------------------------------------------------------- /src/renderer/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #003459; 2 | $secondary-color: #2274a5; 3 | $third-color: #074f57; 4 | $fourth-color: #f7fbff; 5 | $fifth-color: #fff1b8; 6 | $hover-color: #e0e9f1; 7 | $border: 1px solid #003459; 8 | $font-fam: 'Sen-Regular'; 9 | -------------------------------------------------------------------------------- /src/renderer/styles/app.scss: -------------------------------------------------------------------------------- 1 | // IMPORT SASS 2 | @import 'variables'; 3 | @import 'optionBar'; 4 | @import 'leftNav'; 5 | @import 'd3Wrapper'; 6 | 7 | @font-face { 8 | font-family: 'Sen-Regular'; 9 | src: url(./fonts/Sen-Regular.ttf) format('truetype'); 10 | } 11 | 12 | @font-face { 13 | font-family: 'Sen-Bold'; 14 | src: url(./fonts/Sen-Bold.ttf) format('truetype'); 15 | } 16 | 17 | @font-face { 18 | font-family: 'Sen-ExtraBold'; 19 | src: url(./fonts/Sen-ExtraBold.ttf) format('truetype'); 20 | } 21 | 22 | html * { 23 | font-family: $font-fam; 24 | } 25 | 26 | body { 27 | font-size: 14px; 28 | -webkit-user-select: none; 29 | margin: 0; 30 | } 31 | 32 | .flex { 33 | display: flex; 34 | } 35 | 36 | h1 { 37 | font-family: 'Sen-Bold'; 38 | } 39 | 40 | h2 { 41 | color: $primary-color; 42 | font-size: 1.5em; 43 | line-height: 1em; 44 | align-self: center; 45 | } 46 | 47 | .app-class { 48 | background-color: $fourth-color; 49 | display: flex; 50 | min-height: 650px; 51 | height: 100vh; 52 | width: 100vw; 53 | 54 | .main { 55 | flex-grow: 1; 56 | min-width: 700px; 57 | flex-direction: column; 58 | } 59 | 60 | // creates draggable bar at the top of application to replace removed native bar 61 | .draggable { 62 | -webkit-app-region: drag; 63 | height: 1.5em; 64 | position: absolute; 65 | top: 0; 66 | left: 0; 67 | width: 100vw; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/renderer/styles/d3Wrapper.scss: -------------------------------------------------------------------------------- 1 | .d3-wrapper { 2 | display: flex; 3 | flex: 1 1 0; 4 | min-height: 500px; 5 | align-items: center; 6 | justify-content: center; 7 | margin-right: 1em; 8 | 9 | .error-open-wrapper { 10 | width: 70%; 11 | display: flex; 12 | flex-direction: column; 13 | color: red; 14 | 15 | .error-display { 16 | display: flex; 17 | flex-direction: column; 18 | } 19 | } 20 | 21 | .services-wrapper { 22 | flex: 1 1 0; 23 | height: 100%; 24 | 25 | .view-wrapper { 26 | height: 100%; 27 | width: 100%; 28 | 29 | .graph { 30 | width: 100%; 31 | height: 100%; 32 | 33 | .node { 34 | cursor: pointer; 35 | .nodeLabel { 36 | font-family: 'Sen-Bold'; 37 | fill: $fourth-color; 38 | font-size: 16px; 39 | transform: rotate(-13deg); 40 | text-anchor: middle; 41 | } 42 | .nodeLabelStroke { 43 | font-family: 'Sen-Bold'; 44 | stroke: #006896; 45 | stroke-width: 4; 46 | font-size: 16px; 47 | transform: rotate(-13deg); 48 | text-anchor: middle; 49 | } 50 | .port { 51 | stroke: $secondary-color; 52 | transform: rotate(5deg); 53 | fill: $fifth-color; 54 | opacity: 0.9; 55 | } 56 | .ports-text { 57 | transform: rotate(4.6deg); 58 | 59 | tspan { 60 | font-family: 'Sen-Bold' !important; 61 | fill: $primary-color; 62 | } 63 | } 64 | } 65 | 66 | .hide { 67 | display: none; 68 | } 69 | 70 | .arrowHead { 71 | fill: #000000; 72 | .line-cover { 73 | fill: $fourth-color; 74 | } 75 | } 76 | 77 | .link { 78 | stroke: black; 79 | } 80 | } 81 | } 82 | } 83 | 84 | .volumes-wrapper { 85 | width: 200px; 86 | height: 100%; 87 | display: flex; 88 | align-items: center; 89 | margin: 0 1em; 90 | 91 | .container { 92 | width: 100%; 93 | height: 90%; 94 | background-color: $hover-color; 95 | border-radius: 2em; 96 | display: flex; 97 | flex-direction: column; 98 | align-items: stretch; 99 | 100 | .half { 101 | flex: 1 1 0; 102 | display: flex; 103 | flex-direction: column; 104 | align-items: center; 105 | height: fit-content; 106 | max-height: 50%; 107 | padding: 1.5em; 108 | } 109 | 110 | h2 { 111 | margin: 0.75em 0 0.5em 0; 112 | } 113 | 114 | hr { 115 | border-top: $border; 116 | width: 80%; 117 | margin: 0 0 0.75em 0; 118 | } 119 | 120 | .volumeLegend { 121 | margin: 0 0 0.75em 0.5em; 122 | display: flex; 123 | 124 | p { 125 | margin: 0 0 0 0; 126 | } 127 | 128 | .volumeColorName { 129 | align-self: center; 130 | } 131 | 132 | .volumeSvgBox { 133 | height: 2em; 134 | width: 2em; 135 | 136 | .volumeSquare { 137 | height: 1.5em; 138 | width: 1.5em; 139 | } 140 | } 141 | } 142 | 143 | .scroll { 144 | overflow-y: auto; 145 | scroll-behavior: smooth; 146 | min-width: 100%; 147 | } 148 | } 149 | } 150 | 151 | .file-open { 152 | display: flex; 153 | flex-direction: column; 154 | align-items: center; 155 | text-align: center; 156 | color: #003459; 157 | h5 { 158 | font-size: 1em; 159 | align-self: center; 160 | margin: 0; 161 | } 162 | .select-file-button:hover { 163 | cursor: pointer; 164 | opacity: 0.9; 165 | } 166 | 167 | button { 168 | background: none; 169 | color: inherit; 170 | border: none; 171 | padding: 0; 172 | font: inherit; 173 | cursor: pointer; 174 | opacity: 0.9; 175 | outline: inherit; 176 | } 177 | } 178 | 179 | div button { 180 | margin: 0 auto; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/renderer/styles/fonts/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2015 The Sen Project Authors (https://github.com/philatype/Sen) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /src/renderer/styles/fonts/Sen-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nautilusapp/nautilus/3dae444d82a9e16262e75f7ed6be9ca0312143b9/src/renderer/styles/fonts/Sen-Bold.ttf -------------------------------------------------------------------------------- /src/renderer/styles/fonts/Sen-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nautilusapp/nautilus/3dae444d82a9e16262e75f7ed6be9ca0312143b9/src/renderer/styles/fonts/Sen-ExtraBold.ttf -------------------------------------------------------------------------------- /src/renderer/styles/fonts/Sen-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nautilusapp/nautilus/3dae444d82a9e16262e75f7ed6be9ca0312143b9/src/renderer/styles/fonts/Sen-Regular.ttf -------------------------------------------------------------------------------- /src/renderer/styles/leftNav.scss: -------------------------------------------------------------------------------- 1 | .left-nav { 2 | display: grid; 3 | grid-template-columns: 220px; 4 | grid-template-rows: 320px 1fr; 5 | grid-template-areas: 'top-half' 'info'; 6 | background-color: $primary-color; 7 | min-height: 650px; 8 | max-height: 100vh; 9 | 10 | .top-half { 11 | grid-area: top-half; 12 | 13 | .title { 14 | margin: 12% 10% 2% 10%; 15 | display: flex; 16 | flex-direction: column; 17 | align-items: center; 18 | justify-content: center; 19 | .cls-1 { 20 | isolation: isolate; 21 | } 22 | .cls-2 { 23 | opacity: 0.75; 24 | mix-blend-mode: multiply; 25 | } 26 | .cls-3 { 27 | fill: none; 28 | stroke-linecap: round; 29 | stroke-miterlimit: 10; 30 | stroke-width: 15px; 31 | stroke: url(#linear-gradient); 32 | } 33 | .logo { 34 | width: 100%; 35 | padding-right: 0.5em; 36 | margin-left: 1.5em; 37 | } 38 | 39 | h1 { 40 | color: $fourth-color; 41 | font-size: 3em; 42 | margin-top: 0; 43 | } 44 | } 45 | 46 | .file-open { 47 | margin: 10%; 48 | background: none; 49 | color: $fourth-color; 50 | font-size: 1em; 51 | display: flex; 52 | justify-content: center; 53 | 54 | h5 { 55 | font-size: 1em; 56 | align-self: center; 57 | padding-left: 0.5em; 58 | margin: 0; 59 | } 60 | 61 | .open-flex { 62 | display: flex; 63 | justify-content: center; 64 | padding: 0.3em; 65 | } 66 | 67 | .select-file-button:hover { 68 | cursor: hand; 69 | cursor: pointer; 70 | color: $fifth-color; 71 | } 72 | 73 | button { 74 | background: none; 75 | color: inherit; 76 | border: none; 77 | padding: 0; 78 | font: inherit; 79 | cursor: hand; 80 | cursor: pointer; 81 | opacity: 0.9; 82 | outline: inherit; 83 | } 84 | } 85 | } 86 | 87 | .info-dropdown { 88 | padding: 0 1em 1em 1em; 89 | grid-area: info; 90 | max-height: 100%; 91 | min-height: 0; 92 | display: grid; 93 | grid-template-columns: 100%; 94 | grid-template-rows: auto 1fr; 95 | grid-template-areas: 'name' 'card'; 96 | max-width: 100%; 97 | 98 | h3 { 99 | grid-area: name; 100 | text-align: center; 101 | color: $fourth-color; 102 | margin-bottom: 0.5em; 103 | font-size: 16px; 104 | } 105 | 106 | .content-wrapper { 107 | grid-area: card; 108 | height: 100%; 109 | max-width: 100%; 110 | 111 | display: flex; 112 | flex: 1; 113 | min-height: 0; 114 | .overflow-container { 115 | flex: 1; 116 | overflow-y: auto; 117 | scroll-behavior: smooth; 118 | 119 | .overview { 120 | min-height: min-content; 121 | flex-direction: column; 122 | padding: 1em; 123 | background-color: $fourth-color; 124 | font-size: 0.8em; 125 | overflow-wrap: break-word; 126 | border-radius: 0.25em; 127 | 128 | div { 129 | padding-bottom: 0.5em; 130 | } 131 | 132 | .command { 133 | color: $primary-color; 134 | font-weight: 800; 135 | } 136 | 137 | .options { 138 | padding: 0; 139 | 140 | div { 141 | padding-left: 1em; 142 | padding-bottom: 0; 143 | 144 | ul { 145 | padding: 0; 146 | padding-inline-start: 1.3em; 147 | 148 | li { 149 | span { 150 | font-weight: 600; 151 | } 152 | } 153 | } 154 | 155 | .option-key { 156 | font-weight: 600; 157 | } 158 | } 159 | } 160 | 161 | .second-level { 162 | span { 163 | font-weight: 600; 164 | } 165 | } 166 | 167 | ul { 168 | padding-inline-start: 1.3em; 169 | margin: 0; 170 | } 171 | } 172 | } 173 | } 174 | } 175 | 176 | .card-header { 177 | position: sticky; 178 | background: none; 179 | color: $fourth-color; 180 | background-color: $primary-color; 181 | padding: 1em; 182 | } 183 | 184 | .leftNavContainerTitle { 185 | text-align: center; 186 | color: $fifth-color; 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/renderer/styles/optionBar.scss: -------------------------------------------------------------------------------- 1 | .option-bar { 2 | display: flex; 3 | justify-content: center; 4 | border-bottom: $border; 5 | padding: 1.5em; 6 | 7 | .views { 8 | flex: 1 1 0; 9 | justify-content: flex-end; 10 | select { 11 | color: $secondary-color; 12 | background-color: $fourth-color; 13 | font-size: 14px; 14 | border: none; 15 | } 16 | select:hover { 17 | cursor: pointer; 18 | } 19 | select:focus { 20 | outline: none; 21 | } 22 | } 23 | 24 | .titles { 25 | justify-content: center; 26 | 27 | .vl { 28 | border-left: $border; 29 | } 30 | 31 | h2 { 32 | margin: 0em 0.5em 0em 0.3em; 33 | } 34 | } 35 | 36 | .options { 37 | flex: 1 1 0; 38 | justify-content: flex-start; 39 | align-content: flex-start; 40 | } 41 | 42 | .selected { 43 | background-color: $hover-color !important; 44 | border: 1px solid #c1d3e0 !important; 45 | font-weight: bolder; 46 | } 47 | 48 | .option { 49 | color: $secondary-color; 50 | padding: 0em 0.3em !important; 51 | border-radius: 0.5em; 52 | margin: 0 3%; 53 | } 54 | 55 | .option:hover { 56 | background-color: $hover-color; 57 | cursor: pointer; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import 'jest-enzyme'; 2 | -------------------------------------------------------------------------------- /static/Nautilus-text-logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nautilusapp/nautilus/3dae444d82a9e16262e75f7ed6be9ca0312143b9/static/Nautilus-text-logo2.png -------------------------------------------------------------------------------- /static/arrow.svg: -------------------------------------------------------------------------------- 1 | <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{stroke:#000;stroke-miterlimit:10;}</style></defs><title>Nautilus Arrow Design -------------------------------------------------------------------------------- /static/containerPath.ts: -------------------------------------------------------------------------------- 1 | const containerPath = ` 2 | 3 | 4 | 5 | 6 | 7 | 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 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | `; 135 | 136 | export default containerPath; 137 | -------------------------------------------------------------------------------- /static/nautilus-new-ui-mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nautilusapp/nautilus/3dae444d82a9e16262e75f7ed6be9ca0312143b9/static/nautilus-new-ui-mockup.png -------------------------------------------------------------------------------- /static/nautilus-text-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nautilusapp/nautilus/3dae444d82a9e16262e75f7ed6be9ca0312143b9/static/nautilus-text-logo.png -------------------------------------------------------------------------------- /static/nautilus_logo.svg: -------------------------------------------------------------------------------- 1 | Nautilus Logo -------------------------------------------------------------------------------- /static/options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nautilusapp/nautilus/3dae444d82a9e16262e75f7ed6be9ca0312143b9/static/options.png -------------------------------------------------------------------------------- /static/views.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nautilusapp/nautilus/3dae444d82a9e16262e75f7ed6be9ca0312143b9/static/views.png -------------------------------------------------------------------------------- /tsconfig-webpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["__tests__/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/electron-webpack/tsconfig-base.json", 3 | "compilerOptions": { 4 | "jsx": "react", 5 | "noImplicitAny": true, 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "target": "es6", 10 | "allowJs": true 11 | }, 12 | "exclude": [ 13 | "dist/**/*", 14 | "release/**/*", 15 | "webpack.main.ext.js", 16 | "webpack.renderer.ext.js", 17 | "babel.config.js" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /webpack.main.ext.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.module.rules.push({ 3 | test: /\.js$/, 4 | use: ['source-map-loader'], 5 | enforce: 'pre', 6 | }); 7 | 8 | const path = require('path'); 9 | const tsxRule = config.module.rules.filter(rule => 10 | rule.test.toString().match(/tsx/), 11 | )[0]; 12 | const tsLoader = tsxRule.use.filter(use => use.loader === 'ts-loader')[0]; 13 | tsLoader.options.configFile = path.join(__dirname, 'tsconfig-webpack.json'); 14 | 15 | return config; 16 | }; 17 | -------------------------------------------------------------------------------- /webpack.renderer.ext.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | const path = require('path'); 3 | const tsxRule = config.module.rules.filter(rule => 4 | rule.test.toString().match(/tsx/), 5 | )[0]; 6 | const tsLoader = tsxRule.use.filter(use => use.loader === 'ts-loader')[0]; 7 | tsLoader.options.configFile = path.join(__dirname, '/tsconfig-webpack.json'); 8 | 9 | return config; 10 | }; 11 | --------------------------------------------------------------------------------