├── .babelrc
├── .eslintrc.json
├── .gitignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── __tests__
└── enzyme.js
├── backend
├── controllers
│ ├── logController.js
│ └── metricsController.js
└── index.js
├── index.html
├── index.js
├── package-lock.json
├── package.json
├── src
├── App.jsx
├── assets
│ ├── images
│ │ └── logo.png
│ └── stylesheets
│ │ ├── _about.scss
│ │ ├── _cssReset.scss
│ │ ├── _loading.scss
│ │ ├── _noSearchResults.scss
│ │ ├── _splash.scss
│ │ ├── _table.scss
│ │ ├── _variables.scss
│ │ └── app.scss
├── components
│ ├── about
│ │ ├── About.jsx
│ │ └── index.js
│ ├── barChart
│ │ ├── BarChart.jsx
│ │ ├── barChartConstants.js
│ │ └── index.js
│ ├── index.js
│ ├── loading
│ │ ├── Loading.jsx
│ │ └── index.js
│ ├── log
│ │ ├── Download.jsx
│ │ ├── Log.jsx
│ │ ├── NoSearchResults.jsx
│ │ ├── Table.jsx
│ │ ├── index.js
│ │ └── logConstants.jsx
│ ├── metrics
│ │ ├── BarChartContainer.jsx
│ │ ├── Metrics.jsx
│ │ └── index.js
│ ├── navbar
│ │ ├── NavButton.jsx
│ │ ├── Navbar.jsx
│ │ ├── index.js
│ │ └── navbarConstants.js
│ ├── splash
│ │ ├── Splash.jsx
│ │ └── index.js
│ └── utils
│ │ └── index.js
└── index.js
├── utils
└── index.js
├── webpack.common.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react"]
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true,
5 | "node": true
6 | },
7 | "extends": [
8 | "airbnb",
9 | "plugin:prettier/recommended",
10 | "plugin:react/recommended",
11 | "plugin:react-hooks/recommended",
12 | "plugin:jsx-a11y/recommended"
13 | ],
14 | "parserOptions": {
15 | "ecmaFeatures": {
16 | "jsx": true
17 | },
18 | "ecmaVersion": 12,
19 | "sourceType": "module"
20 | },
21 | "plugins": ["prettier", "react"],
22 | "rules": {
23 | "indent": ["warn", 2],
24 | "arrow-parens": "off",
25 | "consistent-return": "off",
26 | "func-names": "off",
27 | "no-console": "off",
28 | "radix": "off",
29 | "react/button-has-type": "off",
30 | "react/destructuring-assignment": "off",
31 | "react/jsx-filename-extension": "off",
32 | "react/prop-types": "off",
33 | "semi": ["warn", "always"],
34 | "space-infix-ops": "warn",
35 | "quotes": ["warn", "single"],
36 | "prefer-const": "warn",
37 | "no-unused-vars": ["off", { "vars": "local" }],
38 | "no-case-declarations": "off"
39 | },
40 | "settings": {
41 | "react": { "version": "detect" },
42 | "import/resolver": {
43 | "node": {
44 | "extensions": [".js", ".jsx", ".ts", ".tsx"]
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 | build
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # TypeScript v1 declaration files
46 | typings/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Microbundle cache
58 | .rpt2_cache/
59 | .rts2_cache_cjs/
60 | .rts2_cache_es/
61 | .rts2_cache_umd/
62 |
63 | # Optional REPL history
64 | .node_repl_history
65 |
66 | # Output of 'npm pack'
67 | *.tgz
68 |
69 | # Yarn Integrity file
70 | .yarn-integrity
71 |
72 | # dotenv environment variables file
73 | .env
74 | .env.test
75 |
76 | # parcel-bundler cache (https://parceljs.org/)
77 | .cache
78 |
79 | # Next.js build output
80 | .next
81 |
82 | # Nuxt.js build / generate output
83 | .nuxt
84 | dist
85 |
86 | # Gatsby files
87 | .cache/
88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
89 | # https://nextjs.org/blog/next-9-1#public-directory-support
90 | # public
91 |
92 | # vuepress build output
93 | .vuepress/dist
94 |
95 | # Serverless directories
96 | .serverless/
97 |
98 | # FuseBox cache
99 | .fusebox/
100 |
101 | # DynamoDB Local files
102 | .dynamodb/
103 |
104 | # TernJS port file
105 | .tern-port
106 |
107 | .DS_Store
108 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "semi": true,
4 | "singleQuote": true,
5 | "tabWidth": 2,
6 | "trailingComma": "all"
7 | }
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 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 | # Kubiquity
7 |
8 |
9 | 
10 | 
11 | 
12 | [](https://github.com/oslabs-beta/Kubiquity#readme)
13 | [](https://github.com/oslabs-beta/Kubiquity/blob/master/LICENSE)
14 | [](https://twitter.com/kubiquityapp)
15 |
16 |
17 | ### 🏠 [Homepage](https://kubiquity.io)
18 |
19 | >Kubiquity is a real-time Kubernetes error-monitoring tool. Kubiquity runs locally as an Electron application and incorporates Prometheus metrics queries for a cluster's CPU and memory usage by node.
20 |
21 |
22 |
23 | # Preview
24 |
25 | 
26 | # Table of contents
27 |
28 | - [Kubiquity](#kubiquity)
29 | - [🏠 Homepage](#-homepage)
30 | - [Preview](#preview)
31 | - [Table of contents](#table-of-contents)
32 | - [Installation](#installation)
33 | - [Usage Requirements](#usage-requirements)
34 | - [Planned implementations](#planned-implementations)
35 | - [Contributions](#contributions)
36 | - [Contributors](#contributors)
37 | - [License](#license)
38 |
39 | # Installation
40 | [(Back to top)](#table-of-contents)
41 |
42 | Visit our [releases page](https://github.com/oslabs-beta/Kubiquity/releases) to download the latest release for your operating system.
43 |
44 | To package the app from the source code, fork and clone the repo, then run the following commands:
45 |
46 | 1. ```yarn```
47 | 2. ```yarn run package-linux``` or ```yarn run package-win``` based on your OS.
48 | 3. The app will be created and placed in the ```release-builds``` folder.
49 |
50 | # Usage Requirements
51 | [(Back to top)](#table-of-contents)
52 |
53 |
54 | - To start, spin up a Kubernetes cluster.
55 | - If not already using Prometheus, follow these steps to install and run Prometheus: https://prometheus-community.github.io/helm-charts
56 | - Download the Kubiquity from our releases or package the app yourself
57 | - Start up Kubiquity
58 |
59 |
60 | # Planned implementations
61 | [(Back to top)](#table-of-contents)
62 |
63 | - [ ] Add Mac OS support.
64 | - [ ] Add time travel debugging by saving a snapshot of the current state of the cluster.
65 | - [ ] Store and display CPU and memory usage over time.
66 | - [ ] Add recommended course of action based on warnings and errors.
67 |
68 | # Contributions
69 | [(Back to top)](#table-of-contents)
70 |
71 | Kubiquity is currently in beta and is being actively developed by the K8sM8s team.
72 |
73 | We also welcome contributions from the community. If you are interested in contributing to the project, please contact us at kubiquityapp@gmail.com or fork the project and submit a pull request.
74 |
75 | # Contributors
76 | [(Back to top)](#table-of-contents)
77 |
78 |
79 |
80 |
81 | **David Anderson**
82 |
83 | **[](https://github.com/dlande000) [](https://www.linkedin.com/in/dlande000/)**
84 |
85 |
86 |
87 |
88 |
89 |
90 | **Robert Hernandez**
91 |
92 | **[](https://github.com/Hydroelectric29) [](https://www.linkedin.com/in/robert-hernandez-879108211/)**
93 |
94 |
95 |
96 |
97 | **David Zhang**
98 |
99 | **[](https://github.com/davidzhangnyc) [](https://www.linkedin.com/in/davidnyc/)**
100 |
101 |
102 |
103 |
104 | **Jeffrey Zheng**
105 |
106 | **[](https://github.com/JefZheng) [](https://www.linkedin.com/in/JefZheng/)**
107 |
108 |
109 | # License
110 | [(Back to top)](#table-of-contents)
111 |
112 | Copyright © 2021 [k8sm8s](https://github.com/oslabs-beta).
113 |
114 | This product is [MIT](https://github.com/oslabs-beta/Kubiquity/blob/master/LICENSE) licensed.
115 |
116 | This product is accelerated by [OS Labs](https://opensourcelabs.io).
117 |
--------------------------------------------------------------------------------
/__tests__/enzyme.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 |
5 | import React from 'react';
6 | import { configure, shallow } from 'enzyme';
7 | import Adapter from 'enzyme-adapter-react-16';
8 |
9 | import NavBar from '../src/components/navBar';
10 | import BarChart from '../src/components/barChart';
11 |
12 | configure({ adapter: new Adapter() });
13 |
14 | describe('React unit tests', () => {
15 | let wrapper;
16 |
17 | describe('NavBar component', () => {
18 | const props = {
19 | setIsLogShowing: jest.fn(() => console.log('setIsLogShowing')),
20 | setAreMetricsShowing: jest.fn(() => console.log('setAreMetricsShowing')),
21 | setIsAboutShowing: jest.fn(() => console.log('setIsAboutShowing')),
22 | };
23 |
24 | beforeAll(() => {
25 | wrapper = shallow();
26 | });
27 |
28 | it('should render an unordered list of buttons', () => {
29 | expect(wrapper.find('ul').length).toBe(1);
30 | });
31 | });
32 |
33 | describe('BarChart component', () => {
34 | const props = {
35 | data: [],
36 | categories: [],
37 | };
38 |
39 | beforeAll(() => {
40 | wrapper = shallow();
41 | });
42 |
43 | it('should render without errors', () => {
44 | expect(wrapper.length).toBe(1);
45 | });
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/backend/controllers/logController.js:
--------------------------------------------------------------------------------
1 | const cmd = require('node-cmd');
2 | const storage = require('electron-json-storage');
3 |
4 | const LOGS = 'logs';
5 |
6 | const HEADERS = [
7 | 'NAMESPACE',
8 | 'LAST SEEN',
9 | 'TYPE',
10 | 'REASON',
11 | 'OBJECT',
12 | 'MESSAGE',
13 | ];
14 |
15 | const logController = {};
16 |
17 | logController.queryLog = () => {
18 | try {
19 | // runs a terminal command that gets the event logs from cluster
20 | const logList = cmd
21 | .runSync('kubectl get events --all-namespaces')
22 | .data.split('\n');
23 |
24 | // pops off last element in logList because it is an empty string
25 | logList.pop();
26 | return logList;
27 | } catch (err) {
28 | console.log('Error in queryLog: ', err);
29 | throw err;
30 | }
31 | };
32 |
33 | logController.formatLog = (array) => {
34 | // Stores indices of the beginning of the columns because indeterminate white space between columns
35 | const headersIndices = HEADERS.map((header) => array[0].indexOf(header));
36 |
37 | return array.map((el) => {
38 | const log = [];
39 |
40 | for (let j = 0; j < headersIndices.length - 1; j++) {
41 | // trims white space of elements
42 | let idx1 = headersIndices[j];
43 | let idx2 = headersIndices[j + 1];
44 | let formattedLog = el.slice(idx1, idx2).trim();
45 |
46 | log.push(formattedLog);
47 |
48 | // provides ending index of each row and formats the element
49 | if (j === headersIndices.length - 2) {
50 | idx1 = idx2;
51 | idx2 = el.length;
52 | formattedLog = el.slice(idx1, idx2).trim();
53 |
54 | log.push(formattedLog);
55 | }
56 | }
57 |
58 | return log;
59 | });
60 | };
61 |
62 | logController.saveLog = async (log) => {
63 | // Shift out first element of array because it is the header row
64 | log.shift();
65 |
66 | try {
67 | const allLogs = log.map(
68 | ([namespace, lastSeen, type, reason, object, message]) => {
69 | const createdAt = new Date().toISOString();
70 |
71 | return {
72 | namespace,
73 | lastSeen,
74 | type,
75 | reason,
76 | object,
77 | message,
78 | createdAt,
79 | };
80 | },
81 | );
82 |
83 | storage.set(LOGS, { data: allLogs }, (err) => {
84 | if (err) {
85 | console.log('Error in saveLog: ', err);
86 | throw err;
87 | }
88 | });
89 |
90 | return;
91 | } catch (err) {
92 | console.log('Error in saveLog: ', err);
93 | throw err;
94 | }
95 | };
96 |
97 | logController.getLog = async () => {
98 | try {
99 | const logs = await storage.getSync(LOGS);
100 | return logs.data;
101 | } catch (err) {
102 | console.log('Error in getLog: ', err);
103 | throw err;
104 | }
105 | };
106 |
107 | module.exports = logController;
108 |
--------------------------------------------------------------------------------
/backend/controllers/metricsController.js:
--------------------------------------------------------------------------------
1 | const fetch = require('node-fetch');
2 | const cmd = require('node-cmd');
3 | const { spawn } = require('child_process');
4 |
5 | const PROM_URL = 'http://127.0.0.1:9090/api/v1/';
6 |
7 | const metricsController = {};
8 |
9 | let isPromUp = false;
10 |
11 | const forwardPromPort = async () => {
12 | // Run command to get pod name for prometheus instance and save to variable
13 | const [promPodName] = cmd
14 | .runSync(
15 | 'export POD_NAME=$(kubectl get pods --all-namespaces -l "app=prometheus,component=server" -o jsonpath="{.items[0].metadata.name}") && echo $POD_NAME',
16 | )
17 | .data.split('\n');
18 |
19 | // Spawns a new persistent process to forward the port 9090 to 9090 in the prometheus pod
20 | const portForward = await spawn('kubectl', [
21 | '--namespace=prometheus',
22 | 'port-forward',
23 | promPodName,
24 | '9090',
25 | ]);
26 |
27 | // if the process is successful, resolve the promise
28 | await portForward.stdout.on('data', (data) => {
29 | console.log(`stdout: ${data}`);
30 | isPromUp = true;
31 | });
32 |
33 | // if the process fails, reject the promise
34 | await portForward.stderr.on('data', (data) => {
35 | console.log(`stderr: ${data}`);
36 | });
37 |
38 | // if prometheus is already running, resolve the promise
39 | await portForward.on('close', (code) => {
40 | if (code === 1) console.log('Prometheus is already running...');
41 | isPromUp = true;
42 | });
43 | };
44 |
45 | metricsController.getMemory = async () => {
46 | if (!isPromUp) await forwardPromPort();
47 |
48 | const currentDate = new Date().toISOString();
49 | // Sums the memory usage rate of all containers and splitting them by pod name
50 | const query = `query_range?query=sum(rate(container_memory_usage_bytes[2m])) by (pod) &start=${currentDate}&end=${currentDate}&step=1m`;
51 |
52 | try {
53 | const data = await fetch(PROM_URL + query);
54 | const results = await data.json();
55 | const memArr = results.data.result;
56 |
57 | // Parses the memory usage and formats it into an array of objects with podId and memory usage
58 | return memArr
59 | .reduce((pods, { values, metric: { pod: podId } }) => {
60 | let memory = values[0][1];
61 |
62 | if (podId) {
63 | memory = Math.round(parseFloat(memory));
64 |
65 | const pod = {
66 | podId,
67 | memory,
68 | };
69 |
70 | pods.push(pod);
71 | }
72 |
73 | return pods;
74 | }, [])
75 | .sort((a, b) => b.memory - a.memory);
76 | } catch (err) {
77 | console.log('Error in getMemory: ', err);
78 | throw err;
79 | }
80 | };
81 |
82 | metricsController.getCPU = async () => {
83 | if (!isPromUp) await forwardPromPort();
84 |
85 | const currentDate = new Date().toISOString();
86 | // Sums the CPU usage rate of all containers with an image and splitting them by pod name
87 | const query = `query_range?query=sum(rate(container_cpu_usage_seconds_total{image!=""}[2m])) by (pod)&start=${currentDate}&end=${currentDate}&step=1m`;
88 |
89 | try {
90 | const data = await fetch(PROM_URL + query);
91 | const results = await data.json();
92 | const cpuArr = results.data.result;
93 |
94 | // Formats the cpuArr into an array of objects with podname and cpu usage as properties
95 | return cpuArr
96 | .map(({ metric: { pod: podId }, values }) => {
97 | const cpuUsage = Math.round(values[0][1] * 1000) / 10;
98 |
99 | return {
100 | podId,
101 | cpuUsage,
102 | };
103 | })
104 | .sort((a, b) => b.cpuUsage - a.cpuUsage);
105 | } catch (err) {
106 | console.log('Error in getCPU: ', err);
107 | throw err;
108 | }
109 | };
110 |
111 | module.exports = metricsController;
112 |
--------------------------------------------------------------------------------
/backend/index.js:
--------------------------------------------------------------------------------
1 | const { getMemory: fetchMemory, getCPU } = require('./controllers/metricsController');
2 | const {
3 | queryLog,
4 | formatLog,
5 | saveLog,
6 | getLog: fetchLog,
7 | } = require('./controllers/logController');
8 |
9 | const getMemory = async () => await fetchMemory();
10 |
11 | const getCpuUse = async () => await getCPU();
12 |
13 | const getLog = async () => {
14 | let log = await queryLog();
15 | log = formatLog(log);
16 | await saveLog(log);
17 | return await fetchLog();
18 | };
19 |
20 | module.exports = { getLog, getMemory, getCpuUse };
21 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Kubiquity
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const { BrowserWindow, app, ipcMain } = require('electron');
2 | const path = require('path');
3 | const electronReload = require('electron-reload');
4 |
5 | const { getLog, getMemory, getCpuUse } = require('./backend');
6 |
7 | const {
8 | GET_LOG,
9 | GET_MEMORY,
10 | GET_CPU_USE,
11 | GOT_LOG,
12 | GOT_MEMORY,
13 | GOT_CPU_USE,
14 | } = require('./utils');
15 |
16 | const ELECTRON_MODULE_PATH = path.join(
17 | __dirname,
18 | 'node_modules',
19 | '.bin',
20 | 'electron'
21 | );
22 |
23 | const ELECTRON_RELOAD_OPTIONS = {
24 | electron: ELECTRON_MODULE_PATH,
25 | };
26 |
27 | const BROWSER_WINDOW_SETTINGS = {
28 | width: 1200,
29 | height: 800,
30 | backgroundColor: 'white',
31 | webPreferences: {
32 | nodeIntegration: true,
33 | contextIsolation: false,
34 | enableRemoteModule: true,
35 | worldSafeExecuteJavaScript: true,
36 | },
37 | };
38 |
39 | const createWindow = () => {
40 | new BrowserWindow(BROWSER_WINDOW_SETTINGS)
41 | .loadFile('index.html');
42 | };
43 |
44 | electronReload(__dirname, ELECTRON_RELOAD_OPTIONS);
45 |
46 | app
47 | .whenReady()
48 | .then(createWindow)
49 | .catch(err => {
50 | throw Error(`Error while launching app: ${ err }`);
51 | });
52 |
53 | ipcMain.on(GET_LOG, async (event, _) => {
54 | const log = await getLog();
55 | event.sender.send(GOT_LOG, JSON.stringify(log));
56 | });
57 |
58 | ipcMain.on(GET_MEMORY, async (event, _) => {
59 | const memory = await getMemory();
60 | event.sender.send(GOT_MEMORY, JSON.stringify(memory));
61 | });
62 |
63 | ipcMain.on(GET_CPU_USE, async (event, _) => {
64 | const cpuUse = await getCpuUse();
65 | event.sender.send(GOT_CPU_USE, JSON.stringify(cpuUse));
66 | });
67 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kubiquity",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "electron .",
8 | "build": "rm -rf build && webpack --mode production --config webpack.common.js",
9 | "dev": "rm -rf build && concurrently 'webpack --config webpack.common.js --watch --mode development' 'electron .'",
10 | "frontend": "rm -rf build && concurrently 'webpack --config webpack.common.js --watch --mode development' 'electron .'",
11 | "backend": "nodemon server/server.js",
12 | "react": "rm -rf build && webpack --config webpack.common.js --watch --mode development",
13 | "test": "jest --verbose",
14 | "package-linux": "electron-packager . --overwrite --platform=linux --arch=x64 --icon=src/assets/images/icon.png --prune=true --out=release-builds",
15 | "package-win": "electron-packager . --overwrite --asar=true --platform=win32 --arch=ia32 --icon=assets/images/icon.ico --prune=true --out=release-builds --version-string.CompanyName=K8sM8s --version-string.FileDescription=CE --version-string.ProductName=\"Kubiquity\"",
16 | "package-mac": "electron-packager . --overwrite --platform=darwin --arch=x64 --icon=assets/icons/mac/icon.icns --prune=true --out=release-builds"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git+https://github.com/oslabs-beta/Kubiquity.git"
21 | },
22 | "author": "k8sm8s",
23 | "license": "MIT",
24 | "bugs": {
25 | "url": "https://github.com/oslabs-beta/Kubiquity/issues"
26 | },
27 | "homepage": "https://github.com/oslabs-beta/Kubiquity#readme",
28 | "dependencies": {
29 | "@kubernetes/client-node": "^0.15.0",
30 | "apexcharts": "^3.27.1",
31 | "child_process": "^1.0.2",
32 | "dotenv": "^10.0.0",
33 | "electron": "^13.1.4",
34 | "electron-json-storage": "^4.5.0",
35 | "electron-reload": "^1.5.0",
36 | "express": "^4.17.1",
37 | "mongoose": "^5.13.1",
38 | "node-cmd": "^4.0.0",
39 | "node-fetch": "^2.6.1",
40 | "path": "^0.12.7",
41 | "prop-types": "^15.7.2",
42 | "react": "^17.0.2",
43 | "react-apexcharts": "^1.3.9",
44 | "react-csv-downloader": "^2.7.0",
45 | "react-dom": "^17.0.2",
46 | "react-table": "^7.7.0"
47 | },
48 | "devDependencies": {
49 | "@babel/core": "^7.14.6",
50 | "@babel/plugin-transform-runtime": "^7.14.5",
51 | "@babel/preset-env": "^7.14.7",
52 | "@babel/preset-react": "^7.14.5",
53 | "babel-loader": "^8.2.2",
54 | "babel-polyfill": "^6.26.0",
55 | "concurrently": "^6.2.0",
56 | "css-loader": "^5.2.6",
57 | "electron-packager": "^15.3.0",
58 | "enzyme": "^3.11.0",
59 | "enzyme-adapter-react-16": "^1.15.6",
60 | "eslint": "^7.29.0",
61 | "eslint-config-airbnb": "^18.2.1",
62 | "eslint-plugin-import": "^2.23.4",
63 | "eslint-plugin-jsx-a11y": "^6.4.1",
64 | "eslint-plugin-react": "^7.24.0",
65 | "file-loader": "^6.2.0",
66 | "html-webpack-plugin": "^5.3.2",
67 | "jest": "^27.0.6",
68 | "nodemon": "^2.0.9",
69 | "sass": "^1.35.1",
70 | "sass-loader": "^12.1.0",
71 | "style-loader": "^3.0.0",
72 | "webpack": "^5.41.1",
73 | "webpack-cli": "^4.7.2",
74 | "webpack-dev-server": "^3.11.2"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { ipcRenderer } from 'electron';
3 |
4 | import { Log, Metrics, Splash, Navbar, About } from './components';
5 |
6 | import {
7 | GET_LOG,
8 | GET_MEMORY,
9 | GET_CPU_USE,
10 | GOT_LOG,
11 | GOT_MEMORY,
12 | GOT_CPU_USE,
13 | } from '../utils';
14 |
15 | import logo from './assets/images/logo.png';
16 | import './assets/stylesheets/app.scss';
17 |
18 | const App = () => {
19 | const [isSplashShowing, setIsSplashShowing] = useState(true);
20 | const [memory, setMemory] = useState([]);
21 | const [cpuUse, setCpuUse] = useState([]);
22 | const [log, setLog] = useState([]);
23 | const [isLogShowing, setIsLogShowing] = useState(true);
24 | const [areMetricsShowing, setAreMetricsShowing] = useState(true);
25 | const [isAboutShowing, setIsAboutShowing] = useState(true);
26 |
27 | const getAppData = () => {
28 | ipcRenderer.send(GET_LOG);
29 | ipcRenderer.send(GET_MEMORY);
30 | ipcRenderer.send(GET_CPU_USE);
31 |
32 | ipcRenderer.once(GOT_LOG, (_, data) => {
33 | const newLog = JSON.parse(data);
34 | setLog(newLog);
35 | });
36 |
37 | ipcRenderer.once(GOT_MEMORY, (_, data) => {
38 | const newMemory = JSON.parse(data);
39 | setMemory(newMemory);
40 | });
41 |
42 | ipcRenderer.once(GOT_CPU_USE, (_, data) => {
43 | const newCpuUse = JSON.parse(data);
44 | setCpuUse(newCpuUse);
45 | });
46 | };
47 |
48 | useEffect(() => {
49 | setTimeout(() => {
50 | setIsSplashShowing(false);
51 | }, 4850);
52 |
53 | getAppData();
54 |
55 | return () => ipcRenderer.off();
56 | }, []);
57 |
58 | useEffect(() => {
59 | setTimeout(getAppData, 10000);
60 | }, [log]);
61 |
62 | if (isSplashShowing) return ;
63 |
64 | return (
65 |
66 |
70 |
71 |
76 |
77 | {isLogShowing &&
}
78 | {areMetricsShowing &&
}
79 | {isAboutShowing &&
}
80 |
81 |
82 |
83 | );
84 | };
85 |
86 | export default App;
87 |
--------------------------------------------------------------------------------
/src/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Kubiquity/38ff499badf93889797ef5012f2cfc0a82a0e01e/src/assets/images/logo.png
--------------------------------------------------------------------------------
/src/assets/stylesheets/_about.scss:
--------------------------------------------------------------------------------
1 | .about-text,
2 | #github-links {
3 | font-size: 12px;
4 | }
5 |
6 | .about-subheader {
7 | font-size: 15px;
8 | }
9 |
10 | #all-about-sections {
11 | display: flex;
12 |
13 | > * {
14 | width: 32%;
15 | margin-right: 18px;
16 | }
17 |
18 | :last-child {
19 | margin-right: 0;
20 | }
21 | }
22 |
23 | @media only screen and (max-width: 860px) {
24 | #all-about-sections {
25 | display: block;
26 |
27 | ul {
28 | justify-content: flex-start;
29 | }
30 |
31 | > * {
32 | width: 100%;
33 | margin-right: 0;
34 | }
35 | }
36 | }
37 |
38 | #github-links {
39 | padding: 0;
40 |
41 | > li > a {
42 | display: block;
43 | margin-bottom: 0;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/assets/stylesheets/_cssReset.scss:
--------------------------------------------------------------------------------
1 | /* Electron CSS reset */
2 |
3 | html {
4 | -webkit-user-select: none;
5 | -webkit-user-drag: none;
6 | cursor: default;
7 | }
8 |
9 | input,
10 | textarea,
11 | select,
12 | button,
13 | img {
14 | -webkit-user-drag: none;
15 | }
16 |
17 | .draggable {
18 | -webkit-app-region: drag;
19 | }
20 |
21 | .selectable {
22 | -webkit-user-select: auto;
23 | cursor: auto;
24 | }
25 |
26 | /* End of Electron CSS reset */
27 |
28 | /* General CSS reset */
29 |
30 | *,
31 | *::before,
32 | *::after {
33 | box-sizing: border-box;
34 | }
35 |
36 | body,
37 | h1,
38 | h2,
39 | h3,
40 | h4,
41 | p,
42 | figure,
43 | blockquote,
44 | dl,
45 | dd {
46 | margin: 0;
47 | }
48 |
49 | ul[role='list'],
50 | ol[role='list'] {
51 | list-style: none;
52 | }
53 |
54 | html:focus-within {
55 | scroll-behavior: smooth;
56 | }
57 |
58 | body {
59 | min-height: 100vh;
60 | text-rendering: optimizeSpeed;
61 | line-height: 1.5;
62 | background: $white;
63 | }
64 |
65 | a:not([class]) {
66 | text-decoration-skip-ink: auto;
67 | }
68 |
69 | img,
70 | picture {
71 | max-width: 100%;
72 | display: block;
73 | }
74 |
75 | input,
76 | button,
77 | textarea,
78 | select {
79 | font: inherit;
80 | }
81 |
82 | @media (prefers-reduced-motion: reduce) {
83 | html:focus-within {
84 | scroll-behavior: auto;
85 | }
86 |
87 | *,
88 | *::before,
89 | *::after {
90 | animation-duration: 0.01ms !important;
91 | animation-iteration-count: 1 !important;
92 | transition-duration: 0.01ms !important;
93 | scroll-behavior: auto !important;
94 | }
95 | }
96 |
97 | /* End general CSS reset */
98 |
--------------------------------------------------------------------------------
/src/assets/stylesheets/_loading.scss:
--------------------------------------------------------------------------------
1 | $len: 300px;
2 | $time: 3000ms;
3 |
4 | #infinity-outline {
5 | fill: transparent;
6 | stroke-width: 4;
7 | stroke: $darkBlue;
8 | stroke-dasharray: $len * 0.01, $len;
9 | stroke-dashoffset: 0;
10 | animation: anim $time linear infinite;
11 | }
12 |
13 | #infinity-bg {
14 | fill: transparent;
15 | stroke-width: 4;
16 | stroke: $grey;
17 | opacity: 0.2;
18 | }
19 |
20 | @keyframes anim {
21 | 12.5% {
22 | stroke-dasharray: $len * 0.14, $len;
23 | stroke-dashoffset: -$len * 0.11;
24 | }
25 | 43.75% {
26 | stroke-dasharray: $len * 0.35, $len;
27 | stroke-dashoffset: -$len * 0.35;
28 | }
29 | 100% {
30 | stroke-dasharray: $len * 0.01, $len;
31 | stroke-dashoffset: -$len * 0.99;
32 | }
33 | }
34 |
35 | #loading-container {
36 | display: flex;
37 | flex-direction: column;
38 | align-items: center;
39 | }
40 |
41 | #loading {
42 | overflow: hidden;
43 | height: 49px;
44 | width: 95px;
45 | margin-top: 6px;
46 | }
47 |
48 | svg {
49 | position: relative;
50 | top: -23px;
51 | left: -47px;
52 | }
53 |
--------------------------------------------------------------------------------
/src/assets/stylesheets/_noSearchResults.scss:
--------------------------------------------------------------------------------
1 | #no-search-results {
2 | background: #ffffff;
3 | display: flex;
4 | align-items: center;
5 | justify-content: center;
6 | padding: 24px;
7 | color: $darkBlue;
8 | }
9 |
--------------------------------------------------------------------------------
/src/assets/stylesheets/_splash.scss:
--------------------------------------------------------------------------------
1 | #splash-container {
2 | height: 100vh;
3 | width: 100%;
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | flex-direction: column;
8 | background-image: linear-gradient(to bottom right, $darkBlue, $lightBlue);
9 | animation: fadeIn 3s, fadeOut 1.5s 3.5s;
10 |
11 | * {
12 | color: $white;
13 | }
14 | }
15 |
16 | #credits {
17 | font-size: 18px;
18 | margin-bottom: 6px;
19 | }
20 |
21 | #loading-splash {
22 | font-size: 15px;
23 | }
24 |
25 | @keyframes fadeIn {
26 | 0% {
27 | opacity: 0;
28 | }
29 | 100% {
30 | opacity: 1;
31 | }
32 | }
33 |
34 | @keyframes fadeOut {
35 | 0% {
36 | opacity: 1;
37 | }
38 | 100% {
39 | opacity: 0;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/assets/stylesheets/_table.scss:
--------------------------------------------------------------------------------
1 | #table-container {
2 | max-height: 95vh;
3 | overflow: scroll;
4 | }
5 |
6 | tbody > * {
7 | font-size: 12px;
8 | font-family: 'Courier New', Courier, monospace;
9 | color: #222222;
10 | }
11 |
12 | td {
13 | border: solid 1px $grey;
14 | }
15 |
16 | th {
17 | border-bottom: solid 3px $black;
18 | background: $darkBlue;
19 | color: $white;
20 | font-weight: bold;
21 | padding: 6px;
22 | position: sticky;
23 | top: 0;
24 | }
25 |
26 | #download-button {
27 | transition: 300ms all;
28 | margin-right: 24px;
29 | }
30 |
31 | #input-buttons-container {
32 | display: flex;
33 | justify-content: space-between;
34 | align-items: center;
35 | }
36 |
--------------------------------------------------------------------------------
/src/assets/stylesheets/_variables.scss:
--------------------------------------------------------------------------------
1 | $darkBlue: #3b4f79;
2 | $lightBlue: #bac1cd;
3 | $white: #f8f8f8;
4 | $grey: #767676;
5 | $black: #222222;
6 |
--------------------------------------------------------------------------------
/src/assets/stylesheets/app.scss:
--------------------------------------------------------------------------------
1 | @import '_variables';
2 | @import '_cssReset';
3 | @import '_table';
4 | @import '_splash';
5 | @import '_loading';
6 | @import '_about';
7 | @import '_noSearchResults';
8 |
9 | body {
10 | min-width: 600px;
11 | }
12 |
13 | p,
14 | div,
15 | h1 {
16 | font-family: Arial, Helvetica, sans-serif;
17 | color: $grey;
18 | }
19 |
20 | #app-container {
21 | padding: 30px 0;
22 | background: $white;
23 | margin-right: 30px;
24 | width: 82vw;
25 |
26 | * {
27 | margin-bottom: 24px;
28 | }
29 | }
30 |
31 | #app-header {
32 | padding: 0 42px;
33 | display: flex;
34 | align-items: center;
35 | background: $darkBlue;
36 |
37 | p {
38 | padding-top: 18px;
39 | font-size: 18px;
40 | color: $white;
41 | }
42 |
43 | img {
44 | max-height: 240px;
45 | }
46 | }
47 |
48 | .section-headers {
49 | color: $grey;
50 | font-size: 18px;
51 | font-weight: 400;
52 | }
53 |
54 | #app {
55 | animation: fadeIn 1s;
56 | }
57 |
58 | @keyframes fadeIn {
59 | 0% {
60 | opacity: 0;
61 | }
62 | 100% {
63 | opacity: 1;
64 | }
65 | }
66 |
67 | .sub-header {
68 | font-size: 14px;
69 | margin-bottom: 24px;
70 | }
71 |
72 | .chart-container {
73 | width: 95%;
74 | margin-left: 42px;
75 | }
76 |
77 | #metrics-loading {
78 | margin-top: 24px;
79 | }
80 |
81 | #navbar-and-app-container {
82 | display: flex;
83 | }
84 |
85 | ul {
86 | list-style-type: none;
87 | margin: 0;
88 | padding: 30px 60px;
89 | }
90 |
91 | button {
92 | background: none;
93 | border: none;
94 | margin: 6px;
95 | color: $darkBlue;
96 | transition: color 100ms linear;
97 | font-weight: 600;
98 | }
99 |
100 | button:hover {
101 | color: $lightBlue;
102 | cursor: pointer;
103 | }
104 |
105 | @media only screen and (max-width: 860px) {
106 | #app-header {
107 | flex-direction: column;
108 | align-items: unset;
109 | justify-content: flex-start;
110 |
111 | h1 {
112 | margin: 0;
113 | }
114 |
115 | p {
116 | padding-top: 0px;
117 | margin: 0 0 12px 24px;
118 | }
119 |
120 | body {
121 | overflow-x: scroll;
122 | }
123 |
124 | #header-logo {
125 | width: 355px;
126 | }
127 | }
128 |
129 | #navbar-and-app-container {
130 | display: block;
131 | }
132 |
133 | ul {
134 | display: flex;
135 | justify-content: space-evenly;
136 | padding: 0;
137 | margin-top: 12px;
138 | }
139 |
140 | #app-container {
141 | margin: 24px 60px 0;
142 | }
143 | }
144 |
145 | input {
146 | width: 40%;
147 | min-width: 300px;
148 | }
149 |
150 | #header-logo {
151 | height: 160px;
152 | }
153 |
154 | a {
155 | color: $darkBlue;
156 | }
157 |
158 | .chart-title {
159 | display: flex;
160 | justify-content: center;
161 | color: $darkBlue;
162 | font-weight: 600;
163 | }
164 |
--------------------------------------------------------------------------------
/src/components/about/About.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const ABOUT_SECTION_STYLE = {
4 | marginBottom: '8px',
5 | };
6 |
7 | const KUBIQUITY_ABOUT_TEXT =
8 | 'Kubiquity is a Kubernetes error logging and tracking tool developed and launched in 2021. Designed for production-level applications using Kubernetes to orchestrate containers, Kubiquity provides metrics on all pods memory use to help developers prevent OOM kill errors additionally, Kubiquity logs Kubernetes events to highlight any errors and warnings that originate within monitored clusters.';
9 |
10 | const K8SM8S_ABOUT_TEXT =
11 | 'K8sM8s is a developer group based in New York City focused on open source application development. Specializing in products that aid container orchestration development, our most recent product is Kubiquity.';
12 |
13 | const K8SM8S_TEAM_TEXT =
14 | 'K8sM8s is composed of the following engineers: David Anderson; Robert Hernandez; David Zhang; and Jeffrey Zheng.';
15 |
16 | const About = () => (
17 |
18 |
ABOUT
19 |
20 |
21 |
22 | About Kubiquity
23 |
24 |
{KUBIQUITY_ABOUT_TEXT}
25 |
26 |
27 |
28 | About K8sM8s
29 |
30 |
{K8SM8S_ABOUT_TEXT}
31 |
{K8SM8S_TEAM_TEXT}
32 |
33 |
59 |
60 |
61 | );
62 |
63 | export default About;
64 |
--------------------------------------------------------------------------------
/src/components/about/index.js:
--------------------------------------------------------------------------------
1 | import About from './About';
2 |
3 | export default About;
4 |
--------------------------------------------------------------------------------
/src/components/barChart/BarChart.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import ReactApexCharts from 'react-apexcharts';
4 |
5 | import {
6 | BAR,
7 | CHART_CONTAINER_CLASS,
8 | ENABLED_FALSE,
9 | FILL,
10 | CHART,
11 | PLOT_OPTIONS,
12 | } from './barChartConstants';
13 |
14 | const BarChart = ({ data, categories, xAxisFormatter }) => {
15 | const height = data.length > 30 ? 1500 : data.length * 60;
16 | const series = [{ data }];
17 |
18 | const options = {
19 | fill: FILL,
20 | chart: CHART,
21 | plotOptions: PLOT_OPTIONS,
22 | dataLabels: ENABLED_FALSE,
23 | xaxis: {
24 | categories,
25 | labels: {
26 | formatter: xAxisFormatter,
27 | },
28 | },
29 | };
30 |
31 | return (
32 |
33 |
39 |
40 | );
41 | };
42 |
43 | BarChart.propTypes = {
44 | data: PropTypes.arrayOf(PropTypes.number).isRequired,
45 | categories: PropTypes.arrayOf(PropTypes.string).isRequired,
46 | xAxisFormatter: PropTypes.func.isRequired,
47 | };
48 |
49 | export default BarChart;
50 |
--------------------------------------------------------------------------------
/src/components/barChart/barChartConstants.js:
--------------------------------------------------------------------------------
1 | export const BAR = 'bar';
2 | export const CHART_CONTAINER_CLASS = 'chart-container';
3 |
4 | export const ENABLED_FALSE = { enabled: false };
5 | export const FILL = { colors: ['#0e2b5f'] };
6 |
7 | export const CHART = {
8 | height: 350,
9 | type: BAR,
10 | toolbar: { show: false },
11 | animations: {
12 | enabled: true,
13 | easing: 'easeinout',
14 | speed: 800,
15 | animateGradually: ENABLED_FALSE,
16 | dynamicAnimation: {
17 | enabled: true,
18 | speed: 350,
19 | },
20 | },
21 | };
22 |
23 | export const PLOT_OPTIONS = {
24 | bar: {
25 | borderRadius: 4,
26 | horizontal: true,
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/src/components/barChart/index.js:
--------------------------------------------------------------------------------
1 | import BarChart from './BarChart';
2 |
3 | export default BarChart;
4 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | import Loading from './loading';
2 | import Log from './log';
3 | import Metrics from './metrics';
4 | import Navbar from './navbar';
5 | import Splash from './splash';
6 | import About from './about';
7 | import BarChart from './barChart';
8 |
9 | export { Loading, Log, Metrics, Navbar, Splash, About, BarChart };
10 |
--------------------------------------------------------------------------------
/src/components/loading/Loading.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { LOG, MEMORY, CPU_USE } from '../utils';
5 |
6 | const ROUND = 'round';
7 |
8 | const SVG_WIDTH_HEIGHT = 200;
9 | const PATH_PROPS = {
10 | strokeLinecap: ROUND,
11 | strokeLinejoin: ROUND,
12 | strokeMiterlimit: 10,
13 | };
14 |
15 | const Loading = ({ resource }) => (
16 |
17 | Loading {resource}, please wait . . .
18 |
32 |
33 | );
34 |
35 | Loading.propTypes = {
36 | resource: PropTypes.oneOf([LOG, MEMORY, CPU_USE]).isRequired,
37 | };
38 |
39 | export default Loading;
40 |
--------------------------------------------------------------------------------
/src/components/loading/index.js:
--------------------------------------------------------------------------------
1 | import Loading from './Loading';
2 |
3 | export default Loading;
4 |
--------------------------------------------------------------------------------
/src/components/log/Download.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import PropTypes from 'prop-types';
3 | import CsvDownloader from 'react-csv-downloader';
4 |
5 | import { CSV_COLUMNS, DOWNLOADED_STYLE } from './logConstants';
6 |
7 | const Download = ({ data }) => {
8 | const [hasDownloaded, setHasDownloaded] = useState(false);
9 |
10 | const handleClick = () => {
11 | setHasDownloaded(true);
12 | setTimeout(() => setHasDownloaded(false), 3000);
13 | };
14 |
15 | return (
16 |
21 |
28 |
29 | );
30 | };
31 |
32 | Download.propTypes = {
33 | data: PropTypes.arrayOf(
34 | PropTypes.shape({
35 | createdAt: PropTypes.string.isRequired,
36 | namespace: PropTypes.string.isRequired,
37 | type: PropTypes.string.isRequired,
38 | reason: PropTypes.string.isRequired,
39 | object: PropTypes.string.isRequired,
40 | message: PropTypes.string.isRequired,
41 | lastSeen: PropTypes.string.isRequired,
42 | }),
43 | ).isRequired,
44 | };
45 |
46 | export default Download;
47 |
--------------------------------------------------------------------------------
/src/components/log/Log.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { Loading } from '../';
5 | import Table from './Table';
6 | import NoSearchResults from './NoSearchResults';
7 | import Download from './Download';
8 |
9 | import { LOG_HEADERS } from './logConstants';
10 | import { LOG } from '../utils';
11 |
12 | const Log = ({ log }) => {
13 | const [searchTerm, setSearchTerm] = useState('');
14 | const [filteredLog, setFilteredLog] = useState(log);
15 |
16 | useEffect(() => {
17 | if (!searchTerm) {
18 | setFilteredLog(log);
19 | } else {
20 | const lowerCaseSearchTerm = searchTerm.toLowerCase();
21 |
22 | const newFilteredLog = log.filter((entry) => {
23 | const values = Object.values(entry);
24 |
25 | for (const value of values) {
26 | if (
27 | typeof value === 'string' &&
28 | value.toLowerCase().includes(lowerCaseSearchTerm)
29 | ) {
30 | return true;
31 | }
32 | }
33 |
34 | return false;
35 | });
36 |
37 | setFilteredLog(newFilteredLog);
38 | }
39 |
40 | return () => setFilteredLog(log);
41 | }, [searchTerm, log]);
42 |
43 | const handleInput = (e) => {
44 | const { value } = e.target;
45 | setSearchTerm(value);
46 | };
47 |
48 | const resetSearch = () => setSearchTerm('');
49 |
50 | let displayComponent;
51 |
52 | if (!log.length) {
53 | displayComponent = ;
54 | } else if (!filteredLog.length) {
55 | displayComponent = ;
56 | } else {
57 | displayComponent = ;
58 | }
59 |
60 | return (
61 |
62 |
EVENT LOG
63 |
66 |
77 | {displayComponent}
78 |
79 | );
80 | };
81 |
82 | Log.propTypes = {
83 | log: PropTypes.arrayOf(
84 | PropTypes.shape({
85 | createdAt: PropTypes.string.isRequired,
86 | namespace: PropTypes.string.isRequired,
87 | type: PropTypes.string.isRequired,
88 | reason: PropTypes.string.isRequired,
89 | object: PropTypes.string.isRequired,
90 | message: PropTypes.string.isRequired,
91 | lastSeen: PropTypes.string.isRequired,
92 | }),
93 | ).isRequired,
94 | };
95 |
96 | export default Log;
97 |
--------------------------------------------------------------------------------
/src/components/log/NoSearchResults.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const NoSearchResults = ({ searchTerm }) => (
5 |
6 | No results found matching "{searchTerm}"—please refine your search and try
7 | again.
8 |
9 | );
10 |
11 | NoSearchResults.propTypes = {
12 | searchTerm: PropTypes.string.isRequired,
13 | };
14 |
15 | export default NoSearchResults;
16 |
--------------------------------------------------------------------------------
/src/components/log/Table.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { useTable, useSortBy } from 'react-table';
4 |
5 | import {
6 | HEADER,
7 | CELL,
8 | NORMAL,
9 | ROW_RED_BACKGROUND_STYLE,
10 | DOWN_ARROW,
11 | UP_ARROW,
12 | } from './logConstants';
13 |
14 | const Table = ({ data, headers }) => {
15 | const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } =
16 | useTable(
17 | {
18 | columns: headers,
19 | data,
20 | },
21 | useSortBy,
22 | );
23 |
24 | const tableProps = getTableProps();
25 | const tableBodyProps = getTableBodyProps();
26 |
27 | const headerComponents = headerGroups.map((headerGroup) => {
28 | const headerProps = headerGroup.getHeaderGroupProps();
29 |
30 | return (
31 |
32 | {headerGroup.headers.map((column) => (
33 |
34 | {column.render(HEADER)}
35 |
36 | {column.isSorted
37 | ? column.isSortedDesc
38 | ? UP_ARROW
39 | : DOWN_ARROW
40 | : ''}
41 |
42 | |
43 | ))}
44 |
45 | );
46 | });
47 |
48 | const rowsComponents = rows.map((row) => {
49 | prepareRow(row);
50 | const { type } = row.original;
51 | const rowStyles = type === NORMAL ? null : ROW_RED_BACKGROUND_STYLE;
52 |
53 | return (
54 |
55 | {row.cells.map((cell) => {
56 | const cellProps = cell.getCellProps();
57 |
58 | return {cell.render(CELL)} | ;
59 | })}
60 |
61 | );
62 | });
63 |
64 | return (
65 |
66 |
67 | {headerComponents}
68 | {rowsComponents}
69 |
70 |
71 | );
72 | };
73 |
74 | Table.propTypes = {
75 | data: PropTypes.arrayOf(
76 | PropTypes.shape({
77 | createdAt: PropTypes.string.isRequired,
78 | namespace: PropTypes.string.isRequired,
79 | type: PropTypes.string.isRequired,
80 | reason: PropTypes.string.isRequired,
81 | object: PropTypes.string.isRequired,
82 | message: PropTypes.string.isRequired,
83 | lastSeen: PropTypes.string.isRequired,
84 | }),
85 | ).isRequired,
86 | headers: PropTypes.arrayOf(
87 | PropTypes.shape({
88 | Header: PropTypes.string.isRequired,
89 | accessor: PropTypes.string.isRequired,
90 | sortType: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
91 | disableSortBy: PropTypes.bool,
92 | }),
93 | ).isRequired,
94 | };
95 |
96 | export default Table;
97 |
--------------------------------------------------------------------------------
/src/components/log/index.js:
--------------------------------------------------------------------------------
1 | import Log from './Log';
2 |
3 | export default Log;
4 |
--------------------------------------------------------------------------------
/src/components/log/logConstants.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const TIMESTAMP = 'Timestamp';
4 | const NAMESPACE = 'Namespace';
5 | const TYPE = 'Type';
6 | const OBJECT = 'Object';
7 | const MESSAGE = 'Message';
8 | const LAST_SEEN = 'Last seen';
9 | const REASON = 'Reason';
10 |
11 | const CREATED_AT_CAMEL_CASE = 'createdAt';
12 | const NAMESPACE_CAMEL_CASE = 'namespace';
13 | const TYPE_CAMEL_CASE = 'type';
14 | const OBJECT_CAMEL_CASE = 'object';
15 | const MESSAGE_CAMEL_CASE = 'message';
16 | const LAST_SEEN_CAMEL_CASE = 'lastSeen';
17 | const REASON_CAMEL_CASE = 'reason';
18 |
19 | const BASIC_SORT = 'basic';
20 |
21 | const sortByTimestamp = (a, b) =>
22 | new Date(b.original.createdAt) - new Date(a.original.createdAt);
23 |
24 | export const LOG_HEADERS = [
25 | {
26 | Header: TIMESTAMP,
27 | accessor: CREATED_AT_CAMEL_CASE,
28 | sortType: sortByTimestamp,
29 | },
30 | {
31 | Header: NAMESPACE,
32 | accessor: NAMESPACE_CAMEL_CASE,
33 | sortType: BASIC_SORT,
34 | },
35 | {
36 | Header: TYPE,
37 | accessor: TYPE_CAMEL_CASE,
38 | sortType: BASIC_SORT,
39 | },
40 | {
41 | Header: REASON,
42 | accessor: REASON_CAMEL_CASE,
43 | sortType: BASIC_SORT,
44 | },
45 | {
46 | Header: OBJECT,
47 | accessor: OBJECT_CAMEL_CASE,
48 | sortType: BASIC_SORT,
49 | },
50 | {
51 | Header: MESSAGE,
52 | accessor: MESSAGE_CAMEL_CASE,
53 | sortType: BASIC_SORT,
54 | },
55 | {
56 | Header: LAST_SEEN,
57 | accessor: LAST_SEEN_CAMEL_CASE,
58 | disableSortBy: true,
59 | },
60 | ];
61 |
62 | export const CSV_COLUMNS = [
63 | {
64 | displayName: TIMESTAMP,
65 | id: CREATED_AT_CAMEL_CASE,
66 | },
67 | {
68 | displayName: NAMESPACE,
69 | id: NAMESPACE_CAMEL_CASE,
70 | },
71 | {
72 | displayName: TYPE,
73 | id: TYPE_CAMEL_CASE,
74 | },
75 | {
76 | displayName: REASON,
77 | id: REASON_CAMEL_CASE,
78 | },
79 | {
80 | displayName: OBJECT,
81 | id: OBJECT_CAMEL_CASE,
82 | },
83 | {
84 | displayName: MESSAGE,
85 | id: MESSAGE_CAMEL_CASE,
86 | },
87 | {
88 | displayName: LAST_SEEN,
89 | id: LAST_SEEN_CAMEL_CASE,
90 | },
91 | ];
92 |
93 | const FIVE_PX = '5px';
94 |
95 | export const DOWNLOADED_STYLE = {
96 | background: '#5aa25a',
97 | color: 'white',
98 | transition: '300ms all',
99 | borderRadius: FIVE_PX,
100 | padding: FIVE_PX,
101 | };
102 |
103 | export const HEADER = 'Header';
104 | export const NORMAL = 'Normal';
105 | export const CELL = 'Cell';
106 |
107 | export const ROW_RED_BACKGROUND_STYLE = {
108 | backgroundColor: '#de8989',
109 | fontWeight: 'bold',
110 | color: '#f8f8f8',
111 | };
112 |
113 | export const DOWN_ARROW = 🔽
;
114 | export const UP_ARROW = 🔼
;
115 |
--------------------------------------------------------------------------------
/src/components/metrics/BarChartContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { Loading, BarChart } from '../';
5 |
6 | import { MEMORY, CPU_USE, CPU_USAGE } from '../utils';
7 |
8 | const BarChartContainer = ({
9 | data,
10 | resource,
11 | xAxisFormatter,
12 | resourceKey,
13 | title,
14 | }) => {
15 | const values = new Array(data.length);
16 | const labels = new Array(data.length);
17 |
18 | data.forEach((datum, i) => {
19 | labels[i] = datum.podId;
20 | values[i] = datum[resourceKey];
21 | });
22 |
23 | return (
24 | <>
25 | {title}
26 | {data.length ? (
27 |
32 | ) : (
33 |
34 |
35 |
36 | )}
37 | >
38 | );
39 | };
40 |
41 | BarChartContainer.propTypes = {
42 | data: PropTypes.arrayOf(
43 | PropTypes.shape({
44 | podId: PropTypes.string.isRequired,
45 | memory: PropTypes.number,
46 | cpuUsage: PropTypes.number,
47 | }),
48 | ).isRequired,
49 | resource: PropTypes.oneOf([MEMORY, CPU_USE]).isRequired,
50 | xAxisFormatter: PropTypes.func.isRequired,
51 | resourceKey: PropTypes.oneOf([MEMORY, CPU_USAGE]).isRequired,
52 | title: PropTypes.string.isRequired,
53 | };
54 |
55 | export default BarChartContainer;
56 |
--------------------------------------------------------------------------------
/src/components/metrics/Metrics.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import BarChartContainer from './BarChartContainer';
5 |
6 | import { MEMORY, CPU_USE, CPU_USAGE } from '../utils';
7 |
8 | const METRICS_TEXT =
9 | 'Prevent OOM (out of memory) kill errors by monitoring the memory and CPU usage of each node in your cluster.';
10 |
11 | const roundNumToOneDecimal = (num) => Math.round(num * 10) / 10;
12 | const formatXAxisToPercent = (val) => `${roundNumToOneDecimal(val)}%`;
13 | const formatXAxisToBytes = (val) =>
14 | `${roundNumToOneDecimal(val).toLocaleString()} B`;
15 |
16 | const Metrics = ({ memory, cpuUse }) => (
17 |
18 |
MEMORY METRICS FOR ACTIVE PODS
19 |
{METRICS_TEXT}
20 |
27 |
34 |
35 | );
36 |
37 | Metrics.propTypes = {
38 | memory: PropTypes.arrayOf(
39 | PropTypes.shape({
40 | podId: PropTypes.string.isRequired,
41 | memory: PropTypes.number.isRequired,
42 | }),
43 | ).isRequired,
44 | cpuUse: PropTypes.arrayOf(
45 | PropTypes.shape({
46 | podId: PropTypes.string.isRequired,
47 | cpuUsage: PropTypes.number.isRequired,
48 | }),
49 | ).isRequired,
50 | };
51 |
52 | export default Metrics;
53 |
--------------------------------------------------------------------------------
/src/components/metrics/index.js:
--------------------------------------------------------------------------------
1 | import Metrics from './Metrics';
2 |
3 | export default Metrics;
4 |
--------------------------------------------------------------------------------
/src/components/navbar/NavButton.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { NAV_OPTIONS } from './navbarConstants';
5 |
6 | const NavButton = ({ navOption, handleClick }) => (
7 |
8 |
9 |
10 | );
11 |
12 | NavButton.propTypes = {
13 | navOption: PropTypes.oneOf(NAV_OPTIONS).isRequired,
14 | handleClick: PropTypes.func.isRequired,
15 | };
16 |
17 | export default NavButton;
18 |
--------------------------------------------------------------------------------
/src/components/navbar/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import NavButton from './NavButton';
5 |
6 | import { HOME, LOG, METRICS, ABOUT, NAV_OPTIONS } from './navbarConstants';
7 |
8 | const Navbar = ({
9 | setIsLogShowing,
10 | setAreMetricsShowing,
11 | setIsAboutShowing,
12 | }) => {
13 | const navOptionToStateUpdater = {
14 | [LOG]: setIsLogShowing,
15 | [METRICS]: setAreMetricsShowing,
16 | [ABOUT]: setIsAboutShowing,
17 | };
18 |
19 | const handleClick = (e) => {
20 | const { textContent } = e.target;
21 | const areAllShowing = textContent === HOME;
22 |
23 | for (let i = 1; i < NAV_OPTIONS.length; i++) {
24 | const navOption = NAV_OPTIONS[i];
25 | const stateUpdater = navOptionToStateUpdater[navOption];
26 | const shouldDisplay = areAllShowing || textContent === navOption;
27 |
28 | stateUpdater(shouldDisplay);
29 | }
30 | };
31 |
32 | const buttons = NAV_OPTIONS.map((navOption, i) => (
33 |
39 | ));
40 |
41 | return ;
42 | };
43 |
44 | Navbar.propTypes = {
45 | setIsLogShowing: PropTypes.func.isRequired,
46 | setAreMetricsShowing: PropTypes.func.isRequired,
47 | setIsAboutShowing: PropTypes.func.isRequired,
48 | };
49 |
50 | export default Navbar;
51 |
--------------------------------------------------------------------------------
/src/components/navbar/index.js:
--------------------------------------------------------------------------------
1 | import Navbar from './Navbar';
2 |
3 | export default Navbar;
4 |
--------------------------------------------------------------------------------
/src/components/navbar/navbarConstants.js:
--------------------------------------------------------------------------------
1 | export const HOME = 'Home';
2 | export const METRICS = 'Metrics';
3 | export const LOG = 'Log';
4 | export const ABOUT = 'About';
5 |
6 | export const NAV_OPTIONS = [HOME, LOG, METRICS, ABOUT];
7 |
--------------------------------------------------------------------------------
/src/components/splash/Splash.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import logo from '../../assets/images/logo.png';
4 |
5 | const Splash = () => (
6 |
7 |

8 |
Developed by k8sm8s, 2021
9 |
App is loading; please wait . . .
10 |
11 | );
12 |
13 | export default Splash;
14 |
--------------------------------------------------------------------------------
/src/components/splash/index.js:
--------------------------------------------------------------------------------
1 | import Splash from './Splash';
2 |
3 | export default Splash;
4 |
--------------------------------------------------------------------------------
/src/components/utils/index.js:
--------------------------------------------------------------------------------
1 | export const MEMORY = 'memory';
2 | export const CPU_USE = 'CPU use';
3 | export const CPU_USAGE = 'cpuUsage';
4 | export const LOG = 'log';
5 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 |
4 | import App from './App.jsx';
5 |
6 | const root = document.getElementById('root');
7 |
8 | render(, root);
9 |
--------------------------------------------------------------------------------
/utils/index.js:
--------------------------------------------------------------------------------
1 | const GET_LOG = 'getLog';
2 | const GET_MEMORY = 'getMemory';
3 | const GET_CPU_USE = 'getCpuUse';
4 | const GOT_LOG = 'gotLog';
5 | const GOT_MEMORY = 'gotMemory';
6 | const GOT_CPU_USE = 'gotCpuUse';
7 |
8 | module.exports = {
9 | GET_LOG,
10 | GET_MEMORY,
11 | GET_CPU_USE,
12 | GOT_LOG,
13 | GOT_MEMORY,
14 | GOT_CPU_USE,
15 | };
16 |
--------------------------------------------------------------------------------
/webpack.common.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | mode: 'development',
6 | entry: './src/index.js',
7 | devtool: 'inline-source-map',
8 | target: 'electron-renderer',
9 | devServer: {
10 | publicPath: '/build',
11 | port: 8080,
12 | proxy: {
13 | '/**': 'http://localhost:3000',
14 | },
15 | hot: true,
16 | historyApiFallback: true,
17 | },
18 | plugins: [
19 | new HtmlWebpackPlugin({
20 | template: './index.html',
21 | }),
22 | ],
23 | module: {
24 | rules: [
25 | {
26 | test: [/\.js$/, /\.jsx$/],
27 | exclude: /node_modules/,
28 | use: {
29 | loader: 'babel-loader',
30 | options: {
31 | presets: [
32 | [
33 | '@babel/preset-env',
34 | {
35 | targets: {
36 | esmodules: true,
37 | },
38 | },
39 | ],
40 | '@babel/preset-react',
41 | ],
42 | },
43 | },
44 | },
45 | {
46 | test: [/\.s[ac]ss$/i, /\.css$/i],
47 | use: ['style-loader', 'css-loader', 'sass-loader'],
48 | },
49 | {
50 | test: /\.(png|jpe?g|gif)$/i,
51 | use: [
52 | {
53 | loader: 'file-loader',
54 | },
55 | ],
56 | },
57 | ],
58 | },
59 | resolve: {
60 | extensions: ['.js', '.jsx'],
61 | },
62 | output: {
63 | filename: 'app.js',
64 | path: path.resolve(__dirname, 'build'),
65 | },
66 | };
67 |
--------------------------------------------------------------------------------