├── .eslintrc.json ├── .gitignore ├── Dockerfile ├── README.md ├── jest.config.js ├── library └── redis.ts ├── next.config.js ├── package-lock.json ├── package.json ├── src ├── __tests__ │ ├── DropDown.test.js │ └── MetricBox.test.js ├── components │ ├── AlertModal.tsx │ ├── Alerts.tsx │ ├── BarChart.tsx │ ├── DropDown.tsx │ ├── DynamicLineGraph.tsx │ ├── DynamicMetricBox.tsx │ ├── LineGraph.tsx │ ├── Main.tsx │ ├── MetricBox.tsx │ ├── MetricContainer.tsx │ ├── NavBar.tsx │ ├── ScatterChart.tsx │ ├── Sidebar.tsx │ ├── TopRow.tsx │ ├── sidebarFunctions │ │ ├── deleteUserEndpoint.ts │ │ └── storeUserEndpoint.ts │ └── styles │ │ ├── Charts.module.scss │ │ ├── DropDown.module.scss │ │ ├── Landing.module.scss │ │ ├── Main.module.scss │ │ ├── MetricBox.module.scss │ │ ├── Modal.module.scss │ │ ├── NavBar.module.scss │ │ ├── SideBar.module.scss │ │ └── TopRow.module.scss ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ ├── auth │ │ │ └── [...auth0].ts │ │ ├── connect.ts │ │ ├── controllers │ │ │ ├── createUser.ts │ │ │ └── userEndpoints.ts │ │ ├── database │ │ │ └── userModel.ts │ │ ├── disconnect.ts │ │ ├── latency.ts │ │ └── retrieveMetrics.ts │ ├── index.tsx │ └── monitoring.tsx └── public │ ├── alan.png │ ├── blog.png │ ├── cloudband.png │ ├── denogres.png │ ├── docketeer.png │ ├── dockwell.png │ ├── elvin.png │ ├── favicon.ico │ ├── github.png │ ├── githubLogo.png │ ├── globals.css │ ├── linkedinLogo.png │ ├── login.png │ ├── logo.png │ ├── logout.png │ ├── luke.png │ ├── monitoring.png │ ├── orcastration.png │ ├── qeraunos.png │ ├── sakura.png │ └── vsbranch.png ├── tsconfig.json └── types └── types.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | WORKDIR /app 3 | COPY . . 4 | RUN npm install && npm run build 5 | CMD ["npm", "run", "start"] 6 | EXPOSE 3000 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Redline Documentation 4 | 5 | _Website and Dev-tool: [Redline](https://redlinemetrics.com)_ 6 | 7 | ## Overview 8 | 9 | Redline is an open-source tool that displays vital metrics for Redis instances. Redline is easy to set up, free to use, and alerts developers to performance issues, so that they can avoid constantly monitoring their applications Redis instances. Redline monitors: 10 | 11 | - Used memory 12 | - Key space hits and misses 13 | - Instantaneous ops per second 14 | - Commands processed 15 | - Connected clients 16 | - Evicted and expired keys 17 | - _and many more_ 18 | 19 | ## Core Features 20 | 21 | - Customizable Metrics: Customize which metrics you want to have displayed in real-time. 22 | - Alerts: Set up alerts for individual metrics to be notified when a metric dips below or exceeds a specified value. 23 | - Login: Use your Google or Github login to quickly and securely access Redline and save your Redis instances. 24 | - Easy Set Up: Simply enter your Redis instance host, port, and password, give it a nickname, and click to connect. 25 | - Free: Redline removes a financial barrier to entry, making it accessible to all developers. 26 | 27 | ## Getting Started 28 | 29 | ### How do you use Redline for applications in production? 30 | 31 | Navigate to [Redline](https://redlinemetrics.com) and set up a user account or click straight through to our monitoring tool. _Users who opt to sign up using Auth0 are able to save their Redis instances_. 32 | 33 | getting started 34 | 35 | Go to your Redis cloud console and copy your host, port and password from your configuration settings. _Your port can be found after the colon in your endpoint_. 36 | 37 | redis instance 38 | 39 | Add your Redis instance’s host, port, and password in the sidebar, and give it a unique nickname before you add it to your collection. Click on the nickname to display your metrics. 40 | 41 | sidebar 42 | 43 | Redline will spin up the graphs for the performance metrics associated with the selected cache, and you can interact with our dynamic graphs to choose which metrics you would like to monitor. 44 | 45 | Click the alert icon at the bottom of the chart you’d like to monitor, and enter the threshold value that will trigger the alert. 46 | 47 | alert gif 48 | 49 | ### How do you use Redline for applications in development? 50 | 51 | There are two ways to use the application for development purposes. The first method: 52 | 53 | 1. First, clone the repository from Github . 54 | 2. Run ```docker build -t redline .``` in your terminal 55 | 3. Run ```docker run -dp 3001:3000 redline``` in your terminal 56 | 4. Navigate to [localhost:3001](http://localhost:3001). You should see the web application and enter your host, port, password (default is empty) and nickname. 57 | 58 | Or, simply pull the image from Docker: 59 | 60 | 1. Navigate to [Docker Hub](https://hub.docker.com/r/sakurakiyama/redline) and pull the image using the command ```docker pull sakurakiyama/redline``` 61 | 2. Run ```docker run -dp 3001:3000 sakurakiyama/redline``` in your terminal 62 | 3. Navigate to [localhost:3001](http://localhost:3001). You should see the web application and enter your host, port, password (default is empty) and nickname. 63 | 64 | *If you're having any trouble, please refer to the images in the section above.* 65 | 66 | ## Tech Stack 67 | 68 | Next.js | Typescript | React.js | PostgreSQL | Auth0 | Chart.js | SASS/CSS | Jest | Docker 69 | 70 | ## How to Contribute 71 | 72 | 1. Clone the repo and make a new branch 73 | 2. Run ```docker build -t redline .``` in your terminal 74 | 3. Run ```docker run -dp 3001:3000 redline``` in your terminal 75 | 4. Add a feature, fix a big or refactor some code 76 | 5. Write/update tests for the changes you made, if necessary 77 | 6. Run unit tests and make sure all tests pass: npm test 78 | 7. Open a Pull Request with a comprehensive description of changes to the dev branch 79 | 8. Open a Pull Request to the docs and Contributors if necessary 80 | 81 | 82 | ## Contributors 83 | 84 | **Sakura Akiyama** [Github](https://github.com/sakurakiyama) | [LinkedIn](https://www.linkedin.com/in/sakura-akiyama-bowden/) 85 | 86 | **Luke Driscoll** [Github](https://github.com/LukeDriscoll4) | [LinkedIn](https://www.linkedin.com/in/luke-driscoll/) 87 | 88 | **Alan Perng** [Github](https://github.com/aperng31) | [LinkedIn](https://www.linkedin.com/in/alanperng/) 89 | 90 | **Elvin Yuen** [Github](https://github.com/elvinyuen) | [LinkedIn](https://www.linkedin.com/in/elvinyuen/) 91 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // jest.config.js 2 | const nextJest = require('next/jest'); 3 | 4 | const createJestConfig = nextJest({ 5 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 6 | dir: './', 7 | }); 8 | 9 | // Add any custom config to be passed to Jest 10 | /** @type {import('jest').Config} */ 11 | const customJestConfig = { 12 | // Add more setup options before each test is run 13 | // setupFilesAfterEnv: ['/jest.setup.js'], 14 | // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work 15 | moduleDirectories: ['node_modules', '/'], 16 | testEnvironment: 'jest-environment-jsdom', 17 | }; 18 | 19 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 20 | module.exports = createJestConfig(customJestConfig); 21 | -------------------------------------------------------------------------------- /library/redis.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import Redis from 'ioredis'; 3 | import { Metrics, Units, MetricCollection, Endpoint, ServerError } from '../types/types'; 4 | let redis: Redis; 5 | let baseMet: Metrics = {}; 6 | 7 | export const connectRedis = (body: Endpoint) => { // input of host, port, password to connect Redis client 8 | // deconstruct the hostname, port and password from the frontend request body 9 | const { host, port, password, nickname } = body; 10 | redis = new Redis({ // default to 6379, but should connect when db info submitted via sidebar 11 | host, 12 | port, 13 | password, 14 | reconnectOnError: () => false, // do not attempt to reconnect if error encountered 15 | retryStrategy: () => null, // do not attempt to reconnect after failed connection 16 | lazyConnect: true, // connect manually within promise 17 | }); 18 | const myPromise = new Promise(function(resolve, reject) { 19 | redis.on('error', (err) => { // on error, set error log and message, set connection to false 20 | const newError = { log: err.code, message: { err: err.message } }; 21 | console.log('Error:', newError) 22 | reject(newError); // send rejected promise w/ error 23 | }) 24 | redis.on('connect', () => { // on successful connection, set connection to true 25 | const connection = `Connected to Redis endpoint ${nickname}` 26 | console.log(connection); 27 | resolve(connection); // send resolved promise w/ connection string 28 | }) 29 | redis.connect((e) => {// manually attempt to connect 30 | if (e) console.log(e) 31 | }) 32 | }) 33 | 34 | return myPromise; 35 | }; 36 | 37 | export const disconnectRedis = () => { 38 | // console.log(redis); 39 | resetBaseMetrics(); 40 | redis.disconnect(); 41 | } 42 | 43 | export const getMetrics = async () => { // depending on 'current' db (hold in state), fetch metrics from that db 44 | // Grab redis data metrics 45 | const data = await redis.info(); 46 | // Returned data string from Redis to be cleaned up and passed into an array. The data in the array should be a string. 47 | const redisinfo = require('redis-info').parse(data); 48 | 49 | const rawData: Metrics = {}; 50 | 51 | for (const key in Units) { 52 | if(key === 'keys' || key === 'expires' || key === 'avg_ttl') { // if key is database (val is nested obj) 53 | if (Object.keys(redisinfo['databases']).length > 0) { // database = { '0': { 'keys': keys, 'expires': expires, 'avg_ttl': avg_ttl } } 54 | rawData[key] = redisinfo['databases']['0'][key]; 55 | } 56 | else { 57 | rawData[key] = 0; 58 | } 59 | } 60 | else if (key in redisinfo) { // normal key metric found in redisinfo 61 | rawData[key] = +redisinfo[key]; 62 | } 63 | } 64 | if (Object.keys(baseMet).length === 0) { 65 | baseMet = {...rawData}; 66 | } 67 | calcSessionMetrics(rawData); 68 | // console.log(rawData); 69 | return rawData; 70 | }; 71 | 72 | const resetBaseMetrics = () => { // reset base metrics compared against 73 | baseMet = {}; 74 | } 75 | 76 | const calcSessionMetrics = (data: Metrics) => { // calculate current session metrics 77 | for (const key in Units) { 78 | if (!(key in data)) { // if undefined val for key in data, calculate session value 79 | if (key === 'keyspace_hitratio_session') { 80 | data[key] = data['keyspace_hits_session'] / (data['keyspace_hits_session'] + data['keyspace_misses_session']); 81 | } 82 | else { 83 | const origKey = key.slice(0,-8); 84 | data[key] = data[origKey] - baseMet[origKey]; 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redline", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "test": "jest --verbose" 11 | }, 12 | "dependencies": { 13 | "@auth0/nextjs-auth0": "^2.0.0", 14 | "@emotion/react": "^11.10.5", 15 | "@emotion/styled": "^11.10.5", 16 | "@mui/icons-material": "^5.11.0", 17 | "@next/font": "^13.1.1", 18 | "@testing-library/jest-dom": "^5.16.5", 19 | "@testing-library/react": "^13.4.0", 20 | "@types/node": "18.11.18", 21 | "@types/react": "18.0.26", 22 | "@types/react-dom": "18.0.10", 23 | "axios": "^1.2.2", 24 | "bcrypt": "^5.1.0", 25 | "chart.js": "^4.1.1", 26 | "cookie": "^0.5.0", 27 | "cookies-next": "^2.1.1", 28 | "cookies-parser": "^1.2.0", 29 | "dotenv": "^16.0.3", 30 | "eslint": "8.31.0", 31 | "eslint-config-next": "13.1.1", 32 | "ioredis": "^5.2.4", 33 | "jest-dom": "^4.0.0", 34 | "jest-environment-jsdom": "^29.3.1", 35 | "mysql": "^2.18.1", 36 | "next": "13.1.1", 37 | "pg": "^8.8.0", 38 | "react": "18.2.0", 39 | "react-chartjs-2": "^5.1.0", 40 | "react-cookie": "^4.1.1", 41 | "react-dom": "18.2.0", 42 | "react-fast-marquee": "^1.3.5", 43 | "react-icons": "^4.7.1", 44 | "react-modal": "^3.16.1", 45 | "react-router-dom": "^6.6.2", 46 | "react-toastify": "^9.1.1", 47 | "sass": "^1.57.1", 48 | "sass-loader": "^13.2.0", 49 | "supertest": "^6.3.3", 50 | "typescript": "4.9.4", 51 | "url-loader": "^4.1.1" 52 | }, 53 | "devDependencies": { 54 | "@types/uniqid": "^5.3.2", 55 | "redis-info": "^3.1.0", 56 | "uniqid": "^5.4.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/__tests__/DropDown.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '@testing-library/jest-dom/extend-expect'; 3 | import { render, screen } from '@testing-library/react'; 4 | import DropDown from '../components/DropDown'; 5 | 6 | test('renders dropdown menu', () => { 7 | const axis = 'y axis'; 8 | render( 9 | { 11 | 1 + 1; 12 | }} 13 | axis={axis} 14 | axisState={undefined} 15 | /> 16 | ); 17 | 18 | const elem = screen.getByText(`Select ${axis}`); 19 | expect(elem).toBeDefined(); 20 | expect(elem).toBeInTheDocument(); 21 | }); 22 | -------------------------------------------------------------------------------- /src/__tests__/MetricBox.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '@testing-library/jest-dom/extend-expect'; 3 | import { render, screen } from '@testing-library/react'; 4 | import MetricBox from '../components/MetricBox'; 5 | import { Units } from '../../types/types'; 6 | 7 | describe('Unit testing Metric Box', () => { 8 | const boxData = 100; 9 | const name = 'Test Metric'; 10 | const metric = 'used_memory'; 11 | beforeEach(() => { 12 | render(); 13 | }); 14 | 15 | test('Test Metric shows up in the document', () => { 16 | const text = screen.getByText(`${name}`); 17 | expect(text).toBeInTheDocument(); 18 | }); 19 | 20 | test('Data and units show up in the document', () => { 21 | const value = screen.getByText(`${boxData} ${Units[metric]}`); 22 | expect(value).toBeInTheDocument(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/components/AlertModal.tsx: -------------------------------------------------------------------------------- 1 | import { ElectricScooterOutlined } from '@mui/icons-material'; 2 | import React, { useState } from 'react'; 3 | import styles from './styles/Modal.module.scss'; 4 | 5 | type Props = { 6 | title: string; 7 | deactivateTitle: string; 8 | deactivateMessage: string; 9 | isOpened: boolean; 10 | onConfirm: () => void; 11 | onClose: () => void; 12 | setDropDownValue: React.Dispatch>; 13 | setNumber: React.Dispatch>; 14 | unit: string; 15 | onDeactivate: () => void; 16 | isActivated: boolean; 17 | dropDownValue: string; 18 | }; 19 | 20 | export default function AlertModal({ 21 | title, 22 | deactivateTitle, 23 | deactivateMessage, 24 | isOpened, 25 | onConfirm, 26 | onClose, 27 | setDropDownValue, 28 | setNumber, 29 | onDeactivate, 30 | isActivated, 31 | unit, 32 | dropDownValue, 33 | }: Props) { 34 | const [warningMessage, setWarningMessage] = useState(''); 35 | 36 | // if isOpened is false, modal is closed and should return nothing 37 | if (!isOpened) return null; 38 | 39 | // onClick handler function that confirms the alert notification and closes the modal 40 | function confirmAndClose() { 41 | if (dropDownValue === '') { 42 | setWarningMessage('Please select an option from the drop down menu.'); 43 | } else { 44 | setWarningMessage(''); 45 | onConfirm(); 46 | onClose(); 47 | } 48 | } 49 | 50 | function deactivateAndClose() { 51 | onDeactivate(); 52 | onClose(); 53 | } 54 | 55 | function onCancel() { 56 | setWarningMessage(''); 57 | onClose(); 58 | } 59 | 60 | const preventAutoClose = (e: React.MouseEvent) => e.stopPropagation(); 61 | 62 | if (isActivated) { 63 | return ( 64 |
65 |
66 |

{deactivateTitle}

67 |
68 |
{deactivateMessage}
69 |
70 |
71 | 78 | 81 |
82 |
83 |
84 |
85 | ); 86 | } 87 | 88 | return ( 89 |
90 |
91 |

{title}

92 |
93 |
94 | 104 |
105 |
106 | setNumber(+event.target.value)} 112 | /> 113 | 114 |
115 | {warningMessage ? ( 116 |
{warningMessage}
117 | ) : ( 118 |
119 | )} 120 | 127 | 130 |
131 |
132 |
133 |
134 |
135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /src/components/Alerts.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { ToastContainer, toast } from 'react-toastify'; 3 | import 'react-toastify/dist/ReactToastify.css'; 4 | import { BsFillBellFill } from 'react-icons/bs'; 5 | import AlertModal from './AlertModal'; 6 | import styles from './styles/Modal.module.scss'; 7 | 8 | type Props = { 9 | data: number[] | number[][]; 10 | metric: string; 11 | unit: string; 12 | selectedMetric?: string; 13 | }; 14 | export default function Push({ data, metric, unit, selectedMetric }: Props) { 15 | // declare opened/close state, initialized to false to hide modal on default 16 | const [isOpened, setIsOpened] = useState(false); 17 | const [isActivated, setIsActivated] = useState(false); 18 | const [dropDownValue, setDropDownValue] = useState('lessThan'); 19 | const [number, setNumber] = useState(0); 20 | const [bellColor, setBellColor] = useState('313641'); 21 | 22 | const getLastValue = (data: number[] | number[][]): number | undefined => { 23 | if (Array.isArray(data) && data.length > 0) { 24 | const lastElement = data[data.length - 1]; 25 | if (Array.isArray(lastElement)) { 26 | return lastElement[lastElement.length - 1]; 27 | } else { 28 | return lastElement; 29 | } 30 | } 31 | return undefined; 32 | }; 33 | 34 | const showToastMessage = () => { 35 | toast.info( 36 | `Current ${metric.toLowerCase()} is ${data[data.length - 1]} ${unit}`, 37 | { 38 | position: toast.POSITION.TOP_RIGHT, 39 | } 40 | ); 41 | 42 | let sound = new Audio( 43 | 'https://www.myinstants.com/media/sounds/old-aol-instant-messenger-aim-sound-effects-youtube.mp3' 44 | ); 45 | sound.play(); 46 | }; 47 | 48 | const humanMetric = metric 49 | .split('_') 50 | .map((e) => e.charAt(0).toUpperCase() + e.slice(1)) 51 | .join(' '); 52 | 53 | const formattedSelectedMetric = 54 | selectedMetric && 55 | selectedMetric 56 | .split('_') 57 | .map((e) => e.charAt(0).toUpperCase() + e.slice(1)) 58 | .join(' '); 59 | 60 | useEffect(() => { 61 | if (isActivated) { 62 | // if greater than, check if the last element of the array is greater than the user submitted number param 63 | const lastValue = getLastValue(data); 64 | if (lastValue !== undefined && number !== undefined) { 65 | if (dropDownValue === 'greaterThan') { 66 | if (lastValue > number) { 67 | showToastMessage(); 68 | onDeactivate(); 69 | } 70 | } 71 | // if less than, check if the last element of the array is less than the user submitted number param 72 | else if (dropDownValue === 'lessThan') { 73 | if (lastValue < number) { 74 | showToastMessage(); 75 | onDeactivate(); 76 | } 77 | } 78 | } 79 | } 80 | }, [data]); 81 | 82 | console.log('humanMetric is: ', humanMetric); 83 | console.log('dropdown Value is: ', dropDownValue); 84 | console.log('metric is: ', metric); 85 | 86 | const onConfirm = () => { 87 | console.log('alert activated/confirmed'); 88 | setIsActivated(true); 89 | console.log('isActivated is ', isActivated); 90 | setBellColor('CB3016'); 91 | }; 92 | 93 | const onDeactivate = () => { 94 | setIsActivated(false); 95 | setBellColor('313641'); 96 | setNumber(undefined); 97 | setDropDownValue(''); 98 | console.log('deactivated message is', deactivateMessage); 99 | }; 100 | 101 | let deactivateMessage = dropDownValue 102 | ? `Active alert: ${ 103 | humanMetric ? humanMetric : formattedSelectedMetric 104 | } to ${ 105 | dropDownValue === 'greaterThan' ? 'exceed' : 'fall below' 106 | } ${number} ${unit}.` 107 | : `No active alert.`; 108 | 109 | // const deactivateMessageHelper = () => { 110 | // if (dropDownValue === 'greaterThan') { 111 | // return (deactivateMessage = `Active alert: ${metric} exceed ${number} ${unit}.`); 112 | // } 113 | // if (dropDownValue === 'lessThan') { 114 | // return (deactivateMessage = `Active alert: ${metric} fall below ${number} ${unit}.`); 115 | // } 116 | // return (deactivateMessage = `No alert created.`); 117 | // }; 118 | 119 | return ( 120 |
121 |
122 | 131 | setIsOpened(false)} 142 | setDropDownValue={setDropDownValue} 143 | setNumber={setNumber} 144 | unit={unit} 145 | onDeactivate={onDeactivate} 146 | isActivated={isActivated} 147 | dropDownValue={dropDownValue} 148 | /> 149 |
150 | 151 |
152 | ); 153 | } 154 | -------------------------------------------------------------------------------- /src/components/BarChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Bar } from 'react-chartjs-2'; 3 | import Chart from 'chart.js/auto'; 4 | import { isPropertySignature } from 'typescript'; 5 | import Alerts from './Alerts'; 6 | import styles from './styles/Charts.module.scss'; 7 | 8 | interface barProps { 9 | barData: number[][]; 10 | name: string; 11 | labels: string[]; 12 | } 13 | 14 | // renders bar chart on landing page 15 | export default function BarChart({ barData, name, labels }: barProps) { 16 | const metric: string = 'Memory Usage'; 17 | const unit: string = 'bytes'; 18 | const options = { 19 | responsive: true, 20 | scales: { 21 | x: { 22 | stacked: true, 23 | }, 24 | y: { 25 | stacked: true, 26 | beginAtZero: true, 27 | }, 28 | }, 29 | }; 30 | const x_axis = ['-8.00', '-6.00', '-4.00', '-2.00']; 31 | const data = { 32 | labels: x_axis, 33 | datasets: [ 34 | { 35 | label: labels[0], 36 | data: barData.map((arr) => arr[0]), 37 | backgroundColor: 'rgb(75, 192, 192)', 38 | }, 39 | { 40 | label: labels[1], 41 | data: barData.map((arr) => arr[1]), 42 | backgroundColor: 'rgb(53, 162, 235)', 43 | }, 44 | ], 45 | }; 46 | 47 | return ( 48 |
49 |

{name}

50 | 51 | 52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/components/DropDown.tsx: -------------------------------------------------------------------------------- 1 | import { Units } from '../../types/types'; 2 | import * as React from 'react'; 3 | import MenuItem from '@mui/material/MenuItem'; 4 | import FormControl from '@mui/material/FormControl'; 5 | import Select, { SelectChangeEvent } from '@mui/material/Select'; 6 | import './styles/DropDown.module.scss'; 7 | 8 | const ITEM_HEIGHT = 48; 9 | const ITEM_PADDING_TOP = 10; 10 | const MenuProps = { 11 | PaperProps: { 12 | style: { 13 | maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, 14 | width: 300, 15 | }, 16 | }, 17 | }; 18 | 19 | const options = Object.keys(Units); 20 | 21 | interface DropDownProps { 22 | setStateFn: React.Dispatch>; 23 | category: string; 24 | axisState: string; 25 | } 26 | 27 | // dropdown for customizable graphs 28 | export default function DropDown({ 29 | setStateFn, 30 | category, 31 | axisState, 32 | }: DropDownProps) { 33 | const handleChange = (event: SelectChangeEvent) => { 34 | if (event.target.value !== null) { 35 | setStateFn(event.target.value); 36 | } 37 | }; 38 | return ( 39 |
40 | 41 | 65 | 66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/components/DynamicLineGraph.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Chart, registerables } from 'chart.js'; 3 | import { Line } from 'react-chartjs-2'; 4 | import { Units, MetricCollection } from '../../types/types'; 5 | import Alerts from './Alerts'; 6 | import DropDown from './DropDown'; 7 | import styles from './styles/Charts.module.scss'; 8 | 9 | Chart.register(...registerables); 10 | 11 | interface scatterProps { 12 | metrics: MetricCollection[]; 13 | } 14 | 15 | // customizable line graph 16 | export default function DynamicLineGraph({ 17 | metrics, 18 | }: scatterProps): JSX.Element { 19 | const [stateY, setStateY] = useState(''); 20 | const [lineData, setLineData] = useState([]); 21 | const [lineTitle, setLineTitle] = useState(''); 22 | const [lineUnit, setLineUnit] = useState(''); 23 | 24 | let tempArr = []; 25 | for (let i = 0; i < metrics.length; i++) { 26 | if (stateY !== '') { 27 | tempArr.push(metrics[i][stateY]); 28 | } 29 | } 30 | 31 | const labels: number[] = []; 32 | lineData.forEach((el, ind) => { 33 | labels.push(ind); 34 | }); 35 | 36 | useEffect(() => { 37 | if (stateY) { 38 | let tempArr = []; 39 | let title = ''; 40 | let unit = ''; 41 | for (let i = 0; i < metrics.length; i++) { 42 | if (stateY !== '') { 43 | tempArr.push(metrics[i][stateY]); 44 | title = stateY; 45 | unit = Units[stateY]; 46 | } 47 | } 48 | setLineData(tempArr); 49 | setLineTitle(title); 50 | setLineUnit(unit); 51 | } 52 | }, [stateY, metrics]); 53 | 54 | const data = { 55 | labels: labels, 56 | datasets: [ 57 | { 58 | data: lineData, 59 | borderColor: 'black', 60 | borderWidth: 1, 61 | }, 62 | ], 63 | }; 64 | 65 | return ( 66 |
67 |
68 | 73 |

vs Time

74 |
75 | 93 | 99 |
100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /src/components/DynamicMetricBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Alerts from './Alerts'; 3 | import styles from './styles/MetricBox.module.scss'; 4 | import { MetricCollection, Units } from '../../types/types'; 5 | import DropDown from './DropDown'; 6 | import { BsTrash } from 'react-icons/bs'; 7 | 8 | interface boxProps { 9 | deleteFn: Function; 10 | metrics: MetricCollection; 11 | id: string; 12 | } 13 | 14 | export default function DynamicMetricBox({ metrics, deleteFn, id }: boxProps) { 15 | const [currMetric, setCurrMetrics] = useState('') 16 | 17 | return ( 18 |
19 | 20 |

{ (metrics && currMetric != '') ? `${metrics[currMetric]} ${Units[currMetric]}` : ''}

21 |
22 | 26 | 29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/components/LineGraph.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { CategoryScale, Chart, registerables } from 'chart.js'; 3 | import { setLabels } from 'react-chartjs-2/dist/utils'; 4 | import { Line } from 'react-chartjs-2'; 5 | import { Units } from '../../types/types'; 6 | import Alerts from './Alerts'; 7 | import styles from './styles/Charts.module.scss'; 8 | 9 | Chart.register(...registerables); 10 | 11 | interface lineProps { 12 | lineData: number[]; 13 | title: string; 14 | axesLabels: string[]; 15 | } 16 | 17 | export default function LineGraph({ 18 | lineData, 19 | title, 20 | axesLabels, 21 | }: lineProps): JSX.Element { 22 | const labels: number[] = []; 23 | const metric: string = title; 24 | const unit: string = 'bytes'; 25 | 26 | lineData.forEach((el, ind) => { 27 | labels.push(ind); 28 | }); 29 | const data = { 30 | labels: labels, 31 | datasets: [ 32 | { 33 | data: lineData, 34 | borderColor: 'black', 35 | borderWidth: 1, 36 | }, 37 | ], 38 | }; 39 | return ( 40 |
41 |

{title}

42 | 60 | 61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/components/Main.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import axios from 'axios'; 3 | import Sidebar from './Sidebar'; 4 | import MetricContainer from './MetricContainer'; 5 | import { MetricCollection, Endpoint } from '../../types/types'; 6 | import NavBar from './NavBar'; 7 | import { useUser } from '@auth0/nextjs-auth0/client'; 8 | import styles from './styles/Main.module.scss'; 9 | 10 | export default function Main() { 11 | // declare state that we pass down to sidebar. sidebar is where the user is entering the endpoints and where the declared state will be updated. 12 | const [metricEndpoint, setMetricEndpoint] = useState(); 13 | const [connected, isConnected] = useState(); 14 | const [metrics, setMetrics] = useState([]); //store array of metric object instances (that updates every X sec) 15 | const [delay, setDelay] = useState(1000); // default interval is 2000ms 16 | const [points, setPoints] = useState(10); // number of data points stored 17 | 18 | const savedCallback = useRef(retrieveData); // use retrieveData fn through useRef so it has access to updated metrics 19 | savedCallback.current = retrieveData; 20 | 21 | const { user } = useUser(); 22 | 23 | // checks to see if the user that has signed in/created an account is already in the database 24 | async function checkUser() { 25 | try { 26 | if (user) { 27 | const response = await axios.post('api/controllers/createUser', { 28 | emailaddress: user.name, 29 | }); 30 | } 31 | } catch (error) { 32 | console.log(error); 33 | } 34 | } 35 | 36 | useEffect(() => { 37 | checkUser(); 38 | }, [user]); 39 | 40 | useEffect(() => { 41 | // setup data fetching interval 42 | if (connected) { 43 | // if connected, then set fetch interval 44 | let id = setInterval(() => { 45 | savedCallback.current(); 46 | }, delay); 47 | return () => clearInterval(id); 48 | } 49 | }, [connected, delay]); // when endpoint changes, clear previous interval and start new one 50 | 51 | async function retrieveData() { 52 | try { 53 | const response = await axios.get('/api/retrieveMetrics'); 54 | if (response.data !== '') { 55 | if (metrics.length == points) { 56 | setMetrics([...metrics.slice((1 - points)), response.data]); 57 | } else { 58 | setMetrics([...metrics, response.data]); 59 | } 60 | } 61 | } catch (err) { 62 | console.log(err); 63 | } 64 | } 65 | 66 | useEffect(() => { 67 | connectEndpoint(); // attempt to connect when endpoint is updated 68 | 69 | return () => { 70 | if (connected) disconnectEndpoint(); // only if connected, disconnect from endpoint when endpoint is changed 71 | }; 72 | async function connectEndpoint() { 73 | if (metricEndpoint) { 74 | // only attempt to connect if endpoint is set 75 | try { 76 | const response = await axios({ 77 | url: '/api/connect', 78 | method: 'POST', 79 | data: metricEndpoint, // uses current endpoint as body sent in request 80 | }); 81 | console.log(response); 82 | isConnected(true); 83 | } catch (err) { 84 | console.log('Could not connect to: ', metricEndpoint.nickname, 'Error message: ', err); 85 | isConnected(false); 86 | } 87 | } 88 | } 89 | 90 | async function disconnectEndpoint() { 91 | if (metricEndpoint) { 92 | // only attempt to disconnect if endpoint is set 93 | const response = await axios.get('/api/disconnect'); 94 | if (response.status === 200) { 95 | // if successful disconnect, reset metrics array for new endpoint 96 | setMetrics([]); 97 | console.log('Disconnected from:', metricEndpoint.nickname); 98 | } else { 99 | console.log(response.status); 100 | } 101 | } 102 | } 103 | }, [metricEndpoint]); 104 | 105 | return ( 106 |
107 | 108 |
109 | 110 | 111 |
112 |
113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /src/components/MetricBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Alerts from './Alerts'; 3 | import styles from './styles/MetricBox.module.scss'; 4 | import { Units } from '../../types/types'; 5 | 6 | interface boxProps { 7 | boxData: number; 8 | name: string; 9 | metric: string; 10 | } 11 | 12 | export default function MetricBox({ name, boxData, metric }: boxProps) { 13 | const title: string = name; 14 | const unit: string = Units[metric]; 15 | 16 | return ( 17 |
18 |

{name}

19 |

{boxData ? `${boxData} ${unit}` : ''}

20 | 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/MetricContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { MetricCollection, Units } from '../../types/types'; 3 | import ScatterChart from './ScatterChart'; 4 | import DynamicLineGraph from './DynamicLineGraph'; 5 | import styles from './styles/Charts.module.scss'; 6 | import TopRow from './TopRow'; 7 | 8 | interface containerProps { 9 | metrics: MetricCollection[]; 10 | } 11 | 12 | export default function MetricContainer({ metrics }: containerProps): JSX.Element { 13 | 14 | return ( 15 |
16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Image from 'next/image'; 3 | import Link from 'next/link'; 4 | import Logo from '../public/logo.png'; 5 | import Logout from '../public/logout.png'; 6 | import Login from '../public/login.png'; 7 | import Github from '../public/github.png'; 8 | import Monitoring from '../public/monitoring.png'; 9 | import Blog from '../public/blog.png'; 10 | import {useUser} from '@auth0/nextjs-auth0/client'; 11 | import styles from './styles/NavBar.module.scss' 12 | 13 | export default function NavBar() { 14 | const { user } = useUser(); 15 | 16 | return ( 17 |
18 | 19 |
20 | 21 | Redline Logo 22 | 23 |
24 | 25 |
26 | 27 |
28 | 29 | 30 | {user ? (logout) : (Login Button)} 31 | 32 | 33 | 34 | Github Button 35 | 36 | 37 | 38 | Blog Button 39 | 40 | 41 | 42 | Monitoring Button 43 | 44 |
45 | 46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/components/ScatterChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { CategoryScale, Chart, registerables } from 'chart.js'; 3 | import { setLabels } from 'react-chartjs-2/dist/utils'; 4 | import { Line, Scatter } from 'react-chartjs-2'; 5 | import { Units, MetricCollection, Metrics } from '../../types/types'; 6 | import DropDown from './DropDown'; 7 | import styles from './styles/Charts.module.scss'; 8 | 9 | 10 | Chart.register(...registerables); 11 | 12 | interface scatterProps { 13 | metrics: MetricCollection[] 14 | } 15 | 16 | interface scatterPoint { 17 | x: number, 18 | y: number 19 | } 20 | 21 | export default function ScatterChart({ metrics }: scatterProps): JSX.Element { 22 | const [stateX, setStateX] = useState(''); 23 | const [stateY, setStateY] = useState(''); 24 | const [scatterData, setScatterData] = useState([]); 25 | 26 | const labels: number[] = []; 27 | const unit: string = 'bytes' 28 | 29 | let tempArr = []; 30 | for(let i = 0; i < metrics.length; i++) { 31 | if(stateX !== '' && stateY !== '') { 32 | tempArr.push({ 33 | x: metrics[i][stateX], 34 | y: metrics[i][stateY] 35 | }) 36 | } 37 | } 38 | 39 | useEffect(() => { 40 | if(stateX && stateY) { 41 | let tempArr = []; 42 | for(let i = 0; i < metrics.length; i++) { 43 | if(stateX !== '' && stateY !== '') { 44 | tempArr.push({ 45 | x: metrics[i][stateX], 46 | y: metrics[i][stateY] 47 | }) 48 | } 49 | } 50 | setScatterData(tempArr); 51 | console.log(tempArr) 52 | } 53 | }, [stateX, stateY, metrics]) 54 | 55 | let data = { 56 | datasets: [ 57 | { 58 | label: 'hello', 59 | data: scatterData, 60 | borderColor: 'black', 61 | borderWidth: 1, 62 | }, 63 | ], 64 | }; 65 | let options = { 66 | plugins: { 67 | title: { display: true }, 68 | legend: { display: false }, 69 | }, 70 | scales: { 71 | x: { 72 | title: { display: true, text: Units[stateX] }, 73 | }, 74 | y: { 75 | title: { display: true, text: Units[stateY] }, 76 | beginAtZero: true, 77 | }, 78 | }, 79 | } 80 | return ( 81 |
82 |
83 | 84 |

vs

85 | 86 |
87 | 91 |
92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { BsTrash } from 'react-icons/bs'; 3 | import { GrAddCircle } from 'react-icons/gr'; 4 | import { Endpoint } from '../../types/types'; 5 | import styles from './styles/SideBar.module.scss'; 6 | import { useUser } from '@auth0/nextjs-auth0/client'; 7 | import axios from 'axios'; 8 | import deleteUserEndpoint from '../components/sidebarFunctions/deleteUserEndpoint'; 9 | import storeUserEndpoint from '../components/sidebarFunctions/storeUserEndpoint'; 10 | 11 | interface SidebarProps { 12 | setMetricEndpoint: React.Dispatch>; 13 | } 14 | 15 | export default function Sidebar({ setMetricEndpoint }: SidebarProps) { 16 | // store the most recently added host, port, password and nickname in state as well as all the endpoints 17 | const [host, setHost] = useState(''); 18 | const [port, setPort] = useState(); 19 | const [password, setPassword] = useState(''); 20 | const [nickname, setNickname] = useState(''); 21 | const [endpoints, setEndpoints] = useState([]); 22 | const [repeatWarning, setRepeatWarning] = useState(''); 23 | 24 | const { user } = useUser(); 25 | 26 | // get a logged-in users endpoints 27 | async function getUserEndpoints() { 28 | try { 29 | if (user) { 30 | const response = await axios.get( 31 | `api/controllers/userEndpoints?emailaddress=${user.name}` 32 | ); 33 | setEndpoints(response.data); 34 | } 35 | } catch (err) { 36 | console.log(err); 37 | } 38 | } 39 | 40 | // if a user is logged in, get the endpoints from the database 41 | useEffect(() => { 42 | if (user) { 43 | let data = getUserEndpoints(); 44 | console.log(data); 45 | } 46 | }, [user]); 47 | 48 | // stores users who aren't logged-ins' endpoints in session storage 49 | function handleFormSubmit(event: any) { 50 | event.preventDefault(); 51 | let repeat: boolean = false; 52 | 53 | for (const object of endpoints) { 54 | if (object['nickname'] === nickname) repeat = true; 55 | } 56 | 57 | if (repeat) 58 | return setRepeatWarning( 59 | 'Warning: endpoint with this nickname already exists, please rename the endpoint and try again.' 60 | ); 61 | else setRepeatWarning(''); 62 | 63 | const newEndpoint: Endpoint = { 64 | host: host, 65 | port: port ?? 6739, 66 | password: password, 67 | nickname: nickname, 68 | }; 69 | 70 | if (user) { 71 | storeUserEndpoint(user.name, newEndpoint); 72 | } 73 | 74 | const previousEndpoints = endpoints; 75 | setEndpoints([...previousEndpoints, newEndpoint]); 76 | 77 | sessionStorage.setItem( 78 | 'allEndpoints', 79 | JSON.stringify([...previousEndpoints, newEndpoint]) 80 | ); 81 | sessionStorage.setItem(nickname, JSON.stringify(newEndpoint)); 82 | event.target[0].value = ''; 83 | event.target[1].value = ''; 84 | event.target[2].value = ''; 85 | event.target[3].value = ''; 86 | } 87 | 88 | // this ensures that the endpoints in state don't get rewritten 89 | useEffect(() => { 90 | const allEndpoints = sessionStorage.getItem('allEndpoints'); 91 | if (allEndpoints !== null) { 92 | const parsedEndpoints = JSON.parse(allEndpoints); 93 | setEndpoints(parsedEndpoints); 94 | } 95 | }, []); 96 | 97 | // stores the most recently clicked on endpoint in sessionStorage so it can be spun up when metrics are displayed on page. 98 | function storeCurrentEndpoint(endpoint: string) { 99 | endpoints.forEach((object) => { 100 | if (object['nickname'] === endpoint) { 101 | sessionStorage.setItem('currentEndpoint', JSON.stringify(object)); 102 | setMetricEndpoint(object); 103 | } 104 | }); 105 | } 106 | 107 | // deletes endpoint from database/session storage 108 | function deleteEndpoint(endpoint: string) { 109 | if (user) { 110 | deleteUserEndpoint(user.name, endpoint); 111 | } 112 | 113 | sessionStorage.removeItem(endpoint); 114 | if ( 115 | JSON.parse(sessionStorage.getItem('currentEndpoint') || '{}').nickname === 116 | endpoint 117 | ) 118 | sessionStorage.removeItem('currentEndpoint'); 119 | 120 | const newEndpoints = []; 121 | for (let data of endpoints) { 122 | if (data.nickname !== endpoint) { 123 | newEndpoints.push(data); 124 | } 125 | } 126 | setEndpoints(newEndpoints); 127 | sessionStorage.setItem('allEndpoints', JSON.stringify(newEndpoints)); 128 | } 129 | 130 | return ( 131 |
132 |
133 | 145 |
146 |
147 | 159 |

160 | 172 |

173 | 185 |
186 |
187 |
{repeatWarning}
188 |
189 | 192 |
193 |
194 |
195 | {endpoints.map((object, index) => { 196 | return ( 197 |
198 | 207 | 216 |
217 |
218 | ); 219 | })} 220 |
221 |
222 | ); 223 | } 224 | -------------------------------------------------------------------------------- /src/components/TopRow.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, ReactEventHandler, use } from 'react'; 2 | import { MetricCollection, Units } from '../../types/types'; 3 | import styles from './styles/TopRow.module.scss'; 4 | import { GrAddCircle } from 'react-icons/gr'; 5 | import DynamicMetricBox from './DynamicMetricBox'; 6 | import uniqid from 'uniqid'; 7 | 8 | interface topRowProps { 9 | metrics: MetricCollection; 10 | } 11 | export default function TopRow({ metrics }: topRowProps) { 12 | 13 | const [metricBoxArray, setMetricBoxArray] = useState([]); //([]); 14 | 15 | const handleDelete = (delId: string): void => { 16 | setMetricBoxArray(metricBoxArray.filter((id) => { 17 | return id !== delId 18 | })) 19 | } 20 | 21 | const handleAdd = (e: React.MouseEvent): void => { 22 | const myId = uniqid(); 23 | setMetricBoxArray([...metricBoxArray, myId]); 24 | } 25 | 26 | useEffect(() => { // start with one metric box 27 | const myId = uniqid(); 28 | setMetricBoxArray([...metricBoxArray, myId]); 29 | }, []) 30 | 31 | return ( 32 |
33 | { metricBoxArray.map((id, ind) => { 34 | return ( 35 | 36 | ) 37 | }) } 38 | 39 | 42 |
43 | ) 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/components/sidebarFunctions/deleteUserEndpoint.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export default async function deleteUserEndpoint(emailaddress:string|null|undefined, endpoint: string) { 4 | 5 | try { 6 | const response = await axios.delete(`api/controllers/userEndpoints?emailaddress=${emailaddress}`, { data: { 7 | nickname: endpoint, 8 | }}) 9 | } catch (err) { 10 | console.log(err); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/components/sidebarFunctions/storeUserEndpoint.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import {Endpoint} from '../../../types/types' 3 | 4 | export default async function storeUserEndpoint(emailaddress: string|null|undefined, newEndpoint: Endpoint) { 5 | try { 6 | const response = await axios.post(`api/controllers/userEndpoints?emailaddress=${emailaddress}`, { 7 | newEndpoint, 8 | }); 9 | } catch (error) { 10 | console.log(error); 11 | } 12 | } -------------------------------------------------------------------------------- /src/components/styles/Charts.module.scss: -------------------------------------------------------------------------------- 1 | 2 | .metricContainer { 3 | display: flex; 4 | gap: 20px; 5 | align-items: center; 6 | flex-direction: column; 7 | box-sizing: border-box; 8 | } 9 | 10 | .graphContainer { 11 | display: flex; 12 | flex-wrap: wrap; 13 | gap: 20px; 14 | box-sizing: border-box; 15 | } 16 | 17 | .graphWrapper { 18 | border: solid 1px #313641; 19 | border-radius: 10px; 20 | padding: 10px; 21 | box-shadow: rgba(0, 0, 0, 0.19) 0px 10px 20px, rgba(0, 0, 0, 0.23) 0px 6px 6px; 22 | width: clamp(500px, 47%, 600px); 23 | height: fit-content; 24 | display: flex; 25 | flex-direction: column; 26 | align-items: center; 27 | 28 | h2 { 29 | font-family: 'DM Serif Display', serif; 30 | font-size: 20px; 31 | margin: 0; 32 | text-align: center; 33 | color: #313614; 34 | } 35 | h1 { 36 | font-family: 'DM Serif Display', serif; 37 | text-align: center; 38 | } 39 | } 40 | 41 | .header { 42 | display: flex; 43 | justify-content: center; 44 | align-items: center; 45 | gap: 8px; 46 | } 47 | 48 | .addEndpoint { 49 | outline: 0; 50 | border: none; 51 | cursor: pointer; 52 | position: relative; 53 | background-color: transparent; 54 | touch-action: manipulation; 55 | } 56 | -------------------------------------------------------------------------------- /src/components/styles/DropDown.module.scss: -------------------------------------------------------------------------------- 1 | .MuiMenuItem-root { 2 | font-size: 1000px; 3 | } 4 | 5 | .MuiOutlinedInput-root { 6 | font-size: 1000px; 7 | } 8 | 9 | .css-1yk1gt9-MuiInputBase-root-MuiOutlinedInput-root-MuiSelect-root { 10 | font-size: 100px; 11 | } 12 | 13 | .MuiInputBase-colorPrimary { 14 | font-size: 100px; 15 | } 16 | 17 | .MuiInputBase-formControl { 18 | font-size: 100px; 19 | } 20 | 21 | .MuiSelect-select { 22 | font-size: 100px; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/styles/Landing.module.scss: -------------------------------------------------------------------------------- 1 | .landingPage { 2 | width: 100%; 3 | min-width: 800px; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | } 8 | 9 | .wrapper { 10 | padding-top: 5vw; 11 | display: flex; 12 | gap: 15px; 13 | flex-direction: column; 14 | justify-content: center; 15 | align-items: center; 16 | width: 1440px; 17 | h1 { 18 | font-family: 'DM Serif Display', serif; 19 | text-align: center; 20 | color: #313641; 21 | font-size: 25px; 22 | } 23 | } 24 | 25 | .headerChart { 26 | display: flex; 27 | flex-direction: row; 28 | justify-content: center; 29 | align-items: center; 30 | width: 80%; 31 | gap: 14px; 32 | } 33 | 34 | .header { 35 | font-family: 'DM Serif Display', serif; 36 | font-size: max(50px, 2vw); 37 | color: #313641; 38 | width: 50%; 39 | display: flex; 40 | flex-direction: column; 41 | justify-content: center; 42 | align-items: flex-start; 43 | } 44 | 45 | .chart { 46 | width: 50%; 47 | height: auto; 48 | padding: 4px; 49 | display: flex; 50 | justify-content: flex-end; 51 | } 52 | 53 | #button { 54 | outline: 0; 55 | font-family: 'Montserrat', sans-serif; 56 | border: solid 1px #313641; 57 | border-radius: 10px; 58 | padding: 10px; 59 | box-shadow: rgba(61, 68, 81, 0.19) 0px 10px 20px, 60 | rgba(61, 68, 81, 0.23) 0px 6px 6px; 61 | font-size: 30px; 62 | letter-spacing: 3px; 63 | color: #3d4451; 64 | cursor: pointer; 65 | position: relative; 66 | background-color: transparent; 67 | touch-action: manipulation; 68 | } 69 | 70 | .caption { 71 | font-family: 'Montserrat', sans-serif; 72 | color: #3d4451; 73 | font-size: 20px; 74 | margin: 1vw; 75 | width: 90%; 76 | padding: 10px 5px 10px 5px; 77 | text-align: center; 78 | } 79 | 80 | .keyFeatures { 81 | display: flex; 82 | justify-content: center; 83 | gap: 3vw; 84 | } 85 | 86 | .feature { 87 | text-align: center; 88 | font-family: 'Montserrat', sans-serif; 89 | color: #3d4451; 90 | border: solid 1px #3d4451; 91 | border-radius: 10px; 92 | padding: 10px; 93 | width: min(400px, 25vw); 94 | box-shadow: rgba(61, 68, 81, 0.19) 0px 10px 20px, 95 | rgba(61, 68, 81, 0.23) 0px 6px 6px; 96 | } 97 | 98 | .team { 99 | display: flex; 100 | align-items: center; 101 | justify-content: center; 102 | text-align: center; 103 | padding-top: 3vw; 104 | padding-bottom: 4vw; 105 | max-width: 90%; 106 | h2 { 107 | font-family: 'Montserrat', sans-serif; 108 | font-size: 20px; 109 | color: #313614; 110 | } 111 | p { 112 | font-family: 'Montserrat', sans-serif; 113 | font-size: 15px; 114 | color: #3d4451; 115 | } 116 | } 117 | 118 | .teamPhoto { 119 | width: 80%; 120 | height: auto; 121 | } 122 | 123 | .person { 124 | color: #313641; 125 | } 126 | 127 | .logo { 128 | height: 20px; 129 | width: auto; 130 | padding: 2px; 131 | } 132 | 133 | .testimonial { 134 | margin: 0 40px; 135 | padding-top: 10px; 136 | padding-bottom: 10px; 137 | width: auto; 138 | height: 90%; 139 | } 140 | -------------------------------------------------------------------------------- /src/components/styles/Main.module.scss: -------------------------------------------------------------------------------- 1 | .mainContainer { 2 | display: grid; 3 | gap: 30px; 4 | padding: 20px; 5 | padding-top: 40px; 6 | box-sizing: border-box; 7 | grid-template-columns: 200px 1fr; 8 | width: 100%; 9 | } 10 | 11 | .bodyContainer { 12 | width: 100vw; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/styles/MetricBox.module.scss: -------------------------------------------------------------------------------- 1 | 2 | .graphWrapper { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | width: 200px; 7 | gap: 6px; 8 | border: solid 1px #313641; 9 | border-radius: 10px; 10 | padding: 8px; 11 | box-shadow: rgba(0, 0, 0, 0.19) 0px 10px 20px, rgba(0, 0, 0, 0.23) 0px 6px 6px; 12 | 13 | h2 { 14 | font-family: 'DM Serif Display', serif; 15 | font-size: 18px; 16 | margin: 0; 17 | text-align: center; 18 | color: #313614; 19 | } 20 | h1 { 21 | font-family: 'DM Serif Display', serif; 22 | text-align: center; 23 | font-size: 20px; 24 | height: 24px; 25 | margin: 0; 26 | } 27 | } 28 | 29 | .buttonDiv { 30 | display: flex; 31 | justify-content: space-between; 32 | width: 100%; 33 | } 34 | 35 | .addEndpoint { 36 | outline: 0; 37 | border: none; 38 | cursor: pointer; 39 | position: relative; 40 | background-color: transparent; 41 | touch-action: manipulation; 42 | } 43 | -------------------------------------------------------------------------------- /src/components/styles/Modal.module.scss: -------------------------------------------------------------------------------- 1 | .modalOverlay { 2 | top: 0; 3 | bottom: 0; 4 | left: 0; 5 | right: 0; 6 | z-index: 19; 7 | position: fixed; 8 | background-color: rgba(1, 5, 2, 0.76); 9 | 10 | .dropdown { 11 | font-family: 'Montserrat', sans-serif; 12 | color: #313641; 13 | font-size: 14px; 14 | border-radius: 5px; 15 | } 16 | 17 | .modal { 18 | border-radius: 10px; 19 | width: 500px; 20 | height: fit-content; 21 | text-align: center; 22 | background-color: white; 23 | z-index: 20; 24 | position: fixed; 25 | left: 50%; 26 | top: 50%; 27 | transform: translate(-50%, -50%); 28 | border: 1px solid #ccc; 29 | 30 | h3 { 31 | color: #313641; 32 | border-bottom: 1px solid #ccc; 33 | padding: 1rem; 34 | margin: 0; 35 | margin: 10px; 36 | font-family: 'DM Serif Display', serif; 37 | } 38 | 39 | .modalContent { 40 | color: #313641; 41 | padding: 1rem; 42 | font-family: 'Montserrat', sans-serif; 43 | font-size: 14px; 44 | } 45 | 46 | .togglebutton { 47 | font-family: 'Montserrat', sans-serif; 48 | font-size: 14px; 49 | border: 0; 50 | border-radius: 5px; 51 | padding: 0.5rem 1rem; 52 | font-size: 0.8rem; 53 | line-height: 1; 54 | border: 1px solid #313641; 55 | margin: 5px; 56 | font-family: 'Montserrat', sans-serif; 57 | font-size: 14px; 58 | } 59 | } 60 | } 61 | .alertWrapper { 62 | align-self: baseline; 63 | } 64 | 65 | .alertButton { 66 | outline: 0; 67 | color: #313641; 68 | border: none; 69 | cursor: pointer; 70 | position: relative; 71 | background-color: transparent; 72 | touch-action: manipulation; 73 | } 74 | -------------------------------------------------------------------------------- /src/components/styles/NavBar.module.scss: -------------------------------------------------------------------------------- 1 | .navBarWrapper { 2 | display: flex; 3 | align-items: center; 4 | top: 0; 5 | width: 100%; 6 | height: 100px; 7 | justify-content: center; 8 | background-color: #dee2e6; 9 | box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; 10 | overflow: hidden; 11 | font-family: 'Montserrat', sans-serif; 12 | } 13 | 14 | .navSpace { 15 | width: 100%; 16 | } 17 | 18 | .rightNavBar { 19 | display: flex; 20 | float: right; 21 | padding-right: 50px; 22 | } 23 | 24 | .leftNavBar { 25 | display: flex; 26 | float: left; 27 | padding-left: 50px; 28 | } 29 | 30 | #logo { 31 | width: 100px; 32 | height: auto; 33 | } 34 | 35 | .icons { 36 | height: 20px; 37 | width: auto; 38 | margin-left: 20px; 39 | } -------------------------------------------------------------------------------- /src/components/styles/SideBar.module.scss: -------------------------------------------------------------------------------- 1 | .formContainer { 2 | font-family: 'DM Serif Display', serif; 3 | font-size: 20px; 4 | color: #313641; 5 | border-radius: 10px; 6 | padding: 10px; 7 | display: inline-block; 8 | text-align: center; 9 | width: 180px; 10 | height: fit-content; 11 | justify-content: center; 12 | border: 1px solid #313641; 13 | box-shadow: rgba(0, 0, 0, 0.19) 0px 10px 20px, rgba(0, 0, 0, 0.23) 0px 6px 6px; 14 | } 15 | 16 | #plusSign { 17 | width: 25px; 18 | height: auto; 19 | } 20 | 21 | .delete { 22 | outline: 0; 23 | border: none; 24 | cursor: pointer; 25 | position: relative; 26 | background-color: transparent; 27 | touch-action: manipulation; 28 | height: fit-content; 29 | } 30 | 31 | .addEndpoint { 32 | outline: 0; 33 | border: none; 34 | cursor: pointer; 35 | position: relative; 36 | background-color: transparent; 37 | touch-action: manipulation; 38 | } 39 | 40 | .eachEndpoint { 41 | font-family: 'DM Serif Display', serif; 42 | color: #313614; 43 | font-size: 20px; 44 | outline: 0; 45 | border: none; 46 | cursor: pointer; 47 | position: relative; 48 | background-color: transparent; 49 | touch-action: manipulation; 50 | overflow-wrap: anywhere; 51 | text-align: left; 52 | } 53 | 54 | .endPointContainer { 55 | display: flex; 56 | align-items: center; 57 | } 58 | 59 | .repeatWarning { 60 | font-family: 'Montserrat', sans-serif; 61 | color: #d55641; 62 | font-size: 10px; 63 | font-style: italic; 64 | } 65 | 66 | .input { 67 | font-family: 'DM Serif Display', serif; 68 | height: 20px; 69 | border-radius: 8px; 70 | border: 1.5px solid #313641; 71 | padding: 4px 8px 4px 8px; 72 | } 73 | -------------------------------------------------------------------------------- /src/components/styles/TopRow.module.scss: -------------------------------------------------------------------------------- 1 | .topRow { 2 | display: flex; 3 | justify-content: flex-start; 4 | align-items: center; 5 | gap: 15px; 6 | width: 100%; 7 | flex-wrap: wrap; 8 | min-height: 136px; 9 | } 10 | 11 | .addBox { 12 | outline: 0; 13 | border: none; 14 | cursor: pointer; 15 | position: relative; 16 | background-color: transparent; 17 | touch-action: manipulation; 18 | height: fit-content; 19 | } -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../public/globals.css' 2 | import type { AppProps } from 'next/app' 3 | import { UserProvider } from '@auth0/nextjs-auth0/client'; 4 | 5 | export default function App({ Component, pageProps }: AppProps) { 6 | return ( 7 | 8 | 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document'; 2 | 3 | 4 | export default function Document() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/pages/api/auth/[...auth0].ts: -------------------------------------------------------------------------------- 1 | import { handleAuth } from '@auth0/nextjs-auth0'; 2 | 3 | export default handleAuth(); -------------------------------------------------------------------------------- /src/pages/api/connect.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import { connectRedis } from '../../../library/redis'; 3 | 4 | export default async function handler( 5 | req: NextApiRequest, 6 | res: NextApiResponse 7 | ) { 8 | if (req.method == 'POST') { 9 | connectRedis(req.body) 10 | .then(connection => { 11 | if(connection) { 12 | return res.status(200).send(connection); 13 | } 14 | }) 15 | .catch(error => { 16 | return res.status(404).send(error) 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/api/controllers/createUser.ts: -------------------------------------------------------------------------------- 1 | const db = require('../database/userModel.ts'); 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | 4 | 5 | export default async function createUser( 6 | req: NextApiRequest, 7 | res: NextApiResponse 8 | ) { 9 | try { 10 | // check to see if the user exists in the database 11 | const {emailaddress} = req.body; 12 | const getUserQuery = `SELECT * FROM users WHERE emailaddress = '${emailaddress}'`; 13 | const { rows } = await db.query(getUserQuery); 14 | const user = rows[0]; 15 | 16 | // if they don't and they've logged in or created an account add them to the user table in sql 17 | if (!user) { 18 | const params = [emailaddress]; 19 | const createUserQuery = ` 20 | INSERT INTO users (emailaddress) 21 | VALUES ($1) 22 | RETURNING * 23 | `; 24 | const response = await db.query(createUserQuery, params); 25 | console.log('user successfully added') 26 | return res.status(201).json(response.rows[0]); 27 | } else { 28 | console.log('user already exists in database') 29 | return res.status(409).json({}); 30 | } 31 | } catch(error) { 32 | return { 33 | log: `Error occurred in createUser middleware ${error}`, 34 | status: 500, 35 | message: { error: 'Unable to create a new user account' }, 36 | }; 37 | } 38 | } 39 | 40 | module.exports = createUser; -------------------------------------------------------------------------------- /src/pages/api/controllers/userEndpoints.ts: -------------------------------------------------------------------------------- 1 | const db = require('../database/userModel.ts'); 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | 4 | export default async function handler( 5 | req: NextApiRequest, 6 | res: NextApiResponse 7 | ) { 8 | 9 | // grab the user id 10 | const { emailaddress } = req.query; 11 | const getUserQuery = `SELECT * FROM users WHERE emailaddress = '${emailaddress}'`; 12 | const { rows } = await db.query(getUserQuery); 13 | const user = rows[0]; 14 | const user_id = user.id; 15 | 16 | try { 17 | // grab the user endpoints in the table 18 | if (req.method == 'GET') { 19 | const getEndpointsQuery= `SELECT * FROM endpoints WHERE user_id = '${user_id}'`; 20 | const response = await db.query(getEndpointsQuery); 21 | const endpoints = response.rows; 22 | return res.status(200).json(endpoints); 23 | } 24 | 25 | // add an endpoint into the endpoint table, assigning the userId. 26 | if (req.method === 'POST') { 27 | const { host, port, password, nickname } = req.body.newEndpoint; 28 | const params = [host, port, password, nickname, user_id]; 29 | const saveEndpointQuery = ` 30 | INSERT INTO endpoints (host, port, password, nickname, user_id) 31 | VALUES ($1, $2, $3, $4, $5) 32 | RETURNING * 33 | `; 34 | const response = await db.query(saveEndpointQuery, params); 35 | console.log('endpoint successfully saved'); 36 | return res.status(201).json(response.rows[0]); 37 | } 38 | 39 | // delete an endpoint from the endpoint table 40 | if (req.method === 'DELETE') { 41 | const { nickname } = req.body; 42 | const params = [nickname, user_id]; 43 | const deleteEndpointQuery = ` 44 | DELETE FROM endpoints WHERE nickname = '${nickname}' AND user_id = ${user_id} 45 | RETURNING * 46 | `; 47 | const response = await db.query(deleteEndpointQuery); 48 | console.log('endpoint successfully deleted'); 49 | return res.status(201).json(response.rows[0]); 50 | } 51 | } catch (error) { 52 | return { 53 | log: `Error occured in getUserEndpoints middleware, ${error}`, 54 | status: 500, 55 | message: {error: 'Unable to handle request retrieve, create or delete endpoint.'} 56 | }; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/pages/api/database/userModel.ts: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | 3 | const PG_URI = process.env.REDLINE_PUBLIC_DB_URI 4 | 5 | const pool = new Pool({ 6 | connectionString: PG_URI, 7 | }); 8 | 9 | module.exports = { 10 | query: (text:string, params:string, callback:()=>void) => { 11 | console.log('executed query', text); 12 | return pool.query(text, params, callback); 13 | }, 14 | }; 15 | 16 | export { pool }; -------------------------------------------------------------------------------- /src/pages/api/disconnect.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import { disconnectRedis } from '../../../library/redis'; 3 | 4 | export default async function handler( 5 | req: NextApiRequest, 6 | res: NextApiResponse 7 | ) { 8 | if (req.method == 'GET') { 9 | disconnectRedis(); 10 | return res.status(200).send('Disconnected from Redis endpoint'); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/api/latency.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | export default async function handler( 4 | req: NextApiRequest, 5 | res: NextApiResponse 6 | ) { 7 | if (req.method == 'GET') { 8 | console.log('in latencyhandler') 9 | return res.status(200); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/api/retrieveMetrics.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | import { getMetrics } from '../../../library/redis'; 3 | import { Metrics } from '../../../types/types'; 4 | 5 | export default async function handler( 6 | req: NextApiRequest, 7 | res: NextApiResponse 8 | ) { 9 | // Logic for specific charts 10 | if (req.method == 'GET') { 11 | const newMetric = await getMetrics(); 12 | 13 | return res.status(200).json(newMetric); 14 | try { 15 | const newMetric = await getMetrics(); 16 | return res.status(200).json(newMetric); 17 | } catch (err) { 18 | return res.status(400).send(err); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import NavBar from '../../src/components/NavBar'; 3 | import Image from 'next/image'; 4 | import Luke from '../public/luke.png'; 5 | import Alan from '../public/alan.png'; 6 | import Elvin from '../public/elvin.png'; 7 | import Sakura from '../public/sakura.png'; 8 | import styles from '../components/styles/Landing.module.scss'; 9 | import { Chart, CategoryScale, registerables } from 'chart.js/auto'; 10 | import BarChart from '../components/BarChart'; 11 | import Link from 'next/link'; 12 | import { useUser } from '@auth0/nextjs-auth0/client'; 13 | import { style } from '@mui/system'; 14 | import { VscGraphLine } from 'react-icons/vsc'; 15 | import { BsFillBellFill } from 'react-icons/bs'; 16 | import { TbReportAnalytics } from 'react-icons/tb'; 17 | import Marquee from 'react-fast-marquee'; 18 | import githubLogo from '../public/githubLogo.png'; 19 | import linkedinLogo from '../public/linkedinLogo.png'; 20 | import DenogresTestimonial from '../public/denogres.png'; 21 | import DocketeerTestimonial from '../public/docketeer.png'; 22 | import DockwellTestimonial from '../public/dockwell.png'; 23 | import OrcastrationTestimonial from '../public/orcastration.png'; 24 | import QeraunosTestimonial from '../public/qeraunos.png'; 25 | import VSBranchTestimonial from '../public/vsbranch.png'; 26 | import CloudbandTestimonial from '../public/cloudband.png'; 27 | import Head from 'next/head'; 28 | 29 | Chart.register(...registerables); 30 | 31 | export default function Landing() { 32 | const { user } = useUser(); 33 | const [dummyData, setDummyData] = useState([]); 34 | 35 | useEffect(() => { 36 | const generateData = (): number[][] => { 37 | const data: number[][] = []; 38 | for (let i = 0; i < 4; i++) { 39 | let mem = Math.floor(Math.random() * 101); 40 | data.push([mem, 100 - mem]); 41 | } 42 | return data; 43 | }; 44 | const interval = setInterval(() => setDummyData(generateData()), 2000); 45 | return () => { 46 | clearInterval(interval); 47 | }; 48 | }, []); 49 | 50 | return ( 51 |
52 | Redline 53 | 54 |
55 | {/* header */} 56 |
57 |
58 |
59 | Redis Performance Monitoring with{' '} 60 | Redline. 61 |
62 | 63 | {user ? ( 64 | 65 | 66 | 67 | ) : ( 68 | 69 | )} 70 | 71 |
72 | 73 | {/* chart */} 74 |
75 | 80 |
81 |
82 | 83 | {/* caption */} 84 |
85 | Redline is a powerful Redis performance metrics visualizer that’s easy 86 | to set up, free to use, and alerts developers to performance issues, 87 | so that they can avoid spending time constantly monitoring their Redis 88 | instances. 89 |
90 | 91 | {/* features */} 92 |
93 |
94 | 95 |

Core Redis Metrics

96 |

97 | With Redline, developers can visualize any performance metrics in real-time to diagnose performance issues and improve efficiency of their Redis instances. 98 |

99 |
100 | 101 |
102 | 103 |

Alerts

104 |

105 | Our intuitive alert system notifies developers when their Redis instances perform outside of specified metric thresholds - allowing developers to spend less time monitoring. 106 |

107 |
108 |
109 | 110 |
111 | 112 |

Manage Endpoints

113 |

114 | Upon signing up, developers can quickly save cache endpoints and jump back in to where they left off. 115 |

116 |
117 |
118 | {/* testimonials */} 119 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 157 | 158 |
159 |
160 | Luke 161 |

LUKE DRISCOLL

162 |

software engineer

163 | 164 | githubLogo 169 | 170 | 171 | 172 | linkedinLogo 177 | 178 |
179 | 180 |
181 | Elvin 182 |

ELVIN YUEN

183 |

software engineer

184 | 185 | githubLogo 190 | 191 | 192 | linkedinLogo 197 | 198 |
199 | 200 |
201 | Alan 202 |

ALAN PERNG

203 |

software engineer

204 | 205 | githubLogo 210 | 211 | 212 | linkedinLogo 217 | 218 |
219 | 220 |
221 | Sakura 222 |

SAKURA AKIYAMA

223 |

software engineer

224 | 225 | githubLogo 230 | 231 | 232 | linkedinLogo 237 | 238 |
239 |
240 |
241 |
242 | ); 243 | } 244 | -------------------------------------------------------------------------------- /src/pages/monitoring.tsx: -------------------------------------------------------------------------------- 1 | import Main from '../components/Main' 2 | import Head from 'next/head'; 3 | 4 | export default function Home() { 5 | return ( 6 |
7 | Redline 8 |
9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/public/alan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/alan.png -------------------------------------------------------------------------------- /src/public/blog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/blog.png -------------------------------------------------------------------------------- /src/public/cloudband.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/cloudband.png -------------------------------------------------------------------------------- /src/public/denogres.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/denogres.png -------------------------------------------------------------------------------- /src/public/docketeer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/docketeer.png -------------------------------------------------------------------------------- /src/public/dockwell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/dockwell.png -------------------------------------------------------------------------------- /src/public/elvin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/elvin.png -------------------------------------------------------------------------------- /src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/favicon.ico -------------------------------------------------------------------------------- /src/public/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/github.png -------------------------------------------------------------------------------- /src/public/githubLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/githubLogo.png -------------------------------------------------------------------------------- /src/public/globals.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | -------------------------------------------------------------------------------- /src/public/linkedinLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/linkedinLogo.png -------------------------------------------------------------------------------- /src/public/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/login.png -------------------------------------------------------------------------------- /src/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/logo.png -------------------------------------------------------------------------------- /src/public/logout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/logout.png -------------------------------------------------------------------------------- /src/public/luke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/luke.png -------------------------------------------------------------------------------- /src/public/monitoring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/monitoring.png -------------------------------------------------------------------------------- /src/public/orcastration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/orcastration.png -------------------------------------------------------------------------------- /src/public/qeraunos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/qeraunos.png -------------------------------------------------------------------------------- /src/public/sakura.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/sakura.png -------------------------------------------------------------------------------- /src/public/vsbranch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Redline/6fed5724138e39d4584af7733b2cd191c1ecf827/src/public/vsbranch.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true 21 | }, 22 | "include": [ 23 | "next-env.d.ts", 24 | "**/*.ts", 25 | "**/*.tsx", 26 | "src/pages/api/controllers/createUser.ts", 27 | "src/database/userModel.js" 28 | ], 29 | "exclude": [ 30 | "node_modules" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /types/types.ts: -------------------------------------------------------------------------------- 1 | export interface MetricCollection extends Metrics { 2 | //one instance of a metric 3 | 4 | used_memory: number; // macro 5 | // used_memory_rss: number; // allocated memory for redis 6 | used_memory_peak: number; // macro 7 | keyspace_hits: number; // macro => micro 8 | keyspace_misses: number; // macro => micro 9 | instantaneous_ops_per_sec: number; // micro 10 | total_commands_processed: number; // macro => micro 11 | connected_clients: number; // macro 12 | total_connections_received: number; // 13 | instantaneous_input_kbps: number; // micro 14 | instantaneous_output_kbps: number; // micro 15 | total_net_input_bytes: number; // macro => micro 16 | total_net_output_bytes: number; // macro => micro 17 | evicted_keys: number; // macro => micro 18 | expired_keys: number; // macro => micro 19 | rejected_connections: number; // macro => micro 20 | uptime_in_seconds: number; // macro => micro 21 | //db0 22 | keys: number, // micro 23 | expires: number, // micro 24 | avg_ttl: number, // micro 25 | // custom data metrics 26 | used_memory_session: number; 27 | keyspace_hits_session: number; 28 | keyspace_misses_session: number; 29 | keyspace_hitratio_session: number; // calc from above 2, may return NaN 30 | total_net_input_bytes_session: number; 31 | total_net_output_bytes_session: number; 32 | total_commands_processed_session: number; 33 | evicted_keys_session: number; 34 | expired_keys_session: number; 35 | rejected_connections_session: number; 36 | uptime_in_seconds_session: number; 37 | } 38 | 39 | export interface Metrics { 40 | [metricKey: string]: number;// | undefined; 41 | } 42 | 43 | export const Units: Units = { 44 | // stores keys to iterate when fetching data, also store unit to corresponding metricKey 45 | used_memory: 'bytes', 46 | // used_memory_rss: 'bytes', 47 | used_memory_peak: 'bytes', 48 | keyspace_hits: 'hits', 49 | keyspace_misses: 'misses', 50 | instantaneous_ops_per_sec: 'ops/sec', 51 | total_commands_processed: 'commands', 52 | connected_clients: 'clients', 53 | total_connections_received: 'cxns', 54 | instantaneous_input_kbps: 'kb/s', 55 | instantaneous_output_kbps: 'kb/s', 56 | total_net_input_bytes: 'bytes', 57 | total_net_output_bytes: 'bytes', 58 | evicted_keys: 'keys', 59 | expired_keys: 'keys', 60 | rejected_connections: 'cxns', 61 | uptime_in_seconds: 'sec', 62 | keys: 'keys', 63 | expires: 'keys', 64 | avg_ttl: 'sec', 65 | // calculated metrics 66 | used_memory_session: 'bytes', 67 | keyspace_hits_session: 'hits', 68 | keyspace_misses_session: 'misses', 69 | keyspace_hitratio_session: 'hits/read', // calc from above 2 70 | total_net_input_bytes_session: 'bytes', 71 | total_net_output_bytes_session: 'bytes', 72 | total_commands_processed_session: 'commands', 73 | evicted_keys_session: 'keys', 74 | expired_keys_session: 'keys', 75 | rejected_connections_session: 'cxns', 76 | uptime_in_seconds_session: 'sec' 77 | }; 78 | 79 | interface Units { 80 | [x: string]: string; 81 | } 82 | 83 | export interface Endpoint { 84 | host: string, 85 | port: number, 86 | password: string, 87 | nickname: string 88 | } 89 | 90 | export interface ServerError { 91 | log?: string, 92 | message?: { err: string } 93 | } 94 | --------------------------------------------------------------------------------