├── .dockerignore ├── .env.example ├── .eslintrc.json ├── .github └── workflows │ └── docker-publish.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── RELEASE_NOTES.md ├── app ├── api │ ├── [...path] │ │ └── route.ts │ ├── auth │ │ └── verify │ │ │ └── route.ts │ ├── bindings │ │ └── [vhost] │ │ │ └── e │ │ │ └── [exchange] │ │ │ └── source │ │ │ └── route.ts │ ├── channels │ │ └── route.ts │ ├── connections │ │ └── route.ts │ ├── exchanges │ │ ├── [vhost] │ │ │ └── [exchange] │ │ │ │ └── bindings │ │ │ │ └── source │ │ │ │ └── route.ts │ │ └── route.ts │ ├── overview │ │ └── route.ts │ ├── queues │ │ ├── [vhost] │ │ │ └── [queue] │ │ │ │ └── get │ │ │ │ └── route.ts │ │ └── route.ts │ └── ws │ │ └── route.ts ├── channels │ └── page.tsx ├── connections │ └── page.tsx ├── exchanges │ └── page.tsx ├── favicon.ico ├── fonts │ ├── GeistMonoVF.woff │ └── GeistVF.woff ├── globals.css ├── layout.tsx ├── login │ └── page.tsx ├── logo.png ├── metadata.ts ├── page.tsx └── queues │ └── page.tsx ├── components.json ├── components ├── auth │ └── login-form.tsx ├── channels │ ├── channel-list-skeleton.tsx │ └── channel-list.tsx ├── common │ └── api-error.tsx ├── connections │ ├── connection-list-skeleton.tsx │ ├── connection-list.tsx │ └── connection-operations.tsx ├── dashboard │ ├── message-rate-chart.tsx │ ├── overview-stats.tsx │ ├── queue-distribution-chart.tsx │ └── queued-messages-chart.tsx ├── exchanges │ ├── binding-viewer.tsx │ ├── exchange-list-skeleton.tsx │ ├── exchange-list.tsx │ └── exchange-operations.tsx ├── layout │ ├── header.tsx │ ├── main-nav.tsx │ └── page-header.tsx ├── mode-toggle.tsx ├── queues │ ├── message-viewer.tsx │ ├── queue-list-skeleton.tsx │ ├── queue-list.tsx │ └── queue-operations.tsx ├── refresh-toggle.tsx ├── theme-provider.tsx └── ui │ ├── badge.tsx │ ├── button.tsx │ ├── card.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── input.tsx │ ├── label.tsx │ ├── select.tsx │ ├── skeleton.tsx │ ├── table.tsx │ ├── toast.tsx │ └── toaster.tsx ├── docker-compose.yml ├── docs └── assets │ ├── dark-dashboard.png │ ├── light-dashboard.png │ └── logo.png ├── hooks ├── use-realtime-updates.ts └── use-toast.ts ├── lib ├── api-utils.ts ├── auth.ts ├── config.ts ├── hooks │ └── use-graph-data.ts ├── store.ts ├── utils.ts └── websocket.ts ├── middleware.ts ├── next.config.js ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public └── images │ └── logo.png ├── tailwind.config.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | npm-debug.log 4 | yarn-debug.log 5 | yarn-error.log 6 | 7 | # Next.js build output 8 | .next 9 | out 10 | 11 | # Environment files 12 | .env 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | # Version control 19 | .git 20 | .gitignore 21 | 22 | # IDE 23 | .idea 24 | .vscode 25 | 26 | # Docker 27 | Dockerfile 28 | docker-compose.yml 29 | .dockerignore 30 | 31 | # Documentation 32 | README.md 33 | docs 34 | *.md 35 | 36 | # Misc 37 | .DS_Store 38 | *.pem 39 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # RabbitMQ Configuration 2 | NEXT_PUBLIC_RABBITMQ_HOST=localhost 3 | NEXT_PUBLIC_RABBITMQ_PORT=15672 4 | NEXT_PUBLIC_RABBITMQ_VHOST=/ 5 | 6 | # RabbitMQ Credentials (not public) 7 | RABBITMQ_USERNAME=guest 8 | RABBITMQ_PASSWORD=guest 9 | 10 | # Next.js Configuration 11 | NEXT_PUBLIC_API_URL=http://localhost:3000 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | tags: [ 'v*.*.*' ] 7 | pull_request: 8 | branches: [ "main" ] 9 | 10 | env: 11 | REGISTRY: ghcr.io 12 | IMAGE_NAME: ${{ github.repository }} 13 | 14 | jobs: 15 | build-and-push: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | packages: write 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | 25 | - name: Log in to the Container registry 26 | uses: docker/login-action@v3 27 | with: 28 | registry: ${{ env.REGISTRY }} 29 | username: ${{ github.actor }} 30 | password: ${{ secrets.GITHUB_TOKEN }} 31 | 32 | - name: Extract metadata (tags, labels) for Docker 33 | id: meta 34 | uses: docker/metadata-action@v5 35 | with: 36 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 37 | tags: | 38 | type=ref,event=branch 39 | type=ref,event=pr 40 | type=semver,pattern={{version}} 41 | type=semver,pattern={{major}}.{{minor}} 42 | type=sha 43 | 44 | - name: Build and push Docker image 45 | uses: docker/build-push-action@v5 46 | with: 47 | context: . 48 | push: ${{ github.event_name != 'pull_request' }} 49 | tags: ${{ steps.meta.outputs.tags }} 50 | labels: ${{ steps.meta.outputs.labels }} 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # env files 29 | .env 30 | !.env.example 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM node:18-alpine AS builder 3 | 4 | WORKDIR /app 5 | 6 | # Copy package files 7 | COPY package.json package-lock.json ./ 8 | 9 | # Install dependencies 10 | RUN npm ci 11 | 12 | # Copy source code 13 | COPY . . 14 | 15 | # Build the application 16 | RUN npm run build 17 | 18 | # Production stage 19 | FROM node:18-alpine AS runner 20 | 21 | LABEL org.opencontainers.image.source="https://github.com/Ralve-org/RabbitScout" \ 22 | org.opencontainers.image.description="Modern, intuitive dashboard for RabbitMQ management" \ 23 | org.opencontainers.image.licenses="MIT" \ 24 | org.opencontainers.image.vendor="Ralve-org" 25 | 26 | WORKDIR /app 27 | 28 | # Set environment variables 29 | ENV NODE_ENV=production 30 | 31 | # Copy necessary files from builder 32 | COPY --from=builder /app/public ./public 33 | COPY --from=builder /app/.next/standalone ./ 34 | COPY --from=builder /app/.next/static ./.next/static 35 | 36 | # Expose the port the app runs on 37 | EXPOSE 3000 38 | 39 | # Command to run the application 40 | CMD ["node", "server.js"] 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Ralve 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | RabbitScout Logo 3 |

RabbitScout

4 |
5 | 6 |

7 | Modern, intuitive dashboard for RabbitMQ management - A powerful alternative to the default RabbitMQ Management UI. 8 |

9 | 10 |

11 | 12 | License 13 | 14 | RabbitMQ Management 15 | Next.js 14 16 | 17 | Stars 18 | 19 | 20 | Issues 21 | 22 |

23 | 24 |
25 |

26 | Features • 27 | Getting Started • 28 | Tech Stack • 29 | Screenshots • 30 | Contributing • 31 | License 32 |

33 |
34 | 35 | ## 🚀 Features 36 | 37 | RabbitScout provides a comprehensive suite of features for managing your RabbitMQ instance: 38 | 39 | ### 📊 Analytics & Monitoring 40 | - Real-time overview of system metrics 41 | - Total message count monitoring 42 | - Queue statistics and distribution 43 | - Active connections tracking 44 | - Memory usage visualization 45 | - Live message rate graphs 46 | - Queue-specific message rate tracking 47 | 48 | ### 💼 Current Features 49 | - **Queue Management** 50 | - 📋 Detailed queue listings with search and filter 51 | - 🔍 Message inspection capabilities 52 | - ⚡ Real-time queue metrics 53 | - 🗑️ Queue operations (purge, delete) 54 | - 📥 Message publishing interface 55 | 56 | - **Exchange & Binding Viewing** 57 | - 🔄 Exchange configuration viewing 58 | - 👁️ View-only binding information 59 | 60 | - **Connection & Channel Monitoring** 61 | - 👥 View active connections 62 | - 📡 Basic channel status viewing 63 | - 📉 Connection metrics viewing 64 | 65 | ### 🚧 Features In Development 66 | - **Binding Management** 67 | - Binding creation and modification 68 | - Advanced binding configuration 69 | 70 | - **Connection & Channel Management** 71 | - Advanced connection controls 72 | - Channel management actions 73 | - Detailed channel metrics 74 | - Connection force-close capabilities 75 | 76 | ### 🛡️ Security Features 77 | - 🔐 Secure authentication system 78 | - 🍪 Cookie-based session management 79 | - ⚙️ Environment variable configuration 80 | - 🔒 Secure credential handling 81 | 82 | ### 💫 User Experience 83 | - 🌓 Dark/Light mode support 84 | - 📱 Responsive design for all devices 85 | - ⚡ Real-time updates 86 | - 🎨 Modern, clean interface 87 | 88 | ## 🚀 Getting Started 89 | 90 | ### System Requirements 91 | - Node.js 18.17 or later 92 | - RabbitMQ Server 3.x or later 93 | - Modern web browser 94 | 95 | ### Prerequisites 96 | Before you begin, ensure you have: 97 | - 🔧 Access to a RabbitMQ instance 98 | - 📝 RabbitMQ management credentials 99 | - 💻 Node.js installed locally 100 | 101 | ### Installation 102 | 103 | 1. Clone the repository 104 | ```bash 105 | git clone https://github.com/Ralve-org/RabbitScout.git 106 | cd RabbitScout 107 | ``` 108 | 109 | 2. Install dependencies 110 | ```bash 111 | npm install 112 | # or 113 | yarn install 114 | ``` 115 | 116 | 3. Configure environment variables 117 | - Copy the example environment file 118 | ```bash 119 | cp .env.example .env 120 | ``` 121 | - Update the .env file with your RabbitMQ credentials: 122 | ```env 123 | # Required Configuration 124 | NEXT_PUBLIC_RABBITMQ_HOST=your-rabbitmq-host # RabbitMQ server hostname 125 | NEXT_PUBLIC_RABBITMQ_PORT=15672 # RabbitMQ management port 126 | NEXT_PUBLIC_RABBITMQ_VHOST=/ # Virtual host 127 | 128 | # Authentication 129 | RABBITMQ_USERNAME=your-username # RabbitMQ admin username 130 | RABBITMQ_PASSWORD=your-password # RabbitMQ admin password 131 | 132 | # Application Settings 133 | NEXT_PUBLIC_API_URL=http://localhost:3000 # Application URL 134 | ``` 135 | 136 | ### Authentication 137 | 138 | 1. **Access the Login Page** 139 | - Navigate to `http://localhost:3000/login` 140 | - You'll be presented with a clean, modern login interface 141 | 142 | 2. **Enter Credentials** 143 | - Username: Your RabbitMQ username (default: guest) 144 | - Password: Your RabbitMQ password (default: guest) 145 | 146 | 3. **Important Notes** 147 | - Default credentials (guest/guest) only work for localhost 148 | - For remote servers, use your RabbitMQ server credentials 149 | - Ensure your RabbitMQ user has management permissions 150 | 151 | 4. **Session Management** 152 | - Login sessions are secured with HTTP-only cookies 153 | - Sessions expire after period of inactivity 154 | - Use the logout button to end your session manually 155 | 156 | ### Development 157 | 158 | Run the development server: 159 | ```bash 160 | npm run dev 161 | # or 162 | yarn dev 163 | ``` 164 | 165 | Access the dashboard at [http://localhost:3000](http://localhost:3000) 166 | 167 | ### Production Build 168 | 169 | Build for production: 170 | ```bash 171 | npm run build 172 | # or 173 | yarn build 174 | ``` 175 | 176 | Start the production server: 177 | ```bash 178 | npm start 179 | # or 180 | yarn start 181 | ``` 182 | 183 | ### 🐳 Docker Usage 184 | 185 | You can run RabbitScout using Docker in two ways: 186 | 187 | #### Using Docker Compose 188 | ```yaml 189 | services: 190 | rabbitscout: 191 | image: ghcr.io/ralve-org/rabbitscout:latest 192 | ports: 193 | - "3000:3000" 194 | environment: 195 | - NEXT_PUBLIC_RABBITMQ_HOST=your-rabbitmq-host 196 | - NEXT_PUBLIC_RABBITMQ_PORT=15672 197 | - NEXT_PUBLIC_RABBITMQ_VHOST=/ 198 | - RABBITMQ_USERNAME=your-username 199 | - RABBITMQ_PASSWORD=your-password 200 | - NEXT_PUBLIC_API_URL=http://localhost:3000 201 | ``` 202 | 203 | #### Using Docker CLI 204 | ```bash 205 | docker run -p 3000:3000 \ 206 | -e NEXT_PUBLIC_RABBITMQ_HOST=your-rabbitmq-host \ 207 | -e NEXT_PUBLIC_RABBITMQ_PORT=15672 \ 208 | -e NEXT_PUBLIC_RABBITMQ_VHOST=/ \ 209 | -e RABBITMQ_USERNAME=your-username \ 210 | -e RABBITMQ_PASSWORD=your-password \ 211 | -e NEXT_PUBLIC_API_URL=http://localhost:3000 \ 212 | ghcr.io/ralve-org/rabbitscout:latest 213 | ``` 214 | 215 | ## 🛠️ Tech Stack 216 | 217 | - **Framework**: [Next.js 14](https://nextjs.org/) 218 | - **UI Components**: [shadcn/ui](https://ui.shadcn.com/) 219 | - **Styling**: [Tailwind CSS](https://tailwindcss.com/) 220 | - **Language**: [TypeScript](https://www.typescriptlang.org/) 221 | - **State Management**: React Hooks 222 | - **Data Fetching**: Next.js App Router & Server Components 223 | 224 | ## 📦 Project Structure 225 | ``` 226 | rabbitscout/ 227 | ├── app/ # Next.js app directory 228 | │ ├── api/ # API routes 229 | │ │ ├── auth/ # Authentication endpoints 230 | │ │ ├── queues/ # Queue management endpoints 231 | │ │ └── stats/ # Statistics and metrics endpoints 232 | │ ├── dashboard/ # Dashboard pages 233 | │ │ ├── connections/ # Connection management 234 | │ │ ├── exchanges/ # Exchange management 235 | │ │ ├── queues/ # Queue management 236 | │ │ └── page.tsx # Main dashboard 237 | │ └── login/ # Authentication pages 238 | ├── components/ # React components 239 | │ ├── auth/ # Authentication components 240 | │ ├── dashboard/ # Dashboard components 241 | │ │ ├── message-rate-chart # Message rate visualization 242 | │ │ ├── overview-stats # System statistics 243 | │ │ ├── queue-distribution # Queue metrics 244 | │ │ └── queued-messages # Queue message charts 245 | │ ├── ui/ # Reusable UI components 246 | │ └── shared/ # Shared components 247 | ├── docs/ # Documentation 248 | │ └── assets/ # Documentation assets 249 | ├── hooks/ # Custom React hooks 250 | │ ├── use-toast.ts # Toast notifications 251 | │ └── use-websocket.ts # WebSocket connections 252 | ├── lib/ # Utility functions 253 | │ ├── api/ # API client functions 254 | │ ├── auth/ # Authentication utilities 255 | │ ├── constants/ # Constants and configs 256 | │ ├── types/ # TypeScript types 257 | │ └── utils/ # Helper functions 258 | ├── public/ # Static assets 259 | │ └── images/ # Image assets 260 | ├── .env.example # Example environment variables 261 | ├── .eslintrc.json # ESLint configuration 262 | ├── .gitignore # Git ignore rules 263 | ├── components.json # UI components config 264 | ├── middleware.ts # Next.js middleware 265 | ├── next.config.js # Next.js configuration 266 | ├── package.json # Project dependencies 267 | ├── postcss.config.mjs # PostCSS configuration 268 | ├── tailwind.config.ts # Tailwind CSS configuration 269 | └── tsconfig.json # TypeScript configuration 270 | ``` 271 | 272 | ## 🎨 Screenshots 273 | 274 | ### Light Mode Dashboard 275 | ![Dashboard Light Mode](docs/assets/light-dashboard.png) 276 | 277 | ### Dark Mode Dashboard 278 | ![Dashboard Dark Mode](docs/assets/dark-dashboard.png) 279 | 280 | These screenshots showcase the dashboard overview tab in both light and dark modes, featuring: 281 | - Real-time message rate graphs 282 | - Queue distribution charts 283 | - System overview statistics 284 | - Memory usage metrics 285 | 286 | ## 🔄 Updates & Roadmap 287 | 288 | ### Currently in Development 289 | - 🔗 Complete binding management system 290 | - 🎮 Advanced connection & channel controls 291 | - 📊 Enhanced channel metrics 292 | - 🔄 Connection management actions 293 | 294 | ### Coming Soon 295 | - 📊 Enhanced visualization options 296 | - 🔔 Real-time notifications 297 | - 🔍 Advanced search capabilities 298 | - 📈 Extended metrics and analytics 299 | 300 | ### Known Limitations 301 | - Binding management functionality is currently disabled 302 | - Channel and connection management actions are in development 303 | - Some advanced features are view-only at this time 304 | 305 | ## 🤝 Contributing 306 | 307 | We welcome contributions! Here's how you can help: 308 | 309 | ### Ways to Contribute 310 | - 🐛 Report bugs and issues 311 | - 💡 Suggest new features 312 | - 📝 Improve documentation 313 | - 🔧 Submit pull requests 314 | 315 | ### Development Process 316 | 1. Fork the repository 317 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 318 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 319 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 320 | 5. Open a Pull Request 321 | 322 | ## 📜 License 323 | 324 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 325 | 326 | ## 🙏 Acknowledgments 327 | 328 | - Built with [Next.js](https://nextjs.org/) 🚀 329 | - UI components from [shadcn/ui](https://ui.shadcn.com/) 🎨 330 | - Powered by [TypeScript](https://www.typescriptlang.org/) 💪 331 | - Styled with [Tailwind CSS](https://tailwindcss.com/) 🎯 332 | 333 | --- 334 | 335 |
336 | Made with ❤️ by the Ralve team 337 |
-------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | # v1.0.0 - Initial Release with Docker Support 2 | 3 | ## 🚀 Features 4 | - Modern, responsive alternative to the default RabbitMQ Management UI 5 | - Real-time monitoring and queue insights 6 | - Built with Next.js and shadcn/ui 7 | - Docker support with GitHub Container Registry integration 8 | 9 | ## 🐳 Docker Usage 10 | You can now use RabbitScout with Docker: 11 | 12 | ```yaml 13 | services: 14 | rabbitscout: 15 | image: ghcr.io/ralve-org/rabbitscout:latest 16 | ports: 17 | - "3000:3000" 18 | environment: 19 | - NEXT_PUBLIC_RABBITMQ_HOST=your-rabbitmq-host 20 | - NEXT_PUBLIC_RABBITMQ_PORT=15672 21 | - NEXT_PUBLIC_RABBITMQ_VHOST=/ 22 | - RABBITMQ_USERNAME=your-username 23 | - RABBITMQ_PASSWORD=your-password 24 | - NEXT_PUBLIC_API_URL=http://localhost:3000 25 | ``` 26 | 27 | All environment variables are required: 28 | - `NEXT_PUBLIC_RABBITMQ_HOST`: Your RabbitMQ server hostname 29 | - `NEXT_PUBLIC_RABBITMQ_PORT`: RabbitMQ management port (default: 15672) 30 | - `NEXT_PUBLIC_RABBITMQ_VHOST`: RabbitMQ virtual host (default: /) 31 | - `RABBITMQ_USERNAME`: RabbitMQ username 32 | - `RABBITMQ_PASSWORD`: RabbitMQ password 33 | - `NEXT_PUBLIC_API_URL`: URL where RabbitScout is accessible (default: http://localhost:3000) 34 | -------------------------------------------------------------------------------- /app/api/[...path]/route.ts: -------------------------------------------------------------------------------- 1 | import { RABBITMQ_CONFIG, API_TIMEOUT_MS } from '../../../lib/config' 2 | import { NextResponse } from 'next/server' 3 | import { headers } from 'next/headers' 4 | 5 | export const dynamic = 'force-dynamic' 6 | export const revalidate = 0 7 | 8 | export async function GET(request: Request, { params }: { params: { path: string[] } }) { 9 | try { 10 | const { host, port, username, password } = RABBITMQ_CONFIG 11 | const baseUrl = `http://${host}:${port}/api` 12 | const path = params.path.join('/') 13 | const url = `${baseUrl}/${path}` 14 | 15 | console.log(`[API Route] Fetching from ${url}`) 16 | 17 | const authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}` 18 | 19 | const response = await fetch(url, { 20 | headers: { 21 | 'Authorization': authHeader, 22 | 'Content-Type': 'application/json', 23 | }, 24 | cache: 'no-store', 25 | signal: AbortSignal.timeout(API_TIMEOUT_MS), 26 | }) 27 | 28 | if (!response.ok) { 29 | const errorText = await response.text() 30 | console.error(`[API Route] Error response from RabbitMQ:`, errorText) 31 | return NextResponse.json( 32 | { error: 'Failed to fetch from RabbitMQ', details: errorText }, 33 | { status: response.status } 34 | ) 35 | } 36 | 37 | const data = await response.json() 38 | console.log(`[API Route] Successfully fetched ${data.length ?? 1} items`) 39 | return NextResponse.json(data) 40 | } catch (error) { 41 | console.error('[API Route] Error:', error) 42 | return NextResponse.json( 43 | { error: 'An unexpected error occurred' }, 44 | { status: 500 } 45 | ) 46 | } 47 | } 48 | 49 | export async function POST(request: Request, { params }: { params: { path: string[] } }) { 50 | try { 51 | const { host, port, username, password } = RABBITMQ_CONFIG 52 | const baseUrl = `http://${host}:${port}/api` 53 | const path = params.path.join('/') 54 | const url = `${baseUrl}/${path}` 55 | 56 | console.log(`[API Route] POSTing to ${url}`) 57 | 58 | const authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}` 59 | const body = await request.json() 60 | 61 | const response = await fetch(url, { 62 | method: 'POST', 63 | headers: { 64 | 'Authorization': authHeader, 65 | 'Content-Type': 'application/json', 66 | }, 67 | body: JSON.stringify(body), 68 | cache: 'no-store', 69 | signal: AbortSignal.timeout(API_TIMEOUT_MS), 70 | }) 71 | 72 | if (!response.ok) { 73 | const errorText = await response.text() 74 | console.error(`[API Route] Error response from RabbitMQ:`, errorText) 75 | return NextResponse.json( 76 | { error: 'Failed to post to RabbitMQ', details: errorText }, 77 | { status: response.status } 78 | ) 79 | } 80 | 81 | const data = await response.json() 82 | return NextResponse.json(data) 83 | } catch (error) { 84 | console.error('[API Route] Error:', error) 85 | return NextResponse.json( 86 | { error: 'An unexpected error occurred' }, 87 | { status: 500 } 88 | ) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/api/auth/verify/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | 3 | export async function POST(request: Request) { 4 | try { 5 | const { username, password } = await request.json() 6 | 7 | // Note: RabbitMQ management API runs on port 15672 8 | const managementPort = process.env.NEXT_PUBLIC_RABBITMQ_PORT 9 | const host = process.env.NEXT_PUBLIC_RABBITMQ_HOST 10 | const url = `http://${host}:${managementPort}/api/whoami` 11 | 12 | console.log('Authentication attempt:', { 13 | host, 14 | port: managementPort, 15 | username, 16 | url 17 | }) 18 | 19 | try { 20 | const response = await fetch(url, { 21 | method: 'GET', 22 | headers: { 23 | 'Authorization': 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'), 24 | 'Accept': 'application/json', 25 | }, 26 | next: { revalidate: 60 } // Cache for 60 seconds 27 | }) 28 | 29 | if (!response.ok) { 30 | return NextResponse.json( 31 | { error: 'Invalid credentials' }, 32 | { status: 401 } 33 | ) 34 | } 35 | 36 | const userData = await response.json() 37 | 38 | return NextResponse.json({ 39 | authenticated: true, 40 | user: { 41 | username: userData.name, 42 | isAdmin: userData.tags.includes('administrator'), 43 | tags: userData.tags 44 | } 45 | }) 46 | } catch (fetchError: unknown) { 47 | console.error('Fetch Error:', { 48 | message: fetchError instanceof Error ? fetchError.message : 'Unknown error', 49 | cause: fetchError instanceof Error ? fetchError.cause : undefined 50 | }) 51 | return NextResponse.json( 52 | { error: 'Failed to connect to RabbitMQ server' }, 53 | { status: 500 } 54 | ) 55 | } 56 | } catch (error) { 57 | console.error('Error:', error) 58 | return NextResponse.json( 59 | { error: 'Internal server error' }, 60 | { status: 500 } 61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/api/bindings/[vhost]/e/[exchange]/source/route.ts: -------------------------------------------------------------------------------- 1 | import { RABBITMQ_CONFIG } from '@/lib/utils' 2 | import { createApiResponse, createApiErrorResponse, NO_CACHE_HEADERS, NO_CACHE_FETCH_OPTIONS } from '@/lib/api-utils' 3 | 4 | export const dynamic = 'force-dynamic' 5 | export const revalidate = 0 6 | 7 | export async function GET( 8 | request: Request, 9 | { params }: { params: { vhost: string; exchange: string } } 10 | ) { 11 | try { 12 | const { host, port, username, password } = RABBITMQ_CONFIG 13 | const { vhost, exchange } = params 14 | const baseUrl = `http://${host}:${port}/api` 15 | const url = `${baseUrl}/bindings/${encodeURIComponent(vhost)}/e/${encodeURIComponent(exchange)}/source` 16 | 17 | console.log(`[API Route] Fetching bindings for exchange ${exchange} in vhost ${vhost}`) 18 | console.log(`[API Route] Using host: ${host}:${port}`) 19 | 20 | const authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}` 21 | 22 | const response = await fetch(url, { 23 | headers: { 24 | 'Authorization': authHeader, 25 | 'Content-Type': 'application/json', 26 | ...NO_CACHE_HEADERS 27 | }, 28 | ...NO_CACHE_FETCH_OPTIONS, 29 | signal: AbortSignal.timeout(5000) 30 | }) 31 | 32 | if (!response.ok) { 33 | const errorText = await response.text() 34 | console.error(`[API Route] RabbitMQ API error:`, { 35 | status: response.status, 36 | statusText: response.statusText, 37 | error: errorText 38 | }) 39 | return createApiErrorResponse( 40 | `Failed to fetch exchange bindings: ${response.statusText}`, 41 | response.status 42 | ) 43 | } 44 | 45 | const data = await response.json() 46 | console.log(`[API Route] Successfully fetched ${data.length} bindings for exchange ${exchange}`) 47 | return createApiResponse(data) 48 | } catch (error) { 49 | console.error('[API Route] Error fetching exchange bindings:', error) 50 | return createApiErrorResponse('Failed to fetch exchange bindings from RabbitMQ') 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/api/channels/route.ts: -------------------------------------------------------------------------------- 1 | import { RABBITMQ_CONFIG } from '../../../lib/config' 2 | import { createApiResponse, createApiErrorResponse, NO_CACHE_HEADERS, NO_CACHE_FETCH_OPTIONS } from '@/lib/api-utils' 3 | 4 | export const dynamic = 'force-dynamic' 5 | export const revalidate = 0 6 | 7 | export async function GET() { 8 | try { 9 | const { host, port, username, password } = RABBITMQ_CONFIG 10 | const baseUrl = `http://${host}:${port}/api` 11 | const url = `${baseUrl}/channels` 12 | 13 | console.log(`[API Route] Fetching channels from ${url}`) 14 | console.log(`[API Route] Using host: ${host}:${port}`) 15 | 16 | const authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}` 17 | 18 | const response = await fetch(url, { 19 | headers: { 20 | 'Authorization': authHeader, 21 | 'Content-Type': 'application/json', 22 | ...NO_CACHE_HEADERS 23 | }, 24 | ...NO_CACHE_FETCH_OPTIONS, 25 | signal: AbortSignal.timeout(5000) 26 | }) 27 | 28 | if (!response.ok) { 29 | const errorText = await response.text() 30 | console.error(`[API Route] RabbitMQ API error:`, { 31 | status: response.status, 32 | statusText: response.statusText, 33 | error: errorText 34 | }) 35 | return createApiErrorResponse( 36 | `Failed to fetch channels: ${response.statusText}`, 37 | response.status 38 | ) 39 | } 40 | 41 | const data = await response.json() 42 | 43 | if (!Array.isArray(data)) { 44 | console.error(`[API Route] Unexpected response format:`, data) 45 | return createApiErrorResponse('Invalid response format from RabbitMQ API', 500) 46 | } 47 | 48 | console.log(`[API Route] Successfully fetched ${data.length} channels`) 49 | return createApiResponse(data) 50 | } catch (error) { 51 | console.error('[API Route] Error fetching channels:', error) 52 | return createApiErrorResponse('Failed to fetch channels from RabbitMQ') 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/api/connections/route.ts: -------------------------------------------------------------------------------- 1 | import { RABBITMQ_CONFIG } from '../../../lib/config' 2 | import { createApiResponse, createApiErrorResponse, NO_CACHE_HEADERS, NO_CACHE_FETCH_OPTIONS } from '@/lib/api-utils' 3 | import { NextResponse } from 'next/server' 4 | 5 | export const dynamic = 'force-dynamic' 6 | export const revalidate = 0 7 | 8 | export async function GET() { 9 | try { 10 | const { host, port, username, password } = RABBITMQ_CONFIG 11 | const baseUrl = `http://${host}:${port}/api` 12 | const url = `${baseUrl}/connections` 13 | 14 | console.log(`[API Route] Fetching connections from ${url}`) 15 | console.log(`[API Route] Using host: ${host}:${port}`) 16 | 17 | const authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}` 18 | 19 | const response = await fetch(url, { 20 | headers: { 21 | 'Authorization': authHeader, 22 | 'Content-Type': 'application/json', 23 | ...NO_CACHE_HEADERS 24 | }, 25 | ...NO_CACHE_FETCH_OPTIONS, 26 | signal: AbortSignal.timeout(5000) 27 | }) 28 | 29 | if (!response.ok) { 30 | const errorText = await response.text() 31 | console.error(`[API Route] RabbitMQ API error:`, { 32 | status: response.status, 33 | statusText: response.statusText, 34 | error: errorText 35 | }) 36 | return createApiErrorResponse( 37 | `Failed to fetch connections: ${response.statusText}`, 38 | response.status 39 | ) 40 | } 41 | 42 | const data = await response.json() 43 | 44 | if (!Array.isArray(data)) { 45 | console.error(`[API Route] Unexpected response format:`, data) 46 | return createApiErrorResponse( 47 | 'Invalid response format from RabbitMQ API', 48 | 500 49 | ) 50 | } 51 | 52 | console.log(`[API Route] Successfully fetched ${data.length} connections`) 53 | return createApiResponse(data) 54 | } catch (error) { 55 | console.error('[API Route] Error fetching connections:', error) 56 | return createApiErrorResponse('Failed to fetch connections from RabbitMQ') 57 | } 58 | } 59 | 60 | // Also handle DELETE requests for closing connections 61 | export async function DELETE(request: Request) { 62 | try { 63 | const { name } = await request.json() 64 | if (!name) { 65 | return NextResponse.json( 66 | { error: 'Connection name is required' }, 67 | { status: 400 } 68 | ) 69 | } 70 | 71 | const { host, port, username, password } = RABBITMQ_CONFIG 72 | const baseUrl = `http://${host}:${port}/api` 73 | const url = `${baseUrl}/connections/${encodeURIComponent(name)}` 74 | 75 | console.log(`[API Route] Closing connection: ${name}`) 76 | 77 | const authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}` 78 | 79 | const response = await fetch(url, { 80 | method: 'DELETE', 81 | headers: { 82 | 'Authorization': authHeader, 83 | }, 84 | }) 85 | 86 | if (!response.ok) { 87 | const errorText = await response.text() 88 | console.error(`[API Route] Error closing connection:`, { 89 | status: response.status, 90 | statusText: response.statusText, 91 | error: errorText 92 | }) 93 | return NextResponse.json( 94 | { error: `Failed to close connection: ${response.statusText}` }, 95 | { status: response.status } 96 | ) 97 | } 98 | 99 | return NextResponse.json({ success: true }) 100 | } catch (error) { 101 | console.error('[API Route] Error closing connection:', error) 102 | return NextResponse.json( 103 | { error: 'Failed to close connection' }, 104 | { status: 500 } 105 | ) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/api/exchanges/[vhost]/[exchange]/bindings/source/route.ts: -------------------------------------------------------------------------------- 1 | import { RABBITMQ_CONFIG } from '@/lib/utils' 2 | import { createApiResponse, createApiErrorResponse, NO_CACHE_HEADERS, NO_CACHE_FETCH_OPTIONS } from '@/lib/api-utils' 3 | 4 | export const dynamic = 'force-dynamic' 5 | export const revalidate = 0 6 | 7 | export async function GET( 8 | request: Request, 9 | { params }: { params: { vhost: string; exchange: string } } 10 | ) { 11 | try { 12 | const { host, port, username, password } = RABBITMQ_CONFIG 13 | const { vhost, exchange } = params 14 | const baseUrl = `http://${host}:${port}/api` 15 | const url = `${baseUrl}/exchanges/${encodeURIComponent(vhost)}/${encodeURIComponent(exchange)}/bindings/source` 16 | 17 | console.log(`[API Route] Fetching bindings for exchange ${exchange} in vhost ${vhost}`) 18 | console.log(`[API Route] Using host: ${host}:${port}`) 19 | 20 | const authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}` 21 | 22 | const response = await fetch(url, { 23 | headers: { 24 | 'Authorization': authHeader, 25 | 'Content-Type': 'application/json', 26 | ...NO_CACHE_HEADERS 27 | }, 28 | ...NO_CACHE_FETCH_OPTIONS, 29 | signal: AbortSignal.timeout(5000) 30 | }) 31 | 32 | if (!response.ok) { 33 | const errorText = await response.text() 34 | console.error(`[API Route] RabbitMQ API error:`, { 35 | status: response.status, 36 | statusText: response.statusText, 37 | error: errorText 38 | }) 39 | return createApiErrorResponse( 40 | `Failed to fetch exchange bindings: ${response.statusText}`, 41 | response.status 42 | ) 43 | } 44 | 45 | const data = await response.json() 46 | console.log(`[API Route] Successfully fetched ${data.length} bindings for exchange ${exchange}`) 47 | return createApiResponse(data) 48 | } catch (error) { 49 | console.error('[API Route] Error fetching exchange bindings:', error) 50 | return createApiErrorResponse('Failed to fetch exchange bindings from RabbitMQ') 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/api/exchanges/route.ts: -------------------------------------------------------------------------------- 1 | import { RABBITMQ_CONFIG } from '../../../lib/config' 2 | import { createApiResponse, createApiErrorResponse, NO_CACHE_HEADERS, NO_CACHE_FETCH_OPTIONS } from '@/lib/api-utils' 3 | 4 | export const dynamic = 'force-dynamic' 5 | export const revalidate = 0 6 | 7 | export async function GET() { 8 | try { 9 | const { host, port, username, password } = RABBITMQ_CONFIG 10 | const baseUrl = `http://${host}:${port}/api` 11 | const url = `${baseUrl}/exchanges` 12 | 13 | console.log(`[API Route] Fetching exchanges from ${url}`) 14 | console.log(`[API Route] Using host: ${host}:${port}`) 15 | 16 | const authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}` 17 | 18 | const response = await fetch(url, { 19 | headers: { 20 | 'Authorization': authHeader, 21 | 'Content-Type': 'application/json', 22 | ...NO_CACHE_HEADERS 23 | }, 24 | ...NO_CACHE_FETCH_OPTIONS, 25 | signal: AbortSignal.timeout(5000) 26 | }) 27 | 28 | if (!response.ok) { 29 | const errorText = await response.text() 30 | console.error(`[API Route] RabbitMQ API error:`, { 31 | status: response.status, 32 | statusText: response.statusText, 33 | error: errorText 34 | }) 35 | return createApiErrorResponse( 36 | `Failed to fetch exchanges: ${response.statusText}`, 37 | response.status 38 | ) 39 | } 40 | 41 | const data = await response.json() 42 | console.log(`[API Route] Successfully fetched ${data.length} exchanges`) 43 | return createApiResponse(data) 44 | } catch (error) { 45 | console.error('[API Route] Error fetching exchanges:', error) 46 | return createApiErrorResponse('Failed to fetch exchanges from RabbitMQ') 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/api/overview/route.ts: -------------------------------------------------------------------------------- 1 | import { getRabbitMQAuthHeaders, getRabbitMQBaseUrl } from '../../../lib/config' 2 | import { NextResponse } from 'next/server' 3 | 4 | export const dynamic = 'force-dynamic' 5 | 6 | // Cache the overview data for 2 seconds to prevent duplicate calls 7 | const CACHE_DURATION = 2000 // 2 seconds 8 | let cachedData: any = null 9 | let lastFetchTime = 0 10 | 11 | export async function GET() { 12 | try { 13 | const now = Date.now() 14 | 15 | // Return cached data if it's still fresh 16 | if (cachedData && (now - lastFetchTime) < CACHE_DURATION) { 17 | return NextResponse.json(cachedData, { 18 | headers: { 19 | 'Cache-Control': `public, max-age=${CACHE_DURATION / 1000}`, 20 | } 21 | }) 22 | } 23 | 24 | const baseUrl = getRabbitMQBaseUrl() 25 | const url = `${baseUrl}/api/overview` 26 | 27 | console.log(`[API Route] Fetching fresh overview data from ${url}`) 28 | 29 | const response = await fetch(url, { 30 | headers: getRabbitMQAuthHeaders(), 31 | cache: 'no-store', 32 | signal: AbortSignal.timeout(5000) 33 | }) 34 | 35 | if (!response.ok) { 36 | throw new Error(`RabbitMQ API error: ${response.statusText}`) 37 | } 38 | 39 | const data = await response.json() 40 | 41 | // Update cache 42 | cachedData = data 43 | lastFetchTime = now 44 | 45 | return NextResponse.json(data, { 46 | headers: { 47 | 'Cache-Control': `public, max-age=${CACHE_DURATION / 1000}`, 48 | } 49 | }) 50 | } catch (error) { 51 | console.error('[API Route] Error fetching overview:', error) 52 | const errorMessage = error instanceof Error ? error.message : String(error) 53 | return NextResponse.json( 54 | { error: errorMessage }, 55 | { status: 500 } 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/api/queues/[vhost]/[queue]/get/route.ts: -------------------------------------------------------------------------------- 1 | import { RABBITMQ_CONFIG } from '@/lib/config' 2 | import { createApiResponse, createApiErrorResponse, NO_CACHE_HEADERS, NO_CACHE_FETCH_OPTIONS } from '@/lib/api-utils' 3 | 4 | export const dynamic = 'force-dynamic' 5 | export const revalidate = 0 6 | 7 | export async function POST( 8 | request: Request, 9 | { params }: { params: { vhost: string; queue: string } } 10 | ) { 11 | try { 12 | const { host, port, username, password } = RABBITMQ_CONFIG 13 | const { vhost, queue } = params 14 | const baseUrl = `http://${host}:${port}/api` 15 | const url = `${baseUrl}/queues/${encodeURIComponent(vhost)}/${encodeURIComponent(queue)}/get` 16 | 17 | console.log(`[API Route] Fetching messages from queue ${queue} in vhost ${vhost}`) 18 | console.log(`[API Route] Using host: ${host}:${port}`) 19 | 20 | // Get the original request body or use defaults that won't affect processing 21 | const body = await request.json().catch(() => ({ 22 | count: 50, 23 | ackmode: "ack_requeue_true", 24 | encoding: "auto" 25 | })) 26 | 27 | const authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}` 28 | 29 | const response = await fetch(url, { 30 | method: 'POST', 31 | headers: { 32 | 'Authorization': authHeader, 33 | 'Content-Type': 'application/json', 34 | ...NO_CACHE_HEADERS 35 | }, 36 | ...NO_CACHE_FETCH_OPTIONS, 37 | body: JSON.stringify(body), // Use the original request body 38 | signal: AbortSignal.timeout(5000) 39 | }) 40 | 41 | if (!response.ok) { 42 | const errorText = await response.text() 43 | console.error(`[API Route] RabbitMQ API error:`, { 44 | status: response.status, 45 | statusText: response.statusText, 46 | error: errorText 47 | }) 48 | return createApiErrorResponse( 49 | `Failed to fetch messages: ${response.statusText}`, 50 | response.status 51 | ) 52 | } 53 | 54 | const data = await response.json() 55 | console.log(`[API Route] Successfully fetched ${data.length} messages from queue ${queue}`) 56 | return createApiResponse(data) 57 | } catch (error) { 58 | console.error('[API Route] Error fetching messages:', error) 59 | return createApiErrorResponse('Failed to fetch messages from RabbitMQ') 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/api/queues/route.ts: -------------------------------------------------------------------------------- 1 | import { getRabbitMQAuthHeaders, getRabbitMQBaseUrl } from '../../../lib/config' 2 | import { NextResponse } from 'next/server' 3 | 4 | export const dynamic = 'force-dynamic' 5 | export const revalidate = 0 6 | 7 | // Cache the queues data for 2 seconds to prevent duplicate calls 8 | const CACHE_DURATION = 2000 // 2 seconds 9 | let cachedData: any = null 10 | let lastFetchTime = 0 11 | 12 | export async function GET() { 13 | try { 14 | const now = Date.now() 15 | 16 | // Return cached data if it's still fresh 17 | if (cachedData && (now - lastFetchTime) < CACHE_DURATION) { 18 | return NextResponse.json(cachedData, { 19 | headers: { 20 | 'Cache-Control': `public, max-age=${CACHE_DURATION / 1000}`, 21 | } 22 | }) 23 | } 24 | 25 | const baseUrl = getRabbitMQBaseUrl() 26 | const url = `${baseUrl}/api/queues` 27 | 28 | console.log(`[API Route] Fetching fresh queues data from ${url}`) 29 | 30 | const response = await fetch(url, { 31 | headers: getRabbitMQAuthHeaders(), 32 | cache: 'no-store', 33 | signal: AbortSignal.timeout(5000) 34 | }) 35 | 36 | if (!response.ok) { 37 | const errorText = await response.text(); 38 | console.error(`[API Route] RabbitMQ API error response:`, errorText); 39 | throw new Error(`RabbitMQ API error: ${response.statusText}`); 40 | } 41 | 42 | const data = await response.json() 43 | 44 | // Update cache 45 | cachedData = data 46 | lastFetchTime = now 47 | 48 | return NextResponse.json(data, { 49 | headers: { 50 | 'Cache-Control': `public, max-age=${CACHE_DURATION / 1000}`, 51 | } 52 | }) 53 | } catch (error) { 54 | console.error('[API Route] Error fetching queues:', error) 55 | return NextResponse.json( 56 | { error: error.message }, 57 | { status: 500 } 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/api/ws/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { headers } from "next/headers" 3 | import { NO_CACHE_HEADERS } from '@/lib/api-utils' 4 | 5 | export async function GET(request: Request) { 6 | const headersList = headers() 7 | const upgrade = headersList.get("upgrade") 8 | 9 | if (!upgrade || upgrade !== "websocket") { 10 | return new NextResponse("Expected Upgrade: websocket", { status: 426 }) 11 | } 12 | 13 | try { 14 | const { searchParams } = new URL(request.url) 15 | const token = searchParams.get("token") 16 | 17 | // Validate token here if needed 18 | if (!token) { 19 | return new NextResponse("Unauthorized", { status: 401 }) 20 | } 21 | 22 | // Return response to upgrade the connection 23 | return new NextResponse(null, { 24 | status: 101, 25 | headers: { 26 | Upgrade: "websocket", 27 | Connection: "Upgrade", 28 | "Sec-WebSocket-Accept": token, 29 | ...NO_CACHE_HEADERS 30 | }, 31 | }) 32 | } catch (error) { 33 | console.error("WebSocket upgrade error:", error) 34 | return new NextResponse("Internal Server Error", { status: 500 }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/channels/page.tsx: -------------------------------------------------------------------------------- 1 | import { ChannelList } from "@/components/channels/channel-list" 2 | import { PageHeader } from "@/components/layout/page-header" 3 | import { ApiError } from "@/components/common/api-error" 4 | import { Suspense } from "react" 5 | import { ChannelListSkeleton } from "@/components/channels/channel-list-skeleton" 6 | 7 | export default function ChannelsPage() { 8 | return ( 9 |
10 | 14 | }> 15 | 16 | 17 | 18 | 19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /app/connections/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react" 2 | import { ConnectionList } from "@/components/connections/connection-list" 3 | import { ConnectionListSkeleton } from "@/components/connections/connection-list-skeleton" 4 | import { ApiError } from "@/components/common/api-error" 5 | import { getRabbitMQAuthHeaders, getRabbitMQBaseUrl } from "@/lib/config" 6 | import { handleRabbitMQError } from "@/lib/utils" 7 | 8 | export const dynamic = "force-dynamic" 9 | export const revalidate = 0 10 | 11 | async function ConnectionsPage() { 12 | try { 13 | const baseUrl = getRabbitMQBaseUrl() 14 | const url = `${baseUrl}/api/connections` 15 | 16 | const response = await fetch(url, { 17 | headers: getRabbitMQAuthHeaders(), 18 | cache: 'no-store', 19 | }) 20 | 21 | if (!response.ok) { 22 | throw new Error(`RabbitMQ API error: ${response.statusText}`) 23 | } 24 | 25 | const connections = await response.json() 26 | 27 | return ( 28 |
29 |
30 |
31 |

Connections

32 |

33 | Monitor and manage RabbitMQ connections 34 |

35 |
36 |
37 | 38 |
39 | ) 40 | } catch (error: unknown) { 41 | const handledError = handleRabbitMQError(error) 42 | console.error("[ConnectionsPage] Error:", handledError) 43 | return 44 | } 45 | } 46 | 47 | export default function Page() { 48 | return ( 49 | }> 50 | 51 | 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /app/exchanges/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react" 2 | import { ExchangeList } from "@/components/exchanges/exchange-list" 3 | import { ExchangeListSkeleton } from "@/components/exchanges/exchange-list-skeleton" 4 | import { ApiError } from "@/components/common/api-error" 5 | import { getRabbitMQAuthHeaders, getRabbitMQBaseUrl } from "@/lib/config" 6 | 7 | export const dynamic = "force-dynamic" 8 | export const revalidate = 0 9 | 10 | async function ExchangesPage() { 11 | try { 12 | const baseUrl = getRabbitMQBaseUrl() 13 | const url = `${baseUrl}/api/exchanges` 14 | 15 | const response = await fetch(url, { 16 | headers: getRabbitMQAuthHeaders(), 17 | cache: 'no-store', 18 | }) 19 | 20 | if (!response.ok) { 21 | throw new Error(`RabbitMQ API error: ${response.statusText}`) 22 | } 23 | 24 | const exchanges = await response.json() 25 | 26 | return ( 27 |
28 |
29 |
30 |

Exchanges

31 |

32 | Manage and monitor RabbitMQ exchanges 33 |

34 |
35 |
36 | 37 |
38 | ) 39 | } catch (error: any) { 40 | console.error("[ExchangesPage] Error:", error) 41 | return 42 | } 43 | } 44 | 45 | export default function Page() { 46 | return ( 47 | }> 48 | 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ralve-org/RabbitScout/aaf2b312215616a0a41b1f15ff8734be80cad7c7/app/favicon.ico -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ralve-org/RabbitScout/aaf2b312215616a0a41b1f15ff8734be80cad7c7/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ralve-org/RabbitScout/aaf2b312215616a0a41b1f15ff8734be80cad7c7/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer utilities { 10 | .text-balance { 11 | text-wrap: balance; 12 | } 13 | } 14 | 15 | 16 | @layer base { 17 | :root { 18 | --background: 0 0% 100%; 19 | --foreground: 20 14.3% 4.1%; 20 | --card: 0 0% 100%; 21 | --card-foreground: 20 14.3% 4.1%; 22 | --popover: 0 0% 100%; 23 | --popover-foreground: 20 14.3% 4.1%; 24 | --primary: 24.6 95% 53.1%; 25 | --primary-foreground: 60 9.1% 97.8%; 26 | --secondary: 60 4.8% 95.9%; 27 | --secondary-foreground: 24 9.8% 10%; 28 | --muted: 60 4.8% 95.9%; 29 | --muted-foreground: 25 5.3% 44.7%; 30 | --accent: 60 4.8% 95.9%; 31 | --accent-foreground: 24 9.8% 10%; 32 | --destructive: 0 84.2% 60.2%; 33 | --destructive-foreground: 60 9.1% 97.8%; 34 | --border: 20 5.9% 90%; 35 | --input: 20 5.9% 90%; 36 | --ring: 24.6 95% 53.1%; 37 | --radius: 0.3rem; 38 | --chart-1: 12 76% 61%; 39 | --chart-2: 173 58% 39%; 40 | --chart-3: 197 37% 24%; 41 | --chart-4: 43 74% 66%; 42 | --chart-5: 27 87% 67%; 43 | } 44 | 45 | .dark { 46 | --background: 20 14.3% 4.1%; 47 | --foreground: 60 9.1% 97.8%; 48 | --card: 20 14.3% 4.1%; 49 | --card-foreground: 60 9.1% 97.8%; 50 | --popover: 20 14.3% 4.1%; 51 | --popover-foreground: 60 9.1% 97.8%; 52 | --primary: 20.5 90.2% 48.2%; 53 | --primary-foreground: 60 9.1% 97.8%; 54 | --secondary: 12 6.5% 15.1%; 55 | --secondary-foreground: 60 9.1% 97.8%; 56 | --muted: 12 6.5% 15.1%; 57 | --muted-foreground: 24 5.4% 63.9%; 58 | --accent: 12 6.5% 15.1%; 59 | --accent-foreground: 60 9.1% 97.8%; 60 | --destructive: 0 72.2% 50.6%; 61 | --destructive-foreground: 60 9.1% 97.8%; 62 | --border: 12 6.5% 15.1%; 63 | --input: 12 6.5% 15.1%; 64 | --ring: 20.5 90.2% 48.2%; 65 | --chart-1: 220 70% 50%; 66 | --chart-2: 160 60% 45%; 67 | --chart-3: 30 80% 55%; 68 | --chart-4: 280 65% 60%; 69 | --chart-5: 340 75% 55%; 70 | } 71 | } 72 | 73 | 74 | @layer base { 75 | * { 76 | @apply border-border; 77 | } 78 | body { 79 | @apply bg-background text-foreground; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Inter } from "next/font/google"; 4 | import { usePathname } from "next/navigation"; 5 | import "./globals.css"; 6 | import { Header } from "@/components/layout/header"; 7 | import { ThemeProvider } from "@/components/theme-provider"; 8 | import { Toaster } from "@/components/ui/toaster"; 9 | 10 | const inter = Inter({ subsets: ["latin"] }); 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: { 15 | children: React.ReactNode; 16 | }) { 17 | const pathname = usePathname(); 18 | const isLoginPage = pathname === "/login"; 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 |
34 | {!isLoginPage &&
} 35 |
42 | {children} 43 |
44 |
45 | 46 |
47 | 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /app/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next" 2 | import Image from "next/image" 3 | import { LoginForm } from "@/components/auth/login-form" 4 | 5 | // Import the logo 6 | import logo from "@/public/images/logo.png" 7 | 8 | export const metadata: Metadata = { 9 | title: "Login - RabbitScout", 10 | description: "Login to RabbitScout Dashboard", 11 | } 12 | 13 | export default function LoginPage() { 14 | return ( 15 |
16 |
17 |
18 |
19 | RabbitScout Logo 27 |
28 |

29 | Welcome to RabbitScout 30 |

31 |

32 | Enter your credentials to access the dashboard 33 |

34 |
35 | 36 |
37 |
38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /app/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ralve-org/RabbitScout/aaf2b312215616a0a41b1f15ff8734be80cad7c7/app/logo.png -------------------------------------------------------------------------------- /app/metadata.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next" 2 | 3 | export const metadata: Metadata = { 4 | title: "RabbitScout - RabbitMQ Dashboard", 5 | description: "A modern RabbitMQ management dashboard", 6 | } 7 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Suspense, useEffect, useState } from "react" 4 | import { MessageRateChart } from "@/components/dashboard/message-rate-chart" 5 | import { OverviewStats } from "@/components/dashboard/overview-stats" 6 | import { QueueDistributionChart } from "@/components/dashboard/queue-distribution-chart" 7 | import { QueuedMessagesChart } from "@/components/dashboard/queued-messages-chart" 8 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" 9 | import { Skeleton } from "@/components/ui/skeleton" 10 | import { getOverview, getQueues, RabbitMQError } from "@/lib/utils" 11 | import { ApiError } from "@/components/common/api-error" 12 | 13 | // Disable static page generation and caching 14 | export const dynamic = 'force-dynamic' 15 | 16 | function Overview() { 17 | const [error, setError] = useState(null); 18 | const [overviewData, setOverviewData] = useState(null); 19 | const [queueDistribution, setQueueDistribution] = useState>([]); 20 | const [messageRateData, setMessageRateData] = useState>([]); 25 | 26 | const [queuedMessagesData, setQueuedMessagesData] = useState>([]); 32 | 33 | const handleDataUpdate = (newData: any) => { 34 | const timestamp = Date.now(); 35 | const oneMinuteAgo = timestamp - 60 * 1000; 36 | 37 | // Update message rate data 38 | setMessageRateData(prev => { 39 | const newPoint = { 40 | timestamp, 41 | publishRate: newData.message_stats?.publish_details?.rate || 0, 42 | deliveryRate: newData.message_stats?.deliver_get_details?.rate || 0, 43 | }; 44 | // Filter out data older than 1 minute and add new point 45 | const filtered = prev.filter(point => point.timestamp > oneMinuteAgo); 46 | return [...filtered, newPoint]; 47 | }); 48 | 49 | // Update queued messages data 50 | setQueuedMessagesData(prev => { 51 | const newPoint = { 52 | timestamp, 53 | messages: newData.queue_totals?.messages || 0, 54 | messagesReady: newData.queue_totals?.messages_ready || 0, 55 | messagesUnacked: newData.queue_totals?.messages_unacknowledged || 0, 56 | }; 57 | // Filter out data older than 1 minute and add new point 58 | const filtered = prev.filter(point => point.timestamp > oneMinuteAgo); 59 | return [...filtered, newPoint]; 60 | }); 61 | }; 62 | 63 | useEffect(() => { 64 | const fetchData = async () => { 65 | try { 66 | // Get auth state from localStorage 67 | const authData = localStorage.getItem('auth-storage'); 68 | if (!authData) { 69 | throw new Error('No authentication data found'); 70 | } 71 | 72 | const { state } = JSON.parse(authData); 73 | if (!state?.authenticated || !state?.user) { 74 | throw new Error('User not authenticated'); 75 | } 76 | 77 | const { username } = state.user; 78 | const host = process.env.NEXT_PUBLIC_RABBITMQ_HOST; 79 | const port = process.env.NEXT_PUBLIC_RABBITMQ_PORT || '15672'; 80 | 81 | const [overviewResponse, queuesResponse] = await Promise.all([ 82 | getOverview(host, port, username), 83 | getQueues(host, port, username) 84 | ]); 85 | 86 | setOverviewData(overviewResponse); 87 | handleDataUpdate(overviewResponse); 88 | 89 | // Process queue distribution data 90 | if (queuesResponse) { 91 | const distribution = queuesResponse 92 | .sort((a, b) => b.messages - a.messages) 93 | .slice(0, 6) 94 | .map((queue) => ({ 95 | name: queue.name, 96 | value: queue.messages, 97 | })); 98 | 99 | if (queuesResponse.length > 6) { 100 | const othersMessages = queuesResponse 101 | .slice(6) 102 | .reduce((sum, queue) => sum + queue.messages, 0); 103 | distribution.push({ 104 | name: "Others", 105 | value: othersMessages, 106 | }); 107 | } 108 | 109 | setQueueDistribution(distribution); 110 | } 111 | 112 | // No error, clear any existing error 113 | setError(null); 114 | } catch (err) { 115 | console.error('Error fetching dashboard data:', err); 116 | setError(err as RabbitMQError); 117 | } 118 | }; 119 | 120 | const interval = setInterval(fetchData, 5000); 121 | fetchData(); // Initial fetch 122 | 123 | return () => clearInterval(interval); 124 | }, []); 125 | 126 | if (error) { 127 | return ; 128 | } 129 | 130 | if (!overviewData) { 131 | return ; 132 | } 133 | 134 | return ( 135 |
136 | 137 | 138 |
139 | 140 | 141 | Message Rate 142 | 143 | Message publish and delivery rates over time 144 | 145 | 146 | 147 |
148 | 149 |
150 |
151 |
152 | 153 | 154 | Queue Distribution 155 | 156 | Distribution of messages across queues 157 | 158 | 159 | 160 |
161 | 162 |
163 |
164 |
165 |
166 | 167 | 168 | 169 | Queued Messages 170 | 171 | Number of messages in queues over time 172 | 173 | 174 | 175 |
176 | 177 |
178 |
179 |
180 |
181 | ); 182 | } 183 | 184 | function OverviewSkeleton() { 185 | return ( 186 |
187 |
188 | {Array.from({ length: 4 }).map((_, i) => ( 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | ))} 202 |
203 | 204 |
205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 |
224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 |
235 | ) 236 | } 237 | 238 | export default function Page() { 239 | return ( 240 | }> 241 | 242 | 243 | ) 244 | } 245 | -------------------------------------------------------------------------------- /app/queues/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react" 2 | import { QueueList } from "@/components/queues/queue-list" 3 | import { QueueListSkeleton } from "@/components/queues/queue-list-skeleton" 4 | import { ApiError } from "@/components/common/api-error" 5 | import { getRabbitMQAuthHeaders, getRabbitMQBaseUrl, API_TIMEOUT_MS } from "@/lib/config" 6 | 7 | export const dynamic = "force-dynamic" 8 | export const revalidate = 0 9 | 10 | async function QueuesPage() { 11 | try { 12 | const baseUrl = getRabbitMQBaseUrl() 13 | const url = `${baseUrl}/api/queues` 14 | 15 | const response = await fetch(url, { 16 | headers: getRabbitMQAuthHeaders(), 17 | cache: 'no-store', 18 | signal: AbortSignal.timeout(API_TIMEOUT_MS), 19 | }) 20 | 21 | if (!response.ok) { 22 | throw new Error(`RabbitMQ API error: ${response.statusText}`) 23 | } 24 | 25 | const queues = await response.json() 26 | 27 | return ( 28 |
29 |
30 |
31 |

Queues

32 |

33 | Manage and monitor your RabbitMQ queues 34 |

35 |
36 |
37 | 38 |
39 | ) 40 | } catch (error: any) { 41 | console.error("[QueuesPage] Error:", error) 42 | return 43 | } 44 | } 45 | 46 | export default function Page() { 47 | return ( 48 | }> 49 | 50 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /components/auth/login-form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | import { useRouter } from "next/navigation" 5 | import { Button } from "@/components/ui/button" 6 | import { Eye, EyeOff } from "lucide-react" 7 | import { 8 | Card, 9 | CardContent, 10 | CardDescription, 11 | CardHeader, 12 | CardTitle, 13 | } from "@/components/ui/card" 14 | import { Input } from "@/components/ui/input" 15 | import { Label } from "@/components/ui/label" 16 | import { useToast } from "@/hooks/use-toast" 17 | import { useAuth } from "@/lib/auth" 18 | 19 | export function LoginForm() { 20 | const [isLoading, setIsLoading] = useState(false) 21 | const [showPassword, setShowPassword] = useState(false) 22 | const router = useRouter() 23 | const { toast } = useToast() 24 | const { login } = useAuth() 25 | 26 | async function onSubmit(event: React.FormEvent) { 27 | event.preventDefault() 28 | setIsLoading(true) 29 | 30 | const formData = new FormData(event.target as HTMLFormElement) 31 | const username = formData.get("username") as string 32 | const password = formData.get("password") as string 33 | 34 | try { 35 | const response = await fetch("/api/auth/verify", { 36 | method: "POST", 37 | headers: { 38 | "Content-Type": "application/json", 39 | }, 40 | body: JSON.stringify({ username, password }), 41 | }) 42 | 43 | const data = await response.json() 44 | 45 | if (!response.ok) { 46 | throw new Error(data.error || "Authentication failed") 47 | } 48 | 49 | if (data.authenticated && data.user) { 50 | // Store auth data in Zustand store 51 | await login(data) 52 | router.push("/") 53 | toast({ 54 | title: "Welcome back!", 55 | description: `Logged in as ${data.user.username}${data.user.isAdmin ? ' (Administrator)' : ''}`, 56 | }) 57 | } else { 58 | throw new Error("Invalid response from server") 59 | } 60 | } catch (error) { 61 | console.error("Login error:", error) 62 | toast({ 63 | variant: "destructive", 64 | title: "Login failed", 65 | description: error instanceof Error ? error.message : "An unexpected error occurred", 66 | }) 67 | } finally { 68 | setIsLoading(false) 69 | } 70 | } 71 | 72 | return ( 73 | 74 | 75 | Login 76 | 77 | Enter your RabbitMQ management credentials 78 | 79 | 80 | 81 |
82 |
83 |
84 | 85 | 92 |
93 |
94 | 95 |
96 | 104 | 115 |
116 |
117 | 123 |
124 |
125 |
126 |
127 | ) 128 | } 129 | -------------------------------------------------------------------------------- /components/channels/channel-list-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton" 2 | import { 3 | Table, 4 | TableBody, 5 | TableCell, 6 | TableHead, 7 | TableHeader, 8 | TableRow, 9 | } from "@/components/ui/table" 10 | 11 | export function ChannelListSkeleton() { 12 | return ( 13 |
14 | 15 | 16 | 17 | Channel 18 | User 19 | State 20 | Prefetch 21 | Unacked 22 | Consumers 23 | Actions 24 | 25 | 26 | 27 | {Array.from({ length: 5 }).map((_, i) => ( 28 | 29 | 30 |
31 | 32 | 33 |
34 |
35 | 36 |
37 | 38 | 39 |
40 |
41 | 42 |
43 | 44 |
45 |
46 | 47 |
48 | 49 |
50 |
51 | 52 |
53 | 54 |
55 |
56 | 57 |
58 | 59 |
60 |
61 | 62 |
63 | 64 |
65 |
66 |
67 | ))} 68 |
69 |
70 |
71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /components/channels/channel-list.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useEffect } from "react" 4 | import { 5 | Table, 6 | TableBody, 7 | TableCell, 8 | TableHead, 9 | TableHeader, 10 | TableRow, 11 | } from "@/components/ui/table" 12 | import { Button } from "@/components/ui/button" 13 | import { API_ENDPOINTS, formatRate, RabbitMQError } from "@/lib/utils" 14 | import { useToast } from "@/hooks/use-toast" 15 | import { ChannelListSkeleton } from "./channel-list-skeleton" 16 | import { MoreHorizontal, ArrowUpDown, XCircle } from "lucide-react" 17 | import { 18 | DropdownMenu, 19 | DropdownMenuContent, 20 | DropdownMenuItem, 21 | DropdownMenuTrigger, 22 | } from "@/components/ui/dropdown-menu" 23 | import { ApiError } from "@/components/common/api-error" 24 | import { useRealtimeUpdates } from "@/hooks/use-realtime-updates" 25 | 26 | interface MessageStats { 27 | publish?: number 28 | publish_details?: { 29 | rate: number 30 | } 31 | deliver_get?: number 32 | deliver_get_details?: { 33 | rate: number 34 | } 35 | confirm?: number 36 | confirm_details?: { 37 | rate: number 38 | } 39 | get?: number 40 | get_details?: { 41 | rate: number 42 | } 43 | deliver?: number 44 | deliver_details?: { 45 | rate: number 46 | } 47 | deliver_no_ack?: number 48 | deliver_no_ack_details?: { 49 | rate: number 50 | } 51 | redeliver?: number 52 | redeliver_details?: { 53 | rate: number 54 | } 55 | return_unroutable?: number 56 | return_unroutable_details?: { 57 | rate: number 58 | } 59 | } 60 | 61 | interface Channel { 62 | name: string 63 | number: number 64 | user: string 65 | connection_details: { 66 | name: string 67 | peer_host: string 68 | peer_port: number 69 | user: string 70 | } 71 | state: string 72 | prefetch_count: number 73 | messages_unacknowledged: number 74 | messages_unconfirmed: number 75 | messages_uncommitted: number 76 | acks_uncommitted: number 77 | consumer_count: number 78 | confirm: boolean 79 | transactional: boolean 80 | global_prefetch_count: number 81 | message_stats?: MessageStats 82 | idle_since?: string 83 | node: string 84 | vhost: string 85 | } 86 | 87 | type SortField = "name" | "connection" | "state" | "prefetch" | "messages" | "consumers" | "rate" | "user" 88 | type SortOrder = "asc" | "desc" 89 | 90 | export function ChannelList() { 91 | const [channels, setChannels] = useState([]) 92 | const [isLoading, setIsLoading] = useState(true) 93 | const [error, setError] = useState(null) 94 | const [sortField, setSortField] = useState("name") 95 | const [sortOrder, setSortOrder] = useState("asc") 96 | const { toast } = useToast() 97 | 98 | const fetchChannels = async () => { 99 | try { 100 | const response = await fetch(API_ENDPOINTS.channels.list) 101 | if (!response.ok) { 102 | const errorText = await response.text() 103 | throw new Error(errorText) 104 | } 105 | const data = await response.json() 106 | setChannels(data) 107 | setError(null) 108 | setIsLoading(false) 109 | } catch (err) { 110 | console.error("Error fetching channels:", err) 111 | setError({ 112 | status: 503, 113 | message: 'Unable to connect to RabbitMQ server. Please check your connection settings.', 114 | type: 'CONNECTION', 115 | details: err instanceof Error ? err.message : String(err) 116 | }) 117 | toast({ 118 | variant: "destructive", 119 | title: "Error fetching channels", 120 | description: "Failed to connect to RabbitMQ server", 121 | }) 122 | } 123 | } 124 | 125 | useEffect(() => { 126 | fetchChannels() 127 | }, []) 128 | 129 | const realtimeChannels = useRealtimeUpdates('channel', channels, (current, update) => { 130 | return current.map(channel => 131 | channel.name === update.name ? { ...channel, ...update } : channel 132 | ) 133 | }) 134 | 135 | const sortChannels = (field: SortField) => { 136 | if (field === sortField) { 137 | setSortOrder(sortOrder === "asc" ? "desc" : "asc") 138 | } else { 139 | setSortField(field) 140 | setSortOrder("asc") 141 | } 142 | } 143 | 144 | const getSortedChannels = () => { 145 | const sorted = [...realtimeChannels].sort((a, b) => { 146 | const multiplier = sortOrder === "asc" ? 1 : -1 147 | switch (sortField) { 148 | case "name": 149 | return multiplier * a.name.localeCompare(b.name) 150 | case "connection": 151 | return multiplier * a.connection_details.name.localeCompare(b.connection_details.name) 152 | case "state": 153 | return multiplier * a.state.localeCompare(b.state) 154 | case "prefetch": 155 | return multiplier * (a.prefetch_count - b.prefetch_count) 156 | case "messages": 157 | return multiplier * (a.messages_unacknowledged - b.messages_unacknowledged) 158 | case "consumers": 159 | return multiplier * (a.consumer_count - b.consumer_count) 160 | case "rate": 161 | const rateA = a.message_stats?.publish_details?.rate || 0 162 | const rateB = b.message_stats?.publish_details?.rate || 0 163 | return multiplier * (rateA - rateB) 164 | case "user": 165 | return multiplier * a.user.localeCompare(b.user) 166 | default: 167 | return 0 168 | } 169 | }) 170 | return sorted 171 | } 172 | 173 | if (isLoading && !error) { 174 | return 175 | } 176 | 177 | if (error) { 178 | return {null} 179 | } 180 | 181 | return ( 182 |
183 | 184 | 185 | 186 | 187 | 195 | 196 | 197 | 205 | 206 | 207 | 215 | 216 | 217 | 225 | 226 | 227 | 235 | 236 | 237 | 245 | 246 | Actions 247 | 248 | 249 | 250 | {getSortedChannels().map((channel) => ( 251 | 252 | 253 |
#{channel.number}
254 |
255 | {channel.connection_details.name} 256 |
257 |
258 | 259 |
{channel.user}
260 |
261 | {channel.vhost} 262 |
263 |
264 | 265 |
266 | {channel.state} 267 |
268 |
269 | 270 |
{channel.prefetch_count}
271 |
272 | 273 |
{channel.messages_unacknowledged}
274 |
275 | 276 |
{channel.consumer_count}
277 |
278 | 279 |
280 | 281 | 282 | 286 | 287 | 288 | 292 | 293 | Close Channel 294 | 295 | 296 | 297 |
298 |
299 |
300 | ))} 301 |
302 |
303 |
304 | ) 305 | } 306 | -------------------------------------------------------------------------------- /components/common/api-error.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ReactNode } from "react" 4 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" 5 | import { RabbitMQError } from "@/lib/utils" 6 | 7 | interface ApiErrorProps { 8 | children?: ReactNode 9 | error?: RabbitMQError 10 | } 11 | 12 | export function ApiError({ children, error }: ApiErrorProps) { 13 | if (!error) { 14 | return <>{children} 15 | } 16 | 17 | return ( 18 |
19 | 20 | 21 | Connection Error 22 | 23 | {error.message} 24 | 25 | 26 | 27 |
28 | {error.type === 'CONNECTION' && ( 29 |
    30 |
  • Check if the RabbitMQ server is running
  • 31 |
  • Verify the host and port settings in your .env.local file
  • 32 |
  • Ensure your network connection is stable
  • 33 |
  • Check if any firewalls are blocking the connection
  • 34 |
35 | )} 36 | {error.type === 'AUTH' && ( 37 |
    38 |
  • Verify your username and password in .env.local
  • 39 |
  • Check if the user has the correct permissions
  • 40 |
  • Ensure the virtual host settings are correct
  • 41 |
42 | )} 43 | {error.type === 'TIMEOUT' && ( 44 |
    45 |
  • The server is taking too long to respond
  • 46 |
  • Check if the RabbitMQ server is under heavy load
  • 47 |
  • Verify network latency between your application and the server
  • 48 |
49 | )} 50 | {error.type === 'UNKNOWN' && ( 51 |
    52 |
  • An unexpected error occurred
  • 53 |
  • Check the browser console for more details
  • 54 |
  • Try refreshing the page
  • 55 |
56 | )} 57 | {error.details && ( 58 |

59 | Technical details: {error.details} 60 |

61 | )} 62 |
63 |
64 |
65 |
66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /components/connections/connection-list-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | TableBody, 4 | TableCell, 5 | TableHead, 6 | TableHeader, 7 | TableRow, 8 | } from "@/components/ui/table" 9 | import { Skeleton } from "@/components/ui/skeleton" 10 | 11 | export function ConnectionListSkeleton() { 12 | return ( 13 |
14 |
15 |
16 | 17 | 18 |
19 |
20 |
21 | 22 | 23 | 24 | Client 25 | User 26 | Channels 27 | SSL/TLS 28 | Protocol 29 | Throughput 30 | Actions 31 | 32 | 33 | 34 | {Array.from({ length: 5 }).map((_, i) => ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ))} 62 | 63 |
64 |
65 |
66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /components/connections/connection-list.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useEffect } from "react" 4 | import { ConnectionOperations } from "./connection-operations" 5 | import { 6 | Table, 7 | TableBody, 8 | TableCell, 9 | TableHead, 10 | TableHeader, 11 | TableRow, 12 | } from "@/components/ui/table" 13 | import { formatBytes, formatRate } from "@/lib/utils" 14 | import { ArrowUpDown, Unplug } from "lucide-react" 15 | import { Button } from "@/components/ui/button" 16 | import { useRealtimeUpdates } from "@/hooks/use-realtime-updates" 17 | import { API_ENDPOINTS, RabbitMQError } from "@/lib/utils" 18 | import { useToast } from "@/hooks/use-toast" 19 | import { ConnectionListSkeleton } from "./connection-list-skeleton" 20 | 21 | interface Connection { 22 | name: string 23 | user: string 24 | vhost: string 25 | host: string 26 | port: number 27 | peer_host: string 28 | peer_port: number 29 | ssl: boolean 30 | protocol: string 31 | auth_mechanism: string 32 | state: string 33 | connected_at: number 34 | timeout: number 35 | frame_max: number 36 | channel_max: number 37 | client_properties: { 38 | platform?: string 39 | product?: string 40 | version?: string 41 | information?: string 42 | } 43 | recv_oct: number 44 | recv_oct_details: { 45 | rate: number 46 | } 47 | send_oct: number 48 | send_oct_details: { 49 | rate: number 50 | } 51 | recv_cnt: number 52 | send_cnt: number 53 | send_pend: number 54 | channels: number 55 | ssl_protocol?: string 56 | ssl_key_exchange?: string 57 | ssl_cipher?: string 58 | ssl_hash?: string 59 | } 60 | 61 | type SortField = keyof Connection 62 | type SortOrder = "asc" | "desc" 63 | 64 | export function ConnectionList({ connections: initialConnections }: { connections?: Connection[] }) { 65 | const [connections, setConnections] = useState(initialConnections || []) 66 | const [isLoading, setIsLoading] = useState(!initialConnections) 67 | const [error, setError] = useState(null) 68 | const [sortField, setSortField] = useState("name") 69 | const [sortOrder, setSortOrder] = useState("asc") 70 | const { toast } = useToast() 71 | 72 | useEffect(() => { 73 | if (!initialConnections) { 74 | fetchConnections() 75 | } 76 | }, []) 77 | 78 | const realtimeConnections = useRealtimeUpdates('connection', connections, (current, update) => { 79 | return current.map(conn => 80 | conn.name === update.name ? { ...conn, ...update } : conn 81 | ) 82 | }) 83 | 84 | const fetchConnections = async () => { 85 | try { 86 | const response = await fetch(API_ENDPOINTS.connections.list) 87 | if (!response.ok) { 88 | const errorText = await response.text() 89 | throw new Error(errorText) 90 | } 91 | const data = await response.json() 92 | setConnections(data) 93 | setError(null) 94 | setIsLoading(false) 95 | } catch (err) { 96 | console.error("Error fetching connections:", err) 97 | setError({ 98 | status: 503, 99 | message: 'Unable to connect to RabbitMQ server. Please check your connection settings.', 100 | type: 'CONNECTION', 101 | details: err instanceof Error ? err.message : String(err) 102 | }) 103 | toast({ 104 | variant: "destructive", 105 | title: "Error fetching connections", 106 | description: "Failed to connect to RabbitMQ server", 107 | }) 108 | } 109 | } 110 | 111 | const sortConnections = (field: SortField) => { 112 | if (sortField === field) { 113 | setSortOrder(sortOrder === "asc" ? "desc" : "asc") 114 | } else { 115 | setSortField(field) 116 | setSortOrder("asc") 117 | } 118 | 119 | const sortedConnections = [...realtimeConnections].sort((a, b) => { 120 | const aValue = a[field] 121 | const bValue = b[field] 122 | 123 | if (typeof aValue === "string" && typeof bValue === "string") { 124 | return sortOrder === "asc" 125 | ? aValue.localeCompare(bValue) 126 | : bValue.localeCompare(aValue) 127 | } 128 | 129 | return sortOrder === "asc" 130 | ? (aValue as number) - (bValue as number) 131 | : (bValue as number) - (aValue as number) 132 | }) 133 | 134 | setConnections(sortedConnections) 135 | } 136 | 137 | const handleSort = (field: SortField) => { 138 | sortConnections(field) 139 | } 140 | 141 | const handleClose = () => { 142 | fetchConnections() 143 | } 144 | 145 | if (isLoading) { 146 | return 147 | } 148 | 149 | return ( 150 |
151 |
152 | 153 | 154 | 155 | 156 | 163 | 164 | 165 | 172 | 173 | VHost 174 | Protocol 175 | SSL/TLS 176 | 177 | 184 | 185 | 186 | 193 | 194 | 195 | 202 | 203 | 204 | 205 | 206 | {realtimeConnections.length === 0 ? ( 207 | 208 | 209 |
210 | 211 |

No Active Connections

212 |

There are currently no active connections to the RabbitMQ server.

213 |
214 |
215 |
216 | ) : ( 217 | realtimeConnections.map((connection) => ( 218 | 219 | 220 | {connection.client_properties?.product || "Unknown Client"} 221 |
222 | {connection.name} 223 |
224 |
225 | 226 | {connection.user} 227 |
228 | {connection.auth_mechanism} 229 |
230 |
231 | {connection.vhost} 232 | 233 | {connection.protocol} 234 |
235 | {connection.client_properties?.platform} 236 |
237 |
238 | 239 | {connection.ssl ? ( 240 |
241 |
242 |
243 | Yes 244 |
245 |
246 | ) : ( 247 |
248 |
249 |
250 | No 251 |
252 |
253 | )} 254 | 255 | {connection.channels} 256 | 257 |
258 | ↓ {formatBytes(connection.recv_oct)} 259 |
260 | {formatRate(connection.recv_oct_details.rate)} /s 261 |
262 |
263 |
264 | ↑ {formatBytes(connection.send_oct)} 265 |
266 | {formatRate(connection.send_oct_details.rate)} /s 267 |
268 |
269 |
270 | 271 |
272 | 273 |
274 |
275 | 276 | )) 277 | )} 278 | 279 |
280 |
281 |
282 | ) 283 | } 284 | -------------------------------------------------------------------------------- /components/connections/connection-operations.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | import { Button } from "@/components/ui/button" 5 | import { 6 | Dialog, 7 | DialogContent, 8 | DialogDescription, 9 | DialogFooter, 10 | DialogHeader, 11 | DialogTitle, 12 | } from "@/components/ui/dialog" 13 | import { 14 | DropdownMenu, 15 | DropdownMenuContent, 16 | DropdownMenuItem, 17 | DropdownMenuTrigger, 18 | } from "@/components/ui/dropdown-menu" 19 | import { closeConnection } from "@/lib/utils" 20 | import { MoreHorizontal, XCircle } from "lucide-react" 21 | import { useRouter } from "next/navigation" 22 | 23 | interface ConnectionOperationsProps { 24 | connection: { 25 | name: string 26 | client_properties?: { 27 | connection_name?: string 28 | product?: string 29 | } 30 | } 31 | onClose: () => void 32 | } 33 | 34 | export function ConnectionOperations({ connection, onClose }: ConnectionOperationsProps) { 35 | const router = useRouter() 36 | const [closeDialogOpen, setCloseDialogOpen] = useState(false) 37 | const [isLoading, setIsLoading] = useState(false) 38 | 39 | const handleClose = async () => { 40 | try { 41 | setIsLoading(true) 42 | await closeConnection(connection.name) 43 | onClose() 44 | } catch (error) { 45 | console.error("Failed to close connection:", error) 46 | } finally { 47 | setIsLoading(false) 48 | setCloseDialogOpen(false) 49 | } 50 | } 51 | 52 | return ( 53 | <> 54 | 55 | 56 | 60 | 61 | 62 | 66 | 67 | Close Connection 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | Close Connection 76 | 77 | Are you sure you want to close this connection? 78 | {connection.client_properties?.product && ( 79 |
80 | Client: {connection.client_properties.product} 81 |
82 | )} 83 |
{connection.name}
84 |
85 |
86 | 87 | 94 | 101 | 102 |
103 |
104 | 105 | ) 106 | } 107 | -------------------------------------------------------------------------------- /components/dashboard/message-rate-chart.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | Line, 5 | LineChart, 6 | CartesianGrid, 7 | ResponsiveContainer, 8 | Tooltip, 9 | XAxis, 10 | YAxis, 11 | Legend, 12 | } from "recharts" 13 | import { formatRate } from "@/lib/utils" 14 | import { format } from 'date-fns' 15 | import { useState, useEffect } from 'react' 16 | 17 | interface MessageRateChartProps { 18 | data: { 19 | timestamp: number 20 | publishRate: number 21 | deliveryRate: number 22 | }[] 23 | } 24 | 25 | export const MessageRateChart = ({ data }: MessageRateChartProps) => { 26 | const [currentTime, setCurrentTime] = useState(Date.now()); 27 | 28 | useEffect(() => { 29 | const interval = setInterval(() => { 30 | setCurrentTime(Date.now()); 31 | }, 1000); 32 | return () => clearInterval(interval); 33 | }, []); 34 | 35 | const timeDomain = { 36 | start: currentTime - 60 * 1000, 37 | end: currentTime, 38 | }; 39 | 40 | const ticks = Array.from({ length: 7 }, (_, i) => timeDomain.start + (i * 10000)); 41 | 42 | return ( 43 |
44 |
45 | 46 | 49 | 50 | format(new Date(timestamp), 'HH:mm:ss')} 56 | scale="time" 57 | className="text-muted-foreground" 58 | stroke="currentColor" 59 | fontSize={11} 60 | dy={10} 61 | /> 62 | `${value}/s`} 64 | className="text-muted-foreground" 65 | stroke="currentColor" 66 | fontSize={11} 67 | width={40} 68 | /> 69 | { 71 | if (active && payload && payload.length) { 72 | return ( 73 |
74 |
75 |
76 | {new Date(payload[0].payload.timestamp).toLocaleTimeString()} 77 |
78 |
79 |
80 | Publish 81 | {payload[0].value}/s 82 |
83 |
84 | Deliver 85 | {payload[1].value}/s 86 |
87 |
88 |
89 |
90 | ) 91 | } 92 | return null 93 | }} 94 | /> 95 | ( 105 | {value} 106 | )} 107 | /> 108 | 119 | 130 |
131 |
132 |
133 |
134 | ) 135 | } 136 | -------------------------------------------------------------------------------- /components/dashboard/overview-stats.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" 4 | import { formatBytes, formatRate, formatUptime, rabbitMQFetch, getNodeStats } from "@/lib/utils" 5 | import { Activity, MessageSquare, Network, Server } from "lucide-react" 6 | import { useEffect, useState } from "react" 7 | import { useRefreshStore } from "@/lib/store" 8 | 9 | interface StatsCardProps { 10 | title: string 11 | value: string 12 | description?: string 13 | icon: React.ReactNode 14 | } 15 | 16 | function StatsCard({ title, value, description, icon }: StatsCardProps) { 17 | return ( 18 | 19 | 20 | {title} 21 | {icon} 22 | 23 | 24 |
{value}
25 | {description && ( 26 |

{description}

27 | )} 28 |
29 |
30 | ) 31 | } 32 | 33 | interface OverviewStatsProps { 34 | data: any; 35 | onDataUpdate?: (data: any) => void; 36 | } 37 | 38 | export function OverviewStats({ data: initialData, onDataUpdate }: OverviewStatsProps) { 39 | const [data, setData] = useState(initialData); 40 | const [nodeStats, setNodeStats] = useState(null); 41 | const { interval } = useRefreshStore(); 42 | 43 | useEffect(() => { 44 | let mounted = true; 45 | 46 | const refreshData = async () => { 47 | try { 48 | // First get overview to get node name 49 | const newData = await rabbitMQFetch('/overview'); 50 | console.log('[OverviewStats] Overview data:', { 51 | node: newData.node, 52 | object_totals: newData.object_totals, 53 | queue_totals: newData.queue_totals 54 | }); 55 | 56 | // Then get node stats using the node name from overview 57 | const newNodeStats = await getNodeStats(newData.node); 58 | console.log('[OverviewStats] Node Stats:', { 59 | raw: newNodeStats, 60 | memUsed: newNodeStats?.mem_used, 61 | uptime: newNodeStats?.uptime, 62 | name: newNodeStats?.name 63 | }); 64 | 65 | if (mounted) { 66 | setData(newData); 67 | setNodeStats(newNodeStats); 68 | onDataUpdate?.(newData); 69 | } 70 | } catch (error) { 71 | console.error('[OverviewStats] Error refreshing data:', error); 72 | } 73 | }; 74 | 75 | refreshData(); 76 | 77 | let intervalId: NodeJS.Timeout | undefined; 78 | 79 | if (interval > 0) { 80 | intervalId = setInterval(refreshData, interval * 1000); 81 | } 82 | 83 | return () => { 84 | mounted = false; 85 | if (intervalId) { 86 | clearInterval(intervalId); 87 | } 88 | }; 89 | }, [interval]); 90 | 91 | const messageRate = data.message_stats?.publish_details?.rate ?? 0; 92 | const deliveryRate = data.message_stats?.deliver_get_details?.rate ?? 0; 93 | const totalMessages = data.queue_totals?.messages ?? 0; 94 | const messagesReady = data.queue_totals?.messages_ready ?? 0; 95 | const messagesUnacked = data.queue_totals?.messages_unacknowledged ?? 0; 96 | const totalQueues = data.object_totals?.queues ?? 0; 97 | const connections = data.object_totals?.connections ?? 0; 98 | const memoryUsed = nodeStats?.mem_used ?? 0; 99 | const uptime = nodeStats?.uptime ?? 0; 100 | 101 | console.log('[OverviewStats] Current Memory Stats:', { 102 | nodeStats, 103 | memoryUsed, 104 | formatted: formatBytes(memoryUsed) 105 | }); 106 | 107 | return ( 108 |
109 | } 114 | /> 115 | } 120 | /> 121 | } 126 | /> 127 | } 132 | /> 133 |
134 | ) 135 | } 136 | -------------------------------------------------------------------------------- /components/dashboard/queue-distribution-chart.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts" 4 | import { Inbox } from "lucide-react" 5 | 6 | interface QueueDistributionChartProps { 7 | data: { 8 | name: string 9 | value: number 10 | }[] 11 | } 12 | 13 | const COLORS = [ 14 | "#3b82f6", 15 | "#34d399", 16 | "#f472b6", 17 | "#a78bfa", 18 | "#fbbf24", 19 | "#fb7185", 20 | "#60a5fa", 21 | ] 22 | 23 | export function QueueDistributionChart({ data }: QueueDistributionChartProps) { 24 | if (!data || data.length === 0 || data.every(item => item.value === 0)) { 25 | return ( 26 |
27 |
28 | 29 |

No messages in queues

30 |

Queue distribution chart will appear when messages are present

31 |
32 |
33 | ) 34 | } 35 | 36 | return ( 37 | 38 | 39 | 48 | {data.map((entry, index) => ( 49 | 55 | ))} 56 | 57 | { 59 | if (active && payload && payload.length) { 60 | const data = payload[0].payload 61 | return ( 62 |
63 |
64 | 65 | {data.name} 66 | 67 | 68 | {data.value.toLocaleString()} messages 69 | 70 |
71 |
72 | ) 73 | } 74 | return null 75 | }} 76 | /> 77 |
78 |
79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /components/dashboard/queued-messages-chart.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | Line, 5 | LineChart, 6 | CartesianGrid, 7 | ResponsiveContainer, 8 | Tooltip, 9 | XAxis, 10 | YAxis, 11 | Legend, 12 | } from "recharts" 13 | import { format } from 'date-fns' 14 | import { useState, useEffect } from 'react' 15 | 16 | interface QueuedMessagesChartProps { 17 | data: { 18 | timestamp: number 19 | messages: number 20 | messagesReady: number 21 | messagesUnacked: number 22 | }[] 23 | } 24 | 25 | export const QueuedMessagesChart = ({ data }: QueuedMessagesChartProps) => { 26 | const [currentTime, setCurrentTime] = useState(Date.now()); 27 | 28 | useEffect(() => { 29 | const interval = setInterval(() => { 30 | setCurrentTime(Date.now()); 31 | }, 1000); 32 | return () => clearInterval(interval); 33 | }, []); 34 | 35 | const timeDomain = { 36 | start: currentTime - 60 * 1000, 37 | end: currentTime, 38 | }; 39 | 40 | const ticks = Array.from({ length: 7 }, (_, i) => timeDomain.start + (i * 10000)); 41 | 42 | return ( 43 |
44 |
45 | 46 | 49 | 50 | format(new Date(timestamp), 'HH:mm:ss')} 56 | scale="time" 57 | className="text-muted-foreground" 58 | stroke="currentColor" 59 | fontSize={11} 60 | dy={10} 61 | /> 62 | value.toLocaleString()} 64 | className="text-muted-foreground" 65 | stroke="currentColor" 66 | fontSize={11} 67 | width={40} 68 | /> 69 | { 71 | if (active && payload && payload.length) { 72 | return ( 73 |
74 |
75 |
76 | {new Date(payload[0].payload.timestamp).toLocaleTimeString()} 77 |
78 |
79 |
80 | Total 81 | {payload[0].value} 82 |
83 |
84 | Ready 85 | {payload[1].value} 86 |
87 |
88 | Unacked 89 | {payload[2].value} 90 |
91 |
92 |
93 |
94 | ) 95 | } 96 | return null 97 | }} 98 | /> 99 | ( 109 | {value} 110 | )} 111 | /> 112 | 123 | 134 | 145 |
146 |
147 |
148 |
149 | ) 150 | } 151 | -------------------------------------------------------------------------------- /components/exchanges/binding-viewer.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogDescription, 7 | DialogHeader, 8 | DialogTitle, 9 | } from "@/components/ui/dialog" 10 | import { 11 | Table, 12 | TableBody, 13 | TableCell, 14 | TableHead, 15 | TableHeader, 16 | TableRow, 17 | } from "@/components/ui/table" 18 | 19 | interface Binding { 20 | source: string 21 | destination: string 22 | destination_type: string 23 | routing_key: string 24 | arguments: Record 25 | } 26 | 27 | interface BindingViewerProps { 28 | bindings: Binding[] 29 | open: boolean 30 | onOpenChange: (open: boolean) => void 31 | exchangeName: string 32 | } 33 | 34 | export function BindingViewer({ 35 | bindings, 36 | open, 37 | onOpenChange, 38 | exchangeName, 39 | }: BindingViewerProps) { 40 | return ( 41 | 42 | 43 | 44 | Exchange Bindings 45 | 46 | Viewing bindings for exchange "{exchangeName}" 47 | 48 | 49 |
50 | 51 | 52 | 53 | Destination 54 | Type 55 | Routing Key 56 | Arguments 57 | 58 | 59 | 60 | {bindings.length === 0 ? ( 61 | 62 | 66 | No bindings found for this exchange 67 | 68 | 69 | ) : ( 70 | bindings.map((binding, index) => ( 71 | 72 | 73 | {binding.destination} 74 | 75 | 76 | {binding.destination_type} 77 | 78 | 79 | {binding.routing_key || ( 80 | None 81 | )} 82 | 83 | 84 | {Object.keys(binding.arguments).length > 0 ? ( 85 |
 86 |                           {JSON.stringify(binding.arguments, null, 2)}
 87 |                         
88 | ) : ( 89 | None 90 | )} 91 |
92 |
93 | )) 94 | )} 95 |
96 |
97 |
98 |
99 |
100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /components/exchanges/exchange-list-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | TableBody, 4 | TableCell, 5 | TableHead, 6 | TableHeader, 7 | TableRow, 8 | } from "@/components/ui/table" 9 | import { Skeleton } from "@/components/ui/skeleton" 10 | 11 | export function ExchangeListSkeleton() { 12 | return ( 13 |
14 |
15 |
16 | 17 | 18 |
19 |
20 |
21 | 22 | 23 | 24 | Name 25 | Type 26 | Features 27 | Message Rate 28 | Actions 29 | 30 | 31 | 32 | {Array.from({ length: 5 }).map((_, i) => ( 33 | 34 | 35 |
36 | 37 |
38 |
39 | 40 |
41 | 42 |
43 |
44 | 45 |
46 | 47 | 48 |
49 |
50 | 51 |
52 | 53 | 54 |
55 |
56 | 57 |
58 | 59 |
60 |
61 |
62 | ))} 63 |
64 |
65 |
66 |
67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /components/exchanges/exchange-list.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | import { ExchangeOperations } from "@/components/exchanges/exchange-operations" 5 | import { Button } from "@/components/ui/button" 6 | import { Badge } from "@/components/ui/badge" 7 | import { 8 | Table, 9 | TableBody, 10 | TableCell, 11 | TableHead, 12 | TableHeader, 13 | TableRow, 14 | } from "@/components/ui/table" 15 | import { formatRate, ExchangeStats } from "@/lib/utils" 16 | import { ArrowUpDown } from "lucide-react" 17 | 18 | interface Exchange extends ExchangeStats { 19 | name: string 20 | vhost: string 21 | type: string 22 | durable: boolean 23 | auto_delete: boolean 24 | internal: boolean 25 | arguments: Record 26 | } 27 | 28 | interface ExchangeListProps { 29 | exchanges: Exchange[] 30 | } 31 | 32 | type SortField = keyof Exchange 33 | type SortOrder = "asc" | "desc" 34 | 35 | export function ExchangeList({ exchanges: initialExchanges }: ExchangeListProps) { 36 | const [exchanges, setExchanges] = useState(initialExchanges) 37 | const [sortField, setSortField] = useState("name") 38 | const [sortOrder, setSortOrder] = useState("asc") 39 | 40 | const sortExchanges = (field: SortField) => { 41 | if (sortField === field) { 42 | setSortOrder(sortOrder === "asc" ? "desc" : "asc") 43 | } else { 44 | setSortField(field) 45 | setSortOrder("asc") 46 | } 47 | 48 | const sortedExchanges = [...exchanges].sort((a, b) => { 49 | const aValue = a[field] 50 | const bValue = b[field] 51 | 52 | if (typeof aValue === "string" && typeof bValue === "string") { 53 | return sortOrder === "asc" 54 | ? aValue.localeCompare(bValue) 55 | : bValue.localeCompare(aValue) 56 | } 57 | 58 | if (typeof aValue === "number" && typeof bValue === "number") { 59 | return sortOrder === "asc" ? aValue - bValue : bValue - aValue 60 | } 61 | 62 | return 0 63 | }) 64 | 65 | setExchanges(sortedExchanges) 66 | } 67 | 68 | const SortButton = ({ field, children }: { field: SortField; children: React.ReactNode }) => ( 69 | 78 | ) 79 | 80 | return ( 81 |
82 | 83 | 84 | 85 | 86 | Name 87 | 88 | 89 | Type 90 | 91 | Features 92 | Message Rate 93 | Actions 94 | 95 | 96 | 97 | {exchanges.map((exchange) => ( 98 | 99 | 100 | {exchange.name === "" ? ( 101 | 102 | (Default Exchange) 103 | 104 | ) : ( 105 | exchange.name 106 | )} 107 | 108 | {exchange.type} 109 | 110 |
111 | {exchange.durable && ( 112 | 113 | Durable 114 | 115 | )} 116 | {exchange.auto_delete && ( 117 | 118 | Auto-delete 119 | 120 | )} 121 | {exchange.internal && ( 122 | 123 | Internal 124 | 125 | )} 126 |
127 |
128 | 129 | {exchange.message_stats ? ( 130 |
131 |
132 | In: {formatRate(exchange.message_stats.publish_in_details?.rate || 0)}/s 133 |
134 |
135 | Out: {formatRate(exchange.message_stats.publish_out_details?.rate || 0)}/s 136 |
137 |
138 | ) : ( 139 | No messages 140 | )} 141 |
142 | 143 |
144 | 145 |
146 |
147 |
148 | ))} 149 |
150 |
151 |
152 | ) 153 | } 154 | -------------------------------------------------------------------------------- /components/exchanges/exchange-operations.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | import { BindingViewer } from "@/components/exchanges/binding-viewer" 5 | import { Button } from "@/components/ui/button" 6 | import { 7 | DropdownMenu, 8 | DropdownMenuContent, 9 | DropdownMenuItem, 10 | DropdownMenuTrigger, 11 | } from "@/components/ui/dropdown-menu" 12 | import { getBindings } from "@/lib/utils" 13 | import { Link2, MoreHorizontal } from "lucide-react" 14 | 15 | interface ExchangeOperationsProps { 16 | exchange: { 17 | name: string 18 | vhost: string 19 | } 20 | } 21 | 22 | export function ExchangeOperations({ exchange }: ExchangeOperationsProps) { 23 | const [bindingViewerOpen, setBindingViewerOpen] = useState(false) 24 | const [bindings, setBindings] = useState([]) 25 | 26 | const onViewBindings = async () => { 27 | try { 28 | const fetchedBindings = await getBindings(exchange.vhost, exchange.name) 29 | setBindings(fetchedBindings) 30 | setBindingViewerOpen(true) 31 | } catch (error) { 32 | console.error("Failed to fetch bindings:", error) 33 | } 34 | } 35 | 36 | return ( 37 | <> 38 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | View Bindings (Temporarily Disabled) 49 | 50 | 51 | 52 | 53 | 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /components/layout/header.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | import { useRouter } from "next/navigation" 5 | import { ModeToggle } from "@/components/mode-toggle" 6 | import { Button } from "@/components/ui/button" 7 | import { useAuth } from "@/lib/auth" 8 | import { MainNav } from "./main-nav" 9 | import { RefreshToggle } from "@/components/refresh-toggle" 10 | 11 | export function Header() { 12 | const router = useRouter() 13 | const { logout } = useAuth() 14 | 15 | const handleLogout = () => { 16 | logout() 17 | router.push("/login") 18 | router.refresh() 19 | } 20 | 21 | return ( 22 |
23 |
24 | 25 |
26 | 27 | 28 | 31 |
32 |
33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /components/layout/main-nav.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | import { usePathname } from "next/navigation" 5 | 6 | import { cn } from "@/lib/utils" 7 | import { Button } from "@/components/ui/button" 8 | 9 | export function MainNav() { 10 | const pathname = usePathname() 11 | 12 | const routes = [ 13 | { 14 | href: "/", 15 | label: "Overview", 16 | active: pathname === "/", 17 | }, 18 | { 19 | href: "/queues", 20 | label: "Queues", 21 | active: pathname === "/queues", 22 | }, 23 | { 24 | href: "/exchanges", 25 | label: "Exchanges", 26 | active: pathname === "/exchanges", 27 | }, 28 | { 29 | href: "/connections", 30 | label: "Connections", 31 | active: pathname === "/connections", 32 | }, 33 | { 34 | href: "/channels", 35 | label: "Channels", 36 | active: pathname === "/channels", 37 | }, 38 | ] 39 | 40 | return ( 41 | 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /components/layout/page-header.tsx: -------------------------------------------------------------------------------- 1 | interface PageHeaderProps { 2 | heading: string 3 | description?: string 4 | } 5 | 6 | export function PageHeader({ heading, description }: PageHeaderProps) { 7 | return ( 8 |
9 |

{heading}

10 | {description && ( 11 |

{description}

12 | )} 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /components/mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | import { Moon, Sun } from "lucide-react" 2 | import { useTheme } from "next-themes" 3 | 4 | import { Button } from "@/components/ui/button" 5 | import { 6 | DropdownMenu, 7 | DropdownMenuContent, 8 | DropdownMenuItem, 9 | DropdownMenuTrigger, 10 | } from "@/components/ui/dropdown-menu" 11 | 12 | export function ModeToggle() { 13 | const { setTheme } = useTheme() 14 | 15 | return ( 16 | 17 | 18 | 23 | 24 | 25 | setTheme("light")}> 26 | Light 27 | 28 | setTheme("dark")}> 29 | Dark 30 | 31 | setTheme("system")}> 32 | System 33 | 34 | 35 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /components/queues/queue-list-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | TableBody, 4 | TableCell, 5 | TableHead, 6 | TableHeader, 7 | TableRow, 8 | } from "@/components/ui/table" 9 | import { Skeleton } from "@/components/ui/skeleton" 10 | 11 | export function QueueListSkeleton() { 12 | return ( 13 |
14 |
15 |
16 | 17 | 18 |
19 |
20 |
21 | 22 | 23 | 24 | Name 25 | Messages 26 | Ready 27 | Unacked 28 | Consumers 29 | State 30 | Actions 31 | 32 | 33 | 34 | {Array.from({ length: 5 }).map((_, i) => ( 35 | 36 | 37 | 38 | 39 | 40 |
41 | 42 | 43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 | 54 | 55 |
56 |
57 | 58 |
59 | 60 | 61 |
62 |
63 | 64 |
65 | 66 |
67 |
68 |
69 | ))} 70 |
71 |
72 |
73 |
74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /components/queues/queue-list.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useEffect, useMemo } from "react" 4 | import { QueueOperations } from "@/components/queues/queue-operations" 5 | import { Button } from "@/components/ui/button" 6 | import { 7 | Table, 8 | TableBody, 9 | TableCell, 10 | TableHead, 11 | TableHeader, 12 | TableRow, 13 | } from "@/components/ui/table" 14 | import { formatBytes, formatRate, QueueStats, API_ENDPOINTS, RabbitMQError, RABBITMQ_CONFIG, rabbitMQFetch } from "@/lib/utils" 15 | import { cn } from "@/lib/utils" 16 | import { ArrowUpDown } from "lucide-react" 17 | import { useRealtimeUpdates } from "@/hooks/use-realtime-updates" 18 | import { useToast } from "@/hooks/use-toast" 19 | import { useRefreshStore } from "@/lib/store" 20 | import { Skeleton } from "@/components/ui/skeleton" 21 | 22 | interface Queue extends QueueStats { 23 | name: string 24 | vhost: string 25 | state: string 26 | consumers: number 27 | consumer_utilisation: number 28 | policy: string 29 | exclusive: boolean 30 | auto_delete: boolean 31 | durable: boolean 32 | } 33 | 34 | interface QueueListProps { 35 | queues?: Queue[] 36 | } 37 | 38 | type SortField = keyof Queue 39 | type SortOrder = "asc" | "desc" 40 | 41 | export function QueueList({ queues: initialQueues }: QueueListProps) { 42 | const [isInitialLoading, setIsInitialLoading] = useState(!initialQueues) 43 | const [queues, setQueues] = useState(initialQueues || []) 44 | const [error, setError] = useState(null) 45 | const [sortField, setSortField] = useState("name") 46 | const [sortOrder, setSortOrder] = useState("asc") 47 | const { toast } = useToast() 48 | const { interval } = useRefreshStore() 49 | 50 | const fetchQueues = async (isInitialFetch = false) => { 51 | try { 52 | // Skip initial fetch since we get data from props 53 | if (isInitialFetch && initialQueues) { 54 | return; 55 | } 56 | 57 | if (isInitialFetch) { 58 | setIsInitialLoading(true); 59 | } 60 | 61 | const response = await fetch('/api/queues', { 62 | headers: { 63 | 'Content-Type': 'application/json', 64 | }, 65 | }); 66 | 67 | if (!response.ok) { 68 | throw new Error(`HTTP error! status: ${response.status}`); 69 | } 70 | 71 | const data = await response.json(); 72 | 73 | // Let's validate the data structure 74 | const validQueues = Array.isArray(data) ? data.map(queue => ({ 75 | name: queue.name || '', 76 | vhost: queue.vhost || '/', 77 | state: queue.state || 'unknown', 78 | consumers: queue.consumers || 0, 79 | consumer_utilisation: queue.consumer_utilisation || 0, 80 | policy: queue.policy || '', 81 | exclusive: queue.exclusive || false, 82 | auto_delete: queue.auto_delete || false, 83 | durable: queue.durable || false, 84 | messages: queue.messages || 0, 85 | messages_ready: queue.messages_ready || 0, 86 | messages_unacknowledged: queue.messages_unacknowledged || 0, 87 | message_stats: queue.message_stats || {} 88 | })) : []; 89 | 90 | setQueues(validQueues); 91 | setError(null); 92 | } catch (err) { 93 | console.error("[QueueList] Error fetching queues:", err) 94 | setError(err as RabbitMQError) 95 | toast({ 96 | variant: "destructive", 97 | title: "Error fetching queues", 98 | description: (err as RabbitMQError).message || "Failed to connect to RabbitMQ server", 99 | }) 100 | } finally { 101 | if (isInitialFetch) { 102 | setIsInitialLoading(false) 103 | } 104 | console.log('[QueueList] Loading state finished') 105 | } 106 | } 107 | 108 | useEffect(() => { 109 | let mounted = true 110 | let intervalId: NodeJS.Timeout | undefined 111 | 112 | const refreshData = async () => { 113 | if (!mounted) return 114 | await fetchQueues(false) // Regular interval refresh 115 | } 116 | 117 | if (!initialQueues) { 118 | console.log('[QueueList] No initial queues provided, fetching queues...') 119 | fetchQueues(true) // Initial fetch 120 | } 121 | 122 | if (interval > 0) { 123 | console.log(`[QueueList] Setting up refresh interval: ${interval}s`) 124 | intervalId = setInterval(refreshData, interval * 1000) 125 | } 126 | 127 | return () => { 128 | mounted = false 129 | if (intervalId) { 130 | clearInterval(intervalId) 131 | } 132 | } 133 | }, [initialQueues, interval]) 134 | 135 | const realtimeQueues = useRealtimeUpdates('queue', queues, (current, update) => { 136 | return current.map(queue => 137 | queue.name === update.name ? { ...queue, ...update } : queue 138 | ) 139 | }) 140 | 141 | // Memoize the sorting function itself to avoid recreating it on every render 142 | const sortQueuesFn = useMemo(() => (queues: Queue[]) => { 143 | return [...queues].sort((a, b) => { 144 | const aValue = a[sortField] 145 | const bValue = b[sortField] 146 | 147 | if (typeof aValue === "string" && typeof bValue === "string") { 148 | return sortOrder === "asc" 149 | ? aValue.localeCompare(bValue) 150 | : bValue.localeCompare(aValue) 151 | } 152 | 153 | if (typeof aValue === "number" && typeof bValue === "number") { 154 | return sortOrder === "asc" ? aValue - bValue : bValue - aValue 155 | } 156 | 157 | return 0 158 | }) 159 | }, [sortField, sortOrder]) 160 | 161 | // Apply the memoized sort function to the queues 162 | const sortedQueues = useMemo(() => { 163 | const queueList = realtimeQueues.length > 0 ? realtimeQueues : queues; 164 | return sortQueuesFn(queueList); 165 | }, [queues, realtimeQueues, sortQueuesFn]); 166 | 167 | // Display the sorted queues directly 168 | const displayQueues = sortedQueues; 169 | 170 | const sortQueues = (field: SortField) => { 171 | if (sortField === field) { 172 | setSortOrder(sortOrder === "asc" ? "desc" : "asc") 173 | } else { 174 | setSortField(field) 175 | setSortOrder("asc") 176 | } 177 | } 178 | 179 | if (error) { 180 | return ( 181 |
182 |

Error Loading Queues

183 |

{error.message}

184 |
185 | ) 186 | } 187 | 188 | if (!queues.length) { 189 | return ( 190 |
191 |

No Queues Found

192 |

There are no queues in this virtual host.

193 |
194 | ) 195 | } 196 | 197 | return ( 198 |
199 |
200 | 201 | 202 | 203 | 204 | 212 | 213 | 214 | 222 | 223 | 224 | 232 | 233 | 234 | 242 | 243 | 244 | 252 | 253 | 254 | 262 | 263 | Actions 264 | 265 | 266 | 267 | {isInitialLoading ? ( 268 | Array.from({ length: 5 }).map((_, i) => ( 269 | 270 | 271 | 272 | 273 | 274 |
275 | 276 | 277 |
278 |
279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 |
287 | 288 | 289 |
290 |
291 | 292 |
293 | 294 | 295 |
296 |
297 | 298 |
299 | 300 |
301 |
302 |
303 | )) 304 | ) : ( 305 | displayQueues.map((queue) => ( 306 | 307 | 308 | {queue.name} 309 | 310 | 311 |
312 | {queue.messages.toLocaleString()} 313 | {queue.message_stats?.publish_details && ( 314 | 315 | {formatRate(queue.message_stats.publish_details.rate)}/s 316 | 317 | )} 318 |
319 |
320 | 321 | {queue.messages_ready.toLocaleString()} 322 | 323 | 324 | {queue.messages_unacknowledged.toLocaleString()} 325 | 326 | 327 |
328 | {queue.consumers} 329 | {queue.consumer_utilisation !== undefined && ( 330 | 331 | {(queue.consumer_utilisation * 100).toFixed(1)}% util 332 | 333 | )} 334 |
335 |
336 | 337 |
338 | 346 | {queue.state} 347 |
348 |
349 | 350 |
351 | 352 |
353 |
354 |
355 | )) 356 | )} 357 |
358 |
359 |
360 |
361 | ) 362 | } 363 | -------------------------------------------------------------------------------- /components/queues/queue-operations.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | import { MessageViewer } from "@/components/queues/message-viewer" 5 | import { Button } from "@/components/ui/button" 6 | import { 7 | Dialog, 8 | DialogContent, 9 | DialogDescription, 10 | DialogFooter, 11 | DialogHeader, 12 | DialogTitle, 13 | } from "@/components/ui/dialog" 14 | import { 15 | DropdownMenu, 16 | DropdownMenuContent, 17 | DropdownMenuItem, 18 | DropdownMenuSeparator, 19 | DropdownMenuTrigger, 20 | } from "@/components/ui/dropdown-menu" 21 | import { getMessages, purgeQueue } from "@/lib/utils" 22 | import { MoreHorizontal, Trash } from "lucide-react" 23 | import { useRouter } from "next/navigation" 24 | 25 | interface QueueOperationsProps { 26 | queue: { 27 | name: string 28 | vhost: string 29 | messages_ready: number 30 | messages_unacknowledged: number 31 | } 32 | } 33 | 34 | export function QueueOperations({ queue }: QueueOperationsProps) { 35 | const router = useRouter() 36 | const [purgeDialogOpen, setPurgeDialogOpen] = useState(false) 37 | const [messageViewerOpen, setMessageViewerOpen] = useState(false) 38 | const [messages, setMessages] = useState([]) 39 | const [isLoading, setIsLoading] = useState(false) 40 | 41 | const onPurge = async () => { 42 | try { 43 | setIsLoading(true) 44 | await purgeQueue(queue.vhost, queue.name) 45 | router.refresh() 46 | } catch (error) { 47 | console.error("Failed to purge queue:", error) 48 | } finally { 49 | setIsLoading(false) 50 | setPurgeDialogOpen(false) 51 | } 52 | } 53 | 54 | const onViewMessages = async () => { 55 | try { 56 | setIsLoading(true) 57 | const fetchedMessages = await getMessages(queue.vhost, queue.name, 50, 'ack_requeue_true') 58 | setMessages(fetchedMessages) 59 | setMessageViewerOpen(true) 60 | } catch (error) { 61 | console.error("Failed to fetch messages:", error) 62 | } finally { 63 | setIsLoading(false) 64 | } 65 | } 66 | 67 | return ( 68 | <> 69 | 70 | 71 | 75 | 76 | 77 | 78 | View Messages 79 | 80 | 81 | setPurgeDialogOpen(true)} 84 | > 85 | 86 | Purge Queue 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | Are you sure? 95 | 96 | This will remove all messages from the queue "{queue.name}". This 97 | action cannot be undone. 98 | 99 | 100 | 101 | 104 | 111 | 112 | 113 | 114 | 115 | 124 | 125 | ) 126 | } 127 | -------------------------------------------------------------------------------- /components/refresh-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Button } from "@/components/ui/button" 4 | import { 5 | DropdownMenu, 6 | DropdownMenuContent, 7 | DropdownMenuItem, 8 | DropdownMenuTrigger, 9 | } from "@/components/ui/dropdown-menu" 10 | import { useRefreshStore } from "@/lib/store" 11 | import { RefreshCw } from "lucide-react" 12 | 13 | const refreshOptions = [ 14 | { value: 0, label: "Off" }, 15 | { value: 5, label: "5s" }, 16 | { value: 10, label: "10s" }, 17 | { value: 20, label: "20s" }, 18 | { value: 60, label: "60s" }, 19 | ] 20 | 21 | export function RefreshToggle() { 22 | const { interval, setInterval } = useRefreshStore() 23 | 24 | return ( 25 | 26 | 27 | 36 | 37 | 38 | {refreshOptions.map((option) => ( 39 | setInterval(option.value)} 42 | > 43 | 44 | {option.label} 45 | 46 | 47 | ))} 48 | 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ThemeProvider as NextThemesProvider } from "next-themes" 5 | import { type ThemeProviderProps } from "next-themes/dist/types" 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children} 9 | } 10 | -------------------------------------------------------------------------------- /components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLDivElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |
53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |
61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogTrigger, 116 | DialogClose, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 5 | import { Check, ChevronRight, Circle } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )) 40 | DropdownMenuSubTrigger.displayName = 41 | DropdownMenuPrimitive.SubTrigger.displayName 42 | 43 | const DropdownMenuSubContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )) 56 | DropdownMenuSubContent.displayName = 57 | DropdownMenuPrimitive.SubContent.displayName 58 | 59 | const DropdownMenuContent = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, sideOffset = 4, ...props }, ref) => ( 63 | 64 | 74 | 75 | )) 76 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 77 | 78 | const DropdownMenuItem = React.forwardRef< 79 | React.ElementRef, 80 | React.ComponentPropsWithoutRef & { 81 | inset?: boolean 82 | } 83 | >(({ className, inset, ...props }, ref) => ( 84 | svg]:size-4 [&>svg]:shrink-0", 88 | inset && "pl-8", 89 | className 90 | )} 91 | {...props} 92 | /> 93 | )) 94 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 95 | 96 | const DropdownMenuCheckboxItem = React.forwardRef< 97 | React.ElementRef, 98 | React.ComponentPropsWithoutRef 99 | >(({ className, children, checked, ...props }, ref) => ( 100 | 109 | 110 | 111 | 112 | 113 | 114 | {children} 115 | 116 | )) 117 | DropdownMenuCheckboxItem.displayName = 118 | DropdownMenuPrimitive.CheckboxItem.displayName 119 | 120 | const DropdownMenuRadioItem = React.forwardRef< 121 | React.ElementRef, 122 | React.ComponentPropsWithoutRef 123 | >(({ className, children, ...props }, ref) => ( 124 | 132 | 133 | 134 | 135 | 136 | 137 | {children} 138 | 139 | )) 140 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 141 | 142 | const DropdownMenuLabel = React.forwardRef< 143 | React.ElementRef, 144 | React.ComponentPropsWithoutRef & { 145 | inset?: boolean 146 | } 147 | >(({ className, inset, ...props }, ref) => ( 148 | 157 | )) 158 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 159 | 160 | const DropdownMenuSeparator = React.forwardRef< 161 | React.ElementRef, 162 | React.ComponentPropsWithoutRef 163 | >(({ className, ...props }, ref) => ( 164 | 169 | )) 170 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 171 | 172 | const DropdownMenuShortcut = ({ 173 | className, 174 | ...props 175 | }: React.HTMLAttributes) => { 176 | return ( 177 | 181 | ) 182 | } 183 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 184 | 185 | export { 186 | DropdownMenu, 187 | DropdownMenuTrigger, 188 | DropdownMenuContent, 189 | DropdownMenuItem, 190 | DropdownMenuCheckboxItem, 191 | DropdownMenuRadioItem, 192 | DropdownMenuLabel, 193 | DropdownMenuSeparator, 194 | DropdownMenuShortcut, 195 | DropdownMenuGroup, 196 | DropdownMenuPortal, 197 | DropdownMenuSub, 198 | DropdownMenuSubContent, 199 | DropdownMenuSubTrigger, 200 | DropdownMenuRadioGroup, 201 | } 202 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SelectPrimitive from "@radix-ui/react-select" 5 | import { Check, ChevronDown, ChevronUp } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Select = SelectPrimitive.Root 10 | 11 | const SelectGroup = SelectPrimitive.Group 12 | 13 | const SelectValue = SelectPrimitive.Value 14 | 15 | const SelectTrigger = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, children, ...props }, ref) => ( 19 | span]:line-clamp-1", 23 | className 24 | )} 25 | {...props} 26 | > 27 | {children} 28 | 29 | 30 | 31 | 32 | )) 33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 34 | 35 | const SelectScrollUpButton = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | 48 | 49 | )) 50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName 51 | 52 | const SelectScrollDownButton = React.forwardRef< 53 | React.ElementRef, 54 | React.ComponentPropsWithoutRef 55 | >(({ className, ...props }, ref) => ( 56 | 64 | 65 | 66 | )) 67 | SelectScrollDownButton.displayName = 68 | SelectPrimitive.ScrollDownButton.displayName 69 | 70 | const SelectContent = React.forwardRef< 71 | React.ElementRef, 72 | React.ComponentPropsWithoutRef 73 | >(({ className, children, position = "popper", ...props }, ref) => ( 74 | 75 | 86 | 87 | 94 | {children} 95 | 96 | 97 | 98 | 99 | )) 100 | SelectContent.displayName = SelectPrimitive.Content.displayName 101 | 102 | const SelectLabel = React.forwardRef< 103 | React.ElementRef, 104 | React.ComponentPropsWithoutRef 105 | >(({ className, ...props }, ref) => ( 106 | 111 | )) 112 | SelectLabel.displayName = SelectPrimitive.Label.displayName 113 | 114 | const SelectItem = React.forwardRef< 115 | React.ElementRef, 116 | React.ComponentPropsWithoutRef 117 | >(({ className, children, ...props }, ref) => ( 118 | 126 | 127 | 128 | 129 | 130 | 131 | {children} 132 | 133 | )) 134 | SelectItem.displayName = SelectPrimitive.Item.displayName 135 | 136 | const SelectSeparator = React.forwardRef< 137 | React.ElementRef, 138 | React.ComponentPropsWithoutRef 139 | >(({ className, ...props }, ref) => ( 140 | 145 | )) 146 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 147 | 148 | export { 149 | Select, 150 | SelectGroup, 151 | SelectValue, 152 | SelectTrigger, 153 | SelectContent, 154 | SelectLabel, 155 | SelectItem, 156 | SelectSeparator, 157 | SelectScrollUpButton, 158 | SelectScrollDownButton, 159 | } 160 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )) 17 | Table.displayName = "Table" 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )) 25 | TableHeader.displayName = "TableHeader" 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )) 37 | TableBody.displayName = "TableBody" 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes 42 | >(({ className, ...props }, ref) => ( 43 | tr]:last:border-b-0", 47 | className 48 | )} 49 | {...props} 50 | /> 51 | )) 52 | TableFooter.displayName = "TableFooter" 53 | 54 | const TableRow = React.forwardRef< 55 | HTMLTableRowElement, 56 | React.HTMLAttributes 57 | >(({ className, ...props }, ref) => ( 58 | 66 | )) 67 | TableRow.displayName = "TableRow" 68 | 69 | const TableHead = React.forwardRef< 70 | HTMLTableCellElement, 71 | React.ThHTMLAttributes 72 | >(({ className, ...props }, ref) => ( 73 |
[role=checkbox]]:translate-y-[2px] whitespace-nowrap", 77 | className 78 | )} 79 | {...props} 80 | /> 81 | )) 82 | TableHead.displayName = "TableHead" 83 | 84 | const TableCell = React.forwardRef< 85 | HTMLTableCellElement, 86 | React.TdHTMLAttributes 87 | >(({ className, ...props }, ref) => ( 88 | [role=checkbox]]:translate-y-[2px] whitespace-nowrap", 92 | className 93 | )} 94 | {...props} 95 | /> 96 | )) 97 | TableCell.displayName = "TableCell" 98 | 99 | const TableCaption = React.forwardRef< 100 | HTMLTableCaptionElement, 101 | React.HTMLAttributes 102 | >(({ className, ...props }, ref) => ( 103 |
108 | )) 109 | TableCaption.displayName = "TableCaption" 110 | 111 | export { 112 | Table, 113 | TableHeader, 114 | TableBody, 115 | TableFooter, 116 | TableHead, 117 | TableRow, 118 | TableCell, 119 | TableCaption, 120 | } 121 | -------------------------------------------------------------------------------- /components/ui/toast.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ToastPrimitives from "@radix-ui/react-toast" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | import { X } from "lucide-react" 7 | 8 | import { cn } from "@/lib/utils" 9 | 10 | const ToastProvider = ToastPrimitives.Provider 11 | 12 | const ToastViewport = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, ...props }, ref) => ( 16 | 24 | )) 25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName 26 | 27 | const toastVariants = cva( 28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", 29 | { 30 | variants: { 31 | variant: { 32 | default: "border bg-background text-foreground", 33 | destructive: 34 | "destructive group border-destructive bg-destructive text-destructive-foreground", 35 | }, 36 | }, 37 | defaultVariants: { 38 | variant: "default", 39 | }, 40 | } 41 | ) 42 | 43 | const Toast = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef & 46 | VariantProps 47 | >(({ className, variant, ...props }, ref) => { 48 | return ( 49 | 54 | ) 55 | }) 56 | Toast.displayName = ToastPrimitives.Root.displayName 57 | 58 | const ToastAction = React.forwardRef< 59 | React.ElementRef, 60 | React.ComponentPropsWithoutRef 61 | >(({ className, ...props }, ref) => ( 62 | 70 | )) 71 | ToastAction.displayName = ToastPrimitives.Action.displayName 72 | 73 | const ToastClose = React.forwardRef< 74 | React.ElementRef, 75 | React.ComponentPropsWithoutRef 76 | >(({ className, ...props }, ref) => ( 77 | 86 | 87 | 88 | )) 89 | ToastClose.displayName = ToastPrimitives.Close.displayName 90 | 91 | const ToastTitle = React.forwardRef< 92 | React.ElementRef, 93 | React.ComponentPropsWithoutRef 94 | >(({ className, ...props }, ref) => ( 95 | 100 | )) 101 | ToastTitle.displayName = ToastPrimitives.Title.displayName 102 | 103 | const ToastDescription = React.forwardRef< 104 | React.ElementRef, 105 | React.ComponentPropsWithoutRef 106 | >(({ className, ...props }, ref) => ( 107 | 112 | )) 113 | ToastDescription.displayName = ToastPrimitives.Description.displayName 114 | 115 | type ToastProps = React.ComponentPropsWithoutRef 116 | 117 | type ToastActionElement = React.ReactElement 118 | 119 | export { 120 | type ToastProps, 121 | type ToastActionElement, 122 | ToastProvider, 123 | ToastViewport, 124 | Toast, 125 | ToastTitle, 126 | ToastDescription, 127 | ToastClose, 128 | ToastAction, 129 | } 130 | -------------------------------------------------------------------------------- /components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useToast } from "@/hooks/use-toast" 4 | import { 5 | Toast, 6 | ToastClose, 7 | ToastDescription, 8 | ToastProvider, 9 | ToastTitle, 10 | ToastViewport, 11 | } from "@/components/ui/toast" 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast() 15 | 16 | return ( 17 | 18 | {toasts.map(function ({ id, title, description, action, ...props }) { 19 | return ( 20 | 21 |
22 | {title && {title}} 23 | {description && ( 24 | {description} 25 | )} 26 |
27 | {action} 28 | 29 |
30 | ) 31 | })} 32 | 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rabbitscout: 3 | build: . 4 | ports: 5 | - "3000:3000" 6 | environment: 7 | - NEXT_PUBLIC_RABBITMQ_HOST 8 | - NEXT_PUBLIC_RABBITMQ_PORT 9 | - NEXT_PUBLIC_RABBITMQ_VHOST 10 | - RABBITMQ_USERNAME 11 | - RABBITMQ_PASSWORD 12 | - NEXT_PUBLIC_API_URL 13 | -------------------------------------------------------------------------------- /docs/assets/dark-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ralve-org/RabbitScout/aaf2b312215616a0a41b1f15ff8734be80cad7c7/docs/assets/dark-dashboard.png -------------------------------------------------------------------------------- /docs/assets/light-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ralve-org/RabbitScout/aaf2b312215616a0a41b1f15ff8734be80cad7c7/docs/assets/light-dashboard.png -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ralve-org/RabbitScout/aaf2b312215616a0a41b1f15ff8734be80cad7c7/docs/assets/logo.png -------------------------------------------------------------------------------- /hooks/use-realtime-updates.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | 3 | export type UpdateType = "queue" | "exchange" | "connection" | "channel" 4 | 5 | export function useRealtimeUpdates( 6 | type: UpdateType, 7 | initialData: T[], 8 | updateHandler?: (current: T[], update: any) => T[] 9 | ): T[] { 10 | const [data, setData] = useState(initialData) 11 | 12 | // Only update data if initialData reference changes 13 | useEffect(() => { 14 | setData(initialData) 15 | }, [initialData]) 16 | 17 | // WebSocket functionality temporarily disabled 18 | /*useEffect(() => { 19 | const socket = new WebSocket(`ws://${RABBITMQ_CONFIG.host}:15674/ws`) 20 | 21 | socket.onopen = () => { 22 | console.log(`[Realtime] WebSocket connected for ${type}`); 23 | socket.send(JSON.stringify({ command: 'subscribe', type })) 24 | } 25 | 26 | socket.onmessage = (event) => { 27 | const update = JSON.parse(event.data) 28 | console.log(`[Realtime] Received ${type} update:`, update); 29 | setData(current => updateHandler!(current, update)) 30 | } 31 | 32 | socket.onerror = (error) => { 33 | console.error(`[Realtime] WebSocket error for ${type}:`, error) 34 | } 35 | 36 | socket.onclose = () => { 37 | console.log(`[Realtime] WebSocket closed for ${type}`) 38 | } 39 | 40 | return () => { 41 | socket.close() 42 | } 43 | }, [type, updateHandler])*/ 44 | 45 | return data 46 | } 47 | 48 | // Example usage: 49 | // const queues = useRealtimeUpdates('queue', initialQueues, (current, update) => { 50 | // return current.map(queue => 51 | // queue.name === update.name ? { ...queue, ...update } : queue 52 | // ) 53 | // }) 54 | -------------------------------------------------------------------------------- /hooks/use-toast.ts: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | // Inspired by react-hot-toast library 4 | import * as React from "react" 5 | 6 | import type { 7 | ToastActionElement, 8 | ToastProps, 9 | } from "@/components/ui/toast" 10 | 11 | const TOAST_LIMIT = 1 12 | const TOAST_REMOVE_DELAY = 1000000 13 | 14 | type ToasterToast = ToastProps & { 15 | id: string 16 | title?: React.ReactNode 17 | description?: React.ReactNode 18 | action?: ToastActionElement 19 | } 20 | 21 | const actionTypes = { 22 | ADD_TOAST: "ADD_TOAST", 23 | UPDATE_TOAST: "UPDATE_TOAST", 24 | DISMISS_TOAST: "DISMISS_TOAST", 25 | REMOVE_TOAST: "REMOVE_TOAST", 26 | } as const 27 | 28 | let count = 0 29 | 30 | function genId() { 31 | count = (count + 1) % Number.MAX_SAFE_INTEGER 32 | return count.toString() 33 | } 34 | 35 | type ActionType = typeof actionTypes 36 | 37 | type Action = 38 | | { 39 | type: ActionType["ADD_TOAST"] 40 | toast: ToasterToast 41 | } 42 | | { 43 | type: ActionType["UPDATE_TOAST"] 44 | toast: Partial 45 | } 46 | | { 47 | type: ActionType["DISMISS_TOAST"] 48 | toastId?: ToasterToast["id"] 49 | } 50 | | { 51 | type: ActionType["REMOVE_TOAST"] 52 | toastId?: ToasterToast["id"] 53 | } 54 | 55 | interface State { 56 | toasts: ToasterToast[] 57 | } 58 | 59 | const toastTimeouts = new Map>() 60 | 61 | const addToRemoveQueue = (toastId: string) => { 62 | if (toastTimeouts.has(toastId)) { 63 | return 64 | } 65 | 66 | const timeout = setTimeout(() => { 67 | toastTimeouts.delete(toastId) 68 | dispatch({ 69 | type: "REMOVE_TOAST", 70 | toastId: toastId, 71 | }) 72 | }, TOAST_REMOVE_DELAY) 73 | 74 | toastTimeouts.set(toastId, timeout) 75 | } 76 | 77 | export const reducer = (state: State, action: Action): State => { 78 | switch (action.type) { 79 | case "ADD_TOAST": 80 | return { 81 | ...state, 82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), 83 | } 84 | 85 | case "UPDATE_TOAST": 86 | return { 87 | ...state, 88 | toasts: state.toasts.map((t) => 89 | t.id === action.toast.id ? { ...t, ...action.toast } : t 90 | ), 91 | } 92 | 93 | case "DISMISS_TOAST": { 94 | const { toastId } = action 95 | 96 | // ! Side effects ! - This could be extracted into a dismissToast() action, 97 | // but I'll keep it here for simplicity 98 | if (toastId) { 99 | addToRemoveQueue(toastId) 100 | } else { 101 | state.toasts.forEach((toast) => { 102 | addToRemoveQueue(toast.id) 103 | }) 104 | } 105 | 106 | return { 107 | ...state, 108 | toasts: state.toasts.map((t) => 109 | t.id === toastId || toastId === undefined 110 | ? { 111 | ...t, 112 | open: false, 113 | } 114 | : t 115 | ), 116 | } 117 | } 118 | case "REMOVE_TOAST": 119 | if (action.toastId === undefined) { 120 | return { 121 | ...state, 122 | toasts: [], 123 | } 124 | } 125 | return { 126 | ...state, 127 | toasts: state.toasts.filter((t) => t.id !== action.toastId), 128 | } 129 | } 130 | } 131 | 132 | const listeners: Array<(state: State) => void> = [] 133 | 134 | let memoryState: State = { toasts: [] } 135 | 136 | function dispatch(action: Action) { 137 | memoryState = reducer(memoryState, action) 138 | listeners.forEach((listener) => { 139 | listener(memoryState) 140 | }) 141 | } 142 | 143 | type Toast = Omit 144 | 145 | function toast({ ...props }: Toast) { 146 | const id = genId() 147 | 148 | const update = (props: ToasterToast) => 149 | dispatch({ 150 | type: "UPDATE_TOAST", 151 | toast: { ...props, id }, 152 | }) 153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) 154 | 155 | dispatch({ 156 | type: "ADD_TOAST", 157 | toast: { 158 | ...props, 159 | id, 160 | open: true, 161 | onOpenChange: (open) => { 162 | if (!open) dismiss() 163 | }, 164 | }, 165 | }) 166 | 167 | return { 168 | id: id, 169 | dismiss, 170 | update, 171 | } 172 | } 173 | 174 | function useToast() { 175 | const [state, setState] = React.useState(memoryState) 176 | 177 | React.useEffect(() => { 178 | listeners.push(setState) 179 | return () => { 180 | const index = listeners.indexOf(setState) 181 | if (index > -1) { 182 | listeners.splice(index, 1) 183 | } 184 | } 185 | }, [state]) 186 | 187 | return { 188 | ...state, 189 | toast, 190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), 191 | } 192 | } 193 | 194 | export { useToast, toast } 195 | -------------------------------------------------------------------------------- /lib/api-utils.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | 3 | // Common cache prevention headers 4 | export const NO_CACHE_HEADERS = { 5 | 'Cache-Control': 'no-cache, no-store, must-revalidate', 6 | 'Pragma': 'no-cache', 7 | 'Expires': '0' 8 | } 9 | 10 | // Common fetch options to prevent caching 11 | export const NO_CACHE_FETCH_OPTIONS = { 12 | cache: 'no-store' as RequestCache 13 | } 14 | 15 | // Helper function to create a response with no-cache headers 16 | export function createApiResponse(data: any, options: { status?: number } = {}) { 17 | return NextResponse.json(data, { 18 | status: options.status || 200, 19 | headers: NO_CACHE_HEADERS 20 | }) 21 | } 22 | 23 | // Helper function to create an error response with no-cache headers 24 | export function createApiErrorResponse(error: string, status: number = 500) { 25 | return NextResponse.json( 26 | { error }, 27 | { 28 | status, 29 | headers: NO_CACHE_HEADERS 30 | } 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /lib/auth.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { persist, createJSONStorage } from 'zustand/middleware'; 3 | import Cookies from 'js-cookie'; 4 | 5 | interface User { 6 | username: string; 7 | isAdmin: boolean; 8 | tags: string[]; 9 | } 10 | 11 | interface AuthState { 12 | authenticated: boolean; 13 | user: User | null; 14 | login: (authData: { authenticated: boolean; user: User }) => void; 15 | logout: () => void; 16 | } 17 | 18 | const isBrowser = typeof window !== 'undefined'; 19 | 20 | // Safe storage that works in both client and server environments 21 | const safeStorage = { 22 | getItem: (name: string): string | null => { 23 | if (!isBrowser) return null; 24 | try { 25 | return localStorage.getItem(name); 26 | } catch (error) { 27 | console.error('Error getting storage item:', error); 28 | return null; 29 | } 30 | }, 31 | setItem: (name: string, value: string) => { 32 | if (!isBrowser) return; 33 | try { 34 | localStorage.setItem(name, value); 35 | // Also set in cookies for SSR 36 | Cookies.set('auth-storage', value, { path: '/' }); 37 | } catch (error) { 38 | console.error('Error setting storage item:', error); 39 | } 40 | }, 41 | removeItem: (name: string) => { 42 | if (!isBrowser) return; 43 | try { 44 | localStorage.removeItem(name); 45 | // Also remove from cookies 46 | Cookies.remove('auth-storage', { path: '/' }); 47 | } catch (error) { 48 | console.error('Error removing storage item:', error); 49 | } 50 | } 51 | }; 52 | 53 | export const useAuth = create()( 54 | persist( 55 | (set) => ({ 56 | authenticated: false, 57 | user: null, 58 | login: (authData) => set({ authenticated: authData.authenticated, user: authData.user }), 59 | logout: () => set({ authenticated: false, user: null }), 60 | }), 61 | { 62 | name: 'auth-storage', 63 | storage: createJSONStorage(() => safeStorage), 64 | skipHydration: true, // Important for SSR 65 | } 66 | ) 67 | ); 68 | -------------------------------------------------------------------------------- /lib/config.ts: -------------------------------------------------------------------------------- 1 | import { useAuth } from './auth' 2 | 3 | export const RABBITMQ_CONFIG = { 4 | host: process.env.NEXT_PUBLIC_RABBITMQ_HOST, 5 | port: process.env.NEXT_PUBLIC_RABBITMQ_PORT, 6 | vhost: process.env.NEXT_PUBLIC_RABBITMQ_VHOST , 7 | username: process.env.RABBITMQ_USERNAME, 8 | password: process.env.RABBITMQ_PASSWORD, 9 | } 10 | 11 | export const API_TIMEOUT_MS = process.env.NEXT_PUBLIC_RABBITMQ_API_TIMEOUT_MS 12 | ? parseInt(process.env.NEXT_PUBLIC_RABBITMQ_API_TIMEOUT_MS, 10) 13 | : 60000; // Default to 60 seconds (60000 ms) 14 | 15 | export const getRabbitMQConfig = () => { 16 | const auth = useAuth.getState() 17 | 18 | return { 19 | host: RABBITMQ_CONFIG.host, 20 | port: RABBITMQ_CONFIG.port, 21 | vhost: RABBITMQ_CONFIG.vhost, 22 | username: auth.user?.username || RABBITMQ_CONFIG.username, 23 | password: RABBITMQ_CONFIG.password, 24 | } 25 | } 26 | 27 | // Helper to get base URL 28 | export const getRabbitMQBaseUrl = () => { 29 | return `http://${RABBITMQ_CONFIG.host}:${RABBITMQ_CONFIG.port}` 30 | } 31 | 32 | // Helper to get auth headers using current auth state 33 | // This function is now simplified for server-side use only, for direct calls 34 | // from Server Components to RabbitMQ. Client-side calls use the /api proxy. 35 | export const getRabbitMQAuthHeaders = () => { 36 | // Always use the server-side configured credentials from RABBITMQ_CONFIG in this file 37 | const credentials = Buffer.from(`${RABBITMQ_CONFIG.username}:${RABBITMQ_CONFIG.password}`).toString('base64'); 38 | return { 39 | 'Authorization': `Basic ${credentials}`, 40 | 'Content-Type': 'application/json', 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /lib/hooks/use-graph-data.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | interface GraphData { 4 | messageRateData: Array<{ 5 | timestamp: number; 6 | publishRate: number; 7 | deliveryRate: number; 8 | }>; 9 | queuedMessagesData: Array<{ 10 | timestamp: number; 11 | messages: number; 12 | messagesReady: number; 13 | messagesUnacked: number; 14 | }>; 15 | } 16 | 17 | export function useGraphData(): GraphData { 18 | const [messageRateData, setMessageRateData] = useState([]); 19 | const [queuedMessagesData, setQueuedMessagesData] = useState([]); 20 | 21 | useEffect(() => { 22 | const updateGraphData = async () => { 23 | try { 24 | const response = await fetch('/api/overview'); 25 | const data = await response.json(); 26 | 27 | const timestamp = Date.now(); 28 | const oneMinuteAgo = timestamp - 60 * 1000; 29 | 30 | // Update message rate data 31 | setMessageRateData(prev => { 32 | const newPoint = { 33 | timestamp, 34 | publishRate: data.message_stats?.publish_details?.rate || 0, 35 | deliveryRate: data.message_stats?.deliver_get_details?.rate || 0, 36 | }; 37 | const filtered = prev.filter(point => point.timestamp > oneMinuteAgo); 38 | return [...filtered, newPoint]; 39 | }); 40 | 41 | // Update queued messages data 42 | setQueuedMessagesData(prev => { 43 | const newPoint = { 44 | timestamp, 45 | messages: data.queue_totals?.messages || 0, 46 | messagesReady: data.queue_totals?.messages_ready || 0, 47 | messagesUnacked: data.queue_totals?.messages_unacknowledged || 0, 48 | }; 49 | const filtered = prev.filter(point => point.timestamp > oneMinuteAgo); 50 | return [...filtered, newPoint]; 51 | }); 52 | } catch (error) { 53 | console.error('Error fetching graph data:', error); 54 | } 55 | }; 56 | 57 | // Initial update 58 | updateGraphData(); 59 | 60 | // Set up 5-second interval 61 | const interval = setInterval(updateGraphData, 5000); 62 | 63 | return () => clearInterval(interval); 64 | }, []); 65 | 66 | return { 67 | messageRateData, 68 | queuedMessagesData, 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /lib/store.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { persist } from 'zustand/middleware' 3 | 4 | interface RefreshState { 5 | interval: number 6 | setInterval: (interval: number) => void 7 | } 8 | 9 | export const useRefreshStore = create()( 10 | persist( 11 | (set) => ({ 12 | interval: 5, // Default 5 seconds 13 | setInterval: (interval) => set({ interval }), 14 | }), 15 | { 16 | name: 'refresh-settings', 17 | } 18 | ) 19 | ) 20 | -------------------------------------------------------------------------------- /lib/websocket.ts: -------------------------------------------------------------------------------- 1 | import { RABBITMQ_CONFIG } from "@/lib/utils" 2 | 3 | type MessageHandler = (data: any) => void 4 | 5 | class RabbitMQWebSocket { 6 | private ws: WebSocket | null = null 7 | private reconnectAttempts = 0 8 | private maxReconnectAttempts = 5 9 | private reconnectDelay = 1000 10 | private handlers: Set = new Set() 11 | 12 | constructor() { 13 | if (typeof window !== "undefined") { 14 | this.connect() 15 | } 16 | } 17 | 18 | private connect() { 19 | const protocol = window.location.protocol === "https:" ? "wss:" : "ws:" 20 | const credentials = btoa( 21 | `${RABBITMQ_CONFIG.username}:${RABBITMQ_CONFIG.password}` 22 | ) 23 | 24 | this.ws = new WebSocket( 25 | `${protocol}//${RABBITMQ_CONFIG.host}:${RABBITMQ_CONFIG.port}/ws` 26 | ) 27 | 28 | this.ws.onopen = () => { 29 | console.log("WebSocket connected") 30 | this.reconnectAttempts = 0 31 | this.ws?.send( 32 | JSON.stringify({ 33 | type: "auth", 34 | credentials, 35 | }) 36 | ) 37 | } 38 | 39 | this.ws.onmessage = (event) => { 40 | try { 41 | const data = JSON.parse(event.data) 42 | this.handlers.forEach((handler) => handler(data)) 43 | } catch (error) { 44 | console.error("Failed to parse WebSocket message:", error) 45 | } 46 | } 47 | 48 | this.ws.onclose = () => { 49 | console.log("WebSocket disconnected") 50 | if (this.reconnectAttempts < this.maxReconnectAttempts) { 51 | setTimeout(() => { 52 | this.reconnectAttempts++ 53 | this.connect() 54 | }, this.reconnectDelay * Math.pow(2, this.reconnectAttempts)) 55 | } 56 | } 57 | 58 | this.ws.onerror = (error) => { 59 | console.error("WebSocket error:", error) 60 | } 61 | } 62 | 63 | subscribe(handler: MessageHandler) { 64 | this.handlers.add(handler) 65 | return () => this.handlers.delete(handler) 66 | } 67 | 68 | send(data: any) { 69 | if (this.ws?.readyState === WebSocket.OPEN) { 70 | this.ws.send(JSON.stringify(data)) 71 | } 72 | } 73 | } 74 | 75 | export const rabbitMQWebSocket = new RabbitMQWebSocket() 76 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | import type { NextRequest } from 'next/server' 3 | 4 | export function middleware(request: NextRequest) { 5 | const authCookie = request.cookies.get('auth-storage') 6 | 7 | let isAuthenticated = false 8 | let userData = null 9 | 10 | try { 11 | if (authCookie?.value) { 12 | let cookieValue = authCookie.value 13 | try { 14 | cookieValue = decodeURIComponent(cookieValue) 15 | } catch (e) { 16 | // If decoding fails, use the raw value 17 | } 18 | 19 | const parsed = JSON.parse(cookieValue) 20 | isAuthenticated = parsed.state?.authenticated || false 21 | userData = parsed.state?.user 22 | } 23 | } catch (error) { 24 | console.error('Error parsing auth cookie:', error) 25 | } 26 | 27 | // Only log auth state for non-API routes to reduce noise 28 | if (!request.nextUrl.pathname.startsWith('/api')) { 29 | console.log('Auth state:', { isAuthenticated, path: request.nextUrl.pathname }) 30 | } 31 | 32 | // Skip auth check for public API routes 33 | if (request.nextUrl.pathname.startsWith('/api/auth')) { 34 | return NextResponse.next() 35 | } 36 | 37 | // Protect all routes except login 38 | if (!isAuthenticated && !request.nextUrl.pathname.startsWith('/login')) { 39 | const loginUrl = new URL('/login', request.url) 40 | console.log('Redirecting to:', loginUrl.toString()) 41 | return NextResponse.redirect(loginUrl) 42 | } 43 | 44 | // Redirect authenticated users away from login page 45 | if (isAuthenticated && request.nextUrl.pathname.startsWith('/login')) { 46 | const homeUrl = new URL('/', request.url) 47 | console.log('Redirecting to:', homeUrl.toString()) 48 | return NextResponse.redirect(homeUrl) 49 | } 50 | 51 | return NextResponse.next() 52 | } 53 | 54 | export const config = { 55 | matcher: [ 56 | '/((?!_next/static|_next/image|favicon.ico).*)', 57 | ], 58 | } 59 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | eslint: { 4 | ignoreDuringBuilds: true, 5 | }, 6 | typescript: { 7 | ignoreBuildErrors: true, 8 | }, 9 | output: 'standalone', 10 | images: { 11 | unoptimized: true, 12 | }, 13 | }; 14 | 15 | module.exports = nextConfig; 16 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rabbitscout", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-dialog": "^1.1.2", 13 | "@radix-ui/react-dropdown-menu": "^2.1.2", 14 | "@radix-ui/react-label": "^2.1.0", 15 | "@radix-ui/react-select": "^2.1.2", 16 | "@radix-ui/react-slot": "^1.1.0", 17 | "@radix-ui/react-toast": "^1.2.2", 18 | "@types/js-cookie": "^3.0.6", 19 | "class-variance-authority": "^0.7.1", 20 | "clsx": "^2.1.1", 21 | "date-fns": "^4.1.0", 22 | "js-cookie": "^3.0.5", 23 | "lucide-react": "^0.462.0", 24 | "next": "14.2.16", 25 | "next-themes": "^0.4.3", 26 | "react": "^18", 27 | "react-dom": "^18", 28 | "recharts": "^2.13.3", 29 | "tailwind-merge": "^2.5.5", 30 | "tailwindcss-animate": "^1.0.7", 31 | "zustand": "^5.0.1" 32 | }, 33 | "devDependencies": { 34 | "@types/node": "^20", 35 | "@types/react": "^18", 36 | "@types/react-dom": "^18", 37 | "eslint": "^8", 38 | "eslint-config-next": "14.2.16", 39 | "postcss": "^8", 40 | "tailwindcss": "^3.4.1", 41 | "typescript": "^5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ralve-org/RabbitScout/aaf2b312215616a0a41b1f15ff8734be80cad7c7/public/images/logo.png -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | background: 'hsl(var(--background))', 14 | foreground: 'hsl(var(--foreground))', 15 | card: { 16 | DEFAULT: 'hsl(var(--card))', 17 | foreground: 'hsl(var(--card-foreground))' 18 | }, 19 | popover: { 20 | DEFAULT: 'hsl(var(--popover))', 21 | foreground: 'hsl(var(--popover-foreground))' 22 | }, 23 | primary: { 24 | DEFAULT: 'hsl(var(--primary))', 25 | foreground: 'hsl(var(--primary-foreground))' 26 | }, 27 | secondary: { 28 | DEFAULT: 'hsl(var(--secondary))', 29 | foreground: 'hsl(var(--secondary-foreground))' 30 | }, 31 | muted: { 32 | DEFAULT: 'hsl(var(--muted))', 33 | foreground: 'hsl(var(--muted-foreground))' 34 | }, 35 | accent: { 36 | DEFAULT: 'hsl(var(--accent))', 37 | foreground: 'hsl(var(--accent-foreground))' 38 | }, 39 | destructive: { 40 | DEFAULT: 'hsl(var(--destructive))', 41 | foreground: 'hsl(var(--destructive-foreground))' 42 | }, 43 | border: 'hsl(var(--border))', 44 | input: 'hsl(var(--input))', 45 | ring: 'hsl(var(--ring))', 46 | chart: { 47 | '1': 'hsl(var(--chart-1))', 48 | '2': 'hsl(var(--chart-2))', 49 | '3': 'hsl(var(--chart-3))', 50 | '4': 'hsl(var(--chart-4))', 51 | '5': 'hsl(var(--chart-5))' 52 | } 53 | }, 54 | borderRadius: { 55 | lg: 'var(--radius)', 56 | md: 'calc(var(--radius) - 2px)', 57 | sm: 'calc(var(--radius) - 4px)' 58 | } 59 | } 60 | }, 61 | plugins: [require("tailwindcss-animate")], 62 | }; 63 | export default config; 64 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------