From a96b9f2c8b0c6ea7700d9df1ff9d60dff2c71157 Mon Sep 17 00:00:00 2001 From: Mike Conrad Date: Mon, 19 May 2025 11:07:03 -0400 Subject: [PATCH] Add redis and basic frontend for viewing data --- .env.example | 5 +- adonisrc.ts | 1 + app/controllers/replays_controller.ts | 68 +++++++++++++++- config/redis.ts | 36 +++++++++ docker-compose.yml | 3 + inertia/pages/Replays/Index.vue | 43 ++++++++++ package-lock.json | 110 ++++++++++++++++++++++---- package.json | 6 +- start/env.ts | 4 + start/routes.ts | 2 + 10 files changed, 258 insertions(+), 20 deletions(-) create mode 100644 config/redis.ts create mode 100644 inertia/pages/Replays/Index.vue diff --git a/.env.example b/.env.example index 496e094..13fed0c 100644 --- a/.env.example +++ b/.env.example @@ -8,4 +8,7 @@ SESSION_DRIVER=cookie PG_HOST=localhost PG_PASSWORD=password SENTRY_TOKEN= -SENTRY_ORG= \ No newline at end of file +SENTRY_ORG= +REDIS_HOST=sentry-redis-1 +REDIS_PORT=6379 +REDIS_PASSWORD= \ No newline at end of file diff --git a/adonisrc.ts b/adonisrc.ts index dcb5d0f..9da686c 100644 --- a/adonisrc.ts +++ b/adonisrc.ts @@ -53,6 +53,7 @@ export default defineConfig({ () => import('@adonisjs/lucid/database_provider'), () => import('@adonisjs/auth/auth_provider'), () => import('@adonisjs/inertia/inertia_provider'), + () => import('@adonisjs/redis/redis_provider') ], /* diff --git a/app/controllers/replays_controller.ts b/app/controllers/replays_controller.ts index 35aeeb3..4384a64 100644 --- a/app/controllers/replays_controller.ts +++ b/app/controllers/replays_controller.ts @@ -4,6 +4,7 @@ import type { HttpContext } from '@adonisjs/core/http' const SENTRY_TOKEN = env.get('SENTRY_TOKEN') const SENTRY_ORG = env.get('SENTRY_ORG') let recordsUpdated = 0 +import redis from '@adonisjs/redis/services/main' interface ApiResponse { @@ -18,7 +19,42 @@ interface SentryPagination { next: string } export default class ReplaysController { - + + public async list({ request, inertia }: HttpContext) { + const page = request.input('page', 1) + const perPage = 20 + const cacheKey = `replays:page:${page}` + + let data = await redis.get(cacheKey) + let paginated, meta, replays + + if (data) { + ({ paginated, meta, replays } = JSON.parse(data)) + } else { + paginated = await Replay.query().paginate(page, perPage) + paginated.baseUrl('/list') + + const json = paginated.toJSON() + + meta = { + ...json.meta, + links: buildPaginationLinks(json.meta) + } + + replays = json.data + + await redis.set(cacheKey, JSON.stringify({ paginated, meta, replays }), 'EX', 60) + } + + return inertia.render('Replays/Index', { + data: { + replays, + meta + } + }) + + +} async index({ request, response }: HttpContext) { const {statsPeriod, start, end} = request.qs() @@ -97,4 +133,32 @@ function parseSentryLinkHeader(header:string): SentryPagination { if (key in data) acc[key] = data[key] return acc }, {} as Record) - } \ No newline at end of file + } + + function buildPaginationLinks(meta) { + const links = [] + + // Previous + links.push({ + url: meta.previousPageUrl, + label: '« Prev', + active: false + }) + + for (let page = 1; page <= meta.lastPage; page++) { + links.push({ + url: `/list?page=${page}`, + label: page.toString(), + active: page === meta.currentPage + }) + } + + // Next + links.push({ + url: meta.nextPageUrl, + label: 'Next »', + active: false + }) + + return links +} \ No newline at end of file diff --git a/config/redis.ts b/config/redis.ts new file mode 100644 index 0000000..49b59d9 --- /dev/null +++ b/config/redis.ts @@ -0,0 +1,36 @@ +import env from '#start/env' +import { defineConfig } from '@adonisjs/redis' +import { InferConnections } from '@adonisjs/redis/types' + +const redisConfig = defineConfig({ + connection: 'main', + + connections: { + /* + |-------------------------------------------------------------------------- + | The default connection + |-------------------------------------------------------------------------- + | + | The main connection you want to use to execute redis commands. The same + | connection will be used by the session provider, if you rely on the + | redis driver. + | + */ + main: { + host: 'sentry-redis-1', + port: env.get('REDIS_PORT'), + password: env.get('REDIS_PASSWORD', ''), + db: 0, + keyPrefix: '', + retryStrategy(times) { + return times > 10 ? null : times * 50 + }, + }, + }, +}) + +export default redisConfig + +declare module '@adonisjs/redis/types' { + export interface RedisConnections extends InferConnections {} +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index b7e3df5..ddd54c9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,5 +32,8 @@ image: grafana/grafana:latest labels: - "traefik.http.routers.grafana.rule=Host(`grafana.docker.localhost`)" + redis: + image: redis:latest + volumes: backend_node_modules: {} \ No newline at end of file diff --git a/inertia/pages/Replays/Index.vue b/inertia/pages/Replays/Index.vue new file mode 100644 index 0000000..644ec63 --- /dev/null +++ b/inertia/pages/Replays/Index.vue @@ -0,0 +1,43 @@ + + + diff --git a/package-lock.json b/package-lock.json index aff4bfa..6d9db39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@adonisjs/cors": "^2.2.1", "@adonisjs/inertia": "^3.1.1", "@adonisjs/lucid": "^21.6.1", + "@adonisjs/redis": "^9.2.0", "@adonisjs/session": "^7.5.1", "@adonisjs/shield": "^8.2.0", "@adonisjs/static": "^1.1.1", @@ -24,6 +25,7 @@ "edge.js": "^6.2.1", "luxon": "^3.6.1", "pg": "^8.16.0", + "pino-pretty": "^13.0.0", "reflect-metadata": "^0.2.2", "vue": "^3.5.14" }, @@ -41,7 +43,6 @@ "@vitejs/plugin-vue": "^5.2.4", "eslint": "^9.26.0", "hot-hook": "^0.4.0", - "pino-pretty": "^13.0.0", "prettier": "^3.5.3", "ts-node-maintained": "^10.9.5", "typescript": "~5.8.3", @@ -594,6 +595,22 @@ "prettier-plugin-edgejs": "^1.0.0" } }, + "node_modules/@adonisjs/redis": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@adonisjs/redis/-/redis-9.2.0.tgz", + "integrity": "sha512-DUI9NrHDLZ2ISNjMlqWbKJT99ZYj1ZmvhNFTfhVs9lc7K2KJmNKZfK8Y85a8eN7q+ZYMBYSu1uRemxGs6xRaYw==", + "dependencies": { + "@poppinss/utils": "^6.9.2", + "emittery": "^1.1.0", + "ioredis": "^5.4.2" + }, + "engines": { + "node": ">=20.6.0" + }, + "peerDependencies": { + "@adonisjs/core": "^6.2.0" + } + }, "node_modules/@adonisjs/repl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@adonisjs/repl/-/repl-4.1.0.tgz", @@ -1578,6 +1595,11 @@ "vue": "^3.0.0" } }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "node_modules/@japa/assert": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@japa/assert/-/assert-4.0.1.tgz", @@ -3746,6 +3768,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/code-block-writer": { "version": "13.0.3", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", @@ -3973,7 +4003,6 @@ "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", - "dev": true, "license": "MIT", "engines": { "node": "*" @@ -4053,6 +4082,14 @@ "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==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4238,7 +4275,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -4766,7 +4802,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", - "dev": true, "license": "MIT" }, "node_modules/fast-deep-equal": { @@ -4839,7 +4874,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "dev": true, "license": "MIT" }, "node_modules/fastest-levenshtein": { @@ -5451,7 +5485,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", - "dev": true, "license": "MIT" }, "node_modules/hosted-git-info": { @@ -5648,6 +5681,29 @@ "node": ">= 0.10" } }, + "node_modules/ioredis": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", + "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", + "dependencies": { + "@ioredis/commands": "^1.1.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", @@ -5813,7 +5869,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -6072,6 +6127,16 @@ "dev": true, "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==" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6351,7 +6416,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6523,7 +6587,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -6953,7 +7016,6 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz", "integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==", - "dev": true, "license": "MIT", "dependencies": { "colorette": "^2.0.7", @@ -6978,7 +7040,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/pino-std-serializers": { @@ -7266,7 +7327,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -7627,6 +7687,25 @@ "node": ">= 10.13.0" } }, + "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==", + "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==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -8218,6 +8297,11 @@ "get-source": "^2.0.12" } }, + "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==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -8328,7 +8412,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9041,7 +9124,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/xtend": { diff --git a/package.json b/package.json index e22dfb9..88afd5b 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@adonisjs/cors": "^2.2.1", "@adonisjs/inertia": "^3.1.1", "@adonisjs/lucid": "^21.6.1", + "@adonisjs/redis": "^9.2.0", "@adonisjs/session": "^7.5.1", "@adonisjs/shield": "^8.2.0", "@adonisjs/static": "^1.1.1", @@ -66,10 +67,9 @@ "edge.js": "^6.2.1", "luxon": "^3.6.1", "pg": "^8.16.0", + "pino-pretty": "^13.0.0", "reflect-metadata": "^0.2.2", - "vue": "^3.5.14", - "pino-pretty": "^13.0.0" - + "vue": "^3.5.14" }, "hotHook": { "boundaries": [ diff --git a/start/env.ts b/start/env.ts index 39e4874..88b74bf 100644 --- a/start/env.ts +++ b/start/env.ts @@ -24,4 +24,8 @@ export default await Env.create(new URL('../', import.meta.url), { |---------------------------------------------------------- */ SESSION_DRIVER: Env.schema.enum(['cookie', 'memory'] as const), + + REDIS_HOST: Env.schema.string({ format: 'host' }), + REDIS_PORT: Env.schema.number(), + REDIS_PASSWORD: Env.schema.string.optional() }) diff --git a/start/routes.ts b/start/routes.ts index 9cc2ca5..02b6934 100644 --- a/start/routes.ts +++ b/start/routes.ts @@ -11,3 +11,5 @@ import ReplaysController from '#controllers/replays_controller' import router from '@adonisjs/core/services/router' router.on('/').renderInertia('home') router.get('/replays', [ReplaysController, 'index']) +router.get('/list', [ReplaysController, 'list' +]) \ No newline at end of file