├── demo-redis-app ├── redis-config │ ├── serverSetup.js │ └── clientConfig.js ├── package.json ├── utils │ ├── mockCommandSettings.js │ └── mockCommandData.js ├── index.js └── package-lock.json ├── .babelrc ├── dist └── server │ ├── controllers │ ├── interfaces.js │ ├── connectionsController.js │ ├── monitorsController.js │ └── utils.js │ ├── redis-monitors │ ├── models │ │ ├── interfaces.js │ │ └── data-stores.js │ └── utils.js │ ├── tests-config │ └── tests-config.json │ ├── assets │ └── index.html │ ├── server │ ├── assets │ │ └── index.html │ └── tests-config │ │ └── tests-config.json │ ├── routers │ ├── connectionsRouter.js │ ├── keyspacesRouter.js │ ├── eventsRouter.js │ └── historiesRouter.js │ └── server.js ├── jestGlobalTeardown.js ├── src ├── client │ ├── components │ │ ├── styles │ │ │ ├── graphs.scss │ │ │ ├── tables.scss │ │ │ ├── config │ │ │ │ └── _variables.scss │ │ │ ├── pagenav.scss │ │ │ ├── app.global.scss │ │ │ ├── instances.scss │ │ │ ├── filternav.scss │ │ │ ├── graphfilters.scss │ │ │ └── styles.scss │ │ ├── navbars │ │ │ ├── InstanceComponent.jsx │ │ │ ├── DatabaseSelector.jsx │ │ │ ├── InstanceNav.jsx │ │ │ ├── PageNav.jsx │ │ │ ├── FilterNav.jsx │ │ │ └── SearchFilter.jsx │ │ ├── graphs │ │ │ ├── KeyspaceChartFilterNav.jsx │ │ │ ├── GraphHolder.jsx │ │ │ ├── EventsChartFilterNav.jsx │ │ │ ├── GraphComponent.jsx │ │ │ └── KeyspaceChartFilter.jsx │ │ ├── keyspace │ │ │ ├── KeyspaceComponent.jsx │ │ │ └── KeyspaceTable.jsx │ │ ├── App.jsx │ │ └── events │ │ │ ├── EventTable.jsx │ │ │ └── EventComponent.jsx │ ├── index.js │ ├── store.js │ ├── reducers │ │ ├── instanceReducer.js │ │ ├── pageReducer.js │ │ ├── graphsReducer.js │ │ ├── databaseReducer.js │ │ ├── instanceInfoReducer.js │ │ ├── dataPageReducer.js │ │ ├── index.js │ │ ├── currentDisplayReducer.js │ │ ├── keyspaceReducer.js │ │ ├── totalEventsReducer.js │ │ └── eventsReducer.js │ ├── actions │ │ └── actionTypes.js │ ├── action-creators │ │ ├── connections.js │ │ └── keyspaceConnections.js │ └── redishawk-logo.svg └── server │ ├── routers │ ├── connectionsRouter.ts │ ├── keyspacesRouter.ts │ └── eventsRouter.ts │ ├── assets │ └── index.html │ ├── tests-config │ └── tests-config.json │ ├── controllers │ ├── interfaces.ts │ ├── monitorsController.ts │ ├── connectionsController.ts │ └── utils.ts │ ├── server.ts │ └── redis-monitors │ ├── models │ └── interfaces.ts │ ├── utils.ts │ └── redis-monitors.ts ├── config.json ├── tsconfig.json ├── jestGlobalSetup.js ├── LICENSE ├── __tests__ ├── client │ ├── react │ │ ├── pageNav.test.js │ │ ├── databaseNav.test.js │ │ ├── instanceNav.test.js │ │ ├── graphComponent.test.js │ │ ├── filterNav.test.js │ │ ├── keyspaceComponent.test.js │ │ └── eventComponent.test.js │ └── redux │ │ └── eventReducers.test.js └── server │ └── data-stores.ts ├── webpack.config.js ├── .gitignore ├── package.json └── README.md /demo-redis-app/redis-config/serverSetup.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } -------------------------------------------------------------------------------- /dist/server/controllers/interfaces.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /jestGlobalTeardown.js: -------------------------------------------------------------------------------- 1 | module.exports = async () => { 2 | for (const server of global.servers) { 3 | await server.close(); 4 | } 5 | } -------------------------------------------------------------------------------- /dist/server/redis-monitors/models/interfaces.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | ; 4 | ; 5 | ; 6 | ; 7 | ; 8 | -------------------------------------------------------------------------------- /src/client/components/styles/graphs.scss: -------------------------------------------------------------------------------- 1 | @import "./config/_variables.scss"; 2 | 3 | canvas { 4 | border-radius: 10px; 5 | height: 300px; 6 | } 7 | 8 | h3 { 9 | text-align: center; 10 | } 11 | 12 | hr { 13 | margin-top: 80px; 14 | margin-bottom: 60px; 15 | } -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "host": "127.0.0.1", 4 | "port": 6379, 5 | "recordKeyspaceHistoryFrequency": 10000, 6 | "maxKeyspaceHistoryCount": 500, 7 | "eventGraphRefreshFrequency": 10000, 8 | "maxEventLogSize": 10000000, 9 | "notifyKeyspaceEvents": "KEA" 10 | } 11 | ] -------------------------------------------------------------------------------- /dist/server/tests-config/tests-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "host": "127.0.0.1", 4 | "port": 49152, 5 | "recordKeyspaceHistoryFrequency": 60000 6 | }, 7 | { 8 | "url": "redis://127.0.0.1:49153", 9 | "port": 49153, 10 | "recordKeyspaceHistoryFrequency": 60000 11 | } 12 | ] -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "CommonJS", 5 | "esModuleInterop": true, 6 | "rootDir": "src/server", 7 | "outDir": "./dist/server", 8 | "allowJs": true, 9 | "removeComments": true 10 | }, 11 | "include": [ 12 | "src/server" 13 | ] 14 | } -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import App from './components/App.jsx'; 5 | 6 | import store from './store.js'; 7 | 8 | render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | -------------------------------------------------------------------------------- /src/client/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import { composeWithDevTools } from 'redux-devtools-extension'; 3 | import thunk from 'redux-thunk'; 4 | import reducers from './reducers/index.js'; 5 | 6 | export default createStore( 7 | reducers, 8 | compose(applyMiddleware(thunk), composeWithDevTools()) 9 | ); 10 | -------------------------------------------------------------------------------- /src/server/routers/connectionsRouter.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import connectionsController from '../controllers/connectionsController'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', 7 | connectionsController.getAllConnections, 8 | (req: express.Request, res: express.Response, next: express.NextFunction) => { 9 | res.status(200).json(res.locals.connections); 10 | }); 11 | 12 | export default router; -------------------------------------------------------------------------------- /src/client/components/styles/tables.scss: -------------------------------------------------------------------------------- 1 | @import './config/_variables.scss'; 2 | 3 | .MuiDataGrid-footer p { 4 | color: $PRIMARY_LIGHT; 5 | font-family: $PRIMARY_FONT; 6 | } 7 | 8 | .MuiTablePagination-root { 9 | color: $PRIMARY_LIGHT; 10 | } 11 | 12 | .MuiInputBase-input { 13 | color: $PRIMARY_LIGHT !important; 14 | font-family: $PRIMARY_FONT !important; 15 | } 16 | 17 | .MuiSvgIcon-root { 18 | fill: $PRIMARY_LIGHT !important; 19 | } -------------------------------------------------------------------------------- /src/client/components/styles/config/_variables.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@300&display=swap'); 2 | 3 | $PRIMARY_RED: rgb(233, 0, 0); 4 | // $PRIMARY_DARK: rgb(35, 35, 35); 5 | $PRIMARY_DARK: rgb(70, 70, 70); 6 | $PRIMARY_LIGHT: rgb(200, 200, 200); 7 | 8 | $PRIMARY_FONT: 'Nunito Sans', 'sans-serif'; 9 | 10 | //Screen media queries: Fixed sizes for 1280px or smaller 11 | $small: 1280px; 12 | $large: 1281px; -------------------------------------------------------------------------------- /demo-redis-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-redis-app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node index.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "faker": "^5.5.3", 15 | "redis": "^3.1.2", 16 | "redis-server": "^1.2.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/client/components/navbars/InstanceComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const InstanceComponent = (props) => { 4 | return ( 5 |
6 |

{ 7 | props.switchInstance(props.instanceId); 8 | }}> 9 | {props.instanceDisplayName} 10 |

11 | {props.databases} 12 |
13 | ); 14 | }; 15 | 16 | export default InstanceComponent; 17 | -------------------------------------------------------------------------------- /dist/server/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | redis-hawk 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/server/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | redis-hawk 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /dist/server/server/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | redis-hawk 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /dist/server/server/tests-config/tests-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "host": "127.0.0.1", 4 | "port": 49152, 5 | "recordKeyspaceHistoryFrequency": 300000, 6 | "maxKeyspaceHistoryCount": 100, 7 | "eventGraphRefreshFrequency": 10000, 8 | "maxEventLogSize": 500000 9 | }, 10 | { 11 | "url": "redis://127.0.0.1:49153", 12 | "port": 49153, 13 | "recordKeyspaceHistoryFrequency": 300000, 14 | "maxKeyspaceHistoryCount": 100, 15 | "eventGraphRefreshFrequency": 10000, 16 | "maxEventLogSize": 500000 17 | } 18 | ] -------------------------------------------------------------------------------- /src/server/tests-config/tests-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "host": "127.0.0.1", 4 | "port": 49152, 5 | "recordKeyspaceHistoryFrequency": 300000, 6 | "maxKeyspaceHistoryCount": 100, 7 | "eventGraphRefreshFrequency": 10000, 8 | "maxEventLogSize": 500000, 9 | "notifyKeyspaceEvents": "KEA" 10 | }, 11 | { 12 | "url": "redis://127.0.0.1:49153", 13 | "port": 49153, 14 | "recordKeyspaceHistoryFrequency": 300000, 15 | "maxKeyspaceHistoryCount": 100, 16 | "eventGraphRefreshFrequency": 10000, 17 | "maxEventLogSize": 500000, 18 | "notifyKeyspaceEvents": "KEA" 19 | } 20 | ] -------------------------------------------------------------------------------- /src/client/reducers/instanceReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/actionTypes.js'; 2 | 3 | const initialState = { 4 | currInstance: 1, 5 | }; 6 | 7 | const instanceReducer = (state = initialState, action) => { 8 | switch (action.type) { 9 | case types.SWITCH_INSTANCE: { 10 | const instanceId = action.payload; 11 | console.log('payload in Switch instance reducer', instanceId); 12 | return { 13 | ...state, 14 | currInstance: instanceId, 15 | }; 16 | } 17 | default: { 18 | return state; 19 | } 20 | } 21 | }; 22 | 23 | export default instanceReducer; 24 | -------------------------------------------------------------------------------- /src/client/reducers/pageReducer.js: -------------------------------------------------------------------------------- 1 | //leave these separate for future developers in case they want to add functionality 2 | import * as types from "../actions/actionTypes.js"; 3 | 4 | const initialState = { 5 | currPage: "keyspace", 6 | }; 7 | 8 | const pageReducer = (state = initialState, action) => { 9 | let currPage; 10 | 11 | switch (action.type) { 12 | case types.UPDATE_PAGE: { 13 | const page = action.payload; 14 | 15 | return { 16 | ...state, 17 | currPage: page, 18 | }; 19 | } 20 | 21 | default: { 22 | return state; 23 | } 24 | } 25 | }; 26 | 27 | export default pageReducer; 28 | -------------------------------------------------------------------------------- /dist/server/routers/connectionsRouter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | var express_1 = __importDefault(require("express")); 7 | var connectionsController_1 = __importDefault(require("../controllers/connectionsController")); 8 | var router = express_1.default.Router(); 9 | router.get('/', connectionsController_1.default.getAllConnections, function (req, res, next) { 10 | res.status(200).json(res.locals.connections); 11 | }); 12 | exports.default = router; 13 | -------------------------------------------------------------------------------- /src/client/components/styles/pagenav.scss: -------------------------------------------------------------------------------- 1 | @import './config/_variables.scss'; 2 | 3 | #pageNavContainer { 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: center; 7 | padding: 1em 0 0; 8 | 9 | @media screen and (max-width: $small) { 10 | font-size: 1.5em; 11 | } 12 | 13 | @media screen and (min-width: $large) { 14 | font-size: 1.875vw; 15 | } 16 | 17 | .page-toggle, .selected-page-toggle { 18 | text-decoration: none; 19 | color: lighten($PRIMARY_LIGHT, 20%); 20 | text-align: center; 21 | border-radius: 10px; 22 | border: 2px solid $PRIMARY_RED; 23 | } 24 | 25 | .selected-page-toggle { 26 | background-color: $PRIMARY_RED; 27 | } 28 | } 29 | 30 | #pageNavContainer > * { 31 | width: 30%; 32 | } -------------------------------------------------------------------------------- /src/client/reducers/graphsReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actions/actionTypes.js"; 2 | 3 | const initialState = { 4 | currDatabase: 0, 5 | keyGraph: [[{ name: "Abigail", memory: "1GB", time: "11:30" }]], 6 | }; 7 | 8 | const keygraphReducer = (state = initialState, action) => { 9 | let keyGraph; 10 | switch (action.type) { 11 | case types.UPDATE_KEYGRAPH: { 12 | const dbIndex = state.currDatabase; 13 | 14 | const newKeyGraph = action.payload; 15 | 16 | keyGraph = state.keyGraph.slice(); 17 | keyGraph[dbIndex].push(newKeyGraph); 18 | 19 | return { 20 | ...state, 21 | keyGraph, 22 | }; 23 | } 24 | 25 | default: { 26 | return state; 27 | } 28 | } 29 | }; 30 | 31 | export default keygraphReducer; 32 | -------------------------------------------------------------------------------- /src/client/reducers/databaseReducer.js: -------------------------------------------------------------------------------- 1 | //leave these separate for future developers in case they want to add functionality 2 | import * as types from '../actions/actionTypes.js'; 3 | 4 | const initialState = { 5 | currDatabase: 0, 6 | }; 7 | 8 | const databaseReducer = (state = initialState, action) => { 9 | switch (action.type) { 10 | case types.SWITCH_DATABASE: { 11 | const dbIndex = action.payload; 12 | console.log('payload in Switch database reducer', dbIndex); 13 | let numberDB = Number(dbIndex); 14 | console.log('number database', numberDB); 15 | return { 16 | ...state, 17 | currDatabase: numberDB, 18 | }; 19 | } 20 | default: { 21 | return state; 22 | } 23 | } 24 | }; 25 | 26 | export default databaseReducer; 27 | -------------------------------------------------------------------------------- /src/client/reducers/instanceInfoReducer.js: -------------------------------------------------------------------------------- 1 | //leave these separate for future developers in case they want to add functionality 2 | import * as types from '../actions/actionTypes.js'; 3 | 4 | const initialState = { 5 | instanceInfo: [ 6 | { 7 | host: '', 8 | port: '', 9 | url: '', 10 | databases: 0, 11 | instanceId: 1, 12 | recordKeyspaceHistoryFrequency: 0, 13 | }, 14 | ], 15 | }; 16 | 17 | const instanceInfo = (state = initialState, action) => { 18 | switch (action.type) { 19 | case types.UPDATE_INSTANCEINFO: { 20 | const instances = action.payload; 21 | return { 22 | ...state, 23 | instanceInfo: instances, 24 | }; 25 | } 26 | 27 | default: { 28 | return state; 29 | } 30 | } 31 | }; 32 | 33 | export default instanceInfo; 34 | -------------------------------------------------------------------------------- /src/client/components/navbars/DatabaseSelector.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const DatabaseSelector = (props) => { 4 | const options = []; 5 | 6 | for (let i = 0; i < props.dbCount; i++) { 7 | options.push( 8 | 11 | ); 12 | } 13 | 14 | return ( 15 |
16 | Select Database: 17 | 26 |
27 | ); 28 | }; 29 | 30 | export default DatabaseSelector; 31 | -------------------------------------------------------------------------------- /src/client/components/styles/app.global.scss: -------------------------------------------------------------------------------- 1 | @import './config/_variables'; 2 | 3 | html { 4 | @media screen and (max-width: $small) { 5 | min-width: $small; 6 | } 7 | } 8 | 9 | #logo-container { 10 | position: absolute; 11 | 12 | svg { 13 | width: 100px; 14 | height: 100px; 15 | } 16 | 17 | left: 30px; 18 | } 19 | 20 | body { 21 | @media screen and (max-width: $small) { 22 | font-size: 1em; 23 | } 24 | 25 | @media screen and (min-width: $large) { 26 | font-size: 1.25vw; 27 | } 28 | 29 | background-color: $PRIMARY_DARK; 30 | color: $PRIMARY_LIGHT; 31 | font-family: $PRIMARY_FONT; 32 | } 33 | 34 | #app { 35 | display: flex; 36 | width: 90%; 37 | padding: 2em 0; 38 | margin: 0 auto; 39 | 40 | #tabs-container { 41 | display: flex; 42 | flex-direction: column; 43 | width: 77%; 44 | align-items: center; 45 | } 46 | 47 | #tabs-container > * { 48 | width: 80%; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /jestGlobalSetup.js: -------------------------------------------------------------------------------- 1 | /* 2 | Start Redis servers prior to tests. 3 | 4 | This is necessary because tests will initiate the RedisMonitors processes, 5 | which need to instantiate node-redis clients for the instances defined in the tests-config.json 6 | */ 7 | 8 | import 'regenerator-runtime/runtime'; 9 | import RedisServer from 'redis-server'; 10 | import { readFileSync } from 'fs'; 11 | import { resolve } from 'path'; 12 | 13 | const testConnections = JSON.parse(readFileSync(resolve(__dirname, './src/server/tests-config/tests-config.json')).toString()); 14 | 15 | module.exports = async () => { 16 | 17 | const servers = []; 18 | for (let conn of testConnections) { 19 | 20 | const server = new RedisServer(conn.port); 21 | try { 22 | await server.open(); 23 | } catch (e) { 24 | console.log(`Error starting server on port ${conn.port}: ${e}`); 25 | } 26 | 27 | servers.push(server); 28 | } 29 | 30 | global.servers = servers 31 | } -------------------------------------------------------------------------------- /src/client/reducers/dataPageReducer.js: -------------------------------------------------------------------------------- 1 | //leave these separate for future developers in case they want to add functionality 2 | import * as types from '../actions/actionTypes.js'; 3 | 4 | const initialState = { 5 | pageSize: 25, 6 | pageNum: 1, 7 | }; 8 | 9 | const dataPageReducer = (state = initialState, action) => { 10 | switch (action.type) { 11 | case types.UPDATE_PAGESIZE: { 12 | const pageSize = action.payload; 13 | console.log('payload in update page size action', pageSize); 14 | return { 15 | ...state, 16 | pageSize: pageSize, 17 | }; 18 | } 19 | case types.UPDATE_PAGENUM: { 20 | const pageNum = action.payload; 21 | console.log('payload in update page num action', pageNum); 22 | return { 23 | ...state, 24 | pageNum: pageNum, 25 | }; 26 | } 27 | default: { 28 | return state; 29 | } 30 | } 31 | }; 32 | 33 | export default dataPageReducer; 34 | -------------------------------------------------------------------------------- /src/server/controllers/interfaces.ts: -------------------------------------------------------------------------------- 1 | import * as redisMonitorInterfaces from '../redis-monitors/models/interfaces'; 2 | 3 | export interface InstanceConnectionDetail extends redisMonitorInterfaces.RedisInstance { 4 | instanceId: number; 5 | databases: number; 6 | } 7 | 8 | export interface ConnectionsResponseBody { 9 | instances: InstanceConnectionDetail[]; 10 | } 11 | 12 | export interface KeyDetails { 13 | key: string; 14 | value: any; 15 | type: string; 16 | } 17 | 18 | export interface EventDetails { 19 | key: string; 20 | event: string; 21 | timestamp: number; 22 | } 23 | 24 | export type Keyspace = KeyDetails[]; 25 | interface InstanceKeyspaceDetail { 26 | instanceId: number; 27 | keyspaces: Keyspace[]; 28 | } 29 | export interface KeyspacesResponseBody { 30 | data: InstanceKeyspaceDetail[]; 31 | } 32 | 33 | export interface KeyspaceResponsePage { 34 | keyTotal: number; 35 | pageSize: number; 36 | pageNum: number; 37 | data: KeyDetails[]; 38 | } -------------------------------------------------------------------------------- /src/server/controllers/monitorsController.ts: -------------------------------------------------------------------------------- 1 | import redisMonitors from '../redis-monitors/redis-monitors'; 2 | import { RequestHandler } from 'express'; 3 | 4 | interface MonitorsController { 5 | findAllMonitors: RequestHandler; 6 | findSingleMonitor: RequestHandler; 7 | } 8 | 9 | const monitorsController: MonitorsController = { 10 | 11 | findAllMonitors: async (req, res, next) => { 12 | res.locals.monitors = redisMonitors; 13 | return next(); 14 | }, 15 | 16 | findSingleMonitor: async (req, res, next) => { 17 | 18 | for (let monitor of redisMonitors) { 19 | if (monitor.instanceId === +req.params.instanceId) { 20 | res.locals.monitors = [monitor]; 21 | } 22 | } 23 | 24 | if (!res.locals.monitors) { 25 | return next({ log: 'User provided invalid instanceId', status: 400, message: { err: 'Please provide a valid instanceId' } }); 26 | } 27 | 28 | return next(); 29 | }, 30 | } 31 | 32 | export default monitorsController -------------------------------------------------------------------------------- /demo-redis-app/utils/mockCommandSettings.js: -------------------------------------------------------------------------------- 1 | /* 2 | Set user-defined frequency for specified Redis commmands to be mocked. 3 | All frequency times are in milliseconds, except for TTL (TIME_TO_LIVE) times which are in seconds. 4 | 5 | Used by index.js and /utils/mock-commands.js 6 | */ 7 | 8 | module.exports = { 9 | STRING_SET_FREQUENCY: 300, 10 | STRING_GET_FREQUENCY: 200, 11 | STRING_DELETE_FREQUENCY: 1000, 12 | STRING_TIME_TO_LIVE: 30000, 13 | LIST_LPUSH_FREQUENCY: 500, 14 | LIST_LRANGE_FREQUENCY: 250, 15 | LIST_LPOP_FREQUENCY: 1000, 16 | LIST_TIME_TO_LIVE: 50000, 17 | SET_SADD_FREQUENCY: 500, 18 | SET_SMEMBERS_FREQUENCY: 250, 19 | SET_SPOP_FREQUENCY: 1000, 20 | SET_TIME_TO_LIVE: 50000, 21 | SORTEDSET_ZADD_FREQUENCY: 350, 22 | SORTEDSET_ZRANGE_FREQUENCY: 1000, 23 | SORTEDSET_ZPOPMIN_FREQUENCY: 2000, 24 | SORTEDSET_TIME_TO_LIVE: 30000, 25 | HASH_HMSET_FREQUENCY: 400, 26 | HASH_HGETALL_FREQUENCY: 500, 27 | HASH_DEL_FREQUENCY: 10000, 28 | HASH_TIME_TO_LIVE: 10000 29 | } -------------------------------------------------------------------------------- /src/client/reducers/index.js: -------------------------------------------------------------------------------- 1 | // combine reducers 2 | import { combineReducers } from 'redux'; 3 | import databaseReducer from './databaseReducer'; 4 | import eventsReducer from './eventsReducer'; 5 | import graphsReducer from './graphsReducer'; 6 | import keyspaceReducer from './keyspaceReducer'; 7 | import instanceInfoReducer from './instanceInfoReducer'; 8 | import pageReducer from './pageReducer'; 9 | import currDisplayReducer from './currentDisplayReducer'; 10 | import instanceReducer from './instanceReducer'; 11 | import dataPageReducer from './dataPageReducer'; 12 | import totalEventsReducer from './totalEventsReducer'; 13 | 14 | export default combineReducers({ 15 | keyspaceStore: keyspaceReducer, 16 | eventsStore: eventsReducer, 17 | totalEventsStore: totalEventsReducer, 18 | keyGraphStore: graphsReducer, 19 | currDatabaseStore: databaseReducer, 20 | currInstanceStore: instanceReducer, 21 | instanceInfoStore: instanceInfoReducer, 22 | currPageStore: pageReducer, 23 | currDisplayStore: currDisplayReducer, 24 | dataPageStore: dataPageReducer, 25 | }); 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /__tests__/client/react/pageNav.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { configure, shallow } from "enzyme"; 3 | import Adapter from "enzyme-adapter-react-16"; 4 | import toJson from "enzyme-to-json"; 5 | import renderer from "react-test-renderer"; 6 | import { Link } from 'react-router-dom' 7 | 8 | import PageNav from "../../../src/client/components/navbars/PageNav.jsx"; 9 | 10 | configure({ adapter: new Adapter() }); 11 | 12 | describe('React PageNav unit tests', () => { 13 | describe('PageNav', () => { 14 | let wrapper; 15 | const props = { 16 | currPage: "keyspace" 17 | } 18 | 19 | beforeAll(() => { 20 | wrapper = shallow() 21 | }) 22 | 23 | it('renders a div with id of pageNavContainer', () => { 24 | expect(wrapper.find('pageNavContainer').find('div')) 25 | }) 26 | it('renders a Link to /', () => { 27 | expect(wrapper.toContainReact(Keyspace)) 28 | }) 29 | it('renders a Link to /events', () => { 30 | expect(wrapper.toContainReact(Events)) 31 | }) 32 | it('renders a Link to /graphs', () => { 33 | expect(wrapper.toContainReact(Graphs)) 34 | }) 35 | }) 36 | }) -------------------------------------------------------------------------------- /src/client/reducers/currentDisplayReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/actionTypes.js'; 2 | 3 | const initialState = { 4 | currDisplay: { 5 | keyNameFilter: '', 6 | keyTypeFilter: '', 7 | keyEventFilter: '', 8 | //add any subsequent filters 9 | }, 10 | }; 11 | 12 | const currentDisplayReducer = (state = initialState, action) => { 13 | switch (action.type) { 14 | case types.UPDATE_CURRDISPLAY: { 15 | console.log('action payload in update curr display', action); 16 | //copy the state 17 | let currDisplay = state.currDisplay; 18 | 19 | //if payload at filtertype is name 20 | if (action.payload.filterType === 'keyName') 21 | currDisplay.keyNameFilter = action.payload.filterValue; 22 | //if payload at filtertype is type 23 | if (action.payload.filterType === 'keyType') 24 | currDisplay.keyTypeFilter = action.payload.filterValue; 25 | 26 | //if payload at filtertype is event 27 | if (action.payload.filterType === 'keyEvent') 28 | currDisplay.keyEventFilter = action.payload.filterValue; 29 | 30 | return { 31 | ...state, 32 | currDisplay: currDisplay, 33 | }; 34 | } 35 | default: 36 | { 37 | return state; 38 | } 39 | x; 40 | } 41 | }; 42 | 43 | export default currentDisplayReducer; 44 | -------------------------------------------------------------------------------- /src/server/routers/keyspacesRouter.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import keyspacesController from '../controllers/keyspacesController'; 3 | import monitorsController from '../controllers/monitorsController' 4 | 5 | const router = express.Router(); 6 | 7 | router.get('/', 8 | monitorsController.findAllMonitors, 9 | keyspacesController.refreshKeyspace, 10 | keyspacesController.getKeyspacePages, 11 | (req: express.Request, res: express.Response): void => { 12 | res.status(200).json(res.locals.keyspaces) 13 | } 14 | ); 15 | 16 | router.get('/:instanceId', 17 | monitorsController.findSingleMonitor, 18 | keyspacesController.refreshKeyspace, 19 | keyspacesController.getKeyspacePages, 20 | (req: express.Request, res: express.Response): void => { 21 | res.status(200).json(res.locals.keyspaces) 22 | } 23 | ); 24 | 25 | 26 | router.get('/:instanceId/:dbIndex', 27 | monitorsController.findSingleMonitor, 28 | keyspacesController.refreshKeyspace, 29 | keyspacesController.getKeyspacePages, 30 | (req: express.Request, res: express.Response): void => { 31 | res.status(200).json(res.locals.keyspaces); 32 | }); 33 | 34 | router.get('/histories/:instanceId/:dbIndex', 35 | monitorsController.findSingleMonitor, 36 | keyspacesController.getKeyspaceHistories, 37 | (req: express.Request, res: express.Response): void => { 38 | res.status(200).json(res.locals.histories); 39 | }); 40 | 41 | export default router; -------------------------------------------------------------------------------- /dist/server/routers/keyspacesRouter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | var express_1 = __importDefault(require("express")); 7 | var keyspacesController_1 = __importDefault(require("../controllers/keyspacesController")); 8 | var monitorsController_1 = __importDefault(require("../controllers/monitorsController")); 9 | var router = express_1.default.Router(); 10 | router.get('/', monitorsController_1.default.findAllMonitors, keyspacesController_1.default.refreshKeyspace, keyspacesController_1.default.getKeyspacePages, function (req, res) { 11 | res.status(200).json(res.locals.keyspaces); 12 | }); 13 | router.get('/:instanceId', monitorsController_1.default.findSingleMonitor, keyspacesController_1.default.refreshKeyspace, keyspacesController_1.default.getKeyspacePages, function (req, res) { 14 | res.status(200).json(res.locals.keyspaces); 15 | }); 16 | router.get('/:instanceId/:dbIndex', monitorsController_1.default.findSingleMonitor, keyspacesController_1.default.refreshKeyspace, keyspacesController_1.default.getKeyspacePages, function (req, res) { 17 | res.status(200).json(res.locals.keyspaces); 18 | }); 19 | router.get('/histories/:instanceId/:dbIndex', monitorsController_1.default.findSingleMonitor, keyspacesController_1.default.getKeyspaceHistories, function (req, res) { 20 | res.status(200).json(res.locals.histories); 21 | }); 22 | exports.default = router; 23 | -------------------------------------------------------------------------------- /src/server/controllers/connectionsController.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from 'express'; 2 | import redisMonitors from '../redis-monitors/redis-monitors'; 3 | import { ConnectionsResponseBody } from './interfaces'; 4 | 5 | interface ConnectionsController { 6 | getAllConnections: RequestHandler; 7 | } 8 | 9 | const connectionsController: ConnectionsController = { 10 | 11 | getAllConnections: (req, res, next) => { 12 | //******LOOK AT OUTPUTTING INTO LOG FILE********** 13 | const connections: ConnectionsResponseBody = { 14 | instances: [] 15 | }; 16 | try { 17 | //iterate through monitors 18 | redisMonitors.forEach((redisMonitor, idx) => { 19 | const instance = { 20 | instanceId: idx + 1, 21 | host: redisMonitor.host, 22 | port: redisMonitor.port, 23 | url: redisMonitor.url, 24 | databases: redisMonitor.databases, 25 | recordKeyspaceHistoryFrequency: redisMonitor.recordKeyspaceHistoryFrequency, 26 | maxKeyspaceHistoryCount: redisMonitor.maxKeyspaceHistoryCount, 27 | eventGraphRefreshFrequency: redisMonitor.eventGraphRefreshFrequency, 28 | maxEventLogSize: redisMonitor.maxEventLogSize, 29 | notifyKeyspaceEvents: redisMonitor.notifyKeyspaceEvents 30 | } 31 | connections.instances.push(instance); 32 | }) 33 | } catch (err) { 34 | return next({ log: 'Could not read connections from RedisMonitor' }); 35 | } 36 | 37 | res.locals.connections = connections; 38 | return next(); 39 | } 40 | 41 | }; 42 | 43 | export default connectionsController; -------------------------------------------------------------------------------- /src/server/routers/eventsRouter.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import monitorsController from '../controllers/monitorsController'; 3 | import eventsController from '../controllers/eventsController'; 4 | 5 | const router = express.Router(); 6 | 7 | router.get('/', 8 | monitorsController.findAllMonitors, 9 | eventsController.refreshEventLog, 10 | eventsController.getEventsPages, 11 | (req: express.Request, res:express.Response): void => { 12 | res.status(200).json(res.locals.events); 13 | }); 14 | 15 | router.get('/:instanceId', 16 | monitorsController.findSingleMonitor, 17 | eventsController.refreshEventLog, 18 | eventsController.getEventsPages, 19 | (req: express.Request, res:express.Response): void => { 20 | res.status(200).json(res.locals.events); 21 | }); 22 | 23 | 24 | router.get('/:instanceId/:dbIndex', 25 | monitorsController.findSingleMonitor, 26 | eventsController.refreshEventLog, 27 | eventsController.getEventsPages, 28 | (req: express.Request, res:express.Response): void => { 29 | res.status(200).json(res.locals.events); 30 | }); 31 | 32 | router.get('/totals/:instanceId/:dbIndex', 33 | monitorsController.findSingleMonitor, 34 | eventsController.validateRequestType, 35 | (req: express.Request, res: express.Response, next: express.NextFunction): void => { 36 | if (req.query.timeInterval) eventsController.getEventsByTimeInterval(req, res, next); 37 | else if (req.query.eventTotal) eventsController.getSingleEventsTotal(req, res, next); 38 | }, 39 | (req: express.Request, res: express.Response): void => { 40 | res.status(200).json(res.locals.eventTotals); 41 | }); 42 | 43 | export default router; 44 | 45 | -------------------------------------------------------------------------------- /demo-redis-app/redis-config/clientConfig.js: -------------------------------------------------------------------------------- 1 | /* 2 | Initializes and configures the node-redis client. 3 | 4 | Imported by /utils/mock-commands.js 5 | 6 | TO-DOS: 7 | we need set standard configurations on the redis database: 8 | -set "maxmemory" - how much memory this database is allowed to use 9 | -set eviction policies if maxmemory is reached 10 | etc. 11 | 12 | */ 13 | 14 | const redis = require('redis'); 15 | const { promisify } = require('util'); 16 | const RedisServer = require('redis-server'); 17 | const clients = []; 18 | 19 | const createClients = async () => { 20 | 21 | for (let PORT = 6379; PORT < 6381; PORT++) { 22 | 23 | const server = new RedisServer(PORT); 24 | 25 | try { 26 | await server.open(); 27 | } catch (e) { 28 | console.log(`Could not start a new server on port ${PORT} - a server may already be running on this port: ${e}`); 29 | } 30 | 31 | const client = redis.createClient({host: '127.0.0.1', port: PORT}); 32 | client.select = promisify(client.select).bind(client); 33 | client.config = promisify(client.config).bind(client); 34 | 35 | try { 36 | await client.config('SET', 'notify-keyspace-events', 'KEA'); 37 | } catch(e) { 38 | `Could not set keyspace notifications for client at port ${PORT}: ${e}`; 39 | } 40 | 41 | client.on('error', (error) => { 42 | console.log(`Redis client error occured (port ${PORT}): ${error} 43 | Please ensure you have a redis server running on this port. 44 | bash: redis-server --port ${PORT}`); 45 | }); 46 | 47 | clients.push(client); 48 | } 49 | 50 | return clients 51 | 52 | } 53 | 54 | module.exports = createClients; -------------------------------------------------------------------------------- /dist/server/controllers/connectionsController.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | var redis_monitors_1 = __importDefault(require("../redis-monitors/redis-monitors")); 7 | var connectionsController = { 8 | getAllConnections: function (req, res, next) { 9 | var connections = { 10 | instances: [] 11 | }; 12 | try { 13 | redis_monitors_1.default.forEach(function (redisMonitor, idx) { 14 | var instance = { 15 | instanceId: idx + 1, 16 | host: redisMonitor.host, 17 | port: redisMonitor.port, 18 | url: redisMonitor.url, 19 | databases: redisMonitor.databases, 20 | recordKeyspaceHistoryFrequency: redisMonitor.recordKeyspaceHistoryFrequency, 21 | maxKeyspaceHistoryCount: redisMonitor.maxKeyspaceHistoryCount, 22 | eventGraphRefreshFrequency: redisMonitor.eventGraphRefreshFrequency, 23 | maxEventLogSize: redisMonitor.maxEventLogSize, 24 | notifyKeyspaceEvents: redisMonitor.notifyKeyspaceEvents 25 | }; 26 | connections.instances.push(instance); 27 | }); 28 | } 29 | catch (err) { 30 | return next({ log: 'Could not read connections from RedisMonitor' }); 31 | } 32 | res.locals.connections = connections; 33 | return next(); 34 | } 35 | }; 36 | exports.default = connectionsController; 37 | -------------------------------------------------------------------------------- /dist/server/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | var express_1 = __importDefault(require("express")); 7 | var path_1 = __importDefault(require("path")); 8 | var app = express_1.default(); 9 | var connectionsRouter_1 = __importDefault(require("./routers/connectionsRouter")); 10 | var eventsRouter_1 = __importDefault(require("./routers/eventsRouter")); 11 | var keyspacesRouter_1 = __importDefault(require("./routers/keyspacesRouter")); 12 | var PORT = +process.env.PORT || 3000; 13 | app.use('/api/connections', connectionsRouter_1.default); 14 | app.use('/api/v2/events', eventsRouter_1.default); 15 | app.use('/api/v2/keyspaces', keyspacesRouter_1.default); 16 | app.get('/dist/bundle.js', function (req, res) { 17 | res.status(200).sendFile(path_1.default.resolve(__dirname, '../bundle.js')); 18 | }); 19 | app.get('/', function (req, res) { 20 | res.status(200).sendFile(path_1.default.resolve(__dirname, "./assets/index.html")); 21 | }); 22 | app.use('*', function (req, res) { 23 | res.sendStatus(404); 24 | }); 25 | app.use(function (err, req, res, next) { 26 | var defaultErr = { 27 | log: "Unknown Express middleware occured", 28 | status: 500, 29 | message: { error: "Oops, something went wrong!" }, 30 | }; 31 | err = Object.assign(defaultErr, err); 32 | console.log("Server error encountered: " + err.log); 33 | res.status(defaultErr.status).json(defaultErr.message); 34 | }); 35 | exports.default = app.listen(PORT, function () { 36 | console.log("Listening on port " + PORT); 37 | }); 38 | -------------------------------------------------------------------------------- /src/server/server.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import path from "path"; 3 | const app = express(); 4 | 5 | import connectionsRouter from './routers/connectionsRouter'; 6 | import eventsRouter from './routers/eventsRouter'; 7 | import keyspacesRouter from './routers/keyspacesRouter'; 8 | 9 | const PORT = +process.env.PORT || 3000; 10 | 11 | app.use('/api/connections', connectionsRouter); 12 | app.use('/api/v2/events', eventsRouter); 13 | app.use('/api/v2/keyspaces', keyspacesRouter); 14 | 15 | app.get('/dist/bundle.js', (req: express.Request, res: express.Response): void => { 16 | res.status(200).sendFile(path.resolve(__dirname, '../bundle.js')); 17 | }) 18 | 19 | app.get('/', (req: express.Request, res: express.Response): void => { 20 | res.status(200).sendFile(path.resolve(__dirname, "./assets/index.html")); 21 | }); 22 | 23 | app.use('*', (req: express.Request, res: express.Response): void => { 24 | res.sendStatus(404); 25 | }) 26 | 27 | interface GlobalError { 28 | log: string; 29 | status?: number; 30 | message?: { 31 | error: string; 32 | }; 33 | } 34 | 35 | app.use( 36 | ( 37 | err: GlobalError, 38 | req: express.Request, 39 | res: express.Response, 40 | next: express.NextFunction 41 | ): void => { 42 | 43 | const defaultErr = { 44 | log: "Unknown Express middleware occured", 45 | status: 500, 46 | message: { error: "Oops, something went wrong!" }, 47 | }; 48 | 49 | err = Object.assign(defaultErr, err); 50 | console.log(`Server error encountered: ${err.log}`); 51 | res.status(defaultErr.status).json(defaultErr.message); 52 | } 53 | ); 54 | 55 | export default app.listen(PORT, (): void => { 56 | console.log(`Listening on port ${PORT}`); 57 | }); 58 | 59 | -------------------------------------------------------------------------------- /src/client/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | // export object of action names: keyname: name, value: action 2 | /** 3 | * ************************************ 4 | * 5 | * @module actionTypes.js 6 | * @author 7 | * @date 8 | * @description Action Type Constants 9 | * 10 | * ************************************ 11 | */ 12 | 13 | // 14 | export const UPDATE_INSTANCEINFO = 'UPDATE_INSTANCEINFO'; 15 | // 16 | 17 | // 18 | export const SWITCH_DATABASE = 'SWITCH_DATABASE'; 19 | // 20 | export const SWITCH_INSTANCE = 'SWITCH_INSTANCE'; 21 | 22 | //KEYSPACE TABLE 23 | //THIS WILL BE USED FOR FILTERING AND PAGINATION - THERE NEEDS TO BE A CONDITION WHETHER THE PAGINATION HAS A FILTER OR NOT 24 | export const CHANGE_KEYSPACE_PAGE = 'CHANGE_KEYSPACE_PAGE'; 25 | // THIS WILL BE USED FOR REFRESHING THE KEYSPACE - CONSIDER WITH OR WITHOUT FILTERS IN THE QUERY PARAMS. 26 | export const REFRESH_KEYSPACE = 'REFRESH_KEYSPACE'; 27 | // THIS WILL ONLY BE USED IN THE APP.JSX ON LOAD - WILL RECEIVE ALL KEYSPACES FOR ALL DATABASES FOR ALL INSTANCES 28 | export const LOAD_KEYSPACE = 'LOAD_KEYSPACE'; 29 | // 30 | 31 | // EVENTS TABLE 32 | export const LOAD_ALL_EVENTS = 'LOAD_ALL_EVENTS'; 33 | // 34 | export const CHANGE_EVENTS_PAGE = 'CHANGE_EVENTS_PAGE'; 35 | // 36 | export const REFRESH_EVENTS = 'REFRESH_EVENTS'; 37 | // 38 | 39 | // EVENTS GRAPH 40 | export const GET_EVENT_TOTALS = 'GET_EVENT_TOTALS'; 41 | // 42 | export const GET_NEXT_EVENTS = 'GET_NEXT_EVENTS'; 43 | // 44 | 45 | // 46 | export const UPDATE_KEYGRAPH = 'UPDATE_KEYGRAPH'; 47 | // 48 | 49 | export const UPDATE_PAGE = 'UDPATE_PAGE'; 50 | // 51 | export const UPDATE_CURRDISPLAY = 'UPDATE_CURRDISPLAY'; 52 | // 53 | export const UPDATE_PAGENUM = 'UPDATE_PAGENUM'; 54 | // 55 | export const UPDATE_PAGESIZE = 'UPDATE_PAGESIZE'; 56 | // 57 | -------------------------------------------------------------------------------- /src/client/components/styles/instances.scss: -------------------------------------------------------------------------------- 1 | @import './config/_variables'; 2 | 3 | .instance-nav-container { 4 | border-right: 3px solid $PRIMARY_LIGHT; 5 | width: 23%; 6 | 7 | .instance-nav-header { 8 | 9 | @media screen and (max-width: $small) { 10 | font-size: 1.5em; 11 | } 12 | 13 | @media screen and (min-width: $large) { 14 | font-size: 1.875vw; 15 | } 16 | 17 | text-align: center; 18 | } 19 | 20 | .instance-container, .selected-instance-container { 21 | padding: 5% 10%; 22 | margin: 0 5%; 23 | 24 | .instance-display-text:hover { 25 | cursor: pointer; 26 | text-decoration: underline; 27 | } 28 | 29 | .instance-display-text { 30 | 31 | margin: 3% 0; 32 | 33 | @media screen and (max-width: $small) { 34 | font-size: 1em; 35 | } 36 | 37 | @media screen and (min-width: $large) { 38 | font-size: 1.25vw 39 | } 40 | } 41 | } 42 | 43 | .selected-instance-container { 44 | background-color: lighten($PRIMARY_DARK, 10%); 45 | } 46 | } 47 | 48 | .database-selector-container { 49 | 50 | .database-selector-prompt { 51 | 52 | @media screen and (max-width: $small) { 53 | font-size: 0.8em; 54 | } 55 | 56 | @media screen and (min-width: $large) { 57 | font-size: 1vw; 58 | } 59 | 60 | } 61 | 62 | .database-selector { 63 | background-color: $PRIMARY_LIGHT; 64 | color: $PRIMARY_DARK; 65 | border-radius: 5px; 66 | border: 1px solid $PRIMARY_LIGHT; 67 | outline: none; 68 | cursor: pointer; 69 | 70 | @media screen and (max-width: $small) { 71 | font-size: 1em; 72 | } 73 | 74 | @media screen and (min-width: $large) { 75 | font-size: 1.25vw; 76 | } 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /dist/server/routers/eventsRouter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | var express_1 = __importDefault(require("express")); 7 | var monitorsController_1 = __importDefault(require("../controllers/monitorsController")); 8 | var eventsController_1 = __importDefault(require("../controllers/eventsController")); 9 | var router = express_1.default.Router(); 10 | router.get('/', monitorsController_1.default.findAllMonitors, eventsController_1.default.refreshEventLog, eventsController_1.default.getEventsPages, function (req, res) { 11 | res.status(200).json(res.locals.events); 12 | }); 13 | router.get('/:instanceId', monitorsController_1.default.findSingleMonitor, eventsController_1.default.refreshEventLog, eventsController_1.default.getEventsPages, function (req, res) { 14 | res.status(200).json(res.locals.events); 15 | }); 16 | router.get('/:instanceId/:dbIndex', monitorsController_1.default.findSingleMonitor, eventsController_1.default.refreshEventLog, eventsController_1.default.getEventsPages, function (req, res) { 17 | res.status(200).json(res.locals.events); 18 | }); 19 | router.get('/totals/:instanceId/:dbIndex', monitorsController_1.default.findSingleMonitor, eventsController_1.default.validateRequestType, function (req, res, next) { 20 | if (req.query.timeInterval) 21 | eventsController_1.default.getEventsByTimeInterval(req, res, next); 22 | else if (req.query.eventTotal) 23 | eventsController_1.default.getSingleEventsTotal(req, res, next); 24 | }, function (req, res) { 25 | res.status(200).json(res.locals.eventTotals); 26 | }); 27 | exports.default = router; 28 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { HotModuleReplacementPlugin } = require('webpack'); 3 | const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); 4 | const webpack = require('webpack'); 5 | const isDevelopment = process.env.NODE_ENV !== 'production'; 6 | 7 | module.exports = { 8 | entry: './src/client/index.js', 9 | output: { 10 | filename: 'bundle.js', 11 | path: path.resolve(__dirname, './dist'), 12 | }, 13 | mode: isDevelopment ? 'development' : 'production', 14 | devServer: { 15 | publicPath: '/dist/', 16 | contentBase: './src/server/assets', 17 | port: 8080, 18 | proxy: { 19 | '/api': 'http://localhost:3000/', 20 | }, 21 | hot: true, 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.(js|jsx)$/, 27 | exclude: /node_modules/, 28 | use: { 29 | loader: 'babel-loader', 30 | options: { 31 | presets: ['@babel/preset-env', '@babel/preset-react'], 32 | plugins: [ 33 | isDevelopment && require.resolve('react-refresh/babel'), 34 | ].filter(Boolean), 35 | }, 36 | }, 37 | }, 38 | { 39 | test: /\.s[ac]ss$/, 40 | use: ['style-loader', 'css-loader', 'sass-loader'], 41 | }, 42 | { 43 | test: /\.css$/, 44 | use: ['style-loader', 'css-loader'], 45 | }, 46 | { 47 | test: /\.svg/, 48 | use: { 49 | loader: "@svgr/webpack", 50 | options: {}, 51 | }, 52 | }, 53 | ], 54 | }, 55 | plugins: [ 56 | isDevelopment && new HotModuleReplacementPlugin(), 57 | isDevelopment && new ReactRefreshWebpackPlugin(), 58 | new webpack.ProvidePlugin({ 59 | process: 'process/browser', 60 | }), 61 | ].filter(Boolean), 62 | }; 63 | -------------------------------------------------------------------------------- /demo-redis-app/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Imports functionality that mocks Redis commands and executes them at a specified interval. 3 | */ 4 | 5 | const mock = require('./utils/mockCommands.js'); 6 | const mockSettings = require('./utils/mockCommandSettings.js'); 7 | const createClients = require('./redis-config/clientConfig.js'); 8 | 9 | const startDemoApp = async () => { 10 | 11 | try { 12 | redisClients = await createClients(); 13 | } catch (e) { 14 | console.log(`Error occured creating Redis Clients: ${e}`); 15 | } 16 | 17 | try { 18 | redisClients.forEach(client => { 19 | setInterval(mock.setString, mockSettings.STRING_SET_FREQUENCY, client); 20 | setInterval(mock.getString, mockSettings.STRING_GET_FREQUENCY, client); 21 | setInterval(mock.delString, mockSettings.STRING_DELETE_FREQUENCY, client); 22 | 23 | setInterval(mock.lpushList, mockSettings.LIST_LPUSH_FREQUENCY, client); 24 | setInterval(mock.lrangeList, mockSettings.LIST_LRANGE_FREQUENCY, client); 25 | setInterval(mock.lpopList, mockSettings.LIST_LPOP_FREQUENCY, client); 26 | 27 | setInterval(mock.saddSet, mockSettings.SET_SADD_FREQUENCY, client); 28 | setInterval(mock.smembersSet, mockSettings.SET_SMEMBERS_FREQUENCY, client); 29 | setInterval(mock.spopSet, mockSettings.SET_SPOP_FREQUENCY, client); 30 | 31 | setInterval(mock.zaddSortedSet, mockSettings.SORTEDSET_ZADD_FREQUENCY, client); 32 | setInterval(mock.zrangeSortedSet, mockSettings.SORTEDSET_ZRANGE_FREQUENCY, client); 33 | setInterval(mock.zpopminSortedSet, mockSettings.SORTEDSET_ZPOPMIN_FREQUENCY, client); 34 | 35 | setInterval(mock.hmsetHash, mockSettings.HASH_HMSET_FREQUENCY, client); 36 | setInterval(mock.hgetallHash, mockSettings.HASH_HGETALL_FREQUENCY, client); 37 | setInterval(mock.delHash, mockSettings.HASH_DEL_FREQUENCY, client); 38 | }); 39 | } catch(e) { 40 | console.log(e); 41 | } 42 | 43 | }; 44 | 45 | startDemoApp(); 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Gatsby files 82 | .cache/ 83 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 84 | # https://nextjs.org/blog/next-9-1#public-directory-support 85 | # public 86 | 87 | # Serverless directories 88 | .serverless/ 89 | 90 | # FuseBox cache 91 | .fusebox/ 92 | 93 | # DynamoDB Local files 94 | .dynamodb/ 95 | 96 | # TernJS port file 97 | .tern-port 98 | 99 | .DS_Store 100 | 101 | #Ignore redis dumps 102 | dump.rdb -------------------------------------------------------------------------------- /src/client/components/styles/filternav.scss: -------------------------------------------------------------------------------- 1 | @import './config/_variables.scss'; 2 | 3 | .filterNavContainer { 4 | display: flex; 5 | margin: 1em 0 2em; 6 | display: flex; 7 | justify-content: space-between; 8 | } 9 | 10 | .search-filters { 11 | width: 70%; 12 | display: flex; 13 | flex-direction: column; 14 | 15 | .MuiInputBase-input { 16 | color: $PRIMARY_LIGHT; 17 | font-family: $PRIMARY_FONT; 18 | } 19 | 20 | .MuiInputLabel-root { 21 | color: $PRIMARY_LIGHT; 22 | font-family: $PRIMARY_FONT; 23 | 24 | @media screen and (max-width: $small) { 25 | font-size: 0.8em; 26 | } 27 | @media screen and (min-width: $large) { 28 | font-size: 1vw; 29 | } 30 | } 31 | 32 | .MuiSelect-icon { 33 | color: $PRIMARY_LIGHT; 34 | } 35 | 36 | .filter-buttons-container { 37 | 38 | @media screen and (max-width: $small) { 39 | margin: 0.5em 0 0; 40 | } 41 | @media screen and (min-width: $large) { 42 | margin: 0.65vw 0 0; 43 | } 44 | 45 | button { 46 | font-family: $PRIMARY_FONT; 47 | color: $PRIMARY_LIGHT; 48 | 49 | @media screen and (max-width: $small) { 50 | margin: 0 3em; 51 | font-size: 0.9em; 52 | // border: 1px solid $PRIMARY_LIGHT; 53 | } 54 | @media screen and (min-width: $large) { 55 | margin: 0 3.75vw; 56 | font-size: 1.12vw; 57 | } 58 | 59 | } 60 | } 61 | 62 | .MuiInput-input { 63 | border-bottom: 1px solid $PRIMARY_LIGHT; 64 | } 65 | 66 | } 67 | 68 | #filter-button-container { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | width: 30%; 73 | } 74 | 75 | #refreshButton { 76 | height: 20%; 77 | align-self: center; 78 | background-color: $PRIMARY_RED; 79 | border: 1px solid $PRIMARY_RED; 80 | border-radius: 5px; 81 | font-family: $PRIMARY_FONT; 82 | 83 | @media screen and (max-height: $small) { 84 | font-size: 0.8em; 85 | } 86 | 87 | @media screen and (max-height: $large) { 88 | font-size: 1.0vw; 89 | } 90 | } 91 | 92 | #refreshButton:hover { 93 | cursor: pointer; 94 | } -------------------------------------------------------------------------------- /demo-redis-app/utils/mockCommandData.js: -------------------------------------------------------------------------------- 1 | /* 2 | Provides functionality for generating mock keys and mock values for in-scope Redis data types 3 | 4 | Used by /utils/mock-commands.js 5 | */ 6 | 7 | const faker = require("faker"); 8 | 9 | const mockData = {}; 10 | 11 | /* <<<<< Strings >>>>> */ 12 | mockData.strings = { 13 | //Generates keys with corresponding string values. Uses "messages" as a mocked use-case for strings. 14 | 15 | createKey() { 16 | return `message:${Math.floor(Math.random() * 100000)}`; 17 | }, 18 | 19 | createValue() { 20 | return faker.lorem.sentence(); 21 | }, 22 | }; 23 | 24 | /* <<<<< Lists >>>>> */ 25 | mockData.lists = { 26 | createKey() { 27 | return `names:${Math.floor(Math.random() * 10000)}`; 28 | }, 29 | 30 | createValue() { 31 | 32 | let list = []; 33 | for (let i = 0; i < 5; i++) { 34 | list.push(faker.name.firstName()); 35 | } 36 | return list; 37 | }, 38 | }; 39 | 40 | /* <<<<< Sets >>>>> */ 41 | mockData.sets = { 42 | createKey() { 43 | return `tech:${Math.floor(Math.random() * 10000)}`; 44 | }, 45 | 46 | createValue() { 47 | 48 | const set = new Set(); 49 | for (let i = 0; i < 5; i++) { 50 | set.add(faker.company.bs()); 51 | } 52 | return Array.from(set); 53 | } 54 | }; 55 | 56 | /* <<<<< Sorted Sets >>>>> */ 57 | mockData.sortedSets = { 58 | createKey() { 59 | return `leaders:${Math.floor(Math.random() * 10000)}`; 60 | }, 61 | 62 | createValue() { 63 | 64 | const zset = []; 65 | 66 | for (let i = 0; i < 5; i++) { 67 | zset.push(Math.floor(Math.random()) * 1000); 68 | zset.push(faker.internet.userName()); 69 | } 70 | 71 | return zset; 72 | } 73 | }; 74 | 75 | /* <<<<< Hashes >>>>> */ 76 | mockData.hashes = { 77 | createKey() { 78 | return `user:${Math.floor(Math.random() * 100000)}`; 79 | }, 80 | 81 | createValue() { 82 | 83 | const user = { 84 | username: faker.internet.userName(), 85 | email: faker.internet.email(), 86 | country: faker.address.country() 87 | } 88 | 89 | return user 90 | }, 91 | }; 92 | 93 | module.exports = mockData; 94 | -------------------------------------------------------------------------------- /dist/server/routers/historiesRouter.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var historiesRouter = express.Router(); 3 | var eventsMonitors = require('../redis-monitors/redis-monitors'); 4 | historiesRouter.get('/', function (req, res, next) { 5 | var body = { data: [] }; 6 | eventsMonitors.forEach(function (monitor) { 7 | var instance = { 8 | instanceId: monitor.instanceId, 9 | keyspaces: [] 10 | }; 11 | monitor.keyspaces.forEach(function (keyspace) { 12 | var historiesCount = req.query.historiesCount; 13 | instance.keyspaces.push(keyspace.keyspaceHistoriesLog.returnLogAsArray((historiesCount) ? historiesCount : 0)); 14 | }); 15 | body.data.push(instance); 16 | }); 17 | res.status(200).json(body); 18 | }); 19 | historiesRouter.get('/:instanceId/', function (req, res, next) { 20 | var body = { data: [] }; 21 | var instanceId = req.params.instanceId; 22 | var monitor = eventsMonitors.find(function (m) { 23 | return m.instanceId === +instanceId; 24 | }); 25 | var instance = { 26 | instanceId: monitor.instanceId, 27 | keyspaces: [] 28 | }; 29 | monitor.keyspaces.forEach(function (keyspace) { 30 | var historiesCount = req.query.historiesCount; 31 | instance.keyspaces.push(keyspace.keyspaceHistoriesLog.returnLogAsArray((historiesCount) ? historiesCount : 0)); 32 | }); 33 | body.data.push(instance); 34 | res.status(200).json(body); 35 | }); 36 | historiesRouter.get('/:instanceId/:dbIndex', function (req, res, next) { 37 | var body = { data: [] }; 38 | var _a = req.params, instanceId = _a.instanceId, dbIndex = _a.dbIndex; 39 | var monitor = eventsMonitors.find(function (m) { 40 | return m.instanceId === +instanceId; 41 | }); 42 | var instance = { 43 | instanceId: monitor.instanceId, 44 | keyspaces: [] 45 | }; 46 | var eventTotal = req.query.eventTotal; 47 | instance.keyspaces.push(monitor.keyspaces[dbIndex].eventLog.returnLogAsArray((eventTotal) ? eventTotal : 0)); 48 | body.data.push(instance); 49 | res.status(200).json(body); 50 | }); 51 | module.exports = historiesRouter; 52 | -------------------------------------------------------------------------------- /src/client/reducers/keyspaceReducer.js: -------------------------------------------------------------------------------- 1 | //leave these separate for future developers in case they want to add functionality 2 | import * as types from '../actions/actionTypes.js'; 3 | 4 | const initialState = { 5 | currInstance: 1, 6 | currDatabase: 0, 7 | keyspace: [ 8 | { 9 | instanceId: 1, 10 | keyspaces: [ 11 | { 12 | keyTotal: 1, 13 | pageSize: 25, 14 | pageNum: 4, 15 | data: [ 16 | { 17 | key: 'loading', 18 | type: 'loading', 19 | value: 'loading', 20 | }, 21 | ], 22 | }, 23 | ], 24 | }, 25 | ], 26 | }; 27 | 28 | const keyspaceReducer = (state = initialState, action) => { 29 | let keyspace; 30 | 31 | switch (action.type) { 32 | case types.LOAD_KEYSPACE: { 33 | const fullKeyspace = action.payload.keyspace; 34 | keyspace = state.keyspace.slice(); 35 | keyspace = fullKeyspace; 36 | 37 | return { 38 | ...state, 39 | keyspace, 40 | }; 41 | } 42 | case types.REFRESH_KEYSPACE: { 43 | //this is for a particular database in a particular instance 44 | console.log( 45 | 'action payload in REFRESH_KEYSPACE keyspace reducer', 46 | action.payload 47 | ); 48 | const specificKeyspace = action.payload.keyspace; 49 | const currInstance = action.payload.currInstance; 50 | const currDatabase = action.payload.currDatabase; 51 | 52 | let updateKeyspace = state.keyspace.slice(); 53 | updateKeyspace[currInstance - 1].keyspaces[currDatabase] = 54 | specificKeyspace; 55 | //do I need to update the database and isntance? 56 | return { 57 | ...state, 58 | keyspace: updateKeyspace, 59 | }; 60 | } 61 | case types.CHANGE_KEYSPACE_PAGE: { 62 | const specificKeyspace = action.payload.keyspace; 63 | const currInstance = action.payload.currInstance; 64 | const currDatabase = action.payload.currDatabase; 65 | 66 | keyspace = state.keyspace.slice(); 67 | keyspace[currInstance - 1].keyspaces[currDatabase] = specificKeyspace; 68 | //do I need to update the database and instance? 69 | return { 70 | ...state, 71 | keyspace, 72 | }; 73 | } 74 | 75 | default: { 76 | return state; 77 | } 78 | } 79 | }; 80 | 81 | export default keyspaceReducer; 82 | -------------------------------------------------------------------------------- /src/client/components/graphs/KeyspaceChartFilterNav.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import KeyspaceChartFilter from "./KeyspaceChartFilter.jsx"; 3 | import "../styles/graphfilters.scss"; 4 | 5 | class KeyspaceChartFilterNav extends Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | render() { 10 | // console.log("this.props in KEYSPACECHARTFILTERNAV", this.props); 11 | return ( 12 |
13 | 27 |
30 | 43 | 59 |
60 |
61 | ); 62 | } 63 | } 64 | 65 | export default KeyspaceChartFilterNav; 66 | -------------------------------------------------------------------------------- /src/client/action-creators/connections.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/actionTypes'; 2 | 3 | //NEEDS COMPLETION ONCE WE GET WHAT THE DATA WILL LOOK LIKE FROM BACKEND 4 | export const updateKeyGraphActionCreator = 5 | (instanceId, dbIndex) => (dispatch) => { 6 | fetch(`/api/keyspaceHistory/${instanceId}/${dbIndex}`) 7 | .then((res) => res.json()) 8 | .then((data) => { 9 | const keyspaceHistory = data.keyspaceHistory; 10 | dispatch({ 11 | type: types.UPDATE_KEYGRAPH, 12 | payload: keyspaceHistory, 13 | }); 14 | }) 15 | .catch((err) => { 16 | console.log('error in keyspaceUpdateActionCreator: ', err); 17 | }); 18 | }; 19 | 20 | //SWITCH DATABASE 21 | export const switchDatabaseActionCreator = (dbIndex) => ( 22 | console.log('switched to database', dbIndex), 23 | { 24 | type: types.SWITCH_DATABASE, 25 | payload: dbIndex, 26 | } 27 | ); 28 | 29 | //SWITCH INSTANCE action creator 30 | 31 | export const switchInstanceActionCreator = (instanceId) => ( 32 | console.log('switched to instance', instanceId), 33 | { 34 | type: types.SWITCH_INSTANCE, 35 | payload: instanceId, 36 | } 37 | ); 38 | 39 | //payload: 40 | // databases: 16 41 | // host: "127.0.0.1" 42 | // instanceId: 1 43 | // port: 6379 44 | 45 | export const updateInstanceInfoActionCreator = () => (dispatch) => { 46 | fetch('/api/connections') 47 | .then((res) => res.json()) 48 | .then((data) => { 49 | //for stretch features, there may be multiple instances here 50 | dispatch({ 51 | type: types.UPDATE_INSTANCEINFO, 52 | payload: data.instances, 53 | }); 54 | }) 55 | .catch((err) => { 56 | console.log( 57 | 'error fetching instanceinfo in updateDBInfoActionCreator:', 58 | err 59 | ); 60 | }); 61 | }; 62 | 63 | export const updatePageActionCreator = (newPage) => ({ 64 | type: types.UPDATE_PAGE, 65 | payload: newPage, 66 | }); 67 | 68 | // { 69 | // filterType: something 70 | // filterValue: something 71 | // } 72 | export const updateCurrDisplayActionCreator = (object) => ({ 73 | type: types.UPDATE_CURRDISPLAY, 74 | payload: object, 75 | }); 76 | 77 | export const updatePageNumActionCreator = (pageNum) => ({ 78 | type: types.UPDATE_PAGENUM, 79 | payload: pageNum, 80 | }); 81 | 82 | export const updatePageSizeActionCreator = (pageSize) => ({ 83 | type: types.UPDATE_PAGESIZE, 84 | payload: pageSize, 85 | }); 86 | -------------------------------------------------------------------------------- /src/client/components/keyspace/KeyspaceComponent.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import KeyspaceTable from './KeyspaceTable.jsx'; 4 | import * as actions from '../../action-creators/connections'; 5 | import * as keyspaceActions from '../../action-creators/keyspaceConnections'; 6 | 7 | const mapStateToProps = (store) => { 8 | return { 9 | currInstance: store.currInstanceStore.currInstance, 10 | currDatabase: store.currDatabaseStore.currDatabase, 11 | keyspace: store.keyspaceStore.keyspace, 12 | currDisplay: store.currDisplayStore.currDisplay, 13 | pageSize: store.dataPageStore.pageSize, 14 | pageNum: store.dataPageStore.pageNum, 15 | }; 16 | }; 17 | const mapDispatchToProps = (dispatch) => ({ 18 | updatePageSize: (pageSize) => 19 | dispatch(actions.updatePageSizeActionCreator(pageSize)), 20 | updatePageNum: (pageNum) => 21 | dispatch(actions.updatePageNumActionCreator(pageNum)), 22 | changeKeyspacePage: (instanceId, dbIndex, queryOptions) => 23 | dispatch( 24 | keyspaceActions.changeKeyspacePageActionCreator( 25 | instanceId, 26 | dbIndex, 27 | queryOptions 28 | ) 29 | ), 30 | }); 31 | 32 | class KeyspaceComponent extends Component { 33 | constructor(props) { 34 | super(props); 35 | } 36 | 37 | render() { 38 | // console.log('all the props in keyspace component', this.props); 39 | 40 | const currDatabaseData = this.props.keyspace[this.props.currInstance - 1] 41 | .keyspaces[this.props.currDatabase]; 42 | 43 | const myCount = currDatabaseData ? currDatabaseData.keyTotal : 0; 44 | return ( 45 |
49 | 61 |
62 | ); 63 | } 64 | } 65 | 66 | export default connect(mapStateToProps, mapDispatchToProps)(KeyspaceComponent); 67 | -------------------------------------------------------------------------------- /demo-redis-app/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo-redis-app", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "denque": { 8 | "version": "1.5.0", 9 | "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz", 10 | "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==" 11 | }, 12 | "faker": { 13 | "version": "5.5.3", 14 | "resolved": "https://registry.npmjs.org/faker/-/faker-5.5.3.tgz", 15 | "integrity": "sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g==" 16 | }, 17 | "promise-queue": { 18 | "version": "2.2.5", 19 | "resolved": "https://registry.npmjs.org/promise-queue/-/promise-queue-2.2.5.tgz", 20 | "integrity": "sha1-L29ffA9tCBCelnZZx5uIqe1ek7Q=" 21 | }, 22 | "redis": { 23 | "version": "3.1.2", 24 | "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz", 25 | "integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==", 26 | "requires": { 27 | "denque": "^1.5.0", 28 | "redis-commands": "^1.7.0", 29 | "redis-errors": "^1.2.0", 30 | "redis-parser": "^3.0.0" 31 | } 32 | }, 33 | "redis-commands": { 34 | "version": "1.7.0", 35 | "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", 36 | "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==" 37 | }, 38 | "redis-errors": { 39 | "version": "1.2.0", 40 | "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", 41 | "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=" 42 | }, 43 | "redis-parser": { 44 | "version": "3.0.0", 45 | "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", 46 | "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", 47 | "requires": { 48 | "redis-errors": "^1.0.0" 49 | } 50 | }, 51 | "redis-server": { 52 | "version": "1.2.2", 53 | "resolved": "https://registry.npmjs.org/redis-server/-/redis-server-1.2.2.tgz", 54 | "integrity": "sha512-pOaSIeSMVFkEFIuaMtpQ3TOr3uI4sUmEHm4ofGks5vTPRseHUszxyIlC70IFjUR9qSeH8o/ARZEM8dqcJmgGJw==", 55 | "requires": { 56 | "promise-queue": "^2.2.5" 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/client/components/graphs/GraphHolder.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { 4 | XYPlot, 5 | XAxis, 6 | YAxis, 7 | HorizontalGridLines, 8 | LineSeries, 9 | } from "react-vis"; 10 | 11 | const GraphHolder = (props) => { 12 | 13 | const graphDataConverter = (array, initTime) => { 14 | const eventTimesArray = []; 15 | let temp = []; 16 | let xRange = 0; 17 | let yRange = 5000; 18 | console.log("array in graphDataConverter", array); 19 | for (let i = 0; i < array.length; i++) { 20 | if ( 21 | array[i].timestamp - initTime >= xRange && 22 | array[i].timestamp - initTime < yRange 23 | ) { 24 | let date = new Date(array[i].timestamp).toString("MMM, dd"); 25 | temp.push({ 26 | name: array[i].key, 27 | time: array[i].timestamp, 28 | timeInMsSinceStart: array[i].timestamp - initTime, 29 | formattedTime: date.slice(16, 24), 30 | }); 31 | } else { 32 | if (temp.length < 2) break; 33 | else { 34 | eventTimesArray.push(temp); 35 | let date = new Date(array[i].timestamp).toString("MMM, dd"); 36 | temp = [ 37 | { 38 | name: array[i].key, 39 | time: array[i].timestamp, 40 | timeInMsSinceStart: array[i].timestamp - initTime, 41 | formattedTime: date.slice(16, 24), 42 | }, 43 | ]; 44 | xRange += 5000; 45 | yRange += 5000; 46 | } 47 | } 48 | } 49 | 50 | const result = []; 51 | console.log("eventtimesarray", eventTimesArray); 52 | // if (props.currDatabase === 0) { 53 | eventTimesArray.forEach((array) => { 54 | // console.log("time", time); 55 | result.push({ x: array[0].formattedTime, y: array.length }); 56 | }); 57 | // } 58 | return result; 59 | }; 60 | 61 | const initialTime = props.events[props.currDatabase][0].timestamp; 62 | const eventsArray = props.events[props.currDatabase]; 63 | console.log("intialTime", initialTime, "eventsArray", eventsArray); 64 | const plotData = graphDataConverter(eventsArray, initialTime); 65 | console.log("plotData", plotData); 66 | 67 | return ( 68 | 73 | 74 | 75 | 76 | 77 | 78 | ); 79 | }; 80 | 81 | export default GraphHolder; 82 | -------------------------------------------------------------------------------- /src/server/controllers/utils.ts: -------------------------------------------------------------------------------- 1 | import { RedisClient } from 'redis'; 2 | import { Keyspace, KeyDetails } from './interfaces'; 3 | import { promisify } from 'util'; 4 | 5 | const getValue = async (key: string, type: string, redisClient: RedisClient): Promise => { 6 | 7 | let value; 8 | switch (type) { 9 | 10 | case 'string': { 11 | //@ts-ignore - incorrect type errors for promisified method's return value 12 | value = await redisClient.get(key); 13 | // console.log(value); 14 | }; break; 15 | 16 | case 'list': { 17 | //@ts-ignore - incorrect type errors for promisified method's return value 18 | value = await redisClient.lrange(key, 0, -1); 19 | }; break; 20 | 21 | case 'set': { 22 | //@ts-ignore - incorrect type errors for promisified method's return value 23 | value = await redisClient.smembers(key); 24 | }; break; 25 | 26 | case 'zset': { 27 | //@ts-ignore - incorrect type errors for promisified method's return value 28 | //note: will need to include a range with the key to return all values in 29 | //the sorted set 30 | value = await redisClient.zrange(key, 0, - 1); 31 | }; break; 32 | 33 | case 'hash': { 34 | //@ts-ignore - incorrect type errors for promisified method's return value 35 | value = await redisClient.hgetall(key); 36 | }; break; 37 | 38 | 39 | }; 40 | 41 | 42 | return value; 43 | 44 | } 45 | 46 | export const getKeyspace = async (redisClient: RedisClient, dbIdx: number): Promise => { 47 | 48 | const res: KeyDetails[] = []; 49 | 50 | await redisClient.select(dbIdx); 51 | 52 | //@ts-ignore - incorrect type errors for promisified method's return value 53 | let scanResults: [string, string[]] = await redisClient.scan('0', 'COUNT', '100'); 54 | let cursor: string = scanResults[0]; 55 | let keys: string[] = scanResults[1]; 56 | 57 | do { 58 | 59 | for (let key of keys) { 60 | //@ts-ignore - incorrect type errors for promisified method's return value 61 | const type: string = await redisClient.type(key); 62 | const value = await getValue(key, type, redisClient); 63 | 64 | res.push({ 65 | key: key, 66 | type: type, 67 | value: value 68 | }) 69 | } 70 | //@ts-ignore - incorrect type errors for promisified method's return value 71 | scanResults = await redisClient.scan(cursor, 'COUNT', '100'); 72 | cursor = scanResults[0]; 73 | keys = scanResults[1]; 74 | 75 | } while (cursor !== '0'); 76 | 77 | return res; 78 | }; -------------------------------------------------------------------------------- /src/client/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { BrowserRouter, Route, Switch } from "react-router-dom"; 3 | import FilterNav from "./navbars/FilterNav.jsx"; 4 | import PageNav from "./navbars/PageNav.jsx"; 5 | import InstanceNav from "./navbars/InstanceNav.jsx"; 6 | import KeyspaceComponent from "./keyspace/KeyspaceComponent.jsx"; 7 | import GraphComponent from "./graphs/GraphComponent.jsx"; 8 | import EventComponent from "./events/EventComponent.jsx"; 9 | import { connect } from "react-redux"; 10 | import * as actions from "../action-creators/connections"; 11 | import * as keyspaceActions from "../action-creators/keyspaceConnections"; 12 | import * as eventActions from "../action-creators/eventsConnections"; 13 | import "../../../node_modules/react-vis/dist/style.css"; 14 | import Logo from '../redishawk-logo.svg'; 15 | 16 | import './styles/app.global.scss'; 17 | 18 | ///still need to check dispatchers here 19 | 20 | //not using this right now 21 | // const mapStateToProps = (store) => { 22 | // return { 23 | // database: store.currDatabaseStore.currDatabase, 24 | // }; 25 | // }; 26 | 27 | const mapDispatchToProps = (dispatch) => ({ 28 | loadKeyspace: () => dispatch(keyspaceActions.loadKeyspaceActionCreator()), 29 | loadAllEvents: () => dispatch(eventActions.loadAllEventsActionCreator()), 30 | updateInstanceInfo: () => dispatch(actions.updateInstanceInfoActionCreator()), 31 | }); 32 | 33 | class App extends Component { 34 | constructor(props) { 35 | super(props); 36 | } 37 | 38 | componentDidMount() { 39 | this.props.loadKeyspace(); 40 | this.props.loadAllEvents(); 41 | this.props.updateInstanceInfo(); 42 | } 43 | 44 | render() { 45 | return ( 46 | 47 |
48 |
49 | 50 |
51 |
52 | 53 | 54 |
55 | 56 | 57 | {/* create a react router to switch between the main area of divs */} 58 | 59 | } /> 60 | } /> 61 | } /> 62 | 63 |
64 |
65 |
66 |
67 | ); 68 | } 69 | } 70 | 71 | export default connect(null, mapDispatchToProps)(App); 72 | -------------------------------------------------------------------------------- /src/client/components/styles/graphfilters.scss: -------------------------------------------------------------------------------- 1 | @import './config/_variables.scss'; 2 | 3 | .graph-filter-nav-container { 4 | display: flex; 5 | margin: 1em 0 2em; 6 | display: flex; 7 | justify-content: space-between; 8 | } 9 | 10 | .graph-search-filters { 11 | width: 50%; 12 | display: flex; 13 | flex-direction: column; 14 | 15 | .MuiInputBase-input { 16 | color: $PRIMARY_LIGHT; 17 | font-family: $PRIMARY_FONT; 18 | } 19 | 20 | .MuiInputLabel-root { 21 | color: $PRIMARY_LIGHT; 22 | font-family: $PRIMARY_FONT; 23 | 24 | @media screen and (max-width: $small) { 25 | font-size: 0.8em; 26 | } 27 | @media screen and (min-width: $large) { 28 | font-size: 1vw; 29 | } 30 | } 31 | 32 | .MuiSelect-icon { 33 | color: $PRIMARY_LIGHT; 34 | } 35 | 36 | .graph-filter-buttons-container { 37 | 38 | @media screen and (max-width: $small) { 39 | margin: 0.5em 0 0; 40 | } 41 | @media screen and (min-width: $large) { 42 | margin: 0.65vw 0 0; 43 | } 44 | 45 | button { 46 | font-family: $PRIMARY_FONT; 47 | color: $PRIMARY_LIGHT; 48 | 49 | @media screen and (max-width: $small) { 50 | margin: 0 2em; 51 | font-size: 0.9em; 52 | // border: 1px solid $PRIMARY_LIGHT; 53 | } 54 | @media screen and (min-width: $large) { 55 | margin: 0 2.5vw; 56 | font-size: 1.12vw; 57 | } 58 | 59 | } 60 | } 61 | 62 | .MuiInput-input { 63 | border-bottom: 1px solid $PRIMARY_LIGHT; 64 | } 65 | 66 | } 67 | 68 | .graph-filter-button-container { 69 | display: flex; 70 | align-items: center; 71 | justify-content: space-around; 72 | width: 50%; 73 | 74 | button { 75 | height: 20%; 76 | align-self: center; 77 | border-radius: 5px; 78 | font-family: $PRIMARY_FONT; 79 | 80 | @media screen and (max-height: $small) { 81 | font-size: 0.8em; 82 | } 83 | 84 | @media screen and (max-height: $large) { 85 | font-size: 1.0vw; 86 | } 87 | } 88 | 89 | button:hover { 90 | cursor: pointer; 91 | } 92 | 93 | #refreshButton { 94 | background-color: $PRIMARY_RED; 95 | border: 1px solid $PRIMARY_RED; 96 | } 97 | 98 | #clear-interval-button { 99 | background-color: transparent; 100 | border: 1px solid $PRIMARY_LIGHT; 101 | color: $PRIMARY_LIGHT; 102 | } 103 | 104 | } 105 | 106 | #keyspace-graph-filter-buttons-container { 107 | 108 | button { 109 | height: 28%; 110 | } 111 | } -------------------------------------------------------------------------------- /src/client/components/graphs/EventsChartFilterNav.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import EventsChartFilter from "./EventsChartFilter.jsx"; 3 | import "../styles/graphfilters.scss"; 4 | 5 | class EventsChartFilterNav extends Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | render() { 10 | // console.log("props in EventsChartFilterNav", this.props); 11 | return ( 12 |
13 | 30 |
31 | 46 | 62 |
63 |
64 | ); 65 | } 66 | } 67 | 68 | export default EventsChartFilterNav; 69 | -------------------------------------------------------------------------------- /src/client/components/navbars/InstanceNav.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from "react-redux"; 3 | import InstanceComponent from "./InstanceComponent.jsx"; 4 | import DatabaseSelector from "./DatabaseSelector.jsx"; 5 | import * as actions from "../../action-creators/connections"; 6 | 7 | import '../styles/instances.scss'; 8 | 9 | const mapStateToProps = (store) => ({ 10 | instanceInfo: store.instanceInfoStore.instanceInfo, 11 | currInstance: store.currInstanceStore.currInstance, 12 | currDatabase: store.currDatabaseStore.currDatabase 13 | }); 14 | 15 | const mapDispatchToProps = (dispatch) => ({ 16 | switchInstance: (instance) => { 17 | dispatch(actions.switchInstanceActionCreator(instance)); 18 | 19 | //When switching instances, automatically flip to db0 20 | dispatch(actions.switchDatabaseActionCreator(0)); 21 | }, 22 | switchDatabase: (database) => { 23 | dispatch(actions.switchDatabaseActionCreator(database)); 24 | } 25 | }); 26 | 27 | class InstanceNav extends Component { 28 | 29 | constructor(props) { 30 | super(props); 31 | } 32 | 33 | render() { 34 | 35 | const instances = []; 36 | this.props.instanceInfo.forEach((instance, idx) => { 37 | 38 | //If instance is currently selected 39 | //Pass down a DatabaseSelector component 40 | let databases, className; 41 | if (instance.instanceId === this.props.currInstance) { 42 | databases = 47 | 48 | className = 'selected-instance-container'; 49 | } else { 50 | className = 'instance-container'; 51 | } 52 | 53 | let instanceDisplayName; 54 | if (instance.host && instance.port) { 55 | instanceDisplayName = `${instance.host}@${instance.port}` 56 | } else if (instance.url) { 57 | instanceDisplayName = instance.url; 58 | } else { 59 | instanceDisplayName = 'N/A'; 60 | } 61 | 62 | instances.push( 63 | 72 | ) 73 | }) 74 | 75 | return ( 76 |
77 |

Connections

78 | {instances} 79 |
80 | ); 81 | } 82 | } 83 | 84 | export default connect(mapStateToProps, mapDispatchToProps)(InstanceNav); 85 | -------------------------------------------------------------------------------- /src/server/redis-monitors/models/interfaces.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Library of interfaces to represent data structures used by the RedisMonitors. 3 | */ 4 | 5 | import { RedisClient } from 'redis'; 6 | 7 | export interface RedisInstance { 8 | readonly host?: string; 9 | readonly port?: number; 10 | readonly url?: string; 11 | readonly recordKeyspaceHistoryFrequency: number; 12 | readonly maxKeyspaceHistoryCount: number; 13 | readonly eventGraphRefreshFrequency: number; 14 | readonly maxEventLogSize: number; 15 | readonly notifyKeyspaceEvents: string; 16 | }; 17 | 18 | export interface RedisMonitor { 19 | readonly instanceId: number; 20 | readonly redisClient: RedisClient; 21 | readonly keyspaceSubscriber: RedisClient; 22 | host?: RedisInstance['host']; 23 | port?: RedisInstance['port']; 24 | url?: RedisInstance['url']; 25 | databases?: number; //Check property - should this be optional on object initialization? 26 | keyspaces: Keyspace[]; 27 | readonly recordKeyspaceHistoryFrequency: RedisInstance['recordKeyspaceHistoryFrequency']; 28 | readonly maxKeyspaceHistoryCount: RedisInstance['maxKeyspaceHistoryCount']; 29 | readonly eventGraphRefreshFrequency: RedisInstance['eventGraphRefreshFrequency']; 30 | readonly maxEventLogSize: RedisInstance['maxEventLogSize']; 31 | readonly notifyKeyspaceEvents: RedisInstance['notifyKeyspaceEvents']; 32 | }; 33 | 34 | export interface Keyspace { 35 | eventLog: EventLog; 36 | keyspaceHistories: KeyspaceHistoriesLog; 37 | keyspaceSnapshot: KeyspaceKeyDetail[]; 38 | eventLogSnapshot: KeyspaceEvent[]; 39 | }; 40 | export interface KeyspaceKeyDetail { 41 | key: string; 42 | value: any; 43 | type: string; 44 | } 45 | export interface EventLog { 46 | head: null | KeyspaceEventNode; 47 | tail: null | KeyspaceEventNode; 48 | eventTotal: number; 49 | maxLength: number; 50 | length: number; 51 | add: (key: string, event: string) => void; 52 | reset: () => void; 53 | returnLogAsArray: (eventTotal: number) => KeyspaceEvent[]; 54 | } 55 | export interface KeyspaceEvent { 56 | key: string; 57 | event: string; 58 | timestamp: number; 59 | } 60 | 61 | export interface KeyspaceEventNode extends KeyspaceEvent { 62 | next: null | KeyspaceEventNode; 63 | previous: null | KeyspaceEventNode; 64 | }; 65 | 66 | export interface KeyspaceHistoriesLog { 67 | head: null | KeyspaceHistoryNode; 68 | tail: null | KeyspaceHistoryNode; 69 | historiesCount: number; 70 | maxLength: number; 71 | length: number; 72 | add: (keyDetails: KeyDetails[]) => void; 73 | reset: () => void; 74 | returnLogAsArray: (historiesCount: number) => KeyspaceHistory[]; 75 | } 76 | 77 | export interface KeyspaceHistory { 78 | timestamp: number; 79 | keys: KeyDetails[]; 80 | }; 81 | 82 | export interface KeyspaceHistoryNode extends KeyspaceHistory { 83 | next: null | KeyspaceHistoryNode; 84 | previous: null | KeyspaceHistoryNode; 85 | } 86 | 87 | export interface KeyDetails { 88 | key: string, 89 | memoryUsage: number 90 | } -------------------------------------------------------------------------------- /src/server/redis-monitors/utils.ts: -------------------------------------------------------------------------------- 1 | import { RedisClient } from 'redis'; 2 | import { promisify } from 'util'; 3 | import { RedisMonitor, KeyDetails } from './models/interfaces'; 4 | 5 | export const promisifyClientMethods = (client: RedisClient) => { 6 | /* 7 | Promisifies methods of the client. 8 | */ 9 | 10 | //redis processes - currently excluded as this method is being used 11 | //in callback form - need to refactor existing code before utilizing this 12 | client.config = promisify(client.config).bind(client); 13 | 14 | //redis commands: databases 15 | client.flushdb = promisify(client.flushdb).bind(client); 16 | client.flushall = promisify(client.flushall).bind(client); 17 | client.select = promisify(client.select).bind(client); 18 | 19 | //redis commands: data 20 | client.scan = promisify(client.scan).bind(client); 21 | client.type = promisify(client.type).bind(client); 22 | 23 | //redis commands: data: strings 24 | client.set = promisify(client.set).bind(client); 25 | client.get = promisify(client.get).bind(client); 26 | client.mget = promisify(client.mget).bind(client); 27 | 28 | //redis commands: data: lists (note: requires index to get value) 29 | client.lrange = promisify(client.lrange).bind(client); 30 | 31 | //redis commands: data: sets (note: returns all members of set associated with given key) 32 | client.smembers = promisify(client.smembers).bind(client); 33 | 34 | //redis commands: data: sorted sets (note: returns range of elements by index 35 | // min and max. Can use -inf and +inf, or since the indices are 0-based, can use 36 | // 0 for the min and -1 for the max, which is the last element) 37 | client.zrange = promisify(client.zrange).bind(client); 38 | 39 | //redis commands: data: hashes (note: requires id to get all field/value pairs) 40 | client.hgetall = promisify(client.hgetall).bind(client); 41 | 42 | return client; 43 | } 44 | 45 | // const getKeyMemoryUsage = async (key: string, client: RedisClient): Promise => { 46 | // /* 47 | // Returns the memory usage, in bytes, for the given key argument. 48 | // */ 49 | 50 | // } 51 | export const recordKeyspaceHistory = async (monitor: RedisMonitor, dbIndex: number): Promise => { 52 | /* 53 | Scans the keyspace for the given database index utilizing a RedisMonitor instance. 54 | Stores the scanned results in the corresponding monitored keyspace's KeyspaceHistoriesLog. 55 | */ 56 | const keyDetails: KeyDetails[] = []; 57 | 58 | let cursor = '0'; 59 | let keys: string[] = []; 60 | 61 | await monitor.redisClient.select(dbIndex); 62 | //@ts-ignore 63 | [cursor, keys] = await monitor.redisClient.scan(cursor) 64 | 65 | 66 | do { 67 | keys.forEach(key => { 68 | keyDetails.push({ 69 | key: key, 70 | memoryUsage: 1 71 | }); 72 | }); 73 | 74 | //@ts-ignore - 75 | [cursor, keys] = await monitor.redisClient.scan(cursor); 76 | } 77 | while (cursor !== '0') 78 | 79 | monitor.keyspaces[dbIndex].keyspaceHistories.add(keyDetails); 80 | } -------------------------------------------------------------------------------- /src/client/reducers/totalEventsReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actions/actionTypes.js"; 2 | 3 | const initialState = { 4 | currInstance: 1, 5 | currDatabase: 0, 6 | // totalEvents: { 7 | // eventTally: 0, 8 | // eventTotals: [], 9 | // }, 10 | data: { 11 | labels: [], 12 | datasets: [ 13 | { 14 | label: "Number of Events", 15 | data: [], 16 | backgroundColor: ["red"], 17 | borderColor: "white", 18 | borderWidth: "2", 19 | pointBorderColor: "red", 20 | pointHoverBackgroundColor: "#55bae7", 21 | }, 22 | ], 23 | }, 24 | }; 25 | const totalEventsReducer = (state = initialState, action) => { 26 | let totalEvents; 27 | switch (action.type) { 28 | case types.GET_EVENT_TOTALS: { 29 | const datasets = action.payload.datasets; 30 | const labels = action.payload.labels; 31 | console.log("action payload in tEReducer", action.payload); 32 | return { 33 | ...state, 34 | totalEvents: action.payload.totalEvents, 35 | data: { 36 | labels: labels, 37 | datasets: [ 38 | { 39 | label: "Number of Events", 40 | data: datasets, 41 | backgroundColor: ["red"], 42 | borderColor: "white", 43 | borderWidth: "2", 44 | pointBorderColor: "red", 45 | pointHoverBackgroundColor: "#55bae7", 46 | }, 47 | ], 48 | }, 49 | }; 50 | } 51 | case types.GET_NEXT_EVENTS: { 52 | const datasets = action.payload.datasets; 53 | const currInstance = state.currInstance; 54 | const currDatabase = state.currDatabase; 55 | const labels = action.payload.labels; 56 | const dataCopy = Object.assign({}, state.data); 57 | console.log("dataCopy", dataCopy); 58 | // dataCopy.datasets[0].data.push(...datasets); 59 | // dataCopy.labels.push(...labels); 60 | datasets.forEach((events) => dataCopy.datasets[0].data.push(events)); 61 | labels.forEach((time) => { 62 | dataCopy.labels.push(time); 63 | }); 64 | console.log("data in getnextevents reducer", dataCopy); 65 | return { 66 | ...state, 67 | currInstance, 68 | currDatabase, 69 | totalEvents: action.payload.totalEvents, 70 | dataCopy, 71 | // data: { 72 | // totalEvents: action.payload.totalEvents, 73 | // labels: labels, 74 | // datasets: [ 75 | // { 76 | // label: "Number of Events", 77 | // data: datasets, 78 | // backgroundColor: ["red"], 79 | // borderColor: "white", 80 | // borderWidth: "2", 81 | // pointBorderColor: "red", 82 | // pointHoverBackgroundColor: "#55bae7", 83 | // }, 84 | // ], 85 | // }, 86 | }; 87 | } 88 | default: { 89 | return state; 90 | } 91 | } 92 | }; 93 | export default totalEventsReducer; 94 | -------------------------------------------------------------------------------- /src/client/redishawk-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | ]> 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 50 | redis 51 | hawk 52 | 53 | -------------------------------------------------------------------------------- /src/client/components/graphs/GraphComponent.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import EventTotalsChart from "./EventTotalsChart.jsx"; 3 | import KeyspaceHistoriesChart from "./KeyspaceHistoriesChart.jsx" 4 | import { connect } from "react-redux"; 5 | import * as actions from "../../action-creators/connections"; 6 | import * as eventActions from "../../action-creators/eventsConnections"; 7 | 8 | const mapStateToProps = (store) => { 9 | return { 10 | currInstance: store.currInstanceStore.currInstance, 11 | currDatabase: store.currDatabaseStore.currDatabase, 12 | totalEvents: store.totalEventsStore.totalEvents, 13 | data: store.totalEventsStore.data, 14 | }; 15 | }; 16 | 17 | const mapDispatchToProps = (dispatch) => ({ 18 | updateCurrentDisplay: (filter, category) => 19 | dispatch(actions.updateCurrDisplayActionCreator(filter, category)), 20 | getEvents: (instanceId, dbIndex, queryParams) => 21 | dispatch( 22 | eventActions.getTotalEventsActionCreator(instanceId, dbIndex, queryParams) 23 | ), 24 | getNextEvents: (instanceId, dbIndex, queryParams) => 25 | dispatch( 26 | eventActions.getNextEventsActionCreator(instanceId, dbIndex, queryParams) 27 | ), 28 | }); 29 | 30 | class GraphComponent extends Component { 31 | constructor(props) { 32 | super(props); 33 | this.state = { 34 | wasCalled: false, 35 | params: { timeInterval: 10000 }, 36 | eventParams: { eventTotal: this.props.eventTotal }, 37 | }; 38 | this.setGraphUpdate = this.setGraphUpdate.bind(this); 39 | } 40 | componentDidMount() { 41 | const self = this; 42 | // this.getInitialData(this.props.currInstance, this.props.currDatabase, { 43 | // timeInterval: 7000, 44 | // }); 45 | // } 46 | // setTimeout(setInterval(this.setGraphUpdate, 7000), 7000); 47 | } 48 | getInitialData(currInstance, currDB, params) { 49 | this.props.getEvents(currInstance, currDB, params); 50 | this.setState({ 51 | wasCalled: true, 52 | }); 53 | } 54 | setGraphUpdate() { 55 | return this.props.getNextEvents( 56 | this.props.currInstance, 57 | this.props.currDatabase, 58 | // this.state.eventParams 59 | { eventTotal: this.props.totalEvents.eventTotal } 60 | ); 61 | } 62 | 63 | render() { 64 | 65 | // if (this.props.data) { 66 | return ( 67 |
68 | {/* */} 78 | 82 | 86 |
87 | ); 88 | // } 89 | } 90 | } 91 | 92 | export default connect(mapStateToProps, mapDispatchToProps)(GraphComponent); 93 | -------------------------------------------------------------------------------- /__tests__/client/react/databaseNav.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { configure, shallow } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import toJson from 'enzyme-to-json'; 5 | import renderer from 'react-test-renderer'; 6 | 7 | import DatabaseNav from '../../../src/client/components/navbars/DatabaseNav.jsx'; 8 | import DatabaseComponent from '../../../src/client/components/navbars/DatabaseComponent.jsx'; 9 | 10 | configure({ adapter: new Adapter() }); 11 | 12 | describe('React DatabaseNav unit tests', () => { 13 | describe('DatabaseNav', () => { 14 | let wrapper; 15 | const props = { 16 | databaseInfo: [ 17 | { 18 | host: 'localhost', 19 | port: '3000', 20 | numberOfDBs: 16, 21 | }, 22 | ], 23 | }; 24 | 25 | beforeAll(() => { 26 | wrapper = shallow(); 27 | }); 28 | 29 | it('renders the div with id databaseNavContainer with database host and database port passed down and a div with id databaseHolder with databaseInfo props passed down ', () => { 30 | // expect(wrapper.find("databaseNavContainer").find("div")); 31 | expect(wrapper.find('databaseNavContainer').find('div')); 32 | // expect(wrapper.find("db-host").find("span")); 33 | // expect(wrapper.find("db-port").find("span")); 34 | // expect( 35 | // wrapper.containsAllMatchingElements([ 36 | //
37 | //
38 | //

39 | // {" "} 40 | // Host{" "} 41 | // {this.props.databaseInfo.host} 42 | //

43 | //

44 | // {" "} 45 | // Port{" "} 46 | // {this.props.databaseInfo.port} 47 | //

48 | //
{dbArray}
49 | //
50 | //
, 51 | // ]) 52 | // ).toEqual(true); 53 | }); 54 | }); 55 | describe('DatabaseComponent', () => { 56 | let wrapper; 57 | const changeDatabasesFunc = () => 'changing databases'; 58 | const props = { 59 | databaseInfo: [ 60 | { 61 | host: 'localhost', 62 | port: '3000', 63 | numberOfDBs: 16, 64 | }, 65 | ], 66 | changeDatabases: changeDatabasesFunc, 67 | }; 68 | 69 | beforeAll(() => { 70 | wrapper = shallow(); 71 | }); 72 | it('renders a div with id singleDatabase that contains the number for the database', () => { 73 | expect(wrapper.find('singleDatabase').find('div')); 74 | expect( 75 | wrapper.containsAllMatchingElements([ 76 |
, 85 | ]) 86 | ); 87 | }); 88 | it('should have functions passed down invoking on click to change databases', () => { 89 | expect(wrapper.find('singleDatabase').invoke('onClick')()).toEqual( 90 | changeDatabasesFunc() 91 | ); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /__tests__/client/react/instanceNav.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { configure, shallow } from "enzyme"; 3 | import Adapter from "enzyme-adapter-react-16"; 4 | import toJson from "enzyme-to-json"; 5 | import renderer from "react-test-renderer"; 6 | 7 | import InstanceNav from "../../../src/client/components/navbars/InstanceNav.jsx"; 8 | import InstanceComponent from "../../../src/client/components/navbars/InstanceComponent.jsx"; 9 | 10 | configure({ adapter: new Adapter() }); 11 | 12 | describe("React DatabaseNav unit tests", () => { 13 | describe("DatabaseNav", () => { 14 | let wrapper; 15 | const props = { 16 | databaseInfo: [ 17 | { 18 | host: "localhost", 19 | port: "3000", 20 | numberOfDBs: 16, 21 | }, 22 | ], 23 | }; 24 | 25 | beforeAll(() => { 26 | wrapper = shallow(); 27 | }); 28 | 29 | it("renders the div with id databaseNavContainer with database host and database port passed down and a div with id databaseHolder with databaseInfo props passed down ", () => { 30 | // expect(wrapper.find("databaseNavContainer").find("div")); 31 | expect(wrapper.find("instanceNavContainer").find("div")); 32 | // expect(wrapper.find("db-host").find("span")); 33 | // expect(wrapper.find("db-port").find("span")); 34 | // expect( 35 | // wrapper.containsAllMatchingElements([ 36 | //
37 | //
38 | //

39 | // {" "} 40 | // Host{" "} 41 | // {this.props.databaseInfo.host} 42 | //

43 | //

44 | // {" "} 45 | // Port{" "} 46 | // {this.props.databaseInfo.port} 47 | //

48 | //
{dbArray}
49 | //
50 | //
, 51 | // ]) 52 | // ).toEqual(true); 53 | }); 54 | }); 55 | describe("InstanceComponent", () => { 56 | let wrapper; 57 | const changeInstanceFunc = () => "changing instance"; 58 | const props = { 59 | instanceInfo: [ 60 | { 61 | host: "localhost", 62 | port: "3000", 63 | numberOfDBs: 16, 64 | instanceId: 1, 65 | }, 66 | ], 67 | changeInstance: changeInstanceFunc, 68 | }; 69 | 70 | beforeAll(() => { 71 | wrapper = shallow(); 72 | }); 73 | it("renders a div with id singleInstance that contains the number for the database", () => { 74 | expect(wrapper.find("singleInstance").find("div")); 75 | expect( 76 | wrapper.containsAllMatchingElements([ 77 |
, 87 | ]) 88 | ); 89 | }); 90 | it("should have functions passed down invoking on click to change databases", () => { 91 | expect(wrapper.find("singleInstance").invoke("onClick")()).toEqual( 92 | changeInstanceFunc() 93 | ); 94 | }); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /__tests__/client/react/graphComponent.test.js: -------------------------------------------------------------------------------- 1 | //incomplete because we don't have the specific graph details we are using so we don't know what to expect 2 | 3 | import React from "react"; 4 | import { configure, shallow } from "enzyme"; 5 | import Adapter from "enzyme-adapter-react-16"; 6 | import toJson from "enzyme-to-json"; 7 | import renderer from "react-test-renderer"; 8 | import { 9 | XYPlot, 10 | XAxis, 11 | YAxis, 12 | HorizontalGridLines, 13 | LineSeries, 14 | } from "react-vis"; 15 | 16 | import GraphComponent from "../../../src/client/components/graphs/GraphComponent.jsx"; 17 | import GraphHolder from "../../../src/client/components/graphs/GraphComponent.jsx"; 18 | 19 | configure({ adapter: new Adapter() }); 20 | 21 | describe("React GraphComponent unit tests", () => { 22 | describe("GraphComponent", () => { 23 | let wrapper; 24 | const props = { 25 | // keyGraph: [ 26 | // { 27 | // name: "keyName", 28 | // memory: "456GB", 29 | // time: "11:52", 30 | // }, 31 | // ], 32 | events: [ 33 | [ 34 | { 35 | name: "hey!", 36 | event: "scan", 37 | time: "8:30", 38 | }, 39 | ], 40 | ], 41 | currDatabase: 0, 42 | }; 43 | 44 | beforeAll(() => { 45 | wrapper = shallow(); 46 | }); 47 | 48 | it("renders the graphHolder with all keyGraph info and events passed down", () => { 49 | expect(wrapper.find("graphHolder")); 50 | expect( 51 | wrapper.containsAllMatchingElements([ 52 | , 66 | ]) 67 | ).toEqual(true); 68 | }); 69 | }); 70 | 71 | describe("GraphHolder", () => { 72 | let wrapper; 73 | const props = { 74 | // keyGraph: [ 75 | // { 76 | // name: "keyName", 77 | // memory: "456GB", 78 | // time: "11:52", 79 | // }, 80 | // ], 81 | events: [ 82 | [ 83 | { 84 | name: "hey!", 85 | event: "scan", 86 | time: "8:30", 87 | }, 88 | ], 89 | ], 90 | currDatabase: 0, 91 | }; 92 | 93 | beforeAll(() => { 94 | wrapper = shallow(); 95 | }); 96 | 97 | it("renders an XYPlot component wrapping HorizontalGridLines, XAxis, YAxis and LineSeries components with plotData array passed in as prop ", () => { 98 | const plotData = [ 99 | { x: "07:37", y: 120 }, 100 | { x: "07:37", y: 140 }, 101 | ]; 102 | expect(wrapper.find("graphHolder")); 103 | expect( 104 | wrapper.containsAllMatchingElements([ 105 | 110 | 111 | 112 | 113 | 114 | , 115 | ]) 116 | ).toEqual(true); 117 | }); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /src/client/components/events/EventTable.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { DataGrid } from '@material-ui/data-grid'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | 5 | function EventTable(props) { 6 | // console.log('props in keyspace table', props); 7 | 8 | const [pageSize, setPageSize] = React.useState(25); 9 | const [loading, setLoading] = React.useState(false); 10 | 11 | const handleEventPageChange = (params) => { 12 | console.log('props in eventTable', props, 'params in eventTable', params); 13 | props.updatePageNum(params.page + 1); 14 | 15 | const funcOptions = { 16 | pageSize: props.pageSize, 17 | pageNum: params.page + 1, 18 | keyNameFilter: props.currDisplay.keyNameFilter, 19 | keyEventFilter: props.currDisplay.keyEventFilter, 20 | refreshData: 0, 21 | }; 22 | 23 | props.changeEventsPage(props.currInstance, props.currDatabase, funcOptions); 24 | }; 25 | 26 | const handleEventPageSizeChange = (params) => { 27 | setPageSize(params.pageSize); 28 | props.updatePageSize(params.pageSize); 29 | 30 | //this is so if your current page is 50 and you select 100, the page will refresh with 100 31 | if (params.pageSize > props.pageSize) { 32 | const funcOptions = { 33 | pageSize: params.pageSize, 34 | pageNum: params.page + 1, 35 | refreshData: 0, 36 | keyNameFilter: props.currDisplay.keyNameFilter, 37 | keyEventFilter: props.currDisplay.keyEventFilter, 38 | }; 39 | props.changeEventsPage( 40 | props.currInstance, 41 | props.currDatabase, 42 | funcOptions 43 | ); 44 | } 45 | }; 46 | 47 | let data = []; 48 | if (props.myCount) { 49 | 50 | data = props.events[props.currInstance - 1] 51 | .keyspaces[props.currDatabase].data; 52 | for (let i = 0; i < data.length; i += 1) { 53 | data[i].id = i; 54 | } 55 | } 56 | 57 | const useStyles = makeStyles({ 58 | dataGrid: { 59 | borderRadius: 3, 60 | border: 'solid rgb(200, 200, 200) 1px', 61 | color: 'rgb(200, 200, 200)', 62 | fontFamily: "'Nunito Sans', 'sans-serif'", 63 | }, 64 | }); 65 | const classes = useStyles(); 66 | // console.log('data in keyspace table', data); 67 | return ( 68 |
69 | 105 |
106 | ); 107 | } 108 | 109 | export default EventTable; 110 | -------------------------------------------------------------------------------- /src/client/components/events/EventComponent.jsx: -------------------------------------------------------------------------------- 1 | // import React, { Component } from "react"; 2 | // import { connect } from "react-redux"; 3 | 4 | // import EventTable from "./EventTable.jsx"; 5 | 6 | // const mapStateToProps = (store) => { 7 | // return { 8 | // currInstance: store.currInstanceStore.currInstance, 9 | // currDatabase: store.currDatabaseStore.currDatabase, 10 | // events: store.eventsStore.events, 11 | // currDisplay: store.currDisplayStore.currDisplay, 12 | // }; 13 | // }; 14 | 15 | // class EventComponent extends Component { 16 | // constructor(props) { 17 | // super(props); 18 | // } 19 | 20 | // render() { 21 | // console.log("props in EventComponent", this.props); 22 | // return ( 23 | //
24 | // 30 | //
31 | // ); 32 | // } 33 | // } 34 | 35 | // export default connect(mapStateToProps, null)(EventComponent); 36 | 37 | import React, { Component } from 'react'; 38 | import { connect } from 'react-redux'; 39 | import EventTable from './EventTable.jsx'; 40 | import * as actions from '../../action-creators/connections'; 41 | import * as eventActions from '../../action-creators/eventsConnections'; 42 | 43 | //withRouter??? -- for props.history -- stretch?? 44 | 45 | const mapStateToProps = (store) => { 46 | return { 47 | currInstance: store.currInstanceStore.currInstance, 48 | currDatabase: store.currDatabaseStore.currDatabase, 49 | events: store.eventsStore.events, 50 | currDisplay: store.currDisplayStore.currDisplay, 51 | pageSize: store.dataPageStore.pageSize, 52 | pageNum: store.dataPageStore.pageNum, 53 | }; 54 | }; 55 | const mapDispatchToProps = (dispatch) => ({ 56 | updatePageSize: (pageSize) => 57 | dispatch(actions.updatePageSizeActionCreator(pageSize)), 58 | updatePageNum: (pageNum) => 59 | dispatch(actions.updatePageNumActionCreator(pageNum)), 60 | changeEventsPage: (instanceId, dbIndex, queryOptions) => 61 | dispatch( 62 | eventActions.changeEventsPageActionCreator( 63 | instanceId, 64 | dbIndex, 65 | queryOptions 66 | ) 67 | ), 68 | }); 69 | 70 | class EventComponent extends Component { 71 | constructor(props) { 72 | super(props); 73 | } 74 | 75 | render() { 76 | // console.log('all the props in event component', this.props); 77 | 78 | const currDatabaseData = this.props.events[this.props.currInstance - 1] 79 | .keyspaces[this.props.currDatabase]; 80 | 81 | const myCount = currDatabaseData ? currDatabaseData.eventTotal : 0; 82 | 83 | return ( 84 |
85 | 97 |
98 | ); 99 | } 100 | } 101 | 102 | export default connect(mapStateToProps, mapDispatchToProps)(EventComponent); 103 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis-hawk", 3 | "version": "1.0.0", 4 | "description": "granular redis monitoring", 5 | "license": "MIT", 6 | "contributors": [ 7 | "Abigail Gjurich", 8 | "Arthur Sato", 9 | "James Espy", 10 | "Wesley Jia" 11 | ], 12 | "keywords": [], 13 | "main": "./client/index.js", 14 | "scripts": { 15 | "build": "webpack", 16 | "test": "IS_TEST=true jest --verbose", 17 | "start": "NODE_ENV=production nodemon dist/server/server.js", 18 | "dev": "NODE_ENV=development webpack serve --open & nodemon dist/server/server.js", 19 | "cp": "copyfiles -u 1 src/server/tests-config/* src/server/assets/* dist/server/" 20 | }, 21 | "jest": { 22 | "transform": { 23 | "^.+\\.(ts|tsx)$": "ts-jest", 24 | "^.+\\.jsx?$": "babel-jest" 25 | }, 26 | "roots": [ 27 | "/" 28 | ], 29 | "moduleFileExtensions": [ 30 | "ts", 31 | "tsx", 32 | "js" 33 | ], 34 | "globalSetup": "./jestGlobalSetup.js", 35 | "globalTeardown": "./jestGlobalTeardown.js" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/oslabs-beta/redis-hawk.git" 40 | }, 41 | "author": "", 42 | "bugs": { 43 | "url": "https://github.com/oslabs-beta/redis-hawk/issues" 44 | }, 45 | "homepage": "https://github.com/oslabs-beta/redis-hawk#readme", 46 | "dependencies": { 47 | "@babel/plugin-transform-async-to-generator": "^7.13.0", 48 | "@babel/runtime": "^7.13.10", 49 | "@material-ui/core": "^4.11.4", 50 | "@material-ui/data-grid": "^4.0.0-alpha.30", 51 | "@material-ui/icons": "^4.11.2", 52 | "@material-ui/lab": "^4.0.0-alpha.58", 53 | "@material-ui/x-grid": "^4.0.0-alpha.29", 54 | "@material-ui/x-grid-data-generator": "^4.0.0-alpha.29", 55 | "@svgr/webpack": "^5.5.0", 56 | "chart.js": "^3.3.2", 57 | "chartjs-plugin-zoom": "^1.0.1", 58 | "express": "^4.17.1", 59 | "fetch-mock": "^9.11.0", 60 | "hammerjs": "^2.0.8", 61 | "node-fetch": "^2.6.1", 62 | "nodemon": "^2.0.7", 63 | "react": "^17.0.2", 64 | "react-chartjs-2": "^3.0.3", 65 | "react-dom": "^17.0.2", 66 | "react-redux": "^7.2.4", 67 | "react-router-dom": "^5.2.0", 68 | "react-vis": "^1.11.7", 69 | "redis": "^3.1.2", 70 | "redux-devtools-extension": "^2.13.9", 71 | "redux-mock-store": "^1.5.4", 72 | "redux-thunk": "^2.3.0", 73 | "regenerator-runtime": "^0.13.7", 74 | "sass": "^1.32.13", 75 | "ws": "^7.4.5" 76 | }, 77 | "devDependencies": { 78 | "@babel/cli": "^7.13.16", 79 | "@babel/core": "^7.14.2", 80 | "@babel/plugin-syntax-jsx": "^7.12.13", 81 | "@babel/plugin-transform-runtime": "^7.13.15", 82 | "@babel/preset-env": "^7.14.2", 83 | "@babel/preset-react": "^7.13.13", 84 | "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", 85 | "@types/express": "^4.17.11", 86 | "@types/jest": "^26.0.23", 87 | "@types/node": "^15.3.0", 88 | "@types/redis": "^2.8.28", 89 | "@types/supertest": "^2.0.11", 90 | "babel-jest": "^23.6.0", 91 | "babel-loader": "^8.2.2", 92 | "copyfiles": "^2.4.1", 93 | "css-loader": "^5.2.4", 94 | "enzyme": "^3.11.0", 95 | "enzyme-adapter-react-16": "^1.15.6", 96 | "enzyme-to-json": "^3.6.2", 97 | "jest": "^26.6.3", 98 | "react-refresh": "^0.10.0", 99 | "redis-server": "^1.2.2", 100 | "sass-loader": "^11.1.1", 101 | "style-loader": "^2.0.0", 102 | "supertest": "^6.1.3", 103 | "svg-url-loader": "^7.1.1", 104 | "ts-jest": "^26.5.6", 105 | "typescript": "^4.2.4", 106 | "webpack": "^5.37.0", 107 | "webpack-cli": "^4.7.0", 108 | "webpack-dev-server": "^3.11.2" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/client/reducers/eventsReducer.js: -------------------------------------------------------------------------------- 1 | //leave these separate for future developers in case they want to add functionality 2 | import * as types from '../actions/actionTypes.js'; 3 | 4 | const initialState = { 5 | currInstance: 1, 6 | currDatabase: 0, 7 | events: [ 8 | { 9 | instanceId: 1, 10 | keyspaces: [ 11 | { 12 | eventTotal: 1, 13 | pageSize: 25, 14 | pageNum: 4, 15 | data: [ 16 | { 17 | key: 'loading', 18 | event: 'loading', 19 | timestamp: 'loading', 20 | }, 21 | ], 22 | }, 23 | ], 24 | }, 25 | ], 26 | }; 27 | 28 | const eventsReducer = (state = initialState, action) => { 29 | let events; 30 | 31 | switch (action.type) { 32 | case types.LOAD_ALL_EVENTS: { 33 | const allEvents = action.payload.events; 34 | console.log('allevents', allEvents); 35 | for (let i = 0; i < allEvents.length; i += 1) { 36 | let instance = allEvents[i]; 37 | for (let j = 0; j < instance.keyspaces.length; j += 1) { 38 | let key = instance.keyspaces[j]; 39 | for (let k = 0; k < key.data.length; k += 1) { 40 | let time = key.data[k].timestamp; 41 | // console.log('key in load all events', time); 42 | const newTime = new Date(time).toString('MMddd').slice(16, 24); 43 | // console.log('my mew time', newTime); 44 | key.data[k].timestamp = newTime; 45 | } 46 | } 47 | } 48 | console.log('allEvents', allEvents); 49 | let allNewEvents = state.events.slice(); 50 | allNewEvents = allEvents; 51 | // console.log("events in eventreducer", events); 52 | return { 53 | ...state, 54 | events: allNewEvents, 55 | }; 56 | } 57 | case types.REFRESH_EVENTS: { 58 | console.log('action payload in refresh events', action.payload); 59 | let updateEvents = state.events.slice(); 60 | const specificInstanceEvents = action.payload.events; 61 | for (let i = 0; i < specificInstanceEvents.data.length; i += 1) { 62 | let database = specificInstanceEvents.data[i]; 63 | let time = database.timestamp; 64 | const newTime = new Date(time).toString('MMddd').slice(16, 24); 65 | database.timestamp = newTime; 66 | } 67 | 68 | const currInstance = action.payload.currInstance; 69 | const currDatabase = action.payload.currDatabase; 70 | updateEvents[currInstance - 1].keyspaces[currDatabase] = 71 | specificInstanceEvents; 72 | 73 | return { 74 | ...state, 75 | events: updateEvents, 76 | }; 77 | } 78 | case types.CHANGE_EVENTS_PAGE: { 79 | console.log('payload in eventsReducer', action.payload); 80 | const specificInstanceEvents = action.payload.events; 81 | for (let i = 0; i < specificInstanceEvents.data.length; i += 1) { 82 | let key = specificInstanceEvents.data[i]; 83 | let time = key.timestamp; 84 | const newTime = new Date(time).toString('MMddd').slice(16, 24); 85 | key.timestamp = newTime; 86 | } 87 | console.log('specificInstance', specificInstanceEvents); 88 | const currInstance = action.payload.currInstance; 89 | const currDatabase = action.payload.currDatabase; 90 | events = state.events.slice(); 91 | events[currInstance - 1].keyspaces[currDatabase] = specificInstanceEvents; 92 | 93 | return { 94 | ...state, 95 | events, 96 | }; 97 | } 98 | 99 | default: { 100 | return state; 101 | } 102 | } 103 | }; 104 | 105 | export default eventsReducer; 106 | -------------------------------------------------------------------------------- /__tests__/client/react/filterNav.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { configure, shallow } from "enzyme"; 3 | import Adapter from "enzyme-adapter-react-16"; 4 | import toJson from "enzyme-to-json"; 5 | import renderer from "react-test-renderer"; 6 | 7 | import FilterNav from "../../../src/client/components/navbars/FilterNav.jsx"; 8 | import SearchFilter from "../../../src/client/components/navbars/SearchFilter.jsx"; 9 | 10 | configure({ adapter: new Adapter() }); 11 | 12 | describe("React FilterNav tests", () => { 13 | describe("FilterNav", () => { 14 | let wrapper; 15 | const props = { 16 | whichPage: "keyspace", 17 | keyspace: [ 18 | [ 19 | { 20 | name: "keyName", 21 | value: "hello", 22 | time: "11:52", 23 | }, 24 | ], 25 | ], 26 | events: [ 27 | [ 28 | { 29 | name: "hey!", 30 | event: "scan", 31 | time: "8:30", 32 | }, 33 | ], 34 | ], 35 | currDatabase: 0, 36 | }; 37 | 38 | it("renders a SearchFilter component, a button with the id refreshButton and a div with the id totals. The button should have onclick functionality passed in as props", () => { 39 | wrapper = shallow(); 40 | expect( 41 | wrapper.containsAllMatchingElements([ 42 | , 43 | 102 | 137 | {/* */} 138 |
139 |
{newArea}
140 |
141 | ); 142 | } 143 | 144 | // 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # redis-hawk 6 | 7 | redis-hawk is an easy-to-use monitoring and visualizing web application for understanding granular key-level details for your Redis deployment. 8 | 9 | It can be deployed locally on your desktop or on a server for continuous and remote monitoring of your Redis deployment. 10 | 11 | ## Table of Contents 12 | 13 | * [Features](#features) 14 | * [Demo](#demo) 15 | * [Installation](#installation) 16 | * [Configuration](#configuration) 17 | * [Feature Roadmap](#feature-roadmap) 18 | 19 | ## Features 20 | 21 | redis-hawk allows you to monitor the keyspace and its events within all databases of any number of deployed instances. 22 | 23 | * View details on every keyspace in your Redis deployment 24 | * View a log of keyspace events occuring in every keyspace 25 | * View graphs to understand both key and event volumes over time 26 | * Utilize flexible filters to filter based on a keyname pattern, specific data type, and/or event type. 27 | 28 | ## Demo 29 | 30 | With redis-hawk you can: 31 | 32 | Access keyspace information and visualize trends 33 | 34 | ![click-through-pages-gif](https://elasticbeanstalk-us-east-2-310828374226.s3.us-east-2.amazonaws.com/demo/click-through-pages.gif) 35 | 36 | View all databases for all instances of your deployment 37 | 38 | ![instance-and-databases-for-events](https://elasticbeanstalk-us-east-2-310828374226.s3.us-east-2.amazonaws.com/demo/instance-and-dbs-for-events.gif) 39 | 40 | Filter by key names, event types, or data types 41 | 42 | ![filtering-and-refreshing-events](https://elasticbeanstalk-us-east-2-310828374226.s3.us-east-2.amazonaws.com/demo/filtering-and-refreshing-events.gif) 43 | 44 | Customize your keyspace view 45 | 46 | ![filtering-pagination-on-keyspace](https://elasticbeanstalk-us-east-2-310828374226.s3.us-east-2.amazonaws.com/demo/filtering-pagination-on-keyspace.gif) 47 | 48 | Filter, zoom, and pan through your graphs 49 | 50 | ![filtering-zooming-on-graph](https://elasticbeanstalk-us-east-2-310828374226.s3.us-east-2.amazonaws.com/demo/filtering-zooming-on-graph.gif) 51 | 52 | ## Installation 53 | 54 | redis-hawk is a web application that you can either run locally or deploy on a server for continuous and remote monitoring. 55 | 56 | To install: 57 | 58 | ``` 59 | npm install 60 | ``` 61 | then, either 62 | 63 | ``` 64 | npm run build 65 | npm start 66 | ``` 67 | 68 | OR 69 | 70 | ``` 71 | npm run dev 72 | ``` 73 | 74 | Then, please configure your redis-hawk monitoring options as decribed in the subsequent [Configuration](#configuration) section. 75 | ## Configuration 76 | 77 | Currently, configuration for your redis-hawk monitoring deployment must be managed via a `config.json`, located in the root directory of the repository. We will aim to support configuration directly via the web application in the near future. 78 | 79 | We support connecting via either a host/port combination or via a conenction URL. Using one option is required to monitor an instance. 80 | 81 |
82 | 83 | | Field | Description | 84 | | --- | --- | 85 | | **host** (string) | IP address of the Redis server | 86 | | **port** (number) | Port of the Redis server | 87 | | **url** (string) | URL of the Redis server. The format should be `[redis[s]:]//[[user][:password@]][host][:port][/db-number][?db=db-number[&password=bar[&option=value]]]`. For more information, please reference [IANA](https://www.iana.org/assignments/uri-schemes/prov/redis).| 88 | | **recordKeyspaceHistoryFrequency** (number) | The frequency at which keyspace histories should be recorded, in milliseconds. For more information, please see the configuration notes below. | 89 | 90 |
91 | 92 | The `config.json` defaults to monitoring the default local Redis instance via the following configuration options: 93 | 94 | ``` 95 | { 96 | "host": "127.0.0.1", 97 | "port": 6379, 98 | "recordKeyspaceHistoryFrequency": 60000 99 | } 100 | ``` 101 | ### Notes: 102 | 103 | * If setting `recordKeyspaceHistoryFrequency` to sub-minute frequencies or deployments with extensively high key volumes, please consider the impact on the performance for both your server and monitored Redis deployment. 104 | * Every `recordKeyspaceHistoryFrequency` milliseconds, the server will perform a non-blocking redis `SCAN` command against each database of the instance to record a snapshot of keyspace details. 105 | * While the `SCAN` command is non-blocking and rapid, it may have performance impacts for your Redis deployment if utilized very frequently for larger Redis deployments. 106 | * For more details on `SCAN` performance and behavior, please read the [Redis documentation](https://redis.io/commands/scan). 107 | 108 | ## Feature Roadmap 109 | 110 | The development team intends to continue improving redis-hawk and adding more features. Future features will include: 111 | 112 | * Ability to configure monitoring preferences directly via the `redis-hawk` UI. 113 | * Ability to configure maximum volumes of events and keyspace histories to record. 114 | * Additional graphs, such as viewing memory usage by keys over time 115 | * Additional database-level and instance-level metrics, such as overall memory usage and average key TTL 116 | * Performance recommendations based on observed metrics and patterns in your Redis deployment 117 | -------------------------------------------------------------------------------- /dist/server/redis-monitors/models/data-stores.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.KeyspaceHistory = exports.KeyspaceHistoriesLog = exports.KeyspaceEvent = exports.EventLog = void 0; 4 | var EventLog = (function () { 5 | function EventLog(maxLength) { 6 | if (!Number.isInteger(maxLength) || maxLength <= 0) 7 | throw new TypeError('maxLength must be positive integer!'); 8 | this.head = null; 9 | this.tail = null; 10 | this.eventTotal = 0; 11 | this.length = 0; 12 | this.maxLength = maxLength; 13 | } 14 | EventLog.prototype.add = function (key, event) { 15 | if (this.length >= this.maxLength) { 16 | this.head = this.head.next; 17 | this.head.previous = null; 18 | this.length -= 1; 19 | } 20 | var newEvent = new KeyspaceEvent(key, event); 21 | this.eventTotal += 1; 22 | this.length += 1; 23 | if (!this.head) { 24 | this.head = newEvent; 25 | this.tail = newEvent; 26 | } 27 | else { 28 | this.tail.next = newEvent; 29 | newEvent.previous = this.tail; 30 | this.tail = newEvent; 31 | } 32 | }; 33 | EventLog.prototype.reset = function () { 34 | this.head = null; 35 | this.tail = null; 36 | this.eventTotal = 0; 37 | this.length = 0; 38 | }; 39 | EventLog.prototype.returnLogAsArray = function (eventTotal) { 40 | if (eventTotal === void 0) { eventTotal = 0; } 41 | if (eventTotal < 0 || eventTotal >= this.eventTotal) 42 | return []; 43 | var logAsArray = []; 44 | var count = this.eventTotal - eventTotal; 45 | if (this.length < count) 46 | count = this.length; 47 | var current = this.tail; 48 | while (count > 0) { 49 | var event_1 = { 50 | key: current.key, 51 | event: current.event, 52 | timestamp: current.timestamp 53 | }; 54 | logAsArray.push(event_1); 55 | current = current.previous; 56 | count -= 1; 57 | } 58 | return logAsArray; 59 | }; 60 | return EventLog; 61 | }()); 62 | exports.EventLog = EventLog; 63 | var KeyspaceEvent = (function () { 64 | function KeyspaceEvent(key, event) { 65 | if (typeof (key) !== 'string' || typeof (event) !== 'string') { 66 | throw new TypeError('KeyspaceEvent must be constructed with string args'); 67 | } 68 | this.key = key; 69 | this.event = event; 70 | this.timestamp = Date.now(); 71 | this.next = null; 72 | this.previous = null; 73 | } 74 | return KeyspaceEvent; 75 | }()); 76 | exports.KeyspaceEvent = KeyspaceEvent; 77 | var KeyspaceHistoriesLog = (function () { 78 | function KeyspaceHistoriesLog(maxLength) { 79 | if (!Number.isInteger(maxLength) || maxLength <= 0) 80 | throw new TypeError('maxLength must be positive integer!'); 81 | this.head = null; 82 | this.tail = null; 83 | this.historiesCount = 0; 84 | this.maxLength = maxLength; 85 | this.length = 0; 86 | } 87 | KeyspaceHistoriesLog.prototype.add = function (keyDetails) { 88 | if (this.length >= this.maxLength) { 89 | this.head = this.head.next; 90 | this.head.previous = null; 91 | this.length -= 1; 92 | } 93 | var newHistory = new KeyspaceHistory(keyDetails); 94 | this.historiesCount += 1; 95 | this.length += 1; 96 | if (!this.head) { 97 | this.head = newHistory; 98 | this.tail = newHistory; 99 | } 100 | else { 101 | this.tail.next = newHistory; 102 | newHistory.previous = this.tail; 103 | this.tail = newHistory; 104 | } 105 | }; 106 | KeyspaceHistoriesLog.prototype.reset = function () { 107 | this.head = null; 108 | this.tail = null; 109 | this.historiesCount = 0; 110 | this.length = 0; 111 | }; 112 | KeyspaceHistoriesLog.prototype.returnLogAsArray = function (historiesCount) { 113 | if (historiesCount === void 0) { historiesCount = 0; } 114 | if (historiesCount < 0 || historiesCount >= this.historiesCount) 115 | return []; 116 | var logAsArray = []; 117 | var count = this.historiesCount - historiesCount; 118 | if (this.length < count) 119 | count = this.length; 120 | var current = this.tail; 121 | while (count > 0) { 122 | var history_1 = { 123 | keys: current.keys, 124 | timestamp: current.timestamp 125 | }; 126 | logAsArray.push(history_1); 127 | current = current.previous; 128 | count -= 1; 129 | } 130 | return logAsArray; 131 | }; 132 | return KeyspaceHistoriesLog; 133 | }()); 134 | exports.KeyspaceHistoriesLog = KeyspaceHistoriesLog; 135 | ; 136 | var KeyspaceHistory = (function () { 137 | function KeyspaceHistory(keys) { 138 | if (!Array.isArray(keys)) { 139 | throw new TypeError('KeyspaceHistory must be constructed with an array of KeyDetails'); 140 | } 141 | this.keys = keys; 142 | this.timestamp = Date.now(); 143 | this.next = null; 144 | this.previous = null; 145 | } 146 | return KeyspaceHistory; 147 | }()); 148 | exports.KeyspaceHistory = KeyspaceHistory; 149 | ; 150 | -------------------------------------------------------------------------------- /src/client/components/styles/styles.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); 2 | 3 | html { 4 | height: 100vh; 5 | width: 100vw; 6 | } 7 | 8 | body { 9 | /* display: inline-block; */ 10 | height: 100%; 11 | width: 100%; 12 | margin: 0 auto; 13 | padding: 1em 0; 14 | background-color: rgb(35, 35, 35); 15 | color: rgb(200, 200, 200); 16 | } 17 | 18 | #root { 19 | height: 100%; 20 | width: 100%; 21 | font-family: 'Roboto', sans-serif; 22 | } 23 | 24 | #app { 25 | height: 100%; 26 | width: 100%; 27 | position: relative; 28 | display: flex; 29 | } 30 | 31 | #graphsComponentContainer { 32 | position: absolute; 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | top: 30%; 37 | height: 55%; 38 | width: 60%; 39 | left: 30%; 40 | border: 1px solid #000000; 41 | background-color: black; 42 | box-sizing: border-box; 43 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 44 | overflow: scroll; 45 | } 46 | 47 | #eventComponentContainer { 48 | position: absolute; 49 | display: flex; 50 | flex-direction: column; 51 | justify-content: center; 52 | align-items: center; 53 | top: 30%; 54 | left: 30%; 55 | height: 55%; 56 | width: 60%; 57 | border: 1px solid #000000; 58 | box-sizing: border-box; 59 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 60 | } 61 | 62 | #keyspaceComponentContainer { 63 | display: flex; 64 | flex-direction: column; 65 | justify-content: center; 66 | position: absolute; 67 | top: 50%; 68 | left: 30%; 69 | height: 55%; 70 | width: 60%; 71 | border: 1px solid #000000; 72 | box-sizing: border-box; 73 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 74 | } 75 | #tableContainer { 76 | background-color: black; 77 | } 78 | 79 | #tableCell { 80 | /* color: white; */ 81 | } 82 | 83 | #tableBody { 84 | /* color: white; */ 85 | } 86 | 87 | .filterNavContainer { 88 | position: absolute; 89 | display: flex; 90 | justify-content: space-between; 91 | flex-direction: row; 92 | top: 18%; 93 | left: 30%; 94 | height: 25%; 95 | width: 60%; 96 | border-bottom: 1px solid grey; 97 | box-sizing: border-box; 98 | } 99 | 100 | .filterNavContainer > .filter-button { 101 | background-color: rgb(233, 0, 0); 102 | border: 1px solid rgb(233, 0, 0); 103 | border-radius: 5px; 104 | margin: 2em 0; 105 | color: rgb(0, 0, 0); 106 | } 107 | 108 | #filter-form { 109 | display: flex; 110 | align-items: center; 111 | } 112 | 113 | #filter-form > * { 114 | margin: 0 0.25em; 115 | } 116 | 117 | #filter-form > .form-filter-button { 118 | background-color: transparent; 119 | border: 1px solid rgb(201, 76, 3); 120 | color: rgb(200, 200, 200); 121 | border-radius: 5px; 122 | padding: 0.25em 0.5em; 123 | width: 10%; 124 | } 125 | 126 | .filterNavContainer > .filter-button:hover, 127 | .form-filter-button:hover { 128 | cursor: pointer; 129 | } 130 | 131 | #refreshButton { 132 | position: relative; 133 | width: 10%; 134 | 135 | z-index: 1; 136 | } 137 | 138 | .searchFilterDiv { 139 | display: flex; 140 | flex-direction: row; 141 | justify-content: center; 142 | align-items: center; 143 | } 144 | 145 | .filterContainer { 146 | display: flex; 147 | flex-direction: column; 148 | justify-content: center; 149 | align-items: center; 150 | width: 40%; 151 | } 152 | 153 | .databaseNavContainer { 154 | position: absolute; 155 | top: 5%; 156 | height: 80%; 157 | width: 10%; 158 | box-sizing: border-box; 159 | border: 1px solid grey; 160 | border-radius: 10px; 161 | overflow-y: auto; 162 | padding: 1em 2.5em 1em; 163 | font-size: 1.1em; 164 | } 165 | 166 | .InstanceNav-Container { 167 | position: absolute; 168 | top: 5%; 169 | left: 10%; 170 | height: 80%; 171 | width: 18%; 172 | box-sizing: border-box; 173 | border: 1px solid grey; 174 | border-radius: 10px; 175 | overflow-y: auto; 176 | padding: 1em 2.5em 1em; 177 | font-size: 1.1em; 178 | } 179 | 180 | #instanceHolder > .singleInstance { 181 | padding: 0.25em 0 0.25em 1em; 182 | } 183 | 184 | #instanceHolder > .singleInstance:hover { 185 | cursor: pointer; 186 | } 187 | 188 | #redisInstance > p { 189 | margin: 0.5em 0.25em; 190 | } 191 | 192 | #redisInstance span { 193 | font-weight: bold; 194 | text-decoration: underline; 195 | } 196 | 197 | #databaseHolder > .singleDatabase { 198 | padding: 0.25em 0 0.25em 1em; 199 | } 200 | 201 | #databaseHolder > .singleDatabase:hover { 202 | cursor: pointer; 203 | } 204 | 205 | #pageNavContainer { 206 | position: absolute; 207 | display: flex; 208 | flex-direction: row; 209 | justify-content: space-evenly; 210 | align-items: center; 211 | top: 2%; 212 | left: 25%; 213 | height: 15%; 214 | width: 70%; 215 | } 216 | 217 | #pageNavContainer .page-toggle { 218 | text-decoration: none; 219 | color: rgb(233, 0, 0); 220 | border: 1px solid red; 221 | border-radius: 5px; 222 | padding: 1em 6em; 223 | } 224 | 225 | #pageNavContainer .selected-page-toggle { 226 | text-decoration: none; 227 | color: white; 228 | background-color: rgb(233, 0, 0); 229 | border-radius: 5px; 230 | padding: 1em 6em; 231 | } 232 | 233 | #keyListComponentContainer { 234 | position: absolute; 235 | height: 20px; 236 | width: 100px; 237 | color: black; 238 | background-color: orange; 239 | } 240 | 241 | #redisInstance { 242 | height: 10%; 243 | } 244 | #databaseHolder { 245 | height: 90%; 246 | } 247 | 248 | .MuiToolbar-root { 249 | color: rgb(200, 200, 200); 250 | font-family: 'Roboto', sans-serif; 251 | } 252 | .MuiDataGrid-main { 253 | color: rgb(200, 200, 200); 254 | font-family: 'Roboto', sans-serif; 255 | } 256 | 257 | .MuiInputLabel-formControl { 258 | color: rgb(200, 200, 200); 259 | font-family: 'Roboto', sans-serif; 260 | } 261 | 262 | .MuiFormControl-root { 263 | color: rgb(200, 200, 200); 264 | } 265 | 266 | .MuiInputBase-root { 267 | color: rgb(200, 200, 200); 268 | } 269 | 270 | .table-header { 271 | font-weight: bold; 272 | } 273 | -------------------------------------------------------------------------------- /dist/server/controllers/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (_) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | Object.defineProperty(exports, "__esModule", { value: true }); 39 | exports.getKeyspace = void 0; 40 | var getValue = function (key, type, redisClient) { return __awaiter(void 0, void 0, void 0, function () { 41 | var value, _a; 42 | return __generator(this, function (_b) { 43 | switch (_b.label) { 44 | case 0: 45 | _a = type; 46 | switch (_a) { 47 | case 'string': return [3, 1]; 48 | case 'list': return [3, 3]; 49 | case 'set': return [3, 5]; 50 | case 'zset': return [3, 7]; 51 | case 'hash': return [3, 9]; 52 | } 53 | return [3, 11]; 54 | case 1: return [4, redisClient.get(key)]; 55 | case 2: 56 | value = _b.sent(); 57 | ; 58 | return [3, 11]; 59 | case 3: return [4, redisClient.lrange(key, 0, -1)]; 60 | case 4: 61 | value = _b.sent(); 62 | ; 63 | return [3, 11]; 64 | case 5: return [4, redisClient.smembers(key)]; 65 | case 6: 66 | value = _b.sent(); 67 | ; 68 | return [3, 11]; 69 | case 7: return [4, redisClient.zrange(key, 0, -1)]; 70 | case 8: 71 | value = _b.sent(); 72 | ; 73 | return [3, 11]; 74 | case 9: return [4, redisClient.hgetall(key)]; 75 | case 10: 76 | value = _b.sent(); 77 | ; 78 | return [3, 11]; 79 | case 11: 80 | ; 81 | return [2, value]; 82 | } 83 | }); 84 | }); }; 85 | var getKeyspace = function (redisClient, dbIdx) { return __awaiter(void 0, void 0, void 0, function () { 86 | var res, scanResults, cursor, keys, _i, keys_1, key, type, value; 87 | return __generator(this, function (_a) { 88 | switch (_a.label) { 89 | case 0: 90 | res = []; 91 | return [4, redisClient.select(dbIdx)]; 92 | case 1: 93 | _a.sent(); 94 | return [4, redisClient.scan('0', 'COUNT', '100')]; 95 | case 2: 96 | scanResults = _a.sent(); 97 | cursor = scanResults[0]; 98 | keys = scanResults[1]; 99 | _a.label = 3; 100 | case 3: 101 | _i = 0, keys_1 = keys; 102 | _a.label = 4; 103 | case 4: 104 | if (!(_i < keys_1.length)) return [3, 8]; 105 | key = keys_1[_i]; 106 | return [4, redisClient.type(key)]; 107 | case 5: 108 | type = _a.sent(); 109 | return [4, getValue(key, type, redisClient)]; 110 | case 6: 111 | value = _a.sent(); 112 | res.push({ 113 | key: key, 114 | type: type, 115 | value: value 116 | }); 117 | _a.label = 7; 118 | case 7: 119 | _i++; 120 | return [3, 4]; 121 | case 8: return [4, redisClient.scan(cursor, 'COUNT', '100')]; 122 | case 9: 123 | scanResults = _a.sent(); 124 | cursor = scanResults[0]; 125 | keys = scanResults[1]; 126 | _a.label = 10; 127 | case 10: 128 | if (cursor !== '0') return [3, 3]; 129 | _a.label = 11; 130 | case 11: return [2, res]; 131 | } 132 | }); 133 | }); }; 134 | exports.getKeyspace = getKeyspace; 135 | -------------------------------------------------------------------------------- /__tests__/server/data-stores.ts: -------------------------------------------------------------------------------- 1 | const { EventLog, KeyspaceEvent } = require('../../src/server/redis-monitors/models/data-stores'); 2 | 3 | //These unit tests need to be updated based on changed data stores. 4 | //Route integration tests still provide coverage and are currently consistently passing. 5 | xdescribe('RedisMonitors Data Stores Unit Tests', () => { 6 | 7 | xdescribe('Keyspace Event', () => { 8 | 9 | const key = 'message:1'; 10 | const eventType = 'set'; 11 | const event = new KeyspaceEvent(key, eventType); 12 | 13 | it('should have a next and previous property', () => { 14 | expect(event).toEqual(expect.objectContaining({ 15 | previous: null, 16 | next: null 17 | })); 18 | }); 19 | 20 | it('should set the key and event properties using the constructor arguments', () => { 21 | expect(event.key).toEqual(key); 22 | expect(event.event).toEqual(eventType); 23 | }) 24 | 25 | it('should have a timestamp property for when the KeyspaceEvent was initialized', () => { 26 | expect(Date.now() - event.timestamp).toBeLessThan(1000); //less than 1000ms ago 27 | }) 28 | 29 | it('should not allow non-string arguments', () => { 30 | expect(() => { 31 | new KeyspaceEvent(1, eventType) 32 | }).toThrow(TypeError); 33 | 34 | expect(() => { 35 | new KeyspaceEvent(key, []) 36 | }).toThrow(TypeError); 37 | }); 38 | 39 | }); 40 | 41 | xdescribe('Event Log', () => { 42 | 43 | let eventLog = new EventLog(); 44 | 45 | it('should have a head and a tail', () => { 46 | expect(eventLog).toEqual(expect.objectContaining({ 47 | head: null, 48 | tail: null, 49 | })); 50 | }); 51 | 52 | it('should have an eventTotal initialized to 0', () => { 53 | expect(eventLog.eventTotal).toEqual(0); 54 | }); 55 | 56 | describe('Add Method', () => { 57 | 58 | let eventLog = new EventLog(); 59 | 60 | it('should add the first Keyspace Event as the head and tail', () => { 61 | 62 | const event = new KeyspaceEvent('somekey', 'get'); 63 | eventLog.add('somekey', 'get'); 64 | expect(eventLog.head.key).toEqual(event.key); 65 | expect(eventLog.head.event).toEqual(event.event); 66 | expect(eventLog.tail.key).toEqual(event.key); 67 | expect(eventLog.tail.event).toEqual(event.event); 68 | }); 69 | 70 | it('should change the tail when adding additional Keyspace Events', () => { 71 | eventLog.add('anotherkey', 'set'); 72 | expect(eventLog.tail).not.toBe(eventLog.head); 73 | }) 74 | 75 | it('should not change the head when adding additional Keyspace Events', () => { 76 | const currentHead = eventLog.head; 77 | eventLog.add('thirdkey', 'mget'); 78 | expect(eventLog.head).toBe(currentHead); 79 | }); 80 | 81 | it('should set the previous and next properties correctly on the newest Keyspace Event', () => { 82 | eventLog.add('fourthkey', 'set'); 83 | expect(eventLog.tail.previous.key).toEqual('thirdkey'); 84 | expect(eventLog.tail.next).toEqual(null); 85 | }); 86 | 87 | it('should properly set the next property of the previous KeyspaceEvent when adding a new KeyspaceEvent', () => { 88 | expect(eventLog.tail.previous.next).toBe(eventLog.tail); 89 | }); 90 | 91 | }); 92 | 93 | describe('returnLogAsArray method', () => { 94 | 95 | const eventLog = new EventLog(); 96 | 97 | const newEvents = [ 98 | { key: 'message:1', event: 'set' }, 99 | { key: 'message:2', event: 'set' }, 100 | { key: 'message:3', event: 'set' }, 101 | { key: 'message:3', event: 'get' }, 102 | { key: 'message:4', event: 'set' }, 103 | { key: 'message:1', event: 'get' }, 104 | ]; 105 | 106 | newEvents.forEach(event => { 107 | eventLog.add(event.key, event.event); 108 | }) 109 | 110 | it('should return an array of KeyspaceEvent objects', () => { 111 | 112 | const arrayLog = eventLog.returnLogAsArray(); 113 | expect(arrayLog).toBeInstanceOf(Array); 114 | arrayLog.forEach(event => { 115 | expect(event).toEqual(expect.objectContaining({ 116 | key: expect.any(String), 117 | event: expect.any(String), 118 | timestamp: expect.any(Number) 119 | })); 120 | }); 121 | }); 122 | 123 | it('should return the KeyspaceEvent objects in the order of most recent to least recent', () => { 124 | 125 | const arrayLog = eventLog.returnLogAsArray(); 126 | let previousTimestamp = arrayLog[0].timestamp; 127 | 128 | arrayLog.slice(1).forEach(event => { 129 | expect(previousTimestamp).toBeGreaterThanOrEqual(event.timestamp); 130 | previousTimestamp = event.timestamp; 131 | }); 132 | }); 133 | 134 | it('should return all of the KeyspaceEvents in the log if no eventTotal argument is provided', () => { 135 | 136 | const arrayLog = eventLog.returnLogAsArray(); 137 | arrayLog.forEach((event, idx) => { 138 | expect(event.key).toEqual(newEvents[newEvents.length - idx - 1].key); 139 | expect(event.event).toEqual(newEvents[newEvents.length - idx - 1].event); 140 | }); 141 | }); 142 | 143 | it('should return the correct number of KeyspaceEvents if an eventTotal argument is provided', () => { 144 | 145 | const arrayLog = eventLog.returnLogAsArray(2); 146 | expect(arrayLog).toHaveLength(newEvents.length - 2); 147 | }); 148 | 149 | it('should return the correct number of KeyspaceEvents if an eventTotal argument is provided', () => { 150 | 151 | const arrayLog = eventLog.returnLogAsArray(2); 152 | expect(arrayLog).toHaveLength(newEvents.length - 2); 153 | }); 154 | 155 | it('should return the most recent KeyspaceEvents if an eventTotal argument is provided, in order of most to least recent', () => { 156 | 157 | const arrayLog = eventLog.returnLogAsArray(2); 158 | let previousTimestamp = arrayLog[0].timestamp; 159 | 160 | arrayLog.slice(1).forEach(event => { 161 | expect(previousTimestamp).toBeGreaterThanOrEqual(event.timestamp); 162 | previousTimestamp = event.timestamp; 163 | }); 164 | }); 165 | 166 | }); 167 | 168 | }); 169 | }); -------------------------------------------------------------------------------- /__tests__/client/redux/eventReducers.test.js: -------------------------------------------------------------------------------- 1 | import eventSubject from "../../../src/client/reducers/eventsReducer"; 2 | 3 | describe("events reducer", () => { 4 | let state; 5 | 6 | beforeEach(() => { 7 | state = { 8 | currInstance: 1, 9 | currDatabase: 0, 10 | events: [ 11 | { 12 | instanceId: 1, 13 | keyspaces: [ 14 | { 15 | eventTotal: 0, 16 | pageSize: 50, 17 | pageNum: 4, 18 | data: [ 19 | { 20 | key: "loading", 21 | event: "loading", 22 | timestamp: "loading", 23 | }, 24 | ], 25 | }, 26 | ], 27 | }, 28 | ], 29 | }; 30 | }); 31 | describe("default state", () => { 32 | it("should return a default state when given an undefined input", () => { 33 | expect(eventSubject(undefined, { type: undefined })).toEqual(state); 34 | }); 35 | }); 36 | 37 | describe("unrecognized action types", () => { 38 | it("should return the original value without any duplication", () => { 39 | const action = { type: "wannabemyfriend?" }; 40 | expect(eventSubject(state, action)).toBe(state); 41 | }); 42 | }); 43 | 44 | describe("LOAD_ALL_EVENTS", () => { 45 | const action = { 46 | type: "LOAD_ALL_EVENTS", 47 | payload: { 48 | events: [ 49 | { 50 | instanceId: 1, 51 | keyspaces: [ 52 | { 53 | eventTotal: 0, 54 | pageSize: 1, 55 | pageNum: 1, 56 | data: [ 57 | { 58 | key: "Arthur", 59 | event: "Set", 60 | timestamp: "07:00", 61 | }, 62 | ], 63 | }, 64 | ], 65 | }, 66 | ], 67 | currDatabase: 0, 68 | }, 69 | }; 70 | it("loads all events from server", () => { 71 | const { events } = eventSubject(state, action); 72 | expect( 73 | events[state.currInstance - 1].keyspaces[state.currDatabase] 74 | ).toEqual({ 75 | key: "Arthur", 76 | event: "Set", 77 | timestamp: "07:00", 78 | }); 79 | }); 80 | it("returns a state object not strictly equal to the original", () => { 81 | const eventState = eventSubject(state, action); 82 | // expect(eventState).toEqual(state) 83 | expect(eventState).not.toBe(state); 84 | }); 85 | it("returns an events value not strictly equal to the original", () => { 86 | const { events } = eventSubject(state, action); 87 | expect(events).not.toBe(state.events); 88 | }); 89 | }); 90 | describe("REFRESH_EVENTS", () => { 91 | const action = { 92 | type: "REFRESH_EVENTS", 93 | payload: { 94 | events: [ 95 | { 96 | instanceId: 1, 97 | keyspaces: [ 98 | { 99 | eventTotal: 2, 100 | pageSize: 1, 101 | pageNum: 2, 102 | data: [ 103 | { 104 | key: "Arthur", 105 | event: "Set", 106 | timestamp: "07:00", 107 | }, 108 | { 109 | key: "Abby", 110 | event: "Set", 111 | timestamp: "07:10", 112 | }, 113 | ], 114 | }, 115 | ], 116 | }, 117 | ], 118 | currDatabase: 0, 119 | }, 120 | }; 121 | it("returns specific page of events", () => { 122 | const { events } = eventSubject(state, action); 123 | expect( 124 | events[state.currInstance - 1].keyspaces[state.currDatabase].data 125 | ).toEqual([ 126 | { 127 | key: "Arthur", 128 | event: "Set", 129 | timestamp: "07:00", 130 | }, 131 | { 132 | key: "Abby", 133 | event: "Set", 134 | timestamp: "07:10", 135 | }, 136 | ]); 137 | }); 138 | it("returns a state object not strictly equal to the original", () => { 139 | const eventState = eventSubject(state, action); 140 | // expect(eventState).toEqual(state) 141 | expect(eventState).not.toBe(state); 142 | }); 143 | it("returns an events value not strictly equal to the original", () => { 144 | const { events } = eventSubject(state, action); 145 | expect(events).not.toBe(state.events); 146 | }); 147 | }); 148 | 149 | describe("CHANGE_EVENTS_PAGE", () => { 150 | const action = { 151 | type: "CHANGE_EVENTS_PAGE", 152 | payload: { 153 | events: [ 154 | { 155 | instanceId: 1, 156 | keyspaces: [ 157 | { 158 | eventTotal: 2, 159 | pageSize: 1, 160 | pageNum: 2, 161 | data: [ 162 | { 163 | key: "Arthur", 164 | event: "Set", 165 | timestamp: "07:00", 166 | }, 167 | { 168 | key: "Abby", 169 | event: "Set", 170 | timestamp: "07:10", 171 | }, 172 | ], 173 | }, 174 | ], 175 | }, 176 | ], 177 | currDatabase: 0, 178 | }, 179 | }; 180 | it("changes the page of events", () => { 181 | const { events } = eventSubject(state, action); 182 | expect( 183 | events[state.currInstance - 1].keyspaces[state.currDatabase].data 184 | ).toEqual([ 185 | { 186 | key: "Arthur", 187 | event: "Set", 188 | timestamp: "07:00", 189 | }, 190 | { 191 | key: "Abby", 192 | event: "Set", 193 | timestamp: "07:10", 194 | }, 195 | ]); 196 | }); 197 | it("returns a state object not strictly equal to the original", () => { 198 | const eventState = eventSubject(state, action); 199 | // expect(eventState).toEqual(state) 200 | expect(eventState).not.toBe(state); 201 | }); 202 | it("returns an events value not strictly equal to the original", () => { 203 | const { events } = eventSubject(state, action); 204 | expect(events).not.toBe(state.events); 205 | }); 206 | }); 207 | }); 208 | -------------------------------------------------------------------------------- /src/client/components/navbars/FilterNav.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import SearchFilter from './SearchFilter.jsx'; 4 | import * as actions from '../../action-creators/connections'; 5 | import * as keyspaceActions from '../../action-creators/keyspaceConnections'; 6 | import * as eventActions from '../../action-creators/eventsConnections'; 7 | 8 | import '../styles/filternav.scss'; 9 | 10 | const mapStateToProps = (store) => { 11 | return { 12 | keyspace: store.keyspaceStore.keyspace, 13 | events: store.eventsStore.events, 14 | keyGraph: store.keyGraphStore.keyGraph, 15 | currDatabase: store.currDatabaseStore.currDatabase, 16 | currPage: store.currPageStore.currPage, 17 | currDisplay: store.currDisplayStore.currDisplay, 18 | currInstance: store.currInstanceStore.currInstance, 19 | pageSize: store.dataPageStore.pageSize, 20 | pageNum: store.dataPageStore.pageNum, 21 | }; 22 | }; 23 | const mapDispatchToProps = (dispatch) => ({ 24 | refreshEvents: (currInstance, currDatabase, pageSize, pageNum, refreshData) => 25 | dispatch( 26 | eventActions.refreshEventsActionCreator( 27 | currInstance, 28 | currDatabase, 29 | pageSize, 30 | pageNum, 31 | refreshData 32 | ) 33 | ), 34 | changeEventsPage: (instanceId, dbIndex, queryOptions) => 35 | dispatch( 36 | eventActions.changeEventsPageActionCreator( 37 | instanceId, 38 | dbIndex, 39 | queryOptions 40 | ) 41 | ), 42 | changeKeyspacePage: (instanceId, dbIndex, queryOptions) => 43 | dispatch( 44 | keyspaceActions.changeKeyspacePageActionCreator( 45 | instanceId, 46 | dbIndex, 47 | queryOptions 48 | ) 49 | ), 50 | refreshKeyspace: (instanceId, dbIndex, pageSize, pageNum, refreshScan) => 51 | dispatch( 52 | keyspaceActions.refreshKeyspaceActionCreator( 53 | instanceId, 54 | dbIndex, 55 | pageSize, 56 | pageNum, 57 | refreshScan 58 | ) 59 | ), 60 | updateKeyGraph: (keyGraph) => 61 | dispatch(actions.updateKeyGraphActionCreator(keyGraph)), 62 | updateCurrDisplay: (object) => 63 | dispatch(actions.updateCurrDisplayActionCreator(object)), 64 | updatePageNum: (pageNum) => { 65 | actions.updatePageActionCreator(pageNum); 66 | }, 67 | }); 68 | 69 | class FilterNav extends Component { 70 | constructor(props) { 71 | super(props); 72 | } 73 | 74 | render() { 75 | if (this.props.currPage === 'graphs') { 76 | return
; 77 | } else if (this.props.currPage === 'events') { 78 | return ( 79 |
80 | 92 |
93 | 121 |
122 |
123 | ); 124 | 125 | //KEYSPACE PAGE : COMPLETED 126 | } else { 127 | return ( 128 |
129 | 141 |
142 | 169 |
170 |
171 | ); 172 | } 173 | } 174 | } 175 | 176 | export default connect(mapStateToProps, mapDispatchToProps)(FilterNav); 177 | -------------------------------------------------------------------------------- /src/client/components/navbars/SearchFilter.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import InputLabel from '@material-ui/core/InputLabel'; 4 | import MenuItem from '@material-ui/core/MenuItem'; 5 | 6 | import FormControl from '@material-ui/core/FormControl'; 7 | import Select from '@material-ui/core/Select'; 8 | import Button from '@material-ui/core/Button'; 9 | import TextField from '@material-ui/core/TextField'; 10 | 11 | const useStyles = makeStyles((theme) => ({ 12 | formControl: { 13 | margin: theme.spacing(1), 14 | minWidth: 120, 15 | }, 16 | })); 17 | 18 | export default function SearchFilter(props) { 19 | const classes = useStyles(); 20 | const [value, setValue] = React.useState(''); 21 | const [category, setCategory] = React.useState(''); 22 | 23 | const handleChange = (event) => { 24 | setValue(event.target.value); 25 | console.log('handlechange', event.target.value); 26 | }; 27 | function selectChange(event) { 28 | setCategory(event.target.value); 29 | console.log('selectchange', event.target.value); 30 | } 31 | 32 | //submitting the filter 33 | function handleSubmit() { 34 | //change the state of currDisplay 35 | props.updateCurrDisplay({ 36 | filterType: 'keyType', 37 | filterValue: category, 38 | }); 39 | props.updateCurrDisplay({ filterType: 'keyName', filterValue: value }); 40 | 41 | const queryOptions = { 42 | pageSize: props.pageSize, 43 | pageNum: props.pageNum, 44 | keyNameFilter: props.currDisplay.keyNameFilter, 45 | keyTypeFilter: props.currDisplay.keyTypeFilter, 46 | refreshScan: 0, 47 | }; 48 | props.changeKeyspacePage( 49 | props.currInstance, 50 | props.currDatabase, 51 | queryOptions 52 | ); 53 | } 54 | 55 | function handleEventSubmit() { 56 | props.updateCurrDisplay({ 57 | filterType: 'keyEvent', 58 | filterValue: category, 59 | }); 60 | props.updateCurrDisplay({ filterType: 'keyName', filterValue: value }); 61 | const queryOptions = { 62 | pageSize: props.pageSize, 63 | pageNum: props.pageNum, 64 | keyNameFilter: props.currDisplay.keyNameFilter, 65 | keyEventFilter: props.currDisplay.keyEventFilter, 66 | refreshScan: 0, 67 | }; 68 | console.log('myquery options in handle event submit', queryOptions); 69 | props.changeEventsPage( 70 | props.currInstance, 71 | props.currDatabase, 72 | queryOptions 73 | ); 74 | } 75 | 76 | function clearFilter() { 77 | setValue(''); 78 | setCategory(''); 79 | document.getElementById('standard-secondary').value = ''; 80 | 81 | props.updateCurrDisplay({ filterType: 'keyName', filterValue: '' }); 82 | props.updateCurrDisplay({ filterType: 'keyType', filterValue: '' }); 83 | console.log('value', value); 84 | console.log('category', category); 85 | const queryOptions = { 86 | pageSize: props.pageSize, 87 | pageNum: props.pageNum, 88 | refreshScan: 0, 89 | keyNameFilter: props.currDisplay.keyNameFilter, 90 | keyTypeFilter: props.currDisplay.keyTypeFilter, 91 | }; 92 | props.changeKeyspacePage( 93 | props.currInstance, 94 | props.currDatabase, 95 | queryOptions 96 | ); 97 | } 98 | 99 | function clearEventFilter() { 100 | setValue(''); 101 | setCategory(''); 102 | document.getElementById('standard-secondary').value = ''; 103 | document.getElementById('secondary-secondary').value = ''; 104 | 105 | props.updateCurrDisplay({ filterType: 'keyName', filterValue: '' }); 106 | props.updateCurrDisplay({ filterType: 'keyEvent', filterValue: '' }); 107 | const queryOptions = { 108 | pageSize: props.pageSize, 109 | pageNum: props.pageNum, 110 | refreshScan: 0, 111 | keyNameFilter: props.currDisplay.keyNameFilter, 112 | keyEventFilter: props.currDisplay.keyEventFilter, 113 | }; 114 | props.changeEventsPage( 115 | props.currInstance, 116 | props.currDatabase, 117 | queryOptions 118 | ); 119 | } 120 | 121 | const newArea = []; 122 | 123 | if (props.currPage === 'keyspace') { 124 | return ( 125 |
126 | 127 | 133 | 134 | 135 | key type filter 136 | 146 | 147 |
150 | 153 | 154 | 157 | {/* */} 158 |
159 |
{newArea}
160 |
161 | ); 162 | 163 | //////////////////////// 164 | } else if (props.currPage === 'events') { 165 | return ( 166 |
167 | 168 | 174 | 175 | 176 | 182 | 183 |
186 | 189 | 192 | {/* */} 193 |
194 |
{newArea}
195 |
196 | ); 197 | } 198 | } 199 | --------------------------------------------------------------------------------