├── .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 |
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 |
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 |
46 |
47 |
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 |
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 |
156 | Sign in
157 |
158 |
166 | Sign up
167 |
168 |
169 |
170 |
177 |
178 |
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 |
219 | Sign up
220 |
221 |
222 |
223 |
231 | Sign in
232 |
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 | }
91 | renderValue={(selected) => (
92 |
93 |
94 |
95 | )}
96 | MenuProps={MenuProps}
97 | >
98 | {policies.map((policy) => (
99 |
104 | {policy}
105 |
106 | ))}
107 |
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 | } style={{marginLeft: "50%", marginTop: "15px"}}>Run Performance Test
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 |
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 |
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 |
85 |
86 |
87 | );
88 | }
--------------------------------------------------------------------------------
/src/client/components/Metrics.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { styled, createTheme, ThemeProvider } from '@mui/material/styles';
3 | import CssBaseline from '@mui/material/CssBaseline';
4 | import MuiDrawer from '@mui/material/Drawer';
5 | import Box from '@mui/material/Box';
6 | import MuiAppBar, { AppBarProps as MuiAppBarProps } from '@mui/material/AppBar';
7 | import Toolbar from '@mui/material/Toolbar';
8 | import List from '@mui/material/List';
9 | import Typography from '@mui/material/Typography';
10 | import Divider from '@mui/material/Divider';
11 | import IconButton from '@mui/material/IconButton';
12 | import Badge from '@mui/material/Badge';
13 | import Container from '@mui/material/Container';
14 | import Grid from '@mui/material/Grid';
15 | import Paper from '@mui/material/Paper';
16 | import Link from '@mui/material/Link';
17 | import MenuIcon from '@mui/icons-material/Menu';
18 | import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
19 | import NotificationsIcon from '@mui/icons-material/Notifications';
20 | import { mainListItems, secondaryListItems } from './NavBar';
21 |
22 | import imgSrc from '../../assets/RediWatch_logo.png';
23 |
24 | function Copyright(props: any) {
25 | return (
26 |
27 | {'Copyright © '}
28 |
29 | RediWatch
30 | {' '}
31 | {new Date().getFullYear()}
32 | {'.'}
33 |
34 | );
35 | }
36 |
37 | const drawerWidth: number = 240;
38 |
39 | interface AppBarProps extends MuiAppBarProps {
40 | open?: boolean;
41 | }
42 |
43 | const AppBar = styled(MuiAppBar, {
44 | shouldForwardProp: (prop) => prop !== 'open',
45 | })(({ theme, open }) => ({
46 | zIndex: theme.zIndex.drawer + 1,
47 | transition: theme.transitions.create(['width', 'margin'], {
48 | easing: theme.transitions.easing.sharp,
49 | duration: theme.transitions.duration.leavingScreen,
50 | }),
51 | ...(open && {
52 | marginLeft: drawerWidth,
53 | width: `calc(100% - ${drawerWidth}px)`,
54 | transition: theme.transitions.create(['width', 'margin'], {
55 | easing: theme.transitions.easing.sharp,
56 | duration: theme.transitions.duration.enteringScreen,
57 | }),
58 | }),
59 | }));
60 |
61 | const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' })(
62 | ({ theme, open }) => ({
63 | '& .MuiDrawer-paper': {
64 | position: 'relative',
65 | whiteSpace: 'nowrap',
66 | width: drawerWidth,
67 | transition: theme.transitions.create('width', {
68 | easing: theme.transitions.easing.sharp,
69 | duration: theme.transitions.duration.enteringScreen,
70 | }),
71 | boxSizing: 'border-box',
72 | ...(!open && {
73 | overflowX: 'hidden',
74 | transition: theme.transitions.create('width', {
75 | easing: theme.transitions.easing.sharp,
76 | duration: theme.transitions.duration.leavingScreen,
77 | }),
78 | width: theme.spacing(7),
79 | [theme.breakpoints.up('sm')]: {
80 | width: theme.spacing(9),
81 | },
82 | }),
83 | },
84 | }),
85 | );
86 |
87 | // Set theme to dark mode always
88 | const darkTheme = createTheme({
89 | palette: {
90 | mode: 'dark',
91 | primary: {
92 | main: '#C63124',
93 | }
94 | },
95 | });
96 |
97 | export default function Dashboard() {
98 | const [open, setOpen] = React.useState(true);
99 | const toggleDrawer = () => {
100 | setOpen(!open);
101 | };
102 |
103 | return (
104 |
105 |
115 |
116 |
117 |
118 |
123 |
133 |
134 |
135 |
142 | Metrics
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | {mainListItems}
167 |
168 | {secondaryListItems}
169 |
170 |
171 |
175 | theme.palette.mode === 'light'
176 | ? theme.palette.grey[100]
177 | : theme.palette.grey[900],
178 | flexGrow: 1,
179 | height: '100vh',
180 | overflow: 'auto',
181 | }}
182 | >
183 |
184 |
185 |
186 | {/* Chart */}
187 |
188 |
196 |
197 |
198 |
199 | {/* Recent Deposits */}
200 |
201 |
209 |
210 |
211 |
212 | {/* Recent Orders */}
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 | );
227 | }
--------------------------------------------------------------------------------
/src/client/components/NavBar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import ListItemButton from '@mui/material/ListItemButton';
3 | import ListItemIcon from '@mui/material/ListItemIcon';
4 | import ListItemText from '@mui/material/ListItemText';
5 | import ListSubheader from '@mui/material/ListSubheader';
6 | import HomeIcon from '@mui/icons-material/Home';
7 | import PersonIcon from '@mui/icons-material/Person';
8 | import SettingsIcon from '@mui/icons-material/Settings';
9 | import BarChartIcon from '@mui/icons-material/BarChart';
10 | import LogoutIcon from '@mui/icons-material/Logout';
11 | import AssignmentIcon from '@mui/icons-material/Assignment';
12 |
13 | export const mainListItems = (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | );
47 |
48 | export const secondaryListItems = (
49 |
50 |
51 | Beta Features
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | );
73 |
--------------------------------------------------------------------------------
/src/client/components/Profile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { styled, createTheme, ThemeProvider } from '@mui/material/styles';
3 | import CssBaseline from '@mui/material/CssBaseline';
4 | import MuiDrawer from '@mui/material/Drawer';
5 | import Box from '@mui/material/Box';
6 | import MuiAppBar, { AppBarProps as MuiAppBarProps } from '@mui/material/AppBar';
7 | import Toolbar from '@mui/material/Toolbar';
8 | import List from '@mui/material/List';
9 | import Typography from '@mui/material/Typography';
10 | import Divider from '@mui/material/Divider';
11 | import IconButton from '@mui/material/IconButton';
12 | import Badge from '@mui/material/Badge';
13 | import Container from '@mui/material/Container';
14 | import Grid from '@mui/material/Grid';
15 | import Paper from '@mui/material/Paper';
16 | import Link from '@mui/material/Link';
17 | import MenuIcon from '@mui/icons-material/Menu';
18 | import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
19 | import NotificationsIcon from '@mui/icons-material/Notifications';
20 | import { mainListItems, secondaryListItems } from './NavBar';
21 | import ProfileCaches from './ProfileCaches';
22 |
23 | import imgSrc from '../../assets/RediWatch_logo.png';
24 |
25 | function Copyright(props: any) {
26 | return (
27 |
28 | {'Copyright © '}
29 |
30 | RediWatch
31 | {' '}
32 | {new Date().getFullYear()}
33 | {'.'}
34 |
35 | );
36 | }
37 |
38 | const drawerWidth: number = 240;
39 |
40 | interface AppBarProps extends MuiAppBarProps {
41 | open?: boolean;
42 | }
43 |
44 | const AppBar = styled(MuiAppBar, {
45 | shouldForwardProp: (prop) => prop !== 'open',
46 | })(({ theme, open }) => ({
47 | zIndex: theme.zIndex.drawer + 1,
48 | transition: theme.transitions.create(['width', 'margin'], {
49 | easing: theme.transitions.easing.sharp,
50 | duration: theme.transitions.duration.leavingScreen,
51 | }),
52 | ...(open && {
53 | marginLeft: drawerWidth,
54 | width: `calc(100% - ${drawerWidth}px)`,
55 | transition: theme.transitions.create(['width', 'margin'], {
56 | easing: theme.transitions.easing.sharp,
57 | duration: theme.transitions.duration.enteringScreen,
58 | }),
59 | }),
60 | }));
61 |
62 | const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' })(
63 | ({ theme, open }) => ({
64 | '& .MuiDrawer-paper': {
65 | position: 'relative',
66 | whiteSpace: 'nowrap',
67 | width: drawerWidth,
68 | transition: theme.transitions.create('width', {
69 | easing: theme.transitions.easing.sharp,
70 | duration: theme.transitions.duration.enteringScreen,
71 | }),
72 | boxSizing: 'border-box',
73 | ...(!open && {
74 | overflowX: 'hidden',
75 | transition: theme.transitions.create('width', {
76 | easing: theme.transitions.easing.sharp,
77 | duration: theme.transitions.duration.leavingScreen,
78 | }),
79 | width: theme.spacing(7),
80 | [theme.breakpoints.up('sm')]: {
81 | width: theme.spacing(9),
82 | },
83 | }),
84 | },
85 | }),
86 | );
87 |
88 | // Set theme to dark mode always
89 | const darkTheme = createTheme({
90 | palette: {
91 | mode: 'dark',
92 | primary: {
93 | main: '#C63124',
94 | }
95 | },
96 | });
97 | export default function Profile() {
98 | const [open, setOpen] = React.useState(true);
99 | const toggleDrawer = () => {
100 | setOpen(!open);
101 | };
102 |
103 | return (
104 |
105 |
115 |
116 |
117 |
118 |
123 |
133 |
134 |
135 |
142 | Profile
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | {mainListItems}
167 |
168 | {secondaryListItems}
169 |
170 |
171 |
175 | theme.palette.mode === 'light'
176 | ? theme.palette.grey[100]
177 | : theme.palette.grey[900],
178 | flexGrow: 1,
179 | height: '100vh',
180 | overflow: 'auto',
181 | }}
182 | >
183 |
184 |
185 |
186 | {/* Profiles */}
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 | );
199 | }
--------------------------------------------------------------------------------
/src/client/components/ProfileCaches.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 Order Data
11 | function createData(
12 | id: number,
13 | connection: string,
14 | port: string,
15 | password: string,
16 | nickName: string,
17 | configs: {name:string, value: string}[],
18 | ) {
19 | return { id, connection, port, password, nickName, configs };
20 | }
21 |
22 | const rows = [
23 | createData(
24 | 0,
25 | 'redis-17515.c60.us-west-1-2.ec2.redns.redis-cloud.com',
26 | '17515',
27 | 'lBz890STemdFFlKctFBP6NdUVpmJ2ZMS',
28 | 'test1',
29 | [{"name": 'LTT',"value":'30'}, {"name": 'LLU',"value":'false'},{"name": 'LKU',"value":'true'},{"name": 'memoryalloct',"value":'50gb'}]
30 | ),
31 | createData(
32 | 1,
33 | 'redis-17516.c60.us-west-1-2.ec2.redns.redis-cloud.com',
34 | '17515',
35 | 'lBz890STemdFFlKctFBP6NdUVpmJ2ZMS',
36 | 'test2',
37 | [{"name": 'LTT',"value":'30'}, {"name": 'LLU',"value":'true'},{"name": 'LKU',"value":'false'}]
38 | ),
39 | createData(
40 | 2,
41 | 'redis-17517.c60.us-west-1-2.ec2.redns.redis-cloud.com',
42 | '17515',
43 | 'lBz890STemdFFlKctFBP6NdUVpmJ2ZMS',
44 | 'test3',
45 | [{"name": 'LTT',"value":'30'}, {"name": 'LLU',"value":'true'}]
46 | ),
47 | createData(
48 | 3,
49 | 'redis-17518.c60.us-west-1-2.ec2.redns.redis-cloud.com',
50 | '17515',
51 | 'lBz890STemdFFlKctFBP6NdUVpmJ2ZMS',
52 | 'test4',
53 | [{"name": 'LTT',"value":'30'}, {"name": 'LLU',"value":'true'}]
54 | ),
55 | ];
56 |
57 | function preventDefault(event: React.MouseEvent) {
58 | event.preventDefault();
59 | }
60 |
61 | export default function ConfigurationMethods1() {
62 | return (
63 |
64 | Profile
65 |
66 |
67 |
68 | Name
69 | Connection
70 | Config
71 |
72 |
73 |
74 | {rows.map((row) => (
75 |
76 | {row.nickName}
77 | {row.connection}
78 | {row.configs.map(({ name, value }) => `${name}:${value}`).join(',')}
79 |
80 | ))}
81 |
82 |
83 |
84 | Show more
85 |
86 |
87 | );
88 | }
--------------------------------------------------------------------------------
/src/client/components/Signin.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Avatar from "@mui/material/Avatar";
3 | import Button from "@mui/material/Button";
4 | import CssBaseline from "@mui/material/CssBaseline";
5 | import TextField from "@mui/material/TextField";
6 | import FormControlLabel from "@mui/material/FormControlLabel";
7 | import Checkbox from "@mui/material/Checkbox";
8 | import Link from "@mui/material/Link";
9 | import Paper from "@mui/material/Paper";
10 | import Box from "@mui/material/Box";
11 | import Grid from "@mui/material/Grid";
12 | import Alert from "@mui/material/Alert";
13 | import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
14 | import Typography from "@mui/material/Typography";
15 | import { createTheme, ThemeProvider } from "@mui/material/styles";
16 | import { Link as RouterLink, useNavigate } from "react-router-dom";
17 | import { useState } from "react";
18 | import videoSrc from "../../assets/background.mp4";
19 | import imgSrc from "../../assets/RediWatch_logo.png";
20 |
21 |
22 | // const [email, setEmail] = useState('');
23 | // const [password, setPassword] = useState('');
24 | function Copyright(props: any) {
25 | return (
26 |
32 | {"Copyright © "}
33 |
37 | RediWatch
38 | {" "}
39 | {new Date().getFullYear()}
40 | {"."}
41 |
42 | );
43 | }
44 |
45 | // Set theme to dark mode always
46 | const darkTheme = createTheme({
47 | palette: {
48 | mode: "dark",
49 | primary: {
50 | main: "#C63124",
51 | },
52 | },
53 | });
54 |
55 | export default function SignInSide() {
56 | const navigate = useNavigate();
57 | const [error, setError] = useState('');
58 | const handleSubmit = async(event: React.FormEvent) => {
59 | event.preventDefault();
60 | setError('');
61 | const data = new FormData(event.currentTarget);
62 | const email = data.get('email');
63 | const password = data.get('password');
64 | const res = await fetch ('/api/users/signin', {
65 | method: 'post',
66 | headers: {'Content-Type': 'application/json'},
67 | body: JSON.stringify({email, password}),
68 | })
69 | if(res.ok){
70 | const user = await res.json();
71 | localStorage.setItem('userToken', user.token);
72 | navigate('/home');
73 | }
74 | else{
75 | const errorData = await res.json();
76 | setError(errorData.error || 'An error occurred during login.');
77 | }
78 | };
79 |
80 | return (
81 |
82 |
87 |
88 |
101 |
114 |
115 | Your browser does not support the video tag.
116 |
117 |
128 |
129 |
130 |
139 |
140 |
141 |
142 |
143 | Sign in
144 |
145 |
151 |
161 |
171 | {/* }
173 | label="Remember me"
174 | /> */}
175 |
182 | Sign In
183 |
184 |
185 | {/*
186 |
187 | Forgot password?
188 |
189 | */}
190 |
191 |
192 | {"Don't have an account? Sign Up"}
193 |
194 |
195 |
196 | {error && (
197 |
198 | {error}
199 |
200 | )}
201 |
202 |
203 |
204 |
205 |
206 |
207 | );
208 | }
209 |
--------------------------------------------------------------------------------
/src/client/components/Signup.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import Avatar from "@mui/material/Avatar";
3 | import Button from "@mui/material/Button";
4 | import CssBaseline from "@mui/material/CssBaseline";
5 | import TextField from "@mui/material/TextField";
6 | import FormControlLabel from "@mui/material/FormControlLabel";
7 | import Checkbox from "@mui/material/Checkbox";
8 | import Link from "@mui/material/Link";
9 | import Grid from "@mui/material/Grid";
10 | import Box from "@mui/material/Box";
11 | import LockOutlinedIcon from "@mui/icons-material/LockOutlined";
12 | import Typography from "@mui/material/Typography";
13 | import Container from "@mui/material/Container";
14 | import { createTheme, ThemeProvider } from "@mui/material/styles";
15 | import { Link as RouterLink, useNavigate } from "react-router-dom";
16 | import { useState } from "react";
17 | import Alert from "@mui/material/Alert";
18 |
19 | function Copyright(props: any) {
20 | return (
21 |
27 | {"Copyright © "}
28 |
32 | RediWatch
33 | {" "}
34 | {new Date().getFullYear()}
35 | {"."}
36 |
37 | );
38 | }
39 |
40 | // Set theme to dark mode always
41 | const darkTheme = createTheme({
42 | palette: {
43 | mode: "dark",
44 | primary: {
45 | main: "#C63124",
46 | },
47 | },
48 | });
49 |
50 | export default function SignUp() {
51 | const navigate = useNavigate();
52 | const [error, setError] = useState("");
53 | const handleSubmit = async (event: React.FormEvent) => {
54 | event.preventDefault();
55 | const data = new FormData(event.currentTarget);
56 | const firstName = data.get("firstName");
57 | const lastName = data.get("lastName");
58 | const email = data.get("email");
59 | const password = data.get("password");
60 | const res = await fetch("/api/users/signup", {
61 | method: "post",
62 | headers: { "Content-Type": "application/json" },
63 | body: JSON.stringify({ firstName, lastName, email, password }),
64 | });
65 | if (res.ok) {
66 | navigate("/");
67 | } else {
68 | const errorData = await res.json();
69 | setError(errorData.error || "An error occurred during login.");
70 | }
71 | };
72 |
73 | return (
74 |
75 |
76 |
77 |
85 |
86 |
87 |
88 |
89 | Sign up
90 |
91 |
97 |
98 |
99 |
108 |
109 |
110 |
118 |
119 |
120 |
128 |
129 |
130 |
139 |
140 | {/*
141 | }
143 | label="I want to receive inspiration, marketing promotions and updates via email."
144 | />
145 | */}
146 |
147 |
154 | Sign Up
155 |
156 |
157 |
158 |
159 | Already have an account? Sign in
160 |
161 |
162 |
163 |
164 |
165 | {error && (
166 |
167 | {error}
168 |
169 | )}
170 |
171 |
172 |
173 | );
174 | }
175 |
--------------------------------------------------------------------------------
/src/client/components/TTLDropdown.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @module TTLDropdown
3 | * @description dropdown component for selecting cache key TTL
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 times = [
28 | '5 seconds',
29 | '10 seconds',
30 | '15 seconds',
31 | '20 seconds',
32 | '25 seconds',
33 | '30 seconds',
34 | '35 seconds',
35 | '40 seconds',
36 | '45 seconds',
37 | '50 seconds',
38 | '55 seconds',
39 | '60 seconds',
40 | '65 seconds',
41 | ];
42 |
43 | function getStyles(time: string, TTLtime: string, theme: Theme) {
44 | return {
45 | fontWeight:
46 | TTLtime.indexOf(time) === -1
47 | ? theme.typography.fontWeightRegular
48 | : theme.typography.fontWeightMedium,
49 | };
50 | }
51 |
52 | export default function MultipleSelectChip() {
53 | const theme = useTheme();
54 | const [TTLtime, setTTLtime] = React.useState('');
55 |
56 | const handleChange = (event: SelectChangeEvent) => {
57 | const {
58 | target: { value },
59 | } = event;
60 | setTTLtime(
61 | // On autofill we get a stringified value.
62 | typeof value === 'string' ? value : '60',
63 | );
64 |
65 | alert(value);
66 |
67 | // post request to "update config"
68 | // fetch('/api/update-policy', {
69 | // method: 'POST',
70 | // headers: {
71 | // 'Content-Type': 'application/json',
72 | // },
73 | // body: JSON.stringify(value),
74 | // })
75 | // .then(res => res.json())
76 | // .then(data => {
77 | // console.log('Successfully updated:', data);
78 | // alert('Updated cache configuration');
79 | // })
80 | // .catch((err) => {
81 | // console.error('Error updating config:', err);
82 | // alert('Failed to update cache configuration');
83 | // });
84 | };
85 |
86 | return (
87 |
88 |
89 | Time-To-Live
90 | }
96 | renderValue={(selected) => (
97 |
98 |
99 |
100 | )}
101 | MenuProps={MenuProps}
102 | >
103 | {times.map((time) => (
104 |
109 | {time}
110 |
111 | ))}
112 |
113 |
114 |
115 | );
116 | }
--------------------------------------------------------------------------------
/src/client/components/Title.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Typography from '@mui/material/Typography';
3 |
4 | interface TitleProps {
5 | children?: React.ReactNode;
6 | }
7 |
8 | export default function Title(props: TitleProps) {
9 | return (
10 |
11 | {props.children}
12 |
13 | );
14 | }
--------------------------------------------------------------------------------
/src/client/components/ToggleColorMode.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { PaletteMode } from '@mui/material';
3 | import Box from '@mui/material/Box';
4 | import Button from '@mui/material/Button';
5 |
6 | import WbSunnyRoundedIcon from '@mui/icons-material/WbSunnyRounded';
7 | import ModeNightRoundedIcon from '@mui/icons-material/ModeNightRounded';
8 |
9 | interface ToggleColorModeProps {
10 | mode: PaletteMode;
11 | toggleColorMode: () => void;
12 | }
13 |
14 | function ToggleColorMode({ mode, toggleColorMode }: ToggleColorModeProps) {
15 | return (
16 |
17 |
24 | {mode === 'dark' ? (
25 |
26 | ) : (
27 |
28 | )}
29 |
30 |
31 | );
32 | }
33 |
34 | export default ToggleColorMode;
--------------------------------------------------------------------------------
/src/client/components/getLPTheme.tsx:
--------------------------------------------------------------------------------
1 | import type {} from '@mui/material/themeCssVarsAugmentation';
2 | import { ThemeOptions, alpha } from '@mui/material/styles';
3 | import { red } from '@mui/material/colors';
4 | import { PaletteMode } from '@mui/material';
5 |
6 | declare module '@mui/material/styles/createPalette' {
7 | interface ColorRange {
8 | 50: string;
9 | 100: string;
10 | 200: string;
11 | 300: string;
12 | 400: string;
13 | 500: string;
14 | 600: string;
15 | 700: string;
16 | 800: string;
17 | 900: string;
18 | }
19 |
20 | interface PaletteColor extends ColorRange {}
21 | }
22 |
23 | export const brand = {
24 | 50: '#FFE9E7', // Very pale red
25 | 100: '#FFC9C5', // Light pinkish red
26 | 200: '#FFA19C', // Soft coral red
27 | 300: '#FF6D5F', // Bright salmon red
28 | 400: '#FF3C30', // Vivid red
29 | 500: '#E5382A', // Strong red
30 | 600: '#D33227', // Dark red
31 | 700: '#C63124', // Deep red (your specified color)
32 | 800: '#A52720', // Dark crimson red
33 | 900: '#871D1A' // Almost burgundy
34 | };
35 |
36 | export const secondary = {
37 | 50: '#F9F0FF',
38 | 100: '#E9CEFD',
39 | 200: '#D49CFC',
40 | 300: '#B355F6',
41 | 400: '#750AC2',
42 | 500: '#6709AA',
43 | 600: '#490679',
44 | 700: '#3B0363',
45 | 800: '#2F024F',
46 | 900: '#23023B',
47 | };
48 |
49 | export const gray = {
50 | 50: '#FBFCFE',
51 | 100: '#EAF0F5',
52 | 200: '#D6E2EB',
53 | 300: '#BFCCD9',
54 | 400: '#94A6B8',
55 | 500: '#5B6B7C',
56 | 600: '#4C5967',
57 | 700: '#364049',
58 | 800: '#131B20',
59 | 900: '#090E10',
60 | };
61 |
62 | export const green = {
63 | 50: '#F6FEF6',
64 | 100: '#E3FBE3',
65 | 200: '#C7F7C7',
66 | 300: '#A1E8A1',
67 | 400: '#51BC51',
68 | 500: '#1F7A1F',
69 | 600: '#136C13',
70 | 700: '#0A470A',
71 | 800: '#042F04',
72 | 900: '#021D02',
73 | };
74 |
75 | const getDesignTokens = (mode: PaletteMode) => ({
76 | palette: {
77 | mode,
78 | primary: {
79 | light: brand[200],
80 | main: brand[500],
81 | dark: brand[800],
82 | contrastText: brand[50],
83 | ...(mode === 'dark' && {
84 | contrastText: brand[100],
85 | light: brand[300],
86 | main: brand[400],
87 | dark: brand[800],
88 | }),
89 | },
90 | secondary: {
91 | light: secondary[300],
92 | main: secondary[500],
93 | dark: secondary[800],
94 | ...(mode === 'dark' && {
95 | light: secondary[400],
96 | main: secondary[500],
97 | dark: secondary[900],
98 | }),
99 | },
100 | warning: {
101 | main: '#F7B538',
102 | dark: '#F79F00',
103 | ...(mode === 'dark' && { main: '#F7B538', dark: '#F79F00' }),
104 | },
105 | error: {
106 | light: red[50],
107 | main: red[500],
108 | dark: red[700],
109 | ...(mode === 'dark' && { light: '#D32F2F', main: '#D32F2F', dark: '#B22A2A' }),
110 | },
111 | success: {
112 | light: green[300],
113 | main: green[400],
114 | dark: green[800],
115 | ...(mode === 'dark' && {
116 | light: green[400],
117 | main: green[500],
118 | dark: green[700],
119 | }),
120 | },
121 | grey: {
122 | 50: gray[50],
123 | 100: gray[100],
124 | 200: gray[200],
125 | 300: gray[300],
126 | 400: gray[400],
127 | 500: gray[500],
128 | 600: gray[600],
129 | 700: gray[700],
130 | 800: gray[800],
131 | 900: gray[900],
132 | },
133 | divider: mode === 'dark' ? alpha(gray[600], 0.3) : alpha(gray[300], 0.5),
134 | background: {
135 | default: '#fff',
136 | paper: gray[50],
137 | ...(mode === 'dark' && { default: gray[900], paper: gray[800] }),
138 | },
139 | text: {
140 | primary: gray[800],
141 | secondary: gray[600],
142 | ...(mode === 'dark' && { primary: '#fff', secondary: gray[400] }),
143 | },
144 | action: {
145 | selected: `${alpha(brand[200], 0.2)}`,
146 | ...(mode === 'dark' && {
147 | selected: alpha(brand[800], 0.2),
148 | }),
149 | },
150 | },
151 | typography: {
152 | fontFamily: ['"Inter", "sans-serif"'].join(','),
153 | h1: {
154 | fontSize: 60,
155 | fontWeight: 600,
156 | lineHeight: 78 / 70,
157 | letterSpacing: -0.2,
158 | },
159 | h2: {
160 | fontSize: 48,
161 | fontWeight: 600,
162 | lineHeight: 1.2,
163 | },
164 | h3: {
165 | fontSize: 42,
166 | lineHeight: 1.2,
167 | },
168 | h4: {
169 | fontSize: 36,
170 | fontWeight: 500,
171 | lineHeight: 1.5,
172 | },
173 | h5: {
174 | fontSize: 20,
175 | fontWeight: 600,
176 | },
177 | h6: {
178 | fontSize: 18,
179 | },
180 | subtitle1: {
181 | fontSize: 18,
182 | },
183 | subtitle2: {
184 | fontSize: 16,
185 | },
186 | body1: {
187 | fontWeight: 400,
188 | fontSize: 15,
189 | },
190 | body2: {
191 | fontWeight: 400,
192 | fontSize: 14,
193 | },
194 | caption: {
195 | fontWeight: 400,
196 | fontSize: 12,
197 | },
198 | },
199 | });
200 |
201 | export default function getLPTheme(mode: PaletteMode): ThemeOptions {
202 | return {
203 | ...getDesignTokens(mode),
204 | components: {
205 | MuiAccordion: {
206 | defaultProps: {
207 | elevation: 0,
208 | disableGutters: true,
209 | },
210 | styleOverrides: {
211 | root: ({ theme }) => ({
212 | padding: 8,
213 | overflow: 'clip',
214 | backgroundColor: '#fff',
215 | border: '1px solid',
216 | borderColor: gray[100],
217 | ':before': {
218 | backgroundColor: 'transparent',
219 | },
220 | '&:first-of-type': {
221 | borderTopLeftRadius: 10,
222 | borderTopRightRadius: 10,
223 | },
224 | '&:last-of-type': {
225 | borderBottomLeftRadius: 10,
226 | borderBottomRightRadius: 10,
227 | },
228 | ...(theme.palette.mode === 'dark' && {
229 | backgroundColor: gray[900],
230 | borderColor: gray[800],
231 | }),
232 | }),
233 | },
234 | },
235 | MuiAccordionSummary: {
236 | styleOverrides: {
237 | root: ({ theme }) => ({
238 | border: 'none',
239 | borderRadius: 8,
240 | '&:hover': { backgroundColor: gray[100] },
241 | ...(theme.palette.mode === 'dark' && {
242 | '&:hover': { backgroundColor: gray[800] },
243 | }),
244 | }),
245 | },
246 | },
247 | MuiAccordionDetails: {
248 | styleOverrides: {
249 | root: { mb: 20, border: 'none' },
250 | },
251 | },
252 | MuiToggleButtonGroup: {
253 | styleOverrides: {
254 | root: ({ theme }) => ({
255 | borderRadius: '10px',
256 | boxShadow: `0 4px 16px ${alpha(gray[400], 0.2)}`,
257 | '& .Mui-selected': {
258 | color: brand[500],
259 | },
260 | ...(theme.palette.mode === 'dark' && {
261 | '& .Mui-selected': {
262 | color: '#fff',
263 | },
264 | boxShadow: `0 4px 16px ${alpha(brand[700], 0.5)}`,
265 | }),
266 | }),
267 | },
268 | },
269 | MuiToggleButton: {
270 | styleOverrides: {
271 | root: ({ theme }) => ({
272 | padding: '12px 16px',
273 | textTransform: 'none',
274 | borderRadius: '10px',
275 | fontWeight: 500,
276 | ...(theme.palette.mode === 'dark' && {
277 | color: gray[400],
278 | boxShadow: '0 4px 16px rgba(0, 0, 0, 0.5)',
279 | '&.Mui-selected': { color: brand[300] },
280 | }),
281 | }),
282 | },
283 | },
284 | MuiButtonBase: {
285 | defaultProps: {
286 | disableTouchRipple: true,
287 | disableRipple: true,
288 | },
289 | styleOverrides: {
290 | root: {
291 | boxSizing: 'border-box',
292 | transition: 'all 100ms ease-in',
293 | '&:focus-visible': {
294 | outline: `3px solid ${alpha(brand[500], 0.5)}`,
295 | outlineOffset: '2px',
296 | },
297 | },
298 | },
299 | },
300 | MuiButton: {
301 | styleOverrides: {
302 | root: ({ theme, ownerState }) => ({
303 | boxSizing: 'border-box',
304 | boxShadow: 'none',
305 | borderRadius: '10px',
306 | textTransform: 'none',
307 | '&:active': {
308 | transform: 'scale(0.98)',
309 | },
310 | ...(ownerState.size === 'small' && {
311 | maxHeight: '32px',
312 | }),
313 | ...(ownerState.size === 'medium' && {
314 | height: '40px',
315 | }),
316 | ...(ownerState.variant === 'contained' &&
317 | ownerState.color === 'primary' && {
318 | color: brand[50],
319 | background: brand[500],
320 | backgroundImage: `linear-gradient(to bottom, ${brand[400]}, ${brand[600]})`,
321 | boxShadow: `inset 0 1px ${alpha(brand[300], 0.4)}`,
322 | outline: `1px solid ${brand[700]}`,
323 | '&:hover': {
324 | background: brand[400],
325 | backgroundImage: 'none',
326 | boxShadow: `0 0 0 1px ${alpha(brand[300], 0.5)}`,
327 | },
328 | }),
329 | ...(ownerState.variant === 'outlined' && {
330 | backgroundColor: alpha(brand[300], 0.1),
331 | borderColor: brand[300],
332 | color: brand[500],
333 | '&:hover': {
334 | backgroundColor: alpha(brand[300], 0.3),
335 | borderColor: brand[200],
336 | },
337 | }),
338 | ...(ownerState.variant === 'text' && {
339 | color: brand[500],
340 | '&:hover': {
341 | backgroundColor: alpha(brand[300], 0.3),
342 | borderColor: brand[200],
343 | },
344 | }),
345 | ...(theme.palette.mode === 'dark' && {
346 | ...(ownerState.variant === 'outlined' && {
347 | backgroundColor: alpha(brand[600], 0.1),
348 | borderColor: brand[700],
349 | color: brand[300],
350 | '&:hover': {
351 | backgroundColor: alpha(brand[600], 0.3),
352 | borderColor: brand[700],
353 | },
354 | }),
355 | ...(ownerState.variant === 'text' && {
356 | color: brand[300],
357 | '&:hover': {
358 | backgroundColor: alpha(brand[600], 0.3),
359 | borderColor: brand[700],
360 | },
361 | }),
362 | }),
363 | }),
364 | },
365 | },
366 | MuiCard: {
367 | styleOverrides: {
368 | root: ({ theme, ownerState }) => ({
369 | backgroundColor: gray[50],
370 | borderRadius: 10,
371 | border: `1px solid ${alpha(gray[200], 0.8)}`,
372 | boxShadow: 'none',
373 | transition: 'background-color, border, 80ms ease',
374 | ...(ownerState.variant === 'outlined' && {
375 | background: `linear-gradient(to bottom, #FFF, ${gray[50]})`,
376 | '&:hover': {
377 | borderColor: brand[300],
378 | boxShadow: `0 0 24px ${brand[100]}`,
379 | },
380 | }),
381 | ...(theme.palette.mode === 'dark' && {
382 | backgroundColor: alpha(gray[800], 0.6),
383 | border: `1px solid ${alpha(gray[700], 0.3)}`,
384 | ...(ownerState.variant === 'outlined' && {
385 | background: `linear-gradient(to bottom, ${gray[900]}, ${alpha(
386 | gray[800],
387 | 0.5,
388 | )})`,
389 | '&:hover': {
390 | borderColor: brand[700],
391 | boxShadow: `0 0 24px ${brand[800]}`,
392 | },
393 | }),
394 | }),
395 | }),
396 | },
397 | },
398 | MuiChip: {
399 | styleOverrides: {
400 | root: ({ theme }) => ({
401 | alignSelf: 'center',
402 | py: 1.5,
403 | px: 0.5,
404 | background: `linear-gradient(to bottom right, ${brand[50]}, ${brand[100]})`,
405 | border: '1px solid',
406 | borderColor: `${alpha(brand[500], 0.3)}`,
407 | fontWeight: '600',
408 | '&:hover': {
409 | backgroundColor: brand[500],
410 | },
411 | '&:focus-visible': {
412 | borderColor: brand[800],
413 | backgroundColor: brand[200],
414 | },
415 | '& .MuiChip-label': {
416 | color: brand[500],
417 | },
418 | '& .MuiChip-icon': {
419 | color: brand[500],
420 | },
421 | ...(theme.palette.mode === 'dark' && {
422 | background: `linear-gradient(to bottom right, ${brand[700]}, ${brand[900]})`,
423 | borderColor: `${alpha(brand[500], 0.5)}`,
424 | '&:hover': {
425 | backgroundColor: brand[600],
426 | },
427 | '&:focus-visible': {
428 | borderColor: brand[200],
429 | backgroundColor: brand[600],
430 | },
431 | '& .MuiChip-label': {
432 | color: brand[200],
433 | },
434 | '& .MuiChip-icon': {
435 | color: brand[200],
436 | },
437 | }),
438 | }),
439 | },
440 | },
441 | MuiDivider: {
442 | styleOverrides: {
443 | root: ({ theme }) => ({
444 | borderColor: `${alpha(gray[200], 0.8)}`,
445 | ...(theme.palette.mode === 'dark' && {
446 | borderColor: `${alpha(gray[700], 0.4)}`,
447 | }),
448 | }),
449 | },
450 | },
451 | MuiLink: {
452 | defaultProps: {
453 | underline: 'none',
454 | },
455 | styleOverrides: {
456 | root: ({ theme }) => ({
457 | color: brand[600],
458 | fontWeight: 500,
459 | position: 'relative',
460 | textDecoration: 'none',
461 | '&::before': {
462 | content: '""',
463 | position: 'absolute',
464 | width: 0,
465 | height: '1px',
466 | bottom: 0,
467 | left: 0,
468 | backgroundColor: brand[200],
469 | opacity: 0.7,
470 | transition: 'width 0.3s ease, opacity 0.3s ease',
471 | },
472 | '&:hover::before': {
473 | width: '100%',
474 | opacity: 1,
475 | },
476 | ...(theme.palette.mode === 'dark' && {
477 | color: brand[200],
478 | }),
479 | }),
480 | },
481 | },
482 | MuiMenuItem: {
483 | styleOverrides: {
484 | root: ({ theme }) => ({
485 | borderRadius: '99px',
486 | color: gray[500],
487 | fontWeight: 500,
488 | ...(theme.palette.mode === 'dark' && {
489 | color: gray[300],
490 | }),
491 | }),
492 | },
493 | },
494 | MuiPaper: {
495 | styleOverrides: {
496 | root: ({ theme }) => ({
497 | backgroundImage: 'none',
498 | backgroundColor: gray[100],
499 | ...(theme.palette.mode === 'dark' && {
500 | backgroundColor: alpha(gray[900], 0.6),
501 | }),
502 | }),
503 | },
504 | },
505 | MuiSwitch: {
506 | styleOverrides: {
507 | root: ({ theme }) => ({
508 | boxSizing: 'border-box',
509 | width: 36,
510 | height: 24,
511 | padding: 0,
512 | transition: 'background-color 100ms ease-in',
513 | '&:hover': {
514 | '& .MuiSwitch-track': {
515 | backgroundColor: brand[600],
516 | },
517 | },
518 | '& .MuiSwitch-switchBase': {
519 | '&.Mui-checked': {
520 | transform: 'translateX(13px)',
521 | },
522 | },
523 | '& .MuiSwitch-track': {
524 | borderRadius: 50,
525 | },
526 | '& .MuiSwitch-thumb': {
527 | boxShadow: '0 0 2px 2px rgba(0, 0, 0, 0.2)',
528 | backgroundColor: '#FFF',
529 | width: 16,
530 | height: 16,
531 | margin: 2,
532 | },
533 | ...(theme.palette.mode === 'dark' && {
534 | width: 36,
535 | height: 24,
536 | padding: 0,
537 | transition: 'background-color 100ms ease-in',
538 | '&:hover': {
539 | '& .MuiSwitch-track': {
540 | backgroundColor: brand[600],
541 | },
542 | },
543 | '& .MuiSwitch-switchBase': {
544 | '&.Mui-checked': {
545 | transform: 'translateX(13px)',
546 | },
547 | },
548 | '& .MuiSwitch-thumb': {
549 | boxShadow: '0 0 2px 2px rgba(0, 0, 0, 0.2)',
550 | backgroundColor: '#FFF',
551 | width: 16,
552 | height: 16,
553 | margin: 2,
554 | },
555 | }),
556 | }),
557 | switchBase: {
558 | height: 24,
559 | width: 24,
560 | padding: 0,
561 | color: '#fff',
562 | '&.Mui-checked + .MuiSwitch-track': {
563 | opacity: 1,
564 | },
565 | },
566 | },
567 | },
568 | MuiTextField: {
569 | styleOverrides: {
570 | root: ({ theme }) => ({
571 | '& label .Mui-focused': {
572 | color: 'white',
573 | },
574 | '& .MuiInputBase-input': {
575 | boxSizing: 'border-box',
576 | '&::placeholder': {
577 | opacity: 0.7,
578 | },
579 | },
580 | '& .MuiOutlinedInput-root': {
581 | boxSizing: 'border-box',
582 | minWidth: 280,
583 | minHeight: 40,
584 | height: '100%',
585 | borderRadius: '10px',
586 | border: '1px solid',
587 | borderColor: gray[200],
588 | transition: 'border-color 120ms ease-in',
589 | '& fieldset': {
590 | border: 'none',
591 | boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)',
592 | background: `${alpha('#FFF', 0.3)}`,
593 | },
594 | '&:hover': {
595 | borderColor: brand[300],
596 | },
597 | '&.Mui-focused': {
598 | borderColor: brand[400],
599 | outline: '4px solid',
600 | outlineColor: brand[200],
601 | },
602 | },
603 | ...(theme.palette.mode === 'dark' && {
604 | '& .MuiOutlinedInput-root': {
605 | boxSizing: 'border-box',
606 | minWidth: 280,
607 | minHeight: 40,
608 | height: '100%',
609 | borderRadius: '10px',
610 | border: '1px solid',
611 | borderColor: gray[600],
612 | transition: 'border-color 120ms ease-in',
613 | '& fieldset': {
614 | border: 'none',
615 | boxShadow: ' 0px 2px 4px rgba(0, 0, 0, 0.4)',
616 | background: `${alpha(gray[800], 0.4)}`,
617 | },
618 | '&:hover': {
619 | borderColor: brand[300],
620 | },
621 | '&.Mui-focused': {
622 | borderColor: brand[400],
623 | outline: '4px solid',
624 | outlineColor: alpha(brand[500], 0.5),
625 | },
626 | },
627 | }),
628 | }),
629 | },
630 | },
631 | },
632 | };
633 | }
--------------------------------------------------------------------------------
/src/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | RediWatch
6 |
7 |
8 |
9 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/custom.d.ts:
--------------------------------------------------------------------------------
1 | // custom.d.ts
2 | declare module '*.mp4' {
3 | const src: string;
4 | export default src;
5 | }
6 |
7 | declare module '*.png' {
8 | const src: string;
9 | export default src;
10 | }
11 |
--------------------------------------------------------------------------------
/src/server/controllers/userController.ts:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const bcrypt = require('bcryptjs');
3 | const client = require('../models/queryModels');
4 | import { Request, Response } from 'express';
5 |
6 | const signupUser = async (req: Request, res: Response) => {
7 | console.log('request to signup user', req.body);
8 | try {
9 | const { firstName, lastName, email, password } = req.body;
10 | const firstName1 = String(firstName);
11 | const lastName1 = String(lastName);
12 | const email1 = String(email);
13 | const password1 = String(password);
14 | // check that all fields have been provided
15 | if (!firstName1 || !lastName1 || !email1 || !password1) {
16 | res.status(400).json({ error: 'Please add all required fields' });
17 | return;
18 | }
19 |
20 | // check if user already exists
21 | const text = `SELECT * FROM USERS WHERE EMAIL= $1`;
22 | const params = [email1];
23 | const result = await client.query(text, params);
24 | // const userExists = result.rows[0];
25 | // console.log(userExists);
26 | if (result.rows.length !== 0) {
27 | res.status(400).json({ error: 'User already exists' });
28 | return;
29 | }
30 |
31 | // hash password using bcrypt
32 | const salt = await bcrypt.genSalt(10);
33 | const hashedPassword = await bcrypt.hash(password1, salt);
34 |
35 | // create user
36 | // console.log(`generated hashedpassword is ${hashedPassword}`)
37 | const textInsert = `INSERT INTO users (firstname, lastname, email, password)
38 | VALUES ($1,$2,$3,$4)
39 | RETURNING *`;
40 | const paramsInsert = [firstName1, lastName1, email1, hashedPassword];
41 |
42 | const resultInsert = await client.query(textInsert, paramsInsert);
43 | if (resultInsert.rowCount !== 1) {
44 | throw 'Insert new user failed!';
45 | }
46 |
47 | const user = resultInsert.rows[0];
48 | if (user) {
49 | res.status(201).json({
50 | _id: user.id,
51 | firstName: user.firstName,
52 | lastName: user.lastName,
53 | email: user.email,
54 | token: generateToken(user._id),
55 | });
56 | } else {
57 | res.status(400).json({ error: 'Invalid user data' });
58 | }
59 | } catch (error) {
60 | console.error(error);
61 | res.status(500).json({ error: 'Internal server error' });
62 | }
63 | };
64 |
65 | const signinUser = async (req: Request, res: Response) => {
66 | console.log('request to signin user', req.body);
67 | const { email, password } = req.body;
68 |
69 | try {
70 | const text = `SELECT * FROM USERS WHERE EMAIL= $1`;
71 | const params = [email];
72 | console.log('in signin user try block');
73 | const result = await client.query(text, params);
74 | console.log('RESULT:', result);
75 | // console.log(result)
76 | console.log(result.rowCount);
77 | if (result.rowCount === 0) {
78 | return res.status(400).json({ error: 'User not found' });
79 | }
80 | const user = result.rows[0];
81 | // console.log(user.password)
82 | const isValid = await bcrypt.compare(password, user.password);
83 | if (!isValid) {
84 | return res.status(400).json({ error: 'Invalid credentials' });
85 | }
86 |
87 | return res.status(200).json({
88 | _id: user._id,
89 | firstName: user.firstname,
90 | lastName: user.lastname,
91 | email: user.email,
92 | cache_id: user.cache_id,
93 | config_id: user.config_id,
94 | token: generateToken(user._id),
95 | });
96 | } catch (error) {
97 | console.error(error);
98 | res.status(500).json({ error: 'signInUser Error' });
99 | }
100 | };
101 |
102 | const generateToken = (id: number) => {
103 | return jwt.sign({ id }, 'abc123', { expiresIn: '30' });
104 | };
105 |
106 | module.exports = { signupUser, signinUser };
107 |
--------------------------------------------------------------------------------
/src/server/models/queryModels.ts:
--------------------------------------------------------------------------------
1 | import { Pool } from 'pg';
2 |
3 | const PG_URI = process.env.DATABASE_URL;
4 |
5 | // Create a new pool using the connection string
6 | const pool = new Pool({
7 | connectionString: PG_URI
8 | });
9 |
10 | // We export an object that contains a property called query,
11 | // which is a function that returns the invocation of pool.query() after logging the query
12 | module.exports = {
13 | connect: () => pool.connect(),
14 | query: (text: string, params: (string | number)[]) => {
15 | console.log('executed query', text, params);
16 | return pool.query(text, params);
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/src/server/routes/userRoutes.ts:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const { signupUser, signinUser } = require('../controllers/userController');
4 |
5 | router.post('/signup', signupUser);
6 | router.post('/signin', signinUser);
7 |
8 | module.exports = router;
--------------------------------------------------------------------------------
/src/server/server.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response } from "express";
2 | import { Client } from 'pg';
3 | import { performance } from "perf_hooks";
4 | import Redis, { Redis as RedisClientType } from "ioredis";
5 |
6 | const app = express();
7 | const PORT = 3001;
8 |
9 | import "dotenv/config";
10 | require("dotenv").config();
11 |
12 | //import model
13 | const db = require('./models/queryModels');
14 |
15 | app.use(express.json());
16 | app.use("/api/users", require("./routes/userRoutes"));
17 |
18 |
19 | // Connect to the database
20 | db.connect()
21 | .then(() => {
22 | console.log('Connected to the database');
23 | })
24 | .catch((err: Error) => {
25 | console.error('Connection error', err.stack);
26 | });
27 |
28 |
29 |
30 | interface PerformanceMetrics {
31 | duration: number;
32 | hits: number;
33 | misses: number;
34 | }
35 |
36 | const redisClient: RedisClientType = new Redis({
37 | host: "redis",
38 | port: 6379,
39 | });
40 |
41 | redisClient.on("error", (err) => console.error("Redis Client Error", err));
42 | redisClient.on("connect", () => console.log("Redis client connected"));
43 |
44 | app.post("/test", async (req: Request, res: Response) => {
45 | try {
46 | // Connect to Redis
47 | await redisClient.ping();
48 | console.log("connected to Redis");
49 |
50 | // Set maxmemory to 30mb
51 | await redisClient.config("SET", "maxmemory", "30mb");
52 | console.log("maxmemory set to 30mb");
53 |
54 | // Fetch maxmemory configuration
55 | const configResult = (await redisClient.config(
56 | "GET",
57 | "maxmemory"
58 | )) as Array;
59 | console.log("Config Result: ", configResult);
60 |
61 | // Test cache performance
62 | const performanceMetrics = await testCachePerformance(redisClient);
63 | res.json(performanceMetrics);
64 | } catch (error) {
65 | console.error("Error during Redis operations: ", error);
66 | res.status(500).send("Error during Redis operations");
67 | }
68 | });
69 |
70 | app.get("/api/test", async (req: Request, res: Response) => {
71 | res.status(200).json("test");
72 | });
73 |
74 | async function testCachePerformance(
75 | redisClient: RedisClientType
76 | ): Promise {
77 | const sampleData = Array.from({ length: 10000 }, (_, i) => `key${i}`);
78 | const ttl = 60 * 60; // 1 hour
79 |
80 | const start = performance.now();
81 |
82 | // Setting keys
83 | for (const key of sampleData) {
84 | await redisClient.set(key, `value${key}`, "EX", ttl);
85 | }
86 |
87 | // Retrieve keys
88 | let hits = 0;
89 | let misses = 0;
90 | for (let i = 0; i < 20000; i++) {
91 | const key = `key${Math.floor(Math.random() * 10000)}`;
92 | const result = await redisClient.get(key);
93 | if (result !== null) {
94 | hits++;
95 | } else {
96 | misses++;
97 | }
98 | }
99 |
100 | const end = performance.now();
101 | const duration = (end - start) / 1000; // converting to seconds
102 |
103 | console.log("Duration: ", duration);
104 | console.log("hits: ", hits);
105 | console.log("misses: ", misses);
106 |
107 | // await redisClient.quit();
108 | return { duration, hits, misses };
109 | }
110 |
111 | interface NewConnectionRequestBody {
112 | connectionnickname: string;
113 | connectionstring: string;
114 | user_id: number;
115 | config_id: number | null;
116 | }
117 |
118 | // Adds a connection string , config_id and nickname to existing user from homepage form input
119 | app.post("/api/add-connection", async (req: Request, res: Response) => {
120 | const {
121 | connectionnickname,
122 | connectionstring,
123 | user_id,
124 | config_id,
125 | }: NewConnectionRequestBody = req.body;
126 |
127 | try {
128 | const result = await db.query(
129 | "INSERT INTO public.caches (connectionnickname, connectionstring, user_id, config_id) VALUES ($1, $2, $3, $4) RETURNING *",
130 | [connectionnickname, connectionstring, user_id, config_id]
131 | );
132 | res.status(200).json(result.rows[0]);
133 | } catch (err) {
134 | console.error("Error inserting connection data:", err);
135 | res.status(500).json({ error: "Failed to add connection" });
136 | }
137 | });
138 |
139 | app.listen(PORT, '0.0.0.0', () => {
140 | console.log(`Server is running on port ${PORT}`);
141 | });
142 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "module": "commonjs",
5 | "outDir": "./dist",
6 | "rootDir": "./src",
7 | "strict": true,
8 | "noImplicitAny": true,
9 | "sourceMap": true,
10 | "esModuleInterop": true,
11 | "moduleResolution": "node",
12 | "jsx": "react",
13 | },
14 | "include": ["src/**/*", "elephantsql.js"],
15 | "exclude": ["node_modules"]
16 | }
--------------------------------------------------------------------------------
/user_database.sql:
--------------------------------------------------------------------------------
1 | SET statement_timeout = 0;
2 | SET lock_timeout = 0;
3 | SET idle_in_transaction_session_timeout = 0;
4 | SET client_encoding = 'UTF8';
5 | SET standard_conforming_strings = on;
6 | SELECT pg_catalog.set_config('search_path', '', false);
7 | SET check_function_bodies = false;
8 | SET xmloption = content;
9 | SET client_min_messages = warning;
10 | SET row_security = off;
11 |
12 |
13 | CREATE TABLE public.users (
14 | "_id" serial NOT NULL,
15 | "email" varchar NOT NULL,
16 | "password" varchar, NOT NULL,
17 | "firstname" varchar NOT NULL,
18 | "lastname" varchar NOT NULL,
19 | "cache_id" bigint,
20 | "config_id" bigint,
21 | CONSTRAINT "users_pk" PRIMARY KEY ("_id")
22 | ) WITH (
23 | OIDS=FALSE
24 | );
25 |
26 |
27 |
28 | CREATE TABLE public.caches (
29 | "_id" serial NOT NULL,
30 | "connectionstring" varchar NOT NULL,
31 | "connectionnickname" varchar NOT NULL,
32 | "user_id" bigint,
33 | "config_id" bigint,
34 | CONSTRAINT "caches_pk" PRIMARY KEY ("_id")
35 | ) WITH (
36 | OIDS=FALSE
37 | );
38 |
39 |
40 |
41 | CREATE TABLE public.configurations (
42 | "_id" serial NOT NULL,
43 | "ttl" bigint,
44 | "method" varchar,
45 | "memory" bigint,
46 | CONSTRAINT "configurations_pk" PRIMARY KEY ("_id")
47 | ) WITH (
48 | OIDS=FALSE
49 | );
50 |
51 |
52 |
53 | ALTER TABLE public.users ADD CONSTRAINT "users_fk0" FOREIGN KEY ("cache_id") REFERENCES public.caches("_id");
54 | ALTER TABLE public.users ADD CONSTRAINT "users_fk1" FOREIGN KEY ("config_id") REFERENCES public.configurations("_id");
55 |
56 | ALTER TABLE public.caches ADD CONSTRAINT "caches_fk0" FOREIGN KEY ("user_id") REFERENCES public.users("_id");
57 | ALTER TABLE public.caches ADD CONSTRAINT "caches_fk1" FOREIGN KEY ("config_id") REFERENCES public.configurations("_id");
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin');
3 | const CopyPlugin = require('copy-webpack-plugin');
4 |
5 | module.exports = {
6 | mode: 'development',
7 | entry: './src/client/App.tsx',
8 | output: {
9 | path: path.resolve(__dirname, 'dist'),
10 | filename: 'bundle.js',
11 | // publicPath: "/",
12 | },
13 | module: {
14 | rules: [
15 | {
16 | test: /\.(ts|tsx)?$/,
17 | exclude: /node_modules/,
18 | use: 'ts-loader',
19 | },
20 | {
21 | test: /\.(png|jpg|gif|svg)$/,
22 | use: [
23 | {
24 | loader: 'file-loader',
25 | options: {
26 | name: '[name].[ext]',
27 | outputPath: 'images/',
28 | },
29 | },
30 | ],
31 | },
32 | {
33 | test: /\.(mp4|webm|ogg)$/,
34 | use: [
35 | {
36 | loader: 'file-loader',
37 | options: {
38 | name: '[name].[ext]',
39 | outputPath: 'videos/',
40 | },
41 | },
42 | ],
43 | },
44 | ],
45 | },
46 | resolve: {
47 | extensions: ['.tsx', '.ts', '.js'],
48 | },
49 | plugins: [
50 | new HtmlWebpackPlugin({
51 | template: './src/client/index.html',
52 | filename: './index.html',
53 | }),
54 | // new CopyPlugin({
55 | // patterns: [{ from: './src/client/style.css' }],
56 | // }),
57 | ],
58 | devServer: {
59 | // Required for Docker to work with dev server
60 | host: '0.0.0.0',
61 | //host: localhost,
62 | port: 8080,
63 | //enable HMR on the devServer
64 | hot: true,
65 | // fallback to root for other urls
66 | historyApiFallback: true,
67 | static: {
68 | directory: path.join(__dirname, './dist'),
69 | // publicPath: "/",
70 | },
71 | proxy: {
72 | // '/api': 'http://localhost:3001',
73 | // secure: false,
74 | '/api': {
75 | target: 'http://server:3001',
76 | changeOrigin: true,
77 | secure: false,
78 | },
79 | },
80 | historyApiFallback: true,
81 | },
82 | };
83 |
--------------------------------------------------------------------------------