├── 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 |
9 | {`db${i}`}
10 |
11 | );
12 | }
13 |
14 | return (
15 |
16 | Select Database:
17 | {
20 | props.switchDatabase(e.target.value);
21 | }}
22 | value={props.currDatabase}
23 | >
24 | {options}
25 |
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 | {
34 | e.preventDefault();
35 | if (this.props.intervalStart) {
36 | this.props.clearInt();
37 | } else {
38 | this.props.setInt();
39 | }
40 | }}>
41 | Pause Interval
42 |
43 | {
47 | e.preventDefault();
48 |
49 | document.getElementById("my-text-field").value = "";
50 | this.props.clearInt();
51 | this.props.clearFilterIntID();
52 | this.props.resetState();
53 | // this.props.getInitialData();
54 | this.props.getMoreData();
55 | this.props.setInt();
56 | }}>
57 | Refresh
58 |
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 | {
35 | console.log("props.intervalStart", this.props.intervalStart);
36 | // console.log("intervalId in click", this.props.intervalId);
37 | if (this.props.intervalStart) {
38 | this.props.clearInt();
39 | } else {
40 | console.log("setting interval");
41 | this.props.setInt();
42 | }
43 | }}>
44 | Pause Interval
45 |
46 | {
50 | e.preventDefault();
51 | document.getElementById("standard-secondary").value = "";
52 | document.getElementById("event-type-filter").value = "";
53 | this.props.clearInt();
54 | this.props.clearFilterIntID();
55 | this.props.resetState();
56 | // this.props.getInitialData()
57 | this.props.getMoreData();
58 | this.props.setInt();
59 | }}>
60 | Refresh
61 |
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 | ,
44 |
,
45 | ])
46 | ).toEqual(true);
47 | });
48 |
49 | it("the div with the id totals should render the number of total keys if on the keyspace page", () => {
50 | wrapper = shallow( );
51 | expect(wrapper.find("totals")).toHaveLength(1);
52 | expect(wrapper.containsAllMatchingElements([1
]));
53 | });
54 |
55 | it("the div with the id totals should render the number of total events if on the events page", () => {
56 | wrapper = shallow( );
57 | expect(wrapper.find("totals")).toHaveLength(1);
58 | expect(wrapper.containsAllMatchingElements([1
]));
59 | });
60 | });
61 |
62 | describe("SearchFilter", () => {
63 | let wrapper;
64 | const props = {
65 | whichPage: "keyspace",
66 | keyspace: [
67 | [
68 | {
69 | name: "keyName",
70 | value: "hello",
71 | time: "11:52",
72 | },
73 | ],
74 | ],
75 | events: [
76 | [
77 | {
78 | name: "hey!",
79 | event: "scan",
80 | time: "8:30",
81 | },
82 | ],
83 | ],
84 | currDatabase: 0,
85 | };
86 |
87 | it("renders an input with an id searchInput", () => {
88 | wrapper = shallow( );
89 | expect(wrapper.find("searchInput").to.have.lengthOf(1));
90 | });
91 |
92 | it("should render two divs with classname filterType when whichPage is equal to keyspace", () => {
93 | wrapper = shallow( );
94 | expect(wrapper.find(".filterType")).toHaveLength(2);
95 | });
96 |
97 | it("should render three divs with classname filterType when whichPage is equal to events", () => {
98 | wrapper = shallow( );
99 | expect(wrapper.find(".filterType")).toHaveLength(3);
100 | });
101 |
102 | it("should render four divs with classname filterType when whichPage is equal to graphs", () => {
103 | wrapper = shallow( );
104 | expect(wrapper.find(".filterType")).toHaveLength(4);
105 | });
106 | });
107 | });
108 |
--------------------------------------------------------------------------------
/dist/server/controllers/monitorsController.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 | var __importDefault = (this && this.__importDefault) || function (mod) {
39 | return (mod && mod.__esModule) ? mod : { "default": mod };
40 | };
41 | Object.defineProperty(exports, "__esModule", { value: true });
42 | var redis_monitors_1 = __importDefault(require("../redis-monitors/redis-monitors"));
43 | var monitorsController = {
44 | findAllMonitors: function (req, res, next) { return __awaiter(void 0, void 0, void 0, function () {
45 | return __generator(this, function (_a) {
46 | res.locals.monitors = redis_monitors_1.default;
47 | return [2, next()];
48 | });
49 | }); },
50 | findSingleMonitor: function (req, res, next) { return __awaiter(void 0, void 0, void 0, function () {
51 | var _i, redisMonitors_1, monitor;
52 | return __generator(this, function (_a) {
53 | for (_i = 0, redisMonitors_1 = redis_monitors_1.default; _i < redisMonitors_1.length; _i++) {
54 | monitor = redisMonitors_1[_i];
55 | if (monitor.instanceId === +req.params.instanceId) {
56 | res.locals.monitors = [monitor];
57 | }
58 | }
59 | if (!res.locals.monitors) {
60 | return [2, next({ log: 'User provided invalid instanceId', status: 400, message: { err: 'Please provide a valid instanceId' } })];
61 | }
62 | return [2, next()];
63 | });
64 | }); },
65 | };
66 | exports.default = monitorsController;
67 |
--------------------------------------------------------------------------------
/src/client/components/navbars/PageNav.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter as Router, Link } from 'react-router-dom';
3 | import * as actions from '../../action-creators/connections';
4 | import * as keyspaceActions from '../../action-creators/keyspaceConnections';
5 | import * as eventsActions from '../../action-creators/eventsConnections';
6 |
7 | import { connect } from 'react-redux';
8 |
9 | import '../styles/pagenav.scss';
10 |
11 | const mapStateToProps = (store) => {
12 | return {
13 | currPage: store.currPageStore.currPage,
14 | currInstance: store.currInstanceStore.currInstance,
15 | currDatabase: store.currDatabaseStore.currDatabase,
16 | pageSize: store.dataPageStore.pageSize,
17 | };
18 | };
19 |
20 | const mapDispatchToProps = (dispatch) => ({
21 | updatePage: (page) => dispatch(actions.updatePageActionCreator(page)),
22 | updatePageNum: (num) => dispatch(actions.updatePageNumActionCreator(num)),
23 | updateCurrDisplay: (object) =>
24 | dispatch(actions.updateCurrDisplayActionCreator(object)),
25 | refreshKeyspace: (instanceId, dbIndex, pageSize, pageNum, refreshScan) =>
26 | dispatch(
27 | keyspaceActions.refreshKeyspaceActionCreator(
28 | instanceId,
29 | dbIndex,
30 | pageSize,
31 | pageNum,
32 | refreshScan
33 | )
34 | ),
35 | refreshEvents: (instanceId, dbIndex, pageSize, pageNum, refreshData) => {
36 | dispatch(
37 | eventsActions.refreshEventsActionCreator(
38 | instanceId,
39 | dbIndex,
40 | pageSize,
41 | pageNum,
42 | refreshData
43 | )
44 | );
45 | },
46 | });
47 |
48 | const PageNav = (props) => {
49 | function handleKeyspaceClick() {
50 | document.getElementById('standard-secondary').value = '';
51 | document.getElementById('standard-secondary').value = '';
52 |
53 | // console.log('this.props', props);
54 | props.updateCurrDisplay({ filterType: 'keyName', filterValue: '' });
55 | props.updateCurrDisplay({ filterType: 'keyType', filterValue: '' });
56 | props.updatePage('keyspace');
57 | props.refreshKeyspace(
58 | props.currInstance,
59 | props.currDatabase,
60 | props.pageSize,
61 | 1,
62 | 1
63 | );
64 | //need to have current graph updated to page 1 -- re render?
65 | props.updatePageNum(1);
66 | }
67 | function handleEventsClick() {
68 | document.getElementById('standard-secondary').value = '';
69 |
70 | props.updateCurrDisplay({ filterType: 'keyName', filterValue: '' });
71 | props.updateCurrDisplay({ filterType: 'keyEvent', filterValue: '' });
72 | props.updatePage('events');
73 | props.refreshEvents(
74 | props.currInstance,
75 | props.currDatabase,
76 | props.pageSize,
77 | 1,
78 | 1
79 | );
80 | }
81 | function handleGraphsClick() {}
82 | return (
83 |
84 |
85 |
94 | Keyspace
95 |
96 |
97 |
98 |
105 | Events
106 |
107 |
108 |
109 |
{
115 | props.updatePage('graphs');
116 | }}
117 | >
118 | Graphs
119 |
120 |
121 |
122 | );
123 | };
124 |
125 | export default connect(mapStateToProps, mapDispatchToProps)(PageNav);
126 |
--------------------------------------------------------------------------------
/src/client/components/keyspace/KeyspaceTable.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 | import '../styles/tables.scss';
6 |
7 | function KeyspaceTable(props) {
8 | // console.log('props in keyspace table', props);
9 |
10 | const [pageSize, setPageSize] = React.useState(25);
11 | const [loading, setLoading] = React.useState(false);
12 |
13 | const handlePageChange = (params) => {
14 | props.updatePageNum(params.page + 1);
15 |
16 | const funcOptions = {
17 | pageSize: props.pageSize,
18 | pageNum: params.page + 1,
19 | keyNameFilter: props.currDisplay.keyNameFilter,
20 | keyTypeFilter: props.currDisplay.keyTypeFilter,
21 | refreshScan: 0,
22 | };
23 |
24 | props.changeKeyspacePage(
25 | props.currInstance,
26 | props.currDatabase,
27 | funcOptions
28 | );
29 | };
30 |
31 | const handlePageSizeChange = (params) => {
32 | setPageSize(params.pageSize);
33 | props.updatePageSize(params.pageSize);
34 |
35 | //this is so if your current page is 50 and you select 100, the page will refresh with 100
36 | if (params.pageSize > props.pageSize) {
37 | const funcOptions = {
38 | pageSize: params.pageSize,
39 | pageNum: params.page + 1,
40 | refreshScan: 0,
41 | keyNameFilter: props.currDisplay.keyNameFilter,
42 | keyTypeFilter: props.currDisplay.keyTypeFilter,
43 | };
44 | props.changeKeyspacePage(
45 | props.currInstance,
46 | props.currDatabase,
47 | funcOptions
48 | );
49 | }
50 | };
51 |
52 | //replace all objects in an array with strings
53 |
54 | let data = [];
55 |
56 | //set data if there is data present (myCount is 0 if there is no key data)
57 | if (props.myCount) {
58 | data = props.keyspace[props.currInstance - 1]
59 | .keyspaces[props.currDatabase].data;
60 |
61 | for (let i = 0; i < data.length; i += 1) {
62 | data[i].id = i;
63 |
64 | if (data[i].type === 'hash') {
65 | // console.log('this is what i looked like before', data[i].value);
66 | let obj = data[i].value;
67 | // console.log('obj', obj);
68 | let objArray = Object.keys(obj);
69 | let newString = '';
70 | objArray.forEach((el) => {
71 | newString += `${el}: ${obj[el]}, `;
72 | });
73 | // console.log(newString);
74 | data[i].value = newString;
75 | // console.log('my new hash', data[i]);
76 | }
77 | }
78 | }
79 |
80 | const useStyles = makeStyles({
81 | dataGrid: {
82 | borderRadius: 3,
83 | border: 'solid rgb(200, 200, 200) 1px',
84 | color: 'rgb(200, 200, 200)',
85 | fontFamily: "'Nunito Sans', 'sans-serif'",
86 | },
87 | });
88 | const classes = useStyles();
89 |
90 | // console.log('data in keyspace table', data);
91 | return (
92 |
93 |
131 |
132 | );
133 | }
134 |
135 | export default KeyspaceTable;
136 |
--------------------------------------------------------------------------------
/src/client/action-creators/keyspaceConnections.js:
--------------------------------------------------------------------------------
1 | import * as types from '../actions/actionTypes';
2 |
3 | //called on initial load of app in App.jsx
4 | // no requirements in for the deployment of this action creator
5 | //response :
6 | // {
7 | // data: [ (array of instances)
8 | // {
9 | // instanceId: 1,
10 | // keyspaces: [ (array of databases)
11 | // {
12 | // keyTotal: 6347,
13 | // pageSize: 50,
14 | // pageNum: 4,
15 | // data: [
16 | // {
17 | // key: '',
18 | // value: any,
19 | // type: any,
20 | // },
21 | // ],
22 | // },
23 | // ],
24 | // },
25 | // ];
26 | // }
27 | export const loadKeyspaceActionCreator = () => (dispatch) => {
28 | fetch('/api/v2/keyspaces/?pageSize=25&refreshScan=0')
29 | .then((res) => res.json())
30 | .then((response) => {
31 | // console.log('response in loadKeyspaceActionCreator', response);
32 | let fullKeyspace = response.data;
33 | dispatch({
34 | type: types.LOAD_KEYSPACE,
35 | payload: {
36 | keyspace: fullKeyspace,
37 | },
38 | });
39 | });
40 | };
41 |
42 | //for refreshing the keyspace of a certain database at a certain instance - need to know if there are filters or not for dispatch
43 | // arguments: database, instance, page size, page num = 1, refreshScan = 1,
44 | //response:
45 | // {
46 | // keyTotal: 6347,
47 | // pageSize: 50,
48 | // pageNum: 4,
49 | // data: [
50 | // {
51 | // key: '',
52 | // value: '',
53 | // type: any,
54 | // }
55 | // ]
56 | // }
57 | export const refreshKeyspaceActionCreator =
58 | (instanceId, dbIndex, pageSize, pageNum, refreshScan) => (dispatch) => {
59 | fetch(
60 | `api/v2/keyspaces/${instanceId}/${dbIndex}/?pageSize=${pageSize}&pageNum=${pageNum}&refreshScan=${refreshScan}`
61 | )
62 | .then((res) => res.json())
63 | .then((response) => {
64 | console.log('response in refreshKeyspaceActionCreator', response);
65 | let refreshKeyspace = response;
66 | dispatch({
67 | type: types.REFRESH_KEYSPACE,
68 | payload: {
69 | keyspace: refreshKeyspace,
70 | currInstance: instanceId,
71 | currDatabase: dbIndex,
72 | },
73 | });
74 | });
75 | };
76 | //change the page and handle the filters for keyspace
77 | //requirements: instanceId, dbIndex, page Size, page num, keyname filter, keytype filter, refreshScan = 0 - need to know whether there is a
78 | //OPTIONS PARAMETER BEING USED HERE CALLED QUERYOPTIONS
79 | //response:
80 | // {
81 | // keyTotal: 6347,
82 | // pageSize: 50,
83 | // pageNum: 4,
84 | // data: [
85 | // {
86 | // key: '',
87 | // value: '',
88 | // type: any,
89 | // }
90 | // ]
91 | // }
92 | export const changeKeyspacePageActionCreator =
93 | (instanceId, dbIndex, queryOptions) => (dispatch) => {
94 | let URI = `api/v2/keyspaces/${instanceId}/${dbIndex}/?pageSize=${queryOptions.pageSize}&pageNum=${queryOptions.pageNum}`;
95 | //this may have an issue in here - be aware of queryOptions
96 | // if (queryOptions.pageSize) URI += `pageSize=${queryOptions.pageSize}`;
97 | // if (queryOptions.pageNum) URI += `&pageNum=${queryOptions.pageNum}`;
98 | if (queryOptions.keyNameFilter.length !== 0 || queryOptions.keyNameFilter)
99 | URI += `&keynameFilter=${queryOptions.keyNameFilter}`;
100 | if (queryOptions.keyTypeFilter.length !== 0 || queryOptions.keyTypeFilter)
101 | URI += `&keytypeFilter=${queryOptions.keyTypeFilter}`;
102 | if (queryOptions.refreshScan !== undefined)
103 | URI += `&refreshScan=${queryOptions.refreshScan}`;
104 |
105 | console.log('MY CHANGE KEYSPACE PAGE URI', URI);
106 |
107 | fetch(URI)
108 | .then((res) => res.json())
109 | .then((response) => {
110 | console.log('response in changeKeyspaceActionCreator', response);
111 | let nextPageKeyspace = response;
112 | dispatch({
113 | type: types.CHANGE_KEYSPACE_PAGE,
114 | payload: {
115 | keyspace: nextPageKeyspace,
116 | currInstance: instanceId,
117 | currDatabase: dbIndex,
118 | },
119 | });
120 | });
121 | };
122 |
--------------------------------------------------------------------------------
/__tests__/client/react/keyspaceComponent.test.js:
--------------------------------------------------------------------------------
1 | //need to complete pagination
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 |
9 | import KeyspaceComponent from '../../../src/client/components/keyspace/KeyspaceComponent.jsx';
10 | import KeyspaceTable from '../../../src/client/components/keyspace/KeyspaceTable.jsx';
11 |
12 | configure({ adapter: new Adapter() });
13 |
14 | describe('React Keyspace unit tests', () => {
15 | describe('KeyspaceComponent', () => {
16 | let wrapper;
17 | const props = {
18 | keyspace: [
19 | [
20 | {
21 | name: 'keyName',
22 | value: 'hello',
23 | type: 'SET',
24 | },
25 | ],
26 | ],
27 | currDisplay: 'keyspace',
28 | currDatabase: 0,
29 | };
30 |
31 | beforeAll(() => {
32 | wrapper = shallow( );
33 | });
34 |
35 | it('render the KeyspaceTable component with all props passed to the KeyspaceTable', () => {
36 | expect(wrapper.find('keyspaceComponentContainer').find('div'))
37 | expect(
38 | wrapper.containsAllMatchingElements([
39 |
40 |
45 |
46 | ])
47 | ).toEqual(true);
48 | });
49 | });
50 |
51 | describe('KeyspaceTable', () => {
52 | let wrapper;
53 | beforeAll(() => {
54 | wrapper = shallow( );
55 | });
56 |
57 | it('renders a div with Material UI keyspace components', () => {
58 | expect(wrapper.containsAllMatchingElements(
59 |
60 |
61 |
62 |
63 | Keyname
64 |
65 | Value
66 |
67 |
68 | Type
69 |
70 |
71 |
72 |
73 | {(rowsPerPage > 0
74 | ? rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
75 | : rows
76 | ).map((row) => (
77 |
78 |
84 | {row.keyname}
85 |
86 |
91 | {row.value}
92 |
93 |
98 | {row.type}
99 |
100 |
101 | ))}
102 |
103 | {emptyRows > 0 && (
104 |
105 |
106 |
107 | )}
108 |
109 |
110 |
111 |
126 |
127 |
128 |
129 |
130 | ))
131 | });
132 | });
133 |
134 |
135 | });
136 |
137 |
--------------------------------------------------------------------------------
/dist/server/redis-monitors/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.recordKeyspaceHistory = exports.promisifyClientMethods = void 0;
40 | var util_1 = require("util");
41 | var promisifyClientMethods = function (client) {
42 | client.config = util_1.promisify(client.config).bind(client);
43 | client.flushdb = util_1.promisify(client.flushdb).bind(client);
44 | client.flushall = util_1.promisify(client.flushall).bind(client);
45 | client.select = util_1.promisify(client.select).bind(client);
46 | client.scan = util_1.promisify(client.scan).bind(client);
47 | client.type = util_1.promisify(client.type).bind(client);
48 | client.set = util_1.promisify(client.set).bind(client);
49 | client.get = util_1.promisify(client.get).bind(client);
50 | client.mget = util_1.promisify(client.mget).bind(client);
51 | client.lrange = util_1.promisify(client.lrange).bind(client);
52 | client.smembers = util_1.promisify(client.smembers).bind(client);
53 | client.zrange = util_1.promisify(client.zrange).bind(client);
54 | client.hgetall = util_1.promisify(client.hgetall).bind(client);
55 | return client;
56 | };
57 | exports.promisifyClientMethods = promisifyClientMethods;
58 | var recordKeyspaceHistory = function (monitor, dbIndex) { return __awaiter(void 0, void 0, void 0, function () {
59 | var keyDetails, cursor, keys;
60 | var _a, _b;
61 | return __generator(this, function (_c) {
62 | switch (_c.label) {
63 | case 0:
64 | keyDetails = [];
65 | cursor = '0';
66 | keys = [];
67 | return [4, monitor.redisClient.select(dbIndex)];
68 | case 1:
69 | _c.sent();
70 | return [4, monitor.redisClient.scan(cursor)];
71 | case 2:
72 | _a = _c.sent(), cursor = _a[0], keys = _a[1];
73 | _c.label = 3;
74 | case 3:
75 | keys.forEach(function (key) {
76 | keyDetails.push({
77 | key: key,
78 | memoryUsage: 1
79 | });
80 | });
81 | return [4, monitor.redisClient.scan(cursor)];
82 | case 4:
83 | _b = _c.sent(), cursor = _b[0], keys = _b[1];
84 | _c.label = 5;
85 | case 5:
86 | if (cursor !== '0') return [3, 3];
87 | _c.label = 6;
88 | case 6:
89 | monitor.keyspaces[dbIndex].keyspaceHistories.add(keyDetails);
90 | return [2];
91 | }
92 | });
93 | }); };
94 | exports.recordKeyspaceHistory = recordKeyspaceHistory;
95 |
--------------------------------------------------------------------------------
/__tests__/client/react/eventComponent.test.js:
--------------------------------------------------------------------------------
1 | //need to complete pagination
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 EventTable from '../../../src/components/events/EventTable.jsx'
9 |
10 | configure({ adapter: new Adapter() });
11 |
12 | describe('React Events unit tests', () => {
13 | describe('EventComponent', () => {
14 | let wrapper;
15 | const props = {
16 | events: [[
17 | {
18 | name: 'hey!',
19 | event: 'scan',
20 | time: '8:30',
21 | },
22 | ]],
23 | currDisplay: 'events',
24 | currDatabase: 0
25 | };
26 |
27 | beforeAll(() => {
28 | wrapper = shallow( );
29 | });
30 |
31 | it('render a div with classname EventComponent-Container, and the EventTable component with currDisplay, currDatabase, and events properties passed into it', () => {
32 | expect(
33 | wrapper.containsAllMatchingElements([
34 |
37 |
42 |
,
43 | ])
44 | ).toEqual(true);
45 | });
46 | });
47 |
48 | describe('EventTable', () => {
49 | let wrapper;
50 | beforeAll(() => {
51 | wrapper = shallow( );
52 | });
53 |
54 | it('renders a div with of material UI components', () => {
55 | expect(
56 | wrapper.containsAllMatchingElements(
57 |
58 |
61 |
62 |
63 | Keyname
64 |
65 | Event
66 |
67 |
68 | Timestamp
69 |
70 |
71 |
72 |
73 | {(rowsPerPage > 0
74 | ? rows.slice(
75 | page * rowsPerPage,
76 | page * rowsPerPage + rowsPerPage
77 | )
78 | : rows
79 | ).map((row) => (
80 |
84 |
89 | {row.keyname}
90 |
91 |
95 | {row.event}
96 |
97 |
101 | {row.time}
102 |
103 |
104 | ))}
105 |
106 | {emptyRows > 0 && (
107 |
108 |
109 |
110 | )}
111 |
112 |
113 |
114 |
134 |
135 |
136 |
137 |
138 | )
139 | );
140 | });
141 | });
142 | });
143 |
--------------------------------------------------------------------------------
/src/server/redis-monitors/redis-monitors.ts:
--------------------------------------------------------------------------------
1 | /*
2 | Configures the RedisMonitors, which provide an interface for:
3 | * Working with a Redis client for a specific Redis instance
4 | * Reading monitored/logged data for Redis instances
5 |
6 | Each RedisMonitor will also create a keyspace notification subscriber
7 | for each keyspace and write them to an event log.
8 | */
9 |
10 | import * as fs from 'fs';
11 | import * as path from 'path';
12 | import * as redis from 'redis';
13 |
14 | import { RedisInstance, RedisMonitor, Keyspace } from './models/interfaces';
15 | import { EventLog, KeyspaceHistoriesLog } from './models/data-stores';
16 | import { promisifyClientMethods, recordKeyspaceHistory } from './utils';
17 | import { getKeyspace } from '../controllers/utils';
18 |
19 | const instances: RedisInstance[] = process.env.IS_TEST ?
20 | JSON.parse(fs.readFileSync(path.resolve(__dirname, '../tests-config/tests-config.json')).toString())
21 | : JSON.parse(fs.readFileSync(path.resolve(__dirname, '../../../config.json')).toString());
22 |
23 | const redisMonitors: RedisMonitor[] = [];
24 |
25 | const initMonitor = async (monitor: RedisMonitor): Promise => {
26 | /*
27 | For a given initialized RedisMonitor, configures the monitoring behaviors for the monitored instance.
28 | */
29 |
30 | redisMonitors.push(monitor);
31 | //Subscribe to all keyspace events
32 | try {
33 | await monitor.redisClient.config('SET', 'notify-keyspace-events', monitor.notifyKeyspaceEvents);
34 | } catch (e) {
35 | console.log('Could not configure client to publish keyspace event notifications.\n' +
36 | 'This instance will not be monitored. Please check the notifyKeyspaceEvents setting ' +
37 | 'in the config.json');
38 | return;
39 | }
40 |
41 | let res;
42 | try {
43 | res = await monitor.redisClient.config('GET', 'databases')
44 | //Sets the number of databases present in this monitored Redis instance
45 | monitor.databases = +res[1];
46 |
47 | } catch (e) {
48 | console.log(`Could not get database count from client`);
49 | }
50 |
51 | monitor.keyspaceSubscriber.psubscribe('__keyspace@*__:*');
52 |
53 | //Configures each keyspace with a event subscriber/event log
54 | //Additionally auto-saves keyspace histories with frequency in JSON config
55 | //This should be futher modularized for readability and maintanability
56 | for (let dbIndex = 0; dbIndex < monitor.databases; dbIndex++) {
57 | const eventLog = new EventLog(monitor.maxEventLogSize);
58 | monitor.keyspaceSubscriber.on('pmessage', (channel: string, message: string, event: string): void => {
59 | if (+message.match(/[0-9]+/)[0] === dbIndex) {
60 | const key = message.replace(/__keyspace@[0-9]*__:/, '');
61 | eventLog.add(key, event);
62 | }
63 | })
64 |
65 | const keyspaceSnapshot = await getKeyspace(monitor.redisClient, dbIndex);
66 | const keyspace: Keyspace = {
67 | eventLog: eventLog,
68 | keyspaceHistories: new KeyspaceHistoriesLog(monitor.maxKeyspaceHistoryCount),
69 | keyspaceSnapshot: keyspaceSnapshot,
70 | eventLogSnapshot: []
71 | }
72 |
73 | monitor.keyspaces.push(keyspace);
74 |
75 | // For every recordKeyspaceHistoryFrequency milliseconds,
76 | // record a keyspace history for this given database
77 | setInterval(
78 | recordKeyspaceHistory,
79 | monitor.recordKeyspaceHistoryFrequency,
80 | monitor,
81 | dbIndex
82 | );
83 | }
84 | }
85 |
86 | instances.forEach((instance: RedisInstance, idx: number): void => {
87 | /*
88 | For each instance in the config.json, set up a RedisMonitor object
89 | initialized with the instance details and a node-redis client for
90 | both the redisClient (used for performing general commands) and the keyspaceSubscriber
91 | */
92 |
93 | let client: redis.RedisClient;
94 | let subscriber: redis.RedisClient;
95 | if (instance.host && instance.port) {
96 | client = redis.createClient({host: instance.host, port: instance.port});
97 | subscriber = redis.createClient({host: instance.host, port: instance.port});
98 | } else if (instance.url) {
99 | client = redis.createClient({url: instance.url});
100 | subscriber = redis.createClient({url: instance.url});
101 | } else {
102 | console.log(`No valid connection host/port or URL provided - check your config. Instance details: ${instance}`);
103 | return
104 | }
105 |
106 | //Promisify methods for the redis client for async/await capability
107 | //Subscriber does not require any promisified method
108 | client = promisifyClientMethods(client);
109 |
110 | const monitor: RedisMonitor = {
111 | instanceId: idx + 1,
112 | redisClient: client,
113 | keyspaceSubscriber: subscriber,
114 | host: instance.host,
115 | port: instance.port,
116 | url: instance.url,
117 | keyspaces: [],
118 | recordKeyspaceHistoryFrequency: instance.recordKeyspaceHistoryFrequency,
119 | maxKeyspaceHistoryCount: instance.maxKeyspaceHistoryCount,
120 | eventGraphRefreshFrequency: instance.eventGraphRefreshFrequency,
121 | maxEventLogSize: instance.maxEventLogSize,
122 | notifyKeyspaceEvents: instance.notifyKeyspaceEvents
123 | }
124 |
125 | initMonitor(monitor);
126 | })
127 |
128 | export default redisMonitors;
--------------------------------------------------------------------------------
/src/client/components/graphs/KeyspaceChartFilter.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 | import FormControl from "@material-ui/core/FormControl";
6 | import Button from "@material-ui/core/Button";
7 | import TextField from "@material-ui/core/TextField";
8 |
9 | const useStyles = makeStyles((theme) => ({
10 | formControl: {
11 | margin: theme.spacing(1),
12 | minWidth: 120,
13 | },
14 | }));
15 |
16 | export default function KeyspaceChartFilter(props) {
17 | const classes = useStyles();
18 | const [valueKey, setValueKey] = React.useState("");
19 |
20 | const handleChangeKey = (event) => {
21 | console.log("handling change");
22 | setValueKey(event.target.value);
23 | };
24 | // const handleChangeSubmit = (event) => {
25 | // console.log("handling change");
26 | // setValue(event.target.value);
27 | // };
28 | function selectChange(event) {
29 | setCategory(event.target.value);
30 | }
31 | //submitting the filter
32 | function handleSubmit(currInstance, currDatabase, queryParams) {
33 | console.log(
34 | "queryParams.keynameFilter in handlesubmit",
35 | queryParams.keynameFilter
36 | );
37 | console.log(
38 | "type of queryparmskeynamefilter",
39 | typeof queryParams.keynameFilter
40 | );
41 | let URI;
42 | // if (queryParams) {
43 | if (queryParams.keynameFilter)
44 | URI = `/api/events/${currInstance}/${currDatabase}/?timeInterval=7000&keynameFilter=${queryParams.keynameFilter}`;
45 | if (queryParams.filterType)
46 | URI = `/api/events/${currInstance}/${currDatabase}/?timeInterval=7000&keynameFilter=${queryParams.filterType}`;
47 | console.log("URI in handleSubmit FETCH", URI);
48 | fetch(URI)
49 | .then((res) => res.json())
50 | .then((response) => {
51 | console.log("response in handleSubmit of Filter", response);
52 | });
53 | // }
54 | }
55 | // const clearFilter = () => {
56 | // setValue('');
57 |
58 | // }
59 |
60 | function clearFilter(e) {
61 | e.preventDefault();
62 | props.clearInt();
63 | props.clearFilterIntID();
64 | props.resetState();
65 | props.getInitialData();
66 | props.getMoreData();
67 | setValueKey("");
68 | document.getElementById("my-text-field").value = "";
69 | // setCategory("");
70 | // props.updateCurrDisplay({ filterType: "keyName", filterValue: "" });
71 | // props.updateCurrDisplay({ filterType: "keyType", filterValue: "" });
72 | // const queryOptions = {
73 | // pageSize: props.pageSize,
74 | // pageNum: props.pageNum,
75 | // refreshScan: 0,
76 | // keyNameFilter: props.currDisplay.keyNameFilter,
77 | // keyTypeFilter: props.currDisplay.keyTypeFilter,
78 | // };
79 | // props.changeKeyspacePage(
80 | // props.currInstance,
81 | // props.currDatabase,
82 | // queryOptions
83 | // );
84 | }
85 |
86 | const newArea = [];
87 | // console.log("props in KSCHARTFILTER", props);
88 | return (
89 |
90 |
91 |
97 |
98 |
99 |
100 | Clear Filter
101 |
102 | {
104 | e.preventDefault();
105 | props.resetState();
106 |
107 | // e.preventDefault();
108 | console.log("props in onclick function", props);
109 | console.log("valueKey", valueKey);
110 | const params = {
111 | keynameFilter: valueKey,
112 | };
113 | // function timeout() {
114 | // this.props.setIntFilter(
115 | // this.props.currInstance,
116 | // this.props.currDatabase,
117 | // this.props.totalEvents,
118 | // queryParams
119 | // );
120 | // }
121 |
122 | props.getInitialFilteredData(
123 | props.currInstance,
124 | props.currDatabase,
125 | params
126 | );
127 | props.setIntFilter(
128 | props.currInstance,
129 | props.currDatabase,
130 | props.totalEvents,
131 | params
132 | );
133 | }}
134 | color='default'>
135 | Apply Filter
136 |
137 | {/* + */}
138 |
139 |
{newArea}
140 |
141 | );
142 | }
143 |
144 | // {
148 | // e.preventDefault();
149 | // this.props.updateCurrDisplay('', '');
150 | // }}
151 | // >
152 | // Clear Filter
153 | //
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 | 
35 |
36 | View all databases for all instances of your deployment
37 |
38 | 
39 |
40 | Filter by key names, event types, or data types
41 |
42 | 
43 |
44 | Customize your keyspace view
45 |
46 | 
47 |
48 | Filter, zoom, and pan through your graphs
49 |
50 | 
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 | {
97 | e.preventDefault();
98 | console.log(
99 | 'currInstance',
100 | this.props.currInstance,
101 | 'currDatabase',
102 | this.props.currDatabase
103 | );
104 | //pageNum is always going to be 1 on refresh and refreshScan is going to be 1
105 | this.props.refreshEvents(
106 | this.props.currInstance,
107 | this.props.currDatabase,
108 | this.props.pageSize,
109 | 1,
110 | 1
111 | );
112 | //need to have current graph updated to page 1 -- re render?
113 | this.props.updatePageNum(1);
114 | document.getElementById('standard-secondary').value = '';
115 | document.getElementById('secondary-secondary').value = '';
116 | }}
117 | id='refreshButton'
118 | >
119 | Refresh
120 |
121 |
122 |
123 | );
124 |
125 | //KEYSPACE PAGE : COMPLETED
126 | } else {
127 | return (
128 |
129 |
141 |
142 | {
146 | e.preventDefault();
147 | console.log(
148 | 'currInstance',
149 | this.props.currInstance,
150 | 'currDatabase',
151 | this.props.currDatabase
152 | );
153 | //pageNum is always going to be 1 on refresh and refreshScan is going to be 1
154 | this.props.refreshKeyspace(
155 | this.props.currInstance,
156 | this.props.currDatabase,
157 | this.props.pageSize,
158 | 1,
159 | 1
160 | );
161 | //need to have current graph updated to page 1 -- re render?
162 | this.props.updatePageNum(1);
163 | document.getElementById('standard-secondary').value = '';
164 | }}
165 | id='refreshButton'
166 | >
167 | Refresh
168 |
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 |
137 |
138 | None
139 |
140 | string
141 | list
142 | set
143 | zset
144 | hash
145 |
146 |
147 |
150 |
151 | Clear Filter
152 |
153 |
154 |
155 | Apply Filter
156 |
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 |
187 | Clear Filter
188 |
189 |
190 | Apply Filter
191 |
192 | {/* + */}
193 |
194 |
{newArea}
195 |
196 | );
197 | }
198 | }
199 |
--------------------------------------------------------------------------------