├── .changeset
├── README.md
└── config.json
├── .gitattributes
├── .github
└── workflows
│ ├── release.yml
│ └── tests.yml
├── .gitignore
├── LICENSE.md
├── README.md
├── docker-compose.yml
├── examples
├── with-elysia-and-bun
│ ├── bun.lockb
│ ├── main.ts
│ ├── package.json
│ └── tsconfig.json
├── with-express
│ ├── main.ts
│ ├── package.json
│ ├── tsconfig.json
│ └── yarn.lock
├── with-fastify
│ ├── main.ts
│ ├── package.json
│ ├── tsconfig.json
│ └── yarn.lock
├── with-hono
│ ├── main.ts
│ ├── package.json
│ ├── pnpm-lock.yaml
│ └── tsconfig.json
└── with-next
│ ├── next-env.d.ts
│ ├── next.config.js
│ ├── package.json
│ ├── pages
│ ├── _app.tsx
│ ├── api
│ │ └── queuedash
│ │ │ └── [trpc].ts
│ └── queuedash
│ │ └── [[...slug]].tsx
│ ├── tsconfig.json
│ └── yarn.lock
├── package.json
├── packages
├── api
│ ├── .eslintrc.js
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── adapters
│ │ │ ├── elysia.ts
│ │ │ ├── express.ts
│ │ │ ├── fastify.ts
│ │ │ ├── hono.ts
│ │ │ └── utils.ts
│ │ ├── main.ts
│ │ ├── routers
│ │ │ ├── _app.ts
│ │ │ ├── job.ts
│ │ │ ├── queue.ts
│ │ │ └── scheduler.ts
│ │ ├── tests
│ │ │ ├── jobs.test.ts
│ │ │ ├── queue.test.ts
│ │ │ ├── schedulers.test.ts
│ │ │ └── test.utils.ts
│ │ ├── trpc.ts
│ │ └── utils
│ │ │ └── global.utils.ts
│ ├── tsconfig.json
│ ├── vite.config.ts
│ └── vitest.config.ts
├── client
│ ├── .eslintrc.js
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ └── main.tsx
│ ├── tsconfig.json
│ ├── vite-env.d.ts
│ └── vite.config.ts
├── dev
│ ├── .eslintrc.js
│ ├── CHANGELOG.md
│ ├── next-env.d.ts
│ ├── next.config.js
│ ├── package.json
│ ├── pages
│ │ ├── _app.tsx
│ │ ├── api
│ │ │ ├── queuedash
│ │ │ │ └── [trpc].ts
│ │ │ └── reset.ts
│ │ └── queuedash
│ │ │ └── [[...slug]].tsx
│ ├── tsconfig.json
│ └── utils
│ │ ├── fake-data.ts
│ │ └── worker.ts
└── ui
│ ├── .eslintrc.js
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── package.json
│ ├── postcss.config.js
│ ├── src
│ ├── App.tsx
│ ├── components
│ │ ├── ActionMenu.tsx
│ │ ├── AddJobModal.tsx
│ │ ├── Alert.tsx
│ │ ├── Button.tsx
│ │ ├── Checkbox.tsx
│ │ ├── ErrorCard.tsx
│ │ ├── JobActionMenu.tsx
│ │ ├── JobModal.tsx
│ │ ├── JobOptionTag.tsx
│ │ ├── JobTable.tsx
│ │ ├── JobTableSkeleton.tsx
│ │ ├── Layout.tsx
│ │ ├── QueueActionMenu.tsx
│ │ ├── QueueStatusTabs.tsx
│ │ ├── SchedulerActionMenu.tsx
│ │ ├── SchedulerModal.tsx
│ │ ├── SchedulerTable.tsx
│ │ ├── Skeleton.tsx
│ │ ├── TableRow.tsx
│ │ ├── ThemeSwitcher.tsx
│ │ └── Tooltip.tsx
│ ├── main.ts
│ ├── pages
│ │ ├── HomePage.tsx
│ │ └── QueuePage.tsx
│ ├── styles
│ │ └── global.css
│ └── utils
│ │ ├── config.ts
│ │ └── trpc.ts
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ ├── vite-env.d.ts
│ └── vite.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
3 | "changelog": [
4 | "@changesets/changelog-github",
5 | {
6 | "repo": "alexbudure/queuedash"
7 | }
8 | ],
9 | "commit": false,
10 | "fixed": [],
11 | "linked": [],
12 | "ignore": ["@queuedash/dev"],
13 | "access": "public",
14 | "baseBranch": "main",
15 | "updateInternalDependencies": "patch"
16 | }
17 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | concurrency: ${{ github.workflow }}-${{ github.ref }}
9 |
10 | jobs:
11 | release:
12 | name: Release
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout Repo
16 | uses: actions/checkout@v3
17 |
18 | - name: Setup Node.js 18.x
19 | uses: actions/setup-node@v3
20 | with:
21 | node-version: 18.x
22 |
23 | - uses: pnpm/action-setup@v2
24 | name: Install pnpm
25 | id: pnpm-install
26 | with:
27 | version: 8
28 | run_install: false
29 |
30 | - name: Get pnpm store directory
31 | id: pnpm-cache
32 | shell: bash
33 | run: |
34 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
35 |
36 | - uses: actions/cache@v3
37 | name: Setup pnpm cache
38 | with:
39 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
40 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
41 | restore-keys: |
42 | ${{ runner.os }}-pnpm-store-
43 |
44 | - name: Install dependencies
45 | run: pnpm install
46 |
47 | - name: Create Release Pull Request or Publish to npm
48 | id: changesets
49 | uses: changesets/action@v1
50 | with:
51 | publish: pnpm release
52 | env:
53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
54 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
55 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 | on: push
3 |
4 | jobs:
5 | tests:
6 | name: Tests
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Checkout Repo
10 | uses: actions/checkout@v3
11 |
12 | - name: Setup Node.js 18.x
13 | uses: actions/setup-node@v3
14 | with:
15 | node-version: 18.x
16 |
17 | - uses: pnpm/action-setup@v2
18 | name: Install pnpm
19 | id: pnpm-install
20 | with:
21 | version: 8
22 | run_install: false
23 |
24 | - name: Get pnpm store directory
25 | id: pnpm-cache
26 | shell: bash
27 | run: |
28 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
29 |
30 | - uses: actions/cache@v3
31 | name: Setup pnpm cache
32 | with:
33 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
34 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
35 | restore-keys: |
36 | ${{ runner.os }}-pnpm-store-
37 |
38 | - name: Install dependencies
39 | run: pnpm install
40 |
41 | - name: Run docker
42 | run: pnpm docker
43 |
44 | - name: Run build
45 | run: pnpm build
46 |
47 | - name: Run tests
48 | run: pnpm test
49 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | node_modules
3 | package-lock.json
4 | .pnpm-store/
5 |
6 | # Editors
7 | **/.#*
8 | .nvmrc
9 | **/.vscode
10 | dist
11 | .turbo
12 | .rollup.cache
13 | .idea
14 | .next
15 | .env
16 |
17 | # Misc
18 | .DS_Store
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Alex Budure
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | A stunning, sleek dashboard for Bull, BullMQ, and Bee-Queue.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ## Features
21 |
22 | - 😍 Simple, clean, and compact UI
23 | - 🧙 Add jobs to your queue with ease
24 | - 🪄 Retry, remove, and more convenient actions for your jobs
25 | - 📊 Stats for job counts, job durations, and job wait times
26 | - ✨ Top-level overview page of all queues
27 | - 🔋 Integrates with Next.js, Express.js, and Fastify
28 | - ⚡️ Compatible with Bull, BullMQ, and Bee-Queue
29 |
30 | ## Getting Started
31 |
32 | ### Express
33 |
34 | `pnpm install @queuedash/api`
35 |
36 | ```typescript
37 | import express from "express";
38 | import Bull from "bull";
39 | import { createQueueDashExpressMiddleware } from "@queuedash/api";
40 |
41 | const app = express();
42 |
43 | const reportQueue = new Bull("report-queue");
44 |
45 | app.use(
46 | "/queuedash",
47 | createQueueDashExpressMiddleware({
48 | ctx: {
49 | queues: [
50 | {
51 | queue: reportQueue,
52 | displayName: "Reports",
53 | type: "bull" as const,
54 | },
55 | ],
56 | },
57 | })
58 | );
59 |
60 | app.listen(3000, () => {
61 | console.log("Listening on port 3000");
62 | console.log("Visit http://localhost:3000/queuedash");
63 | });
64 | ```
65 |
66 | ### Next.js
67 |
68 | `pnpm install @queuedash/api @queuedash/ui`
69 |
70 | ```typescript jsx
71 | // pages/admin/queuedash/[[...slug]].tsx
72 | import { QueueDashApp } from "@queuedash/ui";
73 |
74 | function getBaseUrl() {
75 | if (process.env.VERCEL_URL) {
76 | return `https://${process.env.VERCEL_URL}/api/queuedash`;
77 | }
78 |
79 | return `http://localhost:${process.env.PORT ?? 3000}/api/queuedash`;
80 | }
81 |
82 | const QueueDashPages = () => {
83 | return ;
84 | };
85 |
86 | export default QueueDashPages;
87 |
88 | // pages/api/queuedash/[trpc].ts
89 | import * as trpcNext from "@trpc/server/adapters/next";
90 | import { appRouter } from "@queuedash/api";
91 |
92 | const reportQueue = new Bull("report-queue");
93 |
94 | export default trpcNext.createNextApiHandler({
95 | router: appRouter,
96 | batching: {
97 | enabled: true,
98 | },
99 | createContext: () => ({
100 | queues: [
101 | {
102 | queue: reportQueue,
103 | displayName: "Reports",
104 | type: "bull" as const,
105 | },
106 | ],
107 | }),
108 | });
109 | ```
110 |
111 | See the [./examples](./examples) folder for more.
112 |
113 | ---
114 |
115 | ## API Reference
116 |
117 | ### `createQueueDash<*>Middleware`
118 |
119 | ```typescript
120 | type QueueDashMiddlewareOptions = {
121 | app: express.Application | FastifyInstance; // Express or Fastify app
122 | baseUrl: string; // Base path for the API and UI
123 | ctx: QueueDashContext; // Context for the UI
124 | };
125 |
126 | type QueueDashContext = {
127 | queues: QueueDashQueue[]; // Array of queues to display
128 | };
129 |
130 | type QueueDashQueue = {
131 | queue: Bull.Queue | BullMQ.Queue | BeeQueue; // Queue instance
132 | displayName: string; // Display name for the queue
133 | type: "bull" | "bullmq" | "bee"; // Queue type
134 | };
135 | ```
136 |
137 | ### ``
138 |
139 | ```typescript jsx
140 | type QueueDashAppProps = {
141 | apiUrl: string; // URL to the API endpoint
142 | basename: string; // Base path for the app
143 | };
144 | ```
145 |
146 | ## Roadmap
147 |
148 | - Supports Celery and other queueing systems
149 | - Command+K bar and shortcuts
150 | - Ability to whitelabel the UI
151 |
152 | ## Pro Version
153 |
154 | Right now, QueueDash simply taps into your Redis instance, making it very easy to set up, but also limited in functionality.
155 |
156 | I'm thinking about building a free-to-host version on top of this which will require external services (db, auth, etc.), but it will make the following features possible:
157 |
158 | - Alerts and notifications
159 | - Quick search and filtering
160 | - Queue trends and analytics
161 | - Invite team members
162 |
163 | If you're interested in this version, please let me know!
164 |
165 | ## Acknowledgements
166 |
167 | QueueDash was inspired by some great open source projects. Here's a few of them:
168 |
169 | - [bull-board](https://github.com/vcapretz/bull-board)
170 | - [bull-monitor](https://github.com/s-r-x/bull-monitor)
171 | - [bull-arena](https://github.com/bee-queue/arena)
172 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.9"
2 |
3 | services:
4 | redis:
5 | image: redis
6 | ports:
7 | - "6379:6379"
8 |
--------------------------------------------------------------------------------
/examples/with-elysia-and-bun/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexbudure/queuedash/8416e1b42a7db0928ff79a5fef24599d02ff1051/examples/with-elysia-and-bun/bun.lockb
--------------------------------------------------------------------------------
/examples/with-elysia-and-bun/main.ts:
--------------------------------------------------------------------------------
1 | import Bull from "bull";
2 | import { Elysia } from "elysia";
3 | import { queuedash } from "@queuedash/api";
4 |
5 | const app = new Elysia().use(
6 | queuedash({
7 | baseUrl: "/queuedash",
8 | ctx: {
9 | queues: [
10 | {
11 | queue: new Bull("report-queue"),
12 | displayName: "Reports",
13 | type: "bull" as const,
14 | },
15 | ],
16 | },
17 | })
18 | );
19 |
20 | await app.listen(3000);
21 | console.log(`Running at http://${app.server?.hostname}:${app.server?.port}`);
22 |
--------------------------------------------------------------------------------
/examples/with-elysia-and-bun/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "with-elysia-and-bun",
3 | "module": "main.ts",
4 | "type": "module",
5 | "scripts": {
6 | "start": "bun main.ts"
7 | },
8 | "devDependencies": {
9 | "bun-types": "latest"
10 | },
11 | "peerDependencies": {
12 | "typescript": "^5.2.2"
13 | },
14 | "dependencies": {
15 | "@queuedash/api": "^2.0.3",
16 | "bull": "^4.11.3",
17 | "elysia": "^0.6.24"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/examples/with-elysia-and-bun/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "lib": ["ESNext", "dom"],
5 | "module": "esnext",
6 | "target": "esnext",
7 | "outDir": "dist",
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "noEmit": true,
13 | "incremental": true,
14 | "esModuleInterop": true,
15 | "moduleResolution": "node",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "jsx": "preserve",
19 | "composite": true,
20 | "declaration": true,
21 | "declarationMap": true,
22 | "inlineSources": false,
23 | "noUnusedLocals": false,
24 | "noUnusedParameters": false,
25 | "preserveWatchOutput": true,
26 | "baseUrl": ".",
27 | "types": ["bun-types"]
28 | },
29 | "include": ["main.ts", "package.json"],
30 | "exclude": ["node_modules"]
31 | }
32 |
--------------------------------------------------------------------------------
/examples/with-express/main.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import Bull from "bull";
3 | import { createQueueDashExpressMiddleware } from "@queuedash/api";
4 |
5 | const app = express();
6 |
7 | app.use(
8 | "/queuedash",
9 | createQueueDashExpressMiddleware({
10 | ctx: {
11 | queues: [
12 | {
13 | queue: new Bull("report-queue"),
14 | displayName: "Reports",
15 | type: "bull" as const,
16 | },
17 | ],
18 | },
19 | })
20 | );
21 |
22 | app.listen(3000, () => {
23 | console.log("Listening on port 3000");
24 | console.log("Visit http://localhost:3000/queuedash");
25 | });
26 |
--------------------------------------------------------------------------------
/examples/with-express/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "with-express",
3 | "version": "1.0.0",
4 | "description": "Example of using QueueDash with Express",
5 | "author": "Alex Budure",
6 | "license": "MIT",
7 | "scripts": {
8 | "start": "ts-node main.ts"
9 | },
10 | "dependencies": {
11 | "@queuedash/api": "^2.0.5",
12 | "bull": "^4.11.3",
13 | "express": "^4.18.2"
14 | },
15 | "devDependencies": {
16 | "@types/express": "^4.17.18",
17 | "ts-node": "^10.9.1",
18 | "typescript": "^5.2.2"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/examples/with-express/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "lib": ["ESNext", "dom"],
5 | "module": "commonjs",
6 | "target": "esnext",
7 | "outDir": "dist",
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "noEmit": true,
13 | "incremental": true,
14 | "esModuleInterop": true,
15 | "moduleResolution": "node",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "jsx": "preserve",
19 | "composite": true,
20 | "declaration": true,
21 | "declarationMap": true,
22 | "inlineSources": false,
23 | "noUnusedLocals": false,
24 | "noUnusedParameters": false,
25 | "preserveWatchOutput": true,
26 | "baseUrl": "."
27 | },
28 | "include": ["main.ts", "package.json"],
29 | "exclude": ["node_modules"]
30 | }
31 |
--------------------------------------------------------------------------------
/examples/with-fastify/main.ts:
--------------------------------------------------------------------------------
1 | import fastify from "fastify";
2 | import Bull from "bull";
3 | import { fastifyQueueDashPlugin } from "@queuedash/api";
4 |
5 | const server = fastify();
6 |
7 | server.register(fastifyQueueDashPlugin, {
8 | baseUrl: "/queuedash",
9 | ctx: {
10 | queues: [
11 | {
12 | queue: new Bull("report-queue"),
13 | displayName: "Reports",
14 | type: "bull" as const,
15 | },
16 | ],
17 | },
18 | });
19 |
20 | server.listen({ port: 3000 }, () => {
21 | console.log("Listening on port 3000");
22 | console.log("Visit http://localhost:3000/queuedash");
23 | });
24 |
--------------------------------------------------------------------------------
/examples/with-fastify/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "with-fastify",
3 | "version": "1.0.0",
4 | "description": "Example of using QueueDash with Fastify",
5 | "author": "Alex Budure",
6 | "license": "MIT",
7 | "scripts": {
8 | "start": "ts-node main.ts"
9 | },
10 | "dependencies": {
11 | "@queuedash/api": "^2.0.5",
12 | "bull": "^4.11.3",
13 | "fastify": "^4.23.2"
14 | },
15 | "devDependencies": {
16 | "ts-node": "^10.9.1",
17 | "typescript": "^5.2.2"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/examples/with-fastify/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "lib": ["ESNext", "dom"],
5 | "module": "commonjs",
6 | "target": "esnext",
7 | "outDir": "dist",
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "noEmit": true,
13 | "incremental": true,
14 | "esModuleInterop": true,
15 | "moduleResolution": "node",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "jsx": "preserve",
19 | "composite": true,
20 | "declaration": true,
21 | "declarationMap": true,
22 | "inlineSources": false,
23 | "noUnusedLocals": false,
24 | "noUnusedParameters": false,
25 | "preserveWatchOutput": true,
26 | "baseUrl": "."
27 | },
28 | "include": ["main.ts", "package.json"],
29 | "exclude": ["node_modules"]
30 | }
31 |
--------------------------------------------------------------------------------
/examples/with-hono/main.ts:
--------------------------------------------------------------------------------
1 | import { serve } from "@hono/node-server";
2 | import { Hono } from "hono";
3 | import Bull from "bull";
4 | import { createHonoAdapter } from "@queuedash/api";
5 |
6 | const app = new Hono();
7 |
8 | app.route(
9 | "/queuedash",
10 | createHonoAdapter({
11 | baseUrl: "/queuedash",
12 | ctx: {
13 | queues: [
14 | {
15 | queue: new Bull("report-queue"),
16 | displayName: "Reports",
17 | type: "bull" as const,
18 | },
19 | ],
20 | },
21 | })
22 | );
23 |
24 | const port = 3000;
25 | console.log(`Server is running on http://localhost:${port}`);
26 |
27 | serve({
28 | fetch: app.fetch,
29 | port,
30 | });
31 |
--------------------------------------------------------------------------------
/examples/with-hono/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "with-hono",
3 | "version": "1.0.0",
4 | "description": "Example of using QueueDash with Hono",
5 | "author": "Lukáš Huvar",
6 | "license": "MIT",
7 | "scripts": {
8 | "start": "tsx main.ts"
9 | },
10 | "dependencies": {
11 | "@hono/node-server": "^1.13.7",
12 | "@queuedash/api": "^3.4.0",
13 | "bull": "^4.16.5",
14 | "hono": "^4.6.16"
15 | },
16 | "devDependencies": {
17 | "@types/node": "^22.10.5",
18 | "tsx": "^4.19.2",
19 | "typescript": "^5.7.3"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/examples/with-hono/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "NodeNext",
5 | "strict": true,
6 | "skipLibCheck": true,
7 | "types": ["node"]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/examples/with-next/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/examples/with-next/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | };
5 |
6 | module.exports = nextConfig;
7 |
--------------------------------------------------------------------------------
/examples/with-next/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "with-next",
3 | "version": "1.0.0",
4 | "scripts": {
5 | "dev": "next dev"
6 | },
7 | "author": "Alex Budure",
8 | "license": "MIT",
9 | "dependencies": {
10 | "@queuedash/api": "^2.0.5",
11 | "@queuedash/ui": "^2.0.5",
12 | "@trpc/server": "^10.38.5",
13 | "bull": "^4.11.3",
14 | "next": "^13.5.3",
15 | "react": "^18.2.0",
16 | "react-dom": "^18.2.0"
17 | },
18 | "devDependencies": {
19 | "@types/node": "^18.14.6",
20 | "@types/react": "^18.0.28",
21 | "typescript": "^5.2.2"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/examples/with-next/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "@queuedash/ui/dist/styles.css";
2 |
3 | import type { AppType } from "next/app";
4 |
5 | const MyApp: AppType = ({ Component, pageProps }) => {
6 | return ;
7 | };
8 | export default MyApp;
9 |
--------------------------------------------------------------------------------
/examples/with-next/pages/api/queuedash/[trpc].ts:
--------------------------------------------------------------------------------
1 | import * as trpcNext from "@trpc/server/adapters/next";
2 | import { appRouter } from "@queuedash/api";
3 | import Bull from "bull";
4 |
5 | export default trpcNext.createNextApiHandler({
6 | router: appRouter,
7 | onError({ error }) {
8 | if (error.code === "INTERNAL_SERVER_ERROR") {
9 | // send to bug reporting
10 | console.error("Something went wrong", error);
11 | }
12 | },
13 | batching: {
14 | enabled: true,
15 | },
16 | createContext: () => ({
17 | queues: [
18 | {
19 | queue: new Bull("report-queue"),
20 | displayName: "Reports",
21 | type: "bull" as const,
22 | },
23 | ],
24 | }),
25 | });
26 |
--------------------------------------------------------------------------------
/examples/with-next/pages/queuedash/[[...slug]].tsx:
--------------------------------------------------------------------------------
1 | import { QueueDashApp } from "@queuedash/ui";
2 | import { NextPage } from "next";
3 |
4 | function getBaseUrl() {
5 | if (process.env.VERCEL_URL) {
6 | return `https://${process.env.VERCEL_URL}/api/queuedash`;
7 | }
8 |
9 | return `http://localhost:${process.env.PORT ?? 3000}/api/queuedash`;
10 | }
11 |
12 | const Page: NextPage = () => {
13 | return ;
14 | };
15 |
16 | export default Page;
17 |
--------------------------------------------------------------------------------
/examples/with-next/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "module": "esnext",
6 | "target": "es5",
7 | "allowJs": true,
8 | "skipLibCheck": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noEmit": true,
12 | "incremental": true,
13 | "esModuleInterop": true,
14 | "moduleResolution": "node",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve",
18 | "plugins": [
19 | {
20 | "name": "next"
21 | }
22 | ],
23 | "composite": false,
24 | "declaration": true,
25 | "declarationMap": true,
26 | "inlineSources": false,
27 | "noUnusedLocals": false,
28 | "noUnusedParameters": false,
29 | "preserveWatchOutput": true,
30 | "baseUrl": "."
31 | },
32 | "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
33 | "exclude": ["node_modules"]
34 | }
35 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@queuedash/root",
3 | "version": "0.0.0",
4 | "description": "A stunning, sleek dashboard for Bull, BullMQ, and Bee-Queue",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/alexbudure/queuedash.git"
8 | },
9 | "bugs": {
10 | "url": "https://github.com/alexbudure/queuedash/issues"
11 | },
12 | "scripts": {
13 | "build": "turbo run build",
14 | "test": "turbo run test",
15 | "lint": "turbo run lint",
16 | "dev": "turbo run dev",
17 | "preinstall": "npx only-allow pnpm",
18 | "release": "pnpm build && changeset publish",
19 | "docker": "docker compose up -d"
20 | },
21 | "packageManager": "pnpm@9.0.2",
22 | "keywords": [
23 | "bull",
24 | "bee-queue",
25 | "queue",
26 | "bullmq",
27 | "dashboard"
28 | ],
29 | "author": "Alex Budure",
30 | "license": "MIT",
31 | "devDependencies": {
32 | "@changesets/changelog-github": "^0.5.1",
33 | "@changesets/cli": "^2.28.1",
34 | "turbo": "^2.5.0"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/api/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: "@typescript-eslint/parser",
4 | parserOptions: {
5 | tsconfigRootDir: __dirname,
6 | project: "./tsconfig.json",
7 | },
8 | ignorePatterns: ["**/node_modules", "**/.cache", "dist", ".eslintrc.js"],
9 | plugins: ["@typescript-eslint"],
10 | extends: [
11 | "eslint:recommended",
12 | "plugin:@typescript-eslint/recommended",
13 | "prettier",
14 | ],
15 | rules: {
16 | "@typescript-eslint/consistent-type-imports": "warn",
17 | "@typescript-eslint/no-unused-vars": "error",
18 | "@typescript-eslint/no-explicit-any": "error",
19 | "@typescript-eslint/ban-ts-comment": "off",
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/packages/api/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @queuedash/api
2 |
3 | ## 3.6.0
4 |
5 | ### Minor Changes
6 |
7 | - [#50](https://github.com/alexbudure/queuedash/pull/50) [`1fd1326`](https://github.com/alexbudure/queuedash/commit/1fd1326ebc22045c78ecdbbceef2856fcce6cbb6) Thanks [@alexbudure](https://github.com/alexbudure)! - - Job scheduler support
8 | - Per-job logs
9 | - Global pause & resume
10 |
11 | ## 3.5.0
12 |
13 | ### Minor Changes
14 |
15 | - [#44](https://github.com/alexbudure/queuedash/pull/44) [`ba73bcf`](https://github.com/alexbudure/queuedash/commit/ba73bcf1afec112e6916a4c6beb132a8d9c7edd4) Thanks [@huv1k](https://github.com/huv1k)! - Add support for hono
16 |
17 | ## 3.4.0
18 |
19 | ### Minor Changes
20 |
21 | - [#40](https://github.com/alexbudure/queuedash/pull/40) [`0ccd37a`](https://github.com/alexbudure/queuedash/commit/0ccd37afe5a3d8b158109d1ec80a02cead1480bf) Thanks [@fukouda](https://github.com/fukouda)! - Add the ability to pass in hook handlers on Fastify
22 |
23 | ## 3.3.0
24 |
25 | ### Minor Changes
26 |
27 | - [#38](https://github.com/alexbudure/queuedash/pull/38) [`00346f4`](https://github.com/alexbudure/queuedash/commit/00346f4c11fdae742dae1981061f044aee66697b) Thanks [@alexbudure](https://github.com/alexbudure)! - Improve React 19 compatibility
28 |
29 | ## 3.2.0
30 |
31 | ### Minor Changes
32 |
33 | - [`8e5c7ac`](https://github.com/alexbudure/queuedash/commit/8e5c7ac06bb13674e32b4cd9b1b7c65913e122af) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix importing API
34 |
35 | ## 3.1.0
36 |
37 | ### Minor Changes
38 |
39 | - [#33](https://github.com/alexbudure/queuedash/pull/33) [`011ad3b`](https://github.com/alexbudure/queuedash/commit/011ad3bca2b50b4568fa7edc7bf314948e84eeb9) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix bundle strategy for API
40 |
41 | ## 3.0.0
42 |
43 | ### Major Changes
44 |
45 | - [#21](https://github.com/alexbudure/queuedash/pull/21) [`6692303`](https://github.com/alexbudure/queuedash/commit/6692303bde835b9934e2ae962e4727357f0d4afe) Thanks [@alexbudure](https://github.com/alexbudure)! - This version upgrades core dependencies to their latest major versions, including Elysia, BullMQ, and tRPC
46 |
47 | ## 2.1.1
48 |
49 | ### Patch Changes
50 |
51 | - [#26](https://github.com/alexbudure/queuedash/pull/26) [`2e0956c`](https://github.com/alexbudure/queuedash/commit/2e0956c586b9f5f3190f169e363f76230d037686) Thanks [@p3drosola](https://github.com/p3drosola)! - Improve UI support for long job ids
52 |
53 | ## 2.1.0
54 |
55 | ### Minor Changes
56 |
57 | - [#23](https://github.com/alexbudure/queuedash/pull/23) [`7a2e3c0`](https://github.com/alexbudure/queuedash/commit/7a2e3c000da0b34c4c3a4dd2471e2e19738d1e6d) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix bull api differences
58 |
59 | ## 2.0.5
60 |
61 | ### Patch Changes
62 |
63 | - [`bc47dd5`](https://github.com/alexbudure/queuedash/commit/bc47dd5de7a5ed32cd82365dc27073282afc45be) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix express bundling with app
64 |
65 | ## 2.0.4
66 |
67 | ### Patch Changes
68 |
69 | - [`0948ec2`](https://github.com/alexbudure/queuedash/commit/0948ec21985d33b3ffbb0ec220664493382579da) Thanks [@alexbudure](https://github.com/alexbudure)! - Move html into each adapter
70 |
71 | ## 2.0.3
72 |
73 | ### Patch Changes
74 |
75 | - [`dee7163`](https://github.com/alexbudure/queuedash/commit/dee71633d33c8bceee9bde84a0b340f899adeaf8) Thanks [@alexbudure](https://github.com/alexbudure)! - Remove unnecessary express app in adapter
76 |
77 | ## 2.0.2
78 |
79 | ### Patch Changes
80 |
81 | - [`6680a6a`](https://github.com/alexbudure/queuedash/commit/6680a6a5ece43fef248fedacb31f8fae2242d2d3) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix the adapters.. again
82 |
83 | ## 2.0.1
84 |
85 | ### Patch Changes
86 |
87 | - [`8d2eadd`](https://github.com/alexbudure/queuedash/commit/8d2eadd9ad547ff2e893662474a228bf340f0728) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix the middlewares for Fastify and Express
88 |
89 | ## 2.0.0
90 |
91 | ### Major Changes
92 |
93 | - [#5](https://github.com/alexbudure/queuedash/pull/5) [`1f794d1`](https://github.com/alexbudure/queuedash/commit/1f794d1679225718dcc670e9c7eb59564fee1bc6) Thanks [@alexbudure](https://github.com/alexbudure)! - Updated all adapters to use a more natural API and added real-time Redis info on the queue detail page
94 |
95 | ***
96 |
97 | ### Breaking changes
98 |
99 | **Express**
100 |
101 | Before:
102 |
103 | ```typescript
104 | createQueueDashExpressMiddleware({
105 | app,
106 | baseUrl: "/queuedash",
107 | ctx: {
108 | queues: [
109 | {
110 | queue: new Bull("report-queue"),
111 | displayName: "Reports",
112 | type: "bull" as const,
113 | },
114 | ],
115 | },
116 | });
117 | ```
118 |
119 | After:
120 |
121 | ```typescript
122 | app.use(
123 | "/queuedash",
124 | createQueueDashExpressMiddleware({
125 | ctx: {
126 | queues: [
127 | {
128 | queue: new Bull("report-queue"),
129 | displayName: "Reports",
130 | type: "bull" as const,
131 | },
132 | ],
133 | },
134 | }),
135 | );
136 | ```
137 |
138 | **Fastify**
139 |
140 | Before:
141 |
142 | ```typescript
143 | createQueueDashFastifyMiddleware({
144 | server,
145 | baseUrl: "/queuedash",
146 | ctx: {
147 | queues: [
148 | {
149 | queue: new Bull("report-queue"),
150 | displayName: "Reports",
151 | type: "bull" as const,
152 | },
153 | ],
154 | },
155 | });
156 | ```
157 |
158 | After:
159 |
160 | ```typescript
161 | server.register(fastifyQueueDashPlugin, {
162 | baseUrl: "/queuedash",
163 | ctx: {
164 | queues: [
165 | {
166 | queue: new Bull("report-queue"),
167 | displayName: "Reports",
168 | type: "bull" as const,
169 | },
170 | ],
171 | },
172 | });
173 | ```
174 |
175 | ## 1.2.1
176 |
177 | ### Patch Changes
178 |
179 | - [#12](https://github.com/alexbudure/queuedash/pull/12) [`d79c8ff`](https://github.com/alexbudure/queuedash/commit/d79c8ffe34ae36c74d0663dd2e29e6c93327bf8c) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix Elysia plugin
180 |
181 | ## 1.2.0
182 |
183 | ### Minor Changes
184 |
185 | - [`9aaec9a`](https://github.com/alexbudure/queuedash/commit/9aaec9a21c091680cb30a67e9322eedd3e16dbe8) Thanks [@alexbudure](https://github.com/alexbudure)! - Support for Elysia
186 |
187 | ## 1.1.0
188 |
189 | ### Minor Changes
190 |
191 | - [#7](https://github.com/alexbudure/queuedash/pull/7) [`885aee3`](https://github.com/alexbudure/queuedash/commit/885aee3cecac687d05f5b18cd1855fcb5522f899) Thanks [@alexbudure](https://github.com/alexbudure)! - Add support for prioritized jobs
192 |
193 | ## 1.0.1
194 |
195 | ### Patch Changes
196 |
197 | - [#3](https://github.com/alexbudure/queuedash/pull/3) [`a385f9f`](https://github.com/alexbudure/queuedash/commit/a385f9f9e76df4cea8e69d7e218b65915acef3bf) Thanks [@alexbudure](https://github.com/alexbudure)! - Tighten adapter types to work with NestJS
198 |
199 | ## 1.0.0
200 |
201 | ### Major Changes
202 |
203 | - [#1](https://github.com/alexbudure/queuedash/pull/1) [`c96b93d`](https://github.com/alexbudure/queuedash/commit/c96b93d9659bbb34248ab377e6659ebfb1fc3dd8) Thanks [@alexbudure](https://github.com/alexbudure)! - QueueDash v1 🎉
204 |
205 | - 😍 Simple, clean, and compact UI
206 | - 🧙 Add jobs to your queue with ease
207 | - 🪄 Retry, remove, and more convenient actions for your jobs
208 | - 📊 Stats for job counts, job durations, and job wait times
209 | - ✨ Top-level overview page of all queues
210 | - 🔋 Integrates with Next.js, Express.js, and Fastify
211 | - ⚡️ Compatible with Bull, BullMQ, and Bee-Queue
212 |
--------------------------------------------------------------------------------
/packages/api/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | A stunning, sleek dashboard for Bull, BullMQ, and Bee-Queue.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ## Features
21 |
22 | - 😍 Simple, clean, and compact UI
23 | - 🧙 Add jobs to your queue with ease
24 | - 🪄 Retry, remove, and more convenient actions for your jobs
25 | - 📊 Stats for job counts, job durations, and job wait times
26 | - ✨ Top-level overview page of all queues
27 | - 🔋 Integrates with Next.js, Express.js, and Fastify
28 | - ⚡️ Compatible with Bull, BullMQ, and Bee-Queue
29 |
30 | ## Getting Started
31 |
32 | ### Express
33 |
34 | `pnpm install @queuedash/api`
35 |
36 | ```typescript
37 | import express from "express";
38 | import Bull from "bull";
39 | import { createQueueDashExpressMiddleware } from "@queuedash/api";
40 |
41 | const app = express();
42 |
43 | const reportQueue = new Bull("report-queue");
44 |
45 | app.use(
46 | "/queuedash",
47 | createQueueDashExpressMiddleware({
48 | ctx: {
49 | queues: [
50 | {
51 | queue: reportQueue,
52 | displayName: "Reports",
53 | type: "bull" as const,
54 | },
55 | ],
56 | },
57 | })
58 | );
59 |
60 | app.listen(3000, () => {
61 | console.log("Listening on port 3000");
62 | console.log("Visit http://localhost:3000/queuedash");
63 | });
64 | ```
65 |
66 | ### Next.js
67 |
68 | `pnpm install @queuedash/api @queuedash/ui`
69 |
70 | ```typescript jsx
71 | // pages/admin/queuedash/[[...slug]].tsx
72 | import { QueueDashApp } from "@queuedash/ui";
73 |
74 | function getBaseUrl() {
75 | if (process.env.VERCEL_URL) {
76 | return `https://${process.env.VERCEL_URL}/api/queuedash`;
77 | }
78 |
79 | return `http://localhost:${process.env.PORT ?? 3000}/api/queuedash`;
80 | }
81 |
82 | const QueueDashPages = () => {
83 | return ;
84 | };
85 |
86 | export default QueueDashPages;
87 |
88 | // pages/api/queuedash/[trpc].ts
89 | import * as trpcNext from "@trpc/server/adapters/next";
90 | import { appRouter } from "@queuedash/api";
91 |
92 | const reportQueue = new Bull("report-queue");
93 |
94 | export default trpcNext.createNextApiHandler({
95 | router: appRouter,
96 | batching: {
97 | enabled: true,
98 | },
99 | createContext: () => ({
100 | queues: [
101 | {
102 | queue: reportQueue,
103 | displayName: "Reports",
104 | type: "bull" as const,
105 | },
106 | ],
107 | }),
108 | });
109 | ```
110 |
111 | See the [./examples](./examples) folder for more.
112 |
113 | ---
114 |
115 | ## API Reference
116 |
117 | ### `createQueueDash<*>Middleware`
118 |
119 | ```typescript
120 | type QueueDashMiddlewareOptions = {
121 | app: express.Application | FastifyInstance; // Express or Fastify app
122 | baseUrl: string; // Base path for the API and UI
123 | ctx: QueueDashContext; // Context for the UI
124 | };
125 |
126 | type QueueDashContext = {
127 | queues: QueueDashQueue[]; // Array of queues to display
128 | };
129 |
130 | type QueueDashQueue = {
131 | queue: Bull.Queue | BullMQ.Queue | BeeQueue; // Queue instance
132 | displayName: string; // Display name for the queue
133 | type: "bull" | "bullmq" | "bee"; // Queue type
134 | };
135 | ```
136 |
137 | ### ``
138 |
139 | ```typescript jsx
140 | type QueueDashAppProps = {
141 | apiUrl: string; // URL to the API endpoint
142 | basename: string; // Base path for the app
143 | };
144 | ```
145 |
146 | ## Roadmap
147 |
148 | - Supports Celery and other queueing systems
149 | - Command+K bar and shortcuts
150 | - Ability to whitelabel the UI
151 |
152 | ## Pro Version
153 |
154 | Right now, QueueDash simply taps into your Redis instance, making it very easy to set up, but also limited in functionality.
155 |
156 | I'm thinking about building a free-to-host version on top of this which will require external services (db, auth, etc.), but it will make the following features possible:
157 |
158 | - Alerts and notifications
159 | - Quick search and filtering
160 | - Queue trends and analytics
161 | - Invite team members
162 |
163 | If you're interested in this version, please let me know!
164 |
165 | ## Acknowledgements
166 |
167 | QueueDash was inspired by some great open source projects. Here's a few of them:
168 |
169 | - [bull-board](https://github.com/vcapretz/bull-board)
170 | - [bull-monitor](https://github.com/s-r-x/bull-monitor)
171 | - [bull-arena](https://github.com/bee-queue/arena)
172 |
--------------------------------------------------------------------------------
/packages/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@queuedash/api",
3 | "version": "3.6.0",
4 | "description": "A stunning, sleek dashboard for Bull, BullMQ, and Bee-Queue",
5 | "scripts": {
6 | "build": "tsc && vite build",
7 | "dev": "pnpm run build --watch",
8 | "test": "pnpm run test:bull && pnpm run test:bullmq && pnpm run test:bee",
9 | "test:bull": "QUEUE_TYPE=bull vitest run",
10 | "test:bullmq": "QUEUE_TYPE=bullmq vitest run",
11 | "test:bee": "QUEUE_TYPE=bee vitest run",
12 | "lint": "eslint ./ --fix"
13 | },
14 | "main": "./dist/main.js",
15 | "module": "./dist/main.mjs",
16 | "types": "./dist/src/main.d.ts",
17 | "files": [
18 | "dist"
19 | ],
20 | "keywords": [
21 | "bull",
22 | "bee-queue",
23 | "queue",
24 | "bullmq",
25 | "dashboard"
26 | ],
27 | "dependencies": {
28 | "@trpc/server": "^11.0.2",
29 | "redis": "^4.7.0",
30 | "redis-info": "^3.1.0",
31 | "zod": "^3.24.2"
32 | },
33 | "devDependencies": {
34 | "@elysiajs/trpc": "^1.1.0",
35 | "@faker-js/faker": "^9.6.0",
36 | "@hono/trpc-server": "^0.3.4",
37 | "@rollup/plugin-typescript": "^12.1.2",
38 | "@types/express": "^5.0.1",
39 | "@types/express-serve-static-core": "^5.0.6",
40 | "@types/node": "^22.14.0",
41 | "@types/redis-info": "^3.0.3",
42 | "@typescript-eslint/eslint-plugin": "^8.29.0",
43 | "@typescript-eslint/parser": "^8.29.0",
44 | "bee-queue": "^1.7.1",
45 | "bull": "^4.16.5",
46 | "bullmq": "^5.47.2",
47 | "elysia": "^1.2.25",
48 | "eslint": "^9.24.0",
49 | "eslint-config-prettier": "^10.1.1",
50 | "express": "^5.1.0",
51 | "fastify": "^5.2.2",
52 | "hono": "^4.7.5",
53 | "prettier": "^3.5.3",
54 | "rollup-plugin-typescript-paths": "^1.5.0",
55 | "typescript": "^5.8.3",
56 | "vite": "^5.4.19",
57 | "vitest": "^3.1.1"
58 | },
59 | "license": "MIT"
60 | }
61 |
--------------------------------------------------------------------------------
/packages/api/src/adapters/elysia.ts:
--------------------------------------------------------------------------------
1 | import { Elysia } from "elysia";
2 | import { trpc } from "@elysiajs/trpc";
3 | import type { Context } from "../routers/_app";
4 | import { appRouter } from "../routers/_app";
5 | import { createQueuedashHtml } from "./utils";
6 |
7 | export function queuedash({
8 | baseUrl,
9 | ctx,
10 | }: {
11 | ctx: Context;
12 | baseUrl: string;
13 | }): Elysia {
14 | return new Elysia({
15 | name: "queuedash",
16 | })
17 | .use(
18 | trpc(appRouter, {
19 | endpoint: `${baseUrl}/trpc`,
20 | createContext: (params) => {
21 | return { ...params, ...ctx };
22 | },
23 | }),
24 | )
25 | .get(
26 | baseUrl,
27 | async () =>
28 | new Response(createQueuedashHtml(baseUrl), {
29 | headers: { "Content-Type": "text/html; charset=utf8" },
30 | }),
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/packages/api/src/adapters/express.ts:
--------------------------------------------------------------------------------
1 | import type { Handler } from "express";
2 | import type { Context } from "../routers/_app";
3 | import { appRouter } from "../routers/_app";
4 | import * as trpcNodeHttp from "@trpc/server/adapters/node-http";
5 | import { createQueuedashHtml } from "./utils";
6 |
7 | export function createQueueDashExpressMiddleware({
8 | ctx,
9 | }: {
10 | ctx: Context;
11 | }): Handler {
12 | return async (req, res, next) => {
13 | if (req.path.startsWith("/trpc")) {
14 | const endpoint = req.path.replace("/trpc", "").slice(1);
15 | await trpcNodeHttp.nodeHTTPRequestHandler({
16 | router: appRouter,
17 | createContext: () => ctx,
18 | req,
19 | res,
20 | path: endpoint,
21 | });
22 | } else {
23 | res.type("text/html").send(createQueuedashHtml(req.baseUrl));
24 | next();
25 | }
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/packages/api/src/adapters/fastify.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | FastifyInstance,
3 | onRequestHookHandler,
4 | preHandlerHookHandler,
5 | } from "fastify";
6 | import type { Context } from "../trpc";
7 | import { appRouter } from "../routers/_app";
8 | import * as trpcFastify from "@trpc/server/adapters/fastify";
9 | import { createQueuedashHtml } from "./utils";
10 |
11 | export type FastifyQueueDashHooksOptions = Partial<{
12 | onRequest?: onRequestHookHandler;
13 | preHandler?: preHandlerHookHandler;
14 | }>;
15 |
16 | export function fastifyQueueDashPlugin(
17 | fastify: FastifyInstance,
18 | {
19 | baseUrl,
20 | ctx,
21 | uiHooks,
22 | }: {
23 | ctx: Context;
24 | baseUrl: string;
25 | uiHooks?: FastifyQueueDashHooksOptions;
26 | },
27 | done: () => void
28 | ): void {
29 | fastify.get(`${baseUrl}/*`, { ...uiHooks }, (_, res) => {
30 | res.type("text/html").send(createQueuedashHtml(baseUrl));
31 | });
32 | fastify.get(baseUrl, { ...uiHooks }, (_, res) => {
33 | res.type("text/html").send(createQueuedashHtml(baseUrl));
34 | });
35 | fastify.register(trpcFastify.fastifyTRPCPlugin, {
36 | prefix: `${baseUrl}/trpc`,
37 | trpcOptions: { router: appRouter, createContext: () => ctx },
38 | });
39 |
40 | done();
41 | }
42 |
--------------------------------------------------------------------------------
/packages/api/src/adapters/hono.ts:
--------------------------------------------------------------------------------
1 | import { Hono } from "hono";
2 | import { trpcServer } from "@hono/trpc-server";
3 | import { appRouter } from "../routers/_app";
4 | import type { Context } from "../routers/_app";
5 | import { createQueuedashHtml } from "./utils";
6 |
7 | export const createHonoAdapter = ({
8 | baseUrl,
9 | ctx,
10 | }: {
11 | baseUrl: string;
12 | ctx: Context;
13 | }) =>
14 | new Hono()
15 | .use(
16 | "/trpc/*",
17 | trpcServer({
18 | endpoint: `${baseUrl}/trpc`,
19 | router: appRouter,
20 | createContext: () => ctx,
21 | }),
22 | )
23 | .get("*", (c) => {
24 | return c.html(createQueuedashHtml(baseUrl));
25 | });
26 |
--------------------------------------------------------------------------------
/packages/api/src/adapters/utils.ts:
--------------------------------------------------------------------------------
1 | import { version } from "../../package.json";
2 |
3 | export const createQueuedashHtml = (
4 | baseUrl: string
5 | ) => /* HTML */ `
6 |
7 |
8 |
9 |
10 | QueueDash App
11 |
12 |
13 |
14 |
20 |
24 |
28 |
29 | `;
30 |
--------------------------------------------------------------------------------
/packages/api/src/main.ts:
--------------------------------------------------------------------------------
1 | export * from "./routers/_app";
2 | export * from "./adapters/express";
3 | export * from "./adapters/fastify";
4 | export * from "./adapters/elysia";
5 | export { createHonoAdapter } from "./adapters/hono";
6 |
--------------------------------------------------------------------------------
/packages/api/src/routers/_app.ts:
--------------------------------------------------------------------------------
1 | import { jobRouter } from "./job";
2 | import { queueRouter } from "./queue";
3 | export type { Context } from "../trpc";
4 | import { router } from "../trpc";
5 | import { schedulerRouter } from "./scheduler";
6 |
7 | export const appRouter = router({
8 | job: jobRouter,
9 | queue: queueRouter,
10 | scheduler: schedulerRouter,
11 | });
12 |
13 | export type AppRouter = typeof appRouter;
14 |
--------------------------------------------------------------------------------
/packages/api/src/routers/job.ts:
--------------------------------------------------------------------------------
1 | import { procedure, router } from "../trpc";
2 | import type Bull from "bull";
3 | import type BullMQ from "bullmq";
4 | import { z } from "zod";
5 | import { TRPCError } from "@trpc/server";
6 | import { findQueueInCtxOrFail, formatJob } from "../utils/global.utils";
7 | import type BeeQueue from "bee-queue";
8 | import { createClient } from "redis";
9 | const generateJobMutationProcedure = (
10 | action: (
11 | job: Bull.Job | BullMQ.Job | BeeQueue.Job>,
12 | ) => Promise | void,
13 | ) => {
14 | return procedure
15 | .input(
16 | z.object({
17 | queueName: z.string(),
18 | jobId: z.string(),
19 | }),
20 | )
21 | .mutation(async ({ input: { jobId, queueName }, ctx: { queues } }) => {
22 | const queueInCtx = findQueueInCtxOrFail({
23 | queues,
24 | queueName,
25 | });
26 |
27 | const job = await queueInCtx.queue.getJob(jobId);
28 |
29 | if (!job) {
30 | throw new TRPCError({
31 | code: "BAD_REQUEST",
32 | });
33 | }
34 |
35 | try {
36 | await action(job);
37 | } catch (e) {
38 | if (e instanceof TRPCError) {
39 | throw e;
40 | } else {
41 | throw new TRPCError({
42 | code: "INTERNAL_SERVER_ERROR",
43 | message: e instanceof Error ? e.message : undefined,
44 | });
45 | }
46 | }
47 |
48 | return formatJob({ job, queueInCtx });
49 | });
50 | };
51 |
52 | export const jobRouter = router({
53 | retry: generateJobMutationProcedure(async (job) => {
54 | if ("isFailed" in job) {
55 | if (await job.isFailed()) {
56 | await job.retry();
57 | } else {
58 | throw new TRPCError({
59 | code: "BAD_REQUEST",
60 | });
61 | }
62 | } else {
63 | throw new TRPCError({
64 | code: "BAD_REQUEST",
65 | message: "Bee-Queue does not support retrying jobs",
66 | });
67 | }
68 | }),
69 | discard: generateJobMutationProcedure((job) => {
70 | if ("discard" in job) {
71 | return job.discard();
72 | } else {
73 | return job.remove();
74 | }
75 | }),
76 | rerun: procedure
77 | .input(
78 | z.object({
79 | queueName: z.string(),
80 | jobId: z.string(),
81 | }),
82 | )
83 | .mutation(async ({ input: { jobId, queueName }, ctx: { queues } }) => {
84 | const queueInCtx = findQueueInCtxOrFail({
85 | queues,
86 | queueName,
87 | });
88 |
89 | const job = await queueInCtx.queue.getJob(jobId);
90 |
91 | if (!job) {
92 | throw new TRPCError({
93 | code: "BAD_REQUEST",
94 | });
95 | }
96 |
97 | try {
98 | if (queueInCtx.type === "bee") {
99 | await queueInCtx.queue.createJob(job.data).save();
100 | } else if (queueInCtx.type === "bullmq") {
101 | await queueInCtx.queue.add(job.name, job.data, {});
102 | } else {
103 | await queueInCtx.queue.add(job.data, {});
104 | }
105 | } catch (e) {
106 | if (e instanceof TRPCError) {
107 | throw e;
108 | } else {
109 | throw new TRPCError({
110 | code: "INTERNAL_SERVER_ERROR",
111 | message: e instanceof Error ? e.message : undefined,
112 | });
113 | }
114 | }
115 |
116 | return formatJob({ job, queueInCtx });
117 | }),
118 | promote: generateJobMutationProcedure((job) => {
119 | if ("promote" in job) {
120 | return job.promote();
121 | } else {
122 | throw new TRPCError({
123 | code: "BAD_REQUEST",
124 | message: "Bee-Queue does not support promoting jobs",
125 | });
126 | }
127 | }),
128 | remove: generateJobMutationProcedure((job) => job.remove()),
129 | bulkRemove: procedure
130 | .input(
131 | z.object({
132 | queueName: z.string(),
133 | jobIds: z.array(z.string()),
134 | }),
135 | )
136 | .mutation(async ({ input: { jobIds, queueName }, ctx: { queues } }) => {
137 | const queueInCtx = findQueueInCtxOrFail({
138 | queues,
139 | queueName,
140 | });
141 |
142 | try {
143 | const jobs = await Promise.all(
144 | jobIds.map(async (jobId) => {
145 | const job = await queueInCtx.queue.getJob(jobId);
146 |
147 | if (!job) {
148 | throw new TRPCError({
149 | code: "BAD_REQUEST",
150 | });
151 | }
152 | await job.remove();
153 |
154 | return job;
155 | }),
156 | );
157 | return jobs.map((job) => formatJob({ job, queueInCtx }));
158 | } catch (e) {
159 | if (e instanceof TRPCError) {
160 | throw e;
161 | } else {
162 | throw new TRPCError({
163 | code: "INTERNAL_SERVER_ERROR",
164 | message: e instanceof Error ? e.message : undefined,
165 | });
166 | }
167 | }
168 | }),
169 | logs: procedure
170 | .input(
171 | z.object({
172 | queueName: z.string(),
173 | jobId: z.string(),
174 | }),
175 | )
176 | .query(async ({ input: { queueName, jobId }, ctx: { queues } }) => {
177 | const queueInCtx = findQueueInCtxOrFail({
178 | queues,
179 | queueName,
180 | });
181 | if (queueInCtx.type !== "bullmq") {
182 | return null;
183 | }
184 | const { logs } = await queueInCtx.queue.getJobLogs(jobId);
185 |
186 | return logs;
187 | }),
188 | list: procedure
189 | .input(
190 | z.object({
191 | queueName: z.string(),
192 | cursor: z.number().min(0).optional().default(0),
193 | limit: z.number().min(0).max(100),
194 | status: z.enum([
195 | "completed",
196 | "failed",
197 | "delayed",
198 | "active",
199 | "prioritized",
200 | "waiting",
201 | "paused",
202 | ] as const),
203 | }),
204 | )
205 | .query(
206 | async ({
207 | input: { queueName, status, limit, cursor },
208 | ctx: { queues },
209 | }) => {
210 | const queueInCtx = findQueueInCtxOrFail({
211 | queues,
212 | queueName,
213 | });
214 |
215 | if (queueInCtx.type === "bee") {
216 | try {
217 | const normalizedStatus =
218 | status === "completed" ? "succeeded" : status;
219 | const client = createClient(queueInCtx.queue.settings.redis);
220 |
221 | const [jobs] = await Promise.all([
222 | queueInCtx.queue.getJobs(normalizedStatus, {
223 | start: cursor,
224 | end: cursor + limit - 1,
225 | }),
226 | client.connect(),
227 | ]);
228 |
229 | const totalCount = await client.sCard(
230 | `${queueInCtx.queue.settings.keyPrefix}${normalizedStatus}`,
231 | );
232 |
233 | await client.disconnect();
234 |
235 | const hasNextPage = jobs.length > 0 && cursor + limit < totalCount;
236 |
237 | return {
238 | totalCount,
239 | numOfPages: Math.ceil(totalCount / limit),
240 | nextCursor: hasNextPage ? cursor + limit : undefined,
241 | jobs: jobs.map((job) => formatJob({ job, queueInCtx })),
242 | };
243 | } catch (e) {
244 | throw new TRPCError({
245 | code: "INTERNAL_SERVER_ERROR",
246 | message: e instanceof Error ? e.message : undefined,
247 | });
248 | }
249 | }
250 |
251 | try {
252 | const isBullMq = queueInCtx.type === "bullmq";
253 |
254 | const [jobs, totalCountWithWrongType] = await Promise.all([
255 | status === "prioritized" && isBullMq
256 | ? queueInCtx.queue.getJobs([status], cursor, cursor + limit - 1)
257 | : status === "prioritized"
258 | ? []
259 | : queueInCtx.queue.getJobs(
260 | [status],
261 | cursor,
262 | cursor + limit - 1,
263 | ),
264 | status === "prioritized" && isBullMq
265 | ? queueInCtx.queue.getJobCountByTypes(status)
266 | : status === "prioritized"
267 | ? 0
268 | : queueInCtx.queue.getJobCountByTypes(status),
269 | ]);
270 | const totalCount = totalCountWithWrongType as unknown as number;
271 |
272 | const hasNextPage = jobs.length > 0 && cursor + limit < totalCount;
273 |
274 | return {
275 | totalCount,
276 | numOfPages: Math.ceil(totalCount / limit),
277 | nextCursor: hasNextPage ? cursor + limit : undefined,
278 | jobs: jobs.map((job) => formatJob({ job, queueInCtx })),
279 | };
280 | } catch (e) {
281 | throw new TRPCError({
282 | code: "INTERNAL_SERVER_ERROR",
283 | message: e instanceof Error ? e.message : undefined,
284 | });
285 | }
286 | },
287 | ),
288 | });
289 |
--------------------------------------------------------------------------------
/packages/api/src/routers/queue.ts:
--------------------------------------------------------------------------------
1 | import { procedure, router } from "../trpc";
2 | import { z } from "zod";
3 | import { findQueueInCtxOrFail } from "../utils/global.utils";
4 | import type Bull from "bull";
5 | import type BullMQ from "bullmq";
6 | import { TRPCError } from "@trpc/server";
7 | import type BeeQueue from "bee-queue";
8 | import type { RedisInfo } from "redis-info";
9 | import { parse } from "redis-info";
10 |
11 | const generateQueueMutationProcedure = (
12 | action: (queue: Bull.Queue | BullMQ.Queue | BeeQueue) => void,
13 | ) => {
14 | return procedure
15 | .input(
16 | z.object({
17 | queueName: z.string(),
18 | }),
19 | )
20 | .mutation(async ({ input: { queueName }, ctx: { queues } }) => {
21 | const queueInCtx = findQueueInCtxOrFail({
22 | queues,
23 | queueName,
24 | });
25 |
26 | try {
27 | await action(queueInCtx.queue);
28 | } catch (e) {
29 | if (e instanceof TRPCError) {
30 | throw e;
31 | } else {
32 | throw new TRPCError({
33 | code: "INTERNAL_SERVER_ERROR",
34 | message: e instanceof Error ? e.message : undefined,
35 | });
36 | }
37 | }
38 |
39 | return {
40 | name: queueName,
41 | };
42 | });
43 | };
44 |
45 | export const queueRouter = router({
46 | clean: procedure
47 | .input(
48 | z.object({
49 | queueName: z.string(),
50 | status: z.enum([
51 | "completed",
52 | "failed",
53 | "delayed",
54 | "active",
55 | "waiting",
56 | "prioritized",
57 | "paused",
58 | ] as const),
59 | }),
60 | )
61 | .mutation(async ({ input: { queueName, status }, ctx: { queues } }) => {
62 | const queueInCtx = findQueueInCtxOrFail({
63 | queues,
64 | queueName,
65 | });
66 |
67 | if (queueInCtx.type === "bee") {
68 | throw new TRPCError({
69 | code: "BAD_REQUEST",
70 | message: "Cannot clean Bee-Queue queues",
71 | });
72 | }
73 |
74 | try {
75 | if (queueInCtx.type === "bullmq") {
76 | await queueInCtx.queue.clean(
77 | 0,
78 | 0,
79 | status === "waiting" ? "wait" : status,
80 | );
81 | } else if (status !== "prioritized") {
82 | await queueInCtx.queue.clean(
83 | 0,
84 | status === "waiting" ? "wait" : status,
85 | );
86 | }
87 | } catch (e) {
88 | throw new TRPCError({
89 | code: "INTERNAL_SERVER_ERROR",
90 | message: e instanceof Error ? e.message : undefined,
91 | });
92 | }
93 |
94 | return {
95 | name: queueName,
96 | };
97 | }),
98 | empty: generateQueueMutationProcedure((queue) => {
99 | if ("empty" in queue) {
100 | return queue.empty();
101 | } else if ("drain" in queue) {
102 | return queue.drain();
103 | } else {
104 | throw new TRPCError({
105 | code: "BAD_REQUEST",
106 | message: "Cannot empty Bee-Queue queues",
107 | });
108 | }
109 | }),
110 | pause: generateQueueMutationProcedure((queue) => {
111 | if ("pause" in queue) {
112 | return queue.pause();
113 | } else {
114 | throw new TRPCError({
115 | code: "BAD_REQUEST",
116 | message: "Cannot pause Bee-Queue queues",
117 | });
118 | }
119 | }),
120 | pauseAll: procedure.mutation(async ({ ctx: { queues } }) => {
121 | await Promise.all([
122 | queues.map((queue) => {
123 | if ("pause" in queue.queue) {
124 | return queue.queue.pause();
125 | } else {
126 | return null;
127 | }
128 | }),
129 | ]);
130 | return "ok";
131 | }),
132 | resume: generateQueueMutationProcedure((queue) => {
133 | if ("resume" in queue) {
134 | return queue.resume();
135 | } else {
136 | throw new TRPCError({
137 | code: "BAD_REQUEST",
138 | message: "Cannot resume Bee-Queue queues",
139 | });
140 | }
141 | }),
142 | resumeAll: procedure.mutation(async ({ ctx: { queues } }) => {
143 | await Promise.all([
144 | queues.map((queue) => {
145 | if ("resume" in queue.queue) {
146 | return queue.queue.resume();
147 | } else {
148 | return null;
149 | }
150 | }),
151 | ]);
152 | return "ok";
153 | }),
154 | addJob: procedure
155 | .input(
156 | z.object({
157 | queueName: z.string(),
158 | data: z.object({}).passthrough(),
159 | }),
160 | )
161 | .mutation(async ({ input: { queueName, data }, ctx: { queues } }) => {
162 | const queueInCtx = findQueueInCtxOrFail({
163 | queues,
164 | queueName,
165 | });
166 |
167 | try {
168 | if ("add" in queueInCtx.queue) {
169 | await queueInCtx.queue.add(data, {});
170 | } else {
171 | await queueInCtx.queue.createJob(data).save();
172 | }
173 | } catch (e) {
174 | throw new TRPCError({
175 | code: "INTERNAL_SERVER_ERROR",
176 | message: e instanceof Error ? e.message : undefined,
177 | });
178 | }
179 |
180 | return {
181 | name: queueName,
182 | };
183 | }),
184 |
185 | byName: procedure
186 | .input(
187 | z.object({
188 | queueName: z.string(),
189 | }),
190 | )
191 | .query(async ({ input: { queueName }, ctx: { queues } }) => {
192 | const queueInCtx = findQueueInCtxOrFail({
193 | queues,
194 | queueName,
195 | });
196 |
197 | const isBee = queueInCtx.type === "bee";
198 |
199 | try {
200 | const [counts, isPaused] = await Promise.all([
201 | isBee
202 | ? queueInCtx.queue.checkHealth()
203 | : queueInCtx.queue.getJobCounts(),
204 | isBee ? queueInCtx.queue.paused : queueInCtx.queue.isPaused(),
205 | ]);
206 |
207 | const info: RedisInfo & {
208 | maxclients: string;
209 | } = isBee
210 | ? // @ts-expect-error Bee-Queue does not have a client property
211 | queueInCtx.queue.client.server_info
212 | : queueInCtx.type === "bullmq"
213 | ? parse(await (await queueInCtx.queue.client).info())
214 | : parse(await queueInCtx.queue.client.info());
215 |
216 | return {
217 | displayName: queueInCtx.displayName,
218 | name: queueInCtx.queue.name,
219 | paused: isPaused,
220 | type: queueInCtx.type,
221 | counts: {
222 | active: counts.active,
223 | completed:
224 | "completed" in counts ? counts.completed : counts.succeeded,
225 | delayed: counts.delayed,
226 | failed: counts.failed,
227 | ...("prioritized" in counts
228 | ? { prioritized: counts.prioritized }
229 | : {}),
230 | waiting: counts.waiting,
231 | paused: "paused" in counts ? counts.paused : 0,
232 | },
233 | client: {
234 | usedMemoryPercentage:
235 | Number(info.used_memory) / Number(info.total_system_memory),
236 | usedMemoryHuman: info.used_memory_human,
237 | totalMemoryHuman: info.total_system_memory_human,
238 | uptimeInSeconds: Number(info.uptime_in_seconds),
239 | connectedClients: Number(info.connected_clients),
240 | blockedClients: Number(info.blocked_clients),
241 | maxClients: info.maxclients ? Number(info.maxclients) : 0,
242 | version: info.redis_version,
243 | },
244 | };
245 | } catch (e) {
246 | throw new TRPCError({
247 | code: "INTERNAL_SERVER_ERROR",
248 | message: e instanceof Error ? e.message : undefined,
249 | });
250 | }
251 | }),
252 | list: procedure.query(async ({ ctx: { queues } }) => {
253 | return queues.map((queue) => {
254 | return {
255 | displayName: queue.displayName,
256 | name: queue.queue.name,
257 | };
258 | });
259 | }),
260 | });
261 |
--------------------------------------------------------------------------------
/packages/api/src/routers/scheduler.ts:
--------------------------------------------------------------------------------
1 | import { procedure, router } from "../trpc";
2 | import { z } from "zod";
3 | import { TRPCError } from "@trpc/server";
4 | import type { QueueDashScheduler } from "../utils/global.utils";
5 | import { findQueueInCtxOrFail } from "../utils/global.utils";
6 |
7 | export const schedulerRouter = router({
8 | list: procedure
9 | .input(
10 | z.object({
11 | queueName: z.string(),
12 | }),
13 | )
14 | .query(
15 | async ({
16 | input: { queueName },
17 | ctx: { queues },
18 | }): Promise => {
19 | const queueInCtx = findQueueInCtxOrFail({ queues, queueName });
20 |
21 | if (queueInCtx.type !== "bullmq") {
22 | throw new TRPCError({
23 | code: "BAD_REQUEST",
24 | message: "Scheduled jobs are only supported for BullMQ queues",
25 | });
26 | }
27 |
28 | return queueInCtx.queue.getJobSchedulers();
29 | },
30 | ),
31 |
32 | add: procedure
33 | .input(
34 | z.object({
35 | queueName: z.string(),
36 | jobName: z.string(),
37 | data: z.record(z.any()),
38 | pattern: z.string().optional(),
39 | every: z.number().optional(),
40 | tz: z.string().optional(),
41 | }),
42 | )
43 | .mutation(async ({ input, ctx: { queues } }) => {
44 | const { queueName, jobName, data, pattern, every, tz } = input;
45 |
46 | if (!pattern && !every) {
47 | throw new TRPCError({
48 | code: "BAD_REQUEST",
49 | message: "You must provide either `cron` or `every`",
50 | });
51 | }
52 |
53 | const queueInCtx = findQueueInCtxOrFail({ queues, queueName });
54 |
55 | if (queueInCtx.type !== "bullmq") {
56 | throw new TRPCError({
57 | code: "BAD_REQUEST",
58 | message: "Scheduled jobs are only supported for BullMQ queues",
59 | });
60 | }
61 |
62 | await queueInCtx.queue.add(jobName, data, {
63 | repeat: { pattern, every, tz },
64 | });
65 |
66 | return { success: true };
67 | }),
68 |
69 | remove: procedure
70 | .input(
71 | z.object({
72 | queueName: z.string(),
73 | jobSchedulerId: z.string(),
74 | }),
75 | )
76 | .mutation(
77 | async ({ input: { queueName, jobSchedulerId }, ctx: { queues } }) => {
78 | const queueInCtx = findQueueInCtxOrFail({ queues, queueName });
79 |
80 | if (queueInCtx.type !== "bullmq") {
81 | throw new TRPCError({
82 | code: "BAD_REQUEST",
83 | message: "Scheduled jobs are only supported for BullMQ queues",
84 | });
85 | }
86 |
87 | try {
88 | await queueInCtx.queue.removeJobScheduler(jobSchedulerId);
89 | return { success: true };
90 | } catch (e) {
91 | throw new TRPCError({
92 | code: "INTERNAL_SERVER_ERROR",
93 | message: e instanceof Error ? e.message : undefined,
94 | });
95 | }
96 | },
97 | ),
98 |
99 | bulkRemove: procedure
100 | .input(
101 | z.object({
102 | queueName: z.string(),
103 | jobSchedulerIds: z.array(z.string()),
104 | }),
105 | )
106 | .mutation(
107 | async ({
108 | input: { jobSchedulerIds, queueName },
109 | ctx: { queues },
110 | }): Promise => {
111 | const queueInCtx = findQueueInCtxOrFail({
112 | queues,
113 | queueName,
114 | });
115 |
116 | if (queueInCtx.type !== "bullmq") {
117 | throw new TRPCError({
118 | code: "BAD_REQUEST",
119 | message: "Scheduled jobs are only supported for BullMQ queues",
120 | });
121 | }
122 |
123 | try {
124 | return Promise.all(
125 | jobSchedulerIds.map(async (jobSchedulerId) => {
126 | const scheduler =
127 | await queueInCtx.queue.getJobScheduler(jobSchedulerId);
128 |
129 | if (!scheduler) {
130 | throw new TRPCError({
131 | code: "BAD_REQUEST",
132 | });
133 | }
134 | await queueInCtx.queue.removeJobScheduler(jobSchedulerId);
135 |
136 | return scheduler;
137 | }),
138 | );
139 | } catch (e) {
140 | if (e instanceof TRPCError) {
141 | throw e;
142 | } else {
143 | throw new TRPCError({
144 | code: "INTERNAL_SERVER_ERROR",
145 | message: e instanceof Error ? e.message : undefined,
146 | });
147 | }
148 | }
149 | },
150 | ),
151 | });
152 |
--------------------------------------------------------------------------------
/packages/api/src/tests/jobs.test.ts:
--------------------------------------------------------------------------------
1 | import { appRouter } from "../routers/_app";
2 | import { expect, test } from "vitest";
3 | import {
4 | initRedisInstance,
5 | NUM_OF_COMPLETED_JOBS,
6 | NUM_OF_FAILED_JOBS,
7 | sleep,
8 | type,
9 | } from "./test.utils";
10 | import { TRPCError } from "@trpc/server";
11 |
12 | test("list completed jobs", async () => {
13 | const { ctx, firstQueue } = await initRedisInstance();
14 | const caller = appRouter.createCaller(ctx);
15 |
16 | const list = await caller.job.list({
17 | limit: 10,
18 | cursor: 0,
19 | status: "completed",
20 | queueName: firstQueue.queue.name,
21 | });
22 |
23 | expect(list.totalCount).toBe(NUM_OF_COMPLETED_JOBS);
24 | });
25 |
26 | test("retry job", async () => {
27 | const { ctx, firstQueue } = await initRedisInstance();
28 | const caller = appRouter.createCaller(ctx);
29 |
30 | try {
31 | const { jobs } = await caller.job.list({
32 | limit: 10,
33 | cursor: 0,
34 | status: "failed",
35 | queueName: firstQueue.queue.name,
36 | });
37 |
38 | const job = jobs[0];
39 |
40 | const newJob = await caller.job.retry({
41 | queueName: firstQueue.queue.name,
42 | jobId: job.id,
43 | });
44 | expect(newJob).toMatchObject({
45 | id: job.id,
46 | ...(firstQueue.type === "bull" && { retriedAt: expect.any(Date) }),
47 | });
48 | } catch (e) {
49 | expect(e).toBeInstanceOf(TRPCError);
50 | expect(type).toBe("bee");
51 | if (e instanceof TRPCError) {
52 | expect(e.code).toBe("BAD_REQUEST");
53 | }
54 | }
55 | });
56 |
57 | test("remove job", async () => {
58 | const { ctx, firstQueue } = await initRedisInstance();
59 | const caller = appRouter.createCaller(ctx);
60 |
61 | const { jobs } = await caller.job.list({
62 | limit: 10,
63 | cursor: 0,
64 | status: "failed",
65 | queueName: firstQueue.queue.name,
66 | });
67 |
68 | const job = jobs[0];
69 |
70 | await caller.job.remove({
71 | queueName: firstQueue.queue.name,
72 | jobId: job.id,
73 | });
74 |
75 | const list = await caller.job.list({
76 | limit: 10,
77 | cursor: 0,
78 | status: "failed",
79 | queueName: firstQueue.queue.name,
80 | });
81 |
82 | expect(list.totalCount).toBe(NUM_OF_FAILED_JOBS - 1);
83 | });
84 |
85 | test("bulk remove job", async () => {
86 | const { ctx, firstQueue } = await initRedisInstance();
87 | const caller = appRouter.createCaller(ctx);
88 |
89 | const { jobs } = await caller.job.list({
90 | limit: 10,
91 | cursor: 0,
92 | status: "failed",
93 | queueName: firstQueue.queue.name,
94 | });
95 |
96 | await caller.job.bulkRemove({
97 | queueName: firstQueue.queue.name,
98 | jobIds: jobs.map((job) => job.id),
99 | });
100 |
101 | const list = await caller.job.list({
102 | limit: 10,
103 | cursor: 0,
104 | status: "failed",
105 | queueName: firstQueue.queue.name,
106 | });
107 |
108 | expect(list.totalCount).toBe(0);
109 | });
110 |
111 | test("rerun job", async () => {
112 | const { ctx, firstQueue } = await initRedisInstance();
113 | const caller = appRouter.createCaller(ctx);
114 |
115 | const { jobs } = await caller.job.list({
116 | limit: 10,
117 | cursor: 0,
118 | status: "completed",
119 | queueName: firstQueue.queue.name,
120 | });
121 |
122 | const job = jobs[0];
123 |
124 | await caller.job.rerun({
125 | queueName: firstQueue.queue.name,
126 | jobId: job.id,
127 | });
128 |
129 | await sleep(200);
130 |
131 | const list = await caller.job.list({
132 | limit: 10,
133 | cursor: 0,
134 | status: "completed",
135 | queueName: firstQueue.queue.name,
136 | });
137 |
138 | expect(list.totalCount).toBe(NUM_OF_COMPLETED_JOBS + 1);
139 | });
140 |
141 | // TODO:
142 | // promote job
143 | // discard job
144 |
--------------------------------------------------------------------------------
/packages/api/src/tests/queue.test.ts:
--------------------------------------------------------------------------------
1 | import { appRouter } from "../routers/_app";
2 | import { expect, test } from "vitest";
3 | import { initRedisInstance, type } from "./test.utils";
4 | import { TRPCError } from "@trpc/server";
5 |
6 | test("list queues", async () => {
7 | const { ctx } = await initRedisInstance();
8 | const caller = appRouter.createCaller(ctx);
9 | const queues = await caller.queue.list();
10 |
11 | expect(queues).toMatchObject(
12 | ctx.queues.map((q) => {
13 | return {
14 | name: q.queue.name,
15 | displayName: q.displayName,
16 | };
17 | })
18 | );
19 | });
20 |
21 | test("get queue by name", async () => {
22 | const { ctx, firstQueue } = await initRedisInstance();
23 | const caller = appRouter.createCaller(ctx);
24 | const queue = await caller.queue.byName({
25 | queueName: firstQueue.queue.name,
26 | });
27 |
28 | expect(queue).toMatchObject({
29 | displayName: firstQueue.displayName,
30 | name: firstQueue.queue.name,
31 | });
32 | });
33 |
34 | test("pause queue", async () => {
35 | const { ctx, firstQueue } = await initRedisInstance();
36 | const caller = appRouter.createCaller(ctx);
37 |
38 | try {
39 | await caller.queue.pause({
40 | queueName: firstQueue.queue.name,
41 | });
42 |
43 | const queue = await caller.queue.byName({
44 | queueName: firstQueue.queue.name,
45 | });
46 |
47 | expect(queue).toMatchObject({
48 | displayName: firstQueue.displayName,
49 | name: firstQueue.queue.name,
50 | paused: true,
51 | });
52 | } catch (e) {
53 | expect(e).toBeInstanceOf(TRPCError);
54 | expect(type).toBe("bee");
55 | if (e instanceof TRPCError) {
56 | expect(e.code).toBe("BAD_REQUEST");
57 | }
58 | }
59 | });
60 |
61 | test("resume queue", async () => {
62 | const { ctx, firstQueue } = await initRedisInstance();
63 | const caller = appRouter.createCaller(ctx);
64 |
65 | try {
66 | await caller.queue.pause({
67 | queueName: firstQueue.queue.name,
68 | });
69 |
70 | await caller.queue.resume({
71 | queueName: firstQueue.queue.name,
72 | });
73 |
74 | const queue = await caller.queue.byName({
75 | queueName: firstQueue.queue.name,
76 | });
77 |
78 | expect(queue).toMatchObject({
79 | displayName: firstQueue.displayName,
80 | name: firstQueue.queue.name,
81 | paused: false,
82 | });
83 | } catch (e) {
84 | expect(e).toBeInstanceOf(TRPCError);
85 | expect(type).toBe("bee");
86 | if (e instanceof TRPCError) {
87 | expect(e.code).toBe("BAD_REQUEST");
88 | }
89 | }
90 | });
91 |
92 | test("clean completed jobs", async () => {
93 | const { ctx, firstQueue } = await initRedisInstance();
94 | const caller = appRouter.createCaller(ctx);
95 |
96 | try {
97 | await caller.queue.clean({
98 | queueName: firstQueue.queue.name,
99 | status: "completed",
100 | });
101 |
102 | const queue = await caller.queue.byName({
103 | queueName: firstQueue.queue.name,
104 | });
105 |
106 | expect(queue).toMatchObject({
107 | displayName: firstQueue.displayName,
108 | name: firstQueue.queue.name,
109 | counts: {
110 | completed: 0,
111 | },
112 | });
113 | } catch (e) {
114 | expect(e).toBeInstanceOf(TRPCError);
115 | expect(type).toBe("bee");
116 | if (e instanceof TRPCError) {
117 | expect(e.code).toBe("BAD_REQUEST");
118 | }
119 | }
120 | });
121 |
122 | test("clean failed jobs", async () => {
123 | const { ctx, firstQueue } = await initRedisInstance();
124 | const caller = appRouter.createCaller(ctx);
125 |
126 | try {
127 | await caller.queue.clean({
128 | queueName: firstQueue.queue.name,
129 | status: "failed",
130 | });
131 |
132 | const queue = await caller.queue.byName({
133 | queueName: firstQueue.queue.name,
134 | });
135 |
136 | expect(queue).toMatchObject({
137 | displayName: firstQueue.displayName,
138 | name: firstQueue.queue.name,
139 | counts: {
140 | failed: 0,
141 | },
142 | });
143 | } catch (e) {
144 | expect(e).toBeInstanceOf(TRPCError);
145 | expect(type).toBe("bee");
146 | if (e instanceof TRPCError) {
147 | expect(e.code).toBe("BAD_REQUEST");
148 | }
149 | }
150 | });
151 |
152 | // TODO: addJob
153 | // TODO: empty
154 |
--------------------------------------------------------------------------------
/packages/api/src/tests/schedulers.test.ts:
--------------------------------------------------------------------------------
1 | import { appRouter } from "../routers/_app";
2 | import { expect, test } from "vitest";
3 | import { initRedisInstance, NUM_OF_SCHEDULERS } from "./test.utils";
4 | import { TRPCError } from "@trpc/server";
5 |
6 | test("list schedulers", async () => {
7 | const { ctx, firstQueue } = await initRedisInstance();
8 | try {
9 | const caller = appRouter.createCaller(ctx);
10 | const list = await caller.scheduler.list({
11 | queueName: firstQueue.queue.name,
12 | });
13 |
14 | expect(list.length).toBe(NUM_OF_SCHEDULERS);
15 | } catch (e) {
16 | if (firstQueue.type !== "bullmq") {
17 | expect(e).toBeInstanceOf(TRPCError);
18 | if (e instanceof TRPCError) {
19 | expect(e.code).toBe("BAD_REQUEST");
20 | }
21 | } else {
22 | throw e;
23 | }
24 | }
25 | });
26 |
27 | test("remove scheduler", async () => {
28 | const { ctx, firstQueue } = await initRedisInstance();
29 | try {
30 | const caller = appRouter.createCaller(ctx);
31 |
32 | const schedulers = await caller.scheduler.list({
33 | queueName: firstQueue.queue.name,
34 | });
35 | const scheduler = schedulers[0];
36 |
37 | await caller.scheduler.remove({
38 | queueName: firstQueue.queue.name,
39 | jobSchedulerId: scheduler.key,
40 | });
41 |
42 | const list = await caller.scheduler.list({
43 | queueName: firstQueue.queue.name,
44 | });
45 |
46 | expect(list.length).toBe(NUM_OF_SCHEDULERS - 1);
47 | } catch (e) {
48 | if (firstQueue.type !== "bullmq") {
49 | expect(e).toBeInstanceOf(TRPCError);
50 | if (e instanceof TRPCError) {
51 | expect(e.code).toBe("BAD_REQUEST");
52 | }
53 | } else {
54 | throw e;
55 | }
56 | }
57 | });
58 |
59 | test("bulk remove schedulers", async () => {
60 | const { ctx, firstQueue } = await initRedisInstance();
61 | try {
62 | const caller = appRouter.createCaller(ctx);
63 |
64 | const schedulers = await caller.scheduler.list({
65 | queueName: firstQueue.queue.name,
66 | });
67 |
68 | await caller.scheduler.bulkRemove({
69 | queueName: firstQueue.queue.name,
70 | jobSchedulerIds: schedulers.map((scheduler) => scheduler.key),
71 | });
72 |
73 | const list = await caller.scheduler.list({
74 | queueName: firstQueue.queue.name,
75 | });
76 |
77 | expect(list.length).toBe(0);
78 | } catch (e) {
79 | if (firstQueue.type !== "bullmq") {
80 | expect(e).toBeInstanceOf(TRPCError);
81 | if (e instanceof TRPCError) {
82 | expect(e.code).toBe("BAD_REQUEST");
83 | }
84 | } else {
85 | throw e;
86 | }
87 | }
88 | });
89 |
--------------------------------------------------------------------------------
/packages/api/src/tests/test.utils.ts:
--------------------------------------------------------------------------------
1 | import Bull from "bull";
2 | import { faker } from "@faker-js/faker";
3 | import type { Context } from "../trpc";
4 | import BullMQ from "bullmq";
5 | import BeeQueue from "bee-queue";
6 |
7 | export const NUM_OF_JOBS = 20;
8 | export const NUM_OF_SCHEDULERS = 3;
9 | export const NUM_OF_COMPLETED_JOBS = NUM_OF_JOBS / 2;
10 | export const NUM_OF_FAILED_JOBS = NUM_OF_JOBS / 2;
11 | const QUEUE_NAME_PREFIX = "flight-bookings";
12 | const QUEUE_DISPLAY_NAME = "Flight bookings";
13 |
14 | const getFakeQueueName = () =>
15 | `${QUEUE_NAME_PREFIX}-${faker.string.alpha({ length: 5 })}`;
16 |
17 | export const sleep = (t: number) =>
18 | new Promise((resolve) => setTimeout(resolve, t));
19 |
20 | type QueueType = "bull" | "bullmq" | "bee";
21 |
22 | export const type: QueueType =
23 | (process.env.QUEUE_TYPE as unknown as QueueType) || "bullmq";
24 |
25 | export const initRedisInstance = async () => {
26 | switch (type) {
27 | case "bull": {
28 | const flightBookingsQueue = {
29 | queue: new Bull(getFakeQueueName()),
30 | displayName: QUEUE_DISPLAY_NAME,
31 | type: "bull" as const,
32 | };
33 |
34 | flightBookingsQueue.queue.process(async (job) => {
35 | if (job.data.index > NUM_OF_COMPLETED_JOBS) {
36 | throw new Error("Generic error");
37 | }
38 |
39 | return Promise.resolve();
40 | });
41 |
42 | await flightBookingsQueue.queue.addBulk(
43 | [...new Array(NUM_OF_JOBS)].map((_, index) => {
44 | return {
45 | data: {
46 | index: index + 1,
47 | },
48 | };
49 | }),
50 | );
51 |
52 | await sleep(200);
53 |
54 | return {
55 | ctx: {
56 | queues: [flightBookingsQueue],
57 | } satisfies Context,
58 | firstQueue: flightBookingsQueue,
59 | };
60 | }
61 | case "bullmq": {
62 | const flightBookingsQueue = {
63 | queue: new BullMQ.Queue(getFakeQueueName()),
64 | displayName: QUEUE_DISPLAY_NAME,
65 | type: "bullmq" as const,
66 | };
67 |
68 | new BullMQ.Worker(
69 | flightBookingsQueue.queue.name,
70 | async (job) => {
71 | if (job.data.index > NUM_OF_COMPLETED_JOBS) {
72 | throw new Error("Generic error");
73 | }
74 |
75 | return Promise.resolve();
76 | },
77 | {
78 | connection: {},
79 | },
80 | );
81 |
82 | await flightBookingsQueue.queue.addBulk(
83 | [...new Array(NUM_OF_JOBS)].map((_, index) => {
84 | return {
85 | name: "test",
86 | data: {
87 | index: index + 1,
88 | },
89 | };
90 | }),
91 | );
92 |
93 | const schedulers = [...new Array(NUM_OF_SCHEDULERS)].map(() => {
94 | return {
95 | name: faker.person.fullName(),
96 | template: {
97 | name: faker.person.fullName(),
98 | data: {
99 | name: faker.person.fullName(),
100 | },
101 | },
102 | opts: {
103 | pattern: "0 0 * * *",
104 | tz: "America/Los_Angeles",
105 | },
106 | };
107 | });
108 |
109 | for (const scheduler of schedulers) {
110 | await flightBookingsQueue.queue.upsertJobScheduler(
111 | scheduler.name,
112 | scheduler.opts,
113 | scheduler.template,
114 | );
115 | }
116 |
117 | await sleep(200);
118 |
119 | return {
120 | ctx: {
121 | queues: [flightBookingsQueue],
122 | } satisfies Context,
123 | firstQueue: flightBookingsQueue,
124 | };
125 | }
126 | case "bee": {
127 | const flightBookingsQueue = {
128 | queue: new BeeQueue(getFakeQueueName()),
129 | displayName: QUEUE_DISPLAY_NAME,
130 | type: "bee" as const,
131 | };
132 |
133 | flightBookingsQueue.queue.process(async (job) => {
134 | if (job.data.index > NUM_OF_COMPLETED_JOBS) {
135 | throw new Error("Generic error");
136 | }
137 |
138 | return Promise.resolve();
139 | });
140 |
141 | await flightBookingsQueue.queue.saveAll(
142 | [...new Array(NUM_OF_JOBS)].map((_, index) => {
143 | return flightBookingsQueue.queue.createJob({
144 | index: index + 1,
145 | });
146 | }),
147 | );
148 |
149 | await sleep(200);
150 |
151 | return {
152 | ctx: {
153 | queues: [flightBookingsQueue],
154 | } satisfies Context,
155 | firstQueue: flightBookingsQueue,
156 | };
157 | }
158 | }
159 | };
160 |
--------------------------------------------------------------------------------
/packages/api/src/trpc.ts:
--------------------------------------------------------------------------------
1 | import { initTRPC } from "@trpc/server";
2 | import type Bull from "bull";
3 | import type BullMQ from "bullmq";
4 | import type BeeQueue from "bee-queue";
5 |
6 | type Queue = {
7 | // Display name of the queue in the UI
8 | displayName: string;
9 | // Function to get the job name from the job data
10 | jobName?: (data: Record) => string;
11 | } & (
12 | | {
13 | queue: Bull.Queue;
14 | type: "bull";
15 | }
16 | | {
17 | queue: BullMQ.Queue;
18 | type: "bullmq";
19 | }
20 | | {
21 | queue: BeeQueue;
22 | type: "bee";
23 | }
24 | );
25 |
26 | export type Context = {
27 | // List of queues to expose
28 | queues: Queue[];
29 | };
30 |
31 | const t = initTRPC.context().create();
32 | export const router = t.router;
33 | export const procedure = t.procedure;
34 |
--------------------------------------------------------------------------------
/packages/api/src/utils/global.utils.ts:
--------------------------------------------------------------------------------
1 | import type Bull from "bull";
2 | import type BullMQ from "bullmq";
3 | import { TRPCError } from "@trpc/server";
4 | import type { Context } from "../trpc";
5 | import type BeeQueue from "bee-queue";
6 |
7 | type QueueDashOptions = {
8 | priority?: number;
9 | delay?: number;
10 | attempts?: number;
11 | backoff?: number;
12 | lifo?: boolean;
13 | timeout?: number;
14 | removeOnComplete?: boolean | number;
15 | removeOnFail?: boolean | number;
16 | stackTraceLimit?: number;
17 | preventParsingData?: boolean;
18 | repeat?: {
19 | offset: number;
20 | tz: string;
21 | pattern: string;
22 | count: number;
23 | };
24 | };
25 |
26 | type QueueDashJob = {
27 | id: string;
28 | name: string;
29 | data: object;
30 | opts: QueueDashOptions;
31 | createdAt: Date;
32 | processedAt: Date | null;
33 | finishedAt: Date | null;
34 | failedReason?: string;
35 | stacktrace?: string[];
36 | retriedAt: Date | null;
37 | };
38 |
39 | export type QueueDashScheduler = {
40 | key: string;
41 | name: string;
42 | id?: string | null;
43 | iterationCount?: number;
44 | limit?: number;
45 | endDate?: number;
46 | tz?: string;
47 | pattern?: string;
48 | every?: string;
49 | next?: number;
50 | template?: {
51 | data?: Record;
52 | };
53 | };
54 |
55 | export const formatJob = ({
56 | job,
57 | queueInCtx,
58 | }: {
59 | job: Bull.Job | BullMQ.Job | BeeQueue.Job>;
60 | queueInCtx: Context["queues"][0];
61 | }): QueueDashJob => {
62 | if ("status" in job) {
63 | // TODO:
64 | return {
65 | id: job.id as string,
66 | name: queueInCtx.jobName
67 | ? queueInCtx.jobName(job.data)
68 | : job.id === "__default__"
69 | ? "Default"
70 | : job.id,
71 | data: job.data as object,
72 | opts: job.options,
73 | createdAt: new Date(),
74 | processedAt: new Date(),
75 | finishedAt: new Date(),
76 | failedReason: "",
77 | stacktrace: [""],
78 | retriedAt: new Date(),
79 | };
80 | }
81 |
82 | return {
83 | id: job.id as string,
84 | name: queueInCtx.jobName
85 | ? queueInCtx.jobName(job.data)
86 | : job.name === "__default__"
87 | ? "Default"
88 | : job.name,
89 | data: job.data as object,
90 | opts: job.opts,
91 | createdAt: new Date(job.timestamp),
92 | processedAt: job.processedOn ? new Date(job.processedOn) : null,
93 | finishedAt: job.finishedOn ? new Date(job.finishedOn) : null,
94 | failedReason: job.failedReason,
95 | stacktrace: job.stacktrace,
96 | // @ts-expect-error
97 | retriedAt: job.retriedOn ? new Date(job.retriedOn) : null,
98 | } as QueueDashJob;
99 | };
100 |
101 | export const findQueueInCtxOrFail = ({
102 | queueName,
103 | queues,
104 | }: {
105 | queueName: string;
106 | queues: Context["queues"];
107 | }) => {
108 | const queueInCtx = queues.find((q) => q.queue.name === queueName);
109 | if (!queueInCtx) {
110 | throw new TRPCError({
111 | code: "NOT_FOUND",
112 | });
113 | }
114 | return queueInCtx;
115 | };
116 |
--------------------------------------------------------------------------------
/packages/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "target": "ES2020",
5 | "lib": ["dom", "dom.iterable", "esnext"],
6 | "outDir": "dist",
7 | "allowJs": true,
8 | "skipLibCheck": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noEmit": true,
12 | "incremental": true,
13 | "esModuleInterop": true,
14 | "module": "esnext",
15 | "moduleResolution": "node",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "jsx": "preserve",
19 | "composite": true,
20 | "declaration": true,
21 | "downlevelIteration": true,
22 | "declarationMap": true,
23 | "inlineSources": false,
24 | "noUnusedLocals": false,
25 | "noUnusedParameters": false,
26 | "preserveWatchOutput": true,
27 | "baseUrl": "."
28 | },
29 | "include": ["**/*.ts", "**/*.tsx", "vite.config.ts", "package.json"],
30 | "exclude": ["node_modules"]
31 | }
32 |
--------------------------------------------------------------------------------
/packages/api/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import typescript from "@rollup/plugin-typescript";
3 | import path from "path";
4 | import { typescriptPaths } from "rollup-plugin-typescript-paths";
5 |
6 | export default defineConfig({
7 | define: {
8 | "process.env.NODE_ENV": JSON.stringify("production"),
9 | },
10 | build: {
11 | minify: true,
12 | reportCompressedSize: true,
13 | lib: {
14 | entry: path.resolve(__dirname, "src/main.ts"),
15 | fileName: "main",
16 | name: "QueueDash API",
17 | formats: ["cjs", "es"],
18 | },
19 | rollupOptions: {
20 | external: ["events"],
21 | plugins: [
22 | typescriptPaths({
23 | preserveExtensions: true,
24 | }),
25 | typescript({
26 | sourceMap: false,
27 | declaration: true,
28 | outDir: "dist",
29 | }),
30 | ],
31 | },
32 | },
33 | });
34 |
--------------------------------------------------------------------------------
/packages/api/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vitest/config";
2 |
3 | export default defineConfig({
4 | test: {
5 | globals: true,
6 | },
7 | });
8 |
--------------------------------------------------------------------------------
/packages/client/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: "@typescript-eslint/parser",
4 | parserOptions: {
5 | tsconfigRootDir: __dirname,
6 | project: "./tsconfig.json",
7 | },
8 | ignorePatterns: [
9 | "**/node_modules",
10 | "**/.cache",
11 | "dist",
12 | ".eslintrc.js",
13 | "tailwind.config.js",
14 | "postcss.config.js",
15 | ],
16 | plugins: ["@typescript-eslint", "tailwindcss"],
17 | extends: [
18 | "eslint:recommended",
19 | "plugin:@typescript-eslint/recommended",
20 | "plugin:tailwindcss/recommended",
21 | "prettier",
22 | ],
23 | rules: {
24 | "@typescript-eslint/consistent-type-imports": "warn",
25 | "@typescript-eslint/no-unused-vars": "error",
26 | "@typescript-eslint/no-explicit-any": "error",
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/packages/client/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @queuedash/client
2 |
3 | ## 3.6.0
4 |
5 | ### Minor Changes
6 |
7 | - [#50](https://github.com/alexbudure/queuedash/pull/50) [`1fd1326`](https://github.com/alexbudure/queuedash/commit/1fd1326ebc22045c78ecdbbceef2856fcce6cbb6) Thanks [@alexbudure](https://github.com/alexbudure)! - - Job scheduler support
8 | - Per-job logs
9 | - Global pause & resume
10 |
11 | ## 3.5.0
12 |
13 | ### Minor Changes
14 |
15 | - [#44](https://github.com/alexbudure/queuedash/pull/44) [`ba73bcf`](https://github.com/alexbudure/queuedash/commit/ba73bcf1afec112e6916a4c6beb132a8d9c7edd4) Thanks [@huv1k](https://github.com/huv1k)! - Add support for hono
16 |
17 | ## 3.4.0
18 |
19 | ### Minor Changes
20 |
21 | - [#40](https://github.com/alexbudure/queuedash/pull/40) [`0ccd37a`](https://github.com/alexbudure/queuedash/commit/0ccd37afe5a3d8b158109d1ec80a02cead1480bf) Thanks [@fukouda](https://github.com/fukouda)! - Add the ability to pass in hook handlers on Fastify
22 |
23 | ## 3.3.0
24 |
25 | ### Minor Changes
26 |
27 | - [#38](https://github.com/alexbudure/queuedash/pull/38) [`00346f4`](https://github.com/alexbudure/queuedash/commit/00346f4c11fdae742dae1981061f044aee66697b) Thanks [@alexbudure](https://github.com/alexbudure)! - Improve React 19 compatibility
28 |
29 | ## 3.2.0
30 |
31 | ### Minor Changes
32 |
33 | - [`8e5c7ac`](https://github.com/alexbudure/queuedash/commit/8e5c7ac06bb13674e32b4cd9b1b7c65913e122af) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix importing API
34 |
35 | ## 3.1.0
36 |
37 | ### Minor Changes
38 |
39 | - [#33](https://github.com/alexbudure/queuedash/pull/33) [`011ad3b`](https://github.com/alexbudure/queuedash/commit/011ad3bca2b50b4568fa7edc7bf314948e84eeb9) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix bundle strategy for API
40 |
41 | ## 3.0.0
42 |
43 | ### Major Changes
44 |
45 | - [#21](https://github.com/alexbudure/queuedash/pull/21) [`6692303`](https://github.com/alexbudure/queuedash/commit/6692303bde835b9934e2ae962e4727357f0d4afe) Thanks [@alexbudure](https://github.com/alexbudure)! - This version upgrades core dependencies to their latest major versions, including Elysia, BullMQ, and tRPC
46 |
47 | ## 2.1.1
48 |
49 | ### Patch Changes
50 |
51 | - [#26](https://github.com/alexbudure/queuedash/pull/26) [`2e0956c`](https://github.com/alexbudure/queuedash/commit/2e0956c586b9f5f3190f169e363f76230d037686) Thanks [@p3drosola](https://github.com/p3drosola)! - Improve UI support for long job ids
52 |
53 | ## 2.1.0
54 |
55 | ### Minor Changes
56 |
57 | - [#23](https://github.com/alexbudure/queuedash/pull/23) [`7a2e3c0`](https://github.com/alexbudure/queuedash/commit/7a2e3c000da0b34c4c3a4dd2471e2e19738d1e6d) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix bull api differences
58 |
59 | ## 2.0.5
60 |
61 | ### Patch Changes
62 |
63 | - [`bc47dd5`](https://github.com/alexbudure/queuedash/commit/bc47dd5de7a5ed32cd82365dc27073282afc45be) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix express bundling with app
64 |
65 | ## 2.0.4
66 |
67 | ### Patch Changes
68 |
69 | - [`0948ec2`](https://github.com/alexbudure/queuedash/commit/0948ec21985d33b3ffbb0ec220664493382579da) Thanks [@alexbudure](https://github.com/alexbudure)! - Move html into each adapter
70 |
71 | ## 2.0.3
72 |
73 | ### Patch Changes
74 |
75 | - [`dee7163`](https://github.com/alexbudure/queuedash/commit/dee71633d33c8bceee9bde84a0b340f899adeaf8) Thanks [@alexbudure](https://github.com/alexbudure)! - Remove unnecessary express app in adapter
76 |
77 | ## 2.0.2
78 |
79 | ### Patch Changes
80 |
81 | - [`6680a6a`](https://github.com/alexbudure/queuedash/commit/6680a6a5ece43fef248fedacb31f8fae2242d2d3) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix the adapters.. again
82 |
83 | ## 2.0.1
84 |
85 | ### Patch Changes
86 |
87 | - [`8d2eadd`](https://github.com/alexbudure/queuedash/commit/8d2eadd9ad547ff2e893662474a228bf340f0728) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix the middlewares for Fastify and Express
88 |
89 | ## 2.0.0
90 |
91 | ### Major Changes
92 |
93 | - [#5](https://github.com/alexbudure/queuedash/pull/5) [`1f794d1`](https://github.com/alexbudure/queuedash/commit/1f794d1679225718dcc670e9c7eb59564fee1bc6) Thanks [@alexbudure](https://github.com/alexbudure)! - Updated all adapters to use a more natural API and added real-time Redis info on the queue detail page
94 |
95 | ***
96 |
97 | ### Breaking changes
98 |
99 | **Express**
100 |
101 | Before:
102 |
103 | ```typescript
104 | createQueueDashExpressMiddleware({
105 | app,
106 | baseUrl: "/queuedash",
107 | ctx: {
108 | queues: [
109 | {
110 | queue: new Bull("report-queue"),
111 | displayName: "Reports",
112 | type: "bull" as const,
113 | },
114 | ],
115 | },
116 | });
117 | ```
118 |
119 | After:
120 |
121 | ```typescript
122 | app.use(
123 | "/queuedash",
124 | createQueueDashExpressMiddleware({
125 | ctx: {
126 | queues: [
127 | {
128 | queue: new Bull("report-queue"),
129 | displayName: "Reports",
130 | type: "bull" as const,
131 | },
132 | ],
133 | },
134 | }),
135 | );
136 | ```
137 |
138 | **Fastify**
139 |
140 | Before:
141 |
142 | ```typescript
143 | createQueueDashFastifyMiddleware({
144 | server,
145 | baseUrl: "/queuedash",
146 | ctx: {
147 | queues: [
148 | {
149 | queue: new Bull("report-queue"),
150 | displayName: "Reports",
151 | type: "bull" as const,
152 | },
153 | ],
154 | },
155 | });
156 | ```
157 |
158 | After:
159 |
160 | ```typescript
161 | server.register(fastifyQueueDashPlugin, {
162 | baseUrl: "/queuedash",
163 | ctx: {
164 | queues: [
165 | {
166 | queue: new Bull("report-queue"),
167 | displayName: "Reports",
168 | type: "bull" as const,
169 | },
170 | ],
171 | },
172 | });
173 | ```
174 |
175 | ## 1.2.1
176 |
177 | ### Patch Changes
178 |
179 | - [#12](https://github.com/alexbudure/queuedash/pull/12) [`d79c8ff`](https://github.com/alexbudure/queuedash/commit/d79c8ffe34ae36c74d0663dd2e29e6c93327bf8c) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix Elysia plugin
180 |
181 | ## 1.2.0
182 |
183 | ### Minor Changes
184 |
185 | - [`9aaec9a`](https://github.com/alexbudure/queuedash/commit/9aaec9a21c091680cb30a67e9322eedd3e16dbe8) Thanks [@alexbudure](https://github.com/alexbudure)! - Support for Elysia
186 |
187 | ## 1.0.1
188 |
189 | ### Patch Changes
190 |
191 | - [#3](https://github.com/alexbudure/queuedash/pull/3) [`a385f9f`](https://github.com/alexbudure/queuedash/commit/a385f9f9e76df4cea8e69d7e218b65915acef3bf) Thanks [@alexbudure](https://github.com/alexbudure)! - Tighten adapter types to work with NestJS
192 |
193 | ## 1.0.0
194 |
195 | ### Major Changes
196 |
197 | - [#1](https://github.com/alexbudure/queuedash/pull/1) [`c96b93d`](https://github.com/alexbudure/queuedash/commit/c96b93d9659bbb34248ab377e6659ebfb1fc3dd8) Thanks [@alexbudure](https://github.com/alexbudure)! - QueueDash v1 🎉
198 |
199 | - 😍 Simple, clean, and compact UI
200 | - 🧙 Add jobs to your queue with ease
201 | - 🪄 Retry, remove, and more convenient actions for your jobs
202 | - 📊 Stats for job counts, job durations, and job wait times
203 | - ✨ Top-level overview page of all queues
204 | - 🔋 Integrates with Next.js, Express.js, and Fastify
205 | - ⚡️ Compatible with Bull, BullMQ, and Bee-Queue
206 |
--------------------------------------------------------------------------------
/packages/client/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | A stunning, sleek dashboard for Bull, BullMQ, and Bee-Queue.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ## Features
21 |
22 | - 😍 Simple, clean, and compact UI
23 | - 🧙 Add jobs to your queue with ease
24 | - 🪄 Retry, remove, and more convenient actions for your jobs
25 | - 📊 Stats for job counts, job durations, and job wait times
26 | - ✨ Top-level overview page of all queues
27 | - 🔋 Integrates with Next.js, Express.js, and Fastify
28 | - ⚡️ Compatible with Bull, BullMQ, and Bee-Queue
29 |
30 | ## Getting Started
31 |
32 | ### Express
33 |
34 | `pnpm install @queuedash/api`
35 |
36 | ```typescript
37 | import express from "express";
38 | import Bull from "bull";
39 | import { createQueueDashExpressMiddleware } from "@queuedash/api";
40 |
41 | const app = express();
42 |
43 | const reportQueue = new Bull("report-queue");
44 |
45 | app.use(
46 | "/queuedash",
47 | createQueueDashExpressMiddleware({
48 | ctx: {
49 | queues: [
50 | {
51 | queue: reportQueue,
52 | displayName: "Reports",
53 | type: "bull" as const,
54 | },
55 | ],
56 | },
57 | })
58 | );
59 |
60 | app.listen(3000, () => {
61 | console.log("Listening on port 3000");
62 | console.log("Visit http://localhost:3000/queuedash");
63 | });
64 | ```
65 |
66 | ### Next.js
67 |
68 | `pnpm install @queuedash/api @queuedash/ui`
69 |
70 | ```typescript jsx
71 | // pages/admin/queuedash/[[...slug]].tsx
72 | import { QueueDashApp } from "@queuedash/ui";
73 |
74 | function getBaseUrl() {
75 | if (process.env.VERCEL_URL) {
76 | return `https://${process.env.VERCEL_URL}/api/queuedash`;
77 | }
78 |
79 | return `http://localhost:${process.env.PORT ?? 3000}/api/queuedash`;
80 | }
81 |
82 | const QueueDashPages = () => {
83 | return ;
84 | };
85 |
86 | export default QueueDashPages;
87 |
88 | // pages/api/queuedash/[trpc].ts
89 | import * as trpcNext from "@trpc/server/adapters/next";
90 | import { appRouter } from "@queuedash/api";
91 |
92 | const reportQueue = new Bull("report-queue");
93 |
94 | export default trpcNext.createNextApiHandler({
95 | router: appRouter,
96 | batching: {
97 | enabled: true,
98 | },
99 | createContext: () => ({
100 | queues: [
101 | {
102 | queue: reportQueue,
103 | displayName: "Reports",
104 | type: "bull" as const,
105 | },
106 | ],
107 | }),
108 | });
109 | ```
110 |
111 | See the [./examples](./examples) folder for more.
112 |
113 | ---
114 |
115 | ## API Reference
116 |
117 | ### `createQueueDash<*>Middleware`
118 |
119 | ```typescript
120 | type QueueDashMiddlewareOptions = {
121 | app: express.Application | FastifyInstance; // Express or Fastify app
122 | baseUrl: string; // Base path for the API and UI
123 | ctx: QueueDashContext; // Context for the UI
124 | };
125 |
126 | type QueueDashContext = {
127 | queues: QueueDashQueue[]; // Array of queues to display
128 | };
129 |
130 | type QueueDashQueue = {
131 | queue: Bull.Queue | BullMQ.Queue | BeeQueue; // Queue instance
132 | displayName: string; // Display name for the queue
133 | type: "bull" | "bullmq" | "bee"; // Queue type
134 | };
135 | ```
136 |
137 | ### ``
138 |
139 | ```typescript jsx
140 | type QueueDashAppProps = {
141 | apiUrl: string; // URL to the API endpoint
142 | basename: string; // Base path for the app
143 | };
144 | ```
145 |
146 | ## Roadmap
147 |
148 | - Supports Celery and other queueing systems
149 | - Command+K bar and shortcuts
150 | - Ability to whitelabel the UI
151 |
152 | ## Pro Version
153 |
154 | Right now, QueueDash simply taps into your Redis instance, making it very easy to set up, but also limited in functionality.
155 |
156 | I'm thinking about building a free-to-host version on top of this which will require external services (db, auth, etc.), but it will make the following features possible:
157 |
158 | - Alerts and notifications
159 | - Quick search and filtering
160 | - Queue trends and analytics
161 | - Invite team members
162 |
163 | If you're interested in this version, please let me know!
164 |
165 | ## Acknowledgements
166 |
167 | QueueDash was inspired by some great open source projects. Here's a few of them:
168 |
169 | - [bull-board](https://github.com/vcapretz/bull-board)
170 | - [bull-monitor](https://github.com/s-r-x/bull-monitor)
171 | - [bull-arena](https://github.com/bee-queue/arena)
172 |
--------------------------------------------------------------------------------
/packages/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@queuedash/client",
3 | "version": "3.6.0",
4 | "description": "A stunning, sleek dashboard for Bull, BullMQ, and Bee-Queue",
5 | "files": [
6 | "dist"
7 | ],
8 | "scripts": {
9 | "build": "tsc && vite build",
10 | "lint": "eslint ./ --fix"
11 | },
12 | "dependencies": {
13 | "react": "^19.1.0",
14 | "react-dom": "^19.1.0"
15 | },
16 | "devDependencies": {
17 | "@queuedash/ui": "workspace:*",
18 | "@rollup/plugin-typescript": "^12.1.2",
19 | "@types/react": "^19.1.0",
20 | "@types/react-dom": "^19.1.1",
21 | "@typescript-eslint/eslint-plugin": "^8.29.0",
22 | "@typescript-eslint/parser": "^8.29.0",
23 | "@vitejs/plugin-react": "^4.3.4",
24 | "eslint": "^9.24.0",
25 | "eslint-config-prettier": "^10.1.1",
26 | "eslint-plugin-tailwindcss": "^3.18.0",
27 | "postcss": "^8.5.3",
28 | "prettier": "^3.5.3",
29 | "rollup-plugin-typescript-paths": "^1.5.0",
30 | "tailwindcss": "^3.4.16",
31 | "typescript": "^5.8.3",
32 | "vite": "^5.4.19"
33 | },
34 | "license": "MIT"
35 | }
36 |
--------------------------------------------------------------------------------
/packages/client/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from "react-dom/client";
2 | import { StrictMode } from "react";
3 | import { QueueDashApp } from "@queuedash/ui";
4 |
5 | interface CustomWindow extends Window {
6 | __INITIAL_STATE__: {
7 | apiUrl: string;
8 | basename: string;
9 | };
10 | }
11 |
12 | declare let window: CustomWindow;
13 |
14 | createRoot(document.getElementById("root")!).render(
15 |
16 |
20 |
21 | );
22 |
--------------------------------------------------------------------------------
/packages/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "target": "es5",
5 | "lib": ["dom", "dom.iterable", "esnext"],
6 | "outDir": "dist",
7 | "allowJs": true,
8 | "skipLibCheck": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noEmit": true,
12 | "incremental": true,
13 | "esModuleInterop": true,
14 | "module": "esnext",
15 | "moduleResolution": "node",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "jsx": "preserve",
19 | "composite": true,
20 | "declaration": true,
21 | "declarationMap": true,
22 | "inlineSources": false,
23 | "noUnusedLocals": false,
24 | "noUnusedParameters": false,
25 | "preserveWatchOutput": true,
26 | "baseUrl": "."
27 | },
28 | "include": ["**/*.ts", "**/*.tsx", "vite.config.ts"],
29 | "exclude": ["node_modules"]
30 | }
31 |
--------------------------------------------------------------------------------
/packages/client/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/packages/client/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import typescript from "@rollup/plugin-typescript";
3 | import path from "path";
4 | import { typescriptPaths } from "rollup-plugin-typescript-paths";
5 | import react from "@vitejs/plugin-react";
6 |
7 | export default defineConfig({
8 | plugins: [react()],
9 | define: {
10 | "process.env.NODE_ENV": JSON.stringify("production"),
11 | },
12 | build: {
13 | minify: true,
14 | reportCompressedSize: true,
15 | lib: {
16 | entry: path.resolve(__dirname, "src/main.tsx"),
17 | name: "QueueDash App",
18 | fileName: "main",
19 | formats: ["cjs", "es"],
20 | },
21 | rollupOptions: {
22 | plugins: [
23 | typescriptPaths({
24 | preserveExtensions: true,
25 | }),
26 | typescript({
27 | sourceMap: false,
28 | declaration: true,
29 | outDir: "dist",
30 | }),
31 | ],
32 | },
33 | },
34 | });
35 |
--------------------------------------------------------------------------------
/packages/dev/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: "@typescript-eslint/parser",
4 | parserOptions: {
5 | tsconfigRootDir: __dirname,
6 | project: "./tsconfig.json",
7 | },
8 | ignorePatterns: [
9 | "**/node_modules",
10 | "**/.cache",
11 | "build",
12 | ".next",
13 | ".eslintrc.js",
14 | "next.config.js",
15 | ],
16 | plugins: ["@typescript-eslint", "tailwindcss"],
17 | extends: [
18 | "next/core-web-vitals",
19 | "eslint:recommended",
20 | "plugin:@typescript-eslint/recommended",
21 | "plugin:tailwindcss/recommended",
22 | "prettier",
23 | ],
24 | rules: {
25 | "@typescript-eslint/consistent-type-imports": "warn",
26 | "@typescript-eslint/no-unused-vars": "error",
27 | "@typescript-eslint/no-explicit-any": "error",
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/packages/dev/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @queuedash/dev
2 |
3 | ## 1.2.1
4 |
5 | ### Patch Changes
6 |
7 | - [#12](https://github.com/alexbudure/queuedash/pull/12) [`d79c8ff`](https://github.com/alexbudure/queuedash/commit/d79c8ffe34ae36c74d0663dd2e29e6c93327bf8c) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix Elysia plugin
8 |
9 | - Updated dependencies [[`d79c8ff`](https://github.com/alexbudure/queuedash/commit/d79c8ffe34ae36c74d0663dd2e29e6c93327bf8c)]:
10 | - @queuedash/api@1.2.1
11 | - @queuedash/ui@1.2.1
12 |
13 | ## 1.2.0
14 |
15 | ### Minor Changes
16 |
17 | - [`9aaec9a`](https://github.com/alexbudure/queuedash/commit/9aaec9a21c091680cb30a67e9322eedd3e16dbe8) Thanks [@alexbudure](https://github.com/alexbudure)! - Support for Elysia
18 |
19 | ### Patch Changes
20 |
21 | - Updated dependencies [[`9aaec9a`](https://github.com/alexbudure/queuedash/commit/9aaec9a21c091680cb30a67e9322eedd3e16dbe8)]:
22 | - @queuedash/api@1.2.0
23 | - @queuedash/ui@1.2.0
24 |
25 | ## 1.1.0
26 |
27 | ### Minor Changes
28 |
29 | - [#7](https://github.com/alexbudure/queuedash/pull/7) [`885aee3`](https://github.com/alexbudure/queuedash/commit/885aee3cecac687d05f5b18cd1855fcb5522f899) Thanks [@alexbudure](https://github.com/alexbudure)! - Add support for prioritized jobs
30 |
31 | ### Patch Changes
32 |
33 | - Updated dependencies [[`885aee3`](https://github.com/alexbudure/queuedash/commit/885aee3cecac687d05f5b18cd1855fcb5522f899)]:
34 | - @queuedash/api@1.1.0
35 | - @queuedash/ui@1.1.0
36 |
--------------------------------------------------------------------------------
/packages/dev/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
6 |
--------------------------------------------------------------------------------
/packages/dev/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | transpilePackages: ["@queuedash/api", "@queuedash/ui"],
5 | webpack: (config) => {
6 | config.node = {
7 | ...config.node,
8 | __dirname: true,
9 | };
10 | return config;
11 | },
12 | };
13 |
14 | module.exports = nextConfig;
15 |
--------------------------------------------------------------------------------
/packages/dev/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@queuedash/dev",
3 | "version": "1.2.1",
4 | "private": true,
5 | "scripts": {
6 | "dev": "ts-node-dev --respawn --transpile-only -O \"{\\\"module\\\":\\\"commonjs\\\"}\" utils/worker.ts & next dev",
7 | "build-next": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@faker-js/faker": "^9.6.0",
13 | "@queuedash/api": "workspace:*",
14 | "@queuedash/ui": "workspace:*",
15 | "@trpc/server": "^11.0.2",
16 | "bee-queue": "^1.7.1",
17 | "bull": "^4.16.5",
18 | "bullmq": "^5.47.2",
19 | "next": "^15.2.4",
20 | "react": "^19.1.0",
21 | "react-dom": "^19.1.0"
22 | },
23 | "devDependencies": {
24 | "@types/node": "^22.14.0",
25 | "@types/react": "^19.1.0",
26 | "@typescript-eslint/eslint-plugin": "^8.29.0",
27 | "@typescript-eslint/parser": "^8.29.0",
28 | "eslint": "^9.24.0",
29 | "eslint-config-next": "^15.2.4",
30 | "eslint-config-prettier": "^10.1.1",
31 | "prettier": "^3.5.3",
32 | "ts-node-dev": "^2.0.0",
33 | "typescript": "^5.8.3"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/dev/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "@queuedash/ui/dist/styles.css";
2 |
3 | import type { AppType } from "next/app";
4 |
5 | const MyApp: AppType = ({ Component, pageProps }) => {
6 | return ;
7 | };
8 | export default MyApp;
9 |
--------------------------------------------------------------------------------
/packages/dev/pages/api/queuedash/[trpc].ts:
--------------------------------------------------------------------------------
1 | import * as trpcNext from "@trpc/server/adapters/next";
2 | import { appRouter } from "@queuedash/api";
3 | import { queues } from "../../../utils/fake-data";
4 |
5 | export default trpcNext.createNextApiHandler({
6 | router: appRouter,
7 | onError({ error }) {
8 | if (error.code === "INTERNAL_SERVER_ERROR") {
9 | // send to bug reporting
10 | console.error("Something went wrong", error);
11 | }
12 | },
13 | batching: {
14 | enabled: true,
15 | },
16 | createContext: () => ({
17 | queues: queues.map((queue) => {
18 | return {
19 | queue: queue.queue,
20 | displayName: queue.displayName,
21 | type: queue.type,
22 | jobName: queue.jobName,
23 | };
24 | }),
25 | }),
26 | });
27 |
--------------------------------------------------------------------------------
/packages/dev/pages/api/reset.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from "next";
2 |
3 | import { queues } from "../../utils/fake-data";
4 |
5 | export default async function handler(
6 | req: NextApiRequest,
7 | res: NextApiResponse,
8 | ) {
9 | const client = await queues[0].queue.client;
10 | const pipeline = client.pipeline();
11 | const keys = await client.keys("bull*");
12 | keys.forEach((key) => {
13 | pipeline.del(key);
14 | });
15 | await pipeline.exec();
16 |
17 | for (const item of queues) {
18 | if (item.type === "bull") {
19 | await item.queue.removeJobs("*");
20 | await item.queue.addBulk(item.jobs);
21 | } else {
22 | await item.queue.obliterate({ force: true });
23 | for (const scheduler of item.schedulers) {
24 | await item.queue.upsertJobScheduler(
25 | scheduler.name,
26 | scheduler.opts,
27 | scheduler.template,
28 | );
29 | }
30 | await item.queue.addBulk(
31 | item.jobs.map((job) => {
32 | return {
33 | name: "test",
34 | ...job,
35 | };
36 | }),
37 | );
38 | }
39 | }
40 | res.status(200).json({ ok: "ok" });
41 | }
42 |
--------------------------------------------------------------------------------
/packages/dev/pages/queuedash/[[...slug]].tsx:
--------------------------------------------------------------------------------
1 | import { QueueDashApp } from "@queuedash/ui";
2 |
3 | function getBaseUrl() {
4 | if (process.env.VERCEL_URL) {
5 | return `https://${process.env.VERCEL_URL}/api/queuedash`;
6 | }
7 |
8 | return `http://localhost:${process.env.PORT ?? 3000}/api/queuedash`;
9 | }
10 |
11 | const Pages = () => {
12 | return (
13 |
14 |
15 |
16 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default Pages;
30 |
--------------------------------------------------------------------------------
/packages/dev/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "target": "es5",
5 | "lib": ["dom", "dom.iterable", "esnext"],
6 | "allowJs": true,
7 | "skipLibCheck": true,
8 | "strict": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "noEmit": true,
11 | "incremental": true,
12 | "esModuleInterop": true,
13 | "module": "esnext",
14 | "moduleResolution": "node",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve",
18 | "plugins": [
19 | {
20 | "name": "next"
21 | }
22 | ],
23 | "composite": false,
24 | "declaration": true,
25 | "declarationMap": true,
26 | "inlineSources": false,
27 | "noUnusedLocals": false,
28 | "noUnusedParameters": false,
29 | "preserveWatchOutput": true,
30 | "baseUrl": ".",
31 | "paths": {
32 | "@queuedash/api": ["../api/src/main"],
33 | "@queuedash/ui": ["../ui/src/main"]
34 | }
35 | },
36 | "references": [
37 | {
38 | "path": "../api"
39 | },
40 | {
41 | "path": "../ui"
42 | }
43 | ],
44 | "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
45 | "exclude": ["node_modules"]
46 | }
47 |
--------------------------------------------------------------------------------
/packages/dev/utils/fake-data.ts:
--------------------------------------------------------------------------------
1 | import Bull from "bull";
2 | import type { JobsOptions as BullMQJobOptions, RepeatOptions } from "bullmq";
3 | import { Queue as BullMQQueue } from "bullmq";
4 | import { faker } from "@faker-js/faker";
5 |
6 | type FakeQueue =
7 | | {
8 | queue: Bull.Queue;
9 | type: "bull";
10 | displayName: string;
11 | jobs: { opts: Bull.JobOptions; data: Record }[];
12 | jobName: (job: Record) => string;
13 | }
14 | | {
15 | queue: BullMQQueue;
16 | type: "bullmq";
17 | displayName: string;
18 | jobs: { opts: BullMQJobOptions; data: Record }[];
19 | jobName: (job: Record) => string;
20 | schedulers: {
21 | name: string;
22 | opts: RepeatOptions;
23 | template: {
24 | name?: string | undefined;
25 | data?: Record;
26 | opts?: Omit<
27 | BullMQJobOptions,
28 | "jobId" | "repeat" | "delay" | "deduplication" | "debounce"
29 | >;
30 | };
31 | }[];
32 | };
33 |
34 | export const queues: FakeQueue[] = [
35 | {
36 | queue: new Bull("flight-bookings"),
37 | type: "bull" as const,
38 | displayName: "Flight bookings",
39 | jobs: [...new Array(50)].map(() => {
40 | return {
41 | data: {
42 | from: faker.location.city(),
43 | to: faker.location.city(),
44 | name: faker.person.fullName(),
45 | priority: faker.number.int({
46 | min: 1,
47 | max: 4,
48 | }),
49 | bags: Math.random() > 0.5 ? 1 : 0,
50 | },
51 | opts: {
52 | lifo: true,
53 | delay: 750,
54 | },
55 | };
56 | }),
57 | jobName: (job: Record) => {
58 | return `${job.from} to ${job.to}`;
59 | },
60 | },
61 | // {
62 | // queue: new Bull("check-in-reminders"),
63 | // type: "bull" as const,
64 | // displayName: "Check-in reminders",
65 | // jobs: [...new Array(50)].map(() => {
66 | // return {
67 | // data: {
68 | // from: faker.location.city(),
69 | // },
70 | // opts: {},
71 | // };
72 | // }),
73 | // jobName: (job) => {
74 | // return `${job.from}`;
75 | // },
76 | // },
77 | // {
78 | // queue: new Bull("monthly-promos"),
79 | // type: "bull" as const,
80 | // displayName: "Monthly promos",
81 | // jobs: [...new Array(50)].map(() => {
82 | // return {
83 | // data: {
84 | // from: faker.location.city(),
85 | // },
86 | // opts: {},
87 | // };
88 | // }),
89 | // jobName: (job: Record) => {
90 | // return `${job.from}`;
91 | // },
92 | // },
93 | {
94 | queue: new BullMQQueue("cancellation-follow-ups"),
95 | type: "bullmq" as const,
96 | displayName: "Cancellation follow-ups",
97 | jobs: [...new Array(50)].map((_, index) => {
98 | return {
99 | data: {
100 | name: faker.person.fullName(),
101 | },
102 | opts: {
103 | priority: index === 4 ? undefined : 1,
104 | },
105 | };
106 | }),
107 | schedulers: [...new Array(3)].map(() => {
108 | return {
109 | name: faker.person.fullName(),
110 | template: {
111 | name: faker.person.fullName(),
112 | data: {
113 | name: faker.person.fullName(),
114 | },
115 | },
116 | opts: {
117 | pattern: "0 0 * * *",
118 | tz: "America/Los_Angeles",
119 | },
120 | };
121 | }),
122 | jobName: (job: Record) => {
123 | return `${job.name}`;
124 | },
125 | },
126 | ];
127 |
--------------------------------------------------------------------------------
/packages/dev/utils/worker.ts:
--------------------------------------------------------------------------------
1 | import { queues } from "./fake-data";
2 | import Bull from "bull";
3 | import { Queue as BullMQQueue, Worker } from "bullmq";
4 |
5 | const sleep = (t: number) =>
6 | new Promise((resolve) => setTimeout(resolve, t * 1000));
7 |
8 | for (const item of queues) {
9 | if (item.type === "bull") {
10 | new Bull(item.queue.name).process(async (job) => {
11 | await sleep(Math.random() * 20);
12 |
13 | const completedCount = await job.queue.getCompletedCount();
14 |
15 | if (completedCount === 48) {
16 | throw new Error("Generic error");
17 | }
18 |
19 | return Promise.resolve();
20 | });
21 | } else {
22 | new Worker(
23 | item.queue.name,
24 | async (job) => {
25 | await sleep(Math.random() * 20);
26 |
27 | job.log("Test log 1");
28 | job.log("Test log 2");
29 | const queue = new BullMQQueue(item.queue.name);
30 |
31 | const completedCount = await queue.getCompletedCount();
32 |
33 | if (completedCount === 48) {
34 | throw new Error("Generic error");
35 | }
36 |
37 | return Promise.resolve();
38 | },
39 | {
40 | connection: {},
41 | }
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/packages/ui/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: "@typescript-eslint/parser",
4 | parserOptions: {
5 | tsconfigRootDir: __dirname,
6 | project: "./tsconfig.json",
7 | },
8 | ignorePatterns: [
9 | "**/node_modules",
10 | "**/.cache",
11 | "dist",
12 | ".eslintrc.js",
13 | "tailwind.config.js",
14 | "postcss.config.js",
15 | ],
16 | plugins: ["@typescript-eslint", "tailwindcss"],
17 | extends: [
18 | "eslint:recommended",
19 | "plugin:@typescript-eslint/recommended",
20 | "plugin:tailwindcss/recommended",
21 | "prettier",
22 | ],
23 | rules: {
24 | "@typescript-eslint/consistent-type-imports": "warn",
25 | "@typescript-eslint/no-unused-vars": "error",
26 | "@typescript-eslint/no-explicit-any": "error",
27 | },
28 | };
29 |
--------------------------------------------------------------------------------
/packages/ui/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @queuedash/ui
2 |
3 | ## 3.6.0
4 |
5 | ### Minor Changes
6 |
7 | - [#50](https://github.com/alexbudure/queuedash/pull/50) [`1fd1326`](https://github.com/alexbudure/queuedash/commit/1fd1326ebc22045c78ecdbbceef2856fcce6cbb6) Thanks [@alexbudure](https://github.com/alexbudure)! - - Job scheduler support
8 | - Per-job logs
9 | - Global pause & resume
10 |
11 | ### Patch Changes
12 |
13 | - Updated dependencies [[`1fd1326`](https://github.com/alexbudure/queuedash/commit/1fd1326ebc22045c78ecdbbceef2856fcce6cbb6)]:
14 | - @queuedash/api@3.6.0
15 |
16 | ## 3.5.0
17 |
18 | ### Minor Changes
19 |
20 | - [#44](https://github.com/alexbudure/queuedash/pull/44) [`ba73bcf`](https://github.com/alexbudure/queuedash/commit/ba73bcf1afec112e6916a4c6beb132a8d9c7edd4) Thanks [@huv1k](https://github.com/huv1k)! - Add support for hono
21 |
22 | ### Patch Changes
23 |
24 | - Updated dependencies [[`ba73bcf`](https://github.com/alexbudure/queuedash/commit/ba73bcf1afec112e6916a4c6beb132a8d9c7edd4)]:
25 | - @queuedash/api@3.5.0
26 |
27 | ## 3.4.0
28 |
29 | ### Minor Changes
30 |
31 | - [#40](https://github.com/alexbudure/queuedash/pull/40) [`0ccd37a`](https://github.com/alexbudure/queuedash/commit/0ccd37afe5a3d8b158109d1ec80a02cead1480bf) Thanks [@fukouda](https://github.com/fukouda)! - Add the ability to pass in hook handlers on Fastify
32 |
33 | ### Patch Changes
34 |
35 | - Updated dependencies [[`0ccd37a`](https://github.com/alexbudure/queuedash/commit/0ccd37afe5a3d8b158109d1ec80a02cead1480bf)]:
36 | - @queuedash/api@3.4.0
37 |
38 | ## 3.3.0
39 |
40 | ### Minor Changes
41 |
42 | - [#38](https://github.com/alexbudure/queuedash/pull/38) [`00346f4`](https://github.com/alexbudure/queuedash/commit/00346f4c11fdae742dae1981061f044aee66697b) Thanks [@alexbudure](https://github.com/alexbudure)! - Improve React 19 compatibility
43 |
44 | ### Patch Changes
45 |
46 | - Updated dependencies [[`00346f4`](https://github.com/alexbudure/queuedash/commit/00346f4c11fdae742dae1981061f044aee66697b)]:
47 | - @queuedash/api@3.3.0
48 |
49 | ## 3.2.0
50 |
51 | ### Minor Changes
52 |
53 | - [`8e5c7ac`](https://github.com/alexbudure/queuedash/commit/8e5c7ac06bb13674e32b4cd9b1b7c65913e122af) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix importing API
54 |
55 | ### Patch Changes
56 |
57 | - Updated dependencies [[`8e5c7ac`](https://github.com/alexbudure/queuedash/commit/8e5c7ac06bb13674e32b4cd9b1b7c65913e122af)]:
58 | - @queuedash/api@3.2.0
59 |
60 | ## 3.1.0
61 |
62 | ### Minor Changes
63 |
64 | - [#33](https://github.com/alexbudure/queuedash/pull/33) [`011ad3b`](https://github.com/alexbudure/queuedash/commit/011ad3bca2b50b4568fa7edc7bf314948e84eeb9) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix bundle strategy for API
65 |
66 | ### Patch Changes
67 |
68 | - Updated dependencies [[`011ad3b`](https://github.com/alexbudure/queuedash/commit/011ad3bca2b50b4568fa7edc7bf314948e84eeb9)]:
69 | - @queuedash/api@3.1.0
70 |
71 | ## 3.0.0
72 |
73 | ### Major Changes
74 |
75 | - [#21](https://github.com/alexbudure/queuedash/pull/21) [`6692303`](https://github.com/alexbudure/queuedash/commit/6692303bde835b9934e2ae962e4727357f0d4afe) Thanks [@alexbudure](https://github.com/alexbudure)! - This version upgrades core dependencies to their latest major versions, including Elysia, BullMQ, and tRPC
76 |
77 | ### Patch Changes
78 |
79 | - Updated dependencies [[`6692303`](https://github.com/alexbudure/queuedash/commit/6692303bde835b9934e2ae962e4727357f0d4afe)]:
80 | - @queuedash/api@3.0.0
81 |
82 | ## 2.1.1
83 |
84 | ### Patch Changes
85 |
86 | - [#26](https://github.com/alexbudure/queuedash/pull/26) [`2e0956c`](https://github.com/alexbudure/queuedash/commit/2e0956c586b9f5f3190f169e363f76230d037686) Thanks [@p3drosola](https://github.com/p3drosola)! - Improve UI support for long job ids
87 |
88 | - Updated dependencies [[`2e0956c`](https://github.com/alexbudure/queuedash/commit/2e0956c586b9f5f3190f169e363f76230d037686)]:
89 | - @queuedash/api@2.1.1
90 |
91 | ## 2.1.0
92 |
93 | ### Minor Changes
94 |
95 | - [#23](https://github.com/alexbudure/queuedash/pull/23) [`7a2e3c0`](https://github.com/alexbudure/queuedash/commit/7a2e3c000da0b34c4c3a4dd2471e2e19738d1e6d) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix bull api differences
96 |
97 | ### Patch Changes
98 |
99 | - Updated dependencies [[`7a2e3c0`](https://github.com/alexbudure/queuedash/commit/7a2e3c000da0b34c4c3a4dd2471e2e19738d1e6d)]:
100 | - @queuedash/api@2.1.0
101 |
102 | ## 2.0.5
103 |
104 | ### Patch Changes
105 |
106 | - [`bc47dd5`](https://github.com/alexbudure/queuedash/commit/bc47dd5de7a5ed32cd82365dc27073282afc45be) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix express bundling with app
107 |
108 | - Updated dependencies [[`bc47dd5`](https://github.com/alexbudure/queuedash/commit/bc47dd5de7a5ed32cd82365dc27073282afc45be)]:
109 | - @queuedash/api@2.0.5
110 |
111 | ## 2.0.4
112 |
113 | ### Patch Changes
114 |
115 | - [`0948ec2`](https://github.com/alexbudure/queuedash/commit/0948ec21985d33b3ffbb0ec220664493382579da) Thanks [@alexbudure](https://github.com/alexbudure)! - Move html into each adapter
116 |
117 | - Updated dependencies [[`0948ec2`](https://github.com/alexbudure/queuedash/commit/0948ec21985d33b3ffbb0ec220664493382579da)]:
118 | - @queuedash/api@2.0.4
119 |
120 | ## 2.0.3
121 |
122 | ### Patch Changes
123 |
124 | - [`dee7163`](https://github.com/alexbudure/queuedash/commit/dee71633d33c8bceee9bde84a0b340f899adeaf8) Thanks [@alexbudure](https://github.com/alexbudure)! - Remove unnecessary express app in adapter
125 |
126 | - Updated dependencies [[`dee7163`](https://github.com/alexbudure/queuedash/commit/dee71633d33c8bceee9bde84a0b340f899adeaf8)]:
127 | - @queuedash/api@2.0.3
128 |
129 | ## 2.0.2
130 |
131 | ### Patch Changes
132 |
133 | - [`6680a6a`](https://github.com/alexbudure/queuedash/commit/6680a6a5ece43fef248fedacb31f8fae2242d2d3) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix the adapters.. again
134 |
135 | - Updated dependencies [[`6680a6a`](https://github.com/alexbudure/queuedash/commit/6680a6a5ece43fef248fedacb31f8fae2242d2d3)]:
136 | - @queuedash/api@2.0.2
137 |
138 | ## 2.0.1
139 |
140 | ### Patch Changes
141 |
142 | - [`8d2eadd`](https://github.com/alexbudure/queuedash/commit/8d2eadd9ad547ff2e893662474a228bf340f0728) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix the middlewares for Fastify and Express
143 |
144 | - Updated dependencies [[`8d2eadd`](https://github.com/alexbudure/queuedash/commit/8d2eadd9ad547ff2e893662474a228bf340f0728)]:
145 | - @queuedash/api@2.0.1
146 |
147 | ## 2.0.0
148 |
149 | ### Major Changes
150 |
151 | - [#5](https://github.com/alexbudure/queuedash/pull/5) [`1f794d1`](https://github.com/alexbudure/queuedash/commit/1f794d1679225718dcc670e9c7eb59564fee1bc6) Thanks [@alexbudure](https://github.com/alexbudure)! - Updated all adapters to use a more natural API and added real-time Redis info on the queue detail page
152 |
153 | ***
154 |
155 | ### Breaking changes
156 |
157 | **Express**
158 |
159 | Before:
160 |
161 | ```typescript
162 | createQueueDashExpressMiddleware({
163 | app,
164 | baseUrl: "/queuedash",
165 | ctx: {
166 | queues: [
167 | {
168 | queue: new Bull("report-queue"),
169 | displayName: "Reports",
170 | type: "bull" as const,
171 | },
172 | ],
173 | },
174 | });
175 | ```
176 |
177 | After:
178 |
179 | ```typescript
180 | app.use(
181 | "/queuedash",
182 | createQueueDashExpressMiddleware({
183 | ctx: {
184 | queues: [
185 | {
186 | queue: new Bull("report-queue"),
187 | displayName: "Reports",
188 | type: "bull" as const,
189 | },
190 | ],
191 | },
192 | }),
193 | );
194 | ```
195 |
196 | **Fastify**
197 |
198 | Before:
199 |
200 | ```typescript
201 | createQueueDashFastifyMiddleware({
202 | server,
203 | baseUrl: "/queuedash",
204 | ctx: {
205 | queues: [
206 | {
207 | queue: new Bull("report-queue"),
208 | displayName: "Reports",
209 | type: "bull" as const,
210 | },
211 | ],
212 | },
213 | });
214 | ```
215 |
216 | After:
217 |
218 | ```typescript
219 | server.register(fastifyQueueDashPlugin, {
220 | baseUrl: "/queuedash",
221 | ctx: {
222 | queues: [
223 | {
224 | queue: new Bull("report-queue"),
225 | displayName: "Reports",
226 | type: "bull" as const,
227 | },
228 | ],
229 | },
230 | });
231 | ```
232 |
233 | ### Patch Changes
234 |
235 | - Updated dependencies [[`1f794d1`](https://github.com/alexbudure/queuedash/commit/1f794d1679225718dcc670e9c7eb59564fee1bc6)]:
236 | - @queuedash/api@2.0.0
237 |
238 | ## 1.2.1
239 |
240 | ### Patch Changes
241 |
242 | - [#12](https://github.com/alexbudure/queuedash/pull/12) [`d79c8ff`](https://github.com/alexbudure/queuedash/commit/d79c8ffe34ae36c74d0663dd2e29e6c93327bf8c) Thanks [@alexbudure](https://github.com/alexbudure)! - Fix Elysia plugin
243 |
244 | - Updated dependencies [[`d79c8ff`](https://github.com/alexbudure/queuedash/commit/d79c8ffe34ae36c74d0663dd2e29e6c93327bf8c)]:
245 | - @queuedash/api@1.2.1
246 |
247 | ## 1.2.0
248 |
249 | ### Minor Changes
250 |
251 | - [`9aaec9a`](https://github.com/alexbudure/queuedash/commit/9aaec9a21c091680cb30a67e9322eedd3e16dbe8) Thanks [@alexbudure](https://github.com/alexbudure)! - Support for Elysia
252 |
253 | ### Patch Changes
254 |
255 | - Updated dependencies [[`9aaec9a`](https://github.com/alexbudure/queuedash/commit/9aaec9a21c091680cb30a67e9322eedd3e16dbe8)]:
256 | - @queuedash/api@1.2.0
257 |
258 | ## 1.1.0
259 |
260 | ### Minor Changes
261 |
262 | - [#7](https://github.com/alexbudure/queuedash/pull/7) [`885aee3`](https://github.com/alexbudure/queuedash/commit/885aee3cecac687d05f5b18cd1855fcb5522f899) Thanks [@alexbudure](https://github.com/alexbudure)! - Add support for prioritized jobs
263 |
264 | ### Patch Changes
265 |
266 | - Updated dependencies [[`885aee3`](https://github.com/alexbudure/queuedash/commit/885aee3cecac687d05f5b18cd1855fcb5522f899)]:
267 | - @queuedash/api@1.1.0
268 |
269 | ## 1.0.1
270 |
271 | ### Patch Changes
272 |
273 | - [#3](https://github.com/alexbudure/queuedash/pull/3) [`a385f9f`](https://github.com/alexbudure/queuedash/commit/a385f9f9e76df4cea8e69d7e218b65915acef3bf) Thanks [@alexbudure](https://github.com/alexbudure)! - Tighten adapter types to work with NestJS
274 |
275 | - Updated dependencies [[`a385f9f`](https://github.com/alexbudure/queuedash/commit/a385f9f9e76df4cea8e69d7e218b65915acef3bf)]:
276 | - @queuedash/api@1.0.1
277 |
278 | ## 1.0.0
279 |
280 | ### Major Changes
281 |
282 | - [#1](https://github.com/alexbudure/queuedash/pull/1) [`c96b93d`](https://github.com/alexbudure/queuedash/commit/c96b93d9659bbb34248ab377e6659ebfb1fc3dd8) Thanks [@alexbudure](https://github.com/alexbudure)! - QueueDash v1 🎉
283 |
284 | - 😍 Simple, clean, and compact UI
285 | - 🧙 Add jobs to your queue with ease
286 | - 🪄 Retry, remove, and more convenient actions for your jobs
287 | - 📊 Stats for job counts, job durations, and job wait times
288 | - ✨ Top-level overview page of all queues
289 | - 🔋 Integrates with Next.js, Express.js, and Fastify
290 | - ⚡️ Compatible with Bull, BullMQ, and Bee-Queue
291 |
292 | ### Patch Changes
293 |
294 | - Updated dependencies [[`c96b93d`](https://github.com/alexbudure/queuedash/commit/c96b93d9659bbb34248ab377e6659ebfb1fc3dd8)]:
295 | - @queuedash/api@1.0.0
296 |
--------------------------------------------------------------------------------
/packages/ui/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | A stunning, sleek dashboard for Bull, BullMQ, and Bee-Queue.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ## Features
21 |
22 | - 😍 Simple, clean, and compact UI
23 | - 🧙 Add jobs to your queue with ease
24 | - 🪄 Retry, remove, and more convenient actions for your jobs
25 | - 📊 Stats for job counts, job durations, and job wait times
26 | - ✨ Top-level overview page of all queues
27 | - 🔋 Integrates with Next.js, Express.js, and Fastify
28 | - ⚡️ Compatible with Bull, BullMQ, and Bee-Queue
29 |
30 | ## Getting Started
31 |
32 | ### Express
33 |
34 | `pnpm install @queuedash/api`
35 |
36 | ```typescript
37 | import express from "express";
38 | import Bull from "bull";
39 | import { createQueueDashExpressMiddleware } from "@queuedash/api";
40 |
41 | const app = express();
42 |
43 | const reportQueue = new Bull("report-queue");
44 |
45 | app.use(
46 | "/queuedash",
47 | createQueueDashExpressMiddleware({
48 | ctx: {
49 | queues: [
50 | {
51 | queue: reportQueue,
52 | displayName: "Reports",
53 | type: "bull" as const,
54 | },
55 | ],
56 | },
57 | })
58 | );
59 |
60 | app.listen(3000, () => {
61 | console.log("Listening on port 3000");
62 | console.log("Visit http://localhost:3000/queuedash");
63 | });
64 | ```
65 |
66 | ### Next.js
67 |
68 | `pnpm install @queuedash/api @queuedash/ui`
69 |
70 | ```typescript jsx
71 | // pages/admin/queuedash/[[...slug]].tsx
72 | import { QueueDashApp } from "@queuedash/ui";
73 |
74 | function getBaseUrl() {
75 | if (process.env.VERCEL_URL) {
76 | return `https://${process.env.VERCEL_URL}/api/queuedash`;
77 | }
78 |
79 | return `http://localhost:${process.env.PORT ?? 3000}/api/queuedash`;
80 | }
81 |
82 | const QueueDashPages = () => {
83 | return ;
84 | };
85 |
86 | export default QueueDashPages;
87 |
88 | // pages/api/queuedash/[trpc].ts
89 | import * as trpcNext from "@trpc/server/adapters/next";
90 | import { appRouter } from "@queuedash/api";
91 |
92 | const reportQueue = new Bull("report-queue");
93 |
94 | export default trpcNext.createNextApiHandler({
95 | router: appRouter,
96 | batching: {
97 | enabled: true,
98 | },
99 | createContext: () => ({
100 | queues: [
101 | {
102 | queue: reportQueue,
103 | displayName: "Reports",
104 | type: "bull" as const,
105 | },
106 | ],
107 | }),
108 | });
109 | ```
110 |
111 | See the [./examples](./examples) folder for more.
112 |
113 | ---
114 |
115 | ## API Reference
116 |
117 | ### `createQueueDash<*>Middleware`
118 |
119 | ```typescript
120 | type QueueDashMiddlewareOptions = {
121 | app: express.Application | FastifyInstance; // Express or Fastify app
122 | baseUrl: string; // Base path for the API and UI
123 | ctx: QueueDashContext; // Context for the UI
124 | };
125 |
126 | type QueueDashContext = {
127 | queues: QueueDashQueue[]; // Array of queues to display
128 | };
129 |
130 | type QueueDashQueue = {
131 | queue: Bull.Queue | BullMQ.Queue | BeeQueue; // Queue instance
132 | displayName: string; // Display name for the queue
133 | type: "bull" | "bullmq" | "bee"; // Queue type
134 | };
135 | ```
136 |
137 | ### ``
138 |
139 | ```typescript jsx
140 | type QueueDashAppProps = {
141 | apiUrl: string; // URL to the API endpoint
142 | basename: string; // Base path for the app
143 | };
144 | ```
145 |
146 | ## Roadmap
147 |
148 | - Supports Celery and other queueing systems
149 | - Command+K bar and shortcuts
150 | - Ability to whitelabel the UI
151 |
152 | ## Pro Version
153 |
154 | Right now, QueueDash simply taps into your Redis instance, making it very easy to set up, but also limited in functionality.
155 |
156 | I'm thinking about building a free-to-host version on top of this which will require external services (db, auth, etc.), but it will make the following features possible:
157 |
158 | - Alerts and notifications
159 | - Quick search and filtering
160 | - Queue trends and analytics
161 | - Invite team members
162 |
163 | If you're interested in this version, please let me know!
164 |
165 | ## Acknowledgements
166 |
167 | QueueDash was inspired by some great open source projects. Here's a few of them:
168 |
169 | - [bull-board](https://github.com/vcapretz/bull-board)
170 | - [bull-monitor](https://github.com/s-r-x/bull-monitor)
171 | - [bull-arena](https://github.com/bee-queue/arena)
172 |
--------------------------------------------------------------------------------
/packages/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@queuedash/ui",
3 | "version": "3.6.0",
4 | "description": "A stunning, sleek dashboard for Bull, BullMQ, and Bee-Queue",
5 | "main": "./dist/main.js",
6 | "module": "./dist/main.mjs",
7 | "types": "./dist/src/main.d.ts",
8 | "files": [
9 | "dist"
10 | ],
11 | "keywords": [
12 | "bull",
13 | "bee-queue",
14 | "queue",
15 | "bullmq",
16 | "dashboard"
17 | ],
18 | "scripts": {
19 | "build": "tsc && vite build && npx tailwindcss -i src/styles/global.css -o ./dist/styles.css",
20 | "dev": "pnpm run build --watch",
21 | "tailwind": "npx tailwindcss -i src/styles/global.css -o ./dist/styles.css --watch",
22 | "lint": "eslint ./ --fix"
23 | },
24 | "dependencies": {
25 | "@monaco-editor/react": "^4.7.0",
26 | "@queuedash/api": "workspace:*",
27 | "@radix-ui/react-checkbox": "^1.1.4",
28 | "@radix-ui/react-icons": "^1.3.2",
29 | "@tanstack/react-query": "^5.71.10",
30 | "@tanstack/react-table": "^8.21.2",
31 | "@trpc/client": "^11.0.2",
32 | "@trpc/react-query": "^11.0.2",
33 | "@trpc/server": "^11.0.2",
34 | "clsx": "^2.1.1",
35 | "cronstrue": "^2.61.0",
36 | "date-fns": "^4.1.0",
37 | "date-fns-tz": "^3.2.0",
38 | "monaco-editor": "^0.52.2",
39 | "react-aria-components": "^1.7.1",
40 | "react-intersection-observer": "^9.16.0",
41 | "react-json-tree": "^0.20.0",
42 | "react-router": "^7.5.0",
43 | "sonner": "^2.0.3",
44 | "zod": "^3.24.2"
45 | },
46 | "devDependencies": {
47 | "@rollup/plugin-typescript": "^12.1.2",
48 | "@types/react": "^19.1.0",
49 | "@types/react-dom": "^19.1.1",
50 | "@typescript-eslint/eslint-plugin": "^8.29.0",
51 | "@typescript-eslint/parser": "^8.29.0",
52 | "@vitejs/plugin-react": "^4.3.4",
53 | "autoprefixer": "^10.4.21",
54 | "eslint": "^9.24.0",
55 | "eslint-config-prettier": "^10.1.1",
56 | "eslint-plugin-tailwindcss": "^3.18.0",
57 | "postcss": "^8.5.3",
58 | "prettier": "^3.5.3",
59 | "react": "^19.1.0",
60 | "react-dom": "^19.1.0",
61 | "rollup-plugin-typescript-paths": "^1.5.0",
62 | "tailwindcss": "^3.4.16",
63 | "tailwindcss-radix": "^3.0.5",
64 | "typescript": "^5.8.3",
65 | "vite": "^5.4.19"
66 | },
67 | "peerDependencies": {
68 | "react": ">=18",
69 | "react-dom": ">=18"
70 | },
71 | "license": "MIT"
72 | }
73 |
--------------------------------------------------------------------------------
/packages/ui/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/packages/ui/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2 | import { httpBatchLink } from "@trpc/client";
3 | import { useEffect, useState } from "react";
4 | import { trpc } from "./utils/trpc";
5 | import { BrowserRouter, Route, Routes } from "react-router";
6 | import { QueuePage } from "./pages/QueuePage";
7 | import type { UserPreferences } from "./components/ThemeSwitcher";
8 | import { HomePage } from "./pages/HomePage";
9 | import { Toaster } from "sonner";
10 |
11 | type QueueDashPagesProps = {
12 | // URL to the API
13 | apiUrl: string;
14 | // Base path for the app
15 | basename: string;
16 | };
17 | export const App = ({ apiUrl, basename }: QueueDashPagesProps) => {
18 | const [ready, setReady] = useState(false);
19 | const [queryClient] = useState(() => new QueryClient());
20 | const [trpcClient] = useState(() =>
21 | trpc.createClient({
22 | links: [
23 | httpBatchLink({
24 | url: apiUrl,
25 | }),
26 | ],
27 | }),
28 | );
29 |
30 | useEffect(() => {
31 | const userPreferences: UserPreferences | null = JSON.parse(
32 | localStorage.getItem("user-preferences") || "null",
33 | );
34 |
35 | if (userPreferences) {
36 | if (
37 | userPreferences.theme === "dark" ||
38 | (userPreferences.theme === "system" &&
39 | window.matchMedia("(prefers-color-scheme: dark)").matches)
40 | ) {
41 | document.documentElement.classList.add("dark");
42 | } else {
43 | document.documentElement.classList.remove("dark");
44 | }
45 | } else {
46 | if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
47 | document.documentElement.classList.add("dark");
48 | } else {
49 | document.documentElement.classList.remove("dark");
50 | }
51 | }
52 |
53 | setReady(true);
54 | }, []);
55 |
56 | if (!ready) {
57 | return null;
58 | }
59 |
60 | return (
61 |
62 |
63 |
64 |
65 |
66 | } />
67 | } />
68 |
69 |
70 |
71 |
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/packages/ui/src/components/ActionMenu.tsx:
--------------------------------------------------------------------------------
1 | import { DotsVerticalIcon } from "@radix-ui/react-icons";
2 | import {
3 | Button,
4 | Menu,
5 | MenuItem,
6 | MenuTrigger,
7 | Popover,
8 | } from "react-aria-components";
9 |
10 | type Action = {
11 | label: string;
12 | onSelect: () => void;
13 | icon: React.ReactNode;
14 | };
15 | type ActionMenuProps = {
16 | actions: Action[];
17 | // ariaLabel: todo: add aria-label
18 | };
19 | export const ActionMenu = ({ actions }: ActionMenuProps) => {
20 | return (
21 |
22 |
28 |
29 |
34 |
48 |
49 |
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/packages/ui/src/components/Alert.tsx:
--------------------------------------------------------------------------------
1 | import type { PropsWithChildren, ReactElement } from "react";
2 | import { Button } from "./Button";
3 | import {
4 | Button as AriaButton,
5 | Dialog,
6 | DialogTrigger,
7 | Heading,
8 | Modal,
9 | } from "react-aria-components";
10 |
11 | type AlertProps = {
12 | title: string;
13 | description: string;
14 | action: ReactElement;
15 | };
16 | export const Alert = ({
17 | title,
18 | description,
19 | action,
20 | children,
21 | }: PropsWithChildren) => {
22 | return (
23 |
24 | {children}
25 |
26 |
43 |
44 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/packages/ui/src/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import { SlashIcon } from "@radix-ui/react-icons";
2 | import type { ReactElement } from "react";
3 | import { clsx } from "clsx";
4 |
5 | type ButtonProps = {
6 | variant?: "outline" | "filled";
7 | colorScheme?: "yellow" | "slate" | "red";
8 | size?: "sm" | "md" | "lg";
9 | icon?: ReactElement;
10 | label: string;
11 | isLoading?: boolean;
12 | disabled?: boolean;
13 | onClick?: () => void;
14 | };
15 | export const Button = ({
16 | colorScheme = "slate",
17 | variant = "outline",
18 | size = "md",
19 | icon,
20 | label,
21 | isLoading,
22 | onClick,
23 | disabled,
24 | }: ButtonProps) => {
25 | return (
26 |
70 | );
71 | };
72 |
--------------------------------------------------------------------------------
/packages/ui/src/components/Checkbox.tsx:
--------------------------------------------------------------------------------
1 | import * as RadixCheckbox from "@radix-ui/react-checkbox";
2 | import { CheckIcon, DividerHorizontalIcon } from "@radix-ui/react-icons";
3 | import { clsx } from "clsx";
4 |
5 | export const Checkbox = ({
6 | className,
7 | ...props
8 | }: RadixCheckbox.CheckboxProps) => {
9 | return (
10 | e.stopPropagation()}
24 | {...props}
25 | >
26 |
27 | {props.checked === "indeterminate" && (
28 |
29 | )}
30 | {props.checked === true && }
31 |
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/packages/ui/src/components/ErrorCard.tsx:
--------------------------------------------------------------------------------
1 | type ErrorCardProps = {
2 | message: string;
3 | };
4 | export const ErrorCard = ({ message }: ErrorCardProps) => {
5 | return (
6 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/packages/ui/src/components/JobActionMenu.tsx:
--------------------------------------------------------------------------------
1 | import { ActionMenu } from "./ActionMenu";
2 | import type { Job } from "../utils/trpc";
3 | import { trpc } from "../utils/trpc";
4 | import {
5 | CounterClockwiseClockIcon,
6 | HobbyKnifeIcon,
7 | TrashIcon,
8 | } from "@radix-ui/react-icons";
9 | import { useEffect } from "react";
10 |
11 | type JobActionMenuProps = {
12 | job: Job;
13 | queueName: string;
14 | onRemove?: () => void;
15 | };
16 | export const JobActionMenu = ({
17 | job,
18 | queueName,
19 | onRemove,
20 | }: JobActionMenuProps) => {
21 | const { mutate: retry, isSuccess: retrySuccess } =
22 | trpc.job.retry.useMutation();
23 | const { mutate: discard, isSuccess: discardSuccess } =
24 | trpc.job.discard.useMutation();
25 | const { mutate: rerun, isSuccess: rerunSuccess } =
26 | trpc.job.rerun.useMutation();
27 | const { mutate: remove, isSuccess: removeSuccess } =
28 | trpc.job.remove.useMutation();
29 |
30 | useEffect(() => {
31 | if (retrySuccess || discardSuccess || rerunSuccess || removeSuccess) {
32 | onRemove?.();
33 | }
34 | }, [retrySuccess, discardSuccess, rerunSuccess, removeSuccess, onRemove]);
35 |
36 | const input = {
37 | queueName,
38 | jobId: job.id,
39 | };
40 |
41 | return (
42 | {
49 | retry(input);
50 | },
51 | icon: ,
52 | },
53 | ]
54 | : []),
55 | ...(job.finishedAt
56 | ? [
57 | {
58 | label: "Rerun",
59 | onSelect: () => {
60 | rerun(input);
61 | },
62 | icon: ,
63 | },
64 | ]
65 | : [
66 | {
67 | label: "Discard",
68 | onSelect: () => {
69 | discard(input);
70 | },
71 | icon: ,
72 | },
73 | ]),
74 |
75 | {
76 | label: "Remove",
77 | onSelect: () => {
78 | remove(input);
79 | },
80 | icon: ,
81 | },
82 | ]}
83 | />
84 | );
85 | };
86 |
--------------------------------------------------------------------------------
/packages/ui/src/components/JobModal.tsx:
--------------------------------------------------------------------------------
1 | import { CounterClockwiseClockIcon, Cross2Icon } from "@radix-ui/react-icons";
2 | import type { Job } from "../utils/trpc";
3 | import { JSONTree } from "react-json-tree";
4 | import { JobActionMenu } from "./JobActionMenu";
5 | import { Button } from "./Button";
6 | import { trpc } from "../utils/trpc";
7 | import { Dialog, Heading, Modal } from "react-aria-components";
8 |
9 | type JobModalProps = {
10 | job: Job;
11 | onDismiss: () => void;
12 | queueName: string;
13 | };
14 |
15 | export const JobModal = ({ job, queueName, onDismiss }: JobModalProps) => {
16 | const queueReq = trpc.queue.byName.useQuery({
17 | queueName,
18 | });
19 |
20 | const { mutate: retry } = trpc.job.retry.useMutation();
21 | const { data } = trpc.job.logs.useQuery({
22 | jobId: job.id,
23 | queueName,
24 | });
25 |
26 | return (
27 | {
31 | if (!isOpen) {
32 | onDismiss();
33 | }
34 | }}
35 | className="fixed inset-0 bg-black/10"
36 | >
37 |
131 |
132 | );
133 | };
134 |
--------------------------------------------------------------------------------
/packages/ui/src/components/JobOptionTag.tsx:
--------------------------------------------------------------------------------
1 | type JobOptionTagProps = {
2 | label: string | number;
3 | icon: React.ReactNode;
4 | };
5 | export const JobOptionTag = ({ label, icon }: JobOptionTagProps) => {
6 | return (
7 |
8 | {icon}
9 | {label}
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/packages/ui/src/components/JobTableSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import { JOBS_PER_PAGE } from "../utils/config";
2 | import { clsx } from "clsx";
3 | import { Skeleton } from "./Skeleton";
4 |
5 | export const JobTableSkeleton = () => {
6 | return (
7 |
8 |
9 | Placeholder
10 |
11 | {[...new Array(JOBS_PER_PAGE)].map((_, i) => {
12 | return (
13 |
22 |
23 |
24 | );
25 | })}
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/packages/ui/src/components/Layout.tsx:
--------------------------------------------------------------------------------
1 | import type { FC, PropsWithChildren, ReactNode } from "react";
2 | import { trpc } from "../utils/trpc";
3 | import { NavLink } from "react-router";
4 | import { Skeleton } from "./Skeleton";
5 | import {
6 | GitHubLogoIcon,
7 | DashboardIcon,
8 | ShadowNoneIcon,
9 | } from "@radix-ui/react-icons";
10 | import { ThemeSwitcher } from "./ThemeSwitcher";
11 | import { ErrorCard } from "./ErrorCard";
12 |
13 | type QueueNavLinkProps = {
14 | to: string;
15 | label: string;
16 | icon?: ReactNode;
17 | };
18 | const QueueNavLink = ({ to, label, icon }: QueueNavLinkProps) => {
19 | return (
20 |
23 | `relative -ml-px backdrop-blur-50 flex w-full items-center space-x-2 rounded-md pl-4 py-1 transition duration-150 ease-in-out ${
24 | isActive
25 | ? "bg-slate-100/90 font-medium text-slate-900 dark:bg-slate-900 dark:text-brand-300"
26 | : "text-slate-500 dark:text-slate-400 border-slate-100 hover:bg-slate-50 dark:hover:bg-slate-900 hover:border-slate-300 hover:text-slate-900 dark:border-slate-700 dark:hover:border-slate-600 dark:hover:text-slate-100"
27 | }`
28 | }
29 | >
30 | {icon}
31 | {label}
32 |
33 | );
34 | };
35 |
36 | export const Layout: FC = ({ children }) => {
37 | const { data, isLoading, isError } = trpc.queue.list.useQuery();
38 |
39 | return (
40 |
41 |
42 |
43 |
67 |
68 |
72 |
73 |
74 |
78 | }
79 | to={`../`}
80 | />
81 |
82 |
83 | {isLoading ? (
84 | [...new Array(10)].map((_, i) => {
85 | return ;
86 | })
87 | ) : isError ? (
88 |
89 | ) : (
90 | data?.map((queue) => {
91 | return (
92 |
97 | );
98 | })
99 | )}
100 |
101 |
102 |
103 |
104 |
116 |
117 |
118 |
119 |
122 |
123 | );
124 | };
125 |
--------------------------------------------------------------------------------
/packages/ui/src/components/QueueActionMenu.tsx:
--------------------------------------------------------------------------------
1 | import { ActionMenu } from "./ActionMenu";
2 | import type { Queue } from "../utils/trpc";
3 | import { trpc } from "../utils/trpc";
4 | import {
5 | PauseIcon,
6 | PlayIcon,
7 | PlusIcon,
8 | TrashIcon,
9 | } from "@radix-ui/react-icons";
10 | import { useState } from "react";
11 | import { AddJobModal } from "./AddJobModal";
12 |
13 | type QueueActionMenuProps = {
14 | queue: Queue;
15 | };
16 | export const QueueActionMenu = ({ queue }: QueueActionMenuProps) => {
17 | const { mutate: pause } = trpc.queue.pause.useMutation();
18 | const { mutate: resume } = trpc.queue.resume.useMutation();
19 | const { mutate: empty } = trpc.queue.empty.useMutation();
20 | const [showAddJobModal, setShowAddJobModal] = useState(false);
21 | const input = {
22 | queueName: queue.name,
23 | };
24 |
25 | return (
26 | <>
27 | {
32 | if (queue.paused) {
33 | resume(input);
34 | } else {
35 | pause(input);
36 | }
37 | },
38 | icon: queue.paused ? : ,
39 | },
40 | {
41 | label: "Add job",
42 | onSelect: () => {
43 | setShowAddJobModal(true);
44 | },
45 | icon: ,
46 | },
47 | {
48 | label: "Empty",
49 | onSelect: () => {
50 | empty(input);
51 | },
52 | icon: ,
53 | },
54 | ]}
55 | />
56 | {showAddJobModal ? (
57 | setShowAddJobModal(false)}
60 | />
61 | ) : null}
62 | >
63 | );
64 | };
65 |
--------------------------------------------------------------------------------
/packages/ui/src/components/QueueStatusTabs.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router";
2 | import { clsx } from "clsx";
3 | import { Button } from "./Button";
4 | import { TrashIcon } from "@radix-ui/react-icons";
5 | import type { RouterOutput, Status } from "../utils/trpc";
6 | import { trpc } from "../utils/trpc";
7 | import { Alert } from "./Alert";
8 | import { toast } from "sonner";
9 |
10 | type Tab = {
11 | name: string;
12 | status: Status;
13 | };
14 |
15 | type QueueStatusTabsProps = {
16 | showCleanAllButton: boolean;
17 | status: Status;
18 | queueName: string;
19 | queue?: RouterOutput["queue"]["byName"];
20 | };
21 | export const QueueStatusTabs = ({
22 | showCleanAllButton,
23 | queueName,
24 | status,
25 | queue,
26 | }: QueueStatusTabsProps) => {
27 | const { mutate: cleanQueue, status: cleanQueueStatus } =
28 | trpc.queue.clean.useMutation({
29 | onSuccess() {
30 | toast.success(`All ${status} jobs have been removed`);
31 | },
32 | });
33 |
34 | const tabs: Tab[] = [
35 | {
36 | name: "Completed",
37 | status: "completed",
38 | },
39 | {
40 | name: "Failed",
41 | status: "failed",
42 | },
43 | {
44 | name: "Active",
45 | status: "active",
46 | },
47 | ...(queue && "prioritized" in queue.counts
48 | ? [{ name: "Prioritized", status: "prioritized" as const }]
49 | : []),
50 | {
51 | name: "Waiting",
52 | status: "waiting",
53 | },
54 | {
55 | name: "Delayed",
56 | status: "delayed",
57 | },
58 | {
59 | name: "Paused",
60 | status: "paused",
61 | },
62 | ];
63 |
64 | return (
65 |
66 |
67 | {tabs.map((tab) => {
68 | const isActive = tab.status === status;
69 | return (
70 |
95 | {tab.name}
96 | {queue?.counts[tab.status] ? (
97 |
119 | {queue.counts[tab.status]}
120 |
121 | ) : null}
122 |
123 | );
124 | })}
125 |
126 | {showCleanAllButton ? (
127 |
136 | cleanQueue({
137 | queueName,
138 | status,
139 | })
140 | }
141 | />
142 | }
143 | >
144 | }
147 | label="Clean all"
148 | size="sm"
149 | isLoading={cleanQueueStatus === "pending"}
150 | />
151 |
152 | ) : null}
153 |
154 | );
155 | };
156 |
--------------------------------------------------------------------------------
/packages/ui/src/components/SchedulerActionMenu.tsx:
--------------------------------------------------------------------------------
1 | import { ActionMenu } from "./ActionMenu";
2 | import type { Scheduler } from "../utils/trpc";
3 | import { trpc } from "../utils/trpc";
4 | import { TrashIcon } from "@radix-ui/react-icons";
5 | import { useEffect } from "react";
6 |
7 | type SchedulerActionMenuProps = {
8 | scheduler: Scheduler;
9 | queueName: string;
10 | onRemove?: () => void;
11 | };
12 | export const SchedulerActionMenu = ({
13 | scheduler,
14 | queueName,
15 | onRemove,
16 | }: SchedulerActionMenuProps) => {
17 | const { mutate: remove, isSuccess: removeSuccess } =
18 | trpc.scheduler.remove.useMutation();
19 |
20 | useEffect(() => {
21 | if (removeSuccess) {
22 | onRemove?.();
23 | }
24 | }, [removeSuccess, onRemove]);
25 |
26 | return (
27 | {
32 | remove({
33 | queueName,
34 | jobSchedulerId: scheduler.key,
35 | });
36 | },
37 | icon: ,
38 | },
39 | ]}
40 | />
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/packages/ui/src/components/SchedulerModal.tsx:
--------------------------------------------------------------------------------
1 | import { Cross2Icon } from "@radix-ui/react-icons";
2 | import type { Scheduler } from "../utils/trpc";
3 | import { JSONTree } from "react-json-tree";
4 | import { Dialog, Heading, Modal } from "react-aria-components";
5 | import { SchedulerActionMenu } from "./SchedulerActionMenu";
6 |
7 | type SchedulerModalProps = {
8 | scheduler: Scheduler;
9 | queueName: string;
10 | onDismiss: () => void;
11 | };
12 |
13 | export const SchedulerModal = ({
14 | scheduler,
15 | queueName,
16 | onDismiss,
17 | }: SchedulerModalProps) => {
18 | const template = scheduler.template;
19 | const opts = {
20 | ...scheduler,
21 | };
22 | delete opts.template;
23 |
24 | return (
25 | {
29 | if (!isOpen) {
30 | onDismiss();
31 | }
32 | }}
33 | className="fixed inset-0 bg-black/10"
34 | >
35 |
87 |
88 | );
89 | };
90 |
--------------------------------------------------------------------------------
/packages/ui/src/components/SchedulerTable.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import {
3 | createColumnHelper,
4 | flexRender,
5 | getCoreRowModel,
6 | useReactTable,
7 | } from "@tanstack/react-table";
8 | import { TrashIcon } from "@radix-ui/react-icons";
9 | import type { Scheduler } from "../utils/trpc";
10 | import { trpc } from "../utils/trpc";
11 | import { TableRow } from "./TableRow";
12 | import { JobTableSkeleton } from "./JobTableSkeleton";
13 | import { Checkbox } from "./Checkbox";
14 | import { Button } from "./Button";
15 | import cronstrue from "cronstrue";
16 | import { Tooltip } from "./Tooltip";
17 | import { format, formatDistanceToNow } from "date-fns";
18 | import { SchedulerModal } from "./SchedulerModal";
19 |
20 | const columnHelper = createColumnHelper();
21 |
22 | function getTimezoneAbbreviation(timeZone: string, date: Date = new Date()) {
23 | const formatter = new Intl.DateTimeFormat("en-US", {
24 | timeZone,
25 | timeZoneName: "short",
26 | });
27 |
28 | const parts = formatter.formatToParts(date);
29 | const tzPart = parts.find((part) => part.type === "timeZoneName");
30 | return tzPart?.value || "";
31 | }
32 |
33 | const columns = [
34 | columnHelper.display({
35 | id: "select",
36 | header: ({ table }) => (
37 | {
43 | table.toggleAllRowsSelected();
44 | },
45 | }}
46 | />
47 | ),
48 | cell: ({ row, table }) => (
49 |
62 | ),
63 | }),
64 | columnHelper.accessor("name", {
65 | cell: (props) => (
66 |
67 |
68 | {props.cell.row.original.name}
69 |
70 |
71 | ),
72 | header: "Scheduler",
73 | }),
74 | columnHelper.accessor("pattern", {
75 | cell: (props) => {
76 | return (
77 |
78 | {props.cell.row.original.pattern
79 | ? cronstrue.toString(props.cell.row.original.pattern, {
80 | verbose: true,
81 | })
82 | : props.cell.row.original.every
83 | ? `Every ${props.cell.row.original.every}`
84 | : ""}{" "}
85 | {props.cell.row.original.tz
86 | ? `(${getTimezoneAbbreviation(props.cell.row.original.tz)})`
87 | : ""}
88 |
89 | );
90 | },
91 | header: "Pattern",
92 | }),
93 | columnHelper.accessor("next", {
94 | cell: (props) => {
95 | if (!props.cell.row.original.next) {
96 | return (
97 | No next run
98 | );
99 | }
100 | return (
101 |
107 |
108 |
109 | In {formatDistanceToNow(new Date(props.cell.row.original.next))}{" "}
110 |
111 | ({props.cell.row.original.iterationCount} run
112 | {props.cell.row.original.iterationCount === 1 ? "" : "s"} total)
113 |
114 |
115 |
116 |
117 | );
118 | },
119 | header: "Next Run",
120 | }),
121 | ];
122 |
123 | type SchedulerTableProps = {
124 | queueName: string;
125 | };
126 | export const SchedulerTable = ({ queueName }: SchedulerTableProps) => {
127 | const [rowSelection, setRowSelection] = useState({});
128 | const { data, isLoading } = trpc.scheduler.list.useQuery({
129 | queueName,
130 | });
131 |
132 | const isEmpty = data?.length === 0;
133 |
134 | const table = useReactTable({
135 | data: data || [],
136 | columns,
137 | getCoreRowModel: getCoreRowModel(),
138 | state: {
139 | rowSelection,
140 | },
141 | onRowSelectionChange: setRowSelection,
142 | });
143 |
144 | const [selectedScheduler, setSelectedScheduler] = useState(
145 | null,
146 | );
147 |
148 | const { mutate: bulkRemove } = trpc.scheduler.bulkRemove.useMutation();
149 |
150 | if (!isLoading && isEmpty) return null;
151 |
152 | return (
153 |
154 | {selectedScheduler ? (
155 |
setSelectedScheduler(null)}
159 | />
160 | ) : null}
161 |
162 | {isLoading ? (
163 |
164 | ) : (
165 |
166 | {table.getHeaderGroups().map((headerGroup, headerIndex) => (
167 |
171 | {headerGroup.headers.map((header) => (
172 |
178 | {header.isPlaceholder
179 | ? null
180 | : flexRender(
181 | header.column.columnDef.header,
182 | header.getContext(),
183 | )}
184 |
185 | ))}
186 |
187 | ))}
188 | {table.getRowModel().rows.map((row, rowIndex) => (
189 |
setSelectedScheduler(row.original)}
194 | layoutVariant="scheduler"
195 | >
196 | {row.getVisibleCells().map((cell, cellIndex) => (
197 |
203 | {flexRender(cell.column.columnDef.cell, cell.getContext())}
204 |
205 | ))}
206 |
207 | ))}
208 |
209 | )}
210 |
211 |
212 | {table.getSelectedRowModel().rows.length > 0 ? (
213 |
214 |
215 |
{table.getSelectedRowModel().rows.length} selected
216 |
217 |
}
221 | size="sm"
222 | onClick={() => {
223 | bulkRemove({
224 | queueName,
225 | jobSchedulerIds: table
226 | .getSelectedRowModel()
227 | .rows.map((row) => row.original.key),
228 | });
229 |
230 | table.resetRowSelection();
231 | }}
232 | />
233 |
234 |
235 | ) : null}
236 |
237 | );
238 | };
239 |
--------------------------------------------------------------------------------
/packages/ui/src/components/Skeleton.tsx:
--------------------------------------------------------------------------------
1 | type SkeletonProps = {
2 | className?: string;
3 | };
4 |
5 | export const Skeleton = ({ className }: SkeletonProps) => {
6 | return (
7 |
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/packages/ui/src/components/TableRow.tsx:
--------------------------------------------------------------------------------
1 | import type { PropsWithChildren } from "react";
2 |
3 | type TableRowProps = {
4 | isLastRow: boolean;
5 | isSelected: boolean;
6 | onClick: () => void;
7 | layoutVariant: "job" | "scheduler";
8 | };
9 | export const TableRow = ({
10 | isLastRow,
11 | children,
12 | isSelected,
13 | onClick,
14 | layoutVariant,
15 | }: PropsWithChildren) => {
16 | return (
17 |
28 | {children}
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/packages/ui/src/components/ThemeSwitcher.tsx:
--------------------------------------------------------------------------------
1 | import { DesktopIcon, MoonIcon, SunIcon } from "@radix-ui/react-icons";
2 | import { useState } from "react";
3 | import { ToggleButtonGroup, ToggleButton } from "react-aria-components";
4 |
5 | export function useLocalStorage>(
6 | key: string,
7 | initialValue: T,
8 | ) {
9 | // State to store our value
10 | // Pass initial state function to useState so logic is only executed once
11 | const [storedValue, setStoredValue] = useState(() => {
12 | if (typeof window === "undefined") {
13 | return initialValue;
14 | }
15 | try {
16 | // Get from local storage by key
17 | const item = window.localStorage.getItem(key);
18 | const parsedItem = item ? JSON.parse(item) : initialValue;
19 |
20 | for (const [key] of Object.entries(initialValue)) {
21 | if (typeof parsedItem[key] === "undefined") {
22 | throw new Error("Missing key in local storage");
23 | }
24 | }
25 |
26 | // Parse stored json or if none return initialValue
27 | return parsedItem;
28 | } catch (error) {
29 | console.log(error);
30 | // If error also return initialValue
31 | return initialValue;
32 | }
33 | });
34 | // Return a wrapped version of useState's setter function that ...
35 | // ... persists the new value to localStorage.
36 | const setValue = (value: T | ((val: T) => T)) => {
37 | try {
38 | // Allow value to be a function so we have same API as useState
39 | const valueToStore =
40 | value instanceof Function ? value(storedValue) : value;
41 | // Save state
42 | setStoredValue(valueToStore);
43 | // Save to local storage
44 | if (typeof window !== "undefined") {
45 | window.localStorage.setItem(key, JSON.stringify(valueToStore));
46 | }
47 | } catch (error) {
48 | console.log(error);
49 | // A more advanced implementation would handle the error case
50 | }
51 | };
52 | return [storedValue, setValue] as const;
53 | }
54 |
55 | export type UserPreferences = {
56 | theme: "light" | "dark" | "system";
57 | };
58 |
59 | export const ThemeSwitcher = () => {
60 | const [preferences, setPreferences] = useLocalStorage(
61 | "user-preferences",
62 | {
63 | theme: "system",
64 | },
65 | );
66 |
67 | return (
68 | {
74 | if (value.has("system")) {
75 | setPreferences({ theme: "system" });
76 | } else if (value.has("light")) {
77 | document.documentElement.classList.remove("dark");
78 | setPreferences({ theme: "light" });
79 | } else if (value.has("dark")) {
80 | document.documentElement.classList.add("dark");
81 | setPreferences({ theme: "dark" });
82 | }
83 | }}
84 | >
85 | {[
86 | {
87 | value: "system",
88 | icon: () => ,
89 | ariaLabel: "System theme",
90 | },
91 | {
92 | value: "light",
93 | icon: () => ,
94 | ariaLabel: "Light theme",
95 | },
96 | {
97 | value: "dark",
98 | icon: () => ,
99 | ariaLabel: "Dark theme",
100 | },
101 | ].map((item) => {
102 | return (
103 |
109 | {item.icon()}
110 |
111 | );
112 | })}
113 |
114 | );
115 | };
116 |
--------------------------------------------------------------------------------
/packages/ui/src/components/Tooltip.tsx:
--------------------------------------------------------------------------------
1 | import type { PropsWithChildren } from "react";
2 | import {
3 | Button,
4 | Tooltip as ReactAriaTooltip,
5 | TooltipTrigger,
6 | } from "react-aria-components";
7 |
8 | type TooltipProps = {
9 | message: string;
10 | };
11 |
12 | export const Tooltip = ({
13 | children,
14 | message,
15 | }: PropsWithChildren) => {
16 | return (
17 |
18 |
19 |
23 | {message}
24 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/packages/ui/src/main.ts:
--------------------------------------------------------------------------------
1 | export { App as QueueDashApp } from "./App";
2 |
--------------------------------------------------------------------------------
/packages/ui/src/pages/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import { Layout } from "../components/Layout";
2 | import { trpc } from "../utils/trpc";
3 | import { NUM_OF_RETRIES, REFETCH_INTERVAL } from "../utils/config";
4 | import { Link } from "react-router";
5 | import {
6 | CheckCircledIcon,
7 | CrossCircledIcon,
8 | MinusCircledIcon,
9 | PauseIcon,
10 | PlayIcon,
11 | PlusCircledIcon,
12 | } from "@radix-ui/react-icons";
13 | import { Tooltip } from "../components/Tooltip";
14 | import type { ReactNode } from "react";
15 | import { ActionMenu } from "../components/ActionMenu";
16 |
17 | type CountStatProps = {
18 | count: number;
19 | variant: "completed" | "failed" | "waiting" | "active";
20 | };
21 | const CountStat = ({ count, variant }: CountStatProps) => {
22 | const colorMap: Record = {
23 | active: "text-cyan-600",
24 | completed: "text-green-600",
25 | failed: "text-red-600",
26 | waiting: "text-amber-600",
27 | };
28 | const iconMap: Record = {
29 | active: ,
30 | completed: ,
31 | failed: ,
32 | waiting: ,
33 | };
34 | return (
35 |
36 |
37 |
40 |
{count}
41 | {iconMap[variant]}
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | const QueueCard = ({ queueName }: { queueName: string }) => {
49 | const queueReq = trpc.queue.byName.useQuery(
50 | {
51 | queueName,
52 | },
53 | {
54 | refetchInterval: REFETCH_INTERVAL,
55 | retry: NUM_OF_RETRIES,
56 | }
57 | );
58 |
59 | if (!queueReq.data) return null;
60 |
61 | return (
62 |
66 |
67 |
68 | {queueReq.data.displayName}
69 |
70 |
71 | {queueReq.data.paused ? (
72 |
73 | Paused
74 |
75 | ) : null}
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | );
87 | };
88 | export const HomePage = () => {
89 | const { data } = trpc.queue.list.useQuery();
90 | const { mutate: pauseAll } = trpc.queue.pauseAll.useMutation();
91 | const { mutate: resumeAll } = trpc.queue.resumeAll.useMutation();
92 |
93 | return (
94 |
95 |
96 |
97 |
98 | Queues
99 |
100 |
101 |
,
106 | onSelect: () => resumeAll(),
107 | },
108 | {
109 | label: "Pause all",
110 | icon:
,
111 | onSelect: () => pauseAll(),
112 | },
113 | ]}
114 | />
115 |
116 |
117 |
118 |
119 | {data?.map((queue, index) => {
120 | return (
121 |
129 |
130 |
131 | );
132 | })}
133 |
134 |
135 |
136 | );
137 | };
138 |
--------------------------------------------------------------------------------
/packages/ui/src/pages/QueuePage.tsx:
--------------------------------------------------------------------------------
1 | import type { Status } from "../utils/trpc";
2 | import { trpc } from "../utils/trpc";
3 | import { Layout } from "../components/Layout";
4 | import { JobTable } from "../components/JobTable";
5 | import { useEffect, useState } from "react";
6 | import {
7 | JOBS_PER_PAGE,
8 | NUM_OF_RETRIES,
9 | REFETCH_INTERVAL,
10 | } from "../utils/config";
11 | import { Skeleton } from "../components/Skeleton";
12 | import { ErrorCard } from "../components/ErrorCard";
13 | import { QueueStatusTabs } from "../components/QueueStatusTabs";
14 | import { QueueActionMenu } from "../components/QueueActionMenu";
15 | import { useParams, useSearchParams } from "react-router";
16 | import { SchedulerTable } from "../components/SchedulerTable";
17 |
18 | export const { format: numberFormat } = new Intl.NumberFormat("en-US");
19 |
20 | export const QueuePage = () => {
21 | const { id } = useParams();
22 | const queueName = id as string;
23 |
24 | const [searchParams] = useSearchParams();
25 |
26 | const [status, setStatus] = useState("completed");
27 | const {
28 | data,
29 | fetchNextPage,
30 | isLoading,
31 | isError,
32 | isFetchingNextPage,
33 | hasNextPage,
34 | } = trpc.job.list.useInfiniteQuery(
35 | {
36 | queueName,
37 | limit: JOBS_PER_PAGE,
38 | status,
39 | },
40 | {
41 | getNextPageParam: (lastPage) => lastPage.nextCursor,
42 | enabled: !!queueName,
43 | refetchInterval: REFETCH_INTERVAL,
44 | retry: NUM_OF_RETRIES,
45 | },
46 | );
47 |
48 | useEffect(() => {
49 | const searchStatus = searchParams.get("status");
50 | if (searchStatus) {
51 | setStatus(searchStatus as Status);
52 | }
53 |
54 | if (!searchStatus && status) {
55 | setStatus("completed");
56 | }
57 | }, [searchParams, status]);
58 |
59 | const queueReq = trpc.queue.byName.useQuery(
60 | {
61 | queueName,
62 | },
63 | {
64 | enabled: !!queueName,
65 | refetchInterval: REFETCH_INTERVAL,
66 | retry: NUM_OF_RETRIES,
67 | },
68 | );
69 |
70 | const jobs =
71 | data?.pages
72 | .map((page) => {
73 | return page.jobs;
74 | })
75 | .flat() ?? [];
76 |
77 | return (
78 |
79 | {queueReq.data === null ? (
80 |
81 | ) : isError ? (
82 |
83 | ) : (
84 |
85 |
86 |
87 |
88 | {queueReq.data ? (
89 | queueReq.data.displayName
90 | ) : (
91 |
92 | )}
93 |
94 |
95 | {queueReq.data ? (
96 |
97 | ) : null}
98 | {queueReq.data?.paused ? (
99 |
100 | Paused
101 |
102 | ) : null}
103 |
104 |
105 |
106 |
107 |
108 | {queueReq.data ? (
109 | <>
110 |
111 |
112 | {numberFormat(queueReq.data.client.connectedClients)}{" "}
113 | connected
114 | {" "}
115 | and{" "}
116 |
117 | {numberFormat(queueReq.data.client.blockedClients)}{" "}
118 | blocked
119 | {" "}
120 | out of{" "}
121 |
122 | {numberFormat(queueReq.data.client.maxClients)} max
123 | clients
124 |
125 |
126 |
·
127 |
128 | {queueReq.data.client.usedMemoryHuman} /{" "}
129 | {queueReq.data.client.totalMemoryHuman} (
130 | {(queueReq.data.client.usedMemoryPercentage * 100).toFixed(
131 | 2,
132 | )}
133 | %)
134 |
135 |
·
136 |
137 | Redis v{queueReq.data.client.version}
138 |
139 | >
140 | ) : (
141 |
142 | )}
143 |
144 |
145 |
146 |
147 | {queueReq.data?.type === "bullmq" ? (
148 |
149 | ) : null}
150 |
151 |
152 | 0}
154 | queueName={queueName}
155 | status={status}
156 | queue={queueReq.data}
157 | />
158 | {
160 | if (isFetchingNextPage || !hasNextPage) return;
161 | fetchNextPage();
162 | }}
163 | status={status}
164 | totalJobs={data?.pages.at(-1)?.totalCount || 0}
165 | jobs={jobs.map((j) => ({ ...j, status }))}
166 | isLoading={isLoading}
167 | isFetchingNextPage={isFetchingNextPage}
168 | queueName={queueName}
169 | />
170 |
171 |
172 |
173 | )}
174 |
175 | );
176 | };
177 |
--------------------------------------------------------------------------------
/packages/ui/src/styles/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .data-json-renderer * {
6 | font-family: monospace;
7 | }
8 |
9 | .data-json-renderer > ul {
10 | padding: 4px 16px 12px 16px !important;
11 | overflow-y: scroll;
12 | border-radius: 8px;
13 | margin: 0 !important;
14 | border: 1px solid #e2e8f0 !important;
15 | }
--------------------------------------------------------------------------------
/packages/ui/src/utils/config.ts:
--------------------------------------------------------------------------------
1 | export const JOBS_PER_PAGE = 30;
2 | export const REFETCH_INTERVAL = 2000;
3 | export const NUM_OF_RETRIES = 2;
4 |
--------------------------------------------------------------------------------
/packages/ui/src/utils/trpc.ts:
--------------------------------------------------------------------------------
1 | import { createTRPCReact } from "@trpc/react-query";
2 | import type { AppRouter } from "@queuedash/api";
3 | import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
4 |
5 | export type RouterOutput = inferRouterOutputs;
6 | export type RouterInput = inferRouterInputs;
7 |
8 | export type Job = RouterOutput["job"]["list"]["jobs"][0];
9 | export type Queue = RouterOutput["queue"]["byName"];
10 | export type Status = RouterInput["job"]["list"]["status"];
11 | export type Scheduler = RouterOutput["scheduler"]["list"][0];
12 |
13 | export const trpc = createTRPCReact({
14 | overrides: {
15 | useMutation: {
16 | async onSuccess(opts) {
17 | await opts.originalFn();
18 | await opts.queryClient.invalidateQueries();
19 | },
20 | },
21 | },
22 | });
23 |
--------------------------------------------------------------------------------
/packages/ui/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ["./src/**/*.{js,ts,jsx,tsx}"],
4 | darkMode: "class",
5 | theme: {
6 | extend: {
7 | fontFamily: {
8 | sans: [
9 | "Inter var, sans-serif",
10 | { fontFeatureSettings: '"cv11", "ss01", "rlig", "calt" 0, "tnum"' },
11 | ],
12 | },
13 | colors: {
14 | brand: {
15 | 50: "#edfbff",
16 | 100: "#d6f5ff",
17 | 200: "#b5efff",
18 | 300: "#83e8ff",
19 | 400: "#48d8ff",
20 | 500: "#1ebcff",
21 | 600: "#069fff",
22 | 700: "#008cff",
23 | 800: "#086ac5",
24 | 900: "#0d5b9b",
25 | },
26 | },
27 | },
28 | },
29 | plugins: [require("tailwindcss-radix")()],
30 | };
31 |
--------------------------------------------------------------------------------
/packages/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "target": "es5",
5 | "lib": ["dom", "dom.iterable", "esnext"],
6 | "outDir": "dist",
7 | "allowJs": true,
8 | "skipLibCheck": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noEmit": true,
12 | "incremental": true,
13 | "esModuleInterop": true,
14 | "module": "esnext",
15 | "moduleResolution": "node",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "jsx": "preserve",
19 | "composite": true,
20 | "declaration": true,
21 | "declarationMap": true,
22 | "inlineSources": false,
23 | "noUnusedLocals": false,
24 | "noUnusedParameters": false,
25 | "preserveWatchOutput": true,
26 | "baseUrl": "."
27 | },
28 | "include": ["**/*.ts", "**/*.tsx", "vite.config.ts"],
29 | "exclude": ["node_modules"]
30 | }
31 |
--------------------------------------------------------------------------------
/packages/ui/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/packages/ui/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import typescript from "@rollup/plugin-typescript";
3 | import path from "path";
4 | import { typescriptPaths } from "rollup-plugin-typescript-paths";
5 | import react from "@vitejs/plugin-react";
6 |
7 | export default defineConfig({
8 | plugins: [react()],
9 | define: {
10 | "process.env.NODE_ENV": JSON.stringify("production"),
11 | },
12 | build: {
13 | minify: true,
14 | reportCompressedSize: true,
15 | lib: {
16 | entry: path.resolve(__dirname, "src/main.ts"),
17 | name: "QueueDash App",
18 | fileName: "main",
19 | formats: ["cjs", "es"],
20 | },
21 | rollupOptions: {
22 | external: ["react", "react-dom"],
23 | plugins: [
24 | typescriptPaths({
25 | preserveExtensions: true,
26 | }),
27 | typescript({
28 | sourceMap: false,
29 | declaration: true,
30 | outDir: "dist",
31 | }),
32 | ],
33 | },
34 | },
35 | });
36 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'packages/*'
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "tasks": {
4 | "build": {
5 | "dependsOn": ["^build"],
6 | "outputs": ["dist/**"]
7 | },
8 | "test": {
9 | "outputs": [],
10 | "inputs": ["src/**/*.tsx", "src/**/*.ts"]
11 | },
12 | "lint": {
13 | "outputs": []
14 | },
15 | "dev": {
16 | "cache": false
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------