├── .babelrc ├── client ├── assets │ ├── Favicon.png │ ├── first-m8-logo.png │ ├── first-m8-logo-only.png │ └── first-m8-name-only.png ├── components │ ├── dashboard │ │ ├── dashboardHistory.js │ │ ├── OperatorOrMetric.jsx │ │ ├── OptionsOrSelectedColumn.jsx │ │ ├── DataFilters.jsx │ │ ├── ChartsContainer.jsx │ │ ├── IndividualDropDown.jsx │ │ ├── SingleNumberDisplay.jsx │ │ ├── DonutChart.jsx │ │ ├── promQLQueryAlgorithms.js │ │ ├── TimeSeriesChart.jsx │ │ ├── IndividualChartContainer.jsx │ │ ├── DashboardContainer.jsx │ │ └── ChartSetup.jsx │ ├── history │ │ └── HistoryContainer.jsx │ ├── settings │ │ ├── settingsHelper.js │ │ ├── SettingsCard.jsx │ │ ├── SettingsContainer.jsx │ │ └── AddEditCard.jsx │ ├── MainRoutes.jsx │ └── App.jsx ├── index.js ├── styles │ ├── variables.scss │ ├── mainStyle.scss │ ├── settingStyle.scss │ └── dashboardStyle.scss └── index.html ├── main └── main.js ├── server ├── routes │ ├── webRouter.js │ └── dashboardRouter.js ├── server.js ├── controllers │ ├── webController.js │ └── dashboardController.js └── models │ └── webModel.js ├── .eslintrc.json ├── LICENSE ├── .gitignore ├── webpack.config.js ├── __tests__ ├── server │ └── settingsRoutes.js └── client │ ├── promQLQuery.js │ └── settings.js ├── package.json └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: ["@babel/preset-env", "@babel/preset-react"] 3 | } -------------------------------------------------------------------------------- /client/assets/Favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/First-M8/HEAD/client/assets/Favicon.png -------------------------------------------------------------------------------- /client/assets/first-m8-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/First-M8/HEAD/client/assets/first-m8-logo.png -------------------------------------------------------------------------------- /client/assets/first-m8-logo-only.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/First-M8/HEAD/client/assets/first-m8-logo-only.png -------------------------------------------------------------------------------- /client/assets/first-m8-name-only.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/First-M8/HEAD/client/assets/first-m8-name-only.png -------------------------------------------------------------------------------- /client/components/dashboard/dashboardHistory.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory as history } from 'history'; 2 | 3 | export default history(); 4 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | import App from './components/App'; 5 | import mainStyle from './styles/mainStyle.scss'; 6 | import settingStyle from './styles/settingStyle.scss'; 7 | import dashboardStyle from './styles/dashboardStyle.scss'; 8 | 9 | render(, document.getElementById('root')); 10 | -------------------------------------------------------------------------------- /client/components/history/HistoryContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const HistoryContainer = () => { 4 | return ( 5 |
6 |

History

7 |
Currently in development. Apologies for the inconvenience and thank you for your patience.
8 |
9 | ); 10 | }; 11 | 12 | export default HistoryContainer; 13 | -------------------------------------------------------------------------------- /client/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $mainBlack: rgb(34, 34, 34); 2 | $lighterBlack: lighten($mainBlack, 10%); 3 | $mainGray: rgb(215, 215, 215); 4 | $darkerGray: darken($mainGray, 25%); 5 | $darkestGray: darken($mainGray, 50%); 6 | $mainBlue: rgb(90, 154, 218); 7 | $darkerBlue: darken($mainBlue, 25%); 8 | $mainRed: rgb(209, 68, 68); 9 | $darkerRed: darken($mainRed, 25%); 10 | $mainGreen: rgb(76, 156, 76); 11 | $darkerGreen: darken($mainGreen, 25%); 12 | $chartBlue: rgb(0,90,170) -------------------------------------------------------------------------------- /client/components/settings/settingsHelper.js: -------------------------------------------------------------------------------- 1 | const ipRegex = require('ip-regex'); 2 | 3 | module.exports = { 4 | nameHelper: (name) => { 5 | if (name === 'new') return true; 6 | }, 7 | ipHelper: (ip) => { 8 | if (ipRegex({ exact: true }).test(ip)) return false; 9 | return false; 10 | }, 11 | portHelper: (port) => { 12 | const numPort = parseInt(port); 13 | if ((numPort < 0 && numPort >= 65353) || numPort % 1 !== 0) return true; 14 | return false; 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /client/components/dashboard/OperatorOrMetric.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Draggable } from 'react-beautiful-dnd'; 3 | 4 | const OperatorOrMetric = ({ text, index }) => ( 5 | 6 | {(provided) => ( 7 |
8 | {text} 9 |
10 | )} 11 |
12 | ); 13 | 14 | export default OperatorOrMetric; 15 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | First M8 8 | 9 | 10 |
11 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /client/components/settings/SettingsCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const SettingsCard = ({ setting }) => { 5 | return ( 6 |
7 | 8 |

Name: {setting.name}

9 |

IP Address: {setting.ipAddress}

10 |

Port: {setting.port}

11 | 12 |
13 | ); 14 | }; 15 | 16 | export default SettingsCard; 17 | -------------------------------------------------------------------------------- /main/main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow } = require('electron'); 2 | // eslint-disable-next-line no-unused-expressions 3 | require('../server/server'); 4 | 5 | function createWindow() { 6 | const win = new BrowserWindow({ 7 | width: 800, 8 | height: 600, 9 | title: 'First M8', 10 | }); 11 | 12 | win.loadURL('http://localhost:3001'); 13 | } 14 | 15 | app.whenReady().then(() => { 16 | createWindow(); 17 | 18 | app.on('window-all-closed', () => { 19 | if (process.platform !== 'darwin') app.quit(); 20 | }); 21 | 22 | app.on('activate', () => { 23 | if (BrowserWindow.getAllWindows().length === 0) createWindow(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /server/routes/webRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const webController = require('../controllers/webController'); 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/all', webController.getAll, (req, res) => { 7 | // insert query to database 8 | // all "cluster" information 9 | res.status(200).send({ settings: res.locals.settings }); 10 | }); 11 | 12 | router.post('/new', webController.createCluster, webController.getAll, (req, res) => { 13 | res.status(200).send({ settings: res.locals.settings }); 14 | }); 15 | 16 | router.delete('/:name/delete', webController.deleteCluster, webController.getAll, (req, res) => { 17 | res.status(200).send({ settings: res.locals.settings }); 18 | }); 19 | 20 | router.put('/:name', webController.updateCluster, webController.getAll, (req, res) => { 21 | res.status(200).send({ settings: res.locals.settings }); 22 | }); 23 | 24 | module.exports = router; 25 | -------------------------------------------------------------------------------- /client/components/dashboard/OptionsOrSelectedColumn.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Droppable } from 'react-beautiful-dnd'; 3 | import OperatorOrMetric from './OperatorOrMetric'; 4 | 5 | const OptionsOrSelectedColumn = ({ columnName, columnTitle, listOfOperatorsOrMetrics }) => { 6 | return ( 7 |
8 |
{columnTitle}
9 | 10 | {(provided) => ( 11 |
12 | {listOfOperatorsOrMetrics.map((operatorOrMetric, index) => ( 13 | 14 | ))} 15 | {provided.placeholder} 16 |
17 | )} 18 |
19 |
20 | 21 | ); 22 | }; 23 | 24 | export default OptionsOrSelectedColumn; 25 | -------------------------------------------------------------------------------- /client/components/dashboard/DataFilters.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import IndividualDropDown from './IndividualDropDown'; 3 | 4 | const DataFilters = ({ 5 | filters, 6 | setFilters, 7 | onChange, 8 | prometheusInstance, 9 | setPrometheusInstance, 10 | }) => { 11 | /* 12 | displays drop down for each Prometheus label on chart setup page 13 | */ 14 | const labels = []; 15 | Object.keys(filters).forEach((label) => { 16 | if (label !== '__name__') { 17 | labels.push( 18 | , 26 | ); 27 | } 28 | }); 29 | 30 | return ( 31 |
32 | {labels} 33 |
34 | ); 35 | }; 36 | 37 | export default DataFilters; 38 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["plugin:react/recommended", "airbnb"], 7 | "parserOptions": { 8 | "ecmaFeatures": { 9 | "jsx": true 10 | }, 11 | "ecmaVersion": 12, 12 | "sourceType": "module" 13 | }, 14 | "plugins": ["react"], 15 | "rules": { 16 | "radix": "off", 17 | "react/jsx-filename-extension": "off", 18 | "react/prop-types": "off", 19 | "react/jsx-wrap-multilines": "off", 20 | "no-return-assign": "off", 21 | "react/no-array-index-key": "off", 22 | "react/jsx-one-expression-per-line": "off", 23 | "jsx-a11y/label-has-associated-control": "off", 24 | "import/no-extraneous-dependencies": "off", 25 | "consistent-return": "off", 26 | "arrow-body-style": "off", 27 | "react/jsx-indent": "off", 28 | "no-console": "off", 29 | "jsx-a11y/control-has-associated-label": "off", 30 | "react/jsx-props-no-spreading": "off", 31 | "no-use-before-define": "off", 32 | "no-undef": "off" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const webRouter = require('./routes/webRouter'); 4 | const dashboardRouter = require('./routes/dashboardRouter'); 5 | 6 | const app = express(); 7 | const PORT = 3001; 8 | 9 | app.use(express.json()); 10 | 11 | app.use('/settings', webRouter); 12 | app.use('/dashboard', dashboardRouter); 13 | // app.use('/history', historyRouter); 14 | 15 | app.use(express.static(path.resolve(__dirname, '../client'))); 16 | 17 | app.use('/build', express.static(path.join(__dirname, '../build'))); 18 | 19 | app.get('*', (req, res) => { 20 | res.status(200).sendFile(path.resolve(__dirname, '../client/index.html')); 21 | }); 22 | 23 | // app.get('*', (req, res) => { 24 | // res.status(404).send() 25 | // }) 26 | 27 | // fix 28 | // app.use((err, req, res) => { 29 | // res.status(500).send("There was an error: " + err.message) 30 | // }) 31 | // make a global error handler, console.log 32 | // the getall error 33 | 34 | app.listen(PORT, () => { 35 | console.log('Listening on port: ', PORT); 36 | }); 37 | -------------------------------------------------------------------------------- /client/styles/mainStyle.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | body { 4 | background-color: $mainBlack; 5 | } 6 | 7 | .nav-and-logos { 8 | min-width: 1000px; 9 | display: flex; 10 | flex-direction: row; 11 | justify-content: space-between; 12 | } 13 | 14 | .logos { 15 | align-self: center; 16 | } 17 | 18 | nav { 19 | width: 500px; 20 | margin: 20px; 21 | display: flex; 22 | flex-direction: row; 23 | justify-content: space-between; 24 | align-items: center; 25 | } 26 | 27 | .nav-links { 28 | text-decoration: none; 29 | font-family: Arial, Helvetica, sans-serif; 30 | font-weight: bold; 31 | font-size: 17px; 32 | color: $darkerGray 33 | } 34 | 35 | .nav-links:visited { 36 | text-decoration: none; 37 | } 38 | 39 | .nav-links:hover { 40 | color: $mainGray 41 | } 42 | 43 | .prometheus-selector { 44 | background: $mainBlack; 45 | border: 0px; 46 | outline: 0px; 47 | color: $darkerGray; 48 | font-weight: bold; 49 | font-size: 17px; 50 | } 51 | 52 | #history { 53 | margin-left: 20px; 54 | } 55 | 56 | .prometheus-selector:hover { 57 | color: $mainGray 58 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /client/components/dashboard/ChartsContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import IndividualChartContainer from './IndividualChartContainer'; 3 | 4 | const ChartsContainer = ({ 5 | id, 6 | allCharts, 7 | setAllCharts, 8 | setColumns, 9 | setChartName, 10 | setChart, 11 | setFilters, 12 | prometheusInstance, 13 | setPrometheusInstance, 14 | }) => { 15 | /* 16 | displays charts on main dashboard page 17 | */ 18 | const chartsToDisplay = []; 19 | allCharts.forEach((individualChart) => { 20 | if (individualChart !== null) { 21 | chartsToDisplay.push( 22 | , 38 | ); 39 | } 40 | }); 41 | 42 | return ( 43 |
44 | {chartsToDisplay} 45 |
46 | ); 47 | }; 48 | 49 | export default ChartsContainer; 50 | -------------------------------------------------------------------------------- /client/components/settings/SettingsContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // eslint-disable-next-line object-curly-newline 3 | import { Switch, Route, Link, useRouteMatch } from 'react-router-dom'; 4 | import SettingsCard from './SettingsCard'; 5 | import AddEditCard from './AddEditCard'; 6 | 7 | const SettingsContainer = ({ settingsArr, setSettingsArr, setPrometheusConnections }) => { 8 | const { path, url } = useRouteMatch(); 9 | 10 | const cardArray = settingsArr.map((setting) => { 11 | return ( 12 | 16 | ); 17 | }); 18 | 19 | return ( 20 |
21 | 22 | 23 | 28 | 29 | 30 |

Settings

31 |
32 | {cardArray} 33 |
34 | 35 |
+
36 | 37 |
38 |
39 |
40 |
41 |
42 | ); 43 | }; 44 | 45 | export default SettingsContainer; 46 | -------------------------------------------------------------------------------- /client/components/dashboard/IndividualDropDown.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/control-has-associated-label */ 2 | import React, { useState, useEffect } from 'react'; 3 | 4 | const IndividualDropDown = ({ 5 | filters, 6 | label, 7 | onChange, 8 | prometheusInstance, 9 | }) => { 10 | /* 11 | initializes options for a single label dropdown 12 | */ 13 | const [options, setOptions] = useState(() => []); 14 | 15 | /* 16 | retrieves options from Prometheus for a particular label 17 | */ 18 | const getOptions = async () => { 19 | const optionTags = [, 28 | ); 29 | } else { 30 | optionTags.push( 31 | , 32 | ); 33 | } 34 | }); 35 | setOptions(optionTags); 36 | }); 37 | } 38 | }; 39 | 40 | useEffect(() => { 41 | getOptions(); 42 | }, []); 43 | 44 | return ( 45 |
46 | 47 | 50 |
51 | ); 52 | }; 53 | 54 | export default IndividualDropDown; 55 | -------------------------------------------------------------------------------- /client/components/MainRoutes.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Switch, Route } from 'react-router-dom'; 3 | import DashboardContainer from './dashboard/DashboardContainer'; 4 | import SettingsContainer from './settings/SettingsContainer'; 5 | import HistoryContainer from './history/HistoryContainer'; 6 | 7 | const MainRoutes = ({ 8 | allCharts, 9 | setAllCharts, 10 | prometheusInstance, 11 | setPrometheusInstance, 12 | setPrometheusConnections, 13 | }) => { 14 | /* 15 | initializes state of all settings to display on settings page 16 | */ 17 | const [settingsArr, setSettingsArr] = useState(() => []); 18 | 19 | /* 20 | retrieves all existing settings from database to display on 21 | settings page 22 | */ 23 | const getSettings = async () => { 24 | await fetch('/settings/all') 25 | .then((resp) => resp.json()) 26 | .then((result) => { 27 | setSettingsArr(result.settings); 28 | }).catch((e) => console.log(e)); 29 | }; 30 | 31 | useEffect(() => { 32 | getSettings(); 33 | }, []); 34 | 35 | return ( 36 | 37 | 38 | 44 | 45 | 46 | 51 | 52 | 53 | 54 | ); 55 | }; 56 | 57 | export default MainRoutes; 58 | -------------------------------------------------------------------------------- /server/controllers/webController.js: -------------------------------------------------------------------------------- 1 | const { Data } = require('../models/webModel'); 2 | 3 | const webController = {}; 4 | 5 | webController.getAll = (req, res, next) => { 6 | Data.find({}, (err, data) => { 7 | if (err) { 8 | return next({ status: 500, log: 'There was an error', message: err.message }); 9 | } 10 | res.locals.settings = data; 11 | if (!data) res.locals.settings = []; 12 | return next(); 13 | }); 14 | }; 15 | 16 | webController.createCluster = (req, res, next) => { 17 | Data.create( 18 | { 19 | name: req.body.name, 20 | ipAddress: req.body.ipAddress, 21 | port: req.body.port, 22 | }, 23 | (err, result) => { 24 | if (err) { 25 | console.log(err); 26 | res.status(500).send({ success: false }); 27 | return; 28 | } 29 | console.log(`Created: ${result}`); 30 | return next(); 31 | }, 32 | ); 33 | }; 34 | 35 | webController.updateCluster = (req, res, next) => { 36 | // put request 37 | // PUT is for checking if resource is exists then update , else create new resource 38 | const newData = req.body; 39 | console.log(`This is in update ${req.params.name}`); 40 | Data.findOneAndUpdate({ name: req.params.name }, newData, (err, result) => { 41 | if (err || !result) { 42 | res.status(500).send({ success: false }); 43 | return; 44 | } 45 | return next(); 46 | }); 47 | }; 48 | 49 | webController.deleteCluster = (req, res, next) => { 50 | Data.findOneAndDelete({ name: req.params.name }, (err, result) => { 51 | if (err || !result) { 52 | console.log(err); 53 | res.status(500).send({ success: false }); 54 | return; 55 | } 56 | console.log(`Deleted: ${result}`); 57 | return next(); 58 | }); 59 | }; 60 | 61 | module.exports = webController; 62 | -------------------------------------------------------------------------------- /client/components/dashboard/SingleNumberDisplay.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import queryAlgo from './promQLQueryAlgorithms'; 3 | 4 | const SingleNumberDisplay = ({ 5 | type, 6 | columns, 7 | filters, 8 | prometheusInstance, 9 | }) => { 10 | /* 11 | initializes state of data single number display 12 | */ 13 | const [data, setData] = useState(() => 0); 14 | 15 | /* 16 | retrieves data from Prometheus based on data selector columns 17 | to be represented as a single number 18 | */ 19 | const getData = async () => { 20 | const aggregation = columns.aggregationSelected.list; 21 | const metrics = columns.metricsSelected.list; 22 | const time = columns.timeRangeSelected.list; 23 | 24 | const query = queryAlgo(metrics, time, aggregation, filters); 25 | console.log(query); 26 | 27 | if (prometheusInstance !== undefined) { 28 | await fetch(`http://${prometheusInstance.ipAddress}:${prometheusInstance.port}/api/v1/query?${query}`) 29 | .then((response) => response.json()) 30 | .then((response) => { 31 | console.log(response); 32 | setData(parseInt(response.data.result[0].value[1])); 33 | }); 34 | } 35 | }; 36 | 37 | /* 38 | sets up a recurring fetch request to Prometheus every minute 39 | to update donut chart with latest data 40 | */ 41 | useEffect(() => { 42 | getData(); 43 | if (type === 'saved-chart') { 44 | const interval = setInterval(() => { 45 | console.log('refetching'); 46 | getData(); 47 | }, 60000); 48 | return () => clearInterval(interval); 49 | } 50 | }, []); 51 | 52 | return ( 53 |
54 | {data.toLocaleString()} 55 |
56 | ); 57 | }; 58 | 59 | export default SingleNumberDisplay; 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | package-lock.json 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | release-builds/ 13 | node_modules/ 14 | 15 | build/ 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | .env.test 79 | 80 | # parcel-bundler cache (https://parceljs.org/) 81 | .cache 82 | 83 | # Next.js build output 84 | .next 85 | 86 | # Nuxt.js build / generate output 87 | .nuxt 88 | dist 89 | 90 | # Gatsby files 91 | .cache/ 92 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 93 | # https://nextjs.org/blog/next-9-1#public-directory-support 94 | # public 95 | 96 | # vuepress build output 97 | .vuepress/dist 98 | 99 | # Serverless directories 100 | .serverless/ 101 | 102 | # FuseBox cache 103 | .fusebox/ 104 | 105 | # DynamoDB Local files 106 | .dynamodb/ 107 | 108 | # TernJS port file 109 | .tern-port 110 | -------------------------------------------------------------------------------- /server/models/webModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | // make secure soon 4 | // port 27017 5 | const MONGO_URI = 'mongodb://localhost/First-M8'; 6 | const db = mongoose 7 | .connect(MONGO_URI, { 8 | // options for the connect method to parse the URI 9 | useNewUrlParser: true, 10 | useUnifiedTopology: true, 11 | // sets the name of the DB that our collections are part of 12 | dbName: 'Production_Database', 13 | }) 14 | .then(() => console.log('Connected to Mongo DB.')) 15 | .catch((err) => console.log(err)); 16 | mongoose.set('useFindAndModify', false); 17 | 18 | const { Schema } = mongoose; 19 | 20 | const reqString = { 21 | type: String, 22 | required: true, 23 | }; 24 | 25 | const reqNum = { 26 | type: Number, 27 | required: true, 28 | }; 29 | 30 | const reqArray = { 31 | type: Array, 32 | required: true, 33 | }; 34 | 35 | const reqObject = { 36 | type: Object, 37 | require: true, 38 | }; 39 | 40 | const inputSchema = new Schema({}); 41 | 42 | // basic data schema 43 | const dataSchema = new Schema({ 44 | name: reqString, 45 | 46 | ipAddress: reqString, 47 | 48 | port: reqNum, 49 | }); 50 | 51 | // ChartSetting schema 52 | // rename 53 | // holding state of ChartSettings setup 54 | // if added in functionality to edit, pull this data 55 | // users can see what's currently selected 56 | const ChartSettingSchema = new Schema({ 57 | name: reqString, 58 | columns: reqObject, 59 | filters: reqObject, 60 | }); 61 | 62 | // second ChartSetting schema 63 | // store all table and ChartSettings the user has created 64 | // on primary dashboard page, display all ChartSettings 65 | const displaySchema = new Schema({ 66 | instance: reqString, 67 | display: reqArray, 68 | }); 69 | 70 | // History schema 71 | // store all history related data 72 | const historySchema = new Schema({ 73 | name: reqString, 74 | promqlHis: reqArray, 75 | }); 76 | 77 | const Input = mongoose.model('input', inputSchema); 78 | const Data = mongoose.model('data', dataSchema); 79 | const ChartSetting = mongoose.model('chartSetting', ChartSettingSchema); 80 | const Display = mongoose.model('display', displaySchema); 81 | const History = mongoose.model('history', historySchema); 82 | 83 | module.exports = { 84 | db, 85 | Input, 86 | Data, 87 | ChartSetting, 88 | Display, 89 | History, 90 | }; 91 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | module.exports = { 7 | mode: process.env.NODE_ENV, 8 | entry: './client/index.js', 9 | output: { 10 | path: path.resolve(__dirname, 'build'), 11 | filename: 'bundle.js', 12 | publicPath: './build/', 13 | }, 14 | target: 'electron-main', 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.jsx?/, 19 | exclude: /node_modules/, 20 | use: { 21 | loader: 'babel-loader', 22 | options: { 23 | presets: ['@babel/preset-env', '@babel/preset-react'], 24 | plugins: ['@babel/transform-runtime'], 25 | }, 26 | }, 27 | }, 28 | { 29 | test: /\.s[ac]ss$/i, 30 | use: [ 31 | // Creates `style` nodes from JS strings 32 | 'style-loader', 33 | // Translates CSS into CommonJS 34 | 'css-loader', 35 | // Compiles Sass to CSS 36 | 'sass-loader', 37 | ], 38 | }, 39 | { 40 | test: /\.css$/i, 41 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 42 | }, 43 | { 44 | test: /\.(jpe?g|png|gif|svg)$/i, 45 | type: 'asset', 46 | }, 47 | ], 48 | }, 49 | plugins: [ 50 | new HtmlWebpackPlugin({ 51 | template: '/client/index.html', 52 | }), 53 | new MiniCssExtractPlugin(), 54 | new ImageMinimizerPlugin({ 55 | minimizerOptions: { 56 | // Lossless optimization with custom option 57 | // Feel free to experiment with options for better result for you 58 | plugins: [ 59 | ['gifsicle', { interlaced: true }], 60 | ['jpegtran', { progressive: true }], 61 | ['optipng', { optimizationLevel: 5 }], 62 | [ 63 | 'svgo', 64 | { 65 | plugins: [ 66 | { 67 | removeViewBox: false, 68 | }, 69 | ], 70 | }, 71 | ], 72 | ], 73 | }, 74 | }), 75 | ], 76 | devtool: 'eval-source-map', 77 | devServer: { 78 | // contentBase: path.join(__dirname, "/client"), 79 | // compress: true, 80 | // port: 3000, 81 | publicPath: '/build', 82 | proxy: { 83 | '/**': 'http://localhost:3001', 84 | }, 85 | hot: true, 86 | historyApiFallback: true, 87 | }, 88 | resolve: { 89 | extensions: ['', '.js', '.jsx'], 90 | }, 91 | }; 92 | -------------------------------------------------------------------------------- /client/components/dashboard/DonutChart.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | // eslint-disable-next-line object-curly-newline 3 | import { ResponsiveContainer, PieChart, Pie, Cell, Label } from 'recharts'; 4 | 5 | const DonutChart = ({ 6 | type, 7 | columns, 8 | prometheusInstance, 9 | }) => { 10 | /* 11 | initializes state of data and label for donut chart 12 | */ 13 | const [data, setData] = useState(() => []); 14 | const [labelForDonut, setLabelForDonut] = useState(() => ''); 15 | 16 | /* 17 | retrieves data from Prometheus based on data selector columns 18 | to be represented as a donut chart 19 | */ 20 | const getData = async () => { 21 | const metrics = columns.metricsSelected.list; 22 | 23 | const query = `query=${metrics[0]}/${metrics[1]}`; 24 | 25 | if (prometheusInstance !== undefined) { 26 | await fetch(`http://${prometheusInstance.ipAddress}:${prometheusInstance.port}/api/v1/query?${query}`) 27 | .then((response) => response.json()) 28 | .then((response) => { 29 | const percentageUsed = parseFloat(response.data.result[0].value[1]); 30 | const newData = [ 31 | { name: 'used', value: percentageUsed }, 32 | { name: 'remaining', value: 1 - percentageUsed }, 33 | ]; 34 | setData(newData); 35 | setLabelForDonut(`${Math.round(newData[0].value * 100)}%`); 36 | }); 37 | } 38 | }; 39 | 40 | /* 41 | sets up a recurring fetch request to Prometheus every minute 42 | to update donut chart with latest data 43 | */ 44 | useEffect(() => { 45 | getData(); 46 | if (type === 'saved-chart') { 47 | const interval = setInterval(() => { 48 | console.log('refetching'); 49 | getData(); 50 | }, 60000); 51 | return () => clearInterval(interval); 52 | } 53 | }, []); 54 | 55 | return ( 56 | 57 | 58 | 65 | {data.map((dataPoints, index) => { 66 | if (index === 1) { 67 | return ; 68 | } 69 | return ; 70 | })} 71 | 81 | 82 | 83 | ); 84 | }; 85 | 86 | export default DonutChart; 87 | -------------------------------------------------------------------------------- /client/styles/settingStyle.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .settings-container { 4 | margin: 20px; 5 | display: grid; 6 | grid-gap: 30px 50px; 7 | grid-template-columns: repeat(5, 1fr); 8 | } 9 | 10 | h2 { 11 | margin: 20px; 12 | font-family: Arial, Helvetica, sans-serif; 13 | color: $mainGray; 14 | } 15 | 16 | .settings-card { 17 | min-width: 200px; 18 | border: 0.25pt $darkestGray solid; 19 | border-radius: 5px; 20 | padding: 10px; 21 | } 22 | 23 | .settings-card:hover { 24 | border: 2pt $darkestGray solid; 25 | } 26 | 27 | .settings-detail { 28 | display: flex; 29 | flex-direction: column; 30 | align-items: center; 31 | text-decoration: none; 32 | font-family: Arial, Helvetica, sans-serif; 33 | color: $mainGray; 34 | } 35 | 36 | #newcards { 37 | display: flex; 38 | flex-direction: row; 39 | align-items: center; 40 | } 41 | 42 | .add-card-link { 43 | text-decoration: none; 44 | font-family: Arial, Helvetica, sans-serif; 45 | color: $mainBlack; 46 | font-size: 100px; 47 | } 48 | 49 | #newcard{ 50 | display: flex; 51 | flex-direction: row; 52 | align-items: center; 53 | justify-content: center; 54 | background: $darkestGray; 55 | width: 100px; 56 | height: 100px; 57 | background-size: 100% 100%; 58 | } 59 | 60 | #newcard:hover { 61 | background: $darkerGray; 62 | } 63 | 64 | .settings-form { 65 | margin-left: 20px; 66 | margin-top: 25px; 67 | min-width: 200px; 68 | min-height: 200px; 69 | display: flex; 70 | flex-direction: column; 71 | justify-content: space-between; 72 | } 73 | 74 | .individual-entry { 75 | display: flex; 76 | flex-direction: row; 77 | height: 30px; 78 | width: 250px; 79 | align-items: center; 80 | justify-content: space-between; 81 | } 82 | 83 | .settings-label { 84 | font-family: Arial, Helvetica, sans-serif; 85 | color: $darkerGray; 86 | } 87 | 88 | .settings-input { 89 | align-self: flex-end; 90 | outline: none; 91 | height: 30px; 92 | border: 1pt $darkestGray solid; 93 | background-color: $darkestGray; 94 | color: $mainGray 95 | } 96 | 97 | .settings-input:focus { 98 | outline: none; 99 | background-color: $mainBlack; 100 | color: $mainGray; 101 | } 102 | 103 | .form-buttons { 104 | margin-right: 15px; 105 | margin-top: 15px; 106 | font-family: Arial, Helvetica, sans-serif; 107 | font-weight: bold; 108 | border: none; 109 | border-radius: 2px; 110 | width: 100px; 111 | height: 30px; 112 | color: white; 113 | } 114 | 115 | #submit { 116 | background-color: $mainBlue; 117 | } 118 | 119 | #submit:hover { 120 | background-color: $darkerBlue; 121 | } 122 | 123 | #delete { 124 | background-color: $mainRed; 125 | } 126 | 127 | #delete:hover { 128 | background-color: $darkerRed; 129 | } 130 | -------------------------------------------------------------------------------- /server/routes/dashboardRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const dashboardController = require('../controllers/dashboardController'); 3 | 4 | const router = express.Router(); 5 | 6 | /* 7 | sends back query result of getting all display for 8 | a particular Prometheus instance 9 | */ 10 | router.get('/:instance', dashboardController.getAllDisplay, (req, res) => { 11 | res.status(200).send(res.locals.data); 12 | }); 13 | 14 | /* 15 | sends back whether creating a new chart setting was successful 16 | */ 17 | router.post('/newChart/:name', dashboardController.createChartSetting, (req, res) => { 18 | res.status(200).send({ success: true }); 19 | }); 20 | 21 | /* 22 | sends back whether updating all display for a particular 23 | Prometheus instance was successful 24 | */ 25 | router.patch('/allCharts/:instance', dashboardController.updateAllDisplay, (req, res) => { 26 | res.status(200).send({ success: true }); 27 | }); 28 | 29 | /* 30 | sends back query result of getting chart setting for 31 | a particular chart name 32 | */ 33 | router.get('/editChart/:name', dashboardController.getChartSetting, (req, res) => { 34 | res.status(200).send(res.locals.chartSetting); 35 | }); 36 | 37 | /* 38 | sends back whether a chart name already exists 39 | */ 40 | router.get( 41 | '/chart/:name', 42 | dashboardController.getChartSetting, 43 | dashboardController.checkIfChartExists, 44 | (req, res) => { 45 | res.status(200).send(res.locals.chartExists); 46 | }, 47 | ); 48 | 49 | /* 50 | sends back whether deleting chart setting for a particular 51 | chart name and a single display for a particular Prometheus 52 | instance was successful 53 | */ 54 | router.delete( 55 | '/deleteChart/:instance/:name', 56 | dashboardController.deleteChartSetting, 57 | dashboardController.getAllDisplay, 58 | dashboardController.deleteSingleDisplay, 59 | (req, res) => { 60 | res.status(200).send(res.locals.data); 61 | }, 62 | ); 63 | 64 | /* 65 | sends back whether updating chart setting for a particular 66 | chart name and a single display for a particular 67 | Prometheus instance was successful 68 | */ 69 | router.patch( 70 | '/editChart/:name', 71 | dashboardController.updateChartSetting, 72 | dashboardController.updateSingleDisplay, 73 | (req, res) => { 74 | res.status(200).send({ success: true }); 75 | }, 76 | ); 77 | 78 | /* 79 | sends back query result of getting info for all Prometheus instances 80 | */ 81 | router.get('/connect/all', dashboardController.getAllPrometheusInstances, (req, res) => { 82 | res.status(200).send(res.locals.allPrometheusInstances); 83 | }); 84 | 85 | /* 86 | sends back query result of getting info for a particular 87 | Prometheus instance 88 | */ 89 | router.get('/connect/:name', dashboardController.getPrometheusInstance, (req, res) => { 90 | res.status(200).send(res.locals.prometheusInstance); 91 | }); 92 | module.exports = router; 93 | -------------------------------------------------------------------------------- /__tests__/server/settingsRoutes.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | 3 | const server = 'http://localhost:3001'; 4 | 5 | describe('Route integration for settings', () => { 6 | describe('/settings/all', () => { 7 | describe('GET', () => { 8 | it('responds with 200 status, application/json content type, and all data from DB', () => { 9 | return request(server) 10 | .get('/settings/all') 11 | .expect('Content-Type', /application\/json/) 12 | .expect(200) 13 | .then((response) => { 14 | expect(Array.isArray(response.body.settings)).toEqual(true); 15 | response.body.settings.forEach((setting) => { 16 | expect(typeof setting).toEqual('object'); 17 | }); 18 | }); 19 | }); 20 | }); 21 | }); 22 | 23 | describe('/settings/new', () => { 24 | describe('POST', () => { 25 | it('respond with status 200, application/json content type, and settings info with new setting added', () => { 26 | return request(server) 27 | .post('/settings/new') 28 | .send({ 29 | name: 'Test', 30 | ipAddress: '1.2.3.4.5', 31 | port: 1234, 32 | }) 33 | .expect('Content-Type', /application\/json/) 34 | .expect(200) 35 | .then((response) => { 36 | expect(response.body.settings[response.body.settings.length - 1].name).toEqual('Test'); 37 | }); 38 | }); 39 | }); 40 | }); 41 | 42 | describe('/settings/:name', () => { 43 | describe('PUT', () => { 44 | it('responds with status 200, application/json content type, and settings info with particular setting updated', () => { 45 | return request(server) 46 | .put('/settings/Test') 47 | .send({ 48 | name: 'otherTest', 49 | ipAddress: '1.2.3.4.5', 50 | port: 1234, 51 | }) 52 | .expect('Content-Type', /application\/json/) 53 | .expect(200) 54 | .then((response) => { 55 | expect(response.body.settings[response.body.settings.length - 1].name).toEqual('otherTest'); 56 | }); 57 | }); 58 | }); 59 | }); 60 | 61 | describe('/settings/:name/delete', () => { 62 | describe('DELETE', () => { 63 | it('responds with status 200, application/json content type, and settings info with particular setting deleted', () => { 64 | return request(server) 65 | .delete('/settings/otherTest/delete') 66 | .send({ 67 | name: 'otherTest', 68 | ipAddress: '1.2.3.4.5', 69 | port: 1234, 70 | }) 71 | .expect('Content-Type', /application\/json/) 72 | .expect(200) 73 | .then((response) => { 74 | expect(response.body.settings[response.body.settings.length - 1].name).not.toEqual('otherTest'); 75 | }); 76 | }); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FirstM8", 3 | "version": "1.0.0", 4 | "description": "An easier way to write PromQL", 5 | "main": "./main/main.js", 6 | "scripts": { 7 | "test": "jest", 8 | "start": "cross-env NODE_ENV=production electron .", 9 | "build": "NODE_ENV=production webpack", 10 | "dev": "cross-env NODE_ENV=development concurrently \"NODE_ENV=development webpack serve --open\" \"NODE_ENV=development nodemon ./server/server.js\"", 11 | "package-mac-arm": "electron-packager . --overwrite --platform=darwin --arch=arm64 --prune=true --out=release-builds", 12 | "package-mac-intel": "electron-packager . --overwrite --platform=darwin --arch=x64 --prune=true --out=release-builds", 13 | "package-win-64": "electron-packager . --overwrite --platform=win32 --arch=x64 --prune=true --out=release-builds", 14 | "package-linux": "electron-packager . --overwrite --platform=linux --arch=x64 --prune=true --out=release-builds", 15 | "dmg": "electron-installer-dmg ./release-builds/FirstM8-darwin-x64/FirstM8.app FirstM8" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "dependencies": { 20 | "@babel/core": "^7.14.3", 21 | "@babel/preset-env": "^7.14.4", 22 | "@babel/preset-react": "^7.13.13", 23 | "@babel/runtime": "^7.14.0", 24 | "babel-loader": "^8.2.2", 25 | "concurrently": "^6.2.0", 26 | "cross-env": "^7.0.3", 27 | "css-loader": "^5.2.6", 28 | "express": "^4.17.1", 29 | "html-webpack-plugin": "^5.3.1", 30 | "image-minimizer-webpack-plugin": "^2.2.0", 31 | "ip-regex": "^4.3.0", 32 | "jest-fetch-mock": "^3.0.3", 33 | "mini-css-extract-plugin": "^1.6.0", 34 | "moment": "^2.29.1", 35 | "mongodb": "^3.6.8", 36 | "mongoose": "^5.12.12", 37 | "nodemon": "^2.0.7", 38 | "path-to-regexp": "^6.2.0", 39 | "react": "^17.0.2", 40 | "react-beautiful-dnd": "^13.1.0", 41 | "react-dom": "^17.0.2", 42 | "react-router": "^5.2.0", 43 | "react-router-dom": "^5.2.0", 44 | "recharts": "^2.0.9", 45 | "regenerator-runtime": "^0.13.7", 46 | "sass": "^1.34.0", 47 | "sass-loader": "^11.1.1", 48 | "style-loader": "^2.0.0", 49 | "webpack": "^5.38.1", 50 | "webpack-cli": "^4.7.0", 51 | "webpack-dev-server": "^3.11.2" 52 | }, 53 | "devDependencies": { 54 | "@babel/plugin-transform-runtime": "^7.14.3", 55 | "electron": "*", 56 | "electron-installer-dmg": "^3.0.0", 57 | "electron-installer-zip": "^0.1.2", 58 | "electron-packager": "^15.2.0", 59 | "electron-reloader": "^1.2.1", 60 | "enzyme": "^3.11.0", 61 | "enzyme-to-json": "^3.6.2", 62 | "eslint": "^7.28.0", 63 | "eslint-config-airbnb": "^18.2.1", 64 | "eslint-plugin-import": "^2.23.4", 65 | "eslint-plugin-jsx-a11y": "^6.4.1", 66 | "eslint-plugin-react": "^7.24.0", 67 | "eslint-plugin-react-hooks": "^4.2.0", 68 | "imagemin-gifsicle": "^7.0.0", 69 | "imagemin-jpegtran": "^7.0.0", 70 | "imagemin-optipng": "^8.0.0", 71 | "imagemin-svgo": "^9.0.0", 72 | "jest": "^27.0.4", 73 | "node-gyp": "^8.1.0", 74 | "prettier": "^2.3.0", 75 | "prettier-eslint": "^12.0.0", 76 | "react-test-renderer": "^17.0.2", 77 | "supertest": "^6.1.3", 78 | "webpack-electron-packager": "^1.0.0" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /client/components/dashboard/promQLQueryAlgorithms.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-use-before-define */ 2 | /* 3 | time-to-seconds conversion 4 | */ 5 | const timeToSeconds = { 6 | '1 Second': { value: 1, step: 1 }, 7 | '10 Seconds': { value: 10, step: 1 }, 8 | '1 Minute': { value: 60, step: 1 }, 9 | '5 Minutes': { value: 60 * 5, step: 1 }, 10 | '15 Minutes': { value: 15 * 60, step: 3 }, 11 | '30 Minutes': { value: 30 * 60, step: 7 }, 12 | '1 Hour': { value: 60 * 60, step: 14 }, 13 | '3 Hours': { value: 60 * 60 * 3, step: 42 }, 14 | '6 Hours': { value: 60 * 60 * 6, step: 86 }, 15 | '12 Hours': { value: 60 * 60 * 12, step: 172 }, 16 | }; 17 | 18 | /* 19 | takes data selections and translates them into PromQL queries to send back: 20 | currently, only handles range queries and instant queries with single metric 21 | selection, sum/average/min/max, and filtering 22 | does not yet handle complex queries with multple metrics selected, queries 23 | using division or multiplication, or grouping 24 | all parameters but labels are arrays, labels is an object 25 | */ 26 | function queryAlgo(metric, time, aggre, labels) { 27 | let fullQuery = ''; 28 | if (!metric || metric.length === 0) return fullQuery; 29 | const aggreQuery = aggregationParser(aggre); 30 | const metricQuery = metricParser(metric, aggreQuery.valid); 31 | const labelQuery = labelParser(labels); 32 | fullQuery = aggreQuery.query + metricQuery + labelQuery; 33 | fullQuery += ')'.repeat(aggreQuery.count); 34 | let timeStart; 35 | const timeNow = Date.now() / 1000; 36 | if (time !== null && time !== undefined && time.length > 0) { 37 | timeStart = timeToSeconds[time[0]]; 38 | fullQuery += `&start=${timeNow - timeStart.value}&end=${timeNow}&step=${ 39 | timeStart.step 40 | }`; 41 | } 42 | return fullQuery; 43 | } 44 | 45 | /* 46 | helper function to help build out the aggregation portion 47 | of the query 48 | */ 49 | function aggregationParser(aggre) { 50 | const aggreObj = { count: 0, valid: false }; 51 | let aggreStr = ''; 52 | if ( 53 | aggre !== null 54 | && aggre !== undefined 55 | && Array.isArray(aggre) 56 | && aggre.length > 0 57 | ) { 58 | aggreStr += 'query='; 59 | aggreObj.valid = true; 60 | for (let i = 0; i < aggre.length; i += 1) { 61 | // if (i !== 0) aggreStr += "("; 62 | aggreObj.count += 1; 63 | aggreStr += `${aggre[i].toLowerCase()}(`; 64 | } 65 | } 66 | aggreObj.query = aggreStr; 67 | return aggreObj; 68 | } 69 | 70 | /* 71 | helper function to help build out the metric portion 72 | of the query 73 | */ 74 | function metricParser(metric, aggreBool) { 75 | let metricStr = ''; 76 | if (!aggreBool) metricStr += 'query='; 77 | metricStr += metric[0]; 78 | return metricStr; 79 | } 80 | 81 | /* 82 | helper function to help build out the label portion 83 | of the query 84 | */ 85 | function labelParser(labels) { 86 | let labelStr = ''; 87 | if (labels !== null && labels !== undefined) { 88 | const keyArr = Object.keys(labels); 89 | for (let i = 0; i < keyArr.length; i += 1) { 90 | if (labels[keyArr[i]] !== '') { 91 | if (labelStr !== '') labelStr += ','; 92 | labelStr += `${keyArr[i]}="${labels[keyArr[i]]}"`; 93 | } 94 | } 95 | } 96 | if (labelStr !== '') labelStr = `{${labelStr}}`; 97 | return labelStr; 98 | } 99 | 100 | module.exports = queryAlgo; 101 | -------------------------------------------------------------------------------- /client/components/dashboard/TimeSeriesChart.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | // eslint-disable-next-line object-curly-newline 3 | import { CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis, LineChart, Line } from 'recharts'; 4 | import moment from 'moment'; 5 | import queryAlgo from './promQLQueryAlgorithms'; 6 | 7 | const TimeSeriesChart = ({ 8 | type, 9 | columns, 10 | filters, 11 | prometheusInstance, 12 | }) => { 13 | /* 14 | initializes state of data series for time series chart 15 | */ 16 | const [chartSeries, setChartSeries] = useState(() => []); 17 | 18 | /* 19 | retrieves data from Prometheus based on data selector columns 20 | to be represented as a time series chart 21 | */ 22 | const getData = async () => { 23 | const aggregation = columns.aggregationSelected.list; 24 | const metrics = columns.metricsSelected.list; 25 | const time = columns.timeRangeSelected.list; 26 | 27 | const query = queryAlgo(metrics, time, aggregation, filters); 28 | 29 | const chartLines = []; 30 | const dataSeries = []; 31 | if (prometheusInstance !== undefined) { 32 | await fetch(`http://${prometheusInstance.ipAddress}:${prometheusInstance.port}/api/v1/query_range?${query}`) 33 | .then((response) => response.json()) 34 | .then((response) => { 35 | response.data.result.forEach((metric) => { 36 | const series = metric.values.map((dataPoint) => { 37 | return ({ 38 | value: parseInt(dataPoint[1]), 39 | time: dataPoint[0], 40 | }); 41 | }); 42 | dataSeries.push(series); 43 | }); 44 | dataSeries.forEach((series) => { 45 | chartLines.push( 46 | , 47 | ); 48 | }); 49 | setChartSeries(chartLines); 50 | }); 51 | } 52 | }; 53 | 54 | /* 55 | sets up a recurring fetch request to Prometheus every minute 56 | to update time series chart with latest data 57 | */ 58 | useEffect(() => { 59 | getData(); 60 | if (type === 'saved-chart') { 61 | const interval = setInterval(() => { 62 | console.log('refetching'); 63 | getData(); 64 | }, 60000); 65 | return () => clearInterval(interval); 66 | } 67 | }, []); 68 | 69 | return ( 70 | 71 | 72 | moment.unix(time).format('h:mm:ss A')} 77 | scale="time" 78 | type="number" 79 | style={{ 80 | fontFamily: 'Arial, Helvetica, sans-serif', 81 | }} 82 | /> 83 | (value / 1000).toLocaleString()} 87 | style={{ 88 | fontFamily: 'Arial, Helvetica, sans-serif', 89 | }} 90 | /> 91 | 92 | moment(Date(time)).format('h:mm:ss A')} 94 | /> 95 | {chartSeries} 96 | 97 | 98 | ); 99 | }; 100 | 101 | export default TimeSeriesChart; 102 | -------------------------------------------------------------------------------- /client/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | // eslint-disable-next-line object-curly-newline 3 | import { BrowserRouter as Router, Link } from 'react-router-dom'; 4 | import MainRoutes from './MainRoutes'; 5 | 6 | const App = () => { 7 | /* 8 | initializes state of all charts to display on main dashboard page, 9 | Prometheus connections to list in drop down top of app, 10 | and the selected instance to use to query Prometheus in other components 11 | */ 12 | const [allCharts, setAllCharts] = useState(() => []); 13 | const [prometheusConnections, setPrometheusConnections] = useState(() => []); 14 | const [prometheusInstance, setPrometheusInstance] = useState(() => {}); 15 | 16 | /* 17 | retrieves all existing charts from database to display on 18 | main dashboard page 19 | */ 20 | const getAllCharts = async (instanceName) => { 21 | await fetch(`/dashboard/${instanceName}`) 22 | .then((response) => response.json()) 23 | .then((data) => { 24 | console.log(data); 25 | if (data[0] !== undefined) { 26 | setAllCharts(data[0].display); 27 | } 28 | }); 29 | }; 30 | 31 | /* 32 | retrieves all existing Prometheus connections from database 33 | to list in drop down at top of app 34 | */ 35 | const getAllPrometheusInstances = async () => { 36 | const connectionNames = []; 37 | await fetch('/dashboard/connect/all') 38 | .then((response) => response.json()) 39 | .then((response) => { 40 | response.forEach((connection) => { 41 | connectionNames.push( 42 | , 43 | ); 44 | }); 45 | setPrometheusConnections(connectionNames); 46 | }); 47 | }; 48 | 49 | /* 50 | handles change on Prometheus connection selector drop down: 51 | retrieves settings data for particular connection name and 52 | invokes funciton to get all charts passing in the particular 53 | connection naem 54 | */ 55 | const selectPrometheusInstance = async (event) => { 56 | setAllCharts([]); 57 | await fetch(`/dashboard/connect/${event.target.value}`) 58 | .then((response) => response.json()) 59 | .then((response) => { 60 | setPrometheusInstance(response); 61 | console.log(response); 62 | getAllCharts(response.name); 63 | }); 64 | }; 65 | 66 | useEffect(() => { 67 | getAllPrometheusInstances(); 68 | }, []); 69 | 70 | return ( 71 |
72 | 73 |
74 | 82 |
83 | First M8 logo 84 |
85 |
86 | 94 |
95 |
96 | ); 97 | }; 98 | 99 | export default App; 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ![license](https://img.shields.io/github/license/oslabs-beta/First-M8?color=%2357d3af) ![version](https://img.shields.io/badge/version-Alpha1.0-orange) ![lastcommit](https://img.shields.io/github/last-commit/oslabs-beta/First-M8?color=%2357d3af) ![gitcontribute](https://img.shields.io/github/contributors/oslabs-beta/First-M8) ![gitstars​](https://img.shields.io/github/stars/oslabs-beta/First-M8?style=social) ![gitforks](https://img.shields.io/github/forks/oslabs-beta/First-M8?style=social) 5 | 6 | ## Table of Contents 7 | - About 8 | - Getting Started 9 | - Quick-Start 10 | - Contributors 11 | - Looking Ahead 12 | - License 13 | 14 | ## About 15 | 16 | First M8 is an Electron-based application built to make querying Prometheus instances much more accessable and approachable to those new to Dev Ops or seasoned veterans who want a more intuitive interface when monitoring and scraping data. 17 | 18 | • Drag-and-drop feature eliminates the chance of typing mistakes and greatly streamlines the process of writing complex queries. 19 | 20 | • Users can visualize data that means the most to them in a way that is easy to digest without having to have prior knowledge of PromQL. 21 | 22 | • One-stop-shop application monitoring remains, but users gain the ability to more easily track and separate their queries by different applications and Prometheus instances. 23 | 24 | ## Getting Started 25 | 26 | First M8 can be set up by bundling the electron app and running it as a native desktop App. The instructions are as follows: 27 | 28 | 1. Make sure you're on the main branch 29 | 30 | 2. Go to your terminal and type the following: 31 | 32 | ``` 33 | npm install 34 | 35 | npm run build 36 | 37 | npm run package-mac-arm //for arm-64 Mac OS 38 | 39 | //OR 40 | 41 | npm run package-mac-intel //for intel-based Macs 42 | 43 | //OR 44 | 45 | npm run package-win-64 // for windows computers 46 | 47 | ``` 48 | 3. In your First-M8 folder, navigate to the release-builds folder and double-click on FirstM8.app/exe (executable). 49 | 50 | ## Quick-Start 51 | 52 | When you have opened the First M8 application: 53 | 54 | 1. Select _"Settings"_. 55 | 56 | 2. Input _Name, IP address, and Port_ for your Prometheus instance. 57 | 58 | 3. Select the created setting from the _pull-down menu_. 59 | 60 | 4. Click _'Create a Dashboard'_, name it and select the draggable metrics you want to monitor. 61 | 62 | 5. Once you've selected your metrics and are satisfied with the query, click _"Save"_. 63 | 64 | ## Contributors 65 | 66 | [Jung Ho (Alex) Lee](https://www.linkedin.com/in/jungholee27/) [@jungholee27](https://github.com/jungholee27) 67 | 68 | [Derek Chen](https://www.linkedin.com/in/derek-junhao-chen/) [@derekchen](https://github.com/poofywater) 69 | 70 | [Nisa Lintakoon](https://www.linkedin.com/in/nisalintakoon/) [@nisalintakoon](https://github.com/nisalintakoon) 71 | 72 | [Kevin MacCoy](https://www.linkedin.com/in/kevin-maccoy/) [@kevinmaccoy](https://github.com/kmaccoy) 73 | 74 | ## Looking Ahead 75 | 76 | First M8 is currently in Alpha stage. Some features we look to implement in the future are: 77 | - Downloadable dashboard views and query history. 78 | - Connecting to Grafana viewer. 79 | - Handle more advanced Prometheus queries. 80 | - Add a space to track error logs and highlight trends that may need to be addressed. 81 | - There is currently no Mac installer. Coming soon! 82 | 83 | ## License 84 | 85 | This product is licensed under the MIT License -- see [LICENSE.md](https://github.com/oslabs-beta/First-M8/blob/main/LICENSE) file for more details. 86 | 87 | This product is accelerated by [OS Labs](https://opensourcelabs.io/). 88 | -------------------------------------------------------------------------------- /__tests__/client/promQLQuery.js: -------------------------------------------------------------------------------- 1 | import queryAlgo from '../../client/components/dashboard/promQLQueryAlgorithms'; 2 | 3 | const timeToSeconds = { 4 | oneSecond: 1, 5 | tenSeconds: 10, 6 | oneMinute: 60, 7 | fiveMinutes: 60 * 5, 8 | fifteenMinutes: 15 * 60, 9 | thirtyMinutes: 30 * 60, 10 | oneHour: 60 * 60, 11 | threeHours: 60 * 60 * 3, 12 | sixHours: 60 * 60 * 6, 13 | twelveHours: 60 * 60 * 12, 14 | }; 15 | 16 | describe('simple queries test', () => { 17 | let timeNow; 18 | beforeEach(() => { 19 | jest.useFakeTimers('modern'); 20 | timeNow = Date.now() / 1000; 21 | }); 22 | const myQuery = ['prometheus_http_requests_total']; 23 | it('should carry out one metric default time', () => { 24 | expect(queryAlgo(myQuery)).toEqual( 25 | 'query=prometheus_http_requests_total', 26 | ); 27 | }); 28 | const myTime = ['30 Minutes']; 29 | it('should carry out one metric with custom time', () => { 30 | expect(queryAlgo(myQuery, myTime)).toEqual( 31 | `query=prometheus_http_requests_total&start=${ 32 | timeNow - timeToSeconds.thirtyMinutes 33 | }&end=${timeNow}&step=7`, 34 | ); 35 | }); 36 | describe('should return empty string if empty/null metric', () => { 37 | it('returns empty string for undefined metric', () => { 38 | expect(queryAlgo()).toEqual(''); 39 | }); 40 | it('returns empty string for null metric', () => { 41 | expect(queryAlgo(null, myTime)).toEqual(''); 42 | }); 43 | }); 44 | }); 45 | 46 | describe('query algos with aggregations', () => { 47 | let timeNow; 48 | beforeEach(() => { 49 | jest.useFakeTimers('modern'); 50 | timeNow = Date.now() / 1000; 51 | }); 52 | const myQuery = ['prometheus_http_requests_total']; 53 | let myTime; let 54 | myAggre; 55 | it('handles single aggregation', () => { 56 | myTime = null; 57 | myAggre = ['Sum']; 58 | expect(queryAlgo(myQuery, myTime, myAggre)).toEqual( 59 | 'query=sum(prometheus_http_requests_total)', 60 | ); 61 | }); 62 | it('handles multiple aggregations', () => { 63 | myTime = null; 64 | myAggre = ['Max', 'Sum']; 65 | expect(queryAlgo(myQuery, myTime, myAggre)).toEqual( 66 | 'query=max(sum(prometheus_http_requests_total))', 67 | ); 68 | }); 69 | it('handles aggregations with time argument', () => { 70 | myTime = ['5 Minutes']; 71 | myAggre = ['Sum']; 72 | expect(queryAlgo(myQuery, myTime, myAggre)).toEqual( 73 | `query=sum(prometheus_http_requests_total)&start=${ 74 | timeNow - timeToSeconds.fiveMinutes 75 | }&end=${timeNow}&step=1`, 76 | ); 77 | myAggre = ['Max', 'Sum']; 78 | expect(queryAlgo(myQuery, myTime, myAggre)).toEqual( 79 | `query=max(sum(prometheus_http_requests_total))&start=${ 80 | timeNow - timeToSeconds.fiveMinutes 81 | }&end=${timeNow}&step=1`, 82 | ); 83 | }); 84 | }); 85 | 86 | describe('query algos with labels', () => { 87 | let timeNow; 88 | beforeEach(() => { 89 | jest.useFakeTimers('modern'); 90 | timeNow = Date.now() / 1000; 91 | }); 92 | const myQuery = ['prometheus_http_requests_total']; 93 | let myTime; let myAggre; let 94 | myLabels; 95 | it('handles single labels', () => { 96 | myTime = null; 97 | myAggre = null; 98 | myLabels = { job: 'apiserver' }; 99 | expect(queryAlgo(myQuery, myTime, myAggre, myLabels)).toEqual( 100 | 'query=prometheus_http_requests_total{job="apiserver"}', 101 | ); 102 | }); 103 | it('handles multiple labels', () => { 104 | myTime = null; 105 | myAggre = null; 106 | myLabels = { job: 'apiserver', handler: '/api/comments' }; 107 | expect(queryAlgo(myQuery, myTime, myAggre, myLabels)).toEqual( 108 | 'query=prometheus_http_requests_total{job="apiserver",handler="/api/comments"}', 109 | ); 110 | }); 111 | it('handles labels with time argument', () => { 112 | myTime = ['5 Minutes']; 113 | myAggre = null; 114 | myLabels = { job: 'apiserver', hello: '' }; 115 | expect(queryAlgo(myQuery, myTime, myAggre, myLabels)).toEqual( 116 | `query=prometheus_http_requests_total{job="apiserver"}&start=${ 117 | timeNow - timeToSeconds.fiveMinutes 118 | }&end=${timeNow}&step=1`, 119 | ); 120 | myLabels = { job: 'apiserver', handler: '/api/comments' }; 121 | expect(queryAlgo(myQuery, myTime, myAggre, myLabels)).toEqual( 122 | `query=prometheus_http_requests_total{job="apiserver",handler="/api/comments"}&start=${ 123 | timeNow - timeToSeconds.fiveMinutes 124 | }&end=${timeNow}&step=1`, 125 | ); 126 | }); 127 | }); 128 | 129 | xdescribe('query algos with aggregations and labels', () => { 130 | let timeNow; 131 | beforeEach(() => { 132 | jest.useFakeTimers('modern'); 133 | timeNow = Date.now() / 1000; 134 | }); 135 | const myQuery = 'prometheus_http_requests_total'; 136 | it('', () => {}); 137 | }); 138 | -------------------------------------------------------------------------------- /__tests__/client/settings.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { configure, shallow } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import 'regenerator-runtime'; 5 | 6 | import AddEditCard from '../../client/components/settings/AddEditCard'; 7 | import SettingsCard from '../../client/components/settings/SettingsCard'; 8 | import SettingsContainer from '../../client/components/settings/SettingsContainer'; 9 | 10 | configure({ adapter: new Adapter() }); 11 | 12 | jest.mock('react-router-dom', () => ({ 13 | ...jest.requireActual('react-router-dom'), 14 | useParams: () => ({ 15 | name: 'Test', 16 | }), 17 | useRouteMatch: () => ({ 18 | path: '/settings', 19 | url: '/settings', 20 | }), 21 | })); 22 | 23 | describe('React tests for settings components', () => { 24 | describe('SettingsCard', () => { 25 | let wrapper; 26 | const setting = { 27 | name: 'Test', 28 | ipAddress: 'local:host', 29 | port: 9090, 30 | }; 31 | 32 | beforeAll(() => { 33 | wrapper = shallow(); 34 | }); 35 | 36 | it('Renders a
tag with links to edit settings', () => { 37 | expect(wrapper.type()).toEqual('div'); 38 | expect(wrapper.find('Link').childAt(0).text()).toEqual('Name: Test'); 39 | expect(wrapper.find('Link').childAt(1).text()).toEqual('IP Address: local:host'); 40 | expect(wrapper.find('Link').childAt(2).text()).toEqual('Port: 9090'); 41 | }); 42 | }); 43 | 44 | describe('SettingsContainer', () => { 45 | let wrapper; 46 | const settingsArr = [ 47 | { 48 | name: 'Test', 49 | ipAddress: 'local:host', 50 | port: 9090, 51 | }, 52 | { 53 | name: 'Test2', 54 | ipAddress: 'local:host', 55 | port: 3000, 56 | }, 57 | ]; 58 | beforeAll(() => { 59 | wrapper = shallow(); 60 | }); 61 | 62 | it('Renders a
tag containing AddEditCard component and a
for the SettingsCard component', () => { 63 | expect(wrapper.type()).toEqual('div'); 64 | expect(wrapper.find('AddEditCard').prop('settingsArr')).toEqual(settingsArr); 65 | expect(wrapper.find('.settings-container').type()).toEqual('div'); 66 | }); 67 | 68 | it('Should display all elements in settingsArr as SettingsCard component', () => { 69 | expect(Array.isArray(settingsArr)).toEqual(true); 70 | expect(wrapper.find('SettingsCard').length).toEqual(settingsArr.length); 71 | }); 72 | }); 73 | 74 | describe('AddEditCard', () => { 75 | let wrapper; 76 | const settingsArr = [ 77 | { 78 | name: 'Test', 79 | ipAddress: 'local:host', 80 | port: 9090, 81 | }, 82 | ]; 83 | 84 | beforeAll(() => { 85 | wrapper = shallow(); 86 | }); 87 | 88 | it('Renders a
tag containing a form', () => { 89 | expect(wrapper.type()).toEqual('div'); 90 | expect(wrapper.childAt(0).type()).toEqual('form'); 91 | }); 92 | 93 | it('Should have input fields where the current values are equal to the name, ipAddress, and port in SettingsArr', () => { 94 | const inputFields = wrapper.find('input'); 95 | const textToCheckFor = [settingsArr[0].name, settingsArr[0].ipAddress, settingsArr[0].port]; 96 | inputFields.forEach((node, index) => { 97 | expect(node.prop('value')).toEqual(textToCheckFor[index]); 98 | }); 99 | }); 100 | 101 | it('Takes new input and replaces relevant information', () => { 102 | const eventObjName = { target: { id: 'name', value: 'newname' } }; 103 | const eventObjIp = { target: { id: 'ipaddress', value: 'host:local' } }; 104 | const eventObjPort = { target: { id: 'port', value: 3040 } }; 105 | wrapper.find('#name').simulate('change', eventObjName); 106 | wrapper.find('#ipaddress').simulate('change', eventObjIp); 107 | wrapper.find('#port').simulate('change', eventObjPort); 108 | wrapper.update(); 109 | 110 | expect(wrapper.find('#name').prop('value')).toEqual('newname'); 111 | expect(wrapper.find('#ipaddress').prop('value')).toEqual('host:local'); 112 | expect(wrapper.find('#port').prop('value')).toEqual(3040); 113 | }); 114 | 115 | xit('Should call the onSubmit function when submit button is clicked', () => { 116 | const mockEvent = { preventDefault: jest.fn() }; 117 | wrapper.find('#settings-form').simulate('submit', mockEvent); 118 | expect(mockEvent.preventDefault).toBeCalledTimes(1); 119 | }); 120 | 121 | xit('Should call the onClick function when delete button is clicked', () => { 122 | const button = wrapper.find('#delete'); 123 | const mockDeleteFunction = jest.fn(); 124 | button.simulate('click', mockDeleteFunction); 125 | expect(mockDeleteFunction).toHaveBeenCalled(); 126 | }); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /client/styles/dashboardStyle.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | #new-dashboard-chart { 4 | margin-left: 20px; 5 | margin-bottom: 10px; 6 | font-family: Arial, Helvetica, sans-serif; 7 | font-weight: bold; 8 | width: 200px; 9 | border: none; 10 | border-radius: 2px; 11 | height: 30px; 12 | color: white; 13 | background-color: $mainGreen 14 | } 15 | 16 | #new-dashboard-chart:hover { 17 | background-color: $darkerGreen; 18 | } 19 | 20 | .chart-setup { 21 | margin: 20px; 22 | } 23 | 24 | .enter-chart-name { 25 | margin-bottom: 20px; 26 | } 27 | 28 | .chart-name-label { 29 | font-family: Arial, Helvetica, sans-serif; 30 | color: $darkerGray; 31 | } 32 | 33 | .chart-name-input { 34 | outline: none; 35 | height: 30px; 36 | border: 1pt $darkestGray solid; 37 | background-color: $darkestGray; 38 | color: $mainGray 39 | } 40 | 41 | .chart-name-input:focus { 42 | outline: none; 43 | background-color: $mainBlack; 44 | color: $mainGray; 45 | } 46 | 47 | .notification { 48 | font-family: Arial, Helvetica, sans-serif; 49 | color: $mainRed; 50 | margin-bottom: 20px; 51 | } 52 | 53 | .chart-setup-columns { 54 | display: grid; 55 | grid-template-columns: repeat(6, 1fr); 56 | } 57 | 58 | .chart-setup-individual-column { 59 | border: 0.25pt $darkestGray solid; 60 | padding: 5px; 61 | min-width: 250px; 62 | } 63 | 64 | #droppable-area { 65 | min-width: 250px; 66 | height: 300px; 67 | border: 0.25pt $darkestGray solid; 68 | overflow: auto; 69 | padding: 5px; 70 | margin: 5px; 71 | } 72 | 73 | .column-title { 74 | font-family: Arial, Helvetica, sans-serif; 75 | color: $darkerGray; 76 | margin: 5px; 77 | font-weight: bold; 78 | } 79 | 80 | .operator-or-metric { 81 | font-family: Arial, Helvetica, sans-serif; 82 | color: $darkerGray; 83 | } 84 | 85 | .data-filters { 86 | display: flex; 87 | flex-direction: row; 88 | max-width: 100%; 89 | flex-wrap: wrap; 90 | justify-content: center; 91 | } 92 | 93 | .individual-drop-down { 94 | display: flex; 95 | flex-direction: column; 96 | margin: 10px; 97 | } 98 | 99 | .drop-down-label { 100 | font-family: Arial, Helvetica, sans-serif; 101 | color: $darkerGray; 102 | font-weight: bold; 103 | } 104 | 105 | .drop-down { 106 | background: $mainBlack; 107 | border: 0.25pt $darkestGray solid; 108 | outline: 0px; 109 | color: $darkerGray; 110 | } 111 | 112 | #save-chart-setup { 113 | margin-right: 15px; 114 | margin-top: 25px; 115 | margin-bottom: 25px; 116 | font-family: Arial, Helvetica, sans-serif; 117 | font-weight: bold; 118 | border: none; 119 | border-radius: 2px; 120 | width: 100px; 121 | height: 30px; 122 | color: white; 123 | background-color: $mainBlue; 124 | } 125 | 126 | #save-chart-setup:hover { 127 | background-color: $darkerBlue; 128 | } 129 | 130 | #close-chart-setup { 131 | margin-right: 15px; 132 | margin-top: 25px; 133 | margin-bottom: 25px; 134 | font-family: Arial, Helvetica, sans-serif; 135 | font-weight: bold; 136 | border: none; 137 | border-radius: 2px; 138 | width: 100px; 139 | height: 30px; 140 | background-color: $darkerGray; 141 | color: white; 142 | } 143 | 144 | #close-chart-setup:hover { 145 | background-color: $darkestGray; 146 | } 147 | 148 | .charts-container { 149 | display: flex; 150 | flex-direction: row; 151 | flex-wrap: wrap; 152 | } 153 | 154 | .individual-chart-container { 155 | width: 500px; 156 | flex: 1 1 500px; 157 | margin-top: 10px; 158 | margin-bottom: 10px; 159 | margin-left: 10px; 160 | margin-right: 10px; 161 | padding: 10px; 162 | // background-color: $mainGray; 163 | box-shadow: 5px 5px 5px; 164 | border: 1pt solid $darkerGray; 165 | } 166 | 167 | .chart-name { 168 | margin-bottom: 10px; 169 | margin-top: 10px; 170 | font-family: Arial, Helvetica, sans-serif; 171 | color: $mainGray; 172 | font-weight: bold; 173 | text-align: center; 174 | } 175 | 176 | #edit-chart { 177 | background-color: $mainBlue; 178 | margin: 15px; 179 | font-family: Arial, Helvetica, sans-serif; 180 | font-weight: bold; 181 | border: none; 182 | border-radius: 2px; 183 | width: 100px; 184 | height: 30px; 185 | color: white; 186 | } 187 | 188 | #edit-chart:hover { 189 | background-color: $darkerBlue; 190 | } 191 | 192 | #delete-chart { 193 | background-color: $mainRed; 194 | margin-bottom: 15px; 195 | margin-top: 15px; 196 | font-family: Arial, Helvetica, sans-serif; 197 | font-weight: bold; 198 | border: none; 199 | border-radius: 2px; 200 | width: 100px; 201 | height: 30px; 202 | color: white; 203 | } 204 | 205 | #delete-chart:hover { 206 | background-color: $darkerRed; 207 | } 208 | 209 | .single-number-display { 210 | height: 83%; 211 | display: flex; 212 | flex-direction: row; 213 | justify-content: center; 214 | align-items: center; 215 | } 216 | 217 | .single-number-data { 218 | font-family: Arial, Helvetica, sans-serif; 219 | font-size: 50pt; 220 | color: $chartBlue; 221 | } -------------------------------------------------------------------------------- /client/components/dashboard/IndividualChartContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TimeSeriesChart from './TimeSeriesChart'; 3 | import DonutChart from './DonutChart'; 4 | import SingleNumberDisplay from './SingleNumberDisplay'; 5 | import history from './dashboardHistory'; 6 | 7 | const IndividualChartContainer = ({ 8 | format, 9 | id, 10 | setAllCharts, 11 | setColumns, 12 | chartName, 13 | setChartName, 14 | chart, 15 | setChart, 16 | setFilters, 17 | prometheusInstance, 18 | setPrometheusInstance, 19 | }) => { 20 | /* 21 | handles click on edit button: 22 | retrieves chart name, data selector columns, and filters for 23 | particular chart from database to display on chart set up page 24 | 25 | chart type displayed on chart set up page depenent on 26 | format property passed down from main charts container 27 | */ 28 | const editChart = async () => { 29 | await fetch(`/dashboard/editChart/${chartName}`) 30 | .then((response) => response.json()) 31 | .then((response) => { 32 | console.log('inside fetch', response.columns); 33 | setColumns(response.columns); 34 | setChartName(response.name); 35 | setFilters(response.filters); 36 | const chartToEdit = []; 37 | if (format === 'time-series') { 38 | chartToEdit.push( 39 | , 48 | ); 49 | } else if (format === 'donut') { 50 | chartToEdit.push( 51 | , 60 | ); 61 | } else if (format === 'single-number') { 62 | chartToEdit.push( 63 | , 72 | ); 73 | } 74 | setChart(chartToEdit); 75 | }); 76 | history.push('/dashboard/edit-chart'); 77 | }; 78 | 79 | /* 80 | handles click on delete button: 81 | deletes all information for particular chart from database, 82 | updates all charts accordingly to display on main dashboard page 83 | */ 84 | const deleteChart = async () => { 85 | await fetch(`/dashboard/deleteChart/${prometheusInstance.name}/${chartName}`, { 86 | method: 'DELETE', 87 | }) 88 | .then((response) => response.json()) 89 | .then((response) => { 90 | console.log('individual container', response); 91 | setAllCharts(response); 92 | }); 93 | }; 94 | 95 | /* 96 | displays individual chart on main dashboard page 97 | 98 | individual chart displayed on main dashboard page dependent 99 | on format property passed down from main charts container 100 | */ 101 | const chartToDisplay = []; 102 | if (format === 'time-series') { 103 | chartToDisplay.push( 104 | , 113 | ); 114 | } else if (format === 'donut') { 115 | chartToDisplay.push( 116 | , 125 | ); 126 | } else if (format === 'single-number') { 127 | chartToDisplay.push( 128 | , 137 | ); 138 | } 139 | 140 | return ( 141 |
142 |
{chartName}
143 | {chartToDisplay} 144 |
145 | 146 |
147 |
148 | ); 149 | }; 150 | 151 | export default IndividualChartContainer; 152 | -------------------------------------------------------------------------------- /client/components/dashboard/DashboardContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Router, Switch, Route } from 'react-router-dom'; 3 | import ChartSetup from './ChartSetup'; 4 | import ChartsContainer from './ChartsContainer'; 5 | import history from './dashboardHistory'; 6 | 7 | const DashboardContainer = ({ 8 | allCharts, 9 | setAllCharts, 10 | prometheusInstance, 11 | setPrometheusInstance, 12 | }) => { 13 | /* 14 | helper function to initialize state of data selector 15 | columns for chart setup page 16 | */ 17 | const initialColumns = (metricsList) => ({ 18 | aggregationOptions: { 19 | name: 'aggregationOptions', 20 | title: 'Aggregation Options', 21 | list: ['Sum', 'Avg', 'Multiply', 'Divide', 'Min', 'Max', 'Rate'], 22 | }, 23 | aggregationSelected: { 24 | name: 'aggregationSelected', 25 | title: 'Aggregation Selected', 26 | list: [], 27 | }, 28 | metricsOptions: { 29 | name: 'metricsOptions', 30 | title: 'Metrics Options', 31 | list: metricsList, 32 | }, 33 | metricsSelected: { 34 | name: 'metricsSelected', 35 | title: 'Metrics Selected', 36 | list: [], 37 | }, 38 | timeRange: { 39 | name: 'timeRange', 40 | title: 'Time Range', 41 | list: ['12 Hours', '3 Hours', '1 Hour', '30 Minutes', '15 Minutes', '5 Minutes', '1 Minute', '10 Seconds', '1 Second'], 42 | }, 43 | timeRangeSelected: { 44 | name: 'timeRangeSelected', 45 | title: 'Time Range Selected', 46 | list: ['6 Hours'], 47 | }, 48 | }); 49 | 50 | /* 51 | initializes state of data selector columns, chart name, 52 | chart display, and filters for chart setup page 53 | */ 54 | const [columns, setColumns] = useState(() => initialColumns([])); 55 | const [chartName, setChartName] = useState(() => ''); 56 | const [chart, setChart] = useState(() => []); 57 | const [filters, setFilters] = useState(() => {}); 58 | 59 | /* 60 | retrieves all metrics being tracked by Prometheus that are of gauge 61 | or counter data types to list on chart setup page 62 | */ 63 | const getAllPromMetrics = async () => { 64 | let metrics; 65 | if (prometheusInstance !== undefined) { 66 | await fetch(`http://${prometheusInstance.ipAddress}:${prometheusInstance.port}/api/v1/metadata`) 67 | .then((response) => response.json()) 68 | .then((response) => { 69 | const detailedMetrics = response.data; 70 | metrics = Object.keys(detailedMetrics).filter((metric) => { 71 | return detailedMetrics[metric][0].type === 'gauge' || detailedMetrics[metric][0].type === 'counter'; 72 | }); 73 | setColumns(initialColumns(metrics)); 74 | }); 75 | } 76 | }; 77 | 78 | /* 79 | retrieves all labels from Prometheus to list on chart setup page 80 | */ 81 | const getPrometheusLabels = async () => { 82 | const labels = {}; 83 | if (prometheusInstance !== undefined) { 84 | await fetch(`http://${prometheusInstance.ipAddress}:${prometheusInstance.port}/api/v1/labels`) 85 | .then((response) => response.json()) 86 | .then((response) => { 87 | response.data.forEach((label) => labels[label] = ''); 88 | setFilters(labels); 89 | }); 90 | } 91 | }; 92 | 93 | /* 94 | handles click on new dashboard chart button: 95 | resets filters, retrieves metrics from Prometheus, retrieves labels from Prometheus 96 | resets chart name, resets chart display for chart set up page, and routes to chart 97 | setup page 98 | */ 99 | const newDashboardChart = () => { 100 | setFilters({}); 101 | getAllPromMetrics(); 102 | getPrometheusLabels(); 103 | setChartName(''); 104 | setChart([]); 105 | history.push('/dashboard/new-chart'); 106 | }; 107 | 108 | return ( 109 |
110 | 117 | 118 | 119 | 120 | 135 | 136 | 137 | 152 | 153 | 154 | 169 | 170 | 171 | 172 |
173 | ); 174 | }; 175 | 176 | export default DashboardContainer; 177 | -------------------------------------------------------------------------------- /client/components/settings/AddEditCard.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useParams, useHistory } from 'react-router-dom'; 3 | import { nameHelper, ipHelper, portHelper } from './settingsHelper'; 4 | 5 | const AddEditCard = ({ settingsArr, setSettingsArr, setPrometheusConnections }) => { 6 | const { name } = useParams(); 7 | const history = useHistory(); 8 | const [thisSetting, setThisSetting] = useState(() => { 9 | if (name !== 'new') { 10 | for (const el of settingsArr) { 11 | if (el.name === name) return el; 12 | } 13 | } 14 | return { 15 | name: '', 16 | ipAddress: '', 17 | port: 3000, 18 | }; 19 | }); 20 | const [errMsgNew, setErrMsgNew] = useState(() => false); 21 | const [errMsgIP, setErrMsgIP] = useState(() => false); 22 | const [errMsgPort, setErrMsgPort] = useState(() => false); 23 | 24 | async function handleSumbit(e) { 25 | e.preventDefault(); 26 | const nameErr = nameHelper(thisSetting.name); 27 | const ipErr = ipHelper(thisSetting.ipAddress); 28 | const portErr = portHelper(thisSetting.port); 29 | if (nameErr || ipErr || portErr) { 30 | if (nameErr) setErrMsgNew(true); 31 | if (ipErr) setErrMsgIP(true); 32 | if (portErr) setErrMsgPort(true); 33 | return; 34 | } else { 35 | if (name === 'new') { 36 | const connectionNames = []; 37 | await fetch('/settings/new', { 38 | method: 'POST', 39 | headers: { 40 | 'Content-Type': 'application/json', 41 | }, 42 | body: JSON.stringify(thisSetting), 43 | }) 44 | .then((res) => res.json()) 45 | .then((result) => { 46 | result.settings.forEach((connection) => { 47 | connectionNames.push( 48 | , 49 | ); 50 | }); 51 | setPrometheusConnections(connectionNames); 52 | setSettingsArr(result.settings); 53 | }) 54 | .catch((e) => console.log(e)); 55 | } else { 56 | const connectionNames = []; 57 | await fetch(`/settings/${name}`, { 58 | method: 'PUT', 59 | headers: { 60 | 'Content-Type': 'application/json', 61 | }, 62 | body: JSON.stringify(thisSetting), 63 | }) 64 | .then((res) => res.json()) 65 | .then((result) => { 66 | // const newArray = settingsArr.filter((el) => el.name !== name); 67 | result.settings.forEach((connection) => { 68 | connectionNames.push( 69 | , 70 | ); 71 | }); 72 | setPrometheusConnections(connectionNames); 73 | setSettingsArr(result.settings); 74 | }) 75 | .catch((e) => console.log(e)); 76 | } 77 | history.push('/settings'); 78 | return; 79 | } 80 | } 81 | 82 | function handleChange(e) { 83 | let updatedCluster = { ...thisSetting }; 84 | if (e.target.id === 'name') { 85 | updatedCluster = { ...updatedCluster, name: e.target.value }; 86 | } else if (e.target.id === 'ipaddress') { 87 | updatedCluster = { ...updatedCluster, ipAddress: e.target.value }; 88 | } else if (e.target.id === 'port') { 89 | updatedCluster = { ...updatedCluster, port: e.target.value }; 90 | } 91 | setThisSetting(updatedCluster); 92 | return; 93 | } 94 | 95 | function handleDelete(e) { 96 | const connectionNames = []; 97 | fetch(`/settings/${name}/delete`, { 98 | method: 'DELETE', 99 | headers: { 100 | 'Content-Type': 'application/json', 101 | }, 102 | body: JSON.stringify(thisSetting), 103 | }) 104 | .then((res) => res.json()) 105 | .then((result) => { 106 | console.log("addedit", result.settings); 107 | result.settings.forEach((connection) => { 108 | connectionNames.push( 109 | , 110 | ); 111 | }); 112 | setPrometheusConnections(connectionNames); 113 | setSettingsArr(result.settings); 114 | history.push('/settings'); 115 | }) 116 | .catch((e) => console.log(e)); 117 | } 118 | 119 | return ( 120 |
121 |

Add/Edit Prometheus Instance

122 |
handleSumbit(e)}> 123 |
124 | 125 | handleChange(e)} 133 | > 134 |
135 |
136 | 137 | handleChange(e)} 145 | > 146 |
147 |
148 | 149 | handleChange(e)} 157 | > 158 |
159 |
160 | 161 | 162 |
163 |
164 | {errMsgNew ? ( 165 |

Please make sure the name for the server is not 'new'

166 | ) : null} 167 | {errMsgIP ? ( 168 |

Please make sure your IP is in the correct format

169 | ) : null} 170 | {errMsgPort ?

Please make sure the port is valid

: null} 171 |
172 | ); 173 | }; 174 | 175 | export default AddEditCard; 176 | -------------------------------------------------------------------------------- /server/controllers/dashboardController.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* eslint-disable no-shadow */ 3 | const { ChartSetting, Data } = require('../models/webModel'); 4 | const { Display } = require('../models/webModel'); 5 | 6 | const dashboardController = {}; 7 | 8 | /* 9 | gets chart settings for a particular chart name 10 | */ 11 | dashboardController.getChartSetting = (req, res, next) => { 12 | ChartSetting.findOne({ name: req.params.name }, (err, data) => { 13 | if (err) { 14 | return next({ status: 500, log: 'There was an error', message: err.message }); 15 | } 16 | res.locals.chartSetting = data; 17 | return next(); 18 | }); 19 | }; 20 | 21 | /* 22 | confirms whether or not chart name already exists 23 | */ 24 | dashboardController.findAndComfirm = (req, res, next) => { 25 | if (res.locals.data === null) { 26 | res.locals.chartExists = { found: false }; 27 | } else res.locals.chartExists = { found: true }; 28 | return next(); 29 | }; 30 | 31 | /* 32 | gets display for a particular Prometheus instance 33 | */ 34 | dashboardController.getAllDisplay = (req, res, next) => { 35 | Display.find({ instance: req.params.instance }, (err, data) => { 36 | if (err) { 37 | return next({ status: 500, log: 'There was an error', message: err.message }); 38 | } 39 | res.locals.data = data; 40 | return next(); 41 | }); 42 | }; 43 | 44 | /* 45 | updates display for a particular Prometheus instance if one exists 46 | if not, creates a new document for the Prometheus instance 47 | */ 48 | dashboardController.updateAllDisplay = async (req, res, next) => { 49 | if (req.body.display && Array.isArray(req.body.display)) { 50 | await Display.findOneAndUpdate( 51 | { instance: req.params.instance }, 52 | { display: req.body.display }, 53 | (err, display) => { 54 | if (!display) { 55 | Display.create( 56 | { instance: req.params.instance, display: req.body.display }, 57 | (err, result) => { 58 | if (err) { 59 | return res.status(500).send({ success: false }); 60 | } 61 | return next(); 62 | }, 63 | ); 64 | } else if (err) { 65 | return res.status(500).send({ success: false }); 66 | } else { 67 | return next(); 68 | } 69 | }, 70 | ); 71 | } else { 72 | return res.status(500).send({ success: false }); 73 | } 74 | }; 75 | 76 | /* 77 | creates a new document for chart setting for a new chart 78 | */ 79 | dashboardController.createChartSetting = (req, res, next) => { 80 | console.log(req.body); 81 | ChartSetting.create( 82 | { 83 | name: req.params.name, 84 | columns: req.body.columns, 85 | filters: req.body.filters, 86 | }, 87 | (err, result) => { 88 | if (err) { 89 | console.log(`There was an error: ${err}`); 90 | res.status(500).send({ success: false }); 91 | return; 92 | } 93 | console.log(`Created: ${result}`); 94 | return next(); 95 | }, 96 | ); 97 | }; 98 | 99 | /* 100 | checks whether or not a previous query in a previous middleware 101 | had results 102 | */ 103 | dashboardController.checkIfChartExists = (req, res, next) => { 104 | if (res.locals.chartSetting !== null) { 105 | res.locals.chartExists = { found: true }; 106 | } else { 107 | res.locals.chartExists = { found: false }; 108 | } 109 | return next(); 110 | }; 111 | 112 | /* 113 | deletes chart setting for a particular chart name 114 | */ 115 | dashboardController.deleteChartSetting = (req, res, next) => { 116 | ChartSetting.findOneAndDelete({ name: req.params.name }, (err, data) => { 117 | if (err) { 118 | return next({ status: 500, log: 'There was an error', message: err.message }); 119 | } 120 | return next(); 121 | }); 122 | }; 123 | 124 | /* 125 | deletes single display element from a document for a particular 126 | Prometheus instance 127 | */ 128 | dashboardController.deleteSingleDisplay = (req, res, next) => { 129 | const allDisplay = res.locals.data[0].display; 130 | const updatedDisplays = allDisplay.slice(); 131 | for (let index = 0; index < allDisplay.length; index += 1) { 132 | const currentDisplay = allDisplay[index]; 133 | if (currentDisplay[0].props.id === req.params.name) { 134 | updatedDisplays.splice(index, 1); 135 | break; 136 | } 137 | } 138 | res.locals.data = updatedDisplays; 139 | Display.findOneAndUpdate( 140 | { instance: req.params.instance }, 141 | { display: updatedDisplays }, 142 | (err, result) => { 143 | if (err) { 144 | return next({ status: 500, log: 'There was an error', message: err.message }); 145 | } 146 | return next(); 147 | }, 148 | ); 149 | }; 150 | 151 | /* 152 | updates chart setting for a particular chart name 153 | */ 154 | dashboardController.updateChartSetting = (req, res, next) => { 155 | ChartSetting.findOneAndUpdate( 156 | { name: req.params.name }, 157 | { columns: req.body.columns, name: req.body.name, filters: req.body.filters }, 158 | (err, result) => { 159 | if (err) { 160 | return next({ status: 500, log: 'There was an error', message: err.message }); 161 | } 162 | return next(); 163 | }, 164 | ); 165 | }; 166 | 167 | /* 168 | updates single display element from a document for a particular 169 | Prometheus instance 170 | */ 171 | dashboardController.updateSingleDisplay = (req, res, next) => { 172 | const updatedDisplays = []; 173 | Display.find({ instance: req.body.instance }, (err, result) => { 174 | if (err) { 175 | return next({ status: 500, log: 'There was an error', message: err.message }); 176 | } 177 | result[0].display.forEach((chart) => { 178 | updatedDisplays.push(chart); 179 | }); 180 | for (let index = 0; index < updatedDisplays.length; index += 1) { 181 | const currentDisplay = updatedDisplays[index]; 182 | if (currentDisplay[0].props.id === req.params.name) { 183 | updatedDisplays.splice(index, 1, req.body.updatedChart); 184 | break; 185 | } 186 | } 187 | Display.findOneAndUpdate( 188 | { instance: req.body.instance }, 189 | { display: updatedDisplays }, 190 | (err, result) => { 191 | if (err) { 192 | return next({ status: 500, log: 'There was an error', message: err.message }); 193 | } 194 | return next(); 195 | }, 196 | ); 197 | }); 198 | }; 199 | 200 | /* 201 | gets info for all Prometheus instances 202 | */ 203 | dashboardController.getAllPrometheusInstances = (req, res, next) => { 204 | Data.find({}, (err, result) => { 205 | if (err) { 206 | return next({ status: 500, log: 'There was an error', message: err.message }); 207 | } 208 | res.locals.allPrometheusInstances = result; 209 | return next(); 210 | }); 211 | }; 212 | 213 | /* 214 | gets info for a particular Prometheus instance name 215 | */ 216 | dashboardController.getPrometheusInstance = (req, res, next) => { 217 | Data.findOne({ name: req.params.name }, (err, result) => { 218 | if (err) { 219 | return next({ status: 500, log: 'There was an error', message: err.message }); 220 | } 221 | console.log('main app prom instance', result); 222 | res.locals.prometheusInstance = result; 223 | return next(); 224 | }); 225 | }; 226 | 227 | module.exports = dashboardController; 228 | -------------------------------------------------------------------------------- /client/components/dashboard/ChartSetup.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { DragDropContext } from 'react-beautiful-dnd'; 3 | import OptionsOrSelectedColumn from './OptionsOrSelectedColumn'; 4 | import TimeSeriesChart from './TimeSeriesChart'; 5 | import DonutChart from './DonutChart'; 6 | import SingleNumberDisplay from './SingleNumberDisplay'; 7 | import DataFilters from './DataFilters'; 8 | import history from './dashboardHistory'; 9 | 10 | const ChartSetup = ({ 11 | id, 12 | allCharts, 13 | setAllCharts, 14 | columns, 15 | setColumns, 16 | chartName, 17 | setChartName, 18 | chart, 19 | setChart, 20 | filters, 21 | setFilters, 22 | prometheusInstance, 23 | setPrometheusInstance, 24 | }) => { 25 | /* 26 | initializes state of notification for if a chart name already exists and old chart name 27 | */ 28 | const [alreadyExistsNotification, setNotification] = useState(() => ''); 29 | // eslint-disable-next-line no-unused-vars 30 | const [oldChartName, setOldChartName] = useState(() => chartName); 31 | 32 | /* 33 | handles after an item has been dragged and dropped: 34 | updates data selector columns in order to display correctly reflecting where 35 | items have been dragged and dropped 36 | */ 37 | const onDragEnd = ({ source, destination }) => { 38 | setNotification(''); 39 | if (destination === undefined || destination === null) return; 40 | 41 | if (source.droppableId === destination.droppableId && destination.index === source.index) { 42 | return; 43 | } 44 | const start = columns[source.droppableId]; 45 | const end = columns[destination.droppableId]; 46 | 47 | if (start === end) { 48 | const updatedList = start.list.filter((item, index) => index !== source.index); 49 | updatedList.splice(destination.index, 0, start.list[source.index]); 50 | const updatedColumn = { 51 | ...start, 52 | list: updatedList, 53 | }; 54 | const updatedState = { 55 | ...columns, 56 | [updatedColumn.name]: updatedColumn, 57 | }; 58 | setColumns(updatedState); 59 | } else { 60 | const updatedStartList = start.list.filter((item, index) => index !== source.index); 61 | const updatedStartColumn = { 62 | ...start, 63 | list: updatedStartList, 64 | }; 65 | const updatedEndList = end.list; 66 | updatedEndList.splice(destination.index, 0, start.list[source.index]); 67 | const updatedEndColumn = { 68 | ...end, 69 | list: updatedEndList, 70 | }; 71 | const updatedState = { 72 | ...columns, 73 | [updatedStartColumn.name]: updatedStartColumn, 74 | [updatedEndColumn.name]: updatedEndColumn, 75 | }; 76 | setColumns(updatedState); 77 | } 78 | }; 79 | 80 | /* 81 | handles changes to chart name input: 82 | updates chart name and resets notification for if a chart name already exists 83 | */ 84 | const changeChartName = (event) => { 85 | setChartName(event.target.value); 86 | setNotification(''); 87 | }; 88 | 89 | /* 90 | copy of current state of filters to alter 91 | */ 92 | const updatedFilters = { ...filters }; 93 | /* 94 | handles change on filter drop downs: 95 | updates particular property in updated filters object with 96 | new selection, sets filters to updated filters 97 | */ 98 | const changeFilter = (event) => { 99 | updatedFilters[event.target.id] = event.target.value; 100 | setFilters(updatedFilters); 101 | }; 102 | 103 | /* 104 | handles click on save chart setup button: 105 | if new chart, checks if chart name already exists in database and notifies if so 106 | and if not, adds chart name, data selector columns, and filters to database, adds new chart 107 | to display on chart setup page and main dashboard page 108 | 109 | if edit chart, update chart name, data selector columns, and filters in database, updates chart 110 | to display on chart setup page and main dashboard page 111 | 112 | chart type displayed dependent upon aggregation selected, data selected, and time selected 113 | have notifications set up if aggregation selected, data selected, and/or time selected is not 114 | currently being handled by application 115 | */ 116 | const saveChartSetup = async () => { 117 | if (id === 'new-chart') { 118 | if (columns.metricsSelected.list.length !== 0 && chartName !== '') { 119 | let chartAlreadyExists; 120 | await fetch(`/dashboard/chart/${chartName}`) 121 | .then((response) => response.json()) 122 | .then((response) => { 123 | chartAlreadyExists = response.found; 124 | }); 125 | 126 | if (!chartAlreadyExists) { 127 | let newChart; 128 | const aggregation = columns.aggregationSelected.list; 129 | const metric = columns.metricsSelected.list; 130 | const time = columns.timeRangeSelected.list; 131 | if ( 132 | time.length !== 0 133 | && !aggregation.includes('Divide') 134 | && !aggregation.includes('Multiply') 135 | ) { 136 | newChart = [ 137 | , 146 | ]; 147 | } else if ( 148 | time.length === 0 149 | && aggregation.length === 1 150 | && aggregation[0] === 'Divide' 151 | && metric.length === 2 152 | ) { 153 | newChart = [ 154 | , 163 | ]; 164 | } else if ( 165 | time.length === 0 166 | && aggregation.length >= 0 167 | && metric.length === 1 168 | ) { 169 | newChart = [ 170 | , 179 | ]; 180 | } else { 181 | newChart = [ 182 |
183 | Data visualization and PromQL translation not yet available, but are currently 184 | in development. Apologies for the inconvenience. Please try something else. 185 | Thank you for your patience. 186 |
, 187 | ]; 188 | } 189 | 190 | setChart(newChart); 191 | 192 | if (newChart[0].type !== 'div') { 193 | const updatedAllCharts = allCharts.slice(); 194 | updatedAllCharts.push(newChart); 195 | 196 | setAllCharts(updatedAllCharts); 197 | 198 | await fetch(`/dashboard/newChart/${chartName}`, { 199 | method: 'POST', 200 | headers: { 201 | 'Content-Type': 'application/json', 202 | }, 203 | body: JSON.stringify({ columns, filters: updatedFilters }), 204 | }) 205 | .then((response) => response.json()) 206 | .then((response) => console.log(response, 'adding new chart successful')) 207 | .catch((error) => console.log(error, 'adding new chart failed')); 208 | 209 | await fetch(`/dashboard/allCharts/${prometheusInstance.name}`, { 210 | method: 'PATCH', 211 | headers: { 212 | 'Content-Type': 'application/json', 213 | }, 214 | body: JSON.stringify({ display: updatedAllCharts }), 215 | }) 216 | .then((response) => response.json()) 217 | .then((response) => console.log(response, 'update all charts successful')) 218 | .catch((error) => console.log(error, 'updating all charts failed')); 219 | } 220 | } else { 221 | setNotification('Chart already exists. Please enter another name.'); 222 | } 223 | } else if (columns.metricsSelected.list.length === 0 && chartName === '') { 224 | setNotification('Please enter a chart name and select a metric.'); 225 | } else if (columns.metricsSelected.list.length === 0) { 226 | setNotification('Please select a metric.'); 227 | } else if (chartName === '') { 228 | setNotification('Please enter a chart name.'); 229 | } 230 | } else if (id === 'edit-chart') { 231 | let updatedChart; 232 | if (chart[0].props.format === 'time-series') { 233 | updatedChart = ; 242 | } else if (chart[0].props.format === 'donut') { 243 | updatedChart = ; 252 | } else if (chart[0].props.format === 'single-number') { 253 | updatedChart = ; 262 | } 263 | 264 | setChart(updatedChart); 265 | 266 | for (let index = 0; index < allCharts.length; index += 1) { 267 | const currentChart = allCharts[index]; 268 | if (currentChart[0].props.id === oldChartName) { 269 | allCharts.splice(index, 1, [updatedChart]); 270 | setAllCharts(allCharts); 271 | break; 272 | } 273 | } 274 | 275 | await fetch(`/dashboard/editChart/${oldChartName}`, { 276 | method: 'PATCH', 277 | headers: { 278 | 'Content-Type': 'application/json', 279 | }, 280 | body: JSON.stringify({ 281 | name: chartName, 282 | columns, 283 | updatedChart: [updatedChart], 284 | filters: updatedFilters, 285 | instance: prometheusInstance.name, 286 | }), 287 | }) 288 | .then((response) => response.json()) 289 | .then((response) => console.log(response, 'editing chart successful')) 290 | .catch((error) => console.log(error, 'editing chart failed')); 291 | } 292 | }; 293 | 294 | return ( 295 |
296 |

Add/Edit Dashboard Chart

297 |
298 |
299 | 300 |
301 |
{alreadyExistsNotification}
302 | 303 |
304 | {Object.values(columns).map((column, index) => ( 305 | 311 | ))} 312 |
313 |
314 | 321 | 322 | {chart} 323 |
324 |
325 | ); 326 | }; 327 | 328 | export default ChartSetup; 329 | --------------------------------------------------------------------------------