From c9390f10e86ce1549af8c88cc14aa3b6c2c8b045 Mon Sep 17 00:00:00 2001 From: Bryan Date: Fri, 3 Apr 2026 17:48:51 -0400 Subject: [PATCH 01/21] feat: add Redis service to docker-compose --- docker-compose.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index ec63c53..f494f48 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -61,12 +61,14 @@ services: # This section defines all the services (containers) we want to run - CORS_ORIGIN=http://localhost:5173 - JWT_SECRET=${JWT_SECRET} - SUPABASE_URI=${SUPABASE_URI} + - REDIS_URL=redis://redis:6379 - NODE_ENV=development - OTEL_EXPORTER_OTLP_ENDPOINT=http://grafana-alloy:14318/v1/traces # OTLP HTTP endpoint for Grafana Alloy - OTEL_SERVICE_NAME=queryhawk-backend - OTEL_SERVICE_VERSION=1.0.0 depends_on: - jaeger + - redis networks: - queryhawk_monitoring_network deploy: @@ -74,6 +76,13 @@ services: # This section defines all the services (containers) we want to run limits: memory: 768M + redis: + image: redis:7.2-alpine + ports: + - '6379:6379' + networks: + - queryhawk_monitoring_network + jaeger: # This is the name we're giving to our service image: jaegertracing/all-in-one:latest #ports section maps ports from container to computer From 3d349cd491b64e7dd65d179bf1fc19f7e19872d3 Mon Sep 17 00:00:00 2001 From: Bryan Date: Fri, 3 Apr 2026 17:50:18 -0400 Subject: [PATCH 02/21] feat: add ioredis dependency --- package-lock.json | 138 ++++++++++++++++++++++++++++++++++++++-------- package.json | 1 + 2 files changed, 117 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index e173a9c..14ca48e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "dockerode": "^4.0.4", "dotenv": "^16.4.7", "express": "^4.21.2", + "ioredis": "^5.10.1", "jsonwebtoken": "^9.0.2", "lucide-react": "^0.474.0", "pg": "^8.13.3", @@ -106,7 +107,6 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -465,6 +465,34 @@ "node": ">=v18" } }, + "node_modules/@commitlint/load/node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@commitlint/load/node_modules/cosmiconfig-typescript-loader": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.2.0.tgz", @@ -611,7 +639,6 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -655,7 +682,6 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1394,6 +1420,12 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1480,7 +1512,6 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.18.0.tgz", "integrity": "sha512-bbH/HaJZpFtXGvWg3TsBWG4eyt3gah3E7nCNU8GLyRjVoWcA91Vm/T+sjHfUcwgJSw9iLtucfHBoq+qW/T30aA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.23.9", "@mui/core-downloads-tracker": "^5.18.0", @@ -1708,7 +1739,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -4090,7 +4120,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.0.tgz", "integrity": "sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4157,7 +4186,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -4169,7 +4197,6 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -4267,7 +4294,6 @@ "integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/types": "8.41.0", @@ -4605,7 +4631,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4966,7 +4991,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", @@ -5288,6 +5312,15 @@ "node": ">=6" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -5817,6 +5850,15 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -6135,7 +6177,6 @@ "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -7319,6 +7360,30 @@ "node": ">=12" } }, + "node_modules/ioredis": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -7682,12 +7747,24 @@ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -8401,7 +8478,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -8742,7 +8818,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -8755,7 +8830,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -8992,6 +9066,27 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -9507,6 +9602,12 @@ "dev": true, "license": "MIT" }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -9719,7 +9820,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -9886,7 +9986,6 @@ "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -9952,7 +10051,6 @@ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10173,7 +10271,6 @@ "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -10780,7 +10877,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11317,7 +11413,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -11495,7 +11590,6 @@ "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", - "peer": true, "bin": { "yaml": "bin.mjs" }, diff --git a/package.json b/package.json index 0e43a3f..eab2efe 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "dockerode": "^4.0.4", "dotenv": "^16.4.7", "express": "^4.21.2", + "ioredis": "^5.10.1", "jsonwebtoken": "^9.0.2", "lucide-react": "^0.474.0", "pg": "^8.13.3", From 39d663904782ec4f77087a5c4c321a38d9912138 Mon Sep 17 00:00:00 2001 From: Bryan Date: Fri, 3 Apr 2026 17:55:42 -0400 Subject: [PATCH 03/21] feat: add Redis caching performance test feature Add backend controller and frontend dialog to benchmark Redis retrieval time against PostgreSQL execution time for a given query. - redisController: connects to user DB, runs query, caches result in Redis under key query:, and measures retrieval time via performance.now() - RedisTestDialog: displays a bar chart comparing pgExecutionTime vs redisRetrievalTime using Recharts --- server/controllers/redisController.ts | 72 ++++++++++++++ .../QueryPerformance/RedisTestDialog.tsx | 99 +++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 server/controllers/redisController.ts create mode 100644 src/components/QueryPerformance/RedisTestDialog.tsx diff --git a/server/controllers/redisController.ts b/server/controllers/redisController.ts new file mode 100644 index 0000000..b9f16c0 --- /dev/null +++ b/server/controllers/redisController.ts @@ -0,0 +1,72 @@ +import { NextFunction, Request, RequestHandler, Response } from 'express'; +import { Client } from 'pg'; +import Redis from 'ioredis'; + +type RedisController = { + runRedisTest: RequestHandler; +}; + +const redisController: RedisController = { + runRedisTest: async (req: Request, res: Response, next: NextFunction) => { + const { uri_string, queryId } = req.body; + const queryText = res.locals.originalQuery; + + // users database + const client = new Client({ + connectionString: uri_string, + ssl: { + rejectUnauthorized: false, // Required for Supabase connections + }, + }); + + const redisClient = new Redis(`redis://redis:6379`); + + try { + await client.connect(); + const result = await client.query(queryText); + + await client.end(); + + // create key for cache + const key = `query:${queryId}`; + // use stringify since redis doesnt take objects + await redisClient.set(key, JSON.stringify(result.rows)); + + // measure retrieval time using performance.now() + const start = performance.now(); + + // retrieve value + const value = await redisClient.get(key); + + const end = performance.now(); + + const duration = end - start; + + if (value == null) { + return next({ + log: `redisControler: runRedisTest: Value is null`, + status: 500, + message: { + err: `Value from redisClient.get() is null`, + }, + }); + } + res.json({ + pgExecutionTime: res.locals.queryMetrics, + redisRetrievalTime: duration, + }); + } catch (err) { + return next({ + log: `redisControler: runRedisTest: Unexpected error ${err instanceof Error ? err.message : 'Unknown error'} `, + status: 500, + message: { + err: `Error has occured while executing stored query.`, + }, + }); + } finally { + redisClient.disconnect(); + } + }, +}; + +export default redisController; diff --git a/src/components/QueryPerformance/RedisTestDialog.tsx b/src/components/QueryPerformance/RedisTestDialog.tsx new file mode 100644 index 0000000..f4bf406 --- /dev/null +++ b/src/components/QueryPerformance/RedisTestDialog.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + CircularProgress, + Box, + Typography, + IconButton, +} from '@mui/material'; +import { BarChart, XAxis, YAxis, Bar, Cell } from 'recharts'; +import CloseIcon from '@mui/icons-material/Close'; + +export interface RedisTestResult { + pgExecutionTime: number; + redisRetrievalTime: number; +} + +interface RedisTestDialogProps { + open: boolean; + onClose: () => void; + redisMetrics: RedisTestResult | null; + redisLoading: boolean; +} + +function RedisChart({ redisMetrics }: { redisMetrics: RedisTestResult }) { + const { pgExecutionTime, redisRetrievalTime } = redisMetrics; + const data = [ + { name: 'PostgreSQL', value: pgExecutionTime }, + { name: 'Redis', value: redisRetrievalTime }, + ]; + + return ( + + + + Performance Metrics + + + PostgreSQL Execution Time: {pgExecutionTime.toFixed(2)} ms + + + Redis Retrieval Time: {redisRetrievalTime.toFixed(2)} ms + + + + + + `${value} ms`} + width={65} + stroke='#ffffff' + /> + + + + + + + ); +} + +const RedisTestDialog: React.FC = ({ + open, + onClose, + redisMetrics, + redisLoading, +}) => { + return ( + + + Redis vs. PostgreSQL Performance + + + + + + {redisLoading ? ( + + + + ) : redisMetrics ? ( + + + + ) : null} + + + ); +}; + +export default RedisTestDialog; From 7ba2c4955dafaaccb7c851cd1a78d80c707f4a59 Mon Sep 17 00:00:00 2001 From: Bryan Date: Fri, 3 Apr 2026 18:03:38 -0400 Subject: [PATCH 04/21] feat: add fetchOriginalQuery handler to userDatabaseController - Add fetchOriginalQuery to UserDatabaseController type - Fetches query text and execution time by queryId, stores on res.locals for use in downstream middleware --- server/controllers/userDatabaseController.ts | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/server/controllers/userDatabaseController.ts b/server/controllers/userDatabaseController.ts index aa6fc22..b3f5a2d 100644 --- a/server/controllers/userDatabaseController.ts +++ b/server/controllers/userDatabaseController.ts @@ -36,6 +36,7 @@ type UserDatabaseController = { analyzeQuery: RequestHandler; compareQueries: RequestHandler; getQueryHistory: RequestHandler; + fetchOriginalQuery: RequestHandler; }; // Consistent EXPLAIN wrapper for query analysis @@ -336,6 +337,48 @@ const userDatabaseController: UserDatabaseController = { ); } }, + // Fetch one specific query by queryID + fetchOriginalQuery: async ( + req: Request, + res: Response, + next: NextFunction, + ) => { + try { + const { queryId } = req.body; + if (!queryId) { + sendBadRequest(res, 'Query id is missing.'); + return; + } + const queryResult = await appDbPool.query( + ` + SELECT + q.query_text AS "queryText", + m.execution_time AS "executionTime" + FROM queries q + JOIN metrics m ON q.id = m.query_id + WHERE q.id = $1 + + `, + [queryId], + ); + + if (!queryResult.rows[0]) { + sendBadRequest(res, `Query not found`); + return; + } + res.locals.queryMetrics = queryResult.rows[0].executionTime; + res.locals.originalQuery = queryResult.rows[0].queryText; + return next(); + } catch (err) { + return next({ + log: `userDatabaseController: fetchOriginalQuery: Unexpected error ${err instanceof Error ? err.message : 'Unknown error'} `, + status: 500, + message: { + err: `Error has occured while fetching specific query results.`, + }, + }); + } + }, }; export default userDatabaseController; From 7bce9d92757b13027193847878c205300ab9f895 Mon Sep 17 00:00:00 2001 From: Bryan Date: Fri, 3 Apr 2026 18:05:03 -0400 Subject: [PATCH 05/21] feat: add /run-query/redis route to apiRoutes - Import redisController - Add POST /run-query/redis route with authenticateUser, fetchOriginalQuery, and runRedisTest middleware --- server/routes/apiRoutes.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/routes/apiRoutes.ts b/server/routes/apiRoutes.ts index 9762cb7..1288373 100644 --- a/server/routes/apiRoutes.ts +++ b/server/routes/apiRoutes.ts @@ -12,6 +12,7 @@ import { listActiveTargets, getTargetStatus, } from '../utils/alloyPostgresExporter'; +import redisController from '../controllers/redisController'; const router = express.Router(); @@ -73,6 +74,14 @@ router.get( userDatabaseController.getSavedQueries, ); +// Run query with redis +router.post( + '/run-query/redis', + authenticateUser, + userDatabaseController.fetchOriginalQuery, + redisController.runRedisTest, +); + // Add query analysis endpoints router.post( '/query/analyze', From b43fb5c9208bd4d506bc0166aceeb227c22eab7f Mon Sep 17 00:00:00 2001 From: Bryan Date: Fri, 3 Apr 2026 18:13:07 -0400 Subject: [PATCH 06/21] refactor: rename test query route and button label to query-tester --- src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.tsx b/src/App.tsx index facf5d3..86e0ee5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,7 +22,7 @@ function App() { } /> From 5d19e5d53f39f691df54626f2f86d84a2cb24296 Mon Sep 17 00:00:00 2001 From: Bryan Date: Fri, 3 Apr 2026 18:14:06 -0400 Subject: [PATCH 07/21] refactor: rename test query route and button label in index.tsx --- src/components/QueryMonitor/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/QueryMonitor/index.tsx b/src/components/QueryMonitor/index.tsx index 483a388..56e9ca9 100644 --- a/src/components/QueryMonitor/index.tsx +++ b/src/components/QueryMonitor/index.tsx @@ -203,13 +203,13 @@ const QueryMonitor: React.FC = () => { {/* Test Query Button */} From c408a85f3978c75436cebc096264bf4b3ca8434a Mon Sep 17 00:00:00 2001 From: Bryan Date: Fri, 3 Apr 2026 18:15:00 -0400 Subject: [PATCH 08/21] fix: use toFixed(2) for execution time display in MetricsTable --- src/components/QueryPerformance/MetricsTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/QueryPerformance/MetricsTable.tsx b/src/components/QueryPerformance/MetricsTable.tsx index 414f02f..97ef3e5 100644 --- a/src/components/QueryPerformance/MetricsTable.tsx +++ b/src/components/QueryPerformance/MetricsTable.tsx @@ -50,7 +50,7 @@ const MetricsTable: React.FC = ({ metrics }) => { Execution Time - {Math.floor(metrics.executionTime).toLocaleString()} ms + {metrics.executionTime.toFixed(2)} ms From d4bc4af49c3706e71a1523e01f560963bb327995 Mon Sep 17 00:00:00 2001 From: Bryan Date: Fri, 3 Apr 2026 18:16:47 -0400 Subject: [PATCH 09/21] refactor: rename ComparisonDialog to QueryComparisonDialog --- ...eryComparisonForm.tsx => QueryComparisonDialog.tsx} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename src/components/QueryPerformance/{QueryComparisonForm.tsx => QueryComparisonDialog.tsx} (90%) diff --git a/src/components/QueryPerformance/QueryComparisonForm.tsx b/src/components/QueryPerformance/QueryComparisonDialog.tsx similarity index 90% rename from src/components/QueryPerformance/QueryComparisonForm.tsx rename to src/components/QueryPerformance/QueryComparisonDialog.tsx index 4d6c1e7..9addb88 100644 --- a/src/components/QueryPerformance/QueryComparisonForm.tsx +++ b/src/components/QueryPerformance/QueryComparisonDialog.tsx @@ -11,9 +11,9 @@ import { Select, MenuItem, } from '@mui/material'; -import { SavedQuery } from './QueryHistory'; +import { SavedQuery } from './QueryHistoryDialog'; -interface ComparisonDialogProps { +interface QueryComparisonDialogProps { open: boolean; onClose: () => void; savedQueries: SavedQuery[]; @@ -25,7 +25,7 @@ interface ComparisonDialogProps { onCompare: () => void; } -const ComparisonDialog: React.FC = ({ +const QueryComparisonDialog: React.FC = ({ open, onClose, savedQueries, @@ -84,7 +84,7 @@ const ComparisonDialog: React.FC = ({ onClick={onCompare} variant='contained' sx={{ - textTransform: "none" + textTransform: 'none', }} disabled={ selectedQueries.first === null || selectedQueries.second === null @@ -97,4 +97,4 @@ const ComparisonDialog: React.FC = ({ ); }; -export default ComparisonDialog; +export default QueryComparisonDialog; From 024e9fde708af555750c5f4b9874c7f2a80e4d46 Mon Sep 17 00:00:00 2001 From: Bryan Date: Fri, 3 Apr 2026 18:21:25 -0400 Subject: [PATCH 10/21] refactor: refactor QueryComparisonView into QueryComparisonPage - Rename component and props from QueryComparisonView to QueryComparisonPage - Remove internal handleCompareQueries logic and useEffect, comparison now triggered via onOpenCompare prop - Remove LeanQueryAnalyzer section - Fix percentage calculations to use parseFloat with toFixed(2) - Update import to QueryHistoryDialog --- .../QueryPerformance/QueryComparisonPage.tsx | 88 +++++-------------- 1 file changed, 23 insertions(+), 65 deletions(-) diff --git a/src/components/QueryPerformance/QueryComparisonPage.tsx b/src/components/QueryPerformance/QueryComparisonPage.tsx index 256ad96..002e01c 100644 --- a/src/components/QueryPerformance/QueryComparisonPage.tsx +++ b/src/components/QueryPerformance/QueryComparisonPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { Box, Typography, @@ -8,60 +8,25 @@ import { CardContent, Paper, } from '@mui/material'; -import { SavedQuery } from './QueryHistory'; +import { SavedQuery } from './QueryHistoryDialog'; import MetricsTable from './MetricsTable'; import CompareArrowsIcon from '@mui/icons-material/CompareArrows'; -import LeanQueryAnalyzer from './LeanQueryAnalyzer'; -interface QueryComparisonViewProps { +interface QueryComparisonPageProps { firstQuery: SavedQuery | null; secondQuery: SavedQuery | null; onExitCompare: () => void; + onOpenCompare: () => void; } -const QueryComparisonView: React.FC = ({ +const QueryComparisonPage: React.FC = ({ firstQuery, secondQuery, onExitCompare, + onOpenCompare, }) => { - const [comparisonResults, setComparisonResults] = useState(null); - const [loading, setLoading] = useState(false); - if (!firstQuery || !secondQuery) return null; - // Function to compare queries using the enhanced API - const handleCompareQueries = async () => { - setLoading(true); - try { - const token = localStorage.getItem('authToken'); - const response = await fetch('http://localhost:4002/api/query/compare', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - query1: firstQuery.queryText, - query2: secondQuery.queryText, - }), - }); - - if (response.ok) { - const results = await response.json(); - setComparisonResults(results); - } - } catch (error) { - console.error('Comparison failed:', error); - } finally { - setLoading(false); - } - }; - - // Auto-compare when component mounts - useEffect(() => { - handleCompareQueries(); - }, [firstQuery, secondQuery]); - return ( = ({ Query Comparison - ))} From 79205f9c7072f9004667cb11c5d2503f297002a0 Mon Sep 17 00:00:00 2001 From: Bryan Date: Fri, 3 Apr 2026 18:24:34 -0400 Subject: [PATCH 12/21] feat: add tooltip to submit button in TestQueryForm - Add Tooltip around submit button showing validation message when fields are missing - Add width: 100% to button styles --- .../QueryPerformance/TestQueryForm.tsx | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/components/QueryPerformance/TestQueryForm.tsx b/src/components/QueryPerformance/TestQueryForm.tsx index 5b5fd17..6438a46 100644 --- a/src/components/QueryPerformance/TestQueryForm.tsx +++ b/src/components/QueryPerformance/TestQueryForm.tsx @@ -7,6 +7,7 @@ import { CircularProgress, TextField, Typography, + Tooltip, } from '@mui/material'; // Define the button styles @@ -16,6 +17,7 @@ const buttonStyles = { px: 4, borderRadius: 1.5, whiteSpace: 'nowrap', + width: '100%', }; // Define the input styles @@ -61,6 +63,7 @@ const TestQueryForm: React.FC = ({ fullWidth value={uri_string} onChange={(e) => onUriChange(e.target.value)} + // helperText={!uri_string ? 'Required for Redis Comparison' : ''} sx={inputStyles} /> = ({ value={query} onChange={(e) => onQueryChange(e.target.value)} /> - + + + + From c09c89e2c2bd712774824f58affcdc8aa8b15a6d Mon Sep 17 00:00:00 2001 From: Bryan Date: Fri, 3 Apr 2026 18:26:39 -0400 Subject: [PATCH 13/21] feat: integrate Redis test and refactor TestQueryPage - Add Redis state and handleRedisTest to call /run-query/redis endpoint - Add Run with Redis button in metrics card - Add queryId state, pass to handleLoadQuery for Redis test support - Replace QueryHistory/QueryComparisonForm with QueryHistoryDialog/QueryComparisonDialog - Add RedisTestDialog for displaying Redis vs PostgreSQL results - Remove LeanQueryAnalyzer and handleAnalyzeQuery logic --- .../QueryPerformance/TestQueryPage.tsx | 216 ++++++++++-------- 1 file changed, 121 insertions(+), 95 deletions(-) diff --git a/src/components/QueryPerformance/TestQueryPage.tsx b/src/components/QueryPerformance/TestQueryPage.tsx index 4ce1238..7ab040d 100644 --- a/src/components/QueryPerformance/TestQueryPage.tsx +++ b/src/components/QueryPerformance/TestQueryPage.tsx @@ -10,17 +10,18 @@ import { Alert, Button, CssBaseline, + Tooltip, } from '@mui/material'; import { useNavigate } from 'react-router-dom'; // Import custom components import Header from './Header'; // Nav bar on component on top import MetricsTable, { QueryMetrics } from './MetricsTable'; // component that has the query mertics -import QueryHistory, { SavedQuery } from './QueryHistory'; // component that you can view your past queries. -import QueryComparisonForm from './QueryComparisonForm'; +import QueryHistoryDialog, { SavedQuery } from './QueryHistoryDialog'; // component that you can view your past queries. +import QueryComparisonDialog from './QueryComparisonDialog'; import QueryComparisonPage from './QueryComparisonPage'; import TestQueryForm from './TestQueryForm'; -import LeanQueryAnalyzer from './LeanQueryAnalyzer'; +import RedisTestDialog, { RedisTestResult } from './RedisTestDialog'; // Import the same dark theme configuration as before const darkTheme = createTheme({ @@ -42,6 +43,7 @@ const darkTheme = createTheme({ const TestQueryPage: React.FC = () => { const navigate = useNavigate(); const [uri_string, setUri_string] = useState(''); + const [queryId, setQueryId] = useState(null); const [query, setQuery] = useState(''); const [queryName, setQueryName] = useState(''); const [loading, setLoading] = useState(false); @@ -49,10 +51,6 @@ const TestQueryPage: React.FC = () => { const [queryMetrics, setQueryMetrics] = useState(null); const [isAuthenticated, setIsAuthenticated] = useState(false); - // State for Lean Query Analyzer - const [analysis, setAnalysis] = useState(null); - const [insights, setInsights] = useState(null); - // State for saved queries and comparison const [savedQueries, setSavedQueries] = useState([]); const [showQueryHistory, setShowQueryHistory] = useState(false); @@ -68,6 +66,12 @@ const TestQueryPage: React.FC = () => { const [firstQuery, setFirstQuery] = useState(null); const [secondQuery, setSecondQuery] = useState(null); + // Redis state + const [redisDialogOpen, setRedisDialogOpen] = useState(false); + const [redisLoading, setRedisLoading] = useState(false); + const [redisMetrics, setRedisMetrics] = useState( + null, + ); // Create authentication check const checkAuthentication = () => { const token = localStorage.getItem('authToken'); @@ -165,9 +169,6 @@ const TestQueryPage: React.FC = () => { const data: QueryMetrics = await response.json(); setQueryMetrics(data); - // Analyze the query for enhanced insights - await handleAnalyzeQuery(query); - // Refresh the saved queries list after successful fetch await fetchSavedQueries(); } catch (err) { @@ -195,10 +196,16 @@ const TestQueryPage: React.FC = () => { // Function to handle loading a query from history // Takes in the string and metrics thats that have a set type for each metric. - const handleLoadQuery = (queryText: string, metrics: QueryMetrics) => { + const handleLoadQuery = ( + queryText: string, + metrics: QueryMetrics, + id: number, + ) => { + setQueryId(id); setQuery(queryText); setQueryMetrics(metrics); setShowQueryHistory(false); + setCompareMode(false); }; // Function to handle query selection for comparison @@ -209,60 +216,65 @@ const TestQueryPage: React.FC = () => { }); }; - // Function to analyze query for enhanced insights with tracing - const handleAnalyzeQuery = async (sqlQuery: string) => { - // Create a trace ID for this query analysis - const traceId = Math.random().toString(36).substring(2, 15); - + // Redirect to login if user is not authenticated + const handleLogin = () => { + navigate('/auth'); + }; + + // Function to handle redis test + const handleRedisTest = async () => { + // Things it needs + setRedisLoading(true); + setRedisDialogOpen(true); + setRedisMetrics(null); + setError(null); + try { - console.log(`Starting query analysis trace: ${traceId}`); - - const token = localStorage.getItem('authToken'); - const response = await fetch('http://localhost:4002/api/query/analyze', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - // Add trace context headers - 'X-Trace-Id': traceId, - 'X-Query-Source': 'test-query-page', + // check if user authenticated + if (!checkAuthentication()) { + throw Error('Authentication required. Please login to continue'); + } + const token = localStorage.getItem(`authToken`); + + // need to fetch our route in our backend that has runRedisTest + // it needs the queryId + const response = await fetch( + 'http://localhost:4002/api/run-query/redis', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + uri_string, + queryId, + }), }, - body: JSON.stringify({ - sqlQuery, - traceContext: { - traceId, - source: 'frontend.test-query-page', - timestamp: new Date().toISOString(), - } - }), - }); + ); - if (response.ok) { - const result = await response.json(); - setAnalysis(result.analysis); - setInsights(result.insights); - - // Log successful analysis with trace context - console.log(`Query analysis completed successfully`, { - traceId, - executionTime: result.analysis?.executionTime, - cacheHitRatio: result.analysis?.cacheHitRatio, - recommendations: result.insights?.recommendations?.length || 0, - }); - } else { - console.error(`Query analysis failed with status: ${response.status}`, { - traceId, - status: response.status, - }); + if (response.status === 401) { + localStorage.removeItem('authToken'); + localStorage.removeItem('user'); + setIsAuthenticated(false); + throw Error('Authentication required. Please login to continue.'); } - } catch (error) { - console.error('Analysis failed:', error, { traceId }); - } - }; - // Redirect to login if user is not authenticated - const handleLogin = () => { - navigate('/auth'); + if (!response.ok) { + throw Error('Failed to fetch metrics with Redis.'); + } + + const data: RedisTestResult = await response.json(); + setRedisMetrics(data); + } catch (err) { + setRedisDialogOpen(false); + setError( + 'Error fetching metrics with Redis. Make sure to input correct URI', + ); + console.error(err); + } finally { + setRedisLoading(false); + } }; return ( @@ -274,7 +286,9 @@ const TestQueryPage: React.FC = () => { isAuthenticated={isAuthenticated} onHistoryClick={() => { setShowQueryHistory(true); - setCompareMode(false); + if (!compareMode) { + setSelectedQueries({ first: null, second: null }); + } }} /> @@ -301,6 +315,10 @@ const TestQueryPage: React.FC = () => { { + setShowQueryHistory(false); + setShowComparisonDialog(true); + }} onExitCompare={() => { // When we exit out setCompareMode becomes false and the page goes back to normal view. setCompareMode(false); @@ -335,24 +353,49 @@ const TestQueryPage: React.FC = () => { Query Metrics + + + + + + + )} - - {/* Lean Query Analyzer */} - {analysis && insights && ( - - )} )} {/* Modals */} - setShowQueryHistory(false)} savedQueries={savedQueries} @@ -361,31 +404,8 @@ const TestQueryPage: React.FC = () => { setShowQueryHistory(false); setShowComparisonDialog(true); }} - onCompareWithCurrent={(historicalQuery) => { - setShowQueryHistory(false); - setFirstQuery(historicalQuery); - setSecondQuery({ - id: 0, - queryName: 'Current Query', - queryText: query, - metrics: queryMetrics || { - executionTime: 0, - planningTime: 0, - rowsReturned: 0, - actualLoops: 0, - sharedHitBlocks: 0, - sharedReadBlocks: 0, - workMem: 0, - cacheHitRatio: 0, - startupCost: 0, - totalCost: 0, - }, - createdAt: new Date().toISOString(), - }); - setCompareMode(true); - }} /> - setShowComparisonDialog(false)} savedQueries={savedQueries} @@ -393,6 +413,12 @@ const TestQueryPage: React.FC = () => { onSelectQuery={handleSelectQuery} onCompare={handleCompare} /> + setRedisDialogOpen(false)} + redisMetrics={redisMetrics} + redisLoading={redisLoading} + /> ); }; From af7283c7bd5aad45aac903e2f215a4c96cdd8af4 Mon Sep 17 00:00:00 2001 From: Bryan Date: Wed, 8 Apr 2026 16:17:55 -0400 Subject: [PATCH 14/21] feat: return queryId in saveMetricsToDB response --- server/controllers/userDatabaseController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/controllers/userDatabaseController.ts b/server/controllers/userDatabaseController.ts index b3f5a2d..3223ac7 100644 --- a/server/controllers/userDatabaseController.ts +++ b/server/controllers/userDatabaseController.ts @@ -170,7 +170,7 @@ const userDatabaseController: UserDatabaseController = { ], ); - res.status(200).json(queryMetrics); + res.status(200).json({ ...queryMetrics, id: queryId }); } catch (err) { console.error('Error saving metrics', err); return next({ From a0ee219955ca3554e963cae80770cd1eba3add06 Mon Sep 17 00:00:00 2001 From: Bryan Date: Wed, 8 Apr 2026 16:19:50 -0400 Subject: [PATCH 15/21] feat: add id to QueryMetrics interface and fix cacheHitRatio display --- src/components/QueryPerformance/MetricsTable.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/QueryPerformance/MetricsTable.tsx b/src/components/QueryPerformance/MetricsTable.tsx index 97ef3e5..e81dc77 100644 --- a/src/components/QueryPerformance/MetricsTable.tsx +++ b/src/components/QueryPerformance/MetricsTable.tsx @@ -11,6 +11,7 @@ import { // Define the interface for query metrics export interface QueryMetrics { + id: number; // Required in order to run redis on fresh new query test executionTime: number; planningTime: number; rowsReturned: number; @@ -82,7 +83,9 @@ const MetricsTable: React.FC = ({ metrics }) => { Cache Hit Ratio - {metrics.cacheHitRatio}% + + {metrics.cacheHitRatio.toFixed(2)}% + From fbedac1923b22a619ea30d9f1c8898a16c5bf4d6 Mon Sep 17 00:00:00 2001 From: Bryan Date: Wed, 8 Apr 2026 16:22:14 -0400 Subject: [PATCH 16/21] refactor: replace QueryComparisonDialog with inline selection in QueryHistoryDialog - Remove QueryComparisonDialog component - Replace selectedQueries state with selectedQueryIds array - Move comparison selection into QueryHistoryDialog via onToggleSelect and onCompare props - Set queryId from fetchMetrics response for Redis button on fresh queries - Fix useEffect to run once on mount only to avoid infinite loop - Update handleLoadQuery signature to accept id and name params - Remove showComparisonDialog state --- .../QueryComparisonDialog.tsx | 100 ------------------ .../QueryPerformance/TestQueryPage.tsx | 83 +++++---------- 2 files changed, 29 insertions(+), 154 deletions(-) delete mode 100644 src/components/QueryPerformance/QueryComparisonDialog.tsx diff --git a/src/components/QueryPerformance/QueryComparisonDialog.tsx b/src/components/QueryPerformance/QueryComparisonDialog.tsx deleted file mode 100644 index 9addb88..0000000 --- a/src/components/QueryPerformance/QueryComparisonDialog.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react'; -import { - Dialog, - DialogTitle, - DialogContent, - DialogContentText, - DialogActions, - Button, - FormControl, - InputLabel, - Select, - MenuItem, -} from '@mui/material'; -import { SavedQuery } from './QueryHistoryDialog'; - -interface QueryComparisonDialogProps { - open: boolean; - onClose: () => void; - savedQueries: SavedQuery[]; - selectedQueries: { - first: number | null; - second: number | null; - }; - onSelectQuery: (key: 'first' | 'second', value: number) => void; - onCompare: () => void; -} - -const QueryComparisonDialog: React.FC = ({ - open, - onClose, - savedQueries, - selectedQueries, - onSelectQuery, - onCompare, -}) => { - return ( - - Compare Queries - - - Select two queries to compare their performance metrics side by side. - - - - First Query - - - - - Second Query - - - - - - - - - ); -}; - -export default QueryComparisonDialog; diff --git a/src/components/QueryPerformance/TestQueryPage.tsx b/src/components/QueryPerformance/TestQueryPage.tsx index 7ab040d..562ee42 100644 --- a/src/components/QueryPerformance/TestQueryPage.tsx +++ b/src/components/QueryPerformance/TestQueryPage.tsx @@ -18,7 +18,6 @@ import { useNavigate } from 'react-router-dom'; import Header from './Header'; // Nav bar on component on top import MetricsTable, { QueryMetrics } from './MetricsTable'; // component that has the query mertics import QueryHistoryDialog, { SavedQuery } from './QueryHistoryDialog'; // component that you can view your past queries. -import QueryComparisonDialog from './QueryComparisonDialog'; import QueryComparisonPage from './QueryComparisonPage'; import TestQueryForm from './TestQueryForm'; import RedisTestDialog, { RedisTestResult } from './RedisTestDialog'; @@ -54,17 +53,11 @@ const TestQueryPage: React.FC = () => { // State for saved queries and comparison const [savedQueries, setSavedQueries] = useState([]); const [showQueryHistory, setShowQueryHistory] = useState(false); - const [showComparisonDialog, setShowComparisonDialog] = useState(false); - const [selectedQueries, setSelectedQueries] = useState<{ - first: number | null; - second: number | null; - }>({ - first: null, - second: null, - }); - const [compareMode, setCompareMode] = useState(false); + const [firstQuery, setFirstQuery] = useState(null); const [secondQuery, setSecondQuery] = useState(null); + const [selectedQueryIds, setSelectedQueryIds] = useState([]); + const [compareMode, setCompareMode] = useState(false); // Redis state const [redisDialogOpen, setRedisDialogOpen] = useState(false); @@ -85,13 +78,17 @@ const TestQueryPage: React.FC = () => { } }; - // Check if user is authenticated on component mount + // Run once on mount - check auth and load saved queries if authenticated useEffect(() => { - checkAuthentication(); - if (isAuthenticated) { + const isAuthed = checkAuthentication(); + if (isAuthed) { fetchSavedQueries(); } - }, [isAuthenticated]); + // Run once on mount only - adding fetchSavedQueries to deps would cause an infinite loop + // since it's recreated on every render. checkAuthentication return value is used + // instead of isAuthenticated state to avoid stale state timing issue. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // Fetch saved queries from the backend const fetchSavedQueries = async () => { @@ -168,6 +165,7 @@ const TestQueryPage: React.FC = () => { const data: QueryMetrics = await response.json(); setQueryMetrics(data); + setQueryId(data.id); // adding this for the redis button to work on fresh new query, // Refresh the saved queries list after successful fetch await fetchSavedQueries(); @@ -179,43 +177,33 @@ const TestQueryPage: React.FC = () => { } }; - // Function to handle selecting queries for comparison const handleCompare = () => { - if (selectedQueries.first !== null && selectedQueries.second !== null) { - const first = - savedQueries.find((q) => q.id === selectedQueries.first) || null; - const second = - savedQueries.find((q) => q.id === selectedQueries.second) || null; - - setFirstQuery(first); - setSecondQuery(second); - setCompareMode(true); - setShowComparisonDialog(false); - } + const first = + savedQueries.find((q) => q.id === selectedQueryIds[0]) ?? null; + const second = + savedQueries.find((q) => q.id === selectedQueryIds[1]) ?? null; + setFirstQuery(first); + setSecondQuery(second); + setCompareMode(true); }; // Function to handle loading a query from history // Takes in the string and metrics thats that have a set type for each metric. const handleLoadQuery = ( + id: number, + name: string, queryText: string, metrics: QueryMetrics, - id: number, ) => { setQueryId(id); + setUri_string(''); + setQueryName(name); setQuery(queryText); setQueryMetrics(metrics); setShowQueryHistory(false); setCompareMode(false); }; - // Function to handle query selection for comparison - const handleSelectQuery = (key: 'first' | 'second', value: number) => { - setSelectedQueries({ - ...selectedQueries, - [key]: value, - }); - }; - // Redirect to login if user is not authenticated const handleLogin = () => { navigate('/auth'); @@ -286,9 +274,6 @@ const TestQueryPage: React.FC = () => { isAuthenticated={isAuthenticated} onHistoryClick={() => { setShowQueryHistory(true); - if (!compareMode) { - setSelectedQueries({ first: null, second: null }); - } }} /> @@ -315,15 +300,10 @@ const TestQueryPage: React.FC = () => { { - setShowQueryHistory(false); - setShowComparisonDialog(true); - }} onExitCompare={() => { + setSelectedQueryIds([]); // When we exit out setCompareMode becomes false and the page goes back to normal view. setCompareMode(false); - setFirstQuery(null); // - setSecondQuery(null); }} /> ) : ( @@ -400,17 +380,12 @@ const TestQueryPage: React.FC = () => { onClose={() => setShowQueryHistory(false)} savedQueries={savedQueries} onLoadQuery={handleLoadQuery} - onOpenCompare={() => { - setShowQueryHistory(false); - setShowComparisonDialog(true); + selectedQueryIds={selectedQueryIds} + onToggleSelect={(id: number) => { + setSelectedQueryIds((prev) => + prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id], + ); }} - /> - setShowComparisonDialog(false)} - savedQueries={savedQueries} - selectedQueries={selectedQueries} - onSelectQuery={handleSelectQuery} onCompare={handleCompare} /> Date: Wed, 8 Apr 2026 16:22:38 -0400 Subject: [PATCH 17/21] refactor: remove onOpenCompare prop and button from QueryComparisonPage --- .../QueryPerformance/QueryComparisonPage.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/components/QueryPerformance/QueryComparisonPage.tsx b/src/components/QueryPerformance/QueryComparisonPage.tsx index 002e01c..0d457e1 100644 --- a/src/components/QueryPerformance/QueryComparisonPage.tsx +++ b/src/components/QueryPerformance/QueryComparisonPage.tsx @@ -10,20 +10,17 @@ import { } from '@mui/material'; import { SavedQuery } from './QueryHistoryDialog'; import MetricsTable from './MetricsTable'; -import CompareArrowsIcon from '@mui/icons-material/CompareArrows'; interface QueryComparisonPageProps { firstQuery: SavedQuery | null; secondQuery: SavedQuery | null; onExitCompare: () => void; - onOpenCompare: () => void; } const QueryComparisonPage: React.FC = ({ firstQuery, secondQuery, onExitCompare, - onOpenCompare, }) => { if (!firstQuery || !secondQuery) return null; @@ -40,13 +37,7 @@ const QueryComparisonPage: React.FC = ({ Query Comparison - +