├── .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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
28 | Timestamp
29 | |
30 |
33 | Log
34 | |
35 |
38 | Container ID
39 | |
40 |
43 | Name
44 | |
45 |
48 | Image
49 | |
50 |
53 | Host Port
54 | |
55 |
58 | Stream
59 | |
60 |
61 |
62 |
70 |
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 |
--------------------------------------------------------------------------------