├── .gitignore ├── Dockerfile ├── Dockerfile-dev ├── Dockerfile-postgres ├── README.md ├── docker-compose-dev-hot.yml ├── docker-compose.yml ├── grafana ├── dashboards │ └── rediwatch-1721963499064.json └── provisioning │ ├── dashboards │ └── dashboards.yml │ └── datasources │ └── datasource.yml ├── package-lock.json ├── package.json ├── prometheus.yml ├── src ├── assets │ ├── RediWatch_logo.png │ ├── Rediwatch-Configuration.png │ ├── Rediwatch-Getting-Started.png │ ├── Rediwatch-Metrics.png │ ├── Rediwatch-Redis-Cache.png │ ├── Splash-Configuration.png │ ├── Splash-Metrics.png │ ├── Splash-Profile.png │ └── background.mp4 ├── client │ ├── App.tsx │ ├── components │ │ ├── AppAppBar.tsx │ │ ├── ConfigDropdown.tsx │ │ ├── Configuration.tsx │ │ ├── ConfigurationMethods.tsx │ │ ├── CurrentMemory.tsx │ │ ├── Features.tsx │ │ ├── Footer.tsx │ │ ├── Hero.tsx │ │ ├── Highlights.tsx │ │ ├── Home.tsx │ │ ├── LandingPage.tsx │ │ ├── Metrics.tsx │ │ ├── NavBar.tsx │ │ ├── Profile.tsx │ │ ├── ProfileCaches.tsx │ │ ├── Signin.tsx │ │ ├── Signup.tsx │ │ ├── TTLDropdown.tsx │ │ ├── Title.tsx │ │ ├── ToggleColorMode.tsx │ │ └── getLPTheme.tsx │ └── index.html ├── custom.d.ts └── server │ ├── controllers │ └── userController.ts │ ├── models │ └── queryModels.ts │ ├── routes │ └── userRoutes.ts │ └── server.ts ├── tsconfig.json ├── user_database.sql └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | .env 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.13 2 | WORKDIR /usr/src/app 3 | COPY . /usr/src/app/ 4 | RUN npm install 5 | RUN npm run build 6 | EXPOSE 3001 7 | # ENTRYPOINT [ "node", "src/server/server.ts" ] 8 | ENTRYPOINT [ "node", "dist/server/server.js" ] 9 | 10 | # FROM node:16.13 11 | # WORKDIR /usr/src/app 12 | # COPY . /usr/src/app 13 | # RUN npm install 14 | # RUN npm install typescript 15 | # RUN npm install tsc -g 16 | # RUN npm run build 17 | # EXPOSE 3000 18 | # ENTRYPOINT ["node", "dist/server/server.js"] -------------------------------------------------------------------------------- /Dockerfile-dev: -------------------------------------------------------------------------------- 1 | FROM node:16.13 2 | RUN npm install -g webpack 3 | RUN npm install -g typescript 4 | 5 | WORKDIR /usr/src/app 6 | COPY package*.json /usr/src/app/ 7 | RUN npm install 8 | # RUN npm run build 9 | 10 | 11 | # Compile TypeScript files 12 | 13 | 14 | EXPOSE 3001 15 | 16 | # Start the server using the compiled JS files 17 | # CMD ["npm", "run", "dev:server"] -------------------------------------------------------------------------------- /Dockerfile-postgres: -------------------------------------------------------------------------------- 1 | FROM postgres:12.8 2 | 3 | COPY /user_database.sql /docker-entrypoint-initdb.d/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RediWatch 2 | 3 | _Website and Dev-tool: [Rediwatch](https://github.com/oslabs-beta/RediWatch)_ 4 | 5 | ## Overview 6 | 7 | Caching provides significant performance optimization for any application, however it is difficult to confirm the efficiency of your cache. As applications grow and scale, managing a Redis cache becomes increasingly complex. Redis, a popular in-memory data store, is essential for optimizing application performance by reducing database load and ensuring quick data retrieval. However, gaining clear visibility into how well your Redis cache performs and finding the best configuration is a significant challenge. Without proper insights, you risk inefficient memory usage, low cache hit rates, and suboptimal configurations that can degrade your application's performance. 8 | 9 | Rediwatch is an open-source tool designed to help developers and engineers visualize and optimize their Redis cache by displaying vital metrics for application caches through an intuitive, user-friendly website. Rediwatch is easy to utilize, free to use, and assists developers in visualizing cache performance issues, so that they can avoid constantly monitoring their applications caches. As users select different cache configurations and invalidation methods, metrics will be updated and visualized on the dashboard. With RediWatch, users can enter their Redis connection string to instantly access comprehensive insights into their cache's performance. The app provides visualizations for hit rates, memory allocation, and key performance indicators, allowing users to fine-tune their caching strategies. 10 | 11 | Additionally, RediWatch features a configuration testing environment where users can experiment with different Redis setups without impacting their live cache. The app’s data mocking functionality ensures that tests are safe, preserving the stability of the production environment while allowing for thorough exploration and optimization of Redis configurations. Rediwatch monitors: 12 | 13 | - Used memory 14 | - Key space hits and misses 15 | - Commands processed 16 | - Connected clients 17 | - Evicted and expired keys 18 | - _and many more_ 19 | 20 | ## Core Features 21 | 22 | - Customizable Metrics: Customize which metrics you want to have displayed in real-time. 23 | - Login: Create an authorized profile to quickly and securely access Rediwatch and save your Redis caches. 24 | - Easy Set Up: Simply enter your Redis connection URL, give it a nickname, and click to connect. 25 | - Data Cloning: Rediwatch clones user's caches to provide safe and controlled testing without risking the stability of the production environment. 26 | - Free: Rediwatch offers cache visualization for free, making it accessible to all developers. 27 | 28 | ## Getting Started 29 | 30 | ### How do you use Rediwatch for applications in production? 31 | 32 | Navigate to [Rediwatch](https://github.com/oslabs-beta/RediWatch) and set up a user account. _Only users with an account can use Rediwatch at this time._ 33 | 34 | getting started 35 | 36 | Go to your Redis cloud console and copy your Redis connection URL from your configuration settings. _Your connection url should include your host, port, and password for Redis._ 37 | 38 | Add your Redis connection URL on the homepage, and give it a unique nickname before you add it to your profile. Click submit to generate testing on this cache to display your metrics. 39 | 40 | redis cache 41 | 42 | Rediwatch will generate the graphs for the performance metrics associated with the selected cache, and you can interact with our dynamic graphs by selecting different eviction policies, ttl times, and maxmemory setting on the Configuration page. 43 | Select the cache configuration settings you would like to visualize, then click the run tests button to update the metrics graphs with the latest cache configuration. 44 | 45 | configuration 46 | 47 | metrics 48 | 49 | ### How do you use Rediwatch for applications in development? 50 | 51 | There are two ways to use the application for development purposes. The first method: 52 | 53 | 1. First, clone the repository from Github . 54 | 2. Run `docker build -t rediwatch -f Dockerfile-dev .` in your terminal 55 | 3. Run `docker-compose -f docker-compose-dev-hot.yml up` in your terminal 56 | 4. Navigate to [localhost:8080](https://localhost:8080). You should see the web application and enter your Redis connection URL (default is empty) and nickname. 57 | 58 | Or, simply pull the image from Docker: 59 | 60 | 1. Navigate to [Docker Hub](https://hub.docker.com) and pull the image using the command `docker pull rediwatch/rediwatch` 61 | 2. Run `npm run docker-dev:hot` in your terminal 62 | 3. Navigate to [localhost:8080](https://localhost:8080). You should see the web application and enter your Redis connection URL (default is empty) and nickname. 63 | 64 | _If you're having any trouble, please refer to the images in the section above._ 65 | 66 | ## Tech Stack 67 | 68 | Node.js | Typescript | React.js | PostgreSQL | Redis | Grafana | Express | Material UI | Docker 69 | 70 | ## Technical Challenges 71 | 72 | - **Chart Visualization Overengineering:** 73 | 74 | Initially, we attempted to integrate Prometheus with Grafana for chart visualization, expecting this combination to handle our data needs effectively. However, the complexity of setting up and managing Prometheus introduced unnecessary overhead. We realized that for our use case, directly sending data from Redis to Grafana would simplify our architecture without sacrificing functionality. This change reduced complexity and improved our ability to focus on delivering a more intuitive user experience. 75 | 76 | - **Data Cloning for Safe Testing:** 77 | 78 | One significant challenge was ensuring that our configuration testing feature could run without affecting the live Redis cache. Cloning live data in a safe and controlled way was critical. To achieve this, we developed a robust data mocking system that simulates Redis data. This approach allowed us to provide realistic testing scenarios without risking the stability of the production environment, giving users confidence to experiment and optimize configurations safely. 79 | 80 | - **Containerization for Consistent Deployment:** 81 | 82 | To ensure consistent deployment across different environments, we containerized our app using Docker. This involved creating separate Docker images for the frontend, backend, Redis, Grafana, and PostgreSQL. While this approach offered the benefits of isolation and consistency, it also introduced complexities in managing inter-service communication and maintaining performance parity with non-containerized environments. We overcame these challenges by fine-tuning our Docker Compose setup and optimizing network configurations, ensuring that all services communicated seamlessly and that performance metrics remained accurate and reliable across all environments. 83 | 84 | ## How to Contribute 85 | 86 | 1. Clone the repo and make a new branch 87 | 2. Run `docker build -t rediwatch -f Dockerfile-dev .` in your terminal 88 | 3. Run `npm run docker-dev:hot` in your terminal 89 | 4. Add a feature, fix a bug, or refactor some code 90 | 5. Write tests for the changes you made, if necessary 91 | 6. Run unit tests and make sure all tests pass: npm test 92 | 7. Open a Pull Request with a comprehensive description of changes to the dev branch 93 | 94 | ## Future Features 95 | 96 | There are plenty of features that still need to be integrated into the codebase: 97 | 98 | - Jest testing: Adding more unit tests and end-to-end tests for application performance reliability. 99 | - Alerts and notifications: Set up alerts for individual metrics to be notified when a metric dips below or exceeds a specified value. 100 | - AuthO: Integrating Google or Github login to quickly and securely access Rediwatch. 101 | 102 | ## Contributors 103 | 104 | **Yang Cao** [Github](https://github.com/Oliviak109382) | [LinkedIn](https://linkedin.com) 105 | 106 | **Ansel Nolting** [Github](https://github.com/AnselNolting) | [LinkedIn](https://linkedin.com) 107 | 108 | **Jennifer Slaughter** [Github](https://github.com/j-slaughter) | [LinkedIn](https://linkedin.com) 109 | 110 | **YoonJoo Woo** [Github](https://github.com/YoonJooWoo) | [LinkedIn](https://linkedin.com) 111 | -------------------------------------------------------------------------------- /docker-compose-dev-hot.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | db: 4 | image: postgres 5 | restart: always 6 | container_name: my_postgres_db 7 | environment: 8 | POSTGRES_USER: nenunvau 9 | POSTGRES_PASSWORD: 5sN2jTxq2AbK2OkLRtqJDzfu_I4NFeDG 10 | POSTGRES_DB: RediWatch 11 | volumes: 12 | - db-data:/var/lib/postgresql/data 13 | - ./user_database.sql:/docker-entrypoint-initdb.d/user_database.sql 14 | networks: 15 | - my_network 16 | healthcheck: 17 | test: ["CMD-SHELL", "pg_isready -U nenunvau -d RediWatch"] 18 | interval: 10s 19 | timeout: 5s 20 | retries: 5 21 | ports: 22 | - "5432:5432" 23 | 24 | server: 25 | image: rediwatch 26 | container_name: "rediwatch-backend" 27 | volumes: 28 | - .:/usr/src/app 29 | - node_modules:/usr/src/app/node_modules 30 | ports: 31 | - 3001:3001 32 | depends_on: 33 | - redis 34 | environment: 35 | - Redis_Url=redis://redis:6379 36 | - DATABASE_URL 37 | command: npm run dev:server 38 | env_file: 39 | - .env 40 | 41 | 42 | client: 43 | image: rediwatch 44 | container_name: "rediwatch-frontend" 45 | volumes: 46 | - .:/usr/src/app 47 | - node_modules:/usr/src/app/node_modules 48 | ports: 49 | - 8080:8080 50 | depends_on: 51 | - redis 52 | command: npm run dev:client 53 | 54 | redis: 55 | image: redis 56 | ports: 57 | - 6379:6379 58 | command: redis-server 59 | depends_on: 60 | - db 61 | 62 | grafana: 63 | image: grafana/grafana:latest 64 | container_name: grafana 65 | ports: 66 | - "3000:3000" 67 | volumes: 68 | - grafana-storage:/var/lib/grafana 69 | - ./grafana/provisioning:/etc/grafana/provisioning 70 | - ./grafana/dashboards:/var/lib/grafana/dashboards 71 | environment: 72 | - GF_INSTALL_PLUGINS=redis-datasource 73 | - GF_SECURITY_ALLOW_EMBEDDING=true 74 | - GF_AUTH_ANONYMOUS_ENABLED=true 75 | - GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer 76 | depends_on: 77 | - redis 78 | 79 | volumes: 80 | node_modules: 81 | grafana-storage: 82 | db-data: 83 | 84 | networks: 85 | my_network: 86 | driver: bridge -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | redis: 4 | image: redis:latest 5 | ports: 6 | - "6379:6379" 7 | command: ["redis-server", "--appendonly", "yes"] 8 | 9 | redis_exporter: 10 | image: oliver006/redis_exporter 11 | environment: 12 | - REDIS_ADDR=redis:6379 13 | ports: 14 | - "9121:9121" 15 | 16 | prometheus: 17 | image: prom/prometheus 18 | volumes: 19 | - ./prometheus.yml:/etc/prometheus/prometheus.yml 20 | ports: 21 | - "9090:9090" 22 | 23 | grafana: 24 | image: grafana/grafana 25 | ports: 26 | - "3000:3000" 27 | environment: 28 | - GF_SECURITY_ADMIN_PASSWORD=admin 29 | -------------------------------------------------------------------------------- /grafana/dashboards/rediwatch-1721963499064.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "grafana", 8 | "uid": "-- Grafana --" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "type": "dashboard" 15 | } 16 | ] 17 | }, 18 | "editable": true, 19 | "fiscalYearStartMonth": 0, 20 | "graphTooltip": 0, 21 | "id": 1, 22 | "links": [], 23 | "panels": [ 24 | { 25 | "datasource": { 26 | "type": "redis-datasource", 27 | "uid": "PA7F6415749A3297A" 28 | }, 29 | "fieldConfig": { 30 | "defaults": { 31 | "color": { 32 | "mode": "continuous-GrYlRd" 33 | }, 34 | "mappings": [], 35 | "thresholds": { 36 | "mode": "absolute", 37 | "steps": [ 38 | { 39 | "color": "green", 40 | "value": null 41 | }, 42 | { 43 | "color": "red", 44 | "value": 80 45 | } 46 | ] 47 | } 48 | }, 49 | "overrides": [] 50 | }, 51 | "gridPos": { 52 | "h": 8, 53 | "w": 12, 54 | "x": 0, 55 | "y": 0 56 | }, 57 | "id": 4, 58 | "options": { 59 | "displayMode": "lcd", 60 | "maxVizHeight": 300, 61 | "minVizHeight": 16, 62 | "minVizWidth": 8, 63 | "namePlacement": "auto", 64 | "orientation": "horizontal", 65 | "reduceOptions": { 66 | "calcs": [], 67 | "fields": "", 68 | "values": true 69 | }, 70 | "showUnfilled": true, 71 | "sizing": "auto", 72 | "valueMode": "color" 73 | }, 74 | "pluginVersion": "11.1.0", 75 | "targets": [ 76 | { 77 | "command": "info", 78 | "datasource": { 79 | "type": "redis-datasource", 80 | "uid": "PA7F6415749A3297A" 81 | }, 82 | "query": "", 83 | "refId": "A", 84 | "section": "commandstats", 85 | "type": "command" 86 | } 87 | ], 88 | "title": "Command Stats", 89 | "type": "bargauge" 90 | }, 91 | { 92 | "datasource": { 93 | "type": "redis-datasource", 94 | "uid": "PA7F6415749A3297A" 95 | }, 96 | "fieldConfig": { 97 | "defaults": { 98 | "color": { 99 | "mode": "palette-classic" 100 | }, 101 | "custom": { 102 | "axisBorderShow": false, 103 | "axisCenteredZero": false, 104 | "axisColorMode": "text", 105 | "axisLabel": "", 106 | "axisPlacement": "auto", 107 | "fillOpacity": 80, 108 | "gradientMode": "none", 109 | "hideFrom": { 110 | "legend": false, 111 | "tooltip": false, 112 | "viz": false 113 | }, 114 | "lineWidth": 1, 115 | "scaleDistribution": { 116 | "log": 2, 117 | "type": "log" 118 | }, 119 | "thresholdsStyle": { 120 | "mode": "off" 121 | } 122 | }, 123 | "mappings": [], 124 | "thresholds": { 125 | "mode": "absolute", 126 | "steps": [ 127 | { 128 | "color": "green", 129 | "value": null 130 | }, 131 | { 132 | "color": "red", 133 | "value": 80 134 | } 135 | ] 136 | }, 137 | "unit": "short" 138 | }, 139 | "overrides": [] 140 | }, 141 | "gridPos": { 142 | "h": 8, 143 | "w": 12, 144 | "x": 12, 145 | "y": 0 146 | }, 147 | "id": 1, 148 | "options": { 149 | "barRadius": 0, 150 | "barWidth": 0.97, 151 | "fullHighlight": false, 152 | "groupWidth": 0.7, 153 | "legend": { 154 | "calcs": [], 155 | "displayMode": "list", 156 | "placement": "right", 157 | "showLegend": true 158 | }, 159 | "orientation": "auto", 160 | "showValue": "never", 161 | "stacking": "none", 162 | "tooltip": { 163 | "mode": "single", 164 | "sort": "none" 165 | }, 166 | "xTickLabelRotation": 0, 167 | "xTickLabelSpacing": 0 168 | }, 169 | "targets": [ 170 | { 171 | "command": "info", 172 | "datasource": { 173 | "type": "redis-datasource", 174 | "uid": "PA7F6415749A3297A" 175 | }, 176 | "query": "", 177 | "refId": "A", 178 | "section": "memory", 179 | "type": "command" 180 | } 181 | ], 182 | "title": "Memory", 183 | "type": "barchart" 184 | }, 185 | { 186 | "datasource": { 187 | "type": "redis-datasource", 188 | "uid": "PA7F6415749A3297A" 189 | }, 190 | "fieldConfig": { 191 | "defaults": { 192 | "color": { 193 | "mode": "palette-classic" 194 | }, 195 | "custom": { 196 | "axisBorderShow": false, 197 | "axisCenteredZero": false, 198 | "axisColorMode": "text", 199 | "axisLabel": "", 200 | "axisPlacement": "auto", 201 | "fillOpacity": 80, 202 | "gradientMode": "none", 203 | "hideFrom": { 204 | "legend": false, 205 | "tooltip": false, 206 | "viz": false 207 | }, 208 | "lineWidth": 1, 209 | "scaleDistribution": { 210 | "type": "linear" 211 | }, 212 | "thresholdsStyle": { 213 | "mode": "off" 214 | } 215 | }, 216 | "mappings": [], 217 | "thresholds": { 218 | "mode": "absolute", 219 | "steps": [ 220 | { 221 | "color": "green", 222 | "value": null 223 | }, 224 | { 225 | "color": "red", 226 | "value": 80 227 | } 228 | ] 229 | }, 230 | "unit": "short" 231 | }, 232 | "overrides": [] 233 | }, 234 | "gridPos": { 235 | "h": 8, 236 | "w": 12, 237 | "x": 0, 238 | "y": 8 239 | }, 240 | "id": 2, 241 | "options": { 242 | "barRadius": 0, 243 | "barWidth": 0.97, 244 | "fullHighlight": false, 245 | "groupWidth": 0.7, 246 | "legend": { 247 | "calcs": [], 248 | "displayMode": "list", 249 | "placement": "right", 250 | "showLegend": true 251 | }, 252 | "orientation": "auto", 253 | "showValue": "never", 254 | "stacking": "none", 255 | "tooltip": { 256 | "mode": "single", 257 | "sort": "none" 258 | }, 259 | "xTickLabelRotation": 0, 260 | "xTickLabelSpacing": 0 261 | }, 262 | "targets": [ 263 | { 264 | "command": "info", 265 | "datasource": { 266 | "type": "redis-datasource", 267 | "uid": "PA7F6415749A3297A" 268 | }, 269 | "query": "", 270 | "refId": "A", 271 | "section": "server", 272 | "type": "command" 273 | } 274 | ], 275 | "title": "Server", 276 | "type": "barchart" 277 | }, 278 | { 279 | "datasource": { 280 | "type": "redis-datasource", 281 | "uid": "PA7F6415749A3297A" 282 | }, 283 | "fieldConfig": { 284 | "defaults": { 285 | "mappings": [], 286 | "thresholds": { 287 | "mode": "percentage", 288 | "steps": [ 289 | { 290 | "color": "green", 291 | "value": null 292 | }, 293 | { 294 | "color": "orange", 295 | "value": 70 296 | }, 297 | { 298 | "color": "red", 299 | "value": 85 300 | } 301 | ] 302 | } 303 | }, 304 | "overrides": [] 305 | }, 306 | "gridPos": { 307 | "h": 8, 308 | "w": 12, 309 | "x": 12, 310 | "y": 8 311 | }, 312 | "id": 3, 313 | "options": { 314 | "minVizHeight": 75, 315 | "minVizWidth": 75, 316 | "orientation": "auto", 317 | "reduceOptions": { 318 | "calcs": [ 319 | "lastNotNull" 320 | ], 321 | "fields": "", 322 | "values": false 323 | }, 324 | "showThresholdLabels": false, 325 | "showThresholdMarkers": true, 326 | "sizing": "auto" 327 | }, 328 | "pluginVersion": "11.1.0", 329 | "targets": [ 330 | { 331 | "command": "info", 332 | "datasource": { 333 | "type": "redis-datasource", 334 | "uid": "PA7F6415749A3297A" 335 | }, 336 | "query": "", 337 | "refId": "A", 338 | "section": "clients", 339 | "type": "command" 340 | } 341 | ], 342 | "title": "Clients", 343 | "type": "gauge" 344 | } 345 | ], 346 | "schemaVersion": 39, 347 | "tags": [], 348 | "templating": { 349 | "list": [] 350 | }, 351 | "time": { 352 | "from": "now-6h", 353 | "to": "now" 354 | }, 355 | "timepicker": {}, 356 | "timezone": "browser", 357 | "title": "rediwatch", 358 | "uid": "fdsvo9ttg6k8wd", 359 | "version": 2, 360 | "weekStart": "" 361 | } -------------------------------------------------------------------------------- /grafana/provisioning/dashboards/dashboards.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'default' 5 | orgId: 1 6 | folder: '' 7 | type: file 8 | disableDeletion: false 9 | editable: true 10 | options: 11 | path: /var/lib/grafana/dashboards 12 | -------------------------------------------------------------------------------- /grafana/provisioning/datasources/datasource.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Redis 5 | type: redis-datasource 6 | access: proxy 7 | url: redis://redis:6379 8 | isDefault: true 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rediwatch", 3 | "version": "1.0.0", 4 | "description": "The problem: Caching provides significant performance optimization for any application, however it is difficult to confirm the efficiency of your cache. Also, one of the biggest problems facing developers today is how to handle cache invalidation (checking to see if the value stored in your cache matches the value in the primary source).", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon ./src/server/server.ts & webpack-dev-server", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "webpack --config ./webpack.config.js", 10 | "run-tsc": "tsc", 11 | "dev:server": "NODE_ENV=development ts-node ./src/server/server.ts", 12 | "dev:client": "webpack serve --config webpack.config.js --hot", 13 | "docker-dev:hot": "docker-compose -f docker-compose-dev-hot.yml up" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "@babel/core": "^7.23.9", 19 | "@babel/plugin-transform-typescript": "^7.23.6", 20 | "@babel/preset-env": "^7.23.9", 21 | "@babel/preset-react": "^7.23.3", 22 | "@emotion/react": "^11.11.4", 23 | "@emotion/styled": "^11.11.5", 24 | "@mui/icons-material": "^5.15.19", 25 | "@mui/material": "^5.15.19", 26 | "@types/express": "^4.17.21", 27 | "@types/node": "^20.14.12", 28 | "@types/pg": "^8.11.6", 29 | "@types/react": "^18.2.60", 30 | "@types/react-dom": "^18.2.19", 31 | "@types/redis": "^4.0.11", 32 | "@typescript-eslint/eslint-plugin": "^7.1.0", 33 | "@typescript-eslint/parser": "^7.1.0", 34 | "babel-loader": "^9.1.3", 35 | "copy-webpack-plugin": "^12.0.2", 36 | "css-loader": "^6.10.0", 37 | "eslint": "^8.57.0", 38 | "eslint-plugin-react": "^7.33.2", 39 | "eslint-plugin-react-hooks": "^4.6.0", 40 | "file-loader": "^6.2.0", 41 | "html-webpack-plugin": "^5.6.0", 42 | "nodemon": "^3.1.0", 43 | "ts-loader": "^9.5.1", 44 | "typescript": "^5.5.3", 45 | "typescript-eslint": "^7.1.0", 46 | "url-loader": "^4.1.1", 47 | "webpack": "^5.65.0", 48 | "webpack-cli": "^5.1.4", 49 | "webpack-dev-server": "^4.7.2" 50 | }, 51 | "dependencies": { 52 | "@emotion/react": "^11.11.4", 53 | "@emotion/styled": "^11.11.5", 54 | "@mui/icons-material": "^5.15.19", 55 | "@mui/material": "^5.15.19", 56 | "dotenv": "^16.4.5", 57 | "express": "^4.18.2", 58 | "html-entities": "^2.5.2", 59 | "ioredis": "^5.4.1", 60 | "pg": "^8.12.0", 61 | "react": "^18.3.1", 62 | "react-dom": "^18.2.0", 63 | "react-router-dom": "^6.23.1", 64 | "redis": "^4.6.15", 65 | "ts-node": "^10.9.2", 66 | "tsc": "^2.0.4", 67 | "typescript": "^5.5.3", 68 | "bcryptjs": "^2.4.3", 69 | "jsonwebtoken": "^9.0.2" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | 4 | scrape_configs: 5 | - job_name: 'redis' 6 | static_configs: 7 | - targets: ['redis_exporter:9121'] 8 | -------------------------------------------------------------------------------- /src/assets/RediWatch_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RediWatch/0e99efc14582de696741c1744ef53393b1aaded4/src/assets/RediWatch_logo.png -------------------------------------------------------------------------------- /src/assets/Rediwatch-Configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RediWatch/0e99efc14582de696741c1744ef53393b1aaded4/src/assets/Rediwatch-Configuration.png -------------------------------------------------------------------------------- /src/assets/Rediwatch-Getting-Started.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RediWatch/0e99efc14582de696741c1744ef53393b1aaded4/src/assets/Rediwatch-Getting-Started.png -------------------------------------------------------------------------------- /src/assets/Rediwatch-Metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RediWatch/0e99efc14582de696741c1744ef53393b1aaded4/src/assets/Rediwatch-Metrics.png -------------------------------------------------------------------------------- /src/assets/Rediwatch-Redis-Cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RediWatch/0e99efc14582de696741c1744ef53393b1aaded4/src/assets/Rediwatch-Redis-Cache.png -------------------------------------------------------------------------------- /src/assets/Splash-Configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RediWatch/0e99efc14582de696741c1744ef53393b1aaded4/src/assets/Splash-Configuration.png -------------------------------------------------------------------------------- /src/assets/Splash-Metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RediWatch/0e99efc14582de696741c1744ef53393b1aaded4/src/assets/Splash-Metrics.png -------------------------------------------------------------------------------- /src/assets/Splash-Profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RediWatch/0e99efc14582de696741c1744ef53393b1aaded4/src/assets/Splash-Profile.png -------------------------------------------------------------------------------- /src/assets/background.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RediWatch/0e99efc14582de696741c1744ef53393b1aaded4/src/assets/background.mp4 -------------------------------------------------------------------------------- /src/client/App.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @module App.tsx 3 | * @description Main page of application. Contains all routing info 4 | */ 5 | 6 | import React from 'react'; 7 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; 8 | import { createRoot } from 'react-dom/client'; 9 | 10 | // import React components for each route 11 | import LandingPage from './components/LandingPage'; 12 | import Signin from './components/Signin'; 13 | import Signup from './components/Signup'; 14 | import ConfigurationPage from './components/Configuration'; 15 | import Profile from './components/Profile'; 16 | import Home from './components/Home'; 17 | import Metrics from './components/Metrics'; 18 | 19 | const App: React.FC = () => { 20 | return ( 21 | 22 | 23 | } /> 24 | } /> 25 | } /> 26 | } /> 27 | } /> 28 | } /> 29 | } /> 30 | 31 | 32 | ); 33 | }; 34 | 35 | createRoot(document.querySelector('#root')!).render(); 36 | -------------------------------------------------------------------------------- /src/client/components/AppAppBar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { createTheme, ThemeProvider, CssBaseline, PaletteMode } from '@mui/material'; 3 | import Box from '@mui/material/Box'; 4 | import AppBar from '@mui/material/AppBar'; 5 | import Toolbar from '@mui/material/Toolbar'; 6 | import Button from '@mui/material/Button'; 7 | import Container from '@mui/material/Container'; 8 | import Divider from '@mui/material/Divider'; 9 | import Typography from '@mui/material/Typography'; 10 | import MenuItem from '@mui/material/MenuItem'; 11 | import Drawer from '@mui/material/Drawer'; 12 | import MenuIcon from '@mui/icons-material/Menu'; 13 | import ToggleColorMode from './ToggleColorMode'; 14 | 15 | import imgSrc from '../../assets/RediWatch_logo.png'; 16 | 17 | const logoStyle = { 18 | width: '140px', 19 | height: 'auto', 20 | cursor: 'pointer', 21 | }; 22 | 23 | interface AppAppBarProps { 24 | mode: PaletteMode; 25 | toggleColorMode: () => void; 26 | } 27 | 28 | // Custom theme using #C63124 as the primary color 29 | const lightTheme = createTheme({ 30 | palette: { 31 | mode: 'light', 32 | primary: { 33 | main: '#C63124', // Main color for the theme 34 | }, 35 | background: { 36 | default: '#ffffff', // White background 37 | paper: '#FBEAE9', // Slightly reddish background for paper elements 38 | }, 39 | }, 40 | }); 41 | 42 | function AppAppBar({ mode, toggleColorMode }: AppAppBarProps) { 43 | const [open, setOpen] = React.useState(false); 44 | 45 | const toggleDrawer = (newOpen: boolean) => () => { 46 | setOpen(newOpen); 47 | }; 48 | 49 | const scrollToSection = (sectionId: string) => { 50 | const sectionElement = document.getElementById(sectionId); 51 | const offset = 128; 52 | if (sectionElement) { 53 | const targetScroll = sectionElement.offsetTop - offset; 54 | sectionElement.scrollIntoView({ behavior: 'smooth' }); 55 | window.scrollTo({ 56 | top: targetScroll, 57 | behavior: 'smooth', 58 | }); 59 | setOpen(false); 60 | } 61 | }; 62 | 63 | return ( 64 |
65 | 66 | 67 | 76 | 77 | ({ 80 | display: 'flex', 81 | alignItems: 'center', 82 | justifyContent: 'space-between', 83 | flexShrink: 0, 84 | borderRadius: '999px', 85 | bgcolor: 86 | theme.palette.mode === 'light' 87 | ? 'rgba(255, 255, 255, 0.4)' 88 | : 'rgba(0, 0, 0, 0.4)', 89 | backdropFilter: 'blur(24px)', 90 | maxHeight: 40, 91 | border: '1px solid', 92 | borderColor: 'divider', 93 | boxShadow: 94 | theme.palette.mode === 'light' 95 | ? `0 0 1px rgba(198, 49, 36, 0.1), 1px 1.5px 2px -1px rgba(198, 49, 36, 0.15), 4px 4px 12px -2.5px rgba(198, 49, 36, 0.15)` 96 | : '0 0 1px rgba(2, 31, 59, 0.7), 1px 1.5px 2px -1px rgba(2, 31, 59, 0.65), 4px 4px 12px -2.5px rgba(2, 31, 59, 0.65)', 97 | })} 98 | > 99 | 108 | logo of rediwatch 113 | 114 | scrollToSection('features')} 116 | sx={{ py: '6px', px: '12px' }} 117 | > 118 | 119 | Features 120 | 121 | 122 | scrollToSection('highlights')} 124 | sx={{ py: '6px', px: '12px' }} 125 | > 126 | 127 | Highlights 128 | 129 | 130 | scrollToSection('footer')} 132 | sx={{ py: '6px', px: '12px' }} 133 | > 134 | 135 | Contacts 136 | 137 | 138 | 139 | 140 | 147 | 148 | 158 | 168 | 169 | 170 | 179 | 180 | 188 | 196 | 197 | 198 | scrollToSection('features')}> 199 | Features 200 | 201 | 202 | scrollToSection('highlights')}> 203 | Highlights 204 | 205 | 206 | scrollToSection('footer')}> 207 | Contacts 208 | 209 | 210 | 211 | 221 | 222 | 223 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 |
242 | ); 243 | } 244 | 245 | export default AppAppBar; -------------------------------------------------------------------------------- /src/client/components/ConfigDropdown.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @module ConfigDropdown 3 | * @description dropdown component for selecting cache eviction policy 4 | */ 5 | 6 | import * as React from 'react'; 7 | import { Theme, useTheme } from '@mui/material/styles'; 8 | import Box from '@mui/material/Box'; 9 | import OutlinedInput from '@mui/material/OutlinedInput'; 10 | import InputLabel from '@mui/material/InputLabel'; 11 | import MenuItem from '@mui/material/MenuItem'; 12 | import FormControl from '@mui/material/FormControl'; 13 | import Select, { SelectChangeEvent } from '@mui/material/Select'; 14 | import Chip from '@mui/material/Chip'; 15 | 16 | const ITEM_HEIGHT = 48; 17 | const ITEM_PADDING_TOP = 8; 18 | const MenuProps = { 19 | PaperProps: { 20 | style: { 21 | maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, 22 | width: 250, 23 | }, 24 | }, 25 | }; 26 | 27 | const policies = [ 28 | 'noeviction', 29 | 'allkeys-lru', 30 | 'allkeys-lfu', 31 | 'volatile-lru', 32 | 'volatile-lfu', 33 | 'allkeys-random', 34 | 'volatile-random', 35 | 'volatile-ttl', 36 | ]; 37 | 38 | function getStyles(policy: string, evictionPolicy: string, theme: Theme) { 39 | return { 40 | fontWeight: 41 | evictionPolicy.indexOf(policy) === -1 42 | ? theme.typography.fontWeightRegular 43 | : theme.typography.fontWeightMedium, 44 | }; 45 | } 46 | 47 | export default function MultipleSelectChip() { 48 | const theme = useTheme(); 49 | const [evictionPolicy, setEvictionPolicy] = React.useState(''); 50 | 51 | const handleChange = (event: SelectChangeEvent) => { 52 | const { 53 | target: { value }, 54 | } = event; 55 | setEvictionPolicy( 56 | // On autofill we get a stringified value. 57 | typeof value === 'string' ? value : 'noeviction', 58 | ); 59 | 60 | alert(value); 61 | 62 | // post request to "update config" 63 | // fetch('/api/update-policy', { 64 | // method: 'POST', 65 | // headers: { 66 | // 'Content-Type': 'application/json', 67 | // }, 68 | // body: JSON.stringify(value), 69 | // }) 70 | // .then(res => res.json()) 71 | // .then(data => { 72 | // console.log('Successfully updated:', data); 73 | // alert('Updated cache configuration'); 74 | // }) 75 | // .catch((err) => { 76 | // console.error('Error updating config:', err); 77 | // alert('Failed to update cache configuration'); 78 | // }); 79 | }; 80 | 81 | return ( 82 |
83 | 84 | Eviction Method 85 | 108 | 109 |
110 | ); 111 | } -------------------------------------------------------------------------------- /src/client/components/Configuration.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Configuration 3 | * @description page component for selecting cache configuration 4 | */ 5 | 6 | import * as React from 'react'; 7 | import { styled, createTheme, ThemeProvider } from '@mui/material/styles'; 8 | import CssBaseline from '@mui/material/CssBaseline'; 9 | import MuiDrawer from '@mui/material/Drawer'; 10 | import Box from '@mui/material/Box'; 11 | import MuiAppBar, { AppBarProps as MuiAppBarProps } from '@mui/material/AppBar'; 12 | import Toolbar from '@mui/material/Toolbar'; 13 | import List from '@mui/material/List'; 14 | import Typography from '@mui/material/Typography'; 15 | import Divider from '@mui/material/Divider'; 16 | import IconButton from '@mui/material/IconButton'; 17 | import SpeedIcon from '@mui/icons-material/Speed'; 18 | import Badge from '@mui/material/Badge'; 19 | import Container from '@mui/material/Container'; 20 | import Grid from '@mui/material/Grid'; 21 | import Paper from '@mui/material/Paper'; 22 | import Link from '@mui/material/Link'; 23 | import Button from '@mui/material/Button'; 24 | import MenuIcon from '@mui/icons-material/Menu'; 25 | import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; 26 | import NotificationsIcon from '@mui/icons-material/Notifications'; 27 | import { mainListItems, secondaryListItems } from './NavBar'; 28 | import ConfigDropdown from './ConfigDropdown'; 29 | import TTLDropdown from './TTLDropdown'; 30 | import CurrentMemory from './CurrentMemory'; 31 | import ConfigurationMethods from './ConfigurationMethods'; 32 | 33 | import imgSrc from '../../assets/RediWatch_logo.png'; 34 | 35 | function Copyright(props: any) { 36 | return ( 37 | 38 | {'Copyright © '} 39 | 40 | RediWatch 41 | {' '} 42 | {new Date().getFullYear()} 43 | {'.'} 44 | 45 | ); 46 | } 47 | 48 | const drawerWidth: number = 240; 49 | 50 | interface AppBarProps extends MuiAppBarProps { 51 | open?: boolean; 52 | } 53 | 54 | const AppBar = styled(MuiAppBar, { 55 | shouldForwardProp: (prop) => prop !== 'open', 56 | })(({ theme, open }) => ({ 57 | zIndex: theme.zIndex.drawer + 1, 58 | transition: theme.transitions.create(['width', 'margin'], { 59 | easing: theme.transitions.easing.sharp, 60 | duration: theme.transitions.duration.leavingScreen, 61 | }), 62 | ...(open && { 63 | marginLeft: drawerWidth, 64 | width: `calc(100% - ${drawerWidth}px)`, 65 | transition: theme.transitions.create(['width', 'margin'], { 66 | easing: theme.transitions.easing.sharp, 67 | duration: theme.transitions.duration.enteringScreen, 68 | }), 69 | }), 70 | })); 71 | 72 | const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' })( 73 | ({ theme, open }) => ({ 74 | '& .MuiDrawer-paper': { 75 | position: 'relative', 76 | whiteSpace: 'nowrap', 77 | width: drawerWidth, 78 | transition: theme.transitions.create('width', { 79 | easing: theme.transitions.easing.sharp, 80 | duration: theme.transitions.duration.enteringScreen, 81 | }), 82 | boxSizing: 'border-box', 83 | ...(!open && { 84 | overflowX: 'hidden', 85 | transition: theme.transitions.create('width', { 86 | easing: theme.transitions.easing.sharp, 87 | duration: theme.transitions.duration.leavingScreen, 88 | }), 89 | width: theme.spacing(7), 90 | [theme.breakpoints.up('sm')]: { 91 | width: theme.spacing(9), 92 | }, 93 | }), 94 | }, 95 | }), 96 | ); 97 | 98 | // Set theme to dark mode always 99 | const darkTheme = createTheme({ 100 | palette: { 101 | mode: 'dark', 102 | primary: { 103 | main: '#C63124', 104 | } 105 | }, 106 | }); 107 | 108 | export default function Dashboard() { 109 | const [open, setOpen] = React.useState(true); 110 | const toggleDrawer = () => { 111 | setOpen(!open); 112 | }; 113 | 114 | return ( 115 | 116 | 126 | 127 | 128 | 129 | 134 | 144 | 145 | 146 | 153 | Configuration Dashboard 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | {mainListItems} 178 | 179 | {secondaryListItems} 180 | 181 | 182 | 186 | theme.palette.mode === 'light' 187 | ? theme.palette.grey[100] 188 | : theme.palette.grey[900], 189 | flexGrow: 1, 190 | height: '100vh', 191 | overflow: 'auto', 192 | }} 193 | > 194 | 195 | 196 | 197 | {/* Configuration Selections */} 198 | 199 | 207 | 208 | 209 | 210 | 211 | 212 | {/* Current maxmemory config */} 213 | 214 | 222 | 223 | 224 | 225 | {/* Eviction Policies */} 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | ); 238 | } -------------------------------------------------------------------------------- /src/client/components/ConfigurationMethods.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Link from '@mui/material/Link'; 3 | import Table from '@mui/material/Table'; 4 | import TableBody from '@mui/material/TableBody'; 5 | import TableCell from '@mui/material/TableCell'; 6 | import TableHead from '@mui/material/TableHead'; 7 | import TableRow from '@mui/material/TableRow'; 8 | import Title from './Title'; 9 | 10 | // Generate Eviction Method Data 11 | function createData( 12 | id: number, 13 | name: string, 14 | description: string, 15 | ) { 16 | return { id, name, description }; 17 | } 18 | 19 | const rows = [ 20 | createData( 21 | 0, 22 | 'noeviction', 23 | `New values aren't saved when memory limit is reached. When a database 24 | uses replication, this applies to the primary database`, 25 | ), 26 | createData( 27 | 1, 28 | 'allkeys-lru', 29 | `Keeps most recently used keys; removes least recently used (LRU) keys`, 30 | ), 31 | createData( 32 | 2, 33 | 'allkeys-lfu', 34 | `Keeps most frequently used keys; removes least frequently used (LFU) keys`, 35 | ), 36 | createData( 37 | 3, 38 | 'volatile-lru', 39 | `Removes least recently used keys with the expired field set to true`, 40 | ), 41 | createData( 42 | 4, 43 | 'volatile-lfu', 44 | `Removes least frequently used keys with the expired field set to true`, 45 | ), 46 | createData( 47 | 5, 48 | 'allkeys-random', 49 | `Randomly removes keys to make space for the new data added`, 50 | ), 51 | createData( 52 | 6, 53 | 'volatile-random', 54 | `Randomly removes keys with expire field set to true`, 55 | ), 56 | createData( 57 | 7, 58 | 'volatile-ttl', 59 | `Removes keys with expire field set to true and the shortest remaining time-to-live (TTL) value`, 60 | ), 61 | ]; 62 | 63 | export default function Policies() { 64 | return ( 65 | 66 | Key Eviction Policies 67 | 68 | 69 | 70 | Name 71 | Description 72 | {/* Sale Amount */} 73 | 74 | 75 | 76 | {rows.map((row) => ( 77 | 78 | {row.name} 79 | {row.description} 80 | {/* {`$${row.amount}`} */} 81 | 82 | ))} 83 | 84 |
85 | 86 | Read the docs 87 | 88 |
89 | ); 90 | } -------------------------------------------------------------------------------- /src/client/components/CurrentMemory.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @module CurrentMemory 3 | * @description user component for viewing and adjusting cache maxmemory setting 4 | */ 5 | 6 | import * as React from 'react'; 7 | import Typography from '@mui/material/Typography'; 8 | import Title from './Title'; 9 | import IconButton from '@mui/material/IconButton'; 10 | import ArrowCircleLeftIcon from '@mui/icons-material/ArrowCircleLeft'; 11 | import ArrowCircleRightIcon from '@mui/icons-material/ArrowCircleRight'; 12 | 13 | function preventDefault(event: React.MouseEvent) { 14 | event.preventDefault(); 15 | } 16 | 17 | export default function Memory() { 18 | let memory: number = 3; 19 | return ( 20 | 21 | Current maxmemory Limit 22 | 23 | {memory}GB 24 | 25 | 26 | of memory allocated 27 | 28 |
29 | {memory -= 1}}> 30 | {memory += 1}}> 31 |
32 |
33 | ); 34 | } -------------------------------------------------------------------------------- /src/client/components/Features.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Box from '@mui/material/Box'; 3 | import Button from '@mui/material/Button'; 4 | import Card from '@mui/material/Card'; 5 | import Chip from '@mui/material/Chip'; 6 | import Container from '@mui/material/Container'; 7 | import Grid from '@mui/material/Grid'; 8 | import Link from '@mui/material/Link'; 9 | import Stack from '@mui/material/Stack'; 10 | import Typography from '@mui/material/Typography'; 11 | import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded'; 12 | import ViewQuiltRoundedIcon from '@mui/icons-material/ViewQuiltRounded'; 13 | import ConstructionRoundedIcon from '@mui/icons-material/ConstructionRounded'; 14 | import ContactsRoundedIcon from '@mui/icons-material/ContactsRounded'; 15 | 16 | import metrics_img from '../../assets/Rediwatch-Metrics.png'; 17 | import profile_img from '../../assets/Splash-Profile.png'; 18 | import config_img from '../../assets/Splash-Configuration.png'; 19 | 20 | 21 | const items = [ 22 | { 23 | icon: , 24 | title: 'Dashboard', 25 | description: 26 | 'Provides instant insights into key metrics such as hit rates, memory usage, and more, allowing you to monitor and analyze your cache performance live.', 27 | imageLight: `url(${metrics_img})`, 28 | imageDark: `url(${metrics_img})`, 29 | }, 30 | { 31 | icon: , 32 | title: 'Configuration', 33 | description: 34 | 'Test out different Redis configurations directly from the app. RediWatch mocks your data during testing, so you can safely explore various setups without affecting your live Redis cache. ', 35 | imageLight: `url(${config_img})`, 36 | imageDark: `url(${config_img})`, 37 | }, 38 | { 39 | icon: , 40 | title: 'Profile', 41 | description: 42 | 'Save the cache configurations you test and experiment with. You can return anytime to review and reuse your previous settings without having to redo your tests.', 43 | imageLight: `url(${profile_img})`, 44 | imageDark: `url(${profile_img})`, 45 | }, 46 | ]; 47 | 48 | export default function Features() { 49 | const [selectedItemIndex, setSelectedItemIndex] = React.useState(0); 50 | 51 | const handleItemClick = (index: number) => { 52 | setSelectedItemIndex(index); 53 | }; 54 | 55 | const selectedFeature = items[selectedItemIndex]; 56 | 57 | return ( 58 | 59 | 60 | 61 |
62 | 63 | Features 64 | 65 | 70 | RediWatch offers a suite of robust features designed to enhance your Redis cache management. Explore our dynamic dashboard for real-time performance metrics, test and optimize different configurations safely, and effortlessly manage your cache profiles. 71 | 72 |
73 | 74 | {items.map(({ title }, index) => ( 75 | handleItemClick(index)} 79 | sx={{ 80 | borderColor: (theme) => { 81 | if (theme.palette.mode === 'light') { 82 | return selectedItemIndex === index ? 'primary.light' : ''; 83 | } 84 | return selectedItemIndex === index ? 'primary.light' : ''; 85 | }, 86 | background: (theme) => { 87 | if (theme.palette.mode === 'light') { 88 | return selectedItemIndex === index ? 'none' : ''; 89 | } 90 | return selectedItemIndex === index ? 'none' : ''; 91 | }, 92 | backgroundColor: selectedItemIndex === index ? 'primary.main' : '', 93 | '& .MuiChip-label': { 94 | color: selectedItemIndex === index ? '#fff' : '', 95 | }, 96 | }} 97 | /> 98 | ))} 99 | 100 | 108 | 111 | theme.palette.mode === 'light' 112 | ? items[selectedItemIndex].imageLight 113 | : items[selectedItemIndex].imageDark, 114 | backgroundSize: 'cover', 115 | backgroundPosition: 'center', 116 | minHeight: 280, 117 | }} 118 | /> 119 | 120 | 121 | {selectedFeature.title} 122 | 123 | 124 | {selectedFeature.description} 125 | 126 | svg': { transition: '0.2s' }, 134 | '&:hover > svg': { transform: 'translateX(2px)' }, 135 | }} 136 | > 137 | Learn more 138 | 142 | 143 | 144 | 145 | 153 | {items.map(({ icon, title, description }, index) => ( 154 | handleItemClick(index)} 159 | sx={{ 160 | p: 3, 161 | height: 'fit-content', 162 | width: '100%', 163 | background: 'none', 164 | backgroundColor: 165 | selectedItemIndex === index ? 'action.selected' : undefined, 166 | borderColor: (theme) => { 167 | if (theme.palette.mode === 'light') { 168 | return selectedItemIndex === index 169 | ? 'primary.light' 170 | : 'grey.200'; 171 | } 172 | return selectedItemIndex === index ? 'primary.dark' : 'grey.800'; 173 | }, 174 | }} 175 | > 176 | 186 | { 189 | if (theme.palette.mode === 'light') { 190 | return selectedItemIndex === index 191 | ? 'primary.main' 192 | : 'grey.300'; 193 | } 194 | return selectedItemIndex === index 195 | ? 'primary.main' 196 | : 'grey.700'; 197 | }, 198 | }} 199 | > 200 | {icon} 201 | 202 | 203 | 208 | {title} 209 | 210 | 215 | {description} 216 | 217 | svg': { transition: '0.2s' }, 225 | '&:hover > svg': { transform: 'translateX(2px)' }, 226 | }} 227 | onClick={(event) => { 228 | event.stopPropagation(); 229 | }} 230 | > 231 | Learn more 232 | 236 | 237 | 238 | 239 | 240 | ))} 241 | 242 |
243 | 249 | 258 | 267 | theme.palette.mode === 'light' 268 | ? items[selectedItemIndex].imageLight 269 | : items[selectedItemIndex].imageDark, 270 | }} 271 | /> 272 | 273 | 274 |
275 |
276 | ); 277 | } -------------------------------------------------------------------------------- /src/client/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Box from '@mui/material/Box'; 3 | import Button from '@mui/material/Button'; 4 | import Container from '@mui/material/Container'; 5 | import IconButton from '@mui/material/IconButton'; 6 | import Link from '@mui/material/Link'; 7 | import Stack from '@mui/material/Stack'; 8 | import TextField from '@mui/material/TextField'; 9 | import Typography from '@mui/material/Typography'; 10 | 11 | import FacebookIcon from '@mui/icons-material/GitHub'; 12 | import LinkedInIcon from '@mui/icons-material/LinkedIn'; 13 | import TwitterIcon from '@mui/icons-material/X'; 14 | 15 | import imgSrc from '../../assets/RediWatch_logo.png'; 16 | const logoStyle = { 17 | width: '140px', 18 | height: 'auto', 19 | }; 20 | 21 | function Copyright() { 22 | return ( 23 | 24 | {'Copyright © '} 25 | RediWatch  26 | {new Date().getFullYear()} 27 | 28 | ); 29 | } 30 | 31 | export default function Footer() { 32 | return ( 33 | 43 | 51 | 59 | 60 | 61 | logo of sitemark 66 | 67 | 68 | 69 | 76 | 77 | 78 | 85 | 86 | About us 87 | 88 | 89 | Article 90 | 91 | 92 | 99 | 100 | Legal 101 | 102 | 103 | Terms 104 | 105 | 106 | Privacy 107 | 108 | 109 | Contact 110 | 111 | 112 | 113 | 123 |
124 | 125 | Privacy Policy 126 | 127 | 128 |  •  129 | 130 | 131 | Terms of Service 132 | 133 | 134 |
135 | 144 | 150 | 151 | 152 | 158 | 159 | 160 | 161 |
162 |
163 | ); 164 | } -------------------------------------------------------------------------------- /src/client/components/Hero.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { alpha } from '@mui/material'; 3 | import Box from '@mui/material/Box'; 4 | import Button from '@mui/material/Button'; 5 | import Container from '@mui/material/Container'; 6 | import Link from '@mui/material/Link'; 7 | import Stack from '@mui/material/Stack'; 8 | import TextField from '@mui/material/TextField'; 9 | import Typography from '@mui/material/Typography'; 10 | 11 | import imgSrc from '../../assets/Rediwatch-Metrics.png'; 12 | 13 | export default function Hero() { 14 | return ( 15 | ({ 18 | width: '100%', 19 | backgroundImage: 20 | theme.palette.mode === 'light' 21 | ? 'linear-gradient(180deg, #CEE5FD, #FFF)' 22 | : `linear-gradient(#02294F, ${alpha('#090E10', 0.0)})`, 23 | backgroundSize: '100% 20%', 24 | backgroundRepeat: 'no-repeat', 25 | })} 26 | > 27 | 36 | 37 | 47 | Monitoring Your 48 | 54 | theme.palette.mode === 'light' ? 'primary.main' : 'primary.light', 55 | }} 56 | > 57 | Redis Cache Performance 58 | 59 | 60 | 65 | Transform your Redis cache management with RediWatch—your instant gateway to visualizing and optimizing cache performance. Seamlessly monitor hit rates, memory usage, and configurations, all without interrupting your live cache. 66 | 67 | 68 | ({ 71 | mt: { xs: 8, sm: 10 }, 72 | alignSelf: 'center', 73 | height: { xs: 200, sm: 600 }, 74 | width: '100%', 75 | backgroundImage: `url(${imgSrc})`, 76 | backgroundSize: 'contain', 77 | backgroundPosition: 'center', 78 | backgroundRepeat: 'no-repeat', 79 | borderRadius: '10px', 80 | outline: '1px solid', 81 | outlineColor: 82 | theme.palette.mode === 'light' 83 | ? alpha('#BFCCD9', 0.5) 84 | : alpha('#9CCCFC', 0.1), 85 | boxShadow: 86 | theme.palette.mode === 'light' 87 | ? `0 0 12px 8px ${alpha('#9CCCFC', 0.2)}` 88 | : `0 0 24px 12px ${alpha('#033363', 0.2)}`, 89 | position: 'relative', // Establish stacking context for the image 90 | overflow: 'hidden', // Ensure the image doesn’t overflow 91 | })} 92 | > 93 | 94 | 95 | 96 | ); 97 | } -------------------------------------------------------------------------------- /src/client/components/Highlights.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Box from '@mui/material/Box'; 3 | import Card from '@mui/material/Card'; 4 | import Container from '@mui/material/Container'; 5 | import Grid from '@mui/material/Grid'; 6 | import Stack from '@mui/material/Stack'; 7 | import Typography from '@mui/material/Typography'; 8 | import AutoFixHighRoundedIcon from '@mui/icons-material/AutoFixHighRounded'; 9 | import ConstructionRoundedIcon from '@mui/icons-material/ConstructionRounded'; 10 | import QueryStatsRoundedIcon from '@mui/icons-material/QueryStatsRounded'; 11 | import SettingsSuggestRoundedIcon from '@mui/icons-material/SettingsSuggestRounded'; 12 | import SupportAgentRoundedIcon from '@mui/icons-material/SupportAgentRounded'; 13 | import ThumbUpAltRoundedIcon from '@mui/icons-material/ThumbUpAltRounded'; 14 | import MemoryRoundedIcon from '@mui/icons-material/MemoryRounded'; 15 | import ScreenSearchDesktopRoundedIcon from '@mui/icons-material/ScreenSearchDesktopRounded'; 16 | import AssignmentIndRoundedIcon from '@mui/icons-material/AssignmentIndRounded'; 17 | import CheckRoundedIcon from '@mui/icons-material/CheckRounded'; 18 | 19 | const items = [ 20 | { 21 | icon: , 22 | title: 'Real-Time Performance Metrics', 23 | description: 24 | 'Get instant updates on key performance indicators like cache hit rates, memory usage, and more.', 25 | }, 26 | { 27 | icon: , 28 | title: 'Efficient Cache Monitoring', 29 | description: 30 | 'Quickly assess and fine-tune your Redis cache’s performance with detailed insights and visualizations.', 31 | }, 32 | { 33 | icon: , 34 | title: 'Smart Configuration Management', 35 | description: 36 | 'Save and revisit your cache configurations, enabling you to easily compare and apply optimal setups.', 37 | }, 38 | { 39 | icon: , 40 | title: 'Profile Management', 41 | description: 42 | 'Save and manage multiple cache configurations for easy reference and reuse. ', 43 | }, 44 | { 45 | icon: , 46 | title: 'User-Friendly Dashboard', 47 | description: 48 | 'A sleek interface for easy monitoring and analysis of your Redis cache performance.', 49 | }, 50 | { 51 | icon: , 52 | title: 'Seamless Integration', 53 | description: 54 | 'Effortlessly integrate with your existing Redis setup for a smooth experience.', 55 | }, 56 | ]; 57 | 58 | export default function Highlights() { 59 | return ( 60 | 69 | 78 | 84 | 85 | Highlights 86 | 87 | 88 | Discover the power of RediWatch with key features designed to optimize and simplify your Redis cache management. Effortlessly monitor your cache’s performance with real-time insights, fine-tune configurations using intuitive tools, and manage multiple setups with ease. Enjoy a seamless experience with efficient cache monitoring and smart configuration management, all from one user-friendly platform. 89 | 90 | 91 | 92 | {items.map((item, index) => ( 93 | 94 | 109 | {item.icon} 110 |
111 | 112 | {item.title} 113 | 114 | 115 | {item.description} 116 | 117 |
118 |
119 |
120 | ))} 121 |
122 |
123 |
124 | ); 125 | } -------------------------------------------------------------------------------- /src/client/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useState } from 'react'; 3 | import { styled, createTheme, ThemeProvider } from '@mui/material/styles'; 4 | import CssBaseline from '@mui/material/CssBaseline'; 5 | import MuiDrawer from '@mui/material/Drawer'; 6 | import Box from '@mui/material/Box'; 7 | import MuiAppBar, { AppBarProps as MuiAppBarProps } from '@mui/material/AppBar'; 8 | import Toolbar from '@mui/material/Toolbar'; 9 | import List from '@mui/material/List'; 10 | import Typography from '@mui/material/Typography'; 11 | import Divider from '@mui/material/Divider'; 12 | import IconButton from '@mui/material/IconButton'; 13 | import Badge from '@mui/material/Badge'; 14 | import Container from '@mui/material/Container'; 15 | import Link from '@mui/material/Link'; 16 | import MenuIcon from '@mui/icons-material/Menu'; 17 | import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; 18 | import NotificationsIcon from '@mui/icons-material/Notifications'; 19 | import { mainListItems, secondaryListItems } from './NavBar'; 20 | import TextField from '@mui/material/TextField'; 21 | import { FormGroup } from '@mui/material'; 22 | import FormControlLabel from '@mui/material/FormControlLabel'; 23 | import Checkbox from '@mui/material/Checkbox'; 24 | import Button from '@mui/material/Button'; 25 | 26 | import imgSrc from '../../assets/RediWatch_logo.png'; 27 | 28 | function Copyright(props: any) { 29 | return ( 30 | 36 | {'Copyright © '} 37 | 41 | RediWatch 42 | {' '} 43 | {new Date().getFullYear()} 44 | {'.'} 45 | 46 | ); 47 | } 48 | 49 | const drawerWidth: number = 240; 50 | 51 | interface AppBarProps extends MuiAppBarProps { 52 | open?: boolean; 53 | } 54 | 55 | const AppBar = styled(MuiAppBar, { 56 | shouldForwardProp: (prop) => prop !== 'open', 57 | })(({ theme, open }) => ({ 58 | zIndex: theme.zIndex.drawer + 1, 59 | transition: theme.transitions.create(['width', 'margin'], { 60 | easing: theme.transitions.easing.sharp, 61 | duration: theme.transitions.duration.leavingScreen, 62 | }), 63 | ...(open && { 64 | marginLeft: drawerWidth, 65 | width: `calc(100% - ${drawerWidth}px)`, 66 | transition: theme.transitions.create(['width', 'margin'], { 67 | easing: theme.transitions.easing.sharp, 68 | duration: theme.transitions.duration.enteringScreen, 69 | }), 70 | }), 71 | })); 72 | 73 | const Drawer = styled(MuiDrawer, { 74 | shouldForwardProp: (prop) => prop !== 'open', 75 | })(({ theme, open }) => ({ 76 | '& .MuiDrawer-paper': { 77 | position: 'relative', 78 | whiteSpace: 'nowrap', 79 | width: drawerWidth, 80 | transition: theme.transitions.create('width', { 81 | easing: theme.transitions.easing.sharp, 82 | duration: theme.transitions.duration.enteringScreen, 83 | }), 84 | boxSizing: 'border-box', 85 | ...(!open && { 86 | overflowX: 'hidden', 87 | transition: theme.transitions.create('width', { 88 | easing: theme.transitions.easing.sharp, 89 | duration: theme.transitions.duration.leavingScreen, 90 | }), 91 | width: theme.spacing(7), 92 | [theme.breakpoints.up('sm')]: { 93 | width: theme.spacing(9), 94 | }, 95 | }), 96 | }, 97 | })); 98 | 99 | // Set theme to dark mode always 100 | const darkTheme = createTheme({ 101 | palette: { 102 | mode: 'dark', 103 | primary: { 104 | main: '#C63124', 105 | }, 106 | }, 107 | }); 108 | 109 | export default function Dashboard() { 110 | const [open, setOpen] = React.useState(true); 111 | const toggleDrawer = () => { 112 | setOpen(!open); 113 | }; 114 | 115 | //connection string and nickname state management 116 | const [string, setString] = useState(''); 117 | const [nickname, setNickname] = useState(''); 118 | 119 | //updates the nickname state to user input 120 | const handleNicknameInput = ( 121 | e: React.ChangeEvent 122 | ): void => { 123 | if (e.target.value.length >= 1) { 124 | setNickname(e.target.value); 125 | } 126 | }; 127 | //updates the connection string state to user input 128 | const handleConnectionStringInput = ( 129 | e: React.ChangeEvent 130 | ): void => { 131 | if (e.target.value.length >= 1) { 132 | setString(e.target.value); 133 | } 134 | }; 135 | 136 | //handlesubmit function once form is submitted 137 | function handleSubmit(event: React.FormEvent) { 138 | event.preventDefault(); 139 | //grabs connectin string and nickname from state 140 | const data = { 141 | connectionnickname: nickname, 142 | connectionstring: string, 143 | //hardcoding the user_id, will eventually need to pull this from the user somehow :-) 144 | user_id: 1, 145 | }; 146 | //post request to "add connection" 147 | fetch('/api/add-connection', { 148 | method: 'POST', 149 | headers: { 150 | 'Content-Type': 'application/json', 151 | }, 152 | body: JSON.stringify(data), 153 | }) 154 | .then((response) => response.json()) 155 | .then((data) => { 156 | console.log('Success:', data); 157 | alert('Connection Added Successfully'); 158 | }) 159 | .catch((error) => { 160 | console.error('Error:', error); 161 | alert('Failed to add connection'); 162 | }); 163 | } 164 | 165 | return ( 166 | 167 | 177 | 178 | 179 | 180 | 185 | 195 | 196 | 197 | 204 | Home 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | {mainListItems} 229 | 230 | {secondaryListItems} 231 | 232 | 233 | 237 | theme.palette.mode === 'light' 238 | ? theme.palette.grey[100] 239 | : theme.palette.grey[900], 240 | flexGrow: 1, 241 | height: '100vh', 242 | overflow: 'auto', 243 | }} 244 | > 245 | 246 | 247 | 248 | Create a New Connection 249 |

250 |
251 | 252 | 261 |

262 | 271 |

272 | } 274 | label="Save Connection" 275 | /> 276 |

277 | 284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 | ); 292 | } 293 | -------------------------------------------------------------------------------- /src/client/components/LandingPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { PaletteMode } from '@mui/material'; 3 | import CssBaseline from '@mui/material/CssBaseline'; 4 | import Box from '@mui/material/Box'; 5 | import Divider from '@mui/material/Divider'; 6 | import { ThemeProvider, createTheme } from '@mui/material/styles'; 7 | import ToggleButton from '@mui/material/ToggleButton'; 8 | import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; 9 | import AutoAwesomeRoundedIcon from '@mui/icons-material/AutoAwesomeRounded'; 10 | import AppAppBar from './AppAppBar'; 11 | import Hero from './Hero'; 12 | import Highlights from './Highlights'; 13 | import Features from './Features'; 14 | import Footer from './Footer'; 15 | import getLPTheme from './getLPTheme'; 16 | 17 | interface ToggleCustomThemeProps { 18 | showCustomTheme: Boolean; 19 | toggleCustomTheme: () => void; 20 | } 21 | 22 | function ToggleCustomTheme({ 23 | showCustomTheme, 24 | toggleCustomTheme, 25 | }: ToggleCustomThemeProps) { 26 | return ( 27 | 37 | 50 | 51 | 52 | Custom theme 53 | 54 | Material Design 2 55 | 56 | 57 | ); 58 | } 59 | 60 | export default function LandingPage() { 61 | const [mode, setMode] = React.useState('light'); 62 | const [showCustomTheme, setShowCustomTheme] = React.useState(true); 63 | const LPtheme = createTheme(getLPTheme(mode)); 64 | const defaultTheme = createTheme({ palette: { mode } }); 65 | 66 | const toggleColorMode = () => { 67 | setMode((prev) => (prev === 'dark' ? 'light' : 'dark')); 68 | }; 69 | 70 | const toggleCustomTheme = () => { 71 | setShowCustomTheme((prev) => !prev); 72 | }; 73 | 74 | return ( 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 |