├── .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 | Kubiquity banner 4 |

5 | 6 | # Kubiquity 7 | 8 | 9 | ![Version](https://img.shields.io/badge/version-1.0.0-blue.svg?cacheSeconds=2592000) 10 | ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/oslabs-beta/Kubiquity?include_prereleases) 11 | ![GitHub pull requests](https://img.shields.io/github/issues-pr/oslabs-beta/kubiquity) 12 | [![Documentation](https://img.shields.io/badge/documentation-yes-brightgreen.svg)](https://github.com/oslabs-beta/Kubiquity#readme) 13 | [![License: MIT](https://img.shields.io/github/license/oslabs-beta/Kubiquity)](https://github.com/oslabs-beta/Kubiquity/blob/master/LICENSE) 14 | [![Twitter: kubiquityapp](https://img.shields.io/twitter/follow/kubiquityapp.svg?style=social)](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 | ![Demo](https://imgur.com/BHQuJVW.gif) 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 | **[![David Anderson](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/dlande000) [![Linkedin](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/dlande000/)** 84 | 85 | 86 | 87 | 88 | 89 | 90 | **Robert Hernandez** 91 | 92 | **[![Robert Hernandez](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/Hydroelectric29) [![Linkedin](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/robert-hernandez-879108211/)** 93 | 94 | 95 | 96 | 97 | **David Zhang** 98 | 99 | **[![David Zhang](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/davidzhangnyc) [![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/davidnyc/)** 100 | 101 | 102 | 103 | 104 | **Jeffrey Zheng** 105 | 106 | **[![Jeffrey Zheng](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/JefZheng) [![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](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 |
67 | 68 |

An error logging and visualization tool for Kubernetes.

69 |
70 | 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 |
34 |
35 | Contact 36 |
37 |

38 | If you want to contact the K8sM8s team about Kubiquity, please contact 39 | us at{' '} 40 | kubiquityapp@gmail.com. 41 |

42 |

43 | The Kubiquity repo can be found at{' '} 44 | 45 | github.com/oslabs-beta/Kubiquity 46 | 47 | . 48 |

49 |

The K8sM8s team can be found on GitHub:

50 | 58 |
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 |
19 | 20 | 25 | 30 | 31 |
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 |
64 | Use the Kubiquity event log to find and resolve errors. 65 |
66 |
67 |
68 | 73 | 74 |
75 | 76 |
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 | 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 ; 59 | })} 60 | 61 | ); 62 | }); 63 | 64 | return ( 65 |
66 |
34 | {column.render(HEADER)} 35 | 36 | {column.isSorted 37 | ? column.isSortedDesc 38 | ? UP_ARROW 39 | : DOWN_ARROW 40 | : ''} 41 | 42 |
{cell.render(CELL)}
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
      {buttons}
    ; 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 | --------------------------------------------------------------------------------