├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 1-Bug_report.md │ ├── 2-Question.md │ └── 3-Feature_request.md ├── config.yml ├── stale.yml └── workflows │ └── test.yml ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── app ├── .testcafe-electron-rc ├── Routes.tsx ├── app.global.css ├── app.html ├── app.icns ├── assets │ ├── DockterLogo.png │ ├── DockterLogoSM.png │ ├── DockterScreenshot.png │ ├── Playbutton.gif │ ├── betterfilter.gif │ └── scroll.gif ├── components │ ├── CurrentFilters.tsx │ ├── Filter.tsx │ ├── FilterInput.tsx │ ├── FilterOptions.tsx │ ├── FilterStreamOptions.tsx │ ├── FilterTimeOptions.tsx │ ├── LiveLogController.tsx │ ├── LogsRows.tsx │ ├── LogsTable.tsx │ ├── Navbar.tsx │ ├── SearchBar.tsx │ └── Utilities.tsx ├── constants │ └── routes.json ├── containers │ ├── App.tsx │ ├── LandingPage.tsx │ ├── LogsContainer.tsx │ └── Root.tsx ├── index.tsx ├── main-process │ ├── db.ts │ ├── filter.ts │ ├── liveLogs.ts │ ├── scroll.ts │ └── startup.ts ├── main.dev.ts ├── main.prod.js.LICENSE ├── main.prod.js.LICENSE.txt ├── menu.ts ├── models │ ├── containerModel.ts │ └── logModel.ts ├── package.json ├── rootReducer.ts ├── store.ts ├── worker.html └── yarn.lock ├── babel.config.js ├── configs ├── webpack.config.base.js ├── webpack.config.eslint.js ├── webpack.config.main.prod.babel.js ├── webpack.config.renderer.dev.babel.js ├── webpack.config.renderer.dev.dll.babel.js └── webpack.config.renderer.prod.babel.js ├── db └── database.sqlite3 ├── internals ├── img │ ├── erb-banner.png │ ├── erb-logo.png │ ├── eslint-padded-90.png │ ├── eslint-padded.png │ ├── eslint.png │ ├── jest-padded-90.png │ ├── jest-padded.png │ ├── jest.png │ ├── js-padded.png │ ├── js.png │ ├── npm.png │ ├── react-padded-90.png │ ├── react-padded.png │ ├── react-router-padded-90.png │ ├── react-router-padded.png │ ├── react-router.png │ ├── react.png │ ├── redux-padded-90.png │ ├── redux-padded.png │ ├── redux.png │ ├── webpack-padded-90.png │ ├── webpack-padded.png │ ├── webpack.png │ ├── yarn-padded-90.png │ ├── yarn-padded.png │ └── yarn.png ├── mocks │ └── fileMock.js └── scripts │ ├── BabelRegister.js │ ├── CheckBuildsExist.js │ ├── CheckNativeDep.js │ ├── CheckNodeEnv.js │ ├── CheckPortInUse.js │ ├── CheckYarn.js │ ├── DeleteSourceMaps.js │ └── ElectronRebuild.js ├── package.json ├── postcss.config.js ├── resources ├── icon.icns ├── icon.ico ├── icon.png └── icons │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 512x512.png │ └── 64x64.png ├── setupTests.js ├── tailwind.config.js ├── test ├── components │ ├── CurrentFilters │ │ ├── CurrentFilters.spec.tsx │ │ └── __snapshots__ │ │ │ └── CurrentFilters.spec.tsx.snap │ ├── Filter │ │ ├── Filter.spec.tsx │ │ └── __snapshots__ │ │ │ └── Filter.spec.tsx.snap │ ├── FilterInput │ │ ├── FilterInput.spec.tsx │ │ └── __snapshots__ │ │ │ └── FilterInput.spec.tsx.snap │ ├── FilterOptions │ │ ├── FilterOptions.spec.tsx │ │ └── __snapshots__ │ │ │ └── FilterOptions.spec.tsx.snap │ ├── FilterStreamOptions │ │ ├── FilterStreamOptions.spec.tsx │ │ └── __snapshots__ │ │ │ └── FilterStreamOptions.spec.tsx.snap │ ├── FilterTimeOptions │ │ ├── FilterTimeOptions.spec.tsx │ │ └── __snapshots__ │ │ │ └── FilterTimeOptions.spec.tsx.snap │ ├── LogsRows │ │ ├── LogsRows.spec.tsx │ │ └── __snapshots__ │ │ │ └── LogsRows.spec.tsx.snap │ ├── LogsTable │ │ ├── LogsTable.spec.tsx │ │ └── __snapshots__ │ │ │ └── LogsTable.spec.tsx.snap │ └── Navbar │ │ ├── Navbar.spec.tsx │ │ └── __snapshots__ │ │ └── Navbar.spec.tsx.snap └── e2e │ └── HomePage.e2e.ts ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | .eslintcache 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | # OSX 31 | .DS_Store 32 | 33 | # App packaged 34 | release 35 | app/main.prod.js 36 | app/main.prod.js.map 37 | app/renderer.prod.js 38 | app/renderer.prod.js.map 39 | app/style.css 40 | app/style.css.map 41 | dist 42 | dll 43 | main.js 44 | main.js.map 45 | 46 | .idea 47 | npm-debug.log.* 48 | .*.dockerfile 49 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.exe binary 3 | *.png binary 4 | *.jpg binary 5 | *.jpeg binary 6 | *.ico binary 7 | *.icns binary 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [electron-react-boilerplate, amilajack] 4 | patreon: amilajack 5 | open_collective: electron-react-boilerplate-594 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: You're having technical issues. 🐞 4 | labels: 'bug' 5 | --- 6 | 7 | 8 | 9 | ## Prerequisites 10 | 11 | 12 | 13 | - [ ] Using yarn 14 | - [ ] Using an up-to-date [`master` branch](https://github.com/electron-react-boilerplate/electron-react-boilerplate/tree/master) 15 | - [ ] Using latest version of devtools. [Check the docs for how to update](https://electron-react-boilerplate.js.org/docs/dev-tools/) 16 | - [ ] Tried solutions mentioned in [#400](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/400) 17 | - [ ] For issue in production release, add devtools output of `DEBUG_PROD=true yarn build && yarn start` 18 | 19 | ## Expected Behavior 20 | 21 | 22 | 23 | ## Current Behavior 24 | 25 | 26 | 27 | ## Steps to Reproduce 28 | 29 | 30 | 31 | 32 | 1. 33 | 34 | 2. 35 | 36 | 3. 37 | 38 | 4. 39 | 40 | ## Possible Solution (Not obligatory) 41 | 42 | 43 | 44 | ## Context 45 | 46 | 47 | 48 | 49 | 50 | ## Your Environment 51 | 52 | 53 | 54 | - Node version : 55 | - electron-react-boilerplate version or branch : 56 | - Operating System and version : 57 | - Link to your project : 58 | 59 | 68 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-Question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question.❓ 4 | labels: 'question' 5 | --- 6 | 7 | ## Summary 8 | 9 | 10 | 11 | 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: You want something added to the boilerplate. 🎉 4 | labels: 'enhancement' 5 | --- 6 | 7 | 16 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | requiredHeaders: 2 | - Prerequisites 3 | - Expected Behavior 4 | - Current Behavior 5 | - Possible Solution 6 | - Your Environment 7 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pr 8 | - discussion 9 | - e2e 10 | - enhancement 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | release: 7 | runs-on: ${{ matrix.os }} 8 | 9 | strategy: 10 | matrix: 11 | os: [macos-latest, windows-latest, ubuntu-latest] 12 | 13 | steps: 14 | - name: Check out Git repository 15 | uses: actions/checkout@v1 16 | 17 | - name: Install Node.js, NPM and Yarn 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 14 21 | 22 | - name: yarn install 23 | run: | 24 | yarn install --frozen-lockfile --network-timeout 300000 25 | 26 | - name: yarn test 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | run: | 30 | yarn package-ci 31 | yarn lint 32 | yarn tsc 33 | yarn test 34 | yarn build-e2e 35 | 36 | - if: matrix.os == 'ubuntu-latest' 37 | run: | 38 | Xvfb :99 & 39 | disown -ar 40 | echo "::set-env name=DISPLAY:::99" 41 | 42 | # TODO: Testcafe e2e are broken because of: 43 | # https://github.com/DevExpress/testcafe/issues/4512 44 | # Tests are currently broken on linux and macos 45 | - if: matrix.os == 'windows-latest' 46 | run: | 47 | yarn test-e2e 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | .eslintcache 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | # OSX 31 | .DS_Store 32 | 33 | # App packaged 34 | release 35 | app/main.prod.js 36 | app/main.prod.js.map 37 | app/renderer.prod.js 38 | app/renderer.prod.js.map 39 | app/style.css 40 | app/style.css.map 41 | dist 42 | dll 43 | main.js 44 | main.js.map 45 | 46 | .idea 47 | npm-debug.log.* 48 | *.css.d.ts 49 | *.sass.d.ts 50 | *.scss.d.ts 51 | 52 | # SQLite3 53 | *.sqlite3 54 | 55 | # Vim 56 | *.swp 57 | *.swo 58 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "useTabs": false, 7 | "bracketSpacing": true, 8 | "arrowParens": "always" 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "EditorConfig.EditorConfig", 4 | "msjsdiag.debugger-for-chrome" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Electron: Main", 8 | "protocol": "inspector", 9 | "runtimeExecutable": "yarn", 10 | "runtimeArgs": ["start-main-debug"], 11 | "preLaunchTask": "Start Webpack Dev" 12 | }, 13 | { 14 | "name": "Electron: Renderer", 15 | "type": "chrome", 16 | "request": "attach", 17 | "port": 9223, 18 | "webRoot": "${workspaceFolder}", 19 | "timeout": 15000 20 | } 21 | ], 22 | "compounds": [ 23 | { 24 | "name": "Electron: All", 25 | "configurations": ["Electron: Main", "Electron: Renderer"] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | ".babelrc": "jsonc", 4 | ".prettierrc": "jsonc", 5 | ".stylelintrc": "json", 6 | ".dockerignore": "ignore" 7 | }, 8 | 9 | "javascript.validate.enable": false, 10 | "javascript.format.enable": false, 11 | "typescript.format.enable": false, 12 | 13 | "search.exclude": { 14 | ".git": true, 15 | ".eslintcache": true, 16 | "app/dist": true, 17 | "app/main.prod.js": true, 18 | "app/main.prod.js.map": true, 19 | "bower_components": true, 20 | "dll": true, 21 | "release": true, 22 | "node_modules": true, 23 | "npm-debug.log.*": true, 24 | "test/**/__snapshots__": true, 25 | "yarn.lock": true, 26 | "*.{css,sass,scss}.d.ts": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "label": "Start Webpack Dev", 7 | "script": "start-renderer-dev", 8 | "options": { 9 | "cwd": "${workspaceFolder}" 10 | }, 11 | "isBackground": true, 12 | "problemMatcher": { 13 | "owner": "custom", 14 | "pattern": { 15 | "regexp": "____________" 16 | }, 17 | "background": { 18 | "activeOnStart": true, 19 | "beginsPattern": "Compiling\\.\\.\\.$", 20 | "endsPattern": "(Compiled successfully|Failed to compile)\\.$" 21 | } 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | GitHub 7 | GitHub issues 8 | GitHub last commit 9 | GitHub Repo stars 10 |

11 |
12 |

13 | Dockter - A low-overhead, open-source Docker log management tool | Product Hunt 14 |

15 | 16 | # Appendix 17 | 18 | [_What is Dockter_](#overview) 19 | [_Prerequisites_](#prerequisites) 20 | [_Quick Start_](#quick-start) 21 | [_Built With_](#built-with) 22 | [_Contributions_](#contributions) 23 | [_Testing_](#testing) 24 | [_Authors_](#authors) 25 | [_Beta_](#beta) 26 | 27 | # What is Dockter? 28 | 29 | > Dockter v0.1.0 is a low-overhead , open source Docker log management tool built for: 30 | > 31 | > - **Real-time Log Collection** - communicating with the Docker Engine API to access container streams and collect live logs 32 | > 33 | >
34 | >

35 | > 36 | >

37 | >
38 | > 39 | > - **Log Aggregation** - Storing container logs into a centralized database 40 | > 41 | >
42 | >

43 | > 44 | >

45 | >
46 | > 47 | > - **Filter & Search Capability** - Analyze and extract meaningful data through filter and search 48 | > 49 | >
50 | >

51 | > 52 | >

53 | >
54 | 55 | 59 | 60 | # Prerequisites 61 | 62 | > Ensure that you have the following installed and running on your machine: 63 | > 64 | > - [Docker Client](https://www.docker.com/get-started) 65 | > - [Running Docker Container(s)](https://docs.docker.com/get-started/) 66 | > - [MongoDB](https://docs.mongodb.com/manual/administration/install-community/) 67 | > - [Yarn Package Manager](https://classic.yarnpkg.com/en/docs/getting-started) 68 | 69 | # Quick Start 70 | 71 | > First, clone our repo by running either: 72 | > 73 | > ``` 74 | > git clone git@github.com:oslabs-beta/Dockter.git 75 | > ``` 76 | > 77 | > _or_ 78 | > 79 | > ``` 80 | > gh repo clone oslabs-beta/Dockter 81 | > ``` 82 | > 83 | > (_if you have the [Github CLI](https://cli.github.com/) installed_) 84 | > in your terminal. 85 | > Next, `cd` into your cloned Docktor repository and run the following command to install all necessary dependencies 86 | > 87 | > ``` 88 | > yarn 89 | > ``` 90 | > 91 | > Last, run this command to start Dockter on your machine. It's as easy as that! 92 | > 93 | > ``` 94 | > yarn start 95 | > ``` 96 | 97 | # Built With 98 | 99 | > - Docker - Platform as a Service 100 | > - React (Hooks) - Front-end Library 101 | > - Electron - Desktop Application Framework 102 | > - Tailwind CSS 103 | > - MongoDB - noSQL Document Oriented database 104 | > - Mongoose - Object Data Modeling Library 105 | > - Jest - Testing Framework 106 | > - Enzyme - React Testing Utility 107 | > - ESlint - Javascript Linting Tool 108 | > - Yarn - Package Manager 109 | 110 | # Contributions 111 | 112 | > Dockter welcomes any ideas and/or contributions to its codebase. Please click the button below to reach out to us at with any questions or even if you'd just like to talk shop. We'd love to hear from you! 113 | >
> _To run the application in development mode, please clone the Docktor repository to your local machine and run the following commands in your terminal:_ 114 | > 115 | > ``` 116 | > yarn 117 | > ``` 118 | > 119 | > ``` 120 | > yarn dev 121 | > ``` 122 | > 123 | >
124 | >

125 | > 126 | >

127 | 128 | # Testing 129 | 130 | _To perform tests on the Dockter codebase, please clone the repository to your local machine and run the following commands in your terminal:_ 131 | 132 | > ``` 133 | > yarn 134 | > ``` 135 | > 136 | > ``` 137 | > yarn build 138 | > ``` 139 | > 140 | > ``` 141 | > yarn test 142 | > ``` 143 | 144 | # Authors 145 | 146 | Benjamin Brower [@github](https://github.com/bbrower21) [@linkedIn](https://www.linkedin.com/in/ben-brower-80660073/) 147 | Anup Ramdass [@github](https://github.com/anrax) [@linkedIn](https://www.linkedin.com/in/anupramdass/) 148 | Nancy Koushoult [@github](https://github.com/noodlemonkey) [@linkedIn](https://www.linkedin.com/in/nancy-kousholt-a6a2071b9/) 149 | Kyle Whang [@github](https://github.com/shaisle) [@linkedIn](https://www.linkedin.com/in/kylewhang/) 150 | Samuel Kim [@github](https://github.com/samuy) [@linkedIn](https://www.linkedin.com/in/whalewhalewhale/) 151 | 152 | # Features In Beta 153 | 154 | _The Dockter team is dedicated to working around the clock to ensure proper maintainence and the addition of even more robust features. The following are coming soon:_ 155 | 156 | > - Analysis of Log Metrics - real-time log statistics will be available to the user on the Dockter dashboard in a visualized format with the capability to dynamically focus on logs that may require higher levels of oversight 157 | > - Container management - users will be able to remove, stop, and start containers from within the Dockter dashboard 158 | -------------------------------------------------------------------------------- /app/.testcafe-electron-rc: -------------------------------------------------------------------------------- 1 | { 2 | "mainWindowUrl": "./app.html", 3 | "appPath": "." 4 | } 5 | -------------------------------------------------------------------------------- /app/Routes.tsx: -------------------------------------------------------------------------------- 1 | /* eslint react/jsx-props-no-spreading: off */ 2 | import React from 'react'; 3 | import { Switch, Route } from 'react-router-dom'; 4 | import routes from './constants/routes.json'; 5 | import App from './containers/App'; 6 | import HomePage from './containers/LandingPage'; 7 | 8 | export default function Routes() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/app.global.css: -------------------------------------------------------------------------------- 1 | /* 2 | * @NOTE: Prepend a `~` to css file paths that are in your node_modules 3 | * See https://github.com/webpack-contrib/sass-loader#imports 4 | */ 5 | @import '~@fortawesome/fontawesome-free/css/all.css'; 6 | @tailwind base; 7 | @tailwind components; 8 | @tailwind utilities; 9 | -------------------------------------------------------------------------------- /app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dockter 6 | 20 | 21 | 22 |
23 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/app/app.icns -------------------------------------------------------------------------------- /app/assets/DockterLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/app/assets/DockterLogo.png -------------------------------------------------------------------------------- /app/assets/DockterLogoSM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/app/assets/DockterLogoSM.png -------------------------------------------------------------------------------- /app/assets/DockterScreenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/app/assets/DockterScreenshot.png -------------------------------------------------------------------------------- /app/assets/Playbutton.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/app/assets/Playbutton.gif -------------------------------------------------------------------------------- /app/assets/betterfilter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/app/assets/betterfilter.gif -------------------------------------------------------------------------------- /app/assets/scroll.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/app/assets/scroll.gif -------------------------------------------------------------------------------- /app/components/CurrentFilters.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const CurrentFilters = ({ filterOptions, setFilterOptions }) => { 4 | const buttons = []; 5 | const filterCategories = Object.keys(filterOptions); 6 | 7 | // Iterates over filterOption state 8 | filterCategories.forEach((filterCategory) => { 9 | const option = filterOptions[filterCategory]; 10 | 11 | // At each key check is property is an array 12 | if (Array.isArray(option) && option.length) { 13 | // If property is an array, make a button for each selection 14 | option.forEach((selection, i) => { 15 | buttons.push( 16 | 32 | ); 33 | }); 34 | } 35 | if (filterCategory === 'timestamp' && option.from) { 36 | buttons.push( 37 | 48 | ); 49 | } 50 | }); 51 | 52 | return ( 53 |
54 | {buttons} 55 |
56 | ); 57 | }; 58 | 59 | export default CurrentFilters; 60 | -------------------------------------------------------------------------------- /app/components/Filter.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import FilterInput from './FilterInput'; 3 | import FilterOptions from './FilterOptions'; 4 | import FilterStreamOptions from './FilterStreamOptions'; 5 | import FilterTimeOptions from './FilterTimeOptions'; 6 | import CurrentFilters from './CurrentFilters'; 7 | 8 | const Filter = ({ filterOptions, setFilterOptions }) => { 9 | // A piece of local state to render the correct options to filter by 10 | const [selection, setSelection] = useState(''); 11 | 12 | // Handles userInput into conditionally rendered input elements 13 | // Userinput changes with each key inputted 14 | // On submit, sets filter options at the current text input 15 | const [userInput, setUserInput] = useState(''); 16 | const [fromTimestamp, setFromTimestamp] = useState({ date: '', time: '' }); 17 | const [toTimestamp, setToTimestamp] = useState({ date: '', time: '' }); 18 | 19 | // State to check if filter menu is open 20 | const [isOpen, setIsOpen] = useState(false); 21 | 22 | const removeAllFilters = () => { 23 | // TODO: Make this dynamic 24 | setFilterOptions({ 25 | container_id: [], 26 | container_name: [], 27 | container_image: [], 28 | status: [], 29 | stream: [], 30 | timestamp: { 31 | from: '', 32 | to: '', 33 | }, 34 | host_ip: [], 35 | host_port: [], 36 | log_level: [], 37 | }); 38 | }; 39 | 40 | return ( 41 |
42 |
43 |
44 |
45 | 46 | 73 | {isOpen && ( 74 | 79 | )} 80 | 81 |
82 | {isOpen && ( 83 | 84 | )} 85 |
86 | 87 | {/* Conditionally renders an input field only for the following options */} 88 | {(selection === 'container_id' || 89 | selection === 'container_name' || 90 | selection === 'container_image' || 91 | selection === 'status' || 92 | selection === 'host_ip' || 93 | selection === 'host_port' || 94 | selection === 'log_level') && ( 95 | 102 | )} 103 | 104 | {/* Conditionally renders stream options */} 105 | {selection === 'stream' && ( 106 | 110 | )} 111 | 112 | {/* conditionally renders timestamp options */} 113 | {/* TODO: check if there is a better way of implementing a timestamp input, currently very bulky and annoying UX */} 114 | {selection === 'timestamp' && ( 115 | 123 | )} 124 | 125 | 132 |
133 | 134 | 138 |
139 | ); 140 | }; 141 | 142 | export default Filter; 143 | -------------------------------------------------------------------------------- /app/components/FilterInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FilterInput = ({ 4 | selection, 5 | userInput, 6 | setUserInput, 7 | filterOptions, 8 | setFilterOptions, 9 | }) => { 10 | return ( 11 |
12 |
13 | { 20 | setUserInput(e.target.value); 21 | }} 22 | value={userInput} 23 | > 24 | 46 |
47 |
48 | ); 49 | }; 50 | 51 | export default FilterInput; 52 | -------------------------------------------------------------------------------- /app/components/FilterOptions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FilterOptions = ({ setSelection, setIsOpen }) => { 4 | // TODO: this should use the filterOptions defined in LogsContainer.tsx 5 | const filterOptions = [ 6 | 'container_id', 7 | 'container_name', 8 | 'container_image', 9 | 'host_port', 10 | 'stream', 11 | 'timestamp', 12 | ]; 13 | 14 | const filterOptionsAsComponents = filterOptions.map((filter) => { 15 | // Remove snakecase and capitalize first word of every filter to use as a label 16 | const str = filter.replace('_', ' '); 17 | const label = str[0].toUpperCase() + str.substring(1); 18 | return ( 19 | { 21 | const val = e.currentTarget.id.match(/(?<=-).*$/g)[0]; 22 | setSelection(val); 23 | setIsOpen(false); 24 | }} 25 | key={`filter-${filter}`} 26 | id={`filter-${filter}`} 27 | href="#" 28 | className="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" 29 | role="menuitem" 30 | > 31 | {label} 32 | 33 | ); 34 | }); 35 | 36 | return ( 37 |
38 |
39 |
45 | {filterOptionsAsComponents} 46 |
47 |
48 |
49 | ); 50 | }; 51 | 52 | export default FilterOptions; 53 | -------------------------------------------------------------------------------- /app/components/FilterStreamOptions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FilterStreamOptions = ({ filterOptions, setFilterOptions }) => { 4 | return ( 5 |
6 | {/* className="inline-flex justify-center w-full rounded-md border border-gray-300 px-4 py-2 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800 transition ease-in-out duration-150" */} 7 | 25 |
26 | ); 27 | }; 28 | 29 | export default FilterStreamOptions; 30 | -------------------------------------------------------------------------------- /app/components/FilterTimeOptions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FilterTimeOptions = ({ 4 | filterOptions, 5 | setFilterOptions, 6 | fromTimestamp, 7 | setFromTimestamp, 8 | toTimestamp, 9 | setToTimestamp, 10 | }) => { 11 | const date = new Date(Date.now()); 12 | const today = date.toISOString().slice(0, 10); 13 | 14 | // TODO: Check if currentTime can/should update constantly (ex: with setTimeout) 15 | const currentTime = date.getHours() + ':' + date.getMinutes(); 16 | 17 | return ( 18 |
19 |
From:
20 | 25 | setFromTimestamp({ ...fromTimestamp, date: e.target.value }) 26 | } 27 | > 28 | { 33 | setFromTimestamp({ ...fromTimestamp, time: e.target.value }); 34 | }} 35 | > 36 |
To:
37 | 42 | setFromTimestamp({ ...fromTimestamp, date: e.target.value }) 43 | } 44 | > 45 | 50 | setToTimestamp({ ...fromTimestamp, time: e.target.value }) 51 | } 52 | > 53 | 79 |
80 | ); 81 | }; 82 | 83 | export default FilterTimeOptions; 84 | -------------------------------------------------------------------------------- /app/components/LiveLogController.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { ipcRenderer } from 'electron'; 3 | 4 | const LiveLogController = ({ filterOptions }) => { 5 | const [collectingLiveLogs, setCollectingLiveLogs] = useState('true'); 6 | 7 | return ( 8 | <> 9 |
10 | 30 |
31 |
32 | {collectingLiveLogs ? ( 33 | <> 34 | Collecting Live Logs 35 | 36 | 37 | 38 | 39 | 40 | ) : ( 41 | <> 42 | 43 | Live Logs Collection Paused 44 | 45 | 46 | 47 | 48 | 49 | )} 50 |
51 | 52 | ); 53 | }; 54 | 55 | export default LiveLogController; 56 | -------------------------------------------------------------------------------- /app/components/LogsRows.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Convert from 'ansi-to-html'; 3 | import DOMPurify from 'dompurify'; 4 | import parse from 'html-react-parser'; 5 | 6 | const convert = new Convert(); 7 | 8 | const LogsRows = ({ logs, filterOptions }) => { 9 | return logs.map((log, i) => { 10 | const { 11 | message, 12 | container_id, 13 | container_name, 14 | container_image, 15 | timestamp, 16 | stream, 17 | status, 18 | ports, 19 | } = log._doc; 20 | 21 | // Convert array of ports to a comma diliminated string 22 | const portsStr = ports 23 | .map((port) => { 24 | const { IP, PrivatePort, PublicPort, Type } = port; 25 | 26 | return IP && PrivatePort 27 | ? `${IP}:${PublicPort} -> ${PrivatePort}/${Type}` 28 | : `${PrivatePort}/${Type}`; 29 | }) 30 | .join(', '); 31 | 32 | // Grabbing the keys from filterOptions prop turning into array 33 | const keys = Object.keys(filterOptions); 34 | let render = false; 35 | 36 | const filterProps = []; 37 | keys.forEach((key) => { 38 | if (key === 'timestamp' && filterOptions[key].to) filterProps.push(key); 39 | else if ( 40 | filterOptions[key].length !== 0 && 41 | key !== 'timestamp' && 42 | key !== 'search' 43 | ) 44 | filterProps.push(key); 45 | }); 46 | 47 | // // Check if a user has set any filterOptions that will exclude the current log 48 | filterProps.forEach((activeFilter) => { 49 | let currentOption = filterOptions[activeFilter]; 50 | 51 | // Time check: 52 | // Time has a 'to' and 'from' property 53 | if ( 54 | activeFilter === 'timestamp' && 55 | currentOption.to && 56 | currentOption.from 57 | ) { 58 | // Turn time type into UNIX time format 59 | // Slicing to take off milliseconds 60 | const date = new Date(timestamp.slice(0, 19)).getTime(); 61 | const from = new Date(currentOption.from).getTime(); 62 | const to = new Date(currentOption.to).getTime(); 63 | 64 | // Date is from the log 65 | // 'from' and 'to' is from user input 66 | if (date > from || date < to) render = true; 67 | } 68 | 69 | // Line is checking to see if incoming logs apply to certain filter criteria for live rendering 70 | if ( 71 | currentOption.length && 72 | currentOption.includes(log._doc[activeFilter]) 73 | ) 74 | render = true; 75 | }); 76 | 77 | //if there are no active filters, render the row 78 | if (!filterProps.length) render = true; 79 | if (!render) return null; 80 | 81 | // Converts ansi escape codes to html styling, then sanitize (XSS), then parse into React component 82 | const messageWithANSI = message 83 | ? parse(DOMPurify.sanitize(convert.toHtml(message))) 84 | : ''; 85 | 86 | // TODO: Handle .slice error message 87 | // TODO: Decide if containerId slice should happen server-side 88 | return ( 89 |
90 |
91 |
92 | {timestamp ? new Date(timestamp).toUTCString() : ''} 93 |
94 |
95 |
96 |
97 | {messageWithANSI} 98 |
99 |
100 |
{ 103 | document.execCommand('copy'); 104 | }} 105 | > 106 | {container_id ? container_id : ''} 107 |
108 |
{ 111 | document.execCommand('copy'); 112 | }} 113 | > 114 | {container_name ? container_name : ''} 115 |
116 |
{ 119 | document.execCommand('copy'); 120 | }} 121 | > 122 | {container_image ? container_image : ''} 123 |
124 |
125 | {portsStr} 126 |
127 |
128 | 135 | {stream ? stream : ''} 136 | 137 |
138 |
139 | ); 140 | }); 141 | }; 142 | 143 | export default LogsRows; 144 | -------------------------------------------------------------------------------- /app/components/LogsTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef, useMemo } from 'react'; 2 | import { ipcRenderer } from 'electron'; 3 | import LogsRows from '../components/LogsRows'; 4 | import InfiniteScroll from 'react-infinite-scroll-component'; 5 | 6 | const LogsTable = ({ filterOptions }) => { 7 | const [showScrollToTopBtn, setShowScrollToTopBtn] = useState(false); 8 | const [scrollForMoreLogs, setScrollForMoreLogs] = useState(true); 9 | const [newLog, setNewLog] = useState({ 10 | _doc: { 11 | ports: [], 12 | _id: '', 13 | message: '', 14 | container_id: '', 15 | container_name: '', 16 | container_image: '', 17 | timestamp: '', 18 | stream: '', 19 | status: '', 20 | }, 21 | }); 22 | // Contains an array of all the logs that we will render 23 | const [logs, setLogs] = useState([]); 24 | const tableBody = useRef(null); 25 | 26 | useEffect(() => { 27 | ipcRenderer.on('newLog', (event, newLog) => { 28 | setNewLog(newLog); 29 | }); 30 | 31 | ipcRenderer.on('reply-filter', (event, newLogs) => { 32 | setLogs(newLogs); 33 | }); 34 | 35 | ipcRenderer.on('search-reply', (event, newLogs) => { 36 | setLogs(newLogs); 37 | }); 38 | }, []); 39 | 40 | useEffect(() => { 41 | setLogs([newLog, ...logs]); 42 | }, [newLog]); 43 | 44 | // Filter logic 45 | useEffect(() => { 46 | ipcRenderer.send('filter', filterOptions); 47 | }, [filterOptions]); 48 | 49 | return ( 50 |
51 |
52 |
53 |
54 | Timestamp 55 |
56 |
57 | Log 58 |
59 |
60 | Container ID 61 |
62 |
63 | Name 64 |
65 |
66 | Image 67 |
68 |
69 | Host Port 70 |
71 |
72 | Stream 73 |
74 |
75 |
80 | { 84 | ipcRenderer.send('scroll', { 85 | filterOptions, 86 | nin: logs.map((log) => { 87 | return log._doc._id; 88 | }), 89 | }); 90 | ipcRenderer.on('scroll-reply', (event, arg) => { 91 | // Args is either going to be a boolean or a more logs 92 | // If typeof arg is bool, then we setScrollForMoreLogs(false); 93 | if (typeof arg === 'boolean') setScrollForMoreLogs(false); 94 | // Else, we update logs with setLogs 95 | else setLogs([...logs, ...arg]); 96 | }); 97 | }} 98 | scrollThreshold={1} 99 | scrollableTarget="logs-container" 100 | hasMore={scrollForMoreLogs} 101 | loader={

Loading...

} 102 | endMessage={

Logs are fully loaded

} 103 | onScroll={() => { 104 | if (tableBody.current.scrollTop > 20) setShowScrollToTopBtn(true); 105 | else setShowScrollToTopBtn(false); 106 | }} 107 | > 108 | {useMemo( 109 | () => ( 110 | 111 | ), 112 | [logs] 113 | )} 114 |
115 |
116 | {showScrollToTopBtn && ( 117 | 129 | )} 130 |
131 |
132 | ); 133 | }; 134 | 135 | export default LogsTable; 136 | -------------------------------------------------------------------------------- /app/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LiveLogController from './LiveLogController'; 3 | 4 | const Navbar = ({ filterOptions }) => { 5 | return ( 6 | 33 | ); 34 | }; 35 | 36 | export default Navbar; 37 | -------------------------------------------------------------------------------- /app/components/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | import React, { useState } from 'react'; 3 | 4 | const SearchBar = ({ filterOptions, setFilterOptions }) => { 5 | const [search, setSearch] = useState(''); 6 | // const [searchAllowed, setCanSearch] = useState(''); 7 | 8 | return ( 9 |
10 |
11 | { 18 | setSearch(e.target.value); 19 | }} 20 | value={search} 21 | > 22 | 45 |
46 |
47 | ); 48 | }; 49 | 50 | export default SearchBar; 51 | -------------------------------------------------------------------------------- /app/components/Utilities.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Filter from './Filter'; 3 | import SearchBar from './SearchBar'; 4 | 5 | const Utilities = ({ filterOptions, setFilterOptions }) => { 6 | return ( 7 |
8 | 12 | 16 |
17 | ); 18 | }; 19 | 20 | export default Utilities; 21 | -------------------------------------------------------------------------------- /app/constants/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "HOME": "/" 3 | } -------------------------------------------------------------------------------- /app/containers/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | 3 | type Props = { 4 | children: ReactNode; 5 | }; 6 | 7 | const App = (props: Props) => { 8 | const { children } = props; 9 | return <>{children}; 10 | }; 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /app/containers/LandingPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import LogsContainer from './LogsContainer'; 3 | import Navbar from '../components/Navbar'; 4 | // TODO: Investigate where to put IPC Event Emitter for channel ready 5 | import { ipcRenderer } from 'electron'; 6 | 7 | export default function LandingPage() { 8 | ipcRenderer.send('ready'); 9 | const [filterOptions, setFilterOptions] = useState({ 10 | container_id: [], 11 | container_name: [], 12 | container_image: [], 13 | status: [], 14 | stream: [], 15 | timestamp: { 16 | from: '', 17 | to: '', 18 | }, 19 | host_ip: [], 20 | host_port: [], 21 | search: '', 22 | }); 23 | 24 | return ( 25 | <> 26 | 27 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /app/containers/LogsContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Utilities from '../components/Utilities'; 3 | import LogsTable from '../components/LogsTable.tsx'; 4 | 5 | const LogsContainer = ({ filterOptions, setFilterOptions }) => { 6 | return ( 7 |
8 | 12 | 13 |
14 | ); 15 | }; 16 | 17 | export default LogsContainer; 18 | -------------------------------------------------------------------------------- /app/containers/Root.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { ConnectedRouter } from 'connected-react-router'; 4 | import { hot } from 'react-hot-loader/root'; 5 | import { History } from 'history'; 6 | import { Store } from '../store'; 7 | import Routes from '../Routes'; 8 | 9 | type Props = { 10 | store: Store; 11 | history: History; 12 | }; 13 | 14 | const Root = ({ store, history }: Props) => ( 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | 22 | export default hot(Root); 23 | -------------------------------------------------------------------------------- /app/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { AppContainer as ReactHotAppContainer } from 'react-hot-loader'; 4 | import { history, configuredStore } from './store'; 5 | import './app.global.css'; 6 | 7 | const store = configuredStore(); 8 | 9 | const AppContainer = process.env.PLAIN_HMR ? Fragment : ReactHotAppContainer; 10 | 11 | document.addEventListener('DOMContentLoaded', () => { 12 | const Root = require('./containers/Root').default; 13 | render( 14 | 15 | 16 | , 17 | document.getElementById('root') 18 | ); 19 | }); 20 | -------------------------------------------------------------------------------- /app/main-process/db.ts: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const url = 'mongodb://127.0.0.1:27017/dockter'; 4 | 5 | mongoose 6 | .connect(url, { 7 | useNewUrlParser: true, 8 | useUnifiedTopology: true, 9 | useCreateIndex: true, 10 | useFindAndModify: false, 11 | }) 12 | .catch((err) => console.log(err)); 13 | 14 | const db = mongoose.connection; 15 | 16 | db.once('open', () => { 17 | console.log('Database connected: ', url); 18 | }); 19 | 20 | db.on('error', (err) => { 21 | console.error('connection error: ', url); 22 | }); 23 | 24 | export { db }; 25 | -------------------------------------------------------------------------------- /app/main-process/filter.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { ipcMain } from 'electron'; 3 | import { db } from './db.ts'; 4 | import Log from '../models/logModel'; 5 | 6 | //TODO: Figure out better way to instantiate database 7 | console.log('DB: ', db); 8 | 9 | ipcMain.on('filter', (event, arg) => { 10 | console.log('this is arg', arg); 11 | const filterOptions = arg; 12 | const filterProps = []; 13 | Object.keys(filterOptions).forEach((key) => { 14 | if (key === 'timestamp' && filterOptions[key].to) filterProps.push(key); 15 | else if (filterOptions[key].length !== 0 && key !== 'timestamp') 16 | filterProps.push(key); 17 | }); 18 | // Need some sort of logic within this conditional in order to not throw Mongo ERROR 19 | if (filterProps.length === 0) { 20 | Log.find({}) 21 | .sort({ timestamp: -1 }) 22 | .limit(50) 23 | .exec((err, logs) => { 24 | if (err) console.log(err); 25 | else { 26 | event.reply( 27 | 'reply-filter', 28 | logs.map((log) => { 29 | return { 30 | ...log, 31 | _id: log._id.toString(), 32 | _doc: { ...log._doc, _id: log._id.toString() }, 33 | }; 34 | }) 35 | ); 36 | } 37 | }); 38 | } else { 39 | const query = {}; 40 | const filterQuery = []; 41 | let searchFlag = false; 42 | for (let i = 0; i < filterProps.length; i++) { 43 | if (filterProps[i] === 'timestamp') { 44 | filterQuery.push({ 45 | timestamp: { 46 | $gte: new Date(filterOptions.timestamp.from), 47 | $lte: new Date(filterOptions.timestamp.to), 48 | }, 49 | }); 50 | break; 51 | } 52 | if (filterProps[i] === 'private_port') { 53 | for (let j = 0; j < filterOptions.private_port.length; j++) { 54 | filterQuery.push({ 55 | 'ports.PrivatePort': parseInt(filterOptions.private_port[j]), 56 | }); 57 | } 58 | break; 59 | } 60 | 61 | if (filterProps[i] === 'public_port') { 62 | for (let j = 0; j < filterOptions.public_port.length; j++) { 63 | filterQuery.push({ 64 | 'ports.PublicPort': parseInt(filterOptions.public_port[j]), 65 | }); 66 | } 67 | break; 68 | } 69 | if (filterProps[i] === 'host_ip') { 70 | for (let j = 0; j < filterOptions.host_ip.length; j++) { 71 | filterQuery.push({ 'ports.IP': filterOptions.host_ip[j] }); 72 | } 73 | break; 74 | } 75 | if (filterProps[i] === 'search') { 76 | searchFlag = true; 77 | break; 78 | } 79 | for (let j = 0; j < filterOptions[filterProps[i]].length; j++) { 80 | filterQuery.push({ 81 | [filterProps[i]]: filterOptions[filterProps[i]][j], 82 | }); 83 | } 84 | } 85 | 86 | if (filterQuery.length) query.$and = filterQuery; 87 | if (searchFlag) query.$text = { $search: filterOptions.search }; 88 | 89 | Log.find(query) 90 | .sort({ timestamp: -1 }) 91 | .limit(100) 92 | .exec((err, logs) => { 93 | if (err) { 94 | console.log('ERROR HYD', err); 95 | } else { 96 | event.reply( 97 | 'reply-filter', 98 | logs.map((log) => { 99 | return { 100 | ...log, 101 | _id: log._id.toString(), 102 | _doc: { ...log._doc, _id: log._id.toString() }, 103 | }; 104 | }) 105 | ); 106 | } 107 | }); 108 | } 109 | }); 110 | -------------------------------------------------------------------------------- /app/main-process/liveLogs.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain, webContents } from 'electron'; 2 | import stream from 'stream'; 3 | import path from 'path'; 4 | import Docker from 'dockerode'; 5 | import Log from '../models/logModel'; 6 | import Container from '../models/containerModel'; 7 | import { 8 | applicationStartTime, 9 | applicationStartTimeUnix, 10 | pastLogCollectionComplete, 11 | } from './startup'; 12 | 13 | // Connects dockerode to this path to open up communication with docker api 14 | const scktPath = 15 | process.platform === 'win32' 16 | ? '//./pipe/docker_engine' 17 | : '/var/run/docker.sock'; 18 | const docker = new Docker({ socketPath: scktPath }); 19 | 20 | async function collectLiveLogs() { 21 | // getAllWebContents gives us all the open windows within electron 22 | const content = webContents.getAllWebContents().reduce((window, curr) => { 23 | const appWindow = `file:\/\/${path.resolve(__dirname, '../app')}`; 24 | const reg = new RegExp(appWindow, 'g'); 25 | return curr.getURL().match(reg) ? curr : window; 26 | }); 27 | 28 | try { 29 | const containers = await docker.listContainers({ all: true }); 30 | const liveLogStreams = await containers.map(async (container) => { 31 | const { Id, Image, Status, Names, Ports } = container; 32 | 33 | // Remove logs where timestamp >= timeSinceLastLog to avoid duplication 34 | await Log.deleteMany({ 35 | container_id: Id, 36 | timestamp: { $gte: applicationStartTime }, 37 | }); 38 | 39 | const c = docker.getContainer(container.Id); 40 | 41 | // Initiate following logs for container 42 | const log = await c.logs({ 43 | follow: true, 44 | stdout: true, 45 | stderr: true, 46 | timestamps: true, 47 | since: applicationStartTimeUnix, 48 | }); 49 | 50 | if (log) { 51 | const stdout = new stream.PassThrough(); 52 | const stderr = new stream.PassThrough(); 53 | 54 | c.modem.demuxStream(log, stdout, stderr); 55 | stdout.on('data', async (chunk) => { 56 | const chunkString = chunk.toString(); 57 | const newLog = { 58 | message: chunkString.slice(30), 59 | container_id: Id, 60 | container_name: Names[0], 61 | container_image: Image, 62 | timestamp: new Date(chunkString.slice(0, 30)), 63 | stream: 'stdout', 64 | status: Status, 65 | ports: Ports, 66 | }; 67 | 68 | const newLogToSend = await Log.findOneAndUpdate( 69 | newLog, 70 | { $set: newLog }, 71 | { upsert: true, new: true } 72 | ); 73 | 74 | // Container should only update if hidden-log collector has finished collecting past logs 75 | if (pastLogCollectionComplete[Id]) { 76 | await Container.findOneAndUpdate( 77 | { container_id: Id }, 78 | { $set: { last_log: new Date(chunkString.slice(0, 30)) } }, 79 | { upsert: true, new: true } 80 | ); 81 | } 82 | content.send('newLog', { 83 | ...newLogToSend, 84 | _id: newLogToSend._id.toString(), 85 | _doc: { 86 | ...newLogToSend._doc, 87 | _id: newLogToSend._doc._id.toString(), 88 | }, 89 | }); 90 | }); 91 | 92 | stderr.on('data', async (chunk) => { 93 | const chunkString = chunk.toString(); 94 | const newLog = { 95 | message: chunkString.slice(30), 96 | container_id: Id, 97 | container_name: Names[0], 98 | container_image: Image, 99 | timestamp: new Date(chunkString.slice(0, 30)), 100 | stream: 'stderr', 101 | status: Status, 102 | ports: Ports, 103 | }; 104 | 105 | const newLogToSend = await Log.findOneAndUpdate( 106 | newLog, 107 | { $set: newLog }, 108 | { upsert: true, new: true } 109 | ); 110 | 111 | // Container should only update if hidden-log collector has finished collecting past logs 112 | if (pastLogCollectionComplete[Id]) { 113 | await Container.findOneAndUpdate( 114 | { container_id: Id }, 115 | { $set: { last_log: new Date(chunkString.slice(0, 30)) } }, 116 | { upsert: true, new: true } 117 | ); 118 | } 119 | 120 | content.send('newLog', { 121 | ...newLogToSend, 122 | _id: newLogToSend._id.toString(), 123 | _doc: { 124 | ...newLogToSend._doc, 125 | _id: newLogToSend._doc._id.toString(), 126 | }, 127 | }); 128 | }); 129 | 130 | return [stdout, stderr]; 131 | } 132 | }); 133 | 134 | const streams = await Promise.all(liveLogStreams); 135 | 136 | ipcMain.on('pauseLiveLogs', () => { 137 | console.log('pause all streams'); 138 | streams.forEach(([stdout, stderr]) => { 139 | stdout.pause(); 140 | stderr.pause(); 141 | }); 142 | }); 143 | 144 | ipcMain.on('resumeLiveLogs', () => { 145 | console.log('resume all streams'); 146 | streams.forEach(([stdout, stderr]) => { 147 | stdout.resume(); 148 | stderr.resume(); 149 | }); 150 | }); 151 | } catch (err) { 152 | console.log(err); 153 | } 154 | } 155 | 156 | export default collectLiveLogs; 157 | -------------------------------------------------------------------------------- /app/main-process/scroll.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | import Log from '../models/logModel'; 3 | 4 | ipcMain.on('scroll', async (event, arg) => { 5 | const { filterOptions, nin } = arg; 6 | const filterProps = []; 7 | 8 | const totalAmtOfLogs = await Log.find({}).countDocuments(); 9 | const hasNoMoreLogs = nin.length === totalAmtOfLogs; 10 | 11 | if (hasNoMoreLogs) { 12 | event.reply('scroll-reply', hasNoMoreLogs); 13 | } else { 14 | // Only run query if hasNoMoreLogs is false 15 | Object.keys(filterOptions).forEach((key) => { 16 | if (key === 'timestamp' && filterOptions[key].to) filterProps.push(key); 17 | else if (filterOptions[key].length !== 0 && key !== 'timestamp') 18 | filterProps.push(key); 19 | }); 20 | if (!filterProps.length) { 21 | Log.find({ _id: { $nin: nin } }) 22 | .sort({ timestamp: -1 }) 23 | .limit(100) 24 | .exec((err, logs) => { 25 | if (err) console.log(err); 26 | else { 27 | const scrollReply = logs.map((log) => { 28 | return { 29 | ...log, 30 | _id: log._id.toString(), 31 | _doc: { ...log._doc, _id: log._id.toString() }, 32 | }; 33 | }); 34 | event.reply('scroll-reply', scrollReply); 35 | } 36 | }); 37 | } else { 38 | const query = {}; 39 | const filterQuery = []; 40 | let searchFlag = false; 41 | for (let i = 0; i < filterProps.length; i++) { 42 | if (filterProps[i] === 'timestamp') { 43 | filterQuery.push({ 44 | timestamp: { 45 | $gte: new Date(filterOptions.timestamp.from), 46 | $lte: new Date(filterOptions.timestamp.to), 47 | }, 48 | }); 49 | break; 50 | } 51 | if (filterProps[i] === 'private_port') { 52 | for (let j = 0; j < filterOptions.private_port.length; j++) { 53 | filterQuery.push({ 54 | 'ports.PrivatePort': parseInt(filterOptions.private_port[j]), 55 | }); 56 | } 57 | break; 58 | } 59 | 60 | if (filterProps[i] === 'public_port') { 61 | for (let j = 0; j < filterOptions.public_port.length; j++) { 62 | filterQuery.push({ 63 | 'ports.PublicPort': parseInt(filterOptions.public_port[j]), 64 | }); 65 | } 66 | break; 67 | } 68 | if (filterProps[i] === 'host_ip') { 69 | for (let j = 0; j < filterOptions.host_ip.length; j++) { 70 | filterQuery.push({ 'ports.IP': filterOptions.host_ip[j] }); 71 | } 72 | break; 73 | } 74 | if (filterProps[i] === 'search') { 75 | searchFlag = true; 76 | break; 77 | } 78 | for (let j = 0; j < filterOptions[filterProps[i]].length; j++) { 79 | filterQuery.push({ 80 | [filterProps[i]]: filterOptions[filterProps[i]][j], 81 | }); 82 | } 83 | } 84 | 85 | if (filterQuery.length) query.$or = filterQuery; 86 | if (searchFlag) query.$text = { $search: filterOptions.search }; 87 | 88 | Log.find({ ...query, _id: { $nin: nin } }) 89 | .sort({ timestamp: -1 }) 90 | .limit(100) 91 | .exec((err, logs) => { 92 | if (err) { 93 | console.log('ERROR HYD', err); 94 | } else { 95 | event.reply( 96 | 'scroll-reply', 97 | logs.map((log) => { 98 | return { 99 | ...log, 100 | _id: log._id.toString(), 101 | _doc: { ...log._doc, _id: log._id.toString() }, 102 | }; 103 | }) 104 | ); 105 | } 106 | }); 107 | } 108 | } 109 | }); 110 | -------------------------------------------------------------------------------- /app/main-process/startup.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | import stream from 'stream'; 3 | import Docker from 'dockerode'; 4 | import Log from '../models/logModel'; 5 | import Container from '../models/containerModel'; 6 | import collectLiveLogs from './liveLogs'; 7 | 8 | // Purpose: Before application begins, this process collects logs that have been generated while the application was closed 9 | 10 | // Connects dockerode to this path to open up communication with docker api 11 | const scktPath = 12 | process.platform === 'win32' 13 | ? '//./pipe/docker_engine' 14 | : '/var/run/docker.sock'; 15 | const docker = new Docker({ socketPath: scktPath }); 16 | 17 | // Keeps track of whether a container has past logs that have to be collected 18 | const pastLogCollectionComplete = {}; 19 | 20 | // Global app start time reference 21 | const applicationStartTime = new Date(); 22 | const applicationStartTimeUnix = Math.floor( 23 | applicationStartTime.valueOf() / 1000 24 | ); 25 | 26 | // Listens for 'ready' event to be sent from the front end (landingpage.tsx) and fires 27 | ipcMain.on('ready', async (event, arg) => { 28 | const containers = await docker.listContainers({ all: true }); 29 | await Promise.all( 30 | containers.map(async (container) => { 31 | const { Id, Image, Status, Names, Ports } = container; 32 | const doesExist = await Container.exists({ container_id: container.Id }); 33 | 34 | // Create document for container if container does not exist 35 | if (!doesExist) { 36 | await Container.create({ 37 | container_id: Id, 38 | last_log: new Date(0).toString(), 39 | }); 40 | } 41 | 42 | // Add current container to pastLogCollectionComplete tracker 43 | // false: there are past logs to collect 44 | // true: there are no more past logs to collect 45 | pastLogCollectionComplete[Id] = false; 46 | 47 | const result = await Container.find({ container_id: container.Id }); 48 | // Convert last_log into a js Date 49 | const timeSinceLastLog = new Date(result[0].last_log); 50 | // Degrade precision for Dockerode .logs method 51 | const timeSinceLastLogUnix = Math.floor( 52 | timeSinceLastLog.valueOf() / 1000 53 | ); 54 | 55 | // Remove logs where timestamp >= timeSinceLastLog to avoid duplication 56 | await Log.deleteMany({ 57 | container_id: Id, 58 | timestamp: { $gte: timeSinceLastLog }, 59 | }); 60 | 61 | const c = docker.getContainer(container.Id); 62 | 63 | // Initiate following logs for container 64 | const log = await c.logs({ 65 | follow: true, 66 | stdout: true, 67 | stderr: true, 68 | timestamps: true, 69 | since: timeSinceLastLogUnix, 70 | until: applicationStartTimeUnix, 71 | }); 72 | 73 | if (log) { 74 | const stdout = new stream.PassThrough(); 75 | const stderr = new stream.PassThrough(); 76 | 77 | c.modem.demuxStream(log, stdout, stderr); 78 | stdout.on('data', async (chunk) => { 79 | const chunkString = chunk.toString(); 80 | const newLog = { 81 | message: chunkString.slice(30), 82 | container_id: Id, 83 | container_name: Names[0], 84 | container_image: Image, 85 | timestamp: new Date(chunkString.slice(0, 30)), 86 | stream: 'stdout', 87 | status: Status, 88 | ports: Ports, 89 | }; 90 | 91 | // Edge case where log's timestamp === applicationStartTime 92 | const finishedCollectingPastLogs = 93 | new Date(chunkString.slice(0, 30)) >= applicationStartTime; 94 | 95 | if (!finishedCollectingPastLogs) { 96 | await Log.findOneAndUpdate( 97 | newLog, 98 | { $set: newLog }, 99 | { upsert: true, new: true } 100 | ); 101 | 102 | await Container.findOneAndUpdate( 103 | { container_id: Id }, 104 | { $set: { last_log: new Date(chunkString.slice(0, 30)) } }, 105 | { upsert: true, new: true } 106 | ); 107 | } else { 108 | pastLogCollectionComplete[Id] = true; 109 | } 110 | }); 111 | 112 | stderr.on('data', async (chunk) => { 113 | const chunkString = chunk.toString(); 114 | const newLog = { 115 | message: chunkString.slice(30), 116 | container_id: Id, 117 | container_name: Names[0], 118 | container_image: Image, 119 | timestamp: new Date(chunkString.slice(0, 30)), 120 | stream: 'stderr', 121 | status: Status, 122 | ports: Ports, 123 | }; 124 | 125 | // Edge case where log's timestamp === applicationStartTime 126 | const finishedCollectingPastLogs = 127 | new Date(chunkString.slice(0, 30)) >= applicationStartTime; 128 | 129 | if (!finishedCollectingPastLogs) { 130 | await Log.findOneAndUpdate( 131 | newLog, 132 | { $set: newLog }, 133 | { upsert: true, new: true } 134 | ); 135 | 136 | await Container.findOneAndUpdate( 137 | { container_id: Id }, 138 | { $set: { last_log: new Date(chunkString.slice(0, 30)) } }, 139 | { upsert: true, new: true } 140 | ); 141 | } else { 142 | pastLogCollectionComplete[Id] = true; 143 | } 144 | }); 145 | 146 | log.on('close', () => { 147 | console.log( 148 | `All caught up! No more logs to collection for container: ${Names}` 149 | ); 150 | pastLogCollectionComplete[Id] = true; 151 | }); 152 | } 153 | }) 154 | ); 155 | collectLiveLogs(); 156 | }); 157 | 158 | export { 159 | pastLogCollectionComplete, 160 | applicationStartTime, 161 | applicationStartTimeUnix, 162 | }; 163 | -------------------------------------------------------------------------------- /app/main.dev.ts: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off, no-console: off */ 2 | 3 | /** 4 | * This module executes inside of electron's main process. You can start 5 | * electron renderer process from here and communicate with the other processes 6 | * through IPC. 7 | * 8 | * When running `yarn build` or `yarn build-main`, this file is compiled to 9 | * `./app/main.prod.js` using webpack. This gives us some performance wins. 10 | */ 11 | import 'core-js/stable'; 12 | import 'regenerator-runtime/runtime'; 13 | import path from 'path'; 14 | import { app, BrowserWindow } from 'electron'; 15 | import { autoUpdater } from 'electron-updater'; 16 | import log from 'electron-log'; 17 | import MenuBuilder from './menu'; 18 | import './main-process/liveLogs'; 19 | import './main-process/startup'; 20 | import './main-process/filter'; 21 | import './main-process/scroll'; 22 | 23 | export default class AppUpdater { 24 | constructor() { 25 | log.transports.file.level = 'info'; 26 | autoUpdater.logger = log; 27 | autoUpdater.checkForUpdatesAndNotify(); 28 | } 29 | } 30 | 31 | let mainWindow: BrowserWindow | null = null; 32 | let workerWindow: BrowserWindow | null = null; 33 | 34 | if (process.env.NODE_ENV === 'production') { 35 | const sourceMapSupport = require('source-map-support'); 36 | sourceMapSupport.install(); 37 | } 38 | 39 | if ( 40 | process.env.NODE_ENV === 'development' || 41 | process.env.DEBUG_PROD === 'true' 42 | ) { 43 | require('electron-debug')(); 44 | } 45 | 46 | const installExtensions = async () => { 47 | const installer = require('electron-devtools-installer'); 48 | const forceDownload = !!process.env.UPGRADE_EXTENSIONS; 49 | const extensions = ['REACT_DEVELOPER_TOOLS']; 50 | 51 | return Promise.all( 52 | extensions.map((name) => installer.default(installer[name], forceDownload)) 53 | ).catch(console.log); 54 | }; 55 | 56 | const createWindow = async () => { 57 | if ( 58 | process.env.NODE_ENV === 'development' || 59 | process.env.DEBUG_PROD === 'true' 60 | ) { 61 | await installExtensions(); 62 | } 63 | 64 | const RESOURCES_PATH = app.isPackaged 65 | ? path.join(process.resourcesPath, 'resources') 66 | : path.join(__dirname, '../resources'); 67 | 68 | const getAssetPath = (...paths: string[]): string => { 69 | return path.join(RESOURCES_PATH, ...paths); 70 | }; 71 | 72 | mainWindow = new BrowserWindow({ 73 | show: false, 74 | width: 1024, 75 | height: 728, 76 | icon: getAssetPath('icon.png'), 77 | webPreferences: 78 | (process.env.NODE_ENV === 'development' || 79 | process.env.E2E_BUILD === 'true') && 80 | process.env.ERB_SECURE !== 'true' 81 | ? { 82 | nodeIntegration: true, 83 | } 84 | : { 85 | preload: path.join(__dirname, 'dist/renderer.prod.js'), 86 | }, 87 | }); 88 | 89 | mainWindow.maximize(); 90 | 91 | mainWindow.loadURL(`file://${__dirname}/app.html`); 92 | 93 | // create hidden worker window 94 | workerWindow = new BrowserWindow({ 95 | show: false, 96 | webPreferences: { nodeIntegration: true }, 97 | }); 98 | 99 | workerWindow.loadURL(`file://${__dirname}/worker.html`); 100 | 101 | // @TODO: Use 'ready-to-show' event 102 | // https://github.com/electron/electron/blob/master/docs/api/browser-window.md#using-ready-to-show-event 103 | mainWindow.webContents.on('did-finish-load', () => { 104 | if (!mainWindow) { 105 | throw new Error('"mainWindow" is not defined'); 106 | } 107 | if (process.env.START_MINIMIZED) { 108 | mainWindow.minimize(); 109 | } else { 110 | mainWindow.show(); 111 | mainWindow.focus(); 112 | } 113 | }); 114 | 115 | mainWindow.on('closed', () => { 116 | mainWindow = null; 117 | }); 118 | 119 | // workerWindow.webContents.on('did-finish-load', () => { 120 | // if (!workerWindow) { 121 | // throw new Error('"mainWindow" is not defined'); 122 | // } 123 | // if (process.env.START_MINIMIZED) { 124 | // workerWindow.minimize(); 125 | // } else { 126 | // workerWindow.show(); 127 | // workerWindow.focus(); 128 | // } 129 | // }); 130 | 131 | // workerWindow.on('closed', () => { 132 | // mainWindow = null; 133 | // }); 134 | 135 | const menuBuilder = new MenuBuilder(mainWindow); 136 | menuBuilder.buildMenu(); 137 | 138 | // Remove this if your app does not use auto updates 139 | // eslint-disable-next-line 140 | new AppUpdater(); 141 | }; 142 | 143 | /** 144 | * Add event listeners... 145 | */ 146 | 147 | //write another ipcrenderer.send to shutdown 148 | app.on('window-all-closed', () => { 149 | // Respect the OSX convention of having the application in memory even 150 | // after all windows have been closed 151 | if (process.platform !== 'darwin') { 152 | app.quit(); 153 | } 154 | }); 155 | 156 | if (process.env.E2E_BUILD === 'true') { 157 | // eslint-disable-next-line promise/catch-or-return 158 | app.whenReady().then(createWindow); 159 | } else { 160 | app.on('ready', createWindow); 161 | } 162 | 163 | app.on('activate', () => { 164 | // On macOS it's common to re-create a window in the app when the 165 | // dock icon is clicked and there are no other windows open. 166 | if (mainWindow === null) createWindow(); 167 | }); 168 | -------------------------------------------------------------------------------- /app/main.prod.js.LICENSE: -------------------------------------------------------------------------------- 1 | /*! http://mths.be/fromcodepoint v0.1.0 by @mathias */ 2 | -------------------------------------------------------------------------------- /app/main.prod.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! http://mths.be/fromcodepoint v0.1.0 by @mathias */ 2 | -------------------------------------------------------------------------------- /app/menu.ts: -------------------------------------------------------------------------------- 1 | import { 2 | app, 3 | Menu, 4 | shell, 5 | BrowserWindow, 6 | MenuItemConstructorOptions, 7 | } from 'electron'; 8 | 9 | interface DarwinMenuItemConstructorOptions extends MenuItemConstructorOptions { 10 | selector?: string; 11 | submenu?: DarwinMenuItemConstructorOptions[] | Menu; 12 | } 13 | 14 | export default class MenuBuilder { 15 | mainWindow: BrowserWindow; 16 | 17 | constructor(mainWindow: BrowserWindow) { 18 | this.mainWindow = mainWindow; 19 | } 20 | 21 | buildMenu(): Menu { 22 | if ( 23 | process.env.NODE_ENV === 'development' || 24 | process.env.DEBUG_PROD === 'true' 25 | ) { 26 | this.setupDevelopmentEnvironment(); 27 | } 28 | 29 | const template = 30 | process.platform === 'darwin' 31 | ? this.buildDarwinTemplate() 32 | : this.buildDefaultTemplate(); 33 | 34 | const menu = Menu.buildFromTemplate(template); 35 | Menu.setApplicationMenu(menu); 36 | 37 | return menu; 38 | } 39 | 40 | setupDevelopmentEnvironment(): void { 41 | this.mainWindow.webContents.on('context-menu', (_, props) => { 42 | const { x, y } = props; 43 | 44 | Menu.buildFromTemplate([ 45 | { 46 | label: 'Inspect element', 47 | click: () => { 48 | this.mainWindow.webContents.inspectElement(x, y); 49 | }, 50 | }, 51 | ]).popup({ window: this.mainWindow }); 52 | }); 53 | } 54 | 55 | buildDarwinTemplate(): MenuItemConstructorOptions[] { 56 | const subMenuAbout: DarwinMenuItemConstructorOptions = { 57 | label: 'Electron', 58 | submenu: [ 59 | { 60 | label: 'About ElectronReact', 61 | selector: 'orderFrontStandardAboutPanel:', 62 | }, 63 | { type: 'separator' }, 64 | { label: 'Services', submenu: [] }, 65 | { type: 'separator' }, 66 | { 67 | label: 'Hide ElectronReact', 68 | accelerator: 'Command+H', 69 | selector: 'hide:', 70 | }, 71 | { 72 | label: 'Hide Others', 73 | accelerator: 'Command+Shift+H', 74 | selector: 'hideOtherApplications:', 75 | }, 76 | { label: 'Show All', selector: 'unhideAllApplications:' }, 77 | { type: 'separator' }, 78 | { 79 | label: 'Quit', 80 | accelerator: 'Command+Q', 81 | click: () => { 82 | app.quit(); 83 | }, 84 | }, 85 | ], 86 | }; 87 | const subMenuEdit: DarwinMenuItemConstructorOptions = { 88 | label: 'Edit', 89 | submenu: [ 90 | { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' }, 91 | { label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' }, 92 | { type: 'separator' }, 93 | { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' }, 94 | { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' }, 95 | { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' }, 96 | { 97 | label: 'Select All', 98 | accelerator: 'Command+A', 99 | selector: 'selectAll:', 100 | }, 101 | ], 102 | }; 103 | const subMenuViewDev: MenuItemConstructorOptions = { 104 | label: 'View', 105 | submenu: [ 106 | { 107 | label: 'Reload', 108 | accelerator: 'Command+R', 109 | click: () => { 110 | this.mainWindow.webContents.reload(); 111 | }, 112 | }, 113 | { 114 | label: 'Toggle Full Screen', 115 | accelerator: 'Ctrl+Command+F', 116 | click: () => { 117 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 118 | }, 119 | }, 120 | { 121 | label: 'Toggle Developer Tools', 122 | accelerator: 'Alt+Command+I', 123 | click: () => { 124 | this.mainWindow.webContents.toggleDevTools(); 125 | }, 126 | }, 127 | ], 128 | }; 129 | const subMenuViewProd: MenuItemConstructorOptions = { 130 | label: 'View', 131 | submenu: [ 132 | { 133 | label: 'Toggle Full Screen', 134 | accelerator: 'Ctrl+Command+F', 135 | click: () => { 136 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen()); 137 | }, 138 | }, 139 | ], 140 | }; 141 | const subMenuWindow: DarwinMenuItemConstructorOptions = { 142 | label: 'Window', 143 | submenu: [ 144 | { 145 | label: 'Minimize', 146 | accelerator: 'Command+M', 147 | selector: 'performMiniaturize:', 148 | }, 149 | { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' }, 150 | { type: 'separator' }, 151 | { label: 'Bring All to Front', selector: 'arrangeInFront:' }, 152 | ], 153 | }; 154 | const subMenuHelp: MenuItemConstructorOptions = { 155 | label: 'Help', 156 | submenu: [ 157 | { 158 | label: 'Learn More', 159 | click() { 160 | shell.openExternal('https://electronjs.org'); 161 | }, 162 | }, 163 | { 164 | label: 'Documentation', 165 | click() { 166 | shell.openExternal( 167 | 'https://github.com/electron/electron/tree/master/docs#readme' 168 | ); 169 | }, 170 | }, 171 | { 172 | label: 'Community Discussions', 173 | click() { 174 | shell.openExternal('https://www.electronjs.org/community'); 175 | }, 176 | }, 177 | { 178 | label: 'Search Issues', 179 | click() { 180 | shell.openExternal('https://github.com/electron/electron/issues'); 181 | }, 182 | }, 183 | ], 184 | }; 185 | 186 | const subMenuView = 187 | process.env.NODE_ENV === 'development' || 188 | process.env.DEBUG_PROD === 'true' 189 | ? subMenuViewDev 190 | : subMenuViewProd; 191 | 192 | return [subMenuAbout, subMenuEdit, subMenuView, subMenuWindow, subMenuHelp]; 193 | } 194 | 195 | buildDefaultTemplate() { 196 | const templateDefault = [ 197 | { 198 | label: '&File', 199 | submenu: [ 200 | { 201 | label: '&Open', 202 | accelerator: 'Ctrl+O', 203 | }, 204 | { 205 | label: '&Close', 206 | accelerator: 'Ctrl+W', 207 | click: () => { 208 | this.mainWindow.close(); 209 | }, 210 | }, 211 | ], 212 | }, 213 | { 214 | label: '&View', 215 | submenu: 216 | process.env.NODE_ENV === 'development' || 217 | process.env.DEBUG_PROD === 'true' 218 | ? [ 219 | { 220 | label: '&Reload', 221 | accelerator: 'Ctrl+R', 222 | click: () => { 223 | this.mainWindow.webContents.reload(); 224 | }, 225 | }, 226 | { 227 | label: 'Toggle &Full Screen', 228 | accelerator: 'F11', 229 | click: () => { 230 | this.mainWindow.setFullScreen( 231 | !this.mainWindow.isFullScreen() 232 | ); 233 | }, 234 | }, 235 | { 236 | label: 'Toggle &Developer Tools', 237 | accelerator: 'Alt+Ctrl+I', 238 | click: () => { 239 | this.mainWindow.webContents.toggleDevTools(); 240 | }, 241 | }, 242 | ] 243 | : [ 244 | { 245 | label: 'Toggle &Full Screen', 246 | accelerator: 'F11', 247 | click: () => { 248 | this.mainWindow.setFullScreen( 249 | !this.mainWindow.isFullScreen() 250 | ); 251 | }, 252 | }, 253 | ], 254 | }, 255 | { 256 | label: 'Help', 257 | submenu: [ 258 | { 259 | label: 'Learn More', 260 | click() { 261 | shell.openExternal('https://electronjs.org'); 262 | }, 263 | }, 264 | { 265 | label: 'Documentation', 266 | click() { 267 | shell.openExternal( 268 | 'https://github.com/electron/electron/tree/master/docs#readme' 269 | ); 270 | }, 271 | }, 272 | { 273 | label: 'Community Discussions', 274 | click() { 275 | shell.openExternal('https://www.electronjs.org/community'); 276 | }, 277 | }, 278 | { 279 | label: 'Search Issues', 280 | click() { 281 | shell.openExternal('https://github.com/electron/electron/issues'); 282 | }, 283 | }, 284 | ], 285 | }, 286 | ]; 287 | 288 | return templateDefault; 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /app/models/containerModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | const Schema = mongoose.Schema; 3 | 4 | const containerSchema = new Schema({ 5 | container_id: { type: String, required: true, unique: true }, 6 | last_log: { type: String, required: true }, 7 | }); 8 | 9 | export default mongoose.model('Container', containerSchema); 10 | -------------------------------------------------------------------------------- /app/models/logModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | const Schema = mongoose.Schema; 3 | const Mixed = mongoose.Schema.Types.Mixed; 4 | 5 | const logSchema = new Schema({ 6 | message: { type: Mixed, required: true }, 7 | container_id: { type: String, required: true }, 8 | container_name: { type: String, required: true }, 9 | container_image: { type: String, required: true }, 10 | timestamp: { type: Date, required: true }, 11 | stream: { type: String, required: true }, 12 | status: { type: String, required: true }, 13 | ports: { type: Array, required: true }, 14 | log_level: { type: String }, 15 | }); 16 | 17 | logSchema.index({ message: 'text' }); 18 | 19 | export default mongoose.model('Log', logSchema); 20 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dockter", 3 | "productName": "Dockter", 4 | "version": "0.1.0", 5 | "description": "Low overhead, open-source Docker log management tool", 6 | "main": "./main.prod.js", 7 | "author": { 8 | "name": "BANKS", 9 | "email": "", 10 | "url": "https://github.com/oslabs-beta/Dockter" 11 | }, 12 | "scripts": { 13 | "electron-rebuild": "node -r ../internals/scripts/BabelRegister.js ../internals/scripts/ElectronRebuild.js", 14 | "postinstall": "yarn electron-rebuild" 15 | }, 16 | "license": "MIT", 17 | "dependencies": { 18 | "ansi-to-html": "^0.6.14", 19 | "dockerode": "^3.2.1", 20 | "dompurify": "^2.2.0", 21 | "html-react-parser": "^0.14.0", 22 | "mongoose": "^5.10.12", 23 | "socket.io-client": "^2.3.1" 24 | }, 25 | "devDependencies": {} 26 | } -------------------------------------------------------------------------------- /app/rootReducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { connectRouter } from 'connected-react-router'; 3 | import { History } from 'history'; 4 | 5 | export default function createRootReducer(history: History) { 6 | return combineReducers({ 7 | router: connectRouter(history), 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /app/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore, getDefaultMiddleware, Action } from '@reduxjs/toolkit'; 2 | import { createHashHistory } from 'history'; 3 | import { routerMiddleware } from 'connected-react-router'; 4 | import { createLogger } from 'redux-logger'; 5 | import { ThunkAction } from 'redux-thunk'; 6 | // eslint-disable-next-line import/no-cycle 7 | import createRootReducer from './rootReducer'; 8 | 9 | export const history = createHashHistory(); 10 | const rootReducer = createRootReducer(history); 11 | export type RootState = ReturnType; 12 | 13 | const router = routerMiddleware(history); 14 | const middleware = [...getDefaultMiddleware(), router]; 15 | 16 | const excludeLoggerEnvs = ['test', 'production']; 17 | const shouldIncludeLogger = !excludeLoggerEnvs.includes( 18 | process.env.NODE_ENV || '' 19 | ); 20 | 21 | if (shouldIncludeLogger) { 22 | const logger = createLogger({ 23 | level: 'info', 24 | collapsed: true, 25 | }); 26 | middleware.push(logger); 27 | } 28 | 29 | export const configuredStore = (initialState?: RootState) => { 30 | // Create Store 31 | const store = configureStore({ 32 | reducer: rootReducer, 33 | middleware, 34 | preloadedState: initialState, 35 | }); 36 | 37 | if (process.env.NODE_ENV === 'development' && module.hot) { 38 | module.hot.accept( 39 | './rootReducer', 40 | // eslint-disable-next-line global-require 41 | () => store.replaceReducer(require('./rootReducer').default) 42 | ); 43 | } 44 | return store; 45 | }; 46 | export type Store = ReturnType; 47 | export type AppThunk = ThunkAction>; 48 | -------------------------------------------------------------------------------- /app/worker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | I'm a hidden worker 6 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off, import/no-extraneous-dependencies: off */ 2 | 3 | const developmentEnvironments = ['development', 'test']; 4 | 5 | const developmentPlugins = [require('react-hot-loader/babel')]; 6 | 7 | const productionPlugins = [ 8 | require('babel-plugin-dev-expression'), 9 | 10 | // babel-preset-react-optimize 11 | require('@babel/plugin-transform-react-constant-elements'), 12 | require('@babel/plugin-transform-react-inline-elements'), 13 | require('babel-plugin-transform-react-remove-prop-types'), 14 | ]; 15 | 16 | module.exports = (api) => { 17 | // See docs about api at https://babeljs.io/docs/en/config-files#apicache 18 | 19 | const development = api.env(developmentEnvironments); 20 | 21 | return { 22 | presets: [ 23 | // @babel/preset-env will automatically target our browserslist targets 24 | require('@babel/preset-env'), 25 | require('@babel/preset-typescript'), 26 | [require('@babel/preset-react'), { development }], 27 | ], 28 | plugins: [ 29 | // Stage 0 30 | require('@babel/plugin-proposal-function-bind'), 31 | 32 | // Stage 1 33 | require('@babel/plugin-proposal-export-default-from'), 34 | require('@babel/plugin-proposal-logical-assignment-operators'), 35 | [require('@babel/plugin-proposal-optional-chaining'), { loose: false }], 36 | [ 37 | require('@babel/plugin-proposal-pipeline-operator'), 38 | { proposal: 'minimal' }, 39 | ], 40 | [ 41 | require('@babel/plugin-proposal-nullish-coalescing-operator'), 42 | { loose: false }, 43 | ], 44 | require('@babel/plugin-proposal-do-expressions'), 45 | 46 | // Stage 2 47 | [require('@babel/plugin-proposal-decorators'), { legacy: true }], 48 | require('@babel/plugin-proposal-function-sent'), 49 | require('@babel/plugin-proposal-export-namespace-from'), 50 | require('@babel/plugin-proposal-numeric-separator'), 51 | require('@babel/plugin-proposal-throw-expressions'), 52 | 53 | // Stage 3 54 | require('@babel/plugin-syntax-dynamic-import'), 55 | require('@babel/plugin-syntax-import-meta'), 56 | [require('@babel/plugin-proposal-class-properties'), { loose: true }], 57 | require('@babel/plugin-proposal-json-strings'), 58 | 59 | ...(development ? developmentPlugins : productionPlugins), 60 | ], 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /configs/webpack.config.base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Base webpack config used across other specific configs 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import { dependencies as externals } from '../app/package.json'; 8 | 9 | export default { 10 | externals: [...Object.keys(externals || {})], 11 | 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.tsx?$/, 16 | exclude: /node_modules/, 17 | use: { 18 | loader: 'babel-loader', 19 | options: { 20 | cacheDirectory: true, 21 | }, 22 | }, 23 | }, 24 | ], 25 | }, 26 | 27 | output: { 28 | path: path.join(__dirname, '..', 'app'), 29 | // https://github.com/webpack/webpack/issues/1114 30 | libraryTarget: 'commonjs2', 31 | }, 32 | 33 | /** 34 | * Determine the array of extensions that should be used to resolve modules. 35 | */ 36 | resolve: { 37 | extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'], 38 | modules: [path.join(__dirname, '..', 'app'), 'node_modules'], 39 | }, 40 | 41 | plugins: [ 42 | new webpack.EnvironmentPlugin({ 43 | NODE_ENV: 'production', 44 | }), 45 | 46 | new webpack.NamedModulesPlugin(), 47 | ], 48 | }; 49 | -------------------------------------------------------------------------------- /configs/webpack.config.eslint.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-unresolved: off, import/no-self-import: off */ 2 | require('@babel/register'); 3 | 4 | module.exports = require('./webpack.config.renderer.dev.babel').default; 5 | -------------------------------------------------------------------------------- /configs/webpack.config.main.prod.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Webpack config for production electron main process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import { merge } from 'webpack-merge'; 8 | import TerserPlugin from 'terser-webpack-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import baseConfig from './webpack.config.base'; 11 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv'; 12 | import DeleteSourceMaps from '../internals/scripts/DeleteSourceMaps'; 13 | 14 | CheckNodeEnv('production'); 15 | DeleteSourceMaps(); 16 | 17 | export default merge(baseConfig, { 18 | devtool: process.env.DEBUG_PROD === 'true' ? 'source-map' : 'none', 19 | 20 | mode: 'production', 21 | 22 | target: 'electron-main', 23 | 24 | entry: './app/main.dev.ts', 25 | 26 | output: { 27 | path: path.join(__dirname, '..'), 28 | filename: './app/main.prod.js', 29 | }, 30 | 31 | optimization: { 32 | minimizer: process.env.E2E_BUILD 33 | ? [] 34 | : [ 35 | new TerserPlugin({ 36 | parallel: true, 37 | sourceMap: true, 38 | cache: true, 39 | }), 40 | ], 41 | }, 42 | 43 | plugins: [ 44 | new BundleAnalyzerPlugin({ 45 | analyzerMode: 46 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', 47 | openAnalyzer: process.env.OPEN_ANALYZER === 'true', 48 | }), 49 | 50 | /** 51 | * Create global constants which can be configured at compile time. 52 | * 53 | * Useful for allowing different behaviour between development builds and 54 | * release builds 55 | * 56 | * NODE_ENV should be production so that modules do not perform certain 57 | * development checks 58 | */ 59 | new webpack.EnvironmentPlugin({ 60 | NODE_ENV: 'production', 61 | DEBUG_PROD: false, 62 | START_MINIMIZED: false, 63 | E2E_BUILD: false, 64 | }), 65 | ], 66 | 67 | /** 68 | * Disables webpack processing of __dirname and __filename. 69 | * If you run the bundle in node.js it falls back to these values of node.js. 70 | * https://github.com/webpack/webpack/issues/2010 71 | */ 72 | node: { 73 | __dirname: false, 74 | __filename: false, 75 | }, 76 | }); 77 | -------------------------------------------------------------------------------- /configs/webpack.config.renderer.dev.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build config for development electron renderer process that uses 3 | * Hot-Module-Replacement 4 | * 5 | * https://webpack.js.org/concepts/hot-module-replacement/ 6 | */ 7 | 8 | import path from 'path'; 9 | import fs from 'fs'; 10 | import webpack from 'webpack'; 11 | import chalk from 'chalk'; 12 | import { merge } from 'webpack-merge'; 13 | import { spawn, execSync } from 'child_process'; 14 | import baseConfig from './webpack.config.base'; 15 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv'; 16 | 17 | // When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's 18 | // at the dev webpack config is not accidentally run in a production environment 19 | if (process.env.NODE_ENV === 'production') { 20 | CheckNodeEnv('development'); 21 | } 22 | 23 | const port = process.env.PORT || 1212; 24 | const publicPath = `http://localhost:${port}/dist`; 25 | const dll = path.join(__dirname, '..', 'dll'); 26 | const manifest = path.resolve(dll, 'renderer.json'); 27 | const requiredByDLLConfig = module.parent.filename.includes( 28 | 'webpack.config.renderer.dev.dll' 29 | ); 30 | 31 | /** 32 | * Warn if the DLL is not built 33 | */ 34 | if (!requiredByDLLConfig && !(fs.existsSync(dll) && fs.existsSync(manifest))) { 35 | console.log( 36 | chalk.black.bgYellow.bold( 37 | 'The DLL files are missing. Sit back while we build them for you with "yarn build-dll"' 38 | ) 39 | ); 40 | execSync('yarn build-dll'); 41 | } 42 | 43 | export default merge(baseConfig, { 44 | devtool: 'inline-source-map', 45 | 46 | mode: 'development', 47 | 48 | target: 'electron-renderer', 49 | 50 | entry: [ 51 | 'core-js', 52 | 'regenerator-runtime/runtime', 53 | ...(process.env.PLAIN_HMR ? [] : ['react-hot-loader/patch']), 54 | `webpack-dev-server/client?http://localhost:${port}/`, 55 | 'webpack/hot/only-dev-server', 56 | require.resolve('../app/index.tsx'), 57 | ], 58 | 59 | output: { 60 | publicPath: `http://localhost:${port}/dist/`, 61 | filename: 'renderer.dev.js', 62 | }, 63 | 64 | module: { 65 | rules: [ 66 | { 67 | test: /\.global\.css$/, 68 | use: [ 69 | { 70 | loader: 'style-loader', 71 | }, 72 | { 73 | loader: 'css-loader', 74 | options: { 75 | sourceMap: true, 76 | }, 77 | }, 78 | { 79 | loader: 'postcss-loader', 80 | options: { 81 | postcssOptions: { 82 | ident: 'postcss', 83 | plugins: [ 84 | require('tailwindcss'), 85 | require('autoprefixer'), 86 | ], 87 | }, 88 | } 89 | }, 90 | ], 91 | }, 92 | { 93 | test: /^((?!\.global).)*\.css$/, 94 | use: [ 95 | { 96 | loader: 'style-loader', 97 | }, 98 | { 99 | loader: 'css-loader', 100 | options: { 101 | modules: { 102 | localIdentName: '[name]__[local]__[hash:base64:5]', 103 | }, 104 | sourceMap: true, 105 | importLoaders: 1, 106 | }, 107 | }, 108 | ], 109 | }, 110 | // SASS support - compile all .global.scss files and pipe it to style.css 111 | { 112 | test: /\.global\.(scss|sass)$/, 113 | use: [ 114 | { 115 | loader: 'style-loader', 116 | }, 117 | { 118 | loader: 'css-loader', 119 | options: { 120 | sourceMap: true, 121 | }, 122 | }, 123 | { 124 | loader: 'sass-loader', 125 | }, 126 | ], 127 | }, 128 | // SASS support - compile all other .scss files and pipe it to style.css 129 | { 130 | test: /^((?!\.global).)*\.(scss|sass)$/, 131 | use: [ 132 | { 133 | loader: 'style-loader', 134 | }, 135 | { 136 | loader: '@teamsupercell/typings-for-css-modules-loader', 137 | }, 138 | { 139 | loader: 'css-loader', 140 | options: { 141 | modules: { 142 | localIdentName: '[name]__[local]__[hash:base64:5]', 143 | }, 144 | sourceMap: true, 145 | importLoaders: 1, 146 | }, 147 | }, 148 | { 149 | loader: 'sass-loader', 150 | }, 151 | ], 152 | }, 153 | // WOFF Font 154 | { 155 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 156 | use: { 157 | loader: 'url-loader', 158 | options: { 159 | limit: 10000, 160 | mimetype: 'application/font-woff', 161 | }, 162 | }, 163 | }, 164 | // WOFF2 Font 165 | { 166 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 167 | use: { 168 | loader: 'url-loader', 169 | options: { 170 | limit: 10000, 171 | mimetype: 'application/font-woff', 172 | }, 173 | }, 174 | }, 175 | // TTF Font 176 | { 177 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 178 | use: { 179 | loader: 'url-loader', 180 | options: { 181 | limit: 10000, 182 | mimetype: 'application/octet-stream', 183 | }, 184 | }, 185 | }, 186 | // EOT Font 187 | { 188 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 189 | use: 'file-loader', 190 | }, 191 | // SVG Font 192 | { 193 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 194 | use: { 195 | loader: 'url-loader', 196 | options: { 197 | limit: 10000, 198 | mimetype: 'image/svg+xml', 199 | }, 200 | }, 201 | }, 202 | // Common Image Formats 203 | { 204 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, 205 | use: 'url-loader', 206 | }, 207 | ], 208 | }, 209 | resolve: { 210 | alias: { 211 | 'react-dom': '@hot-loader/react-dom', 212 | }, 213 | }, 214 | plugins: [ 215 | requiredByDLLConfig 216 | ? null 217 | : new webpack.DllReferencePlugin({ 218 | context: path.join(__dirname, '..', 'dll'), 219 | manifest: require(manifest), 220 | sourceType: 'var', 221 | }), 222 | 223 | new webpack.HotModuleReplacementPlugin({ 224 | multiStep: true, 225 | }), 226 | 227 | new webpack.NoEmitOnErrorsPlugin(), 228 | 229 | /** 230 | * Create global constants which can be configured at compile time. 231 | * 232 | * Useful for allowing different behaviour between development builds and 233 | * release builds 234 | * 235 | * NODE_ENV should be production so that modules do not perform certain 236 | * development checks 237 | * 238 | * By default, use 'development' as NODE_ENV. This can be overriden with 239 | * 'staging', for example, by changing the ENV variables in the npm scripts 240 | */ 241 | new webpack.EnvironmentPlugin({ 242 | NODE_ENV: 'development', 243 | }), 244 | 245 | new webpack.LoaderOptionsPlugin({ 246 | debug: true, 247 | }), 248 | ], 249 | 250 | node: { 251 | __dirname: false, 252 | __filename: false, 253 | }, 254 | 255 | devServer: { 256 | port, 257 | publicPath, 258 | compress: true, 259 | noInfo: false, 260 | stats: 'errors-only', 261 | inline: true, 262 | lazy: false, 263 | hot: true, 264 | headers: { 'Access-Control-Allow-Origin': '*' }, 265 | contentBase: path.join(__dirname, 'dist'), 266 | watchOptions: { 267 | aggregateTimeout: 300, 268 | ignored: /node_modules/, 269 | poll: 100, 270 | }, 271 | historyApiFallback: { 272 | verbose: true, 273 | disableDotRule: false, 274 | }, 275 | before() { 276 | if (process.env.START_HOT) { 277 | console.log('Starting Main Process...'); 278 | spawn('npm', ['run', 'start-main-dev'], { 279 | shell: true, 280 | env: process.env, 281 | stdio: 'inherit', 282 | }) 283 | .on('close', (code) => process.exit(code)) 284 | .on('error', (spawnError) => console.error(spawnError)); 285 | } 286 | }, 287 | }, 288 | }); 289 | -------------------------------------------------------------------------------- /configs/webpack.config.renderer.dev.dll.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds the DLL for development electron renderer process 3 | */ 4 | 5 | import webpack from 'webpack'; 6 | import path from 'path'; 7 | import { merge } from 'webpack-merge'; 8 | import baseConfig from './webpack.config.base'; 9 | import { dependencies } from '../package.json'; 10 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv'; 11 | 12 | CheckNodeEnv('development'); 13 | 14 | const dist = path.join(__dirname, '..', 'dll'); 15 | 16 | export default merge(baseConfig, { 17 | context: path.join(__dirname, '..'), 18 | 19 | devtool: 'eval', 20 | 21 | mode: 'development', 22 | 23 | target: 'electron-renderer', 24 | 25 | externals: ['fsevents', 'crypto-browserify'], 26 | 27 | /** 28 | * Use `module` from `webpack.config.renderer.dev.js` 29 | */ 30 | module: require('./webpack.config.renderer.dev.babel').default.module, 31 | 32 | entry: { 33 | renderer: Object.keys(dependencies || {}), 34 | }, 35 | 36 | output: { 37 | library: 'renderer', 38 | path: dist, 39 | filename: '[name].dev.dll.js', 40 | libraryTarget: 'var', 41 | }, 42 | 43 | plugins: [ 44 | new webpack.DllPlugin({ 45 | path: path.join(dist, '[name].json'), 46 | name: '[name]', 47 | }), 48 | 49 | /** 50 | * Create global constants which can be configured at compile time. 51 | * 52 | * Useful for allowing different behaviour between development builds and 53 | * release builds 54 | * 55 | * NODE_ENV should be production so that modules do not perform certain 56 | * development checks 57 | */ 58 | new webpack.EnvironmentPlugin({ 59 | NODE_ENV: 'development', 60 | }), 61 | 62 | new webpack.LoaderOptionsPlugin({ 63 | debug: true, 64 | options: { 65 | context: path.join(__dirname, '..', 'app'), 66 | output: { 67 | path: path.join(__dirname, '..', 'dll'), 68 | }, 69 | }, 70 | }), 71 | ], 72 | }); 73 | -------------------------------------------------------------------------------- /configs/webpack.config.renderer.prod.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build config for electron renderer process 3 | */ 4 | 5 | import path from 'path'; 6 | import webpack from 'webpack'; 7 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 8 | import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin'; 9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 10 | import { merge } from 'webpack-merge'; 11 | import TerserPlugin from 'terser-webpack-plugin'; 12 | import baseConfig from './webpack.config.base'; 13 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv'; 14 | import DeleteSourceMaps from '../internals/scripts/DeleteSourceMaps'; 15 | 16 | CheckNodeEnv('production'); 17 | DeleteSourceMaps(); 18 | 19 | export default merge(baseConfig, { 20 | devtool: process.env.DEBUG_PROD === 'true' ? 'source-map' : 'none', 21 | 22 | mode: 'production', 23 | 24 | target: 25 | process.env.E2E_BUILD || process.env.ERB_SECURE !== 'true' 26 | ? 'electron-renderer' 27 | : 'electron-preload', 28 | 29 | entry: [ 30 | 'core-js', 31 | 'regenerator-runtime/runtime', 32 | path.join(__dirname, '..', 'app/index.tsx'), 33 | ], 34 | 35 | output: { 36 | path: path.join(__dirname, '..', 'app/dist'), 37 | publicPath: './dist/', 38 | filename: 'renderer.prod.js', 39 | }, 40 | 41 | module: { 42 | rules: [ 43 | // Extract all .global.css to style.css as is 44 | { 45 | test: /\.global\.css$/, 46 | use: [ 47 | { 48 | loader: MiniCssExtractPlugin.loader, 49 | options: { 50 | publicPath: './', 51 | }, 52 | }, 53 | { 54 | loader: 'css-loader', 55 | options: { 56 | sourceMap: true, 57 | }, 58 | }, 59 | { 60 | loader: 'postcss-loader', 61 | options: { 62 | postcssOptions: { 63 | ident: 'postcss', 64 | plugins: [require('tailwindcss'), require('autoprefixer')], 65 | }, 66 | }, 67 | }, 68 | ], 69 | }, 70 | // Pipe other styles through css modules and append to style.css 71 | { 72 | test: /^((?!\.global).)*\.css$/, 73 | use: [ 74 | { 75 | loader: MiniCssExtractPlugin.loader, 76 | }, 77 | { 78 | loader: 'css-loader', 79 | options: { 80 | modules: { 81 | localIdentName: '[name]__[local]__[hash:base64:5]', 82 | }, 83 | sourceMap: true, 84 | }, 85 | }, 86 | ], 87 | }, 88 | // Add SASS support - compile all .global.scss files and pipe it to style.css 89 | { 90 | test: /\.global\.(scss|sass)$/, 91 | use: [ 92 | { 93 | loader: MiniCssExtractPlugin.loader, 94 | }, 95 | { 96 | loader: 'css-loader', 97 | options: { 98 | sourceMap: true, 99 | importLoaders: 1, 100 | }, 101 | }, 102 | { 103 | loader: 'sass-loader', 104 | options: { 105 | sourceMap: true, 106 | }, 107 | }, 108 | ], 109 | }, 110 | // Add SASS support - compile all other .scss files and pipe it to style.css 111 | { 112 | test: /^((?!\.global).)*\.(scss|sass)$/, 113 | use: [ 114 | { 115 | loader: MiniCssExtractPlugin.loader, 116 | }, 117 | { 118 | loader: 'css-loader', 119 | options: { 120 | modules: { 121 | localIdentName: '[name]__[local]__[hash:base64:5]', 122 | }, 123 | importLoaders: 1, 124 | sourceMap: true, 125 | }, 126 | }, 127 | { 128 | loader: 'sass-loader', 129 | options: { 130 | sourceMap: true, 131 | }, 132 | }, 133 | ], 134 | }, 135 | // WOFF Font 136 | { 137 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 138 | use: { 139 | loader: 'url-loader', 140 | options: { 141 | limit: 10000, 142 | mimetype: 'application/font-woff', 143 | }, 144 | }, 145 | }, 146 | // WOFF2 Font 147 | { 148 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 149 | use: { 150 | loader: 'url-loader', 151 | options: { 152 | limit: 10000, 153 | mimetype: 'application/font-woff', 154 | }, 155 | }, 156 | }, 157 | // TTF Font 158 | { 159 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 160 | use: { 161 | loader: 'url-loader', 162 | options: { 163 | limit: 10000, 164 | mimetype: 'application/octet-stream', 165 | }, 166 | }, 167 | }, 168 | // EOT Font 169 | { 170 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 171 | use: 'file-loader', 172 | }, 173 | // SVG Font 174 | { 175 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 176 | use: { 177 | loader: 'url-loader', 178 | options: { 179 | limit: 10000, 180 | mimetype: 'image/svg+xml', 181 | }, 182 | }, 183 | }, 184 | // Common Image Formats 185 | { 186 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/, 187 | use: 'url-loader', 188 | }, 189 | ], 190 | }, 191 | 192 | optimization: { 193 | minimizer: process.env.E2E_BUILD 194 | ? [] 195 | : [ 196 | new TerserPlugin({ 197 | parallel: true, 198 | sourceMap: true, 199 | cache: true, 200 | }), 201 | new OptimizeCSSAssetsPlugin({ 202 | cssProcessorOptions: { 203 | map: { 204 | inline: false, 205 | annotation: true, 206 | }, 207 | }, 208 | }), 209 | ], 210 | }, 211 | 212 | plugins: [ 213 | /** 214 | * Create global constants which can be configured at compile time. 215 | * 216 | * Useful for allowing different behaviour between development builds and 217 | * release builds 218 | * 219 | * NODE_ENV should be production so that modules do not perform certain 220 | * development checks 221 | */ 222 | new webpack.EnvironmentPlugin({ 223 | NODE_ENV: 'production', 224 | DEBUG_PROD: false, 225 | E2E_BUILD: false, 226 | }), 227 | 228 | new MiniCssExtractPlugin({ 229 | filename: 'style.css', 230 | }), 231 | 232 | new BundleAnalyzerPlugin({ 233 | analyzerMode: 234 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled', 235 | openAnalyzer: process.env.OPEN_ANALYZER === 'true', 236 | }), 237 | ], 238 | }); 239 | -------------------------------------------------------------------------------- /db/database.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/db/database.sqlite3 -------------------------------------------------------------------------------- /internals/img/erb-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/erb-banner.png -------------------------------------------------------------------------------- /internals/img/erb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/erb-logo.png -------------------------------------------------------------------------------- /internals/img/eslint-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/eslint-padded-90.png -------------------------------------------------------------------------------- /internals/img/eslint-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/eslint-padded.png -------------------------------------------------------------------------------- /internals/img/eslint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/eslint.png -------------------------------------------------------------------------------- /internals/img/jest-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/jest-padded-90.png -------------------------------------------------------------------------------- /internals/img/jest-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/jest-padded.png -------------------------------------------------------------------------------- /internals/img/jest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/jest.png -------------------------------------------------------------------------------- /internals/img/js-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/js-padded.png -------------------------------------------------------------------------------- /internals/img/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/js.png -------------------------------------------------------------------------------- /internals/img/npm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/npm.png -------------------------------------------------------------------------------- /internals/img/react-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/react-padded-90.png -------------------------------------------------------------------------------- /internals/img/react-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/react-padded.png -------------------------------------------------------------------------------- /internals/img/react-router-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/react-router-padded-90.png -------------------------------------------------------------------------------- /internals/img/react-router-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/react-router-padded.png -------------------------------------------------------------------------------- /internals/img/react-router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/react-router.png -------------------------------------------------------------------------------- /internals/img/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/react.png -------------------------------------------------------------------------------- /internals/img/redux-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/redux-padded-90.png -------------------------------------------------------------------------------- /internals/img/redux-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/redux-padded.png -------------------------------------------------------------------------------- /internals/img/redux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/redux.png -------------------------------------------------------------------------------- /internals/img/webpack-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/webpack-padded-90.png -------------------------------------------------------------------------------- /internals/img/webpack-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/webpack-padded.png -------------------------------------------------------------------------------- /internals/img/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/webpack.png -------------------------------------------------------------------------------- /internals/img/yarn-padded-90.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/yarn-padded-90.png -------------------------------------------------------------------------------- /internals/img/yarn-padded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/yarn-padded.png -------------------------------------------------------------------------------- /internals/img/yarn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/internals/img/yarn.png -------------------------------------------------------------------------------- /internals/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | export default 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /internals/scripts/BabelRegister.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | require('@babel/register')({ 4 | extensions: ['.es6', '.es', '.jsx', '.js', '.mjs', '.ts', '.tsx'], 5 | cwd: path.join(__dirname, '..', '..'), 6 | }); 7 | -------------------------------------------------------------------------------- /internals/scripts/CheckBuildsExist.js: -------------------------------------------------------------------------------- 1 | // Check if the renderer and main bundles are built 2 | import path from 'path'; 3 | import chalk from 'chalk'; 4 | import fs from 'fs'; 5 | 6 | const mainPath = path.join(__dirname, '..', '..', 'app', 'main.prod.js'); 7 | const rendererPath = path.join( 8 | __dirname, 9 | '..', 10 | '..', 11 | 'app', 12 | 'dist', 13 | 'renderer.prod.js' 14 | ); 15 | 16 | if (!fs.existsSync(mainPath)) { 17 | throw new Error( 18 | chalk.whiteBright.bgRed.bold( 19 | 'The main process is not built yet. Build it by running "yarn build-main"' 20 | ) 21 | ); 22 | } 23 | 24 | if (!fs.existsSync(rendererPath)) { 25 | throw new Error( 26 | chalk.whiteBright.bgRed.bold( 27 | 'The renderer process is not built yet. Build it by running "yarn build-renderer"' 28 | ) 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /internals/scripts/CheckNativeDep.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import chalk from 'chalk'; 3 | import { execSync } from 'child_process'; 4 | import { dependencies } from '../../package.json'; 5 | 6 | if (dependencies) { 7 | const dependenciesKeys = Object.keys(dependencies); 8 | const nativeDeps = fs 9 | .readdirSync('node_modules') 10 | .filter((folder) => fs.existsSync(`node_modules/${folder}/binding.gyp`)); 11 | try { 12 | // Find the reason for why the dependency is installed. If it is installed 13 | // because of a devDependency then that is okay. Warn when it is installed 14 | // because of a dependency 15 | const { dependencies: dependenciesObject } = JSON.parse( 16 | execSync(`npm ls ${nativeDeps.join(' ')} --json`).toString() 17 | ); 18 | const rootDependencies = Object.keys(dependenciesObject); 19 | const filteredRootDependencies = rootDependencies.filter((rootDependency) => 20 | dependenciesKeys.includes(rootDependency) 21 | ); 22 | if (filteredRootDependencies.length > 0) { 23 | const plural = filteredRootDependencies.length > 1; 24 | console.log(` 25 | ${chalk.whiteBright.bgYellow.bold( 26 | 'Webpack does not work with native dependencies.' 27 | )} 28 | ${chalk.bold(filteredRootDependencies.join(', '))} ${ 29 | plural ? 'are native dependencies' : 'is a native dependency' 30 | } and should be installed inside of the "./app" folder. 31 | First, uninstall the packages from "./package.json": 32 | ${chalk.whiteBright.bgGreen.bold('yarn remove your-package')} 33 | ${chalk.bold( 34 | 'Then, instead of installing the package to the root "./package.json":' 35 | )} 36 | ${chalk.whiteBright.bgRed.bold('yarn add your-package')} 37 | ${chalk.bold('Install the package to "./app/package.json"')} 38 | ${chalk.whiteBright.bgGreen.bold('cd ./app && yarn add your-package')} 39 | Read more about native dependencies at: 40 | ${chalk.bold( 41 | 'https://electron-react-boilerplate.js.org/docs/adding-dependencies/#module-structure' 42 | )} 43 | `); 44 | process.exit(1); 45 | } 46 | } catch (e) { 47 | console.log('Native dependencies could not be checked'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internals/scripts/CheckNodeEnv.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export default function CheckNodeEnv(expectedEnv) { 4 | if (!expectedEnv) { 5 | throw new Error('"expectedEnv" not set'); 6 | } 7 | 8 | if (process.env.NODE_ENV !== expectedEnv) { 9 | console.log( 10 | chalk.whiteBright.bgRed.bold( 11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config` 12 | ) 13 | ); 14 | process.exit(2); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internals/scripts/CheckPortInUse.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import detectPort from 'detect-port'; 3 | 4 | const port = process.env.PORT || '1212'; 5 | 6 | detectPort(port, (err, availablePort) => { 7 | if (port !== String(availablePort)) { 8 | throw new Error( 9 | chalk.whiteBright.bgRed.bold( 10 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 yarn dev` 11 | ) 12 | ); 13 | } else { 14 | process.exit(0); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /internals/scripts/CheckYarn.js: -------------------------------------------------------------------------------- 1 | if (!/yarn\.js$/.test(process.env.npm_execpath || '')) { 2 | console.warn( 3 | "\u001b[33mYou don't seem to be using yarn. This could produce unexpected results.\u001b[39m" 4 | ); 5 | } 6 | -------------------------------------------------------------------------------- /internals/scripts/DeleteSourceMaps.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import rimraf from 'rimraf'; 3 | 4 | export default function deleteSourceMaps() { 5 | rimraf.sync(path.join(__dirname, '../../app/dist/*.js.map')); 6 | rimraf.sync(path.join(__dirname, '../../app/*.js.map')); 7 | } 8 | -------------------------------------------------------------------------------- /internals/scripts/ElectronRebuild.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { execSync } from 'child_process'; 3 | import fs from 'fs'; 4 | import { dependencies } from '../../app/package.json'; 5 | 6 | const nodeModulesPath = path.join(__dirname, '..', '..', 'app', 'node_modules'); 7 | 8 | if ( 9 | Object.keys(dependencies || {}).length > 0 && 10 | fs.existsSync(nodeModulesPath) 11 | ) { 12 | const electronRebuildCmd = 13 | '../node_modules/.bin/electron-rebuild --parallel --force --types prod,dev,optional --module-dir .'; 14 | const cmd = 15 | process.platform === 'win32' 16 | ? electronRebuildCmd.replace(/\//g, '\\') 17 | : electronRebuildCmd; 18 | execSync(cmd, { 19 | cwd: path.join(__dirname, '..', '..', 'app'), 20 | stdio: 'inherit', 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dockter", 3 | "productName": "Dockter", 4 | "version": "0.1.0", 5 | "description": "Low overhead, open-source Docker log management tool", 6 | "scripts": { 7 | "build": "concurrently \"yarn build-main\" \"yarn build-renderer\"", 8 | "build-dll": "cross-env NODE_ENV=development webpack --config ./configs/webpack.config.renderer.dev.dll.babel.js --colors", 9 | "build-e2e": "cross-env E2E_BUILD=true yarn build", 10 | "build-main": "cross-env NODE_ENV=production webpack --config ./configs/webpack.config.main.prod.babel.js --colors", 11 | "build-renderer": "cross-env NODE_ENV=production webpack --config ./configs/webpack.config.renderer.prod.babel.js --colors", 12 | "dev": "cross-env START_HOT=1 node -r @babel/register ./internals/scripts/CheckPortInUse.js && cross-env START_HOT=1 yarn start-renderer-dev", 13 | "electron-rebuild": "electron-rebuild --parallel --force --types prod,dev,optional --module-dir app", 14 | "package": "yarn build && electron-builder build --publish never", 15 | "package-all": "yarn build && electron-builder build -mwl", 16 | "package-ci": "yarn postinstall && yarn build && electron-builder --publish always", 17 | "package-mac": "yarn build && electron-builder build --mac", 18 | "package-linux": "yarn build && electron-builder build --linux", 19 | "package-win": "yarn build && electron-builder build --win --x64", 20 | "postinstall": "node -r @babel/register internals/scripts/CheckNativeDep.js && electron-builder install-app-deps && yarn build-dll && opencollective-postinstall", 21 | "preinstall": "node ./internals/scripts/CheckYarn.js", 22 | "prestart": "yarn build", 23 | "start": "cross-env NODE_ENV=production electron ./app/main.prod.js", 24 | "start-main-debug": "yarn start-main-dev --inspect=5858 --remote-debugging-port=9223", 25 | "start-main-dev": "cross-env START_HOT=1 NODE_ENV=development electron -r ./internals/scripts/BabelRegister ./app/main.dev.ts", 26 | "start-renderer-dev": "cross-env NODE_ENV=development webpack-dev-server --config configs/webpack.config.renderer.dev.babel.js", 27 | "test": "cross-env BABEL_DISABLE_CACHE=1 jest", 28 | "test-all": "yarn tsc && yarn build && yarn test", 29 | "test-e2e": "node -r @babel/register ./internals/scripts/CheckBuildsExist.js && cross-env NODE_ENV=test testcafe electron:./app ./test/e2e/HomePage.e2e.ts", 30 | "test-e2e-live": "node -r @babel/register ./internals/scripts/CheckBuildsExist.js && cross-env NODE_ENV=test testcafe --live electron:./app ./test/e2e/HomePage.e2e.ts", 31 | "test-watch": "yarn test --watch" 32 | }, 33 | "build": { 34 | "productName": "Dockter", 35 | "appId": "", 36 | "files": [ 37 | "dist/", 38 | "node_modules/", 39 | "app.html", 40 | "main.prod.js", 41 | "main.prod.js.map", 42 | "package.json" 43 | ], 44 | "dmg": { 45 | "contents": [ 46 | { 47 | "x": 130, 48 | "y": 220 49 | }, 50 | { 51 | "x": 410, 52 | "y": 220, 53 | "type": "link", 54 | "path": "/Applications" 55 | } 56 | ] 57 | }, 58 | "win": { 59 | "target": [ 60 | "nsis", 61 | "msi" 62 | ] 63 | }, 64 | "linux": { 65 | "target": [ 66 | "deb", 67 | "rpm", 68 | "AppImage" 69 | ], 70 | "category": "Development" 71 | }, 72 | "directories": { 73 | "buildResources": "resources", 74 | "output": "release" 75 | }, 76 | "extraResources": [ 77 | "./resources/**" 78 | ], 79 | "publish": { 80 | "provider": "github", 81 | "owner": "Dockter", 82 | "repo": "Dockter", 83 | "private": false 84 | } 85 | }, 86 | "repository": { 87 | "type": "git", 88 | "url": "git+https://github.com/oslabs-beta/Dockter.git" 89 | }, 90 | "author": { 91 | "name": "BANKS", 92 | "email": "", 93 | "url": "https://dockter.io" 94 | }, 95 | "contributors": [ 96 | { 97 | "name": "Ben Brower", 98 | "email": "", 99 | "url": "https://github.com/bbrower21" 100 | }, 101 | { 102 | "name": "Anup Ramdass", 103 | "email": "", 104 | "url": "https://github.com/anrax" 105 | }, 106 | { 107 | "name": "Nancy Kousholt", 108 | "email": "", 109 | "url": "https://github.com/noodlemonkey" 110 | }, 111 | { 112 | "name": "Kyle Whang", 113 | "email": "", 114 | "url": "https://github.com/shaisle" 115 | }, 116 | { 117 | "name": "Samuel Kim", 118 | "email": "", 119 | "url": "https://github.com/samuy" 120 | } 121 | ], 122 | "license": "MIT", 123 | "bugs": { 124 | "url": "https://github.com/oslabs-beta/Dockter/issues" 125 | }, 126 | "keywords": [ 127 | "electron", 128 | "boilerplate", 129 | "react", 130 | "redux", 131 | "typescript", 132 | "ts", 133 | "sass", 134 | "webpack", 135 | "hot", 136 | "reload" 137 | ], 138 | "homepage": "https://github.com/oslabs-beta/Dockter/README.md", 139 | "jest": { 140 | "testURL": "http://localhost/", 141 | "moduleNameMapper": { 142 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/internals/mocks/fileMock.js", 143 | "\\.(css|less|sass|scss)$": "identity-obj-proxy" 144 | }, 145 | "moduleFileExtensions": [ 146 | "js", 147 | "jsx", 148 | "ts", 149 | "tsx", 150 | "json" 151 | ], 152 | "moduleDirectories": [ 153 | "node_modules", 154 | "app/node_modules" 155 | ], 156 | "setupFiles": [ 157 | "./internals/scripts/CheckBuildsExist.js" 158 | ], 159 | "setupFilesAfterEnv": [ 160 | "/setupTests.js" 161 | ] 162 | }, 163 | "devDependencies": { 164 | "@amilajack/testcafe-browser-provider-electron": "^0.0.15-alpha.1", 165 | "@babel/core": "^7.11.5", 166 | "@babel/plugin-proposal-class-properties": "^7.10.4", 167 | "@babel/plugin-proposal-decorators": "^7.10.5", 168 | "@babel/plugin-proposal-do-expressions": "^7.10.4", 169 | "@babel/plugin-proposal-export-default-from": "^7.10.4", 170 | "@babel/plugin-proposal-export-namespace-from": "^7.10.4", 171 | "@babel/plugin-proposal-function-bind": "^7.11.5", 172 | "@babel/plugin-proposal-function-sent": "^7.10.4", 173 | "@babel/plugin-proposal-json-strings": "^7.10.4", 174 | "@babel/plugin-proposal-logical-assignment-operators": "^7.11.0", 175 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", 176 | "@babel/plugin-proposal-numeric-separator": "^7.10.4", 177 | "@babel/plugin-proposal-optional-chaining": "^7.11.0", 178 | "@babel/plugin-proposal-pipeline-operator": "^7.10.5", 179 | "@babel/plugin-proposal-throw-expressions": "^7.10.4", 180 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 181 | "@babel/plugin-syntax-import-meta": "^7.10.4", 182 | "@babel/plugin-transform-react-constant-elements": "^7.10.4", 183 | "@babel/plugin-transform-react-inline-elements": "^7.10.4", 184 | "@babel/preset-env": "^7.11.5", 185 | "@babel/preset-react": "^7.10.4", 186 | "@babel/preset-typescript": "^7.10.4", 187 | "@babel/register": "^7.10.5", 188 | "@teamsupercell/typings-for-css-modules-loader": "^2.2.1", 189 | "@types/enzyme": "^3.10.5", 190 | "@types/enzyme-adapter-react-16": "^1.0.6", 191 | "@types/history": "^4.7.7", 192 | "@types/jest": "^26.0.10", 193 | "@types/node": "12", 194 | "@types/react": "^16.9.44", 195 | "@types/react-dom": "^16.9.8", 196 | "@types/react-redux": "^7.1.9", 197 | "@types/react-router": "^5.1.8", 198 | "@types/react-router-dom": "^5.1.5", 199 | "@types/react-test-renderer": "^16.9.3", 200 | "@types/redux-logger": "^3.0.8", 201 | "@types/webpack": "^4.41.21", 202 | "@types/webpack-env": "^1.15.2", 203 | "autoprefixer": "^9.0.0", 204 | "babel-jest": "^26.1.0", 205 | "babel-loader": "^8.1.0", 206 | "babel-plugin-dev-expression": "^0.2.2", 207 | "babel-plugin-transform-react-remove-prop-types": "^0.4.24", 208 | "browserslist-config-erb": "^0.0.1", 209 | "chalk": "^4.1.0", 210 | "concurrently": "^5.3.0", 211 | "core-js": "^3.6.5", 212 | "cross-env": "^7.0.2", 213 | "css-loader": "^3.6.0", 214 | "detect-port": "^1.3.0", 215 | "electron": "^10.1", 216 | "electron-builder": "^22.3.6", 217 | "electron-devtools-installer": "^2.2.4", 218 | "electron-rebuild": "^1.10.0", 219 | "enzyme": "^3.11.0", 220 | "enzyme-adapter-react-16": "^1.15.3", 221 | "enzyme-to-json": "^3.5.0", 222 | "file-loader": "^6.0.0", 223 | "husky": "^4.2.5", 224 | "identity-obj-proxy": "^3.0.0", 225 | "jest": "^26.1.0", 226 | "mini-css-extract-plugin": "^0.9.0", 227 | "node-sass": "^4.14.1", 228 | "opencollective-postinstall": "^2.0.3", 229 | "optimize-css-assets-webpack-plugin": "^5.0.3", 230 | "postcss-loader": "^4.0.4", 231 | "prettier": "^2.0.5", 232 | "react-test-renderer": "^16.12.0", 233 | "redux-logger": "^3.0.6", 234 | "rimraf": "^3.0.0", 235 | "sass-loader": "^9.0.3", 236 | "style-loader": "^1.2.1", 237 | "tailwindcss": "^1.9.6", 238 | "terser-webpack-plugin": "^3.0.7", 239 | "testcafe": "^1.8.8", 240 | "testcafe-browser-provider-electron": "^0.0.15", 241 | "testcafe-react-selectors": "^4.0.0", 242 | "typescript": "^3.9.7", 243 | "url-loader": "^4.1.0", 244 | "webpack": "^4.43.0", 245 | "webpack-bundle-analyzer": "^3.8.0", 246 | "webpack-cli": "^3.3.12", 247 | "webpack-dev-server": "^3.11.0", 248 | "webpack-merge": "^5.0.9" 249 | }, 250 | "dependencies": { 251 | "@fortawesome/fontawesome-free": "^5.14.0", 252 | "@hot-loader/react-dom": "^16.13.0", 253 | "@reduxjs/toolkit": "^1.4.0", 254 | "connected-react-router": "^6.6.1", 255 | "dockerode": "^3.2.1", 256 | "electron-debug": "^3.1.0", 257 | "electron-log": "^4.2.4", 258 | "electron-updater": "^4.3.4", 259 | "history": "^4.7.2", 260 | "node-ipc": "^9.1.1", 261 | "path": "^0.12.7", 262 | "react": "^16.13.1", 263 | "react-dom": "^16.12.0", 264 | "react-hot-loader": "^4.12.21", 265 | "react-infinite-scroll-component": "^5.1.0", 266 | "react-redux": "^7.2.1", 267 | "react-router-dom": "^5.2.0", 268 | "react-table": "^7.6.0", 269 | "redux": "^4.0.5", 270 | "redux-thunk": "^2.3.0", 271 | "regenerator-runtime": "^0.13.5", 272 | "source-map-support": "^0.5.19" 273 | }, 274 | "devEngines": { 275 | "node": ">=7.x", 276 | "npm": ">=4.x", 277 | "yarn": ">=0.21.3" 278 | }, 279 | "collective": { 280 | "url": "" 281 | }, 282 | "browserslist": [], 283 | "prettier": { 284 | "overrides": [ 285 | { 286 | "files": [ 287 | ".prettierrc", 288 | ".babelrc" 289 | ], 290 | "options": { 291 | "parser": "json" 292 | } 293 | } 294 | ], 295 | "singleQuote": true 296 | }, 297 | "renovate": { 298 | "extends": [ 299 | "bliss" 300 | ], 301 | "baseBranches": [ 302 | "next" 303 | ] 304 | } 305 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('tailwindcss'), 4 | require('autoprefixer'), 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/resources/icon.icns -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/resources/icon.png -------------------------------------------------------------------------------- /resources/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/resources/icons/1024x1024.png -------------------------------------------------------------------------------- /resources/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/resources/icons/128x128.png -------------------------------------------------------------------------------- /resources/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/resources/icons/16x16.png -------------------------------------------------------------------------------- /resources/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/resources/icons/24x24.png -------------------------------------------------------------------------------- /resources/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/resources/icons/256x256.png -------------------------------------------------------------------------------- /resources/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/resources/icons/32x32.png -------------------------------------------------------------------------------- /resources/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/resources/icons/48x48.png -------------------------------------------------------------------------------- /resources/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/resources/icons/512x512.png -------------------------------------------------------------------------------- /resources/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Dockter/84510c5dcc3ca5f67d7c5b102fbd51f9c1d2b094/resources/icons/64x64.png -------------------------------------------------------------------------------- /setupTests.js: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | Enzyme.configure({ adapter: new Adapter() }); 4 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | future: { 3 | // removeDeprecatedGapUtilities: true, 4 | // purgeLayersByDefault: true, 5 | }, 6 | purge: [], 7 | target: 'relaxed', 8 | prefix: '', 9 | important: false, 10 | separator: ':', 11 | presets: [], 12 | theme: { 13 | screens: { 14 | sm: '640px', 15 | md: '768px', 16 | lg: '1024px', 17 | xl: '1280px', 18 | }, 19 | colors: { 20 | transparent: 'transparent', 21 | current: 'currentColor', 22 | 23 | black: '#000', 24 | white: '#fff', 25 | 26 | gray: { 27 | 100: '#f7fafc', 28 | 200: '#edf2f7', 29 | 300: '#e2e8f0', 30 | 400: '#cbd5e0', 31 | 500: '#a0aec0', 32 | 600: '#718096', 33 | 700: '#4a5568', 34 | 800: '#2d3748', 35 | 900: '#1a202c', 36 | }, 37 | red: { 38 | 100: '#fff5f5', 39 | 200: '#fed7d7', 40 | 300: '#feb2b2', 41 | 400: '#fc8181', 42 | 500: '#f56565', 43 | 600: '#e53e3e', 44 | 700: '#c53030', 45 | 800: '#9b2c2c', 46 | 900: '#742a2a', 47 | }, 48 | orange: { 49 | 100: '#fffaf0', 50 | 200: '#feebc8', 51 | 300: '#fbd38d', 52 | 400: '#f6ad55', 53 | 500: '#ed8936', 54 | 600: '#dd6b20', 55 | 700: '#c05621', 56 | 800: '#9c4221', 57 | 900: '#7b341e', 58 | }, 59 | yellow: { 60 | 100: '#fffff0', 61 | 200: '#fefcbf', 62 | 300: '#faf089', 63 | 400: '#f6e05e', 64 | 500: '#ecc94b', 65 | 600: '#d69e2e', 66 | 700: '#b7791f', 67 | 800: '#975a16', 68 | 900: '#744210', 69 | }, 70 | green: { 71 | 100: '#f0fff4', 72 | 200: '#c6f6d5', 73 | 300: '#9ae6b4', 74 | 400: '#68d391', 75 | 500: '#48bb78', 76 | 600: '#38a169', 77 | 700: '#2f855a', 78 | 800: '#276749', 79 | 900: '#22543d', 80 | }, 81 | teal: { 82 | 100: '#e6fffa', 83 | 200: '#b2f5ea', 84 | 300: '#81e6d9', 85 | 400: '#4fd1c5', 86 | 500: '#38b2ac', 87 | 600: '#319795', 88 | 700: '#2c7a7b', 89 | 800: '#285e61', 90 | 900: '#234e52', 91 | }, 92 | blue: { 93 | 100: '#ebf8ff', 94 | 200: '#bee3f8', 95 | 300: '#90cdf4', 96 | 400: '#63b3ed', 97 | 500: '#4299e1', 98 | 600: '#3182ce', 99 | 700: '#2b6cb0', 100 | 800: '#2c5282', 101 | 900: '#2a4365', 102 | }, 103 | indigo: { 104 | 100: '#ebf4ff', 105 | 200: '#c3dafe', 106 | 300: '#a3bffa', 107 | 400: '#7f9cf5', 108 | 500: '#667eea', 109 | 600: '#5a67d8', 110 | 700: '#4c51bf', 111 | 800: '#434190', 112 | 900: '#3c366b', 113 | }, 114 | purple: { 115 | 100: '#faf5ff', 116 | 200: '#e9d8fd', 117 | 300: '#d6bcfa', 118 | 400: '#b794f4', 119 | 500: '#9f7aea', 120 | 600: '#805ad5', 121 | 700: '#6b46c1', 122 | 800: '#553c9a', 123 | 900: '#44337a', 124 | }, 125 | pink: { 126 | 100: '#fff5f7', 127 | 200: '#fed7e2', 128 | 300: '#fbb6ce', 129 | 400: '#f687b3', 130 | 500: '#ed64a6', 131 | 600: '#d53f8c', 132 | 700: '#b83280', 133 | 800: '#97266d', 134 | 900: '#702459', 135 | }, 136 | }, 137 | spacing: { 138 | px: '1px', 139 | 0: '0', 140 | 1: '0.25rem', 141 | 2: '0.5rem', 142 | 3: '0.75rem', 143 | 4: '1rem', 144 | 5: '1.25rem', 145 | 6: '1.5rem', 146 | 8: '2rem', 147 | 10: '2.5rem', 148 | 12: '3rem', 149 | 16: '4rem', 150 | 20: '5rem', 151 | 24: '6rem', 152 | 32: '8rem', 153 | 40: '10rem', 154 | 48: '12rem', 155 | 56: '14rem', 156 | 64: '16rem', 157 | }, 158 | backgroundColor: (theme) => theme('colors'), 159 | backgroundImage: { 160 | none: 'none', 161 | 'gradient-to-t': 'linear-gradient(to top, var(--gradient-color-stops))', 162 | 'gradient-to-tr': 163 | 'linear-gradient(to top right, var(--gradient-color-stops))', 164 | 'gradient-to-r': 'linear-gradient(to right, var(--gradient-color-stops))', 165 | 'gradient-to-br': 166 | 'linear-gradient(to bottom right, var(--gradient-color-stops))', 167 | 'gradient-to-b': 168 | 'linear-gradient(to bottom, var(--gradient-color-stops))', 169 | 'gradient-to-bl': 170 | 'linear-gradient(to bottom left, var(--gradient-color-stops))', 171 | 'gradient-to-l': 'linear-gradient(to left, var(--gradient-color-stops))', 172 | 'gradient-to-tl': 173 | 'linear-gradient(to top left, var(--gradient-color-stops))', 174 | }, 175 | gradientColorStops: (theme) => theme('colors'), 176 | backgroundOpacity: (theme) => theme('opacity'), 177 | backgroundPosition: { 178 | bottom: 'bottom', 179 | center: 'center', 180 | left: 'left', 181 | 'left-bottom': 'left bottom', 182 | 'left-top': 'left top', 183 | right: 'right', 184 | 'right-bottom': 'right bottom', 185 | 'right-top': 'right top', 186 | top: 'top', 187 | }, 188 | backgroundSize: { 189 | auto: 'auto', 190 | cover: 'cover', 191 | contain: 'contain', 192 | }, 193 | borderColor: (theme) => ({ 194 | ...theme('colors'), 195 | default: theme('colors.gray.300', 'currentColor'), 196 | }), 197 | borderOpacity: (theme) => theme('opacity'), 198 | borderRadius: { 199 | none: '0', 200 | sm: '0.125rem', 201 | default: '0.25rem', 202 | md: '0.375rem', 203 | lg: '0.5rem', 204 | xl: '0.75rem', 205 | '2xl': '1rem', 206 | '3xl': '1.5rem', 207 | full: '9999px', 208 | }, 209 | borderWidth: { 210 | default: '1px', 211 | 0: '0', 212 | 2: '2px', 213 | 4: '4px', 214 | 8: '8px', 215 | }, 216 | boxShadow: { 217 | xs: '0 0 0 1px rgba(0, 0, 0, 0.05)', 218 | sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', 219 | default: 220 | '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)', 221 | md: 222 | '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', 223 | lg: 224 | '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', 225 | xl: 226 | '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)', 227 | '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)', 228 | inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)', 229 | outline: '0 0 0 3px rgba(66, 153, 225, 0.5)', 230 | none: 'none', 231 | }, 232 | container: {}, 233 | cursor: { 234 | auto: 'auto', 235 | default: 'default', 236 | pointer: 'pointer', 237 | wait: 'wait', 238 | text: 'text', 239 | move: 'move', 240 | 'not-allowed': 'not-allowed', 241 | }, 242 | divideColor: (theme) => theme('borderColor'), 243 | divideOpacity: (theme) => theme('borderOpacity'), 244 | divideWidth: (theme) => theme('borderWidth'), 245 | fill: { 246 | current: 'currentColor', 247 | }, 248 | flex: { 249 | 1: '1 1 0%', 250 | auto: '1 1 auto', 251 | initial: '0 1 auto', 252 | none: 'none', 253 | }, 254 | flexGrow: { 255 | 0: '0', 256 | default: '1', 257 | }, 258 | flexShrink: { 259 | 0: '0', 260 | default: '1', 261 | }, 262 | fontFamily: { 263 | sans: [ 264 | 'system-ui', 265 | '-apple-system', 266 | 'BlinkMacSystemFont', 267 | '"Segoe UI"', 268 | 'Roboto', 269 | '"Helvetica Neue"', 270 | 'Arial', 271 | '"Noto Sans"', 272 | 'sans-serif', 273 | '"Apple Color Emoji"', 274 | '"Segoe UI Emoji"', 275 | '"Segoe UI Symbol"', 276 | '"Noto Color Emoji"', 277 | ], 278 | serif: ['Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'], 279 | mono: [ 280 | 'Menlo', 281 | 'Monaco', 282 | 'Consolas', 283 | '"Liberation Mono"', 284 | '"Courier New"', 285 | 'monospace', 286 | ], 287 | }, 288 | fontSize: { 289 | xs: '0.75rem', 290 | sm: '0.875rem', 291 | base: '1rem', 292 | lg: '1.125rem', 293 | xl: '1.25rem', 294 | '2xl': '1.5rem', 295 | '3xl': '1.875rem', 296 | '4xl': '2.25rem', 297 | '5xl': '3rem', 298 | '6xl': '4rem', 299 | }, 300 | fontWeight: { 301 | hairline: '100', 302 | thin: '200', 303 | light: '300', 304 | normal: '400', 305 | medium: '500', 306 | semibold: '600', 307 | bold: '700', 308 | extrabold: '800', 309 | black: '900', 310 | }, 311 | height: (theme) => ({ 312 | auto: 'auto', 313 | ...theme('spacing'), 314 | full: '100%', 315 | screen: '100vh', 316 | screen75: '75vh', 317 | }), 318 | inset: { 319 | 0: '0', 320 | 40: '40px', 321 | '1/2': '50%', 322 | auto: 'auto', 323 | }, 324 | letterSpacing: { 325 | tighter: '-0.05em', 326 | tight: '-0.025em', 327 | normal: '0', 328 | wide: '0.025em', 329 | wider: '0.05em', 330 | widest: '0.1em', 331 | }, 332 | lineHeight: { 333 | none: '1', 334 | tight: '1.25', 335 | snug: '1.375', 336 | normal: '1.5', 337 | relaxed: '1.625', 338 | loose: '2', 339 | 3: '.75rem', 340 | 4: '1rem', 341 | 5: '1.25rem', 342 | 6: '1.5rem', 343 | 7: '1.75rem', 344 | 8: '2rem', 345 | 9: '2.25rem', 346 | 10: '2.5rem', 347 | }, 348 | listStyleType: { 349 | none: 'none', 350 | disc: 'disc', 351 | decimal: 'decimal', 352 | }, 353 | margin: (theme, { negative }) => ({ 354 | auto: 'auto', 355 | ...theme('spacing'), 356 | ...negative(theme('spacing')), 357 | }), 358 | maxHeight: { 359 | full: '100%', 360 | screen: '100vh', 361 | }, 362 | maxWidth: (theme, { breakpoints }) => ({ 363 | none: 'none', 364 | xs: '20rem', 365 | sm: '24rem', 366 | md: '28rem', 367 | lg: '32rem', 368 | xl: '36rem', 369 | '2xl': '42rem', 370 | '3xl': '48rem', 371 | '4xl': '56rem', 372 | '5xl': '64rem', 373 | '6xl': '72rem', 374 | full: '100%', 375 | ...breakpoints(theme('screens')), 376 | }), 377 | minHeight: { 378 | 0: '0', 379 | full: '100%', 380 | screen: '100vh', 381 | }, 382 | minWidth: { 383 | 0: '0', 384 | full: '100%', 385 | 56: '14rem', 386 | }, 387 | objectPosition: { 388 | bottom: 'bottom', 389 | center: 'center', 390 | left: 'left', 391 | 'left-bottom': 'left bottom', 392 | 'left-top': 'left top', 393 | right: 'right', 394 | 'right-bottom': 'right bottom', 395 | 'right-top': 'right top', 396 | top: 'top', 397 | }, 398 | opacity: { 399 | 0: '0', 400 | 25: '0.25', 401 | 50: '0.5', 402 | 75: '0.75', 403 | 100: '1', 404 | }, 405 | order: { 406 | first: '-9999', 407 | last: '9999', 408 | none: '0', 409 | 1: '1', 410 | 2: '2', 411 | 3: '3', 412 | 4: '4', 413 | 5: '5', 414 | 6: '6', 415 | 7: '7', 416 | 8: '8', 417 | 9: '9', 418 | 10: '10', 419 | 11: '11', 420 | 12: '12', 421 | }, 422 | outline: { 423 | none: ['2px solid transparent', '2px'], 424 | white: ['2px dotted white', '2px'], 425 | black: ['2px dotted black', '2px'], 426 | }, 427 | padding: (theme) => theme('spacing'), 428 | placeholderColor: (theme) => theme('colors'), 429 | placeholderOpacity: (theme) => theme('opacity'), 430 | space: (theme, { negative }) => ({ 431 | ...theme('spacing'), 432 | ...negative(theme('spacing')), 433 | }), 434 | stroke: { 435 | current: 'currentColor', 436 | }, 437 | strokeWidth: { 438 | 0: '0', 439 | 1: '1', 440 | 2: '2', 441 | }, 442 | textColor: (theme) => theme('colors'), 443 | textOpacity: (theme) => theme('opacity'), 444 | width: (theme) => ({ 445 | auto: 'auto', 446 | ...theme('spacing'), 447 | '1/2': '50%', 448 | '1/3': '33.333333%', 449 | '2/3': '66.666667%', 450 | '1/4': '25%', 451 | '2/4': '50%', 452 | '3/4': '75%', 453 | '1/5': '20%', 454 | '2/5': '40%', 455 | '3/5': '60%', 456 | '4/5': '80%', 457 | '1/6': '16.666667%', 458 | '2/6': '33.333333%', 459 | '3/6': '50%', 460 | '4/6': '66.666667%', 461 | '5/6': '83.333333%', 462 | '1/10': '10%', 463 | '1/12': '8.333333%', 464 | '2/12': '16.666667%', 465 | '3/12': '25%', 466 | '4/12': '33.333333%', 467 | '5/12': '41.666667%', 468 | '6/12': '50%', 469 | '7/12': '58.333333%', 470 | '8/12': '66.666667%', 471 | '9/12': '75%', 472 | '10/12': '83.333333%', 473 | '11/12': '91.666667%', 474 | full: '100%', 475 | screen: '100vw', 476 | }), 477 | zIndex: { 478 | auto: 'auto', 479 | 0: '0', 480 | 10: '10', 481 | 20: '20', 482 | 30: '30', 483 | 40: '40', 484 | 50: '50', 485 | }, 486 | gap: (theme) => theme('spacing'), 487 | gridTemplateColumns: { 488 | none: 'none', 489 | 1: 'repeat(1, minmax(0, 1fr))', 490 | 2: 'repeat(2, minmax(0, 1fr))', 491 | 3: 'repeat(3, minmax(0, 1fr))', 492 | 4: 'repeat(4, minmax(0, 1fr))', 493 | 5: 'repeat(5, minmax(0, 1fr))', 494 | 6: 'repeat(6, minmax(0, 1fr))', 495 | 7: 'repeat(7, minmax(0, 1fr))', 496 | 8: 'repeat(8, minmax(0, 1fr))', 497 | 9: 'repeat(9, minmax(0, 1fr))', 498 | 10: 'repeat(10, minmax(0, 1fr))', 499 | 11: 'repeat(11, minmax(0, 1fr))', 500 | 12: 'repeat(12, minmax(0, 1fr))', 501 | }, 502 | gridAutoColumns: { 503 | auto: 'auto', 504 | min: 'min-content', 505 | max: 'max-content', 506 | fr: 'minmax(0, 1fr)', 507 | }, 508 | gridColumn: { 509 | auto: 'auto', 510 | 'span-1': 'span 1 / span 1', 511 | 'span-2': 'span 2 / span 2', 512 | 'span-3': 'span 3 / span 3', 513 | 'span-4': 'span 4 / span 4', 514 | 'span-5': 'span 5 / span 5', 515 | 'span-6': 'span 6 / span 6', 516 | 'span-7': 'span 7 / span 7', 517 | 'span-8': 'span 8 / span 8', 518 | 'span-9': 'span 9 / span 9', 519 | 'span-10': 'span 10 / span 10', 520 | 'span-11': 'span 11 / span 11', 521 | 'span-12': 'span 12 / span 12', 522 | 'span-full': '1 / -1', 523 | }, 524 | gridColumnStart: { 525 | auto: 'auto', 526 | 1: '1', 527 | 2: '2', 528 | 3: '3', 529 | 4: '4', 530 | 5: '5', 531 | 6: '6', 532 | 7: '7', 533 | 8: '8', 534 | 9: '9', 535 | 10: '10', 536 | 11: '11', 537 | 12: '12', 538 | 13: '13', 539 | }, 540 | gridColumnEnd: { 541 | auto: 'auto', 542 | 1: '1', 543 | 2: '2', 544 | 3: '3', 545 | 4: '4', 546 | 5: '5', 547 | 6: '6', 548 | 7: '7', 549 | 8: '8', 550 | 9: '9', 551 | 10: '10', 552 | 11: '11', 553 | 12: '12', 554 | 13: '13', 555 | }, 556 | gridTemplateRows: { 557 | none: 'none', 558 | 1: 'repeat(1, minmax(0, 1fr))', 559 | 2: 'repeat(2, minmax(0, 1fr))', 560 | 3: 'repeat(3, minmax(0, 1fr))', 561 | 4: 'repeat(4, minmax(0, 1fr))', 562 | 5: 'repeat(5, minmax(0, 1fr))', 563 | 6: 'repeat(6, minmax(0, 1fr))', 564 | }, 565 | gridAutoRows: { 566 | auto: 'auto', 567 | min: 'min-content', 568 | max: 'max-content', 569 | fr: 'minmax(0, 1fr)', 570 | }, 571 | gridRow: { 572 | auto: 'auto', 573 | 'span-1': 'span 1 / span 1', 574 | 'span-2': 'span 2 / span 2', 575 | 'span-3': 'span 3 / span 3', 576 | 'span-4': 'span 4 / span 4', 577 | 'span-5': 'span 5 / span 5', 578 | 'span-6': 'span 6 / span 6', 579 | 'span-full': '1 / -1', 580 | }, 581 | gridRowStart: { 582 | auto: 'auto', 583 | 1: '1', 584 | 2: '2', 585 | 3: '3', 586 | 4: '4', 587 | 5: '5', 588 | 6: '6', 589 | 7: '7', 590 | }, 591 | gridRowEnd: { 592 | auto: 'auto', 593 | 1: '1', 594 | 2: '2', 595 | 3: '3', 596 | 4: '4', 597 | 5: '5', 598 | 6: '6', 599 | 7: '7', 600 | }, 601 | transformOrigin: { 602 | center: 'center', 603 | top: 'top', 604 | 'top-right': 'top right', 605 | right: 'right', 606 | 'bottom-right': 'bottom right', 607 | bottom: 'bottom', 608 | 'bottom-left': 'bottom left', 609 | left: 'left', 610 | 'top-left': 'top left', 611 | }, 612 | scale: { 613 | 0: '0', 614 | 50: '.5', 615 | 75: '.75', 616 | 90: '.9', 617 | 95: '.95', 618 | 100: '1', 619 | 105: '1.05', 620 | 110: '1.1', 621 | 125: '1.25', 622 | 150: '1.5', 623 | }, 624 | rotate: { 625 | '-180': '-180deg', 626 | '-90': '-90deg', 627 | '-45': '-45deg', 628 | '-12': '-12deg', 629 | '-6': '-6deg', 630 | '-3': '-3deg', 631 | '-2': '-2deg', 632 | '-1': '-1deg', 633 | 0: '0', 634 | 1: '1deg', 635 | 2: '2deg', 636 | 3: '3deg', 637 | 6: '6deg', 638 | 12: '12deg', 639 | 45: '45deg', 640 | 90: '90deg', 641 | 180: '180deg', 642 | }, 643 | translate: (theme, { negative }) => ({ 644 | ...theme('spacing'), 645 | ...negative(theme('spacing')), 646 | '-full': '-100%', 647 | '-1/2': '-50%', 648 | '1/2': '50%', 649 | full: '100%', 650 | }), 651 | skew: { 652 | '-12': '-12deg', 653 | '-6': '-6deg', 654 | '-3': '-3deg', 655 | '-2': '-2deg', 656 | '-1': '-1deg', 657 | 0: '0', 658 | 1: '1deg', 659 | 2: '2deg', 660 | 3: '3deg', 661 | 6: '6deg', 662 | 12: '12deg', 663 | }, 664 | transitionProperty: { 665 | none: 'none', 666 | all: 'all', 667 | default: 668 | 'background-color, border-color, color, fill, stroke, opacity, box-shadow, transform', 669 | colors: 'background-color, border-color, color, fill, stroke', 670 | opacity: 'opacity', 671 | shadow: 'box-shadow', 672 | transform: 'transform', 673 | }, 674 | transitionTimingFunction: { 675 | linear: 'linear', 676 | in: 'cubic-bezier(0.4, 0, 1, 1)', 677 | out: 'cubic-bezier(0, 0, 0.2, 1)', 678 | 'in-out': 'cubic-bezier(0.4, 0, 0.2, 1)', 679 | }, 680 | transitionDuration: { 681 | 75: '75ms', 682 | 100: '100ms', 683 | 150: '150ms', 684 | 200: '200ms', 685 | 300: '300ms', 686 | 500: '500ms', 687 | 700: '700ms', 688 | 1000: '1000ms', 689 | }, 690 | transitionDelay: { 691 | 75: '75ms', 692 | 100: '100ms', 693 | 150: '150ms', 694 | 200: '200ms', 695 | 300: '300ms', 696 | 500: '500ms', 697 | 700: '700ms', 698 | 1000: '1000ms', 699 | }, 700 | animation: { 701 | none: 'none', 702 | spin: 'spin 1s linear infinite', 703 | ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite', 704 | pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite', 705 | bounce: 'bounce 1s infinite', 706 | }, 707 | keyframes: { 708 | spin: { 709 | to: { transform: 'rotate(360deg)' }, 710 | }, 711 | ping: { 712 | '75%, 100%': { transform: 'scale(2)', opacity: '0' }, 713 | }, 714 | pulse: { 715 | '50%': { opacity: '.5' }, 716 | }, 717 | bounce: { 718 | '0%, 100%': { 719 | transform: 'translateY(-25%)', 720 | animationTimingFunction: 'cubic-bezier(0.8,0,1,1)', 721 | }, 722 | '50%': { 723 | transform: 'none', 724 | animationTimingFunction: 'cubic-bezier(0,0,0.2,1)', 725 | }, 726 | }, 727 | }, 728 | }, 729 | variants: { 730 | accessibility: ['responsive', 'focus'], 731 | alignContent: ['responsive'], 732 | alignItems: ['responsive'], 733 | alignSelf: ['responsive'], 734 | appearance: ['responsive'], 735 | backgroundAttachment: ['responsive'], 736 | backgroundClip: ['responsive'], 737 | backgroundColor: ['responsive', 'hover', 'focus'], 738 | backgroundImage: ['responsive'], 739 | gradientColorStops: ['responsive', 'hover', 'focus'], 740 | backgroundOpacity: ['responsive', 'hover', 'focus'], 741 | backgroundPosition: ['responsive'], 742 | backgroundRepeat: ['responsive'], 743 | backgroundSize: ['responsive'], 744 | borderCollapse: ['responsive'], 745 | borderColor: ['responsive', 'hover', 'focus'], 746 | borderOpacity: ['responsive', 'hover', 'focus'], 747 | borderRadius: ['responsive'], 748 | borderStyle: ['responsive'], 749 | borderWidth: ['responsive'], 750 | boxShadow: ['responsive', 'hover', 'focus'], 751 | boxSizing: ['responsive'], 752 | container: ['responsive'], 753 | cursor: ['responsive'], 754 | display: ['responsive'], 755 | divideColor: ['responsive'], 756 | divideOpacity: ['responsive'], 757 | divideStyle: ['responsive'], 758 | divideWidth: ['responsive'], 759 | fill: ['responsive'], 760 | flex: ['responsive'], 761 | flexDirection: ['responsive'], 762 | flexGrow: ['responsive'], 763 | flexShrink: ['responsive'], 764 | flexWrap: ['responsive'], 765 | float: ['responsive'], 766 | clear: ['responsive'], 767 | fontFamily: ['responsive'], 768 | fontSize: ['responsive'], 769 | fontSmoothing: ['responsive'], 770 | fontVariantNumeric: ['responsive'], 771 | fontStyle: ['responsive'], 772 | fontWeight: ['responsive', 'hover', 'focus'], 773 | height: ['responsive'], 774 | inset: ['responsive'], 775 | justifyContent: ['responsive'], 776 | justifyItems: ['responsive'], 777 | justifySelf: ['responsive'], 778 | letterSpacing: ['responsive'], 779 | lineHeight: ['responsive'], 780 | listStylePosition: ['responsive'], 781 | listStyleType: ['responsive'], 782 | margin: ['responsive'], 783 | maxHeight: ['responsive'], 784 | maxWidth: ['responsive'], 785 | minHeight: ['responsive'], 786 | minWidth: ['responsive'], 787 | objectFit: ['responsive'], 788 | objectPosition: ['responsive'], 789 | opacity: ['responsive', 'hover', 'focus'], 790 | order: ['responsive'], 791 | outline: ['responsive', 'focus'], 792 | overflow: ['responsive'], 793 | overscrollBehavior: ['responsive'], 794 | padding: ['responsive'], 795 | placeContent: ['responsive'], 796 | placeItems: ['responsive'], 797 | placeSelf: ['responsive'], 798 | placeholderColor: ['responsive', 'focus'], 799 | placeholderOpacity: ['responsive', 'focus'], 800 | pointerEvents: ['responsive'], 801 | position: ['responsive'], 802 | resize: ['responsive'], 803 | space: ['responsive'], 804 | stroke: ['responsive'], 805 | strokeWidth: ['responsive'], 806 | tableLayout: ['responsive'], 807 | textAlign: ['responsive'], 808 | textColor: ['responsive', 'hover', 'focus'], 809 | textOpacity: ['responsive', 'hover', 'focus'], 810 | textDecoration: ['responsive', 'hover', 'focus'], 811 | textTransform: ['responsive'], 812 | userSelect: ['responsive'], 813 | verticalAlign: ['responsive'], 814 | visibility: ['responsive'], 815 | whitespace: ['responsive'], 816 | width: ['responsive'], 817 | wordBreak: ['responsive'], 818 | zIndex: ['responsive'], 819 | gap: ['responsive'], 820 | gridAutoFlow: ['responsive'], 821 | gridTemplateColumns: ['responsive'], 822 | gridAutoColumns: ['responsive'], 823 | gridColumn: ['responsive'], 824 | gridColumnStart: ['responsive'], 825 | gridColumnEnd: ['responsive'], 826 | gridTemplateRows: ['responsive'], 827 | gridAutoRows: ['responsive'], 828 | gridRow: ['responsive'], 829 | gridRowStart: ['responsive'], 830 | gridRowEnd: ['responsive'], 831 | transform: ['responsive'], 832 | transformOrigin: ['responsive'], 833 | scale: ['responsive', 'hover', 'focus'], 834 | rotate: ['responsive', 'hover', 'focus'], 835 | translate: ['responsive', 'hover', 'focus'], 836 | skew: ['responsive', 'hover', 'focus'], 837 | transitionProperty: ['responsive'], 838 | transitionTimingFunction: ['responsive'], 839 | transitionDuration: ['responsive'], 840 | transitionDelay: ['responsive'], 841 | animation: ['responsive'], 842 | }, 843 | corePlugins: {}, 844 | plugins: [], 845 | }; 846 | -------------------------------------------------------------------------------- /test/components/CurrentFilters/CurrentFilters.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import renderer from 'react-test-renderer'; 4 | 5 | import CurrentFilters from '../../../app/components/CurrentFilters'; 6 | 7 | describe('CurrentFilters component', () => { 8 | // TODO: Update to match new schema 9 | let filterOptions = { 10 | container_id: [], 11 | container_name: [], 12 | container_image: [], 13 | status: [], 14 | stream: [], 15 | timestamp: { 16 | from: '', 17 | to: '', 18 | }, 19 | host_ip: [], 20 | host_port: [], 21 | log_level: [], 22 | }; 23 | 24 | const setFilterOptions = (newState) => { 25 | filterOptions = newState; 26 | }; 27 | 28 | it('should match exact snapshot', () => { 29 | const tree = renderer 30 | .create( 31 | 35 | ) 36 | .toJSON(); 37 | expect(tree).toMatchSnapshot(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/components/CurrentFilters/__snapshots__/CurrentFilters.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CurrentFilters component should match exact snapshot 1`] = ` 4 |
8 | `; 9 | -------------------------------------------------------------------------------- /test/components/Filter/Filter.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import renderer from 'react-test-renderer'; 4 | 5 | import Filter from '../../../app/components/Filter'; 6 | 7 | describe('Filter component', () => { 8 | // TODO: Update to match new schema 9 | let filterOptions = { 10 | container_id: [], 11 | container_name: [], 12 | container_image: [], 13 | status: [], 14 | stream: [], 15 | timestamp: { 16 | from: '', 17 | to: '', 18 | }, 19 | host_ip: [], 20 | host_port: [], 21 | log_level: [], 22 | }; 23 | 24 | const setFilterOptions = (newState) => { 25 | filterOptions = newState; 26 | }; 27 | 28 | it('should match exact snapshot', () => { 29 | const tree = renderer 30 | .create( 31 | 35 | ) 36 | .toJSON(); 37 | expect(tree).toMatchSnapshot(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/components/Filter/__snapshots__/Filter.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Filter component should match exact snapshot 1`] = ` 4 |
8 |
11 |
14 |
15 | 18 | 40 | 41 |
42 |
43 |
44 |
48 |
49 | `; 50 | -------------------------------------------------------------------------------- /test/components/FilterInput/FilterInput.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import renderer from 'react-test-renderer'; 4 | 5 | import FilterInput from '../../../app/components/FilterInput'; 6 | 7 | describe('FilterInput component', () => { 8 | // TODO: Update to match new schema 9 | let filterOptions = { 10 | container_id: [], 11 | container_name: [], 12 | container_image: [], 13 | status: [], 14 | stream: [], 15 | timestamp: { 16 | from: '', 17 | to: '', 18 | }, 19 | host_ip: [], 20 | host_port: [], 21 | log_level: [], 22 | }; 23 | 24 | const setFilterOptions = (newState) => { 25 | filterOptions = newState; 26 | }; 27 | 28 | let selection = ''; 29 | 30 | let userInput = ''; 31 | const setUserInput = (input) => (userInput = input); 32 | 33 | it('should match exact snapshot', () => { 34 | const tree = renderer 35 | .create( 36 | 43 | ) 44 | .toJSON(); 45 | expect(tree).toMatchSnapshot(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/components/FilterInput/__snapshots__/FilterInput.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`FilterInput component should match exact snapshot 1`] = ` 4 |
7 |
10 | 19 | 26 |
27 |
28 | `; 29 | -------------------------------------------------------------------------------- /test/components/FilterOptions/FilterOptions.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import renderer from 'react-test-renderer'; 4 | 5 | import FilterOptions from '../../../app/components/FilterOptions'; 6 | 7 | describe('FilterOptions component', () => { 8 | let selection = ''; 9 | const setSelection = (newSelection) => (selection = newSelection); 10 | 11 | let isOpen = false; 12 | const setIsOpen = () => (isOpen = false); 13 | 14 | it('should match exact snapshot', () => { 15 | const tree = renderer 16 | .create( 17 | 18 | ) 19 | .toJSON(); 20 | expect(tree).toMatchSnapshot(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/components/FilterOptions/__snapshots__/FilterOptions.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`FilterOptions component should match exact snapshot 1`] = ` 4 | 73 | `; 74 | -------------------------------------------------------------------------------- /test/components/FilterStreamOptions/FilterStreamOptions.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import renderer from 'react-test-renderer'; 4 | 5 | import FilterStreamOptions from '../../../app/components/FilterStreamOptions'; 6 | 7 | describe('FilterStreamOptions component', () => { 8 | // TODO: Update to match new schema 9 | let filterOptions = { 10 | container_id: [], 11 | container_name: [], 12 | container_image: [], 13 | status: [], 14 | stream: [], 15 | timestamp: { 16 | from: '', 17 | to: '', 18 | }, 19 | host_ip: [], 20 | host_port: [], 21 | log_level: [], 22 | }; 23 | 24 | const setFilterOptions = (newState) => (filterOptions = newState); 25 | 26 | it('should match exact snapshot', () => { 27 | const tree = renderer 28 | .create( 29 | 33 | ) 34 | .toJSON(); 35 | expect(tree).toMatchSnapshot(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/components/FilterStreamOptions/__snapshots__/FilterStreamOptions.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`FilterStreamOptions component should match exact snapshot 1`] = ` 4 |
7 | 21 |
24 | 29 | 32 | 33 |
34 |
35 | `; 36 | -------------------------------------------------------------------------------- /test/components/FilterTimeOptions/FilterTimeOptions.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import renderer from 'react-test-renderer'; 4 | 5 | import FilterTimeOptions from '../../../app/components/FilterTimeOptions'; 6 | 7 | describe('FilterTimeOptions component', () => { 8 | // TODO: Update to match schema 9 | let filterOptions = { 10 | container_id: [], 11 | container_name: [], 12 | container_image: [], 13 | status: [], 14 | stream: [], 15 | timestamp: { 16 | from: '', 17 | to: '', 18 | }, 19 | host_ip: [], 20 | host_port: [], 21 | log_level: [], 22 | }; 23 | const setFilterOptions = (newState) => (filterOptions = newState); 24 | 25 | let fromTimeStamp = { date: '', time: '' }; 26 | const setFromTimeStamp = (newTimeStamp) => (fromTimeStamp = newTimeStamp); 27 | 28 | let toTimeStamp = { date: '', time: '' }; 29 | const setToTimeStamp = (newTimeStamp) => (toTimeStamp = newTimeStamp); 30 | 31 | Date.now = jest.fn(() => 1604781916895); 32 | 33 | it('should match exact snapshot', () => { 34 | const tree = renderer 35 | .create( 36 | 44 | ) 45 | .toJSON(); 46 | expect(tree).toMatchSnapshot(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/components/FilterTimeOptions/__snapshots__/FilterTimeOptions.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`FilterTimeOptions component should match exact snapshot 1`] = ` 4 |
7 |
10 | From: 11 |
12 | 18 | 24 |
27 | To: 28 |
29 | 35 | 41 | 48 |
49 | `; 50 | -------------------------------------------------------------------------------- /test/components/LogsRows/LogsRows.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import renderer from 'react-test-renderer'; 4 | 5 | import LogsRows from '../../../app/components/LogsRows'; 6 | 7 | describe('LogsRows component', () => { 8 | // TODO: Update to match new schema 9 | let filterOptions = { 10 | container_id: [], 11 | container_name: [], 12 | container_image: [], 13 | status: [], 14 | stream: [], 15 | timestamp: { 16 | from: '', 17 | to: '', 18 | }, 19 | host_ip: [], 20 | host_port: [], 21 | log_level: [], 22 | }; 23 | 24 | let logs = []; 25 | 26 | it('should match exact snapshot', () => { 27 | const tree = renderer 28 | .create() 29 | .toJSON(); 30 | expect(tree).toMatchSnapshot(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/components/LogsRows/__snapshots__/LogsRows.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`LogsRows component should match exact snapshot 1`] = `null`; 4 | -------------------------------------------------------------------------------- /test/components/LogsTable/LogsTable.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import renderer from 'react-test-renderer'; 4 | 5 | import LogsTable from '../../../app/components/LogsTable'; 6 | import LogsRows from '../../../app/components/LogsRows'; 7 | 8 | describe('LogsTable component', () => { 9 | let wrapper; 10 | // TODO: Change filterOptions to activeFilters 11 | // TODO: Update to match new schema 12 | const filterOptions = { 13 | container_id: [], 14 | container_name: [], 15 | container_image: [], 16 | status: [], 17 | stream: [], 18 | timestamp: { 19 | from: '', 20 | to: '', 21 | }, 22 | host_ip: [], 23 | host_port: [], 24 | log_level: [], 25 | }; 26 | 27 | beforeAll(() => { 28 | wrapper = shallow(); 29 | }); 30 | 31 | it('should match exact snapshot', () => { 32 | const tree = renderer 33 | .create() 34 | .toJSON(); 35 | 36 | expect(tree).toMatchSnapshot(); 37 | }); 38 | 39 | it('should have one LogsRows component', () => { 40 | expect(wrapper.find(LogsRows)).toHaveLength(1); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/components/LogsTable/__snapshots__/LogsTable.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`LogsTable component should match exact snapshot 1`] = ` 4 |
7 |
10 |
13 |
16 | 19 | 22 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 61 | 62 | 70 |
28 | Timestamp 29 | 33 | Log 34 | 38 | Container ID 39 | 43 | Name 44 | 48 | Image 49 | 53 | Host Port 54 | 58 | Stream 59 |
71 |
72 |
73 |
74 |
75 | `; 76 | -------------------------------------------------------------------------------- /test/components/Navbar/Navbar.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import renderer from 'react-test-renderer'; 4 | 5 | import Navbar from '../../../app/components/Navbar'; 6 | 7 | describe('Navbar component', () => { 8 | let wrapper; 9 | 10 | beforeAll(() => { 11 | wrapper = shallow(); 12 | }); 13 | 14 | it('should match exact snapshot', () => { 15 | const tree = renderer.create().toJSON(); 16 | expect(tree).toMatchSnapshot(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/components/Navbar/__snapshots__/Navbar.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Navbar component should match exact snapshot 1`] = ` 4 | 159 | `; 160 | -------------------------------------------------------------------------------- /test/e2e/HomePage.e2e.ts: -------------------------------------------------------------------------------- 1 | /* eslint jest/expect-expect: off, jest/no-test-callback: off */ 2 | import { ClientFunction, Selector } from 'testcafe'; 3 | 4 | const getPageUrl = ClientFunction(() => window.location.href); 5 | const getPageTitle = ClientFunction(() => document.title); 6 | const counterSelector = Selector('[data-tid="counter"]'); 7 | const buttonsSelector = Selector('[data-tclass="btn"]'); 8 | const clickToCounterLink = (t) => 9 | t.click(Selector('a').withExactText('to Counter')); 10 | const incrementButton = buttonsSelector.nth(0); 11 | const decrementButton = buttonsSelector.nth(1); 12 | const oddButton = buttonsSelector.nth(2); 13 | const asyncButton = buttonsSelector.nth(3); 14 | const getCounterText = () => counterSelector().innerText; 15 | const assertNoConsoleErrors = async (t) => { 16 | const { error } = await t.getBrowserConsoleMessages(); 17 | await t.expect(error).eql([]); 18 | }; 19 | 20 | fixture`Home Page`.page('../../app/app.html').afterEach(assertNoConsoleErrors); 21 | 22 | test('e2e', async (t) => { 23 | await t.expect(getPageTitle()).eql('Hello Electron React!'); 24 | }); 25 | 26 | test('should open window and contain expected page title', async (t) => { 27 | await t.expect(getPageTitle()).eql('Hello Electron React!'); 28 | }); 29 | 30 | test( 31 | 'should not have any logs in console of main window', 32 | assertNoConsoleErrors 33 | ); 34 | 35 | test('should navigate to Counter with click on the "to Counter" link', async (t) => { 36 | await t.click('[data-tid=container] > a').expect(getCounterText()).eql('0'); 37 | }); 38 | 39 | test('should navigate to /counter', async (t) => { 40 | await t.click('a').expect(getPageUrl()).contains('/counter'); 41 | }); 42 | 43 | fixture`Counter Tests` 44 | .page('../../app/app.html') 45 | .beforeEach(clickToCounterLink) 46 | .afterEach(assertNoConsoleErrors); 47 | 48 | test('should display updated count after the increment button click', async (t) => { 49 | await t.click(incrementButton).expect(getCounterText()).eql('1'); 50 | }); 51 | 52 | test('should display updated count after the descrement button click', async (t) => { 53 | await t.click(decrementButton).expect(getCounterText()).eql('-1'); 54 | }); 55 | 56 | test('should not change even counter if odd button clicked', async (t) => { 57 | await t.click(oddButton).expect(getCounterText()).eql('0'); 58 | }); 59 | 60 | test('should change odd counter if odd button clicked', async (t) => { 61 | await t 62 | .click(incrementButton) 63 | .click(oddButton) 64 | .expect(getCounterText()) 65 | .eql('2'); 66 | }); 67 | 68 | test('should change if async button clicked and a second later', async (t) => { 69 | await t 70 | .click(asyncButton) 71 | .expect(getCounterText()) 72 | .eql('0') 73 | .expect(getCounterText()) 74 | .eql('1'); 75 | }); 76 | 77 | test('should back to home if back button clicked', async (t) => { 78 | await t 79 | .click('[data-tid="backButton"] > a') 80 | .expect(Selector('[data-tid="container"]').visible) 81 | .ok(); 82 | }); 83 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "CommonJS", 5 | "lib": ["dom", "esnext"], 6 | "declaration": true, 7 | "declarationMap": true, 8 | "noEmit": true, 9 | "jsx": "react", 10 | "strict": true, 11 | "pretty": true, 12 | "sourceMap": true, 13 | /* Additional Checks */ 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | /* Module Resolution Options */ 19 | "moduleResolution": "node", 20 | "esModuleInterop": true, 21 | "allowSyntheticDefaultImports": true, 22 | "resolveJsonModule": true, 23 | "allowJs": true 24 | }, 25 | "exclude": [ 26 | "test", 27 | "release", 28 | "app/main.prod.js", 29 | "app/renderer.prod.js", 30 | "app/dist", 31 | "dll" 32 | ] 33 | } 34 | --------------------------------------------------------------------------------