├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── Reference └── NautilusReactComponentHierarchy.jpg ├── babel.config.js ├── build ├── background.tiff ├── nautilus_logo.icns ├── nautilus_logo.ico └── nautilus_logo.png ├── electron-webpack.json ├── package.json ├── samples ├── conf │ ├── back │ │ ├── .initial_setup.lock │ │ ├── celery.py │ │ └── config.py │ ├── front │ │ ├── .initial_setup.lock │ │ └── config.json │ └── proxy │ │ ├── .initial_setup.lock │ │ ├── nginx.conf │ │ └── proxy_params ├── data │ └── media │ │ └── taiga-media ├── docker-compose.BAD.yml ├── docker-compose.bpc.yml ├── docker-compose.dur.yml ├── docker-compose.durran.yml ├── docker-compose.rem.yml ├── docker-compose.taiga.yml ├── docker-compose.yml ├── docker-compose2.yml ├── docker-compose3.yml ├── docker-composeDEMO.yml ├── kube.yml ├── kube2.yml ├── kubernetes.yml ├── nginx-golang-mysql │ ├── README.md │ ├── backend │ │ ├── Dockerfile │ │ ├── go.mod │ │ └── main.go │ ├── db │ │ └── password.txt │ ├── docker-compose.yaml │ └── proxy │ │ ├── Dockerfile │ │ └── conf ├── react-express-mongodb │ ├── .gitignore │ ├── README.md │ ├── backend │ │ ├── .dockerignore │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── config │ │ │ ├── config.js │ │ │ ├── config.json │ │ │ └── messages.js │ │ ├── db │ │ │ └── index.js │ │ ├── logs │ │ │ └── .gitkeep │ │ ├── models │ │ │ └── todos │ │ │ │ └── todo.js │ │ ├── package.json │ │ ├── routes │ │ │ └── index.js │ │ ├── server.js │ │ └── utils │ │ │ └── helpers │ │ │ ├── logger.js │ │ │ └── responses.js │ ├── docker-compose.rem.yml │ ├── frontend │ │ ├── .dockerignore │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── package.json │ │ ├── public │ │ │ ├── favicon.ico │ │ │ ├── index.html │ │ │ ├── logo192.png │ │ │ ├── logo512.png │ │ │ ├── manifest.json │ │ │ └── robots.txt │ │ └── src │ │ │ ├── App.js │ │ │ ├── App.scss │ │ │ ├── components │ │ │ ├── AddTodo.js │ │ │ └── TodoList.js │ │ │ ├── custom.scss │ │ │ ├── index.css │ │ │ ├── index.js │ │ │ ├── logo.svg │ │ │ └── serviceWorker.js │ └── output.png └── variables.env ├── scripts ├── deploy.sh └── run-test.sh ├── src ├── common │ ├── resolveEnvVariables.ts │ └── runShellTasks.ts ├── hooks.ts ├── main │ ├── main.ts │ └── menu.ts ├── reducers │ └── appSlice.ts ├── renderer │ ├── App.d.ts │ ├── App.tsx │ ├── components │ │ ├── BindMounts.tsx │ │ ├── ClusterDeployment.tsx │ │ ├── ComposeDeployment.tsx │ │ ├── D3Wrapper.tsx │ │ ├── ErrorDisplay.tsx │ │ ├── FileSelector.tsx │ │ ├── KubeDeployment.tsx │ │ ├── LeftNav.tsx │ │ ├── Links.tsx │ │ ├── NetworksDropdown.tsx │ │ ├── Nodes.tsx │ │ ├── OptionBar.tsx │ │ ├── ServiceInfo.tsx │ │ ├── SwarmDeployment.tsx │ │ ├── Tab.tsx │ │ ├── TabBar.tsx │ │ ├── Title.tsx │ │ ├── View.tsx │ │ ├── Volume.tsx │ │ ├── Volumes.tsx │ │ └── VolumesWrapper.tsx │ ├── helpers │ │ ├── colorSchemeIndex.ts │ │ ├── fileOpen.ts │ │ ├── getSimulationDimensions.ts │ │ ├── parseOpenError.ts │ │ ├── setD3State.ts │ │ ├── static.ts │ │ └── yamlParser.ts │ ├── index.tsx │ └── styles │ │ ├── _variables.scss │ │ ├── app.scss │ │ ├── d3Wrapper.scss │ │ ├── deployButton.scss │ │ ├── fonts │ │ ├── OFL.txt │ │ ├── Sen-Bold.ttf │ │ ├── Sen-ExtraBold.ttf │ │ └── Sen-Regular.ttf │ │ ├── leftNav.scss │ │ ├── optionBar.scss │ │ ├── popup.scss │ │ └── tabBar.scss ├── setupTests.ts └── store.ts ├── static ├── NautilX-app-Tree.jpg ├── NautilX-logo.png ├── Nautilus-text-logo2.png ├── arrow.svg ├── box.svg ├── boxPath.ts ├── containerPath_old.ts ├── container_old.svg ├── kubernetes-icon-color.png ├── nautilus-new-ui-mockup.png ├── nautilus-text-logo.png ├── nautilus_logo.svg ├── nautilx_logo.png ├── options.png ├── screenshots │ ├── container-dependent-view.png │ ├── deploy-container.gif │ ├── deploy-to-swarm.gif │ ├── healthvid.gif │ ├── info-ports-volumes.png │ ├── network-view.png │ ├── open-file.png │ └── view-multiple-files.gif └── views.png ├── tsconfig-webpack.json ├── tsconfig.json ├── webpack.main.ext.js ├── webpack.renderer.ext.js ├── yarn-error.log └── 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 | "object-curly-newline": ["error", { "multiline": true, "minProperties": 3 }], 13 | "react/jsx-filename-extension": ["warn", { "extensions": [".js", ".jsx"] }], 14 | "max-len": [ 15 | "warn", 16 | { 17 | "code": 80, 18 | "tabWidth": 2, 19 | "comments": 80, 20 | "ignoreComments": false, 21 | "ignoreTrailingComments": true, 22 | "ignoreUrls": true, 23 | "ignoreStrings": true, 24 | "ignoreTemplateLiterals": true, 25 | "ignoreRegExpLiterals": true 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 | - 17 5 | 6 | env: 7 | global: PATH=/opt/python/3.7.1/bin:$PATH 8 | 9 | notifications: 10 | email: false 11 | 12 | # cache some files for faster builds 13 | cache: 14 | yarn: true 15 | directories: 16 | - node_modules 17 | 18 | # install dependenices 19 | before-script: 20 | - yarn 21 | 22 | # on PRs and merges to master and prod run tests and build the app - REMOVED DUE TO BROKEN TESTING - To restore add script: yarn test 23 | script: 24 | - yarn test 25 | # only run this script on pull requests and merges into 26 | # the 'master' branches 27 | branches: 28 | only: 29 | - michaelbayday-patch1 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Reference/NautilusReactComponentHierarchy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/Reference/NautilusReactComponentHierarchy.jpg -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | '@babel/preset-react', 5 | '@babel/preset-typescript', 6 | ], 7 | plugins: [ 8 | '@babel/plugin-proposal-class-properties', 9 | '@babel/plugin-transform-runtime', 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /build/background.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/build/background.tiff -------------------------------------------------------------------------------- /build/nautilus_logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/build/nautilus_logo.icns -------------------------------------------------------------------------------- /build/nautilus_logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/build/nautilus_logo.ico -------------------------------------------------------------------------------- /build/nautilus_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/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": "2.0.0", 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 | "build": { 19 | "appId": "com.nautilus.app", 20 | "productName": "Nautilus", 21 | "files": [ 22 | "dist", 23 | "static", 24 | "package.json" 25 | ], 26 | "linux": { 27 | "category": "WebDevelopment", 28 | "icon": "build/nautilus_logo.icns", 29 | "desktop": { 30 | "Comment": "Nautilus: A Docker Compose Charting Tool", 31 | "Name": "Nautilus", 32 | "StartupNotify": "true", 33 | "Terminal": "false", 34 | "Type": "Application", 35 | "Categories": "WebDevelopment" 36 | }, 37 | "executableName": "nautilus", 38 | "maintainer": "", 39 | "target": [ 40 | "AppImage" 41 | ] 42 | }, 43 | "mac": { 44 | "category": "public.app-category.developer-tools", 45 | "icon": "build/nautilus_logo.icns", 46 | "target": [ 47 | "dmg" 48 | ] 49 | }, 50 | "dmg": { 51 | "background": "build/background.tiff", 52 | "contents": [ 53 | { 54 | "x": 100, 55 | "y": 400 56 | }, 57 | { 58 | "x": 550, 59 | "y": 400, 60 | "type": "link", 61 | "path": "/Applications" 62 | } 63 | ] 64 | }, 65 | "win": { 66 | "asar": false, 67 | "icon": "build/nautilus_logo.ico", 68 | "target": [ 69 | "nsis" 70 | ] 71 | }, 72 | "nsis": { 73 | "createStartMenuShortcut": true, 74 | "createDesktopShortcut": true, 75 | "runAfterFinish": true 76 | }, 77 | "directories": { 78 | "output": "release" 79 | } 80 | }, 81 | "repository": { 82 | "type": "git", 83 | "url": "git+https://github.com/oslabs-beta/nautilus.git" 84 | }, 85 | "keywords": [ 86 | "docker", 87 | "compose", 88 | "visualizer" 89 | ], 90 | "author": "team nautilis", 91 | "license": "MIT", 92 | "bugs": { 93 | "url": "https://github.com/oslabs-beta/nautilus/issues" 94 | }, 95 | "homepage": "https://github.com/oslabs-beta/nautilus#readme", 96 | "dependencies": { 97 | "@babel/runtime-corejs3": "^7.10.2", 98 | "@electron/remote": "^2.0.8", 99 | "@types/react-redux": "^7.1.24", 100 | "babel-polyfill": "^6.26.0", 101 | "css-hot-loader": "^1.4.4", 102 | "d3": "^5.15.0", 103 | "electron": "17.1.0", 104 | "electron-devtools-installer": "^3.2.0", 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-draggable": "^4.4.2", 112 | "react-icons": "^3.9.0", 113 | "redux-devtools-extension": "^2.13.9" 114 | }, 115 | "devDependencies": { 116 | "@babel/core": "^7.0.0-0", 117 | "@babel/plugin-proposal-class-properties": "^7.8.3", 118 | "@babel/plugin-transform-runtime": "^7.10.1", 119 | "@babel/preset-env": "^7.9.0", 120 | "@babel/preset-react": "^7.9.4", 121 | "@babel/preset-typescript": "^7.9.0", 122 | "@reduxjs/toolkit": "^1.8.2", 123 | "@types/d3": "^5.7.2", 124 | "@types/electron-devtools-installer": "^2.2.0", 125 | "@types/enzyme": "^3.10.5", 126 | "@types/enzyme-adapter-react-16": "^1.0.6", 127 | "@types/eslint": "^6.1.8", 128 | "@types/express": "^4.17.3", 129 | "@types/jest": "^25.1.4", 130 | "@types/js-yaml": "^3.12.2", 131 | "@types/node": "^13.9.1", 132 | "@types/react": "^16.9.23", 133 | "@types/react-dom": "^16.9.5", 134 | "@types/react-test-renderer": "^16.9.2", 135 | "@typescript-eslint/eslint-plugin": "^2.24.0", 136 | "@typescript-eslint/parser": "^2.24.0", 137 | "babel": "^6.23.0", 138 | "babel-eslint": "^10.1.0", 139 | "babel-jest": "^25.2.4", 140 | "concurrently": "^5.1.0", 141 | "cross-env": "^7.0.2", 142 | "css-loader": "^3.4.2", 143 | "electron-builder": "^22.4.1", 144 | "electron-webpack": "^2.7.4", 145 | "enzyme": "^3.11.0", 146 | "enzyme-adapter-react-16": "^1.15.2", 147 | "eslint-config-airbnb": "^18.1.0", 148 | "eslint-config-prettier": "^6.11.0", 149 | "eslint-plugin-import": "^2.20.2", 150 | "eslint-plugin-jsx-a11y": "^6.2.3", 151 | "eslint-plugin-prettier": "^3.1.3", 152 | "eslint-plugin-react": "^7.20.0", 153 | "eslint-plugin-react-hooks": "2.5.0", 154 | "jest": "^26.0.1-alpha.0", 155 | "jest-environment-enzyme": "^7.1.2", 156 | "jest-enzyme": "^7.1.2", 157 | "nodemon": "^2.0.2", 158 | "react-hot-loader": "^4.12.20", 159 | "react-redux": "^8.0.2", 160 | "react-test-renderer": "^16.13.1", 161 | "sass": "^1.52.3", 162 | "sass-loader": "^8.0.2", 163 | "source-map-loader": "^0.2.4", 164 | "style-loader": "^1.1.3", 165 | "ts-loader": "^6.2.1", 166 | "typescript": "^3.8.3", 167 | "url-loader": "^3.0.0", 168 | "webpack": "^4.42.0", 169 | "webpack-cli": "^3.3.11", 170 | "webpack-dev-server": "^3.10.3" 171 | }, 172 | "resolutions": { 173 | "@types/react": "^16.9.23" 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /samples/conf/back/.initial_setup.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/samples/conf/back/.initial_setup.lock -------------------------------------------------------------------------------- /samples/conf/back/celery.py: -------------------------------------------------------------------------------- 1 | from kombu import Queue 2 | 3 | broker_url = 'amqp://taiga:password@rabbit:5672/taiga' 4 | result_backend = 'redis://anything:password@redis:6379/0' 5 | 6 | accept_content = ['pickle',] # Values are 'pickle', 'json', 'msgpack' and 'yaml' 7 | task_serializer = "pickle" 8 | result_serializer = "pickle" 9 | 10 | timezone = 'Europe/Madrid' 11 | 12 | task_default_queue = 'tasks' 13 | task_queues = ( 14 | Queue('tasks', routing_key='task.#'), 15 | Queue('transient', routing_key='transient.#', delivery_mode=1) 16 | ) 17 | task_default_exchange = 'tasks' 18 | task_default_exchange_type = 'topic' 19 | task_default_routing_key = 'task.default' 20 | -------------------------------------------------------------------------------- /samples/conf/back/config.py: -------------------------------------------------------------------------------- 1 | from .common import * 2 | 3 | PUBLIC_REGISTER_ENABLED = False 4 | DEBUG = False 5 | TEMPLATE_DEBUG = False 6 | 7 | SECRET_KEY = 'secret' 8 | 9 | MEDIA_URL = "http://taiga.lan/media/" 10 | STATIC_URL = "http://taiga.lan/static/" 11 | ADMIN_MEDIA_PREFIX = "http://taiga.lan/static/admin/" 12 | SITES["api"]["scheme"] = "http" 13 | SITES["api"]["domain"] = "taiga.lan" 14 | SITES["front"]["scheme"] = "http" 15 | SITES["front"]["domain"] = "taiga.lan" 16 | 17 | DATABASES = { 18 | "default": { 19 | "ENGINE": "django.db.backends.postgresql", 20 | "NAME": "taiga", 21 | "HOST": "db", 22 | "USER": "postgres", 23 | "PASSWORD": "password" 24 | } 25 | } 26 | 27 | #DEFAULT_FROM_EMAIL = "john@doe.com" 28 | #CHANGE_NOTIFICATIONS_MIN_INTERVAL = 300 #seconds 29 | #EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 30 | #EMAIL_USE_TLS = False 31 | #EMAIL_USE_SSL = False # You cannot use both (TLS and SSL) at the same time! 32 | #EMAIL_HOST = 'localhost' 33 | #EMAIL_PORT = 25 34 | #EMAIL_HOST_USER = 'user' 35 | #EMAIL_HOST_PASSWORD = 'password' 36 | 37 | EVENTS_PUSH_BACKEND = "taiga.events.backends.rabbitmq.EventsPushBackend" 38 | EVENTS_PUSH_BACKEND_OPTIONS = {"url": "amqp://taiga:password@rabbit:5672/taiga"} 39 | 40 | CELERY_ENABLED = True 41 | -------------------------------------------------------------------------------- /samples/conf/front/.initial_setup.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/samples/conf/front/.initial_setup.lock -------------------------------------------------------------------------------- /samples/conf/front/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": "http://taiga.lan/api/v1/", 3 | "eventsUrl": "ws://taiga.lan/events/", 4 | "eventsMaxMissedHeartbeats": 5, 5 | "eventsHeartbeatIntervalTime": 15000, 6 | "debug": true, 7 | "debugInfo": false, 8 | "defaultLanguage": "en", 9 | "publicRegisterEnabled": false, 10 | "gravatar": true, 11 | "feedbackEnabled": false, 12 | "privacyPolicyUrl": null, 13 | "termsOfServiceUrl": null, 14 | "maxUploadFileSize": null, 15 | "contribPlugins": [] 16 | } 17 | -------------------------------------------------------------------------------- /samples/conf/proxy/.initial_setup.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/samples/conf/proxy/.initial_setup.lock -------------------------------------------------------------------------------- /samples/conf/proxy/nginx.conf: -------------------------------------------------------------------------------- 1 | client_max_body_size 200m; 2 | 3 | server { 4 | server_name taiga.lan; 5 | listen 80; 6 | 7 | location ^~ /events { 8 | proxy_pass http://127.0.0.1:8888/; 9 | proxy_http_version 1.1; 10 | proxy_set_header Upgrade $http_upgrade; 11 | proxy_set_header Connection "upgrade"; 12 | proxy_connect_timeout 7d; 13 | proxy_send_timeout 7d; 14 | proxy_read_timeout 7d; 15 | } 16 | 17 | location ^~ /api { 18 | include proxy_params; 19 | proxy_pass http://127.0.0.1; 20 | } 21 | 22 | location ^~ /admin { 23 | include proxy_params; 24 | proxy_pass http://127.0.0.1; 25 | } 26 | 27 | location ^~ /static { 28 | include proxy_params; 29 | proxy_pass http://127.0.0.1; 30 | } 31 | 32 | location ^~ /media { 33 | include proxy_params; 34 | proxy_pass http://127.0.0.1; 35 | } 36 | 37 | location / { 38 | include proxy_params; 39 | proxy_pass http://127.0.0.1; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /samples/conf/proxy/proxy_params: -------------------------------------------------------------------------------- 1 | proxy_set_header Host $http_host; 2 | proxy_set_header X-Real-IP $remote_addr; 3 | proxy_set_header X-Scheme $scheme; 4 | proxy_set_header X-Forwarded-Proto $scheme; 5 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 6 | proxy_redirect off; 7 | -------------------------------------------------------------------------------- /samples/data/media/taiga-media: -------------------------------------------------------------------------------- 1 | /taiga-media -------------------------------------------------------------------------------- /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.dur.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-compose.durran.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | db: 5 | image: mysql:5.7 6 | volumes: 7 | - db_data:/var/lib/mysql 8 | restart: always 9 | environment: 10 | MYSQL_ROOT_PASSWORD: somewordpress 11 | MYSQL_DATABASE: wordpress 12 | MYSQL_USER: wordpress 13 | MYSQL_PASSWORD: wordpress 14 | 15 | wordpress: 16 | depends_on: 17 | - db 18 | image: wordpress:latest 19 | ports: 20 | - '8000:80' 21 | restart: always 22 | environment: 23 | WORDPRESS_DB_HOST: db:3306 24 | WORDPRESS_DB_USER: wordpress 25 | WORDPRESS_DB_PASSWORD: wordpress 26 | WORDPRESS_DB_NAME: wordpress 27 | volumes: 28 | db_data: {} 29 | -------------------------------------------------------------------------------- /samples/docker-compose.rem.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | frontend: 4 | build: frontend 5 | ports: 6 | - 3000:3000 7 | stdin_open: true 8 | volumes: 9 | - ./frontend:/usr/src/app 10 | - /usr/src/app/node_modules 11 | container_name: frontend 12 | restart: always 13 | networks: 14 | - react-express 15 | depends_on: 16 | - backend 17 | 18 | backend: 19 | container_name: backend 20 | restart: always 21 | build: backend 22 | volumes: 23 | - ./backend:/usr/src/app 24 | - /usr/src/app/node_modules 25 | depends_on: 26 | - mongo 27 | networks: 28 | - express-mongo 29 | - react-express 30 | 31 | mongo: 32 | container_name: mongo 33 | restart: always 34 | image: mongo:4.2.0 35 | volumes: 36 | - ./data:/data/db 37 | networks: 38 | - express-mongo 39 | networks: 40 | react-express: 41 | express-mongo: 42 | -------------------------------------------------------------------------------- /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-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | redis: 4 | image: "redis:alpine" -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /samples/kube.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nginx-deployment 5 | labels: 6 | app: nginx 7 | spec: 8 | replicas: 3 9 | selector: 10 | matchLabels: 11 | app: nginx 12 | template: 13 | metadata: 14 | labels: 15 | app: nginx 16 | spec: 17 | containers: 18 | - name: nginx 19 | image: nginx:1.14.2 20 | ports: 21 | - containerPort: 80 22 | -------------------------------------------------------------------------------- /samples/kube2.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nginx-deployment 5 | labels: 6 | app: nginx 7 | spec: 8 | replicas: 3 9 | selector: 10 | matchLabels: 11 | app: nginx 12 | template: 13 | metadata: 14 | labels: 15 | app: nginx 16 | spec: 17 | containers: 18 | - name: nginx 19 | image: nginx:1.14.2 20 | ports: 21 | - containerPort: 80 22 | - name: node 23 | image: node:16:12 24 | ports: 25 | - containerPort: 90 26 | - name: rss-reader 27 | image: nickchase/rss-php-nginx:v1 28 | ports: 29 | - containerPort: 88 30 | -------------------------------------------------------------------------------- /samples/kubernetes.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nginx-deployment 5 | labels: 6 | app: nginx 7 | spec: 8 | replicas: 3 9 | selector: 10 | matchLabels: 11 | app: nginx 12 | template: 13 | metadata: 14 | labels: 15 | app: nginx 16 | spec: 17 | containers: 18 | - name: nginx 19 | image: nginx:1.14.2 20 | ports: 21 | - containerPort: 80 22 | - name: node 23 | image: node:16:12 24 | ports: 25 | - containerPort: 90 26 | -------------------------------------------------------------------------------- /samples/nginx-golang-mysql/README.md: -------------------------------------------------------------------------------- 1 | ## Compose sample application 2 | ### Go server with an Nginx proxy and a MySQL database 3 | 4 | Project structure: 5 | ``` 6 | . 7 | ├── backend 8 | │   ├── Dockerfile 9 | │   ├── go.mod 10 | │   └── main.go 11 | ├── db 12 | │   └── password.txt 13 | ├── docker-compose.yaml 14 | ├── proxy 15 | │   ├── conf 16 | │   └── Dockerfile 17 | └── README.md 18 | ``` 19 | 20 | [_docker-compose.yaml_](docker-compose.yaml) 21 | ``` 22 | services: 23 | backend: 24 | build: backend 25 | ... 26 | db: 27 | image: mysql:8.0.19 28 | ... 29 | proxy: 30 | build: proxy 31 | ports: 32 | - 80:80 33 | ... 34 | ``` 35 | The compose file defines an application with three services `proxy`, `backend` and `db`. 36 | When deploying the application, docker-compose maps port 80 of the proxy service container to port 80 of the host as specified in the file. 37 | Make sure port 80 on the host is not already being in use. 38 | 39 | ## Deploy with docker-compose 40 | 41 | ``` 42 | $ docker-compose up -d 43 | Creating network "nginx-golang-mysql_default" with the default driver 44 | Building backend 45 | Step 1/8 : FROM golang:1.13-alpine AS build 46 | 1.13-alpine: Pulling from library/golang 47 | ... 48 | Successfully built 5f7c899f9b49 49 | Successfully tagged nginx-golang-mysql_proxy:latest 50 | WARNING: Image for service proxy was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`. 51 | Creating nginx-golang-mysql_db_1 ... done 52 | Creating nginx-golang-mysql_backend_1 ... done 53 | Creating nginx-golang-mysql_proxy_1 ... done 54 | ``` 55 | 56 | ## Expected result 57 | 58 | Listing containers must show two containers running and the port mapping as below: 59 | ``` 60 | $ docker ps 61 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 62 | 8906b14c5ad1 nginx-golang-mysql_proxy "nginx -g 'daemon of…" 2 minutes ago Up 2 minutes 0.0.0.0:80->80/tcp nginx-golang-mysq 63 | l_proxy_1 64 | 13e0e0a7715a nginx-golang-mysql_backend "/server" 2 minutes ago Up 2 minutes 8000/tcp nginx-golang-mysq 65 | l_backend_1 66 | ca8c5975d205 mysql:5.7 "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 3306/tcp, 33060/tcp nginx-golang-mysq 67 | l_db_1 68 | ``` 69 | 70 | After the application starts, navigate to `http://localhost:80` in your web browser or run: 71 | ``` 72 | $ curl localhost:80 73 | ["Blog post #0","Blog post #1","Blog post #2","Blog post #3","Blog post #4"] 74 | ``` 75 | 76 | Stop and remove the containers 77 | ``` 78 | $ docker-compose down 79 | ``` 80 | -------------------------------------------------------------------------------- /samples/nginx-golang-mysql/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13-alpine AS build 2 | WORKDIR /go/src/github.com/org/repo 3 | COPY . . 4 | 5 | RUN go build -o server . 6 | 7 | FROM alpine:3.7 8 | EXPOSE 8000 9 | COPY --from=build /go/src/github.com/org/repo/server /server 10 | CMD ["/server"] 11 | -------------------------------------------------------------------------------- /samples/nginx-golang-mysql/backend/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/org/repo 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/go-sql-driver/mysql v1.3.0 7 | github.com/gorilla/context v1.1.1 8 | github.com/gorilla/handlers v1.3.0 9 | github.com/gorilla/mux v1.6.2 10 | ) 11 | -------------------------------------------------------------------------------- /samples/nginx-golang-mysql/backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "os" 11 | "time" 12 | 13 | _ "github.com/go-sql-driver/mysql" 14 | "github.com/gorilla/handlers" 15 | "github.com/gorilla/mux" 16 | ) 17 | 18 | func connect() (*sql.DB, error) { 19 | bin, err := ioutil.ReadFile("/run/secrets/db-password") 20 | if err != nil { 21 | return nil, err 22 | } 23 | return sql.Open("mysql", fmt.Sprintf("root:%s@tcp(db:3306)/example", string(bin))) 24 | } 25 | 26 | func blogHandler(w http.ResponseWriter, r *http.Request) { 27 | db, err := connect() 28 | if err != nil { 29 | w.WriteHeader(500) 30 | return 31 | } 32 | defer db.Close() 33 | 34 | rows, err := db.Query("SELECT title FROM blog") 35 | if err != nil { 36 | w.WriteHeader(500) 37 | return 38 | } 39 | var titles []string 40 | for rows.Next() { 41 | var title string 42 | err = rows.Scan(&title) 43 | titles = append(titles, title) 44 | } 45 | json.NewEncoder(w).Encode(titles) 46 | } 47 | 48 | func main() { 49 | log.Print("Prepare db...") 50 | if err := prepare(); err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | log.Print("Listening 8000") 55 | r := mux.NewRouter() 56 | r.HandleFunc("/", blogHandler) 57 | log.Fatal(http.ListenAndServe(":8000", handlers.LoggingHandler(os.Stdout, r))) 58 | } 59 | 60 | func prepare() error { 61 | db, err := connect() 62 | if err != nil { 63 | return err 64 | } 65 | defer db.Close() 66 | 67 | for i := 0; i < 60; i++ { 68 | if err := db.Ping(); err == nil { 69 | break 70 | } 71 | time.Sleep(time.Second) 72 | } 73 | 74 | if _, err := db.Exec("DROP TABLE IF EXISTS blog"); err != nil { 75 | return err 76 | } 77 | 78 | if _, err := db.Exec("CREATE TABLE IF NOT EXISTS blog (id int NOT NULL AUTO_INCREMENT, title varchar(255), PRIMARY KEY (id))"); err != nil { 79 | return err 80 | } 81 | 82 | for i := 0; i < 5; i++ { 83 | if _, err := db.Exec("INSERT INTO blog (title) VALUES (?);", fmt.Sprintf("Blog post #%d", i)); err != nil { 84 | return err 85 | } 86 | } 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /samples/nginx-golang-mysql/db/password.txt: -------------------------------------------------------------------------------- 1 | db-q5n2g -------------------------------------------------------------------------------- /samples/nginx-golang-mysql/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | backend: 4 | build: backend 5 | secrets: 6 | - db-password 7 | depends_on: 8 | - db 9 | db: 10 | image: mysql:8.0.19 11 | command: '--default-authentication-plugin=mysql_native_password' 12 | restart: always 13 | secrets: 14 | - db-password 15 | volumes: 16 | - db-data:/var/lib/mysql 17 | environment: 18 | - MYSQL_DATABASE=example 19 | - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db-password 20 | proxy: 21 | build: proxy 22 | ports: 23 | - 80:80 24 | depends_on: 25 | - backend 26 | volumes: 27 | db-data: 28 | secrets: 29 | db-password: 30 | file: db/password.txt 31 | -------------------------------------------------------------------------------- /samples/nginx-golang-mysql/proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.13-alpine 2 | COPY conf /etc/nginx/conf.d/default.conf -------------------------------------------------------------------------------- /samples/nginx-golang-mysql/proxy/conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | location / { 5 | proxy_pass http://backend:8000; 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/.gitignore: -------------------------------------------------------------------------------- 1 | frontend/node_modules/ 2 | server/node_modules/ 3 | .idea/ 4 | data 5 | *.log 6 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/README.md: -------------------------------------------------------------------------------- 1 | ## Compose sample application 2 | ### React application with a NodeJS backend and a MongoDB database 3 | 4 | Project structure: 5 | ``` 6 | . 7 | ├── backend 8 | │ ├── Dockerfile 9 | │ ... 10 | ├── docker-compose.yaml 11 | ├── frontend 12 | │ ├── ... 13 | │ └── Dockerfile 14 | └── README.md 15 | ``` 16 | 17 | [_docker-compose.yaml_](docker-compose.yaml) 18 | ``` 19 | services: 20 | frontend: 21 | build: 22 | context: frontend 23 | ... 24 | ports: 25 | - 5000:5000 26 | ... 27 | server: 28 | container_name: server 29 | restart: always 30 | build: 31 | context: server 32 | args: 33 | NODE_PORT: 3000 34 | ports: 35 | - 3000:3000 36 | ... 37 | depends_on: 38 | - mongo 39 | mongo: 40 | container_name: mongo 41 | restart: always 42 | ... 43 | ``` 44 | The compose file defines an application with three services `frontend`, `backend` and `db`. 45 | When deploying the application, docker-compose maps port 5000 of the frontend service container to port 5000 of the host as specified in the file. 46 | Make sure port 5000 on the host is not already being in use. 47 | 48 | ## Deploy with docker-compose 49 | 50 | ``` 51 | $ docker-compose up -d 52 | Creating network "react-express-mongodb_default" with the default driver 53 | Building frontend 54 | Step 1/9 : FROM node:13.13.0-stretch-slim 55 | ---> aa6432763c11 56 | ... 57 | Successfully tagged react-express-mongodb_app:latest 58 | WARNING: Image for service app was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`. 59 | Creating frontend ... done 60 | Creating mongo ... done 61 | Creating app ... done 62 | ``` 63 | 64 | ## Expected result 65 | 66 | Listing containers must show containers running and the port mapping as below: 67 | ``` 68 | $ docker ps 69 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 70 | 06e606d69a0e react-express-mongodb_server "docker-entrypoint.s…" 23 minutes ago Up 23 minutes 0.0.0.0:3000->3000/tcp server 71 | ff56585e1db4 react-express-mongodb_frontend "docker-entrypoint.s…" 23 minutes ago Up 23 minutes 0.0.0.0:5000->5000/tcp frontend 72 | a1f321f06490 mongo:4.2.0 "docker-entrypoint.s…" 23 minutes ago Up 23 minutes 0.0.0.0:27017->27017/tcp mongo 73 | ``` 74 | 75 | After the application starts, navigate to `http://localhost:5000` in your web browser. 76 | 77 | ![page](./output.png) 78 | 79 | Stop and remove the containers 80 | ``` 81 | $ docker-compose down 82 | Stopping server ... done 83 | Stopping frontend ... done 84 | Stopping mongo ... done 85 | Removing server ... done 86 | Removing frontend ... done 87 | Removing mongo ... done 88 | ``` 89 | 90 | ##### Explanation of `docker-compose` 91 | 92 | __Version__ 93 | 94 | The first line defines the version of a file. It sounds confusing :confused:. What is meant by version of file ?? 95 | 96 | :pill: The Compose file is a YAML file defining services, networks, and volumes for a Docker application. So it is only a version of describing compose.yaml file. There are several versions of the Compose file format – 1, 2, 2.x, and 3.x. 97 | 98 | __Services__ 99 | 100 | Our main goal to create a containers, it starts from here. As you can see there are three services(Docker images): 101 | - First is __frontend__ 102 | - Second is __server__ which is __backend - Express(NodeJS)__. I used a name server here, it's totally on you to name it __backend__. 103 | - Third is __mongo__ which is db __MongoDB__. 104 | 105 | ##### Service app (backend - NodeJS) 106 | 107 | We make image of app from our `DockeFile`, explanation below. 108 | 109 | __Explanation of service server__ 110 | 111 | - Defining a **nodejs** service as __server__. 112 | - We named our **node server** container service as **server**. Assigning a name to the containers makes it easier to read when there are lot of containers on a machine, it can aslo avoid randomly generated container names. (Although in this case, __container_name__ is also __server__, this is merely personal preference, the name of the service and container do not have to be the same.) 113 | - Docker container starts automatically if its fails. 114 | - Building the __server__ image using the Dockerfile from the current directory and passing an argument to the 115 | backend(server) `DockerFile`. 116 | - Mapping the host port to the container port. 117 | 118 | ##### Service mongo 119 | 120 | We add another service called **mongo** but this time instead of building it from `DockerFile` we write all the instruction here directly. We simply pull down the standard __mongo image__ from the [DockerHub](https://hub.docker.com/) registry as we have done it for Node image. 121 | 122 | __Explanation of service mongo__ 123 | 124 | - Defining a **mongodb** service as __mongo__. 125 | - Pulling the mongo 4.2.0 image image again from [DockerHub](https://hub.docker.com/). 126 | - Mount our current db directory to container. 127 | - For persistent storage, we mount the host directory ( just like I did it in **Node** image inside `DockerFile` to reflect the changes) `/data` ( you need to create a directory in root of your project in order to save changes to locally as well) to the container directory `/data/db`, which was identified as a potential mount point in the `mongo Dockerfile` we saw earlier. 128 | - Mounting volumes gives us persistent storage so when starting a new container, Docker Compose will use the volume of any previous containers and copy it to the new container, ensuring that no data is lost. 129 | - Finally, we link/depends_on the app container to the mongo container so that the mongo service is reachable from the app service. 130 | - In last mapping the host port to the container port. 131 | 132 | :key: `If you wish to check your DB changes on your local machine as well. You should have installed MongoDB locally, otherwise you can't access your mongodb service of container from host machine.` 133 | 134 | :white_check_mark: You should check your __mongo__ version is same as used in image. You can see the version of __mongo__ image in `docker-compose `file, I used __image: mongo:4.2.0__. If your mongo db version on your machine is not same then furst you have to updated your local __mongo__ version in order to works correctly. 135 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/backend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-buster-slim 2 | 3 | # Create app directory 4 | WORKDIR /usr/src/app 5 | 6 | COPY package.json /usr/src/app/package.json 7 | COPY package-lock.json /usr/src/app/package-lock.json 8 | RUN npm ci 9 | 10 | COPY . /usr/src/app 11 | 12 | EXPOSE 3000 13 | 14 | CMD [ "npm", "run", "dev" ] 15 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/backend/README.md: -------------------------------------------------------------------------------- 1 | #### Snippet of backend(Node.js)`DockerFile` 2 | 3 | You will find this `DockerFile` file in the root directory of the project. 4 | 5 | ```bash 6 | FROM node:13.13.0-stretch-slim 7 | #Argument that is passed from docer-compose.yaml file 8 | ARG NODE_PORT 9 | #Echo the argument to check passed argument loaded here correctly 10 | RUN echo "Argument port is : $NODE_PORT" 11 | # Create app directory 12 | WORKDIR /usr/src/app 13 | #COPY . . 14 | COPY . . 15 | # Install app dependencies 16 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 17 | # where available (npm@5+) 18 | RUN npm install 19 | #In my case my app binds to port NODE_PORT so you'll use the EXPOSE instruction to have it mapped by the docker daemon: 20 | EXPOSE ${NODE_PORT} 21 | CMD npm run dev 22 | ``` 23 | 24 | ##### Explanation of backend(Node.js) `DockerFile` 25 | 26 | - The first line tells Docker to use another Node image from the [DockerHub](https://hub.docker.com/). We’re using the official Docker image for Node.js and it’s version 10 image. 27 | 28 | - On second line we declare argument `NODE_PORT` which we will pass it from `docker-compose`. 29 | 30 | - On third line we log to check argument is successfully read 31 | 32 | - On fourth line we sets a working directory from where the app code will live inside the Docker container. 33 | 34 | - On fifth line, we are copying/bundling our code working directory into container working directory on line three. 35 | 36 | - On line seven, we run npm install for dependencies in container on line four. 37 | 38 | - On Line eight, we setup the port, that Docker will expose when the container is running. In our case it is the port which we define inside `.env` file, read it from `docker-compose` then passed as a argument to the (backend)`DockerFile`. 39 | 40 | - And in last, we tell docker to execute our app inside the container by using node to run `npm run dev. It is the command which I registered in __package.json__ in script section. 41 | ###### :clipboard: `Note: For development purpose I used __nodemon__ , If you need to deploy at production you should change CMD from __npm run dev__ to __npm start__.` 42 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/backend/config/config.js: -------------------------------------------------------------------------------- 1 | const env = process.env.NODE_ENV || "development"; 2 | 3 | if (env === "development" || env === "test") { 4 | const config = require("./config.json"); 5 | const envConfig = config[env]; 6 | console.log(envConfig); 7 | 8 | Object.keys(envConfig).forEach((key) => { 9 | process.env[key] = envConfig[key]; 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/backend/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "test":{ 3 | "PORT": 3000, 4 | "MONGODB_URI": "mongodb://mongo:27017/TodoAppTest" 5 | }, 6 | "development":{ 7 | "PORT": 3000, 8 | "MONGODB_URI": "mongodb://mongo:27017/TodoApp" 9 | } 10 | } -------------------------------------------------------------------------------- /samples/react-express-mongodb/backend/config/messages.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | AUTHENTICATION_FAILED: { 3 | code: 400, 4 | message: "Authentication failed. Please login with valid credentials.", 5 | success: false, 6 | }, 7 | SUCCESSFUL_LOGIN: { 8 | code: 200, 9 | message: "Successfully logged in", 10 | success: true, 11 | }, 12 | INTERNAL_SERVER_ERROR: { 13 | code: 500, 14 | message: "Something unexpected happened", 15 | success: false, 16 | }, 17 | UNAUTHORIZED: { 18 | code: 401, 19 | message: "You session is expired. Please login again", 20 | success: false, 21 | }, 22 | SUCCESSFUL_DELETE: { 23 | code: 200, 24 | message: "Successfully deleted", 25 | success: true, 26 | }, 27 | SUCCESSFUL_UPDATE: { 28 | code: 200, 29 | message: "Updated successfully", 30 | success: true, 31 | }, 32 | SUCCESSFUL: { 33 | code: 200, 34 | success: true, 35 | message: "Successfully completed", 36 | }, 37 | NOT_FOUND: { 38 | code: 404, 39 | success: true, 40 | message: "Requested API not found", 41 | }, 42 | ALREADY_EXIST: { 43 | code: 200, 44 | success: true, 45 | message: "Already exists", 46 | }, 47 | FORBIDDEN: { 48 | code: 403, 49 | message: "You are not authorized to complete this action", 50 | success: false, 51 | }, 52 | BAD_REQUEST: { 53 | code: 400, 54 | message: "Bad request. Please try again with valid parameters", 55 | success: false, 56 | }, 57 | IN_COMPLETE_REQUEST: { 58 | code: 422, 59 | message: "Required parameter missing", 60 | success: false, 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/backend/db/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Syed Afzal 3 | */ 4 | const mongoose = require("mongoose"); 5 | 6 | exports.connect = (app) => { 7 | const options = { 8 | useNewUrlParser: true, 9 | autoIndex: false, // Don't build indexes 10 | reconnectTries: 30, // Retry up to 30 times 11 | reconnectInterval: 500, // Reconnect every 500ms 12 | poolSize: 10, // Maintain up to 10 socket connections 13 | // If not connected, return errors immediately rather than waiting for reconnect 14 | bufferMaxEntries: 0, 15 | }; 16 | 17 | const connectWithRetry = () => { 18 | mongoose.Promise = global.Promise; 19 | console.log("MongoDB connection with retry"); 20 | mongoose 21 | .connect(process.env.MONGODB_URI, options) 22 | .then(() => { 23 | console.log("MongoDB is connected"); 24 | app.emit("ready"); 25 | }) 26 | .catch((err) => { 27 | console.log("MongoDB connection unsuccessful, retry after 2 seconds."); 28 | setTimeout(connectWithRetry, 2000); 29 | }); 30 | }; 31 | connectWithRetry(); 32 | }; 33 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/backend/logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/samples/react-express-mongodb/backend/logs/.gitkeep -------------------------------------------------------------------------------- /samples/react-express-mongodb/backend/models/todos/todo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Syed Afzal 3 | */ 4 | const mongoose = require('mongoose'); 5 | 6 | const Todo = mongoose.model('Todo', { 7 | text : { 8 | type: String, 9 | trim: true, 10 | required: true 11 | } 12 | }); 13 | 14 | module.exports = {Todo}; 15 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docker_node_mongo_starter", 3 | "version": "1.0.0", 4 | "description": "docker starter with node js and mongodb services", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "format": "prettier --write {**/,}*.js", 9 | "lint": "prettier --check {**/,}*.js", 10 | "dev": "nodemon server.js" 11 | }, 12 | "author": "Syed Afzal", 13 | "license": "ISC", 14 | "dependencies": { 15 | "bcryptjs": "^2.4.3", 16 | "body-parser": "^1.18.2", 17 | "cookie-parser": "^1.4.4", 18 | "cors": "^2.8.4", 19 | "express": "^4.17.1", 20 | "lodash": "^4.17.13", 21 | "mongodb": "^3.0.7", 22 | "mongoose": "^5.0.15", 23 | "simple-node-logger": "^18.12.23", 24 | "validator": "^10.1.0" 25 | }, 26 | "devDependencies": { 27 | "nodemon": "^2.0.3", 28 | "prettier": "^2.0.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/backend/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const serverResponses = require("../utils/helpers/responses"); 3 | const messages = require("../config/messages"); 4 | const { Todo } = require("../models/todos/todo"); 5 | 6 | const routes = (app) => { 7 | const router = express.Router(); 8 | 9 | router.post("/todos", (req, res) => { 10 | const todo = new Todo({ 11 | text: req.body.text, 12 | }); 13 | 14 | todo 15 | .save() 16 | .then((result) => { 17 | serverResponses.sendSuccess(res, messages.SUCCESSFUL, result); 18 | }) 19 | .catch((e) => { 20 | serverResponses.sendError(res, messages.BAD_REQUEST, e); 21 | }); 22 | }); 23 | 24 | router.get("/", (req, res) => { 25 | Todo.find({}, { __v: 0 }) 26 | .then((todos) => { 27 | serverResponses.sendSuccess(res, messages.SUCCESSFUL, todos); 28 | }) 29 | .catch((e) => { 30 | serverResponses.sendError(res, messages.BAD_REQUEST, e); 31 | }); 32 | }); 33 | 34 | //it's a prefix before api it is useful when you have many modules and you want to 35 | //differentiate b/w each module you can use this technique 36 | app.use("/api", router); 37 | }; 38 | module.exports = routes; 39 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/backend/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Syed Afzal 3 | */ 4 | require("./config/config"); 5 | 6 | const express = require("express"); 7 | const path = require("path"); 8 | const cookieParser = require("cookie-parser"); 9 | const bodyParser = require("body-parser"); 10 | const cors = require("cors"); 11 | const db = require("./db"); 12 | 13 | const app = express(); 14 | 15 | //connection from db here 16 | db.connect(app); 17 | 18 | app.use(cors()); 19 | app.use(bodyParser.json()); 20 | app.use(bodyParser.urlencoded({ extended: false })); 21 | app.use(cookieParser()); 22 | app.use(express.static(path.join(__dirname, "public"))); 23 | 24 | // adding routes 25 | require("./routes")(app); 26 | 27 | app.on("ready", () => { 28 | app.listen(3000, () => { 29 | console.log("Server is up on port", 3000); 30 | }); 31 | }); 32 | 33 | module.exports = app; 34 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/backend/utils/helpers/logger.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const filename = path.join(__dirname, '../../logs/project.log'); 3 | 4 | //you can change format according to you 5 | const log = require('simple-node-logger').createSimpleLogger( { 6 | logFilePath:filename, 7 | timestampFormat:'YYYY-MM-DD HH:mm:ss'} 8 | ); 9 | module.exports = {log}; -------------------------------------------------------------------------------- /samples/react-express-mongodb/backend/utils/helpers/responses.js: -------------------------------------------------------------------------------- 1 | const serverResponse = { 2 | sendSuccess: (res, message, data = null) => { 3 | const responseMessage = { 4 | code: message.code ? message.code : 500, 5 | success: message.success, 6 | message: message.message, 7 | }; 8 | if (data) { responseMessage.data = data; } 9 | return res.status(message.code).json(responseMessage); 10 | }, 11 | sendError: (res, error) => { 12 | const responseMessage = { 13 | code: error.code ? error.code : 500, 14 | success: false, 15 | message: error.message, 16 | }; 17 | return res.status(error.code ? error.code : 500).json(responseMessage); 18 | }, 19 | }; 20 | 21 | module.exports = serverResponse; -------------------------------------------------------------------------------- /samples/react-express-mongodb/docker-compose.rem.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | frontend: 4 | build: frontend 5 | ports: 6 | - 3000:3000 7 | stdin_open: true 8 | volumes: 9 | - ./frontend:/usr/src/app 10 | - /usr/src/app/node_modules 11 | container_name: frontend 12 | restart: always 13 | networks: 14 | - react-express 15 | depends_on: 16 | - backend 17 | 18 | backend: 19 | container_name: backend 20 | restart: always 21 | build: backend 22 | volumes: 23 | - ./backend:/usr/src/app 24 | - /usr/src/app/node_modules 25 | depends_on: 26 | - mongo 27 | networks: 28 | - express-mongo 29 | - react-express 30 | 31 | mongo: 32 | container_name: mongo 33 | restart: always 34 | image: mongo:4.2.0 35 | volumes: 36 | - ./data:/data/db 37 | networks: 38 | - express-mongo 39 | networks: 40 | react-express: 41 | express-mongo: 42 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .idea 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Create image based on the official Node image from dockerhub 2 | FROM node:lts-buster-slim 3 | 4 | # Create app directory 5 | WORKDIR /usr/src/app 6 | 7 | # Copy dependency definitions 8 | COPY package.json /usr/src/app 9 | COPY package-lock.json /usr/src/app 10 | 11 | # Install dependecies 12 | #RUN npm set progress=false \ 13 | # && npm config set depth 0 \ 14 | # && npm i install 15 | RUN npm ci 16 | 17 | # Get all the code needed to run the app 18 | COPY . /usr/src/app 19 | 20 | # Expose the port the app runs in 21 | EXPOSE 3000 22 | 23 | # Serve the app 24 | CMD ["npm", "start"] 25 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/README.md: -------------------------------------------------------------------------------- 1 | #### Snippet of frontend(ReactJS)`DockerFile` 2 | 3 | You will find this `DockerFile` inside **frontend** directory. 4 | 5 | ```bash 6 | # Create image based on the official Node image from dockerhub 7 | FROM node:10 8 | #Argument that is passed from docer-compose.yaml file 9 | ARG FRONT_END_PORT 10 | # Create app directory 11 | WORKDIR /usr/src/app 12 | #Echo the argument to check passed argument loaded here correctly 13 | RUN echo "Argument port is : $FRONT_END_PORT" 14 | # Copy dependency definitions 15 | COPY package.json /usr/src/app 16 | # Install dependecies 17 | RUN npm install 18 | # Get all the code needed to run the app 19 | COPY . /usr/src/app 20 | # Expose the port the app runs in 21 | EXPOSE ${FRONT_END_PORT} 22 | # Serve the app 23 | CMD ["npm", "start"] 24 | ``` 25 | ##### Explanation of frontend(ReactJS) `DockerFile` 26 | 27 | Frontend `DockerFile` is almost the same as Backend `DockerFile`. 28 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.5.0", 8 | "@testing-library/user-event": "^7.2.1", 9 | "axios": "^0.19.0", 10 | "bootstrap": "^4.3.1", 11 | "node-sass": "^4.14.0", 12 | "react": "^16.13.1", 13 | "react-dom": "^16.13.1", 14 | "react-scripts": "3.4.1" 15 | }, 16 | "optionalDependencies": { 17 | "fsevents": "^2.1.2" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": "react-app" 27 | }, 28 | "proxy": "http://backend:3000", 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/samples/react-express-mongodb/frontend/public/favicon.ico -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/samples/react-express-mongodb/frontend/public/logo192.png -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/samples/react-express-mongodb/frontend/public/logo512.png -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import axios from "axios"; 3 | import "./App.scss"; 4 | import AddTodo from "./components/AddTodo"; 5 | import TodoList from "./components/TodoList"; 6 | 7 | export default class App extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | todos: [], 13 | }; 14 | } 15 | 16 | componentDidMount() { 17 | axios 18 | .get("/api") 19 | .then((response) => { 20 | this.setState({ 21 | todos: response.data.data, 22 | }); 23 | }) 24 | .catch((e) => console.log("Error : ", e)); 25 | } 26 | 27 | handleAddTodo = (value) => { 28 | axios 29 | .post("/api/todos", { text: value }) 30 | .then(() => { 31 | this.setState({ 32 | todos: [...this.state.todos, { text: value }], 33 | }); 34 | }) 35 | .catch((e) => console.log("Error : ", e)); 36 | }; 37 | 38 | render() { 39 | return ( 40 |
41 |
42 |
43 |
44 |

Todos

45 |
46 | 47 | 48 |
49 |
50 |
51 |
52 |
53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/src/App.scss: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | .todo-app { 5 | background-color: #efefef; 6 | padding: 1.2em; 7 | .new-todo{ 8 | display: flex; 9 | justify-content: space-between; 10 | align-items: center; 11 | input{ 12 | width: 80% !important; 13 | } 14 | } 15 | } 16 | 17 | //.list-group-item{ 18 | // &.active:hover{ 19 | // 20 | // } 21 | // &active:hover{ 22 | // background-color: #d3d3d3; 23 | // } 24 | //} -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/src/components/AddTodo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class AddTodo extends React.Component { 4 | handleSubmit = (e) => { 5 | e.preventDefault(); 6 | const { value } = e.target.elements.value; 7 | if (value.length > 0) { 8 | this.props.handleAddTodo(value); 9 | e.target.reset(); 10 | } 11 | }; 12 | 13 | render() { 14 | return ( 15 |
20 | 27 | 30 |
31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/src/components/TodoList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class TodoList extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | 7 | this.state = { 8 | activeIndex: 0, 9 | }; 10 | } 11 | 12 | handleActive(index) { 13 | this.setState({ 14 | activeIndex: index, 15 | }); 16 | } 17 | 18 | renderTodos(todos) { 19 | return ( 20 | 36 | ); 37 | } 38 | 39 | render() { 40 | let { todos } = this.props; 41 | return todos.length > 0 ? ( 42 | this.renderTodos(todos) 43 | ) : ( 44 |
45 | No Todos to display 46 |
47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/src/custom.scss: -------------------------------------------------------------------------------- 1 | // Override default variables before the import 2 | $body-bg: #fff; 3 | // Import Bootstrap and its default variables 4 | @import '~bootstrap/scss/bootstrap.scss'; 5 | 6 | .cursor-pointer { 7 | cursor: pointer; 8 | } -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | import './custom.scss'; 8 | 9 | ReactDOM.render(, document.getElementById('root')); 10 | 11 | // If you want your app to work offline and load faster, you can change 12 | // unregister() to register() below. Note this comes with some pitfalls. 13 | // Learn more about service workers: https://bit.ly/CRA-PWA 14 | serviceWorker.unregister(); 15 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/frontend/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /samples/react-express-mongodb/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/samples/react-express-mongodb/output.png -------------------------------------------------------------------------------- /samples/variables.env: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/samples/variables.env -------------------------------------------------------------------------------- /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/resolveEnvVariables.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | type EnvObject = { 4 | [variableName: string]: string; 5 | }; 6 | //env variables in docker: https://docs.docker.com/compose/environment-variables/ 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 | //split by line 21 | const envArray = envString.split('\n'); 22 | //remove empty last element 23 | envArray.splice(-1, 1); 24 | //create Object that stores the variable notation to be replace with its value 25 | const envObject: EnvObject = envArray.reduce((acc: EnvObject, el: string) => { 26 | const [variableName, value] = el.split('='); 27 | const variableKey: string = '\\${' + variableName + '}'; 28 | acc[variableKey] = value; 29 | return acc; 30 | }, {}); 31 | //replace the variables 32 | Object.keys(envObject).forEach((variableKey: string) => { 33 | yamlTextCopy = yamlTextCopy.replace( 34 | new RegExp(variableKey, 'g'), 35 | envObject[variableKey], 36 | ); 37 | }); 38 | return yamlTextCopy; 39 | }; 40 | 41 | export default resolveEnvVariables; 42 | -------------------------------------------------------------------------------- /src/common/runShellTasks.ts: -------------------------------------------------------------------------------- 1 | import child_process from 'child_process'; 2 | import { shellResults } from '../renderer/App.d'; 3 | 4 | const runDockerStats = ( 5 | handleOnData: Function, 6 | containerNames: Array, 7 | ): Function => { 8 | return runSpawn( 9 | (data: any) => handleOnData(data, containerNames), 10 | 'stats', 11 | containerNames, 12 | ); 13 | }; 14 | 15 | const runSpawn = ( 16 | handleOnData: Function, 17 | cmd: string, 18 | args: Array, 19 | ): Function => { 20 | const sp = child_process.spawn('docker', [cmd, ...args]); 21 | 22 | sp.stdout.on('data', (data) => { 23 | handleOnData(data); 24 | }); 25 | 26 | sp.stderr.on('data', (data) => { 27 | console.log(`spawn stderr: ${data}`); 28 | }); 29 | 30 | sp.on('error', (error) => { 31 | console.log(`child process error: ${error.message}`); 32 | }); 33 | return sp.kill.bind(sp); 34 | }; 35 | 36 | //docker compose commands 37 | const runDockerComposeKill = (filePath: string) => 38 | runShell(`docker-compose -f ${filePath} kill`, false); 39 | 40 | const runDockerComposeListContainer = (filePath: string) => 41 | runShell(`docker-compose -f ${filePath} ps`, false); 42 | 43 | const runDockerComposeDeployment = (filePath: string) => 44 | runShell(`docker-compose -f ${filePath} up -d`, false); 45 | 46 | const runDockerComposeValidation = (filePath: string) => 47 | runShell(`docker-compose -f ${filePath} config`, true); 48 | // 49 | //https://docs.docker.com/compose/reference/ 50 | //https://stackoverflow.com/questions/29225972/validating-docker-compose-yml-file 51 | 52 | //docker swarm commands 53 | const runDockerSwarmInit = (filePath: string) => 54 | runShell(`docker swarm init`, false); 55 | 56 | const runDockerSwarmDeployStack = (filePath: string, stackName: string) => 57 | runShell(`docker stack deploy -c ${filePath} ${stackName}`, false); 58 | 59 | const runLeaveSwarm = () => runShell(`docker swarm leave -f`, false); 60 | 61 | const runCheckStack = () => runShell(`docker stack ls`, false); 62 | 63 | const runDockerSwarmDeployment = async ( 64 | filePath: string, 65 | stackName: string, 66 | ) => { 67 | let stackDeployResult, initResult; 68 | await runDockerSwarmInit(filePath) 69 | .then((data) => (initResult = data)) 70 | .then(() => runDockerSwarmDeployStack(filePath, stackName)) 71 | .then((info) => { 72 | stackDeployResult = info; 73 | }); 74 | 75 | return JSON.stringify({ init: initResult, stackDeploy: stackDeployResult }); 76 | }; 77 | 78 | //kubernetes commands 79 | const runKubeCtlApply = (filePath: string) => 80 | runShell(`kubectl apply -f ${filePath}`, false); 81 | 82 | const runGetKubeDeployStatus = (deploymentName: string) => 83 | runShell(`kubectl get deployment ${deploymentName} --subresource=status`, false); 84 | 85 | const runKubeCtlGetNode = () => 86 | runShell("kubectl get nodes", false); 87 | 88 | const runKubeCtlDrainNode = (node: string) => 89 | runShell(`kubectl drain ${node}`, false); 90 | 91 | const runShell = (cmd: string, filter: boolean) => 92 | // promise for the electron application 93 | new Promise((resolve, reject) => { 94 | try { 95 | // run docker's validation command in a bash shell 96 | child_process.exec( 97 | cmd, 98 | // callback function to access output of docker-compose command 99 | (error, stdout, stderr) => { 100 | // add output to object 101 | const shellResult: shellResults = { 102 | out: stdout.toString(), 103 | envResolutionRequired: false, 104 | }; 105 | if (error) { 106 | //if docker-compose uses env file to run, store this variable to handle later 107 | if (filter) { 108 | if (error.message.includes('variable is not set')) { 109 | shellResult.envResolutionRequired = true; 110 | } 111 | // filter errors we don't care about 112 | if ( 113 | !error.message.includes("Couldn't find env file") && 114 | !error.message.includes( 115 | 'either does not exist, is not accessible', 116 | ) && 117 | !error.message.includes('variable is not set') 118 | ) { 119 | shellResult.error = error; 120 | } 121 | } else { 122 | shellResult.error = error; 123 | } 124 | } 125 | resolve(shellResult); 126 | }, 127 | ); 128 | } catch {} 129 | }); 130 | 131 | export default runShell; 132 | export { 133 | runDockerComposeDeployment, 134 | runDockerComposeValidation, 135 | runDockerComposeKill, 136 | runDockerComposeListContainer, 137 | runDockerSwarmDeployment, 138 | runDockerSwarmInit, 139 | runLeaveSwarm, 140 | runCheckStack, 141 | runDockerSwarmDeployStack, 142 | runSpawn, 143 | runDockerStats, 144 | runKubeCtlApply, 145 | runGetKubeDeployStatus, 146 | runKubeCtlGetNode, 147 | runKubeCtlDrainNode 148 | }; 149 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; 2 | import type { AppState, AppDispatch } from './store'; 3 | 4 | export const useAppDispatch: () => AppDispatch = useDispatch; 5 | export const useAppSelector: TypedUseSelectorHook = useSelector; -------------------------------------------------------------------------------- /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, REDUX_DEVTOOLS 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 | contextIsolation: false 38 | }, 39 | }); 40 | 41 | // set electron app size to full screen 42 | window.maximize(); 43 | 44 | // if in development, load content from dev server and install dev tools 45 | if (process.env.NODE_ENV === 'development') { 46 | // load from webpack dev server 47 | window.loadURL(`http://localhost:9080`); 48 | // install non-native dev tools 49 | window.webContents.on('did-frame-finish-load', () => { 50 | installExtension(REACT_DEVELOPER_TOOLS) 51 | .then((name: string) => { 52 | window.webContents.openDevTools(); 53 | console.log(`Added Extension: ${name}`); 54 | }) 55 | .catch((err: Error) => console.log(`An error occurred hello: ${err}`)); 56 | }); 57 | window.webContents.on('did-frame-finish-load', () => { 58 | installExtension(REDUX_DEVTOOLS) 59 | .then((name: string) => { 60 | window.webContents.openDevTools(); 61 | console.log(`Added Extension: ${name}`); 62 | }) 63 | .catch((err: Error) => console.log(`An error occurred hello: ${err}`)); 64 | }); 65 | } else { 66 | // in production, load content from electron application files 67 | const startUrl = process.env.NOT_PACKAGE 68 | ? `file://${app.getAppPath()}/../renderer/index.html` 69 | : url.format({ 70 | pathname: path.join(__dirname, '/dist/renderer/index.html'), 71 | protocol: 'file:', 72 | slashes: true, 73 | }); 74 | window.loadURL(startUrl); 75 | } 76 | return window; 77 | }; 78 | 79 | // when the electron engine is ready, create window and menubar 80 | app.whenReady().then(createWindow).then(createMenu); 81 | 82 | // for every OS but mac, quit the application when windows are closed 83 | app.on('window-all-closed', () => { 84 | if (process.platform !== 'darwin') { 85 | app.quit(); 86 | } 87 | }); 88 | 89 | // open a new window if no windows are opened 90 | app.on('activate', () => { 91 | if (BrowserWindow.getAllWindows().length === 0) { 92 | createMenu(createWindow()); 93 | } 94 | }); 95 | -------------------------------------------------------------------------------- /src/main/menu.ts: -------------------------------------------------------------------------------- 1 | import { dialog, Menu, BrowserWindow, shell } from 'electron'; 2 | import fs from 'fs'; 3 | import { runDockerComposeValidation } from '../common/runShellTasks'; 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 runDockerComposeValidation(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/reducers/appSlice.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @module appSlice.ts 4 | * 5 | * @author Jordan Long, Michael Villamor, Nathan Lovell, Giovanni Rodriguez 6 | * @date 6/22/2022 7 | * @description Primary reducer which also initializes state 8 | * 9 | */ 10 | 11 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 12 | import setD3State from "../renderer/helpers/setD3State"; 13 | import { 14 | State, 15 | SwitchTab, 16 | YamlState, 17 | ViewAndSelectNetwork 18 | } from '../renderer/App.d'; 19 | 20 | const initialState: State = { 21 | openFiles: [], 22 | openErrors: [], 23 | selectedContainer: '', 24 | fileOpened: false, 25 | filePath: '', 26 | services: {}, 27 | dependsOn: { 28 | name: 'placeholder', 29 | }, 30 | networks: {}, 31 | selectedNetwork: 'networks', 32 | volumes: {}, 33 | volumesClicked: {}, 34 | bindMounts: [], 35 | bindMountsClicked: {}, 36 | view: 'depends_on', 37 | options: { 38 | ports: false, 39 | volumes: false, 40 | selectAll: false, 41 | }, 42 | version: '', 43 | kubeObj: {}, 44 | kubeBool: false 45 | }; 46 | 47 | const appSlice = createSlice({ 48 | name: 'app', 49 | initialState, 50 | reducers: { 51 | yamlToState (state: State, action: PayloadAction) { 52 | state = { 53 | ...state, 54 | ...action.payload 55 | }; 56 | state.services = action.payload; 57 | return state; 58 | }, 59 | switchTab (state: State, action: PayloadAction) { 60 | 61 | 62 | const tabState = JSON.parse(localStorage.getItem(action.payload.filePath) || '{}'); 63 | if (action.payload.openFiles && !state.openFiles.includes(action.payload.filePath)){ 64 | state = { 65 | ...state, 66 | ...tabState, 67 | openFiles: state.openFiles.concat(action.payload.openFiles), 68 | filePath: action.payload.filePath 69 | }; 70 | } 71 | else { 72 | state = { 73 | ...state, 74 | ...tabState 75 | }; 76 | if (tabState.kubeBool){ 77 | state.services = tabState.kubeObj; 78 | state = { 79 | ...state, 80 | ...tabState, 81 | kubeBool: true, 82 | selectedContainer: '' 83 | } 84 | state.filePath = action.payload.filePath; 85 | } 86 | } 87 | state.selectedNetwork = 'networks'; 88 | state.view = 'depends_on'; 89 | if(state.kubeBool){ 90 | state.services = tabState.kubeObj; 91 | } 92 | // Set the 'state' item in localStorage to the tab state. This means that tab is the current tab, which would be used if the app got reloaded. 93 | localStorage.setItem('state', JSON.stringify(tabState)); 94 | // Set the d3 state using the services extracted from the tabState and then setState 95 | if(state.kubeBool){ 96 | window.d3State = setD3State(state.kubeObj || {}) 97 | }else{ 98 | window.d3State = setD3State(state.services); 99 | } 100 | return state; 101 | }, 102 | closeTab (state: State, action: PayloadAction) { 103 | // Grab current open files and remove the file path of the tab to be closed, assign the 104 | // updated array to newOpenFiles 105 | const { openFiles } = state; 106 | const newOpenFiles = openFiles.filter((file: string) => file != action.payload.filePath); 107 | // Remove the state object associated with the file path in localStorage 108 | 109 | localStorage.removeItem(action.payload.filePath); 110 | // If the tab to be closed is the active tab, reset d3 and delete "state" object from local 111 | // storage and set state to the initial state with the updated open files array included. 112 | 113 | if (action.payload.filePath === state.filePath) { 114 | // Remove the 'state' localStorage item, which represents the 115 | // services of the currently opened file. 116 | localStorage.removeItem('state'); 117 | // Stop the simulation to prevent d3 transform errors related 118 | // to 'tick' events 119 | const { simulation } = window.d3State; 120 | simulation.stop(); 121 | // If there are other open tabs, switch to the first open one 122 | // If not, reset to initialState with selected options. 123 | if (openFiles.length > 1){ 124 | let newFilePath = newOpenFiles[0]; 125 | const tabState = JSON.parse(localStorage.getItem(newFilePath) || '{}'); 126 | if (tabState.kubeBool) tabState.filePath = newFilePath; 127 | localStorage.setItem('state', JSON.stringify(tabState)); 128 | // appSlice.caseReducers.switchTab(state, {payload: {filePath: newFilePath, openFiles: newOpenFiles, closeTab: true}, type: 'switchTab'}); 129 | state = {...state, ...tabState, openFiles: newOpenFiles, selectedContainer: '', view: 'depends_on'}; 130 | if (tabState.kubeBool){ 131 | state.services = tabState.kubeObj; 132 | } 133 | window.d3State = setD3State(state.services); 134 | return state; 135 | } 136 | // else this.setState({ ...initialState, options }); 137 | else return {...initialState }; 138 | } 139 | return { ...state, openFiles: newOpenFiles }; 140 | }, 141 | updateViewStore(state: State, action: PayloadAction){ 142 | state.view = action.payload.view; 143 | state.selectedNetwork = ''; 144 | return state; 145 | }, 146 | selectNetwork(state: State, action: PayloadAction){ 147 | state.selectedNetwork = action.payload; 148 | state.view = 'networks'; 149 | return state; 150 | }, 151 | setSelectedContainers(state: State, action: PayloadAction){ 152 | state.selectedContainer = action.payload; 153 | return state; 154 | }, 155 | fileOpenError (state: State, action: PayloadAction) { 156 | if(action.payload[0] === 'reset'){ 157 | state = {...state, openErrors: []}; 158 | return state; 159 | } 160 | // state.openErrors.concat(action.payload) 161 | let newOpenErrors = []; 162 | newOpenErrors.push(action.payload[0]); 163 | // state.fileOpened = false; 164 | 165 | state = {...state, openErrors: newOpenErrors, fileOpened: false }; 166 | return state; 167 | }, 168 | openYamlFiles (state: State, action: PayloadAction ) { 169 | state.openFiles.concat(action.payload); 170 | return state; 171 | }, 172 | updateOption (state: State, action: PayloadAction) { 173 | // let option = action.payload.option; 174 | // if (action.payload === 'ports') Object.assign(newState, newState[action.payload] = true) 175 | // check if toggling select all on or off 176 | if (action.payload === 'ports') state.options.ports = !state.options.ports; 177 | if (action.payload === 'volumes') state.options.volumes = !state.options.volumes; 178 | if (action.payload === 'selectAll') { 179 | state.options.ports = !state.options.ports; 180 | state.options.volumes = !state.options.volumes; 181 | }else if (state.options.ports && state.options.volumes) { 182 | state.options.selectAll = true; 183 | } else { 184 | state.options.selectAll = false; 185 | } 186 | 187 | 188 | return state; 189 | } 190 | 191 | } 192 | }) 193 | 194 | export const {yamlToState, switchTab, closeTab, updateViewStore, selectNetwork, openYamlFiles, setSelectedContainers, fileOpenError, updateOption} = appSlice.actions; 195 | export default appSlice.reducer; 196 | 197 | -------------------------------------------------------------------------------- /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 | filePath: string; 14 | networks: ReadOnlyObj; 15 | options: Options; 16 | selectedContainer: string; 17 | selectedNetwork: string; 18 | services: Services; 19 | openErrors: Array; 20 | openFiles: Array; 21 | version: string; 22 | view: ViewT; 23 | volumes: ReadOnlyObj; 24 | volumesClicked: Clicked; 25 | kubeObj?: KubeObj; 26 | kubeBool: Boolean; 27 | }; 28 | 29 | type ReadOnlyObj = { 30 | readonly [prop: string]: ReadOnlyObj | Array | string; 31 | }; 32 | 33 | type Clicked = { 34 | readonly [propName: string]: string; 35 | }; 36 | 37 | type DependsOn = { 38 | readonly name: string; 39 | readonly children?: Array; 40 | }; 41 | 42 | export type Kind = "Deployment" | "Pod" | "Node" | "Service"; 43 | 44 | export type Container = { 45 | name: string; 46 | image: string; 47 | port: number; 48 | volumes: ReadOnlyObj; 49 | }; 50 | 51 | export interface KubeObj { 52 | kind?: Kind; 53 | name?: string; 54 | containers?: Container[]; 55 | replica?: number; 56 | selector?: string; 57 | ports?: [] 58 | } 59 | 60 | export type Services = { 61 | [service: string]: any; 62 | }; 63 | 64 | export type Service = { 65 | build?: string; 66 | image?: string; 67 | command?: string; 68 | environment?: ReadOnlyObj | string[]; 69 | env_file?: string[]; 70 | ports?: Ports; 71 | volumes?: Volumes; 72 | depends_on?: string[]; 73 | networks?: string[] | {}; 74 | }; 75 | 76 | export type Ports = string[] | string | Port[]; 77 | 78 | export type Port = { 79 | mode: string; 80 | protocol: string; 81 | published: number; 82 | target: number; 83 | }; 84 | 85 | export type Volumes = VolumeType[]; 86 | 87 | /** Volumes may have different syntax, depending on the version 88 | * 89 | * https://docs.docker.com/compose/compose-file/#long-syntax-3 90 | */ 91 | type LongVolumeSyntax = Partial<{ 92 | type: 'volume' | 'bind' | 'tmpfs' | 'npipe'; 93 | source: string; 94 | target: string; 95 | read_only: boolean; 96 | bind: { 97 | propogation: string; 98 | }; 99 | volume: { 100 | nocopy: boolean; 101 | }; 102 | tmpft: { 103 | size: number; 104 | }; 105 | consistency: 'consistent' | 'cached' | 'delegated'; 106 | }>; 107 | 108 | type VolumeType = string | LongVolumeSyntax; 109 | 110 | type ViewT = 'networks' | 'depends_on' | undefined; //hack fix, shouldn't take undefined 111 | 112 | export type Options = { 113 | ports: boolean; 114 | volumes: boolean; 115 | selectAll: boolean; 116 | }; 117 | 118 | /** 119 | * ********************** 120 | * APP METHOD FUNCTION TYPES 121 | * ********************** 122 | */ 123 | 124 | export type SwitchTab = { 125 | filePath: string, openFiles?: any, closeTab?: boolean; 126 | }; 127 | 128 | export type FileOpen = { 129 | (file: File): any; 130 | }; 131 | 132 | export type UpdateOption = { 133 | (id: 'ports' | 'volumes' | 'selectAll'): void; 134 | }; 135 | 136 | export type Handler = { 137 | ( 138 | e: 139 | | React.ChangeEvent 140 | | React.MouseEvent, 141 | ): void; 142 | }; 143 | 144 | export type UpdateView = { 145 | (view: 'networks' | 'depends_on'): void; 146 | }; 147 | 148 | export type SelectNetwork = { 149 | (network: string): void; 150 | }; 151 | 152 | export type SetSelectedContainer = { 153 | (containerName: string): void; 154 | }; 155 | 156 | 157 | export type Void = { 158 | ():void 159 | } 160 | /** 161 | * ********************** 162 | * D3 SIMULATION TYPES 163 | * ********************** 164 | */ 165 | type D3State = { 166 | simulation: Simulation; 167 | treeDepth: number; 168 | serviceGraph: SGraph; 169 | }; 170 | 171 | interface SNode extends SimulationNodeDatum { 172 | id: number; 173 | name: string; 174 | ports: string[]; 175 | volumes?: string[]; 176 | networks?: string[]; 177 | row: number; 178 | column: number; 179 | rowLength: number; 180 | children: NodesObject; 181 | } 182 | 183 | interface Link extends SimulationLinkDatum { 184 | source: string; 185 | target: string; 186 | } 187 | 188 | type SGraph = { 189 | nodes: SNode[]; 190 | links: Link[]; 191 | }; 192 | 193 | export type NodesObject = { 194 | [service: string]: SNode; 195 | }; 196 | 197 | export type TreeMap = { 198 | [row: string]: string[]; 199 | }; 200 | 201 | export type Simulation = d3.Simulation; 202 | 203 | export type shellResults = { 204 | error?: Error; 205 | out: string; 206 | envResolutionRequired: boolean; 207 | }; 208 | 209 | export type YamlState = { 210 | fileOpened: boolean; 211 | kubeBool?: boolean; 212 | kubeObj?: KubeObj; 213 | services: Services; 214 | filePath?: string; 215 | dependsOn?: DependsOn; 216 | networks?: ReadOnlyObj; 217 | volumes?: ReadOnlyObj; 218 | bindMounts?: Array; 219 | }; 220 | 221 | export interface ViewAndSelectNetwork { 222 | view?: ViewT; 223 | selectedNetwork?: string; 224 | } -------------------------------------------------------------------------------- /src/renderer/App.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module App.tsx 5 | * @author Joshua Nordstrom - edited by Nathan Lovell, Michael Villamor, Giovanni Rodriguez, Jordan Long 6 | * @date 3/7/20 - edited 6/30/22 7 | * @description start of the application 8 | * 9 | * ************************************ 10 | */ 11 | //IMPORT LIBRARIES 12 | import React, { useEffect } from 'react'; 13 | import { ipcRenderer } from 'electron'; 14 | 15 | 16 | //IMPORT HELPER FUNCTIONS 17 | import {convertAndStoreYamlJSON, handleFileOpenError} from './helpers/fileOpen'; 18 | import setD3State from './helpers/setD3State'; 19 | 20 | 21 | // IMPORT REACT CONTAINERS OR COMPONENTS 22 | import LeftNav from './components/LeftNav'; 23 | import D3Wrapper from './components/D3Wrapper'; 24 | import TabBar from './components/TabBar'; 25 | import OptionBar from './components/OptionBar'; 26 | 27 | 28 | //IMPORT ACTIONS/REDUCERS 29 | import { useDispatch } from 'react-redux'; 30 | import { useAppSelector } from '../hooks' 31 | import { openYamlFiles, fileOpenError } from '../reducers/appSlice'; 32 | 33 | 34 | 35 | 36 | const App: React.FC = ({/**state to be loaded for App */}) => { 37 | 38 | const dispatch = useDispatch(); 39 | 40 | 41 | useEffect(() => { 42 | if (ipcRenderer) { 43 | ipcRenderer.on('file-open-error-within-electron', (event, arg) => { 44 | dispatch(fileOpenError(handleFileOpenError(arg))); 45 | }); 46 | ipcRenderer.on('file-opened-within-electron', (event, arg) => { 47 | convertAndStoreYamlJSON(arg, '', useAppSelector(state => state.openFiles)); 48 | }); 49 | } 50 | const stateJSON = localStorage.getItem('state'); 51 | if (stateJSON) { 52 | const stateJS = JSON.parse(stateJSON); 53 | // set d3 state 54 | window.d3State = setD3State(stateJS.services); 55 | //Create openFile state array from items in localStorage 56 | const openFiles = []; 57 | const keys = Object.keys(localStorage); 58 | for (let key of keys) { 59 | if (key !== 'state') { 60 | const item = localStorage.getItem(key); 61 | try { 62 | const parsed = JSON.parse(item || '{}'); 63 | openFiles.push(parsed.filePath); 64 | } catch { 65 | console.log( 66 | 'Item from localStorage not included in openFiles: ', 67 | item, 68 | ); 69 | } 70 | } 71 | } 72 | dispatch(openYamlFiles(openFiles)); 73 | } 74 | return () => { 75 | if (ipcRenderer) { 76 | ipcRenderer.removeAllListeners('file-opened-within-electron'); 77 | ipcRenderer.removeAllListeners('file-open-error-within-electron'); 78 | } 79 | } 80 | }, []) 81 | 82 | return ( 83 |
84 | {/* dummy div to create draggable bar at the top of application to replace removed native bar */} 85 |
86 | 87 |
88 | 89 | 90 | 91 |
92 |
93 | ); 94 | 95 | } 96 | 97 | export default App 98 | -------------------------------------------------------------------------------- /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/ClusterDeployment.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module ComposeDeployment.tsx 5 | * @author David Soerensen, Michael Villamor 6 | * @date 3/11/20 edited 7/7/22 7 | * @description container for hte swarm deployment. 8 | * 9 | * ************************************ 10 | */ 11 | 12 | import React from 'react'; 13 | 14 | import SwarmDeployment from './SwarmDeployment'; 15 | 16 | const ClusterDeployment: React.FC = () => { 17 | return ( 18 |
19 | 20 | 21 | {/*planned feature */} 22 | 23 | {/* */} 24 |
25 | ); 26 | }; 27 | 28 | export default ClusterDeployment; 29 | -------------------------------------------------------------------------------- /src/renderer/components/D3Wrapper.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module D3Wrapper.tsx 5 | * @author Michael Villamor, Nathan Lovell, Jordan Long, Giovanni Rodriguez 6 | * @date 3/11/20 edited 7/7/22 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 | import { useAppSelector } from '../../hooks'; 21 | 22 | 23 | const D3Wrapper: React.FC= () => { 24 | const fileOpened = useAppSelector((state) => state.fileOpened); 25 | const services = useAppSelector((state) => state.services); 26 | const options = useAppSelector((state) => state.options); 27 | const volumes = useAppSelector((state) => state.volumes); 28 | const bindMounts = useAppSelector((state) => state.bindMounts); 29 | const networks = useAppSelector((state) => state.networks); 30 | const selectedNetwork = useAppSelector((state) => state.selectedNetwork); 31 | const openErrors = useAppSelector((state) => state.openErrors); 32 | const view = useAppSelector((state) => state.view); 33 | const kubeBool = useAppSelector((state) => state.kubeBool); 34 | 35 | // invoke function that returns a function with the closure object for tracking colors 36 | const getColor = colorSchemeIndex(); 37 | 38 | 39 | return ( 40 | 41 |
42 | {/** 43 | * if a file hasn't been opened 44 | * ** if errors, display them 45 | * ** always display open button 46 | * else display visualizer 47 | * (yes, this is nested terinary operator) 48 | */} 49 | {!fileOpened ? ( //if no file has been opened... 50 |
51 | {openErrors.length > 0 ? ( //check if there are any errors 52 | 53 | ) : ( //if there aren't any errors, display the option to open a file 54 | <> 55 | )} 56 | 57 |
58 | ) : ( //if the file has been opened with no errors, display this: 59 | <> 60 |
61 | 69 |
70 | {kubeBool ? null : 71 | } 76 | 77 | 78 | )} 79 |
80 | ); 81 | }; 82 | 83 | export default D3Wrapper; 84 | -------------------------------------------------------------------------------- /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, Michael Villamor, Nathan Lovell, Jordan Long, Giovanni Rodriguez 6 | * @date 3/11/20 edited on 6/30/22 7 | * @description Button to allow user to open docker-compose and kubernetes files 8 | * 9 | * ************************************ 10 | */ 11 | import React from 'react'; 12 | import { FaUpload } from 'react-icons/fa'; 13 | import { yamlToState, fileOpenError, switchTab } from "../../reducers/appSlice"; 14 | import { fileOpen, getCache, cacheErrors } from '../helpers/fileOpen' 15 | import { useAppDispatch } from '../../hooks'; 16 | 17 | /** 18 | * @param file: a File classed object 19 | * @returns void 20 | * @description validates the docker-compose file 21 | * ** if no errors, passes file string along to convert and store yaml method 22 | * ** if errors, passes error string to handle file open errors method 23 | */ 24 | 25 | 26 | 27 | const FileSelector: React.FC = () => { 28 | const dispatch = useAppDispatch(); 29 | 30 | return ( 31 |
    32 | 38 | ) => { 45 | // make sure there was something selected 46 | 47 | //make sure to clear fileOpenErrors when opening a new file 48 | dispatch(fileOpenError(['reset'])); 49 | cacheErrors('reset'); 50 | 51 | if (event.currentTarget) { 52 | // make sure user opened a file 53 | if (event.currentTarget.files) { 54 | // fire fileOpen function on first file opened 55 | /** fileOpen cannot have hooks called inside because it's not a functional component 56 | * To circumvent, we're returning the necessary, adjusted files into 'openedFile' 57 | * If it's an array, than it's outputting a string of error messages and calling error reducer 58 | * If it's an object, dispatch yamlState and switchTab reducers with object properties 59 | */ 60 | 61 | fileOpen(event.currentTarget.files[0]); //goes to helper function to process 62 | 63 | 64 | /* 65 | 66 | setTimeout solution because the result cannot return into this file. 67 | Result from file open and subsequent parsing is added to a cache function 68 | the setTimeout waits for the file to be read and grabs data from the cache 69 | 70 | 71 | */ 72 | setTimeout(() => { 73 | 74 | //The cache functions use closure and can be accessed by calling the function with string '123' 75 | let result = getCache('123'); 76 | let errors = cacheErrors('123'); 77 | 78 | let openedFile = result[0]; 79 | let dispatchError; 80 | 81 | if(errors.length){ 82 | dispatchError = errors[0]; 83 | } 84 | 85 | if (openedFile !== undefined && !errors.length){ 86 | Array.isArray(openedFile) ? dispatch(fileOpenError(openedFile)) : dispatch(yamlToState(openedFile.yamlState)), dispatch(switchTab({filePath: openedFile.filePath, openFiles: openedFile.openFiles, closeTab: false})); 87 | } 88 | else if (dispatchError) { 89 | dispatch(fileOpenError(dispatchError)); 90 | } 91 | }, 500) 92 | } 93 | } 94 | }} 95 | /> 96 |
    97 | ); 98 | }; 99 | export default FileSelector; 100 | -------------------------------------------------------------------------------- /src/renderer/components/KubeDeployment.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module KubeDeployment.tsx 5 | * @author Nathan Lovell and Giovanni Rodriguez 6 | * @date 6/15/20 7 | * @description component to deploy a docker-compose file to local Kubernetes cluster 8 | * 9 | * ************************************ 10 | */ 11 | 12 | /** 13 | *Placeholder for deploying to a local kubernetes cluster - will be worked on over the next few months - Nathan L 7/7/22 14 | */ -------------------------------------------------------------------------------- /src/renderer/components/LeftNav.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module LeftNav.tsx 5 | * @author Michael Villamor 6 | * @date 3/11/20 edited 6/30/22 7 | * @description container for the title, the service info and the file open; hides elements if it is a kubernetes file 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 ComposeDeployment from './ComposeDeployment'; 17 | import ClusterDeployment from './ClusterDeployment'; 18 | import Title from './Title'; 19 | import NetworksDropDown from './NetworksDropdown'; 20 | 21 | import { useAppDispatch, useAppSelector } from '../../hooks'; 22 | import { updateOption, updateViewStore } from '../../reducers/appSlice'; 23 | import { Handler } from '../App.d'; 24 | 25 | 26 | 27 | const LeftNav: React.FC = ({ 28 | }) => { 29 | 30 | const dispatch = useAppDispatch(); 31 | const fileOpened = useAppSelector(state => state.fileOpened); 32 | const options = useAppSelector((state) => state.options); 33 | const view = useAppSelector((state) => state.view); 34 | const selectedContainer = useAppSelector((state) => state.selectedContainer) 35 | const kubeBool = useAppSelector((state) => state.kubeBool); 36 | const kubeObj = useAppSelector((state) => state.kubeObj); 37 | 38 | let kubeDepoloyInfo: any = []; 39 | if (kubeBool){ 40 | if(kubeObj?.containers){ 41 | kubeObj.containers.forEach((el: any, i: any) => { 42 | let key = `randomKey${i}` 43 | let key2 = `randomKey2${i}` 44 | if(el.name === selectedContainer){ 45 | kubeDepoloyInfo.push(
    Image: {el.image}
    ) 46 | kubeDepoloyInfo.push(
    Port: {el.ports[0].containerPort}
    ) 47 | } 48 | }) 49 | } 50 | }; 51 | const dependsOnClass = view === 'depends_on' ? 'option selected' : 'option'; 52 | 53 | const handleViewUpdate: Handler = (e) => { 54 | const view = e.currentTarget.id as 'networks' | 'depends_on'; 55 | 56 | dispatch(updateViewStore({view:view})) 57 | }; 58 | 59 | const handleOptionUpdate: Handler = (e) => { 60 | const option = e.currentTarget.id as 'ports' | 'volumes' | 'selectAll'; 61 | dispatch(updateOption(option)); 62 | }; 63 | 64 | // creates an array of jsx elements for each option 65 | const optionsDisplay = Object.keys(options).map((opt, i) => { 66 | let title = ''; 67 | // format select all title 68 | if (opt === 'selectAll') title = ' Select All'; 69 | else if (opt === 'ports') title = 'Ports '; 70 | else if (opt === 'volumes') title = ' Volumes '; 71 | // otherwise set title to option name 72 | // else title = opt; 73 | 74 | return ( 75 | 86 | {title} 87 | 88 | ); 89 | }); 90 | return ( 91 |
    92 |
    93 | 94 | {fileOpened ? <FileSelector /> : null} 95 | </div> 96 | {!kubeBool ? <ServiceInfo /> : null} 97 | 98 | {kubeBool && kubeObj?.kind === 'Deployment' ? 99 | <div className='kubeData'> 100 | <h2 className='kInfo'>Kubernetes Info:</h2> 101 | <ol className='kInfo2'> 102 | <ul>Name: {kubeObj.name}</ul> 103 | <ul>Kind: {kubeObj.kind}</ul> 104 | <ul>Replicas: {kubeObj.replica}</ul> 105 | <h2 className='kInfo'>Container Info:</h2> 106 | <ul>{kubeDepoloyInfo}</ul> 107 | </ol> 108 | </div> 109 | : null} 110 | {!kubeBool && fileOpened ? <NetworksDropDown/> : null} 111 | {!kubeBool && fileOpened ? <div> 112 | <span 113 | className={dependsOnClass} 114 | id="depends_on" 115 | onClick={handleViewUpdate} 116 | > 117 | Depends On 118 | </span> 119 | <div className="options-flex2">{optionsDisplay}</div> 120 | </div> : null} 121 | {!kubeBool && fileOpened ? <ComposeDeployment/> : null} 122 | {!kubeBool && fileOpened ? <ClusterDeployment/> : null} 123 | 124 | 125 | </div> 126 | ); 127 | }; 128 | 129 | export default LeftNav; 130 | -------------------------------------------------------------------------------- /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 | /** 2 | * ************************************ 3 | * 4 | * @module Links.tsx 5 | * @author Giovanni Rodriguez 6 | * @date 7/7/22 7 | * @description Rendering of the nodes in d3 simulation 8 | * 9 | * ************************************ 10 | */ 11 | 12 | 13 | 14 | import React from 'react'; 15 | import { Handler } from '../App.d'; 16 | import { selectNetwork } from '../../reducers/appSlice'; 17 | import { useAppDispatch, useAppSelector } from '../../hooks'; 18 | 19 | 20 | const NetworksDropDown: React.FC = ({ 21 | 22 | }) => { 23 | const dispatch = useAppDispatch(); 24 | const networks = useAppSelector(state => state.networks) 25 | const selectedNetwork = useAppSelector(state => state.selectedNetwork) 26 | const handleNetworkUpdate: Handler = e => { 27 | const network = (e as React.ChangeEvent<HTMLSelectElement>).currentTarget 28 | .value; 29 | dispatch(selectNetwork(network)); 30 | }; 31 | 32 | const groupNetworks = (): JSX.Element | void => { 33 | if (Object.keys(networks).length === 1) return; 34 | const title: string = 35 | Object.keys(networks).length > 1 ? 'group networks' : 'default'; 36 | return ( 37 | <option 38 | className="networkOption" 39 | key={title} 40 | id="groupNetworks" 41 | value="groupNetworks" 42 | > 43 | {title} 44 | </option> 45 | ); 46 | }; 47 | const networksOptions = Object.keys(networks).map((network, i) => { 48 | return ( 49 | <option 50 | className="networkOption" 51 | key={`networks option: ${network}`} 52 | id={network} 53 | value={network} 54 | > 55 | {network} 56 | </option> 57 | ); 58 | }); 59 | 60 | let selectClass = selectedNetwork ? 'option selected' : 'option'; 61 | return ( 62 | <> 63 | <select 64 | id="networks" 65 | className={selectClass} 66 | name="networks" 67 | onChange={handleNetworkUpdate} 68 | value={selectedNetwork} 69 | > 70 | <option 71 | key="networks option header" 72 | id="networkHeader" 73 | value="" 74 | disabled 75 | > 76 | networks 77 | </option> 78 | {networksOptions} 79 | {groupNetworks()} 80 | </select> 81 | </> 82 | ); 83 | }; 84 | 85 | export default NetworksDropDown; 86 | -------------------------------------------------------------------------------- /src/renderer/components/OptionBar.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module OptionBar.tsx 5 | 6 | * @author Tyler Hurtt, Michael Villamor 7 | * @date 3/11/20 edited 7/7/22 8 | * @description Used to display toggle options. Refactored into a draggable bar, moved all buttons to left nav 9 | * 10 | * ************************************ 11 | */ 12 | import React from 'react'; 13 | 14 | const OptionBar: React.FC = () => { 15 | 16 | 17 | return ( 18 | <div className="option-bar"> 19 | <div className="views flex"> 20 | </div> 21 | <div className="titles flex"> 22 | <div className="vl"></div> 23 | </div> 24 | </div> 25 | ); 26 | }; 27 | 28 | export default OptionBar; 29 | -------------------------------------------------------------------------------- /src/renderer/components/SwarmDeployment.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module SwarmDeployment.tsx 5 | * @author Kim Wysocki 6 | * @date 5/30/20 7 | * @description component to deploy stacks to Docker Swarm and show deployment state 8 | * 9 | * ************************************ 10 | */ 11 | 12 | import React, { useState, useEffect, useRef } from 'react'; 13 | import { FaUpload, FaRegPlayCircle } from 'react-icons/fa'; 14 | import { GiHeartPlus } from 'react-icons/gi'; 15 | import Draggable from 'react-draggable'; 16 | import { useAppSelector } from '../../hooks'; 17 | import { 18 | runDockerSwarmDeployment, 19 | runLeaveSwarm, 20 | runDockerSwarmDeployStack, 21 | runCheckStack, 22 | } from '../../common/runShellTasks'; 23 | import { Void } from '../App.d'; 24 | 25 | 26 | 27 | const SwarmDeployment: React.FC = () => { 28 | const currentFilePath = useAppSelector((state) => state.filePath); 29 | // Create React hooks to hold onto state 30 | const [success, setSuccess] = useState(false); 31 | const [swarmExists, setSwarmExists] = useState(false); 32 | const [joinToken, setJoinToken] = useState(''); 33 | const [nodeAddress, setNodeAddress] = useState(''); 34 | const [swarmDeployState, setSwarmDeployState] = useState(0); 35 | const [popUpContent, setPopupContent] = useState(<div></div>); 36 | const [popupIsOpen, setPopupIsOpen] = useState(false); 37 | const [stackName, setStackName] = useState(''); 38 | const [allStackNames, setAllStackNames] = useState([] as any); 39 | const stackNameRef = useRef(stackName); 40 | 41 | // if there is no active file, ask user to open a file to deploy 42 | useEffect(() => { 43 | if (currentFilePath && !swarmExists && !success) { 44 | setSwarmDeployState(1); 45 | setPopupContent(popupStartDiv); 46 | } else if (currentFilePath && swarmExists && success) { 47 | setSwarmDeployState(3); 48 | setPopupContent(successDiv); 49 | } else if (!currentFilePath && swarmExists && success) { 50 | setSwarmDeployState(3); 51 | setPopupContent(errorDiv); 52 | } else if (!currentFilePath && !swarmExists && !success) { 53 | setSwarmDeployState(0); 54 | setPopupContent(errorDiv); 55 | } else if (swarmExists && success) { 56 | setPopupContent(successDiv); 57 | } else if (swarmExists && !success) { 58 | setPopupContent(errorDiv); 59 | } 60 | }, [currentFilePath, swarmExists, success]); 61 | 62 | // Submit Swarm name input on pressing 'enter' 63 | const handleKeyPress = (event: any) => { 64 | if (event.key === 'Enter') { 65 | handleClick(event); 66 | } 67 | }; 68 | 69 | // handle click of buttons to add a stack to the swarm 70 | const handleClick = (event: any) => { 71 | if ( 72 | event.target.className === 'create-swarm' || 73 | event.target.className === 'stack-name' 74 | ) { 75 | if (currentFilePath) { 76 | if (swarmExists) addStackToSwarm(); 77 | else if (!swarmExists) getNameAndDeploy(); 78 | } else { 79 | setSuccess(false); 80 | setSwarmDeployState(0); 81 | } 82 | } else if ( 83 | event.target.className === 'add-stack-btn' || 84 | event.target.className === 'new-stack-name' 85 | ) 86 | addStackToSwarm(); 87 | }; 88 | 89 | // save html code in variables for easier access later 90 | // the default for the pop-up div, before any interaction with swarm / after leaving swarm 91 | const popupStartDiv = ( 92 | <div className="initialize-swarm"> 93 | <label htmlFor="stack-name" className="stack-name-label"> 94 | Stack Name 95 | </label> 96 | <input 97 | className="stack-name" 98 | name="stack-name" 99 | placeholder="Enter name...." 100 | onKeyPress={handleKeyPress} 101 | onChange={(event) => { 102 | stackNameRef.current = event.target.value; 103 | }} 104 | ></input> 105 | <button className="create-swarm" onClick={handleClick}> 106 | Create Swarm 107 | </button> 108 | </div> 109 | ); 110 | 111 | // render this div if successful joining swarm 112 | const successDiv = ( 113 | <div className="success-div"> 114 | <p className="success-p"> 115 | <span>Success!</span> 116 | <br></br> 117 | The current node 118 | <span className="success-msg-spans"> {nodeAddress} </span> 119 | is now a manager 120 | </p> 121 | <p className="join-token-p"> 122 | Join swarm from a different machine using: <br></br> 123 | <span className="success-msg-spans">{joinToken}</span> 124 | </p> 125 | <br></br> 126 | 127 | <div className="add-stack-div"> 128 | <label htmlFor="new-stack-name" className="new-stack-name-label"> 129 | Deploy Additional Stack 130 | </label> 131 | <input 132 | className="new-stack-name" 133 | name="new-stack-name" 134 | placeholder="Enter name...." 135 | onKeyPress={handleKeyPress} 136 | onChange={(event) => { 137 | stackNameRef.current = event.target.value; 138 | }} 139 | ></input> 140 | <button className="add-stack-btn" onClick={handleClick}> 141 | Add new stack 142 | </button> 143 | </div> 144 | </div> 145 | ); 146 | 147 | // if unsuccessful / if no active file, render error dive 148 | const errorDiv = ( 149 | <div className="error-div"> 150 | <p className="error-p"> 151 | Sorry, there was an issue initializing your swarm 152 | </p> 153 | <button 154 | className="try-again-btn" 155 | onClick={() => { 156 | leaveSwarm(); 157 | }} 158 | > 159 | Try Again 160 | </button> 161 | </div> 162 | ); 163 | 164 | const startButton = ( 165 | <FaRegPlayCircle className="start-button hidden" size={20} /> 166 | ); 167 | const healthIcon = <GiHeartPlus className="health-icon hidden" size={20} />; 168 | 169 | // retrieve input from user and pass it to runDockerSwarmDeployment as an argument 170 | // the function will return stdout from running each function, so that we have access to that information 171 | const getNameAndDeploy: Void = async (): Promise<any> => { 172 | // hide pop-up while running commands 173 | setPopupIsOpen(false); 174 | setSwarmDeployState(2); 175 | setAllStackNames([...allStackNames, stackNameRef.current]); 176 | 177 | // await results from running dwarm deployment shell tasks 178 | const returnedFromPromise = await runDockerSwarmDeployment( 179 | currentFilePath, 180 | stackNameRef.current, 181 | ); 182 | const infoReturned = JSON.parse(returnedFromPromise); 183 | 184 | // if there is no error on the returned object, swarm initialisation was successful 185 | if (!infoReturned.init.error) { 186 | // Save token for joining, to allow user to copy and use 187 | const tokenForJoiningSwarm = infoReturned.init.out.split('\n')[4].trim(); 188 | setJoinToken(tokenForJoiningSwarm); 189 | // the split here is to get just the 25-character node ID of the swarm manager node 190 | const managerID = infoReturned.init.out 191 | .split('\n')[0] 192 | .split(' ')[4] 193 | .replace(/[()]/g, ''); 194 | setNodeAddress(managerID); 195 | setSuccess(true); 196 | setSwarmExists(true); 197 | setSwarmDeployState(3); 198 | setPopupIsOpen(true); 199 | } else { 200 | setSwarmExists(true); 201 | setSuccess(false); 202 | setSwarmDeployState(1); 203 | setPopupIsOpen(true); 204 | } 205 | }; 206 | 207 | const addStackToSwarm: Void = async (): Promise<any> => { 208 | setPopupIsOpen(false); 209 | setSwarmDeployState(2); 210 | setAllStackNames([...allStackNames, stackNameRef.current]); 211 | 212 | await runDockerSwarmDeployStack(currentFilePath, stackNameRef.current); 213 | await runCheckStack(); 214 | 215 | setSwarmDeployState(3); 216 | setPopupIsOpen(true); 217 | }; 218 | 219 | // function to allow the user to leave the swarm 220 | // called in onClicks 221 | const leaveSwarm: Void = (): void => { 222 | setPopupIsOpen(false); 223 | setSwarmExists(false); 224 | setSuccess(false); 225 | runLeaveSwarm(); 226 | setSwarmDeployState(1); 227 | setNodeAddress(''); 228 | setJoinToken(''); 229 | setStackName(''); 230 | setAllStackNames([]); 231 | }; 232 | 233 | // uninitialised variable allowing the values to change depending on state 234 | // used for swarm deploy button in leftNav 235 | let swarmBtnTitle: string | undefined, swarmOnClick: any; 236 | 237 | if (!swarmExists || (swarmExists && !success)) { 238 | swarmBtnTitle = 'Deploy to Swarm'; 239 | swarmOnClick = () => { 240 | setPopupIsOpen(true); 241 | }; 242 | } else if (swarmExists) { 243 | swarmBtnTitle = 'Leave Swarm'; 244 | swarmOnClick = () => { 245 | setPopupIsOpen(false); 246 | leaveSwarm(); 247 | }; 248 | } 249 | 250 | return ( 251 | <div className="deploy-container"> 252 | <button className="deploy-btn" onClick={swarmOnClick}> 253 | <span> 254 | <FaUpload className="deployment-button" size={24} /> 255 | </span> 256 | {swarmBtnTitle} 257 | <div className="status-container"> 258 | {startButton} 259 | {healthIcon} 260 | <span 261 | className={`deployment-status status-healthy ${ 262 | swarmDeployState === 3 ? 'status-active' : '' 263 | }`} 264 | ></span> 265 | <span 266 | className={`deployment-status status-moderate ${ 267 | swarmDeployState === 2 ? 'status-active' : '' 268 | }`} 269 | ></span> 270 | <span 271 | className={`deployment-status status-dead ${ 272 | swarmDeployState === 1 ? 'status-active' : '' 273 | }`} 274 | ></span> 275 | </div> 276 | </button> 277 | 278 | {/* If popupIsOpen state is set to true, render the popup div, else don't render anything here */} 279 | {popupIsOpen ? ( 280 | <Draggable handle=".exit-popup-div"> 281 | <div className="popup-div"> 282 | <div className="exit-button-and-content-divs"> 283 | <div className="exit-popup-div"> 284 | <button 285 | className="exit-popup-button" 286 | onClick={() => { 287 | setPopupIsOpen(false); 288 | }} 289 | > 290 | X 291 | </button> 292 | </div> 293 | 294 | <div className="popup-content-wrapper">{popUpContent}</div> 295 | </div> 296 | </div> 297 | </Draggable> 298 | ) : null} 299 | </div> 300 | ); 301 | }; 302 | 303 | export default SwarmDeployment; 304 | -------------------------------------------------------------------------------- /src/renderer/components/Tab.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useDispatch } from 'react-redux'; 3 | import { switchTab, closeTab } from '../../reducers/appSlice'; 4 | 5 | 6 | type Props = { 7 | activePath: string; 8 | filePath: string; 9 | }; 10 | 11 | const Tab: React.FC<Props> = ({ 12 | filePath, 13 | activePath, 14 | }) => { 15 | const dispatch = useDispatch(); 16 | 17 | 18 | 19 | let fileSplit; 20 | if (process.platform === 'win32') fileSplit = filePath.split('\\'); 21 | else fileSplit = filePath.split('/'); 22 | const fileName = fileSplit[fileSplit.length - 1]; 23 | const splitName = fileName.split('-'); 24 | const tabClass = filePath === activePath ? 'tab active-tab' : 'tab'; 25 | return ( 26 | <div className={tabClass} id={filePath}> 27 | <div className="tab-text" onClick={() => dispatch(switchTab({filePath: filePath}))}> 28 | {splitName[0]}‑{splitName[1]} 29 | </div> 30 | <button className="close-btn" onClick={() => dispatch(closeTab({filePath: filePath}))}> 31 | {' '} 32 | X 33 | </button> 34 | </div> 35 | ); 36 | }; 37 | 38 | export default Tab; 39 | -------------------------------------------------------------------------------- /src/renderer/components/TabBar.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module TabBar.tsx 5 | 6 | * @author David Soerensen and Linda Everswick, Jordan Long, Michael Villamor, Nathan Lovell, Giovanni Rodriguez 7 | * @date 5/30/20 edited 7/7/22 8 | * @description Used to display tabs of all open docker-compose files; added redux state management 9 | * 10 | * ************************************ 11 | */ 12 | 13 | import React from 'react'; 14 | import Tab from './Tab'; 15 | import { useAppSelector } from '../../hooks' 16 | 17 | const TabBar: React.FC = () => { 18 | const openFiles = useAppSelector(state => state.openFiles); 19 | const activePath = useAppSelector(state => state.filePath); 20 | const tabs = openFiles.map((filePath) => ( 21 | <Tab 22 | key={`${filePath}`} 23 | filePath={`${filePath}`} 24 | activePath={activePath} 25 | /> 26 | )); 27 | return <div className="tab-bar">{tabs}</div>; 28 | }; 29 | 30 | export default TabBar; 31 | -------------------------------------------------------------------------------- /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('nautilx_logo.png')} width='190px' /> 9 | <h1>Nautil<span>X</span></h1> 10 | </div> 11 | ); 12 | 13 | export default Title; 14 | -------------------------------------------------------------------------------- /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 | 19 | 20 | type Props = { 21 | volumes: ReadOnlyObj; 22 | bindMounts: Array<string>; 23 | getColor: (str: string | undefined) => string; 24 | }; 25 | 26 | const VolumesWrapper: React.FC<Props> = ({ volumes, bindMounts, getColor }) => { 27 | return ( 28 | <div className="volumes-wrapper"> 29 | <div className="container"> 30 | <div className="half"> 31 | <h2>Bind Mounts</h2> 32 | <hr /> 33 | <div className="scroll"> 34 | <BindMounts bindMounts={bindMounts} getColor={getColor} /> 35 | </div> 36 | </div> 37 | <div className="half"> 38 | <h2>Volumes</h2> 39 | <hr /> 40 | <div className="scroll"> 41 | <Volumes volumes={volumes} getColor={getColor} /> 42 | </div> 43 | </div> 44 | </div> 45 | </div> 46 | ); 47 | }; 48 | 49 | export default VolumesWrapper; 50 | -------------------------------------------------------------------------------- /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/fileOpen.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module fileOpen.tsx 5 | * @author Michael Villamor, Nathan Lovell, Jordan Long, Giovanni Rodriguez 6 | * @date 7/7/22 7 | * @description created a fileOpen helper function 8 | * ************************************ 9 | */ 10 | 11 | 12 | import yaml from 'js-yaml'; 13 | import convertYamlToState from './yamlParser'; 14 | import setD3State from './setD3State'; 15 | import parseOpenError from './parseOpenError'; 16 | import resolveEnvVariables from '../../common/resolveEnvVariables'; 17 | import { runDockerComposeValidation } from "../../common/runShellTasks"; 18 | 19 | import { FileOpen } from '../App.d' 20 | 21 | 22 | const readFileAsync = (file:File) => { 23 | return new Promise((resolve, reject) => { 24 | let reader = new FileReader(); 25 | 26 | reader.onload = () => { 27 | resolve(reader.result); 28 | }; 29 | 30 | reader.onerror = reject; 31 | 32 | reader.readAsArrayBuffer(file); 33 | }) 34 | }; 35 | 36 | export const fileOpen: FileOpen = async (file: File, openFiles = []): Promise<any> => { 37 | // check for valid file path 38 | if (file.path) { 39 | await runDockerComposeValidation(file.path).then( async (validationResults: any) => { 40 | if (validationResults.error) { 41 | 42 | /** 43 | * @MUSTDO 44 | * if validationResults.error is related to kubernetes yaml, 45 | * run a composeValidation for the kubernetes file 46 | * if it succeeds, go to the else block; 47 | */ 48 | if (validationResults.error.message.includes('apiVersion') || validationResults.error.message.includes('kind') || (validationResults.error.message.includes('spec') && !validationResults.error.message.includes('specify'))|| validationResults.error.message.includes('metadata')){ 49 | 50 | let text:any = await readFileAsync(file); 51 | text = new TextDecoder().decode(text); 52 | const yamlText = convertAndStoreYamlJSON(text, file.path, openFiles); 53 | getCache(yamlText); 54 | } 55 | else { 56 | handleFileOpenError(validationResults.error) 57 | } 58 | } else { 59 | let yamlText: any = await readFileAsync(file); 60 | yamlText = new TextDecoder().decode(yamlText); 61 | //if docker-compose uses env file, replace the variables with value from env file 62 | if (validationResults.envResolutionRequired) { 63 | yamlText = resolveEnvVariables(yamlText, file.path); 64 | } 65 | const yaml = convertAndStoreYamlJSON(yamlText, file.path, openFiles); 66 | 67 | getCache(yaml); 68 | } 69 | }); 70 | } 71 | }; 72 | //Makeshift solution to get async file read working when called from fileSelector 73 | //without putting read file text in some sort of cache the call in fileSelector was returning undefined 74 | export function cacheFile (){ 75 | let pw = '123' 76 | let cache:any = []; 77 | 78 | return function (password: any){ 79 | if (password === pw) return cache 80 | else if (!cache.includes(password)){ 81 | cache.unshift(password); 82 | } 83 | } 84 | } 85 | export function cacheError(){ 86 | let pw = '123' 87 | let cache:any = []; 88 | 89 | return function(password: any){ 90 | if (password === pw) return cache 91 | else if (password === 'reset'){ 92 | cache = []; 93 | } 94 | else { 95 | cache = []; 96 | cache.push(password); 97 | } 98 | } 99 | } 100 | export const getCache = cacheFile(); 101 | export const cacheErrors = cacheError(); 102 | 103 | export const convertAndStoreYamlJSON = (yamlText: string, filePath: string, openFiles: string[] = []) => { 104 | // Convert Yaml to state object. 105 | const yamlJSON = yaml.safeLoad(yamlText); 106 | const yamlState = convertYamlToState(yamlJSON, filePath); 107 | 108 | // Don't add a file that is already opened to the openFiles array 109 | if (!openFiles.includes(filePath)) openFiles.push(filePath); 110 | 111 | // Set global variables for d3 simulation 112 | if(yamlState.kubeObj){ 113 | window.d3State = setD3State(yamlState.kubeObj); 114 | } 115 | else{ 116 | window.d3State = setD3State(yamlState.services); 117 | }; 118 | 119 | // Store opened file state in localStorage under the current state item call "state" as well as an individual item using the filePath as the key. 120 | localStorage.setItem('state', JSON.stringify(yamlState)); 121 | localStorage.setItem(`${filePath}`, JSON.stringify(yamlState)); 122 | return {yamlState: yamlState, filePath:filePath, openFiles: openFiles} 123 | }; 124 | 125 | 126 | /** 127 | * @param errorText -> string 128 | * @returns void 129 | * @description sets state with array of strings of different errors 130 | */ 131 | export const handleFileOpenError = (errorText: Error) => { 132 | // Stop the simulation to prevent hundreds of d3 transform errors from occuring. This is rare but its a simple fix to prevent it. 133 | const { simulation } = window.d3State; 134 | simulation.stop(); 135 | // Grab the current openFiles array so that we don't lose them when setting state. 136 | const openErrors = parseOpenError(errorText); 137 | cacheErrors(openErrors); 138 | return openErrors; 139 | }; 140 | -------------------------------------------------------------------------------- /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:any) => { 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 | 116 | /* 117 | DAG: directed acyclic graph 118 | This means that it is impossible to traverse the entire graph starting at one edge. The edges of the directed graph only go one way. 119 | */ 120 | interface DagCreator { 121 | (nodesObject: SNode[], Links: Link[], kubeBool: boolean): number; 122 | } 123 | export const dagCreator: DagCreator = (nodes, links, kubeBool) => { 124 | //roots object creation, needs to be a deep copy or else deletion of non-roots will remove from nodesObject 125 | const nodesObject: NodesObject = {}; 126 | nodes.forEach((node) => { 127 | nodesObject[node.name] = node; 128 | 129 | }); 130 | 131 | const roots = JSON.parse(JSON.stringify(nodesObject)); 132 | 133 | //iterate through links and find if the roots object contains any of the link targets 134 | 135 | links.forEach((link: Link) => { 136 | if (roots[link.target]) { 137 | //filter the roots 138 | delete roots[link.target]; 139 | } 140 | }); 141 | 142 | //create Tree 143 | const createTree = (node: NodesObject) => { 144 | Object.keys(node).forEach((root: string) => { 145 | links.forEach((link: Link) => { 146 | if (link.source === root) { 147 | node[root].children[link.target] = nodesObject[link.target]; 148 | } 149 | }); 150 | createTree(node[root].children); 151 | }); 152 | }; 153 | createTree(roots); 154 | 155 | //traverse tree and create object outlining the rows/columns in each tree 156 | const treeMap: TreeMap = {}; 157 | const createTreeMap = (node: NodesObject, height: number = 0) => { 158 | 159 | if (!treeMap[height] && Object.keys(node).length > 0) treeMap[height] = []; 160 | Object.keys(node).forEach((sName: string) => { 161 | treeMap[height].push(sName); 162 | createTreeMap(node[sName].children, height + 1); 163 | }); 164 | }; 165 | createTreeMap(roots); 166 | 167 | // populate nodesObject with column, row, and rowLength 168 | const storePositionLocation = (treeHierarchy: TreeMap) => { 169 | if(treeHierarchy[0] && kubeBool){ 170 | let sorted = treeHierarchy[0].sort(); 171 | let copy = {...treeHierarchy, '0': sorted}; 172 | treeHierarchy = copy; 173 | } 174 | 175 | Object.keys(treeHierarchy).forEach((row: string) => { 176 | treeHierarchy[row].forEach((sName: string, column: number) => { 177 | nodesObject[sName].row = Number(row); 178 | if (!nodesObject[sName].column) nodesObject[sName].column = column + 1; 179 | nodesObject[sName].rowLength = treeHierarchy[row].length; 180 | }); 181 | }); 182 | } 183 | storePositionLocation(treeMap); 184 | return Object.keys(treeMap).length; 185 | 186 | }; 187 | 188 | 189 | 190 | /** 191 | * ******************** 192 | * @param services 193 | * @returns an object with serviceGraph, simulation and treeDepth properties 194 | * ******************** 195 | * 196 | * GIO: 197 | * made SetD3State take a kubeObj or Service object 198 | * yamlState: { Kind: ..., name: ... , containers: name: ...., image:...., port:.... } 199 | */ 200 | 201 | 202 | 203 | const setD3State: SetD3State = (services:any = {}) => { 204 | 205 | let nodes: any = []; 206 | if(services.containers){ 207 | 208 | nodes = services.containers.map((sName:any, i:any) => { 209 | const ports = services.containers[i].containerPort; 210 | const node: SNode = { 211 | id: i + 1, 212 | name: sName.name, 213 | ports, 214 | volumes: [], 215 | children: {}, 216 | row: 0, 217 | rowLength: 0, 218 | column: 0, 219 | }; 220 | 221 | return node; 222 | 223 | }) 224 | 225 | 226 | const node: SNode = { 227 | id: 10, 228 | name: services.name, 229 | ports: ['0000'], 230 | volumes: [], 231 | children: {}, 232 | row: 0, 233 | rowLength: 0, 234 | column: 0, 235 | }; 236 | nodes[nodes.length] = node; 237 | let newNodesArr = []; 238 | 239 | let counter = 0; 240 | while(counter < services.replica - 1){ 241 | for(let i = 0; i < services.replica - 1; i++){ 242 | let newNode = nodes[i]; 243 | if (newNode.name === node.name) continue; 244 | else { 245 | if(counter === 0){ 246 | newNode = {...nodes[i], id: nodes[i].id + counter + i, name: nodes[i].name + ` replica ${counter + 1}`}; 247 | } 248 | else { 249 | newNode = {...nodes[i], id: nodes[i].id + counter + i, name: nodes[i].name + ` replica ${counter + 1}`}; 250 | } 251 | newNodesArr.push(newNode); 252 | } 253 | } 254 | counter += 1; 255 | } 256 | 257 | nodes = nodes.concat(newNodesArr); 258 | 259 | }else{ 260 | nodes = Object.keys(services).map((sName: string, i) => { 261 | // extract ports data if available 262 | const ports = services[sName].hasOwnProperty('ports') 263 | ? extractPorts(services[sName].ports as Ports) 264 | : []; 265 | // extract volumes data if available 266 | const volumes: string[] = services[sName].hasOwnProperty('volumes') 267 | ? extractVolumes(services[sName].volumes as Volumes) 268 | : []; 269 | // extract networks data if available 270 | const networks: string[] = services[sName].hasOwnProperty('networks') 271 | ? extractNetworks(services[sName].networks as string[]) 272 | : []; 273 | const node: SNode = { 274 | id: i, 275 | name: sName, 276 | ports, 277 | volumes, 278 | networks, 279 | children: {}, 280 | row: 0, 281 | rowLength: 0, 282 | column: 0, 283 | }; 284 | 285 | return node; 286 | }); 287 | } 288 | const links: Link[] = []; 289 | 290 | if(services.containers){ 291 | 292 | nodes.forEach((node:any) => { 293 | if (!node.name.includes('replica') && services.name !== node.name) links.push({source: node.name, target: services.name}); 294 | }); 295 | 296 | for(let i = 0 ; i < services.containers.length; i++){ 297 | for(let j = 0; j < services.replica - 1; j++){ 298 | links.push({target: `${services.containers[i].name}`, source: `${services.containers[i].name} replica ${j + 1}`}) 299 | } 300 | } 301 | } 302 | else{ 303 | 304 | Object.keys(services).forEach((sName: string) => { 305 | if (services[sName].hasOwnProperty('depends_on')) { 306 | services[sName].depends_on!.forEach((el:any) => { 307 | links.push({ source: el, target: sName }); 308 | }); 309 | } 310 | }); 311 | } 312 | let kubeBool = false; 313 | if(services.kind){ 314 | kubeBool = true; 315 | } 316 | 317 | const treeDepth = dagCreator(nodes, links, kubeBool); 318 | /** 319 | ********************* 320 | * Variables for d3 visualizer 321 | ********************* 322 | */ 323 | nodes = nodes.sort((a:any,b:any) => { 324 | return a.id - b.id 325 | }) 326 | const d3State: D3State = { 327 | treeDepth, 328 | serviceGraph: { 329 | nodes, 330 | links, 331 | }, 332 | simulation: d3.forceSimulation<SNode>(nodes), 333 | }; 334 | return d3State; 335 | }; 336 | 337 | export default setD3State; 338 | -------------------------------------------------------------------------------- /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, KubeObj } from '../App.d'; 2 | 3 | type YamlState = { 4 | fileOpened: boolean; 5 | kubeBool?: boolean; 6 | services?: Services; 7 | filePath?: string; 8 | dependsOn?: DependsOn; 9 | networks?: ReadOnlyObj; 10 | volumes?: ReadOnlyObj; 11 | bindMounts?: Array<string>; 12 | kubeObj?: KubeObj; 13 | }; 14 | 15 | const kubeParser = (file:any):any => { 16 | const kube: KubeObj = { 17 | kind: file.kind, 18 | name: file.metadata.name 19 | }; 20 | if (kube.kind === 'Pod') { 21 | return {...kube, containers: file.spec.containers} 22 | } 23 | if (kube.kind === 'Deployment') { 24 | return {...kube, containers: file.spec.template.spec.containers, replica: file.spec.replicas} 25 | } 26 | if (kube.kind === 'Service') { 27 | return {...kube, selector: file.spec.selector, ports: file.spec.ports} 28 | } 29 | } 30 | 31 | const convertYamlToState = (file: any, filePath: string):any => { 32 | //check if file.apiVersion exists, if so Kube logic -> 33 | //save kind as variable, execeute logic if deployement, service, pod 34 | if (file.apiVersion) { 35 | return {fileOpened: true, kubeBool: true, kubeObj: kubeParser(file)} 36 | } 37 | else { 38 | const services = file.services; 39 | const volumes = file.volumes ? file.volumes : {}; 40 | const networks = file.networks ? file.networks : {}; 41 | const state: YamlState = Object.assign( 42 | {}, 43 | { fileOpened: true, kubeBool: false, services, volumes, networks, filePath }, 44 | ); 45 | const bindMounts: string[] = []; 46 | // iterate through each service 47 | Object.keys(services).forEach((name): void => { 48 | // IF SERVICE HAS VOLUMES PROPERTY 49 | if (services[name].volumes) { 50 | // iterate from all the volumes 51 | services[name].volumes.forEach((volume: VolumeType): void => { 52 | let v = ''; 53 | if (typeof volume === 'string') { 54 | // if its a bind mount, capture it 55 | v = volume.split(':')[0]; 56 | } else if ( 57 | 'source' in volume && 58 | volume.source && 59 | volume.type === 'bind' 60 | ) { 61 | v = volume.source; 62 | } 63 | if (!!v && !volumes.hasOwnProperty(v)) { 64 | bindMounts.push(v); 65 | } 66 | }); 67 | } 68 | }); 69 | state.bindMounts = bindMounts; 70 | return state; 71 | }; 72 | } 73 | export default convertYamlToState; 74 | -------------------------------------------------------------------------------- /src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module index.tsx 5 | * @author Joshua Nordstrom edited by Jordan Long 6 | * @date 3/7/20 edited: (6/16/2022) 7 | * @description entry point for application. Hangs React app off of #root in index.html 8 | * App.tsx is the main child of this 9 | * 10 | * ************************************ 11 | */ 12 | 13 | import * as React from 'react'; 14 | import { render } from 'react-dom'; 15 | import { Provider } from 'react-redux'; 16 | import App from './App'; 17 | import { D3State } from '../renderer/App.d'; 18 | import store from '../store'; 19 | 20 | // IMPORT STYLES 21 | import './styles/app.scss'; 22 | 23 | if (module.hot) { 24 | module.hot.accept(); 25 | } 26 | //boilerplate code for setting up D3 in TS 27 | declare global { 28 | interface Window { //defining that 'Window' is an object with a property, d3State which is based on the D3State object literal defined in App.d.ts 29 | d3State: D3State; 30 | } 31 | } 32 | 33 | //boilerplate code for setting up react app 34 | render( 35 | <Provider store={store}> 36 | <App /> 37 | </Provider>, 38 | document.getElementById('app'), 39 | ); 40 | -------------------------------------------------------------------------------- /src/renderer/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | 2 | $primary-color: #1d282d; 3 | $secondary-color: #08a5ff; 4 | $third-color: #020a0b; 5 | $fourth-color: #cfe7fe; 6 | $fifth-color: #2ba5de; 7 | $hover-color: #12aef6; 8 | 9 | $border: 1px solid #003459; 10 | $font-fam: 'Sen-Regular'; 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/renderer/styles/app.scss: -------------------------------------------------------------------------------- 1 | // IMPORT SASS 2 | @import 'variables'; 3 | @import 'optionBar'; 4 | @import 'leftNav'; 5 | @import 'd3Wrapper'; 6 | @import 'tabBar'; 7 | @import 'popup'; 8 | @import 'deployButton'; 9 | 10 | @font-face { 11 | font-family: 'Sen-Regular'; 12 | src: url(./fonts/Sen-Regular.ttf) format('truetype'); 13 | } 14 | 15 | @font-face { 16 | font-family: 'Sen-Bold'; 17 | src: url(./fonts/Sen-Bold.ttf) format('truetype'); 18 | } 19 | 20 | @font-face { 21 | font-family: 'Sen-ExtraBold'; 22 | src: url(./fonts/Sen-ExtraBold.ttf) format('truetype'); 23 | } 24 | 25 | html * { 26 | font-family: $font-fam; 27 | } 28 | 29 | body { 30 | font-size: 14px; 31 | -webkit-user-select: text; 32 | margin: 0; 33 | } 34 | 35 | .flex { 36 | display: flex; 37 | } 38 | 39 | h1 { 40 | font-family: 'Sen-Bold'; 41 | } 42 | 43 | h2 { 44 | color: $primary-color; 45 | font-size: 1.5em; 46 | line-height: 1em; 47 | align-self: center; 48 | } 49 | 50 | .app-class { 51 | background-color: $fourth-color; 52 | display: flex; 53 | min-height: 650px; 54 | height: 100vh; 55 | width: 100vw; 56 | 57 | .main { 58 | flex-grow: 1; 59 | min-width: 700px; 60 | flex-direction: column; 61 | } 62 | 63 | // creates draggable bar at the top of application to replace removed native bar 64 | .draggable { 65 | -webkit-app-region: drag; 66 | height: 1.5em; 67 | position: absolute; 68 | top: 0; 69 | left: 0; 70 | width: 100vw; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /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: 0.1em; 8 | .error-open-wrapper { 9 | width: 70%; 10 | display: flex; 11 | flex-direction: column; 12 | color: red; 13 | 14 | .error-display { 15 | display: flex; 16 | flex-direction: column; 17 | } 18 | } 19 | 20 | .services-wrapper { 21 | flex: 1 1 0; 22 | height: 100%; 23 | 24 | .view-wrapper { 25 | height: 100%; 26 | width: 100%; 27 | 28 | .graph { 29 | width: 100%; 30 | height: 100%; 31 | 32 | .node { 33 | cursor: pointer; 34 | .nodeLabel { 35 | font-family: 'Sen-Bold'; 36 | fill: rgb(1, 133, 177); 37 | font-size: 16px; 38 | text-anchor: middle; 39 | } 40 | .nodeLabelStroke { 41 | font-family: 'Sen-Bold'; 42 | stroke: #006896; //original: #006896 43 | stroke-width: 4; 44 | font-size: 14px; 45 | text-anchor: middle; 46 | } 47 | .port { 48 | stroke: $secondary-color; 49 | fill: rgb(223, 223, 223); 50 | opacity: 0.9; 51 | } 52 | .ports-text { 53 | tspan { 54 | font-family: 'Sen-Bold' !important; 55 | fill: $primary-color; 56 | } 57 | } 58 | } 59 | 60 | .hide { 61 | display: none; 62 | } 63 | 64 | .arrowHead { 65 | fill: #000000; 66 | .line-cover { 67 | fill: $fourth-color; 68 | } 69 | } 70 | 71 | .link { 72 | stroke: black; 73 | } 74 | } 75 | } 76 | } 77 | 78 | .volumes-wrapper { 79 | width: 245px; 80 | height: 100%; 81 | display: flex; 82 | align-items: center; 83 | margin: 0 1em; 84 | font-size: 1.3em; 85 | color: white; 86 | 87 | .container { 88 | width: 100%; 89 | height: 90%; 90 | background-color: $primary-color; 91 | border-radius: .5em; 92 | display: flex; 93 | flex-direction: column; 94 | align-items: stretch; 95 | 96 | .half { 97 | flex: 1 1 0; 98 | display: flex; 99 | flex-direction: column; 100 | align-items: center; 101 | height: fit-content; 102 | max-height: 50%; 103 | padding: 1.5em; 104 | } 105 | 106 | h2 { 107 | color: white; 108 | margin: 0.75em 0 0.5em 0; 109 | -webkit-user-select: none; 110 | } 111 | 112 | hr { 113 | border-top: $border; 114 | width: 80%; 115 | margin: 0 0 0.75em 0; 116 | } 117 | 118 | .volumeLegend { 119 | margin: 0 0 0.75em 0.1em; 120 | display: flex; 121 | 122 | p { 123 | margin: 0 0 0 0; 124 | word-break: break-all; 125 | width: 175px; 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/deployButton.scss: -------------------------------------------------------------------------------- 1 | .deploy-container { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: initial; 5 | color: white; 6 | width: 100%; 7 | text-align: center; 8 | margin-bottom: 8%; 9 | align-items: center; 10 | 11 | .deploy-btn { 12 | background-color: rgb(223, 223, 223); 13 | color: black; 14 | padding-bottom: 7px; 15 | padding-top: 7px; 16 | padding-left: 0px; 17 | padding-right: 0px; 18 | outline: none; 19 | font-size: 1.1em; 20 | width: 85%; 21 | border: none; 22 | border-radius: 3px; 23 | margin-bottom: 0.6em; 24 | transition: background-color 0.2s; 25 | 26 | .deployment-title { 27 | font-size: 1em; 28 | padding-left: 0.5em; 29 | } 30 | .open-button { 31 | float: left; 32 | transform: translate(14px, 12px); 33 | color: $primary-color; 34 | } 35 | .deployment-button { 36 | float: left; 37 | transform: translate(14px, 12px); 38 | color: $primary-color; 39 | } 40 | span { 41 | margin-right: 5px; 42 | } 43 | 44 | &:hover { 45 | cursor: hand; 46 | cursor: pointer; 47 | color: black; 48 | background-color: darken($fourth-color, 10%); 49 | } 50 | &:active { 51 | background-color: darken($fourth-color, 25%); 52 | } 53 | } 54 | } 55 | 56 | .status-container { 57 | display: flex; 58 | align-items: center; 59 | padding-left: 20%; 60 | padding-right: 5%; 61 | width: 11em; 62 | color: white; 63 | } 64 | 65 | #compose-deploy-div { 66 | margin-bottom: 8%; 67 | } 68 | 69 | .deployment-status { 70 | display: inline-flex; 71 | border-radius: 50%; 72 | height: 25px; 73 | width: 25px; 74 | border: 1px solid; 75 | font-size: 1.7em; 76 | font-weight: bolder; 77 | align-items: center; 78 | justify-content: center; 79 | } 80 | 81 | .start-button { 82 | color: red; 83 | margin-right: 0.9em; 84 | margin-left: 0.1em; 85 | } 86 | .stop-button { 87 | color: red; 88 | margin-right: 0.9em; 89 | margin-left: 0.1em; 90 | } 91 | 92 | .health-icon { 93 | margin-left: 0.5em; 94 | color: $primary-color; 95 | &.green { 96 | color: rgb(6, 122, 6); 97 | } 98 | } 99 | 100 | .status-healthy { 101 | border-color: rgb(140, 170, 194); 102 | background-color: rgb(140, 170, 194); 103 | &.status-active { 104 | background-color: rgb(9, 247, 9); 105 | } 106 | } 107 | 108 | .status-moderate { 109 | border-color: rgb(140, 170, 194); 110 | background-color: rgb(140, 170, 194); 111 | &.status-active { 112 | background-color: rgb(246, 246, 8); 113 | } 114 | } 115 | .status-dead { 116 | border-color: rgb(140, 170, 194); 117 | background-color: rgb(140, 170, 194); 118 | &.status-active { 119 | background-color: rgb(253, 7, 7); 120 | } 121 | } 122 | 123 | .button-container:hover { 124 | cursor: hand; 125 | cursor: pointer; 126 | color: $fifth-color; 127 | } 128 | 129 | .clickable-status:hover { 130 | cursor: hand; 131 | cursor: pointer; 132 | } 133 | 134 | .deployment-title:hover { 135 | cursor: hand; 136 | cursor: pointer; 137 | } 138 | 139 | .button-container { 140 | display: flex; 141 | justify-content: center; 142 | } 143 | .hidden { 144 | visibility: hidden; 145 | } 146 | 147 | .health-spinner { 148 | animation: rotate 2s linear infinite; 149 | width: 24px; 150 | height: 24px; 151 | margin-right: 11px; 152 | 153 | & .path { 154 | stroke: hsl(210, 70%, 75%); 155 | stroke-linecap: round; 156 | animation: dash 1.5s ease-in-out infinite; 157 | } 158 | } 159 | 160 | @keyframes rotate { 161 | 100% { 162 | transform: rotate(360deg); 163 | } 164 | } 165 | 166 | @keyframes dash { 167 | 0% { 168 | stroke-dasharray: 1, 150; 169 | stroke-dashoffset: 0; 170 | } 171 | 50% { 172 | stroke-dasharray: 90, 150; 173 | stroke-dashoffset: -35; 174 | } 175 | 100% { 176 | stroke-dasharray: 90, 150; 177 | stroke-dashoffset: -124; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /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/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/src/renderer/styles/fonts/Sen-Bold.ttf -------------------------------------------------------------------------------- /src/renderer/styles/fonts/Sen-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/src/renderer/styles/fonts/Sen-ExtraBold.ttf -------------------------------------------------------------------------------- /src/renderer/styles/fonts/Sen-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/src/renderer/styles/fonts/Sen-Regular.ttf -------------------------------------------------------------------------------- /src/renderer/styles/leftNav.scss: -------------------------------------------------------------------------------- 1 | $info-background: rgb(224, 233, 241); 2 | 3 | .left-nav { 4 | display: grid; 5 | grid-template-columns: 238px; 6 | grid-template-rows: 320px 1fr; 7 | grid-template-areas: 'top-half' 'info'; 8 | background-color: $primary-color; 9 | min-height: 650px; 10 | max-height: 100vh; 11 | 12 | 13 | //Nautilus logo and "Open" button 14 | .top-half { 15 | grid-area: top-half; 16 | 17 | .title { 18 | margin: 12% 10% 2% 10%; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | -webkit-user-select: none; 24 | .cls-1 { 25 | isolation: isolate; 26 | } 27 | .cls-2 { 28 | opacity: 0.75; 29 | mix-blend-mode: multiply; 30 | } 31 | .cls-3 { 32 | fill: none; 33 | stroke-linecap: round; 34 | stroke-miterlimit: 10; 35 | stroke-width: 15px; 36 | stroke: url(#linear-gradient); 37 | } 38 | .logo { 39 | width: 100%; 40 | padding-right: 0.5em; 41 | margin-left: 1.5em; 42 | } 43 | 44 | h1 { 45 | color: rgb(251, 251, 251); //original: rgb(12, 142, 255) 46 | font-size: 3.5em; 47 | margin-top: 0; 48 | } 49 | span { 50 | font-family: 'Sen-Bold'; 51 | color: #0C8EFF; 52 | font-size: 53px; 53 | } 54 | } 55 | 56 | .file-open { 57 | margin: 10%; 58 | background: none; 59 | color: white; 60 | font-size: 1em; 61 | display: flex; 62 | justify-content: center; 63 | 64 | h5 { 65 | font-size: 1em; 66 | align-self: center; 67 | padding-left: 0.5em; 68 | margin: 0; 69 | } 70 | 71 | .open-flex { 72 | display: flex; 73 | justify-content: center; 74 | padding: 0.3em; 75 | } 76 | 77 | .select-file-button:hover { 78 | cursor: hand; 79 | cursor: pointer; 80 | color: $fifth-color; 81 | } 82 | 83 | button { 84 | background: none; 85 | color: inherit; 86 | border: none; 87 | padding: 0; 88 | font: inherit; 89 | cursor: hand; 90 | cursor: pointer; 91 | opacity: 0.9; 92 | outline: inherit; 93 | } 94 | } 95 | } 96 | 97 | //"Please select a container with details to display" 98 | .info-dropdown { 99 | padding: 0 1em 1em 1em; 100 | grid-area: info; 101 | max-height: 100%; 102 | min-height: 0; 103 | display: grid; 104 | grid-template-columns: 100%; 105 | grid-template-rows: auto 1fr; 106 | grid-template-areas: 'name' 'card'; 107 | max-width: 100%; 108 | 109 | h3 { 110 | grid-area: name; 111 | text-align: center; 112 | color: white; 113 | margin-bottom: 0.5em; 114 | font-size: 16px; 115 | } 116 | 117 | .content-wrapper { 118 | grid-area: card; 119 | height: 100%; 120 | max-width: 100%; 121 | 122 | display: flex; 123 | flex: 1; 124 | min-height: 0; 125 | .overflow-container { 126 | flex: 1; 127 | overflow-y: auto; 128 | scroll-behavior: smooth; 129 | 130 | .overview { 131 | min-height: min-content; 132 | flex-direction: column; 133 | padding: 1em; 134 | background-color: $info-background; 135 | font-size: 1em; 136 | overflow-wrap: break-word; 137 | border-radius: 0.25em; 138 | 139 | div { 140 | padding-bottom: 0.5em; 141 | } 142 | 143 | .command { 144 | color: $secondary-color; 145 | font-weight: 800; 146 | } 147 | 148 | .options { 149 | padding: 0; 150 | 151 | div { 152 | padding-left: 1em; 153 | padding-bottom: 0; 154 | 155 | ul { 156 | padding: 0; 157 | padding-inline-start: 1.3em; 158 | 159 | li { 160 | span { 161 | font-weight: 600; 162 | } 163 | } 164 | } 165 | 166 | .option-key { 167 | font-weight: 600; 168 | } 169 | } 170 | } 171 | 172 | .second-level { 173 | span { 174 | font-weight: 600; 175 | } 176 | } 177 | 178 | ul { 179 | padding-inline-start: 1.3em; 180 | margin: 0; 181 | } 182 | } 183 | } 184 | } 185 | } 186 | 187 | .card-header { 188 | position: sticky; 189 | background: none; 190 | color: $fourth-color; 191 | background-color: $primary-color; 192 | padding: 1em; 193 | } 194 | 195 | .leftNavContainerTitle { 196 | text-align: center; 197 | color: $fifth-color; 198 | } 199 | #depends_on { 200 | padding: 0em .5em !important; 201 | margin: 5% 5%; 202 | margin-left: auto; 203 | font-size: 18px; 204 | color: white; 205 | display: flex; 206 | justify-content: center; 207 | } 208 | #depends_on:hover { 209 | color: $hover-color; 210 | cursor: pointer; 211 | } 212 | 213 | .options-flex2 { 214 | font-size: 16px; 215 | color: white; 216 | padding: .5em 1em !important; 217 | margin: 5% 5%; 218 | } 219 | .option { 220 | 221 | } 222 | .option:hover { 223 | color: $hover-color; 224 | cursor: pointer; 225 | } 226 | p { 227 | font-size: 18px; 228 | color: white; 229 | display: flex; 230 | justify-content: center; 231 | } 232 | .selected { 233 | color: $hover-color !important; 234 | // border: 1px solid #c1d3e0 !important; 235 | font-weight: bolder; 236 | } 237 | 238 | 239 | .kubeData{ 240 | font-size: 12px; 241 | background-color: $info-background; 242 | margin-top: 10%; 243 | margin-bottom: 200%; 244 | margin-left: 5%; 245 | margin-right: 5%; 246 | border-radius: .25em; 247 | padding: 1em; 248 | 249 | 250 | 251 | 252 | h2 { 253 | font-size: 18px; 254 | color: #08a5ff; 255 | 256 | } 257 | } 258 | .kInfo { 259 | color: black; 260 | font-size: 16px; 261 | display: flex; 262 | flex-direction: column; 263 | align-items: center; 264 | } 265 | .kInfo2 { 266 | font-size: 16px; 267 | padding-left: 0%; 268 | padding-right: 0%; 269 | 270 | } 271 | 272 | } -------------------------------------------------------------------------------- /src/renderer/styles/optionBar.scss: -------------------------------------------------------------------------------- 1 | .option-bar { 2 | display: flex; 3 | justify-content: center; 4 | // border-bottom: $border; 5 | padding: 1.3em; 6 | 7 | background-color: #1d282d; //original: #00325c; 8 | 9 | .views { 10 | flex: 1 1 0; 11 | justify-content: flex-end; 12 | select { 13 | color: $secondary-color; 14 | background-color: $fourth-color; 15 | font-size: 16px; 16 | border: none; 17 | } 18 | select:hover { 19 | cursor: pointer; 20 | } 21 | select:focus { 22 | outline: none; 23 | } 24 | } 25 | 26 | .titles { 27 | justify-content: center; 28 | 29 | .vl { 30 | border-left: $border; 31 | } 32 | 33 | h2 { 34 | color: $fourth-color; 35 | margin: 0em 0.5em 0em 0.3em; 36 | -webkit-user-select: none; 37 | } 38 | } 39 | 40 | .options { 41 | flex: 1 1 0; 42 | justify-content: flex-start; 43 | align-content: flex-start; 44 | } 45 | 46 | .selected { 47 | background-color: $hover-color !important; 48 | border: 1px solid #c1d3e0 !important; 49 | font-weight: bolder; 50 | } 51 | 52 | .option { 53 | font-size: 16px; 54 | color: $secondary-color; 55 | padding: 0em 0.3em !important; 56 | border-radius: 1em; 57 | margin: 0 3%; 58 | } 59 | 60 | .option:hover { 61 | background-color: $hover-color; 62 | cursor: pointer; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/renderer/styles/popup.scss: -------------------------------------------------------------------------------- 1 | //pop-up div 2 | .popup-div { 3 | position: absolute; 4 | width: 35em; 5 | top: 40%; 6 | left: 30%; 7 | background-color: #dfe9fd; 8 | border: solid 1px #013874; 9 | // border-radius: 20px; 10 | border-bottom-left-radius: 1em; 11 | border-bottom-right-radius: 1em; 12 | 13 | .exit-button-and-content-divs { 14 | // background-color: gainsboro; 15 | width: 100%; 16 | height: 100%; 17 | display: flex; 18 | flex-direction: column; 19 | justify-content: space-between; 20 | } 21 | 22 | .exit-popup-button { 23 | margin-right: 0.4em; 24 | margin-top: 0.2em; 25 | background-color: red; 26 | color: white; 27 | font-size: 1.5em; 28 | } 29 | .exit-popup-div { 30 | background-color: $primary-color; 31 | display: flex; 32 | flex-direction: row-reverse; 33 | button { 34 | outline: none; 35 | margin-bottom: 0.2em; 36 | border: none; 37 | } 38 | } 39 | .popup-content-wrapper { 40 | width: 100%; 41 | height: 100%; 42 | display: flex; 43 | justify-content: center; 44 | align-items: center; 45 | // margin-left: 12%; 46 | 47 | .initialize-swarm { 48 | display: flex; 49 | flex-direction: column; 50 | justify-content: center; 51 | align-items: center; 52 | 53 | .stack-name-label { 54 | color: teal; 55 | font-size: 1.5em; 56 | margin-top: 0.2em; 57 | padding: 7px; 58 | } 59 | 60 | .stack-name { 61 | border-radius: 10px; 62 | height: 1.5rem; 63 | font-size: 1em; 64 | padding: 5px; 65 | margin-bottom: 6px; 66 | } 67 | 68 | .stack-name:focus, 69 | textarea:focus, 70 | select:focus { 71 | outline: none; 72 | } 73 | 74 | .create-swarm { 75 | background-color: teal; 76 | color: white; 77 | font-size: 1.2em; 78 | width: 50%; 79 | border-radius: 10px; 80 | margin-bottom: 1em; 81 | } 82 | } 83 | .success-msg-spans { 84 | margin-bottom: 1em; 85 | font-weight: 600; 86 | } 87 | 88 | .error-div { 89 | width: 90%; 90 | flex-direction: column; 91 | justify-content: center; 92 | align-items: center; 93 | margin-bottom: 1em; 94 | 95 | .error-p { 96 | color: red; 97 | } 98 | .try-again-btn { 99 | background-color: teal; 100 | color: white; 101 | font-size: 1.2em; 102 | width: 40%; 103 | border-radius: 10px; 104 | } 105 | } 106 | 107 | .success-div { 108 | width: 105%; 109 | height: 110%; 110 | flex-direction: column; 111 | justify-content: center; 112 | align-items: center; 113 | 114 | .success-p { 115 | color: green; 116 | font-size: 1.1em; 117 | } 118 | 119 | .success-msg-spans { 120 | color: lighten($color: green, $amount: 10%); 121 | } 122 | 123 | .join-token-p { 124 | color: green; 125 | font-size: 0.8em; 126 | } 127 | 128 | .add-stack-div { 129 | display: flex; 130 | flex-direction: column; 131 | justify-content: center; 132 | align-items: center; 133 | margin-top: -5%; 134 | 135 | .new-stack-name-label { 136 | color: teal; 137 | font-size: 1em; 138 | margin-top: 0.2em; 139 | padding: 5px; 140 | } 141 | 142 | .add-stack-btn { 143 | background-color: teal; 144 | color: white; 145 | font-size: 1em; 146 | width: 40%; 147 | border-radius: 10px; 148 | margin: 1em; 149 | } 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/renderer/styles/tabBar.scss: -------------------------------------------------------------------------------- 1 | .tab-bar { 2 | display: flex; 3 | 4 | background-color: rgb(56, 69, 79); 5 | 6 | height: 2.2em; 7 | align-items: bottom; 8 | overflow-y: hidden; 9 | overflow-x: scroll; 10 | .tab { 11 | width: auto; 12 | height: 2.2em; 13 | color: darken($secondary-color, 10%); 14 | 15 | background-color: rgb(25, 46, 30); 16 | 17 | color: white; 18 | overflow: hidden; 19 | display: flex; 20 | align-items: center; 21 | justify-content: space-around; 22 | padding-left: 10px; 23 | .tab-text { 24 | max-width: 100%; 25 | overflow: hidden; 26 | font-size: 0.95em; 27 | } 28 | .tab-text:hover { 29 | cursor: pointer; 30 | } 31 | .close-btn { 32 | color: rgb(184, 184, 189); 33 | font-size: 0.77em; 34 | border: none; 35 | background-color: rgba(0, 0, 0, 0); 36 | } 37 | .close-btn:hover { 38 | color: white; 39 | } 40 | } 41 | .active-tab { 42 | 43 | background-color: rgb(40, 141, 249); 44 | 45 | color: white; 46 | } 47 | } 48 | 49 | .tab-bar::-webkit-scrollbar { 50 | display: none; 51 | } 52 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import 'jest-enzyme'; 2 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ************************************ 3 | * 4 | * @module store.ts 5 | * @author Jordan Long, Michael Villamor, Nathan Lovell, Giovanni Rodriguez 6 | * @date 6/16/2022 7 | * @description Redux 'single source of truth' 8 | * 9 | * ************************************ 10 | */ 11 | 12 | import { configureStore } from '@reduxjs/toolkit'; 13 | import appSlice from './reducers/appSlice'; 14 | 15 | 16 | const store = configureStore({ 17 | reducer: appSlice, 18 | }); 19 | 20 | export type AppState = ReturnType<typeof store.getState> 21 | export type AppDispatch = typeof store.dispatch; 22 | export default store; -------------------------------------------------------------------------------- /static/NautilX-app-Tree.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/static/NautilX-app-Tree.jpg -------------------------------------------------------------------------------- /static/NautilX-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/static/NautilX-logo.png -------------------------------------------------------------------------------- /static/Nautilus-text-logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/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/box.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/boxPath.ts: -------------------------------------------------------------------------------- 1 | export default ` `; 2 | -------------------------------------------------------------------------------- /static/kubernetes-icon-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/static/kubernetes-icon-color.png -------------------------------------------------------------------------------- /static/nautilus-new-ui-mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/static/nautilus-new-ui-mockup.png -------------------------------------------------------------------------------- /static/nautilus-text-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/static/nautilus-text-logo.png -------------------------------------------------------------------------------- /static/nautilus_logo.svg: -------------------------------------------------------------------------------- 1 | Nautilus Logo -------------------------------------------------------------------------------- /static/nautilx_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/static/nautilx_logo.png -------------------------------------------------------------------------------- /static/options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/static/options.png -------------------------------------------------------------------------------- /static/screenshots/container-dependent-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/static/screenshots/container-dependent-view.png -------------------------------------------------------------------------------- /static/screenshots/deploy-container.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/static/screenshots/deploy-container.gif -------------------------------------------------------------------------------- /static/screenshots/deploy-to-swarm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/static/screenshots/deploy-to-swarm.gif -------------------------------------------------------------------------------- /static/screenshots/healthvid.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/static/screenshots/healthvid.gif -------------------------------------------------------------------------------- /static/screenshots/info-ports-volumes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/static/screenshots/info-ports-volumes.png -------------------------------------------------------------------------------- /static/screenshots/network-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/static/screenshots/network-view.png -------------------------------------------------------------------------------- /static/screenshots/open-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/static/screenshots/open-file.png -------------------------------------------------------------------------------- /static/screenshots/view-multiple-files.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/static/screenshots/view-multiple-files.gif -------------------------------------------------------------------------------- /static/views.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-source-labs/Nautilus/bd4c811d7436c8e03a19ac1e76e0cccbb484bd3c/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 | "noImplicitReturns": false 12 | }, 13 | "exclude": [ 14 | "dist/**/*", 15 | "release/**/*", 16 | "webpack.main.ext.js", 17 | "webpack.renderer.ext.js", 18 | "babel.config.js" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------