queen-mq 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +1226 -0
- package/AUTH.md +2044 -0
- package/LICENSE.md +202 -0
- package/README.md +276 -53
- package/WEBAPP.md +1889 -0
- package/assets/dashboard-01.png +0 -0
- package/assets/queen-logo-blue.svg +210 -0
- package/assets/queen-logo-cyan.svg +210 -0
- package/assets/queen-logo-indigo.svg +210 -0
- package/assets/queen-logo-orange.svg +210 -0
- package/assets/queen-logo-pink.svg +210 -0
- package/assets/queen-logo-purple.svg +210 -0
- package/assets/queen-logo-rose.svg +239 -0
- package/assets/queen-logo.svg +263 -0
- package/examples/batch-processing.js +58 -0
- package/examples/test-dashboard-api.js +200 -0
- package/package.json +4 -2
- package/server.log +1 -0
- package/src/benchmark/consumer.js +207 -0
- package/src/benchmark/consumer_multi.js +216 -0
- package/src/benchmark/producer.js +75 -0
- package/src/benchmark/producer_multi.js +115 -0
- package/src/client/client.js +192 -16
- package/src/client/queenClient.js +5 -0
- package/src/cluster-server.js +242 -0
- package/src/config.js +19 -5
- package/src/database/connection.js +42 -16
- package/src/database/poolManager.js +7 -0
- package/src/database/schema-v2.sql +194 -130
- package/src/managers/queueManagerOptimized.js +823 -933
- package/src/managers/systemEventManager.js +8 -3
- package/src/routes/messages.js +127 -57
- package/src/routes/pop.js +27 -43
- package/src/routes/resources.js +61 -27
- package/src/routes/status.js +1037 -0
- package/src/server.js +308 -272
- package/src/services/evictionService.js +57 -28
- package/src/services/retentionService.js +44 -11
- package/src/test/advanced-pattern-tests.js +5 -5
- package/src/test/bus-mode-tests.js +24 -11
- package/src/test/edge-case-tests.js +12 -5
- package/src/test/enterprise-tests.js +48 -15
- package/src/test/utils.js +1 -1
- package/src/utils/streaming.js +231 -0
- package/src/utils/uuid.js +2 -2
- package/src/websocket/wsServer.js +10 -3
- package/test-keepalive-v2.sh +22 -0
- package/webapp/COLOR_GUIDE.md +118 -0
- package/webapp/README.md +143 -0
- package/webapp/index.html +14 -0
- package/webapp/package-lock.json +3184 -0
- package/webapp/package.json +25 -0
- package/webapp/postcss.config.js +7 -0
- package/webapp/public/assets/queen-logo-blue.svg +210 -0
- package/webapp/public/assets/queen-logo-cyan.svg +210 -0
- package/webapp/public/assets/queen-logo-indigo.svg +210 -0
- package/webapp/public/assets/queen-logo-orange.svg +210 -0
- package/webapp/public/assets/queen-logo-pink.svg +210 -0
- package/webapp/public/assets/queen-logo-purple.svg +210 -0
- package/webapp/public/assets/queen-logo-rose.svg +239 -0
- package/webapp/public/assets/queen-logo.svg +263 -0
- package/webapp/src/App.vue +19 -0
- package/webapp/src/api/analytics.js +10 -0
- package/webapp/src/api/client.js +29 -0
- package/webapp/src/api/consumers.js +52 -0
- package/webapp/src/api/health.js +7 -0
- package/webapp/src/api/messages.js +26 -0
- package/webapp/src/api/queues.js +14 -0
- package/webapp/src/api/resources.js +8 -0
- package/webapp/src/assets/styles/main.css +357 -0
- package/webapp/src/components/analytics/AnalyticsFilters.vue +87 -0
- package/webapp/src/components/analytics/AnalyticsMetrics.vue +57 -0
- package/webapp/src/components/analytics/MessageDistributionChart.vue +111 -0
- package/webapp/src/components/analytics/MessageFlowChart.vue +173 -0
- package/webapp/src/components/analytics/TimeRangeSelector.vue +27 -0
- package/webapp/src/components/analytics/TopQueuesChart.vue +132 -0
- package/webapp/src/components/common/ConfirmDialog.vue +56 -0
- package/webapp/src/components/common/LoadingSpinner.vue +6 -0
- package/webapp/src/components/common/MetricCard.vue +43 -0
- package/webapp/src/components/common/StatusBadge.vue +45 -0
- package/webapp/src/components/dashboard/MessageStatusCard.vue +50 -0
- package/webapp/src/components/dashboard/PerformanceCard.vue +38 -0
- package/webapp/src/components/dashboard/ThroughputChart.vue +182 -0
- package/webapp/src/components/dashboard/TopQueuesTable.vue +53 -0
- package/webapp/src/components/layout/AppLayout.vue +110 -0
- package/webapp/src/components/layout/AppSidebar.vue +304 -0
- package/webapp/src/components/messages/MessageDetailPanel.vue +242 -0
- package/webapp/src/components/messages/MessageFilters.vue +114 -0
- package/webapp/src/components/queue-detail/PartitionList.vue +79 -0
- package/webapp/src/components/queue-detail/PushMessageModal.vue +175 -0
- package/webapp/src/components/queue-detail/QueueConfig.vue +63 -0
- package/webapp/src/components/queue-detail/QueueDetailHeader.vue +53 -0
- package/webapp/src/components/queue-detail/RecentMessages.vue +76 -0
- package/webapp/src/components/queues/CreateQueueModal.vue +193 -0
- package/webapp/src/components/queues/QueueFilters.vue +90 -0
- package/webapp/src/composables/useApi.js +34 -0
- package/webapp/src/composables/useTheme.js +36 -0
- package/webapp/src/main.js +11 -0
- package/webapp/src/router/index.js +42 -0
- package/webapp/src/utils/colors.js +96 -0
- package/webapp/src/utils/formatters.js +49 -0
- package/webapp/src/views/Analytics.vue +377 -0
- package/webapp/src/views/ConsumerGroups.vue +433 -0
- package/webapp/src/views/Dashboard.vue +418 -0
- package/webapp/src/views/Messages.vue +361 -0
- package/webapp/src/views/QueueDetail.vue +582 -0
- package/webapp/src/views/Queues.vue +496 -0
- package/webapp/tailwind.config.js +25 -0
- package/webapp/vite.config.js +10 -0
- package/dashboard/.vscode/extensions.json +0 -3
- package/dashboard/README.md +0 -5
- package/dashboard/index.html +0 -14
- package/dashboard/package-lock.json +0 -1458
- package/dashboard/package.json +0 -25
- package/dashboard/public/vite.svg +0 -1
- package/dashboard/src/App.vue +0 -29
- package/dashboard/src/assets/styles/main.css +0 -908
- package/dashboard/src/assets/vue.svg +0 -1
- package/dashboard/src/components/cards/MetricCard.vue +0 -298
- package/dashboard/src/components/charts/QueueDepthChart.vue +0 -276
- package/dashboard/src/components/charts/QueueLagChart.vue +0 -436
- package/dashboard/src/components/charts/ThroughputChart.vue +0 -302
- package/dashboard/src/components/common/ActivityFeed.vue +0 -251
- package/dashboard/src/components/layout/AppHeader.vue +0 -208
- package/dashboard/src/components/layout/AppLayout.vue +0 -88
- package/dashboard/src/components/layout/AppSidebar.vue +0 -261
- package/dashboard/src/main.js +0 -44
- package/dashboard/src/router.js +0 -54
- package/dashboard/src/services/api.js +0 -187
- package/dashboard/src/services/websocket.js +0 -167
- package/dashboard/src/utils/constants.js +0 -56
- package/dashboard/src/utils/helpers.js +0 -118
- package/dashboard/src/views/Analytics.vue +0 -912
- package/dashboard/src/views/Dashboard.vue +0 -906
- package/dashboard/src/views/Messages.vue +0 -437
- package/dashboard/src/views/QueueDetail.vue +0 -501
- package/dashboard/src/views/Queues.vue +0 -333
- package/dashboard/vite.config.js +0 -30
- package/src/routes/analytics.js +0 -812
package/WEBAPP.md
ADDED
|
@@ -0,0 +1,1889 @@
|
|
|
1
|
+
# Queen Message Queue - Web Application Design
|
|
2
|
+
|
|
3
|
+
**Technology Stack:**
|
|
4
|
+
- Vue 3 (Composition API)
|
|
5
|
+
- JavaScript (ES6+)
|
|
6
|
+
- Tailwind CSS
|
|
7
|
+
- Chart.js with vue-chartjs
|
|
8
|
+
- Vue Router
|
|
9
|
+
- Axios for API calls
|
|
10
|
+
|
|
11
|
+
**Design Philosophy:**
|
|
12
|
+
- Clean, professional interface inspired by Kafka UI and modern analytics dashboards
|
|
13
|
+
- Real-time data from API endpoints (no mocked data)
|
|
14
|
+
- Responsive design with sidebar navigation
|
|
15
|
+
- Light and dark mode support
|
|
16
|
+
- Clear data visualization with meaningful charts
|
|
17
|
+
- Intuitive user interactions
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Table of Contents
|
|
22
|
+
|
|
23
|
+
1. [Application Structure](#application-structure)
|
|
24
|
+
2. [Design System](#design-system)
|
|
25
|
+
3. [Pages & Components](#pages--components)
|
|
26
|
+
4. [API Integration Map](#api-integration-map)
|
|
27
|
+
5. [File Structure](#file-structure)
|
|
28
|
+
6. [Implementation Plan](#implementation-plan)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Application Structure
|
|
33
|
+
|
|
34
|
+
### Core Layout
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
┌────────────────────────────────────────────────────┐
|
|
38
|
+
│ Header (Logo, Health Status, Theme Toggle) │
|
|
39
|
+
├──────────┬─────────────────────────────────────────┤
|
|
40
|
+
│ │ │
|
|
41
|
+
│ Sidebar │ Main Content Area │
|
|
42
|
+
│ │ │
|
|
43
|
+
│ - Dashboard │
|
|
44
|
+
│ - Queues │ [Dynamic Page Content] │
|
|
45
|
+
│ - Consumer │
|
|
46
|
+
│ Groups │ │
|
|
47
|
+
│ - Messages │
|
|
48
|
+
│ - Analytics │
|
|
49
|
+
│ │ │
|
|
50
|
+
│ │ │
|
|
51
|
+
└──────────┴─────────────────────────────────────────┘
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Pages Overview
|
|
55
|
+
|
|
56
|
+
1. **Dashboard** - System overview with key metrics
|
|
57
|
+
2. **Queues** - Queue list and management
|
|
58
|
+
3. **Queue Detail** - Individual queue deep-dive
|
|
59
|
+
4. **Consumer Groups** - Consumer group monitoring
|
|
60
|
+
5. **Messages** - Message browser and operations
|
|
61
|
+
6. **Analytics** - Time-series analysis and insights
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Design System
|
|
66
|
+
|
|
67
|
+
### Color Palette
|
|
68
|
+
|
|
69
|
+
**Light Mode:**
|
|
70
|
+
```css
|
|
71
|
+
--bg-primary: #ffffff
|
|
72
|
+
--bg-secondary: #f9fafb
|
|
73
|
+
--bg-tertiary: #f3f4f6
|
|
74
|
+
--text-primary: #111827
|
|
75
|
+
--text-secondary: #6b7280
|
|
76
|
+
--text-tertiary: #9ca3af
|
|
77
|
+
--border-color: #e5e7eb
|
|
78
|
+
--accent-blue: #3b82f6
|
|
79
|
+
--accent-indigo: #6366f1
|
|
80
|
+
--accent-purple: #8b5cf6
|
|
81
|
+
--success: #10b981
|
|
82
|
+
--warning: #f59e0b
|
|
83
|
+
--danger: #ef4444
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Dark Mode:**
|
|
87
|
+
```css
|
|
88
|
+
--bg-primary: #0f172a
|
|
89
|
+
--bg-secondary: #1e293b
|
|
90
|
+
--bg-tertiary: #334155
|
|
91
|
+
--text-primary: #f1f5f9
|
|
92
|
+
--text-secondary: #cbd5e1
|
|
93
|
+
--text-tertiary: #94a3b8
|
|
94
|
+
--border-color: #334155
|
|
95
|
+
--accent-blue: #60a5fa
|
|
96
|
+
--accent-indigo: #818cf8
|
|
97
|
+
--accent-purple: #a78bfa
|
|
98
|
+
--success: #34d399
|
|
99
|
+
--warning: #fbbf24
|
|
100
|
+
--danger: #f87171
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Typography
|
|
104
|
+
|
|
105
|
+
```css
|
|
106
|
+
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif
|
|
107
|
+
--font-mono: 'JetBrains Mono', 'Courier New', monospace
|
|
108
|
+
|
|
109
|
+
--text-xs: 0.75rem (12px)
|
|
110
|
+
--text-sm: 0.875rem (14px)
|
|
111
|
+
--text-base: 1rem (16px)
|
|
112
|
+
--text-lg: 1.125rem (18px)
|
|
113
|
+
--text-xl: 1.25rem (20px)
|
|
114
|
+
--text-2xl: 1.5rem (24px)
|
|
115
|
+
--text-3xl: 1.875rem (30px)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Spacing System
|
|
119
|
+
|
|
120
|
+
Following Tailwind's 4px base unit:
|
|
121
|
+
- `xs`: 4px
|
|
122
|
+
- `sm`: 8px
|
|
123
|
+
- `md`: 16px
|
|
124
|
+
- `lg`: 24px
|
|
125
|
+
- `xl`: 32px
|
|
126
|
+
- `2xl`: 48px
|
|
127
|
+
|
|
128
|
+
### Component Patterns
|
|
129
|
+
|
|
130
|
+
**Cards:**
|
|
131
|
+
- White/dark background with subtle shadow
|
|
132
|
+
- Rounded corners (8px)
|
|
133
|
+
- Padding: 24px
|
|
134
|
+
- Border: 1px solid border-color
|
|
135
|
+
|
|
136
|
+
**Buttons:**
|
|
137
|
+
- Primary: Blue background, white text
|
|
138
|
+
- Secondary: Gray background, dark text
|
|
139
|
+
- Danger: Red background, white text
|
|
140
|
+
- Height: 40px, padding: 12px 24px
|
|
141
|
+
- Rounded: 6px
|
|
142
|
+
|
|
143
|
+
**Tables:**
|
|
144
|
+
- Alternating row colors
|
|
145
|
+
- Hover state on rows
|
|
146
|
+
- Sortable columns with indicators
|
|
147
|
+
- Sticky header
|
|
148
|
+
|
|
149
|
+
**Status Badges:**
|
|
150
|
+
- Rounded pill shape
|
|
151
|
+
- Color-coded: green (healthy/stable), yellow (warning), red (error)
|
|
152
|
+
- Small text with icon
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Pages & Components
|
|
157
|
+
|
|
158
|
+
### 1. Dashboard (`/`)
|
|
159
|
+
|
|
160
|
+
**Purpose:** System overview showing health, throughput, and key metrics at a glance.
|
|
161
|
+
|
|
162
|
+
**API Routes Used:**
|
|
163
|
+
- `GET /health` - System health status
|
|
164
|
+
- `GET /metrics` - Performance metrics
|
|
165
|
+
- `GET /api/v1/resources/overview` - System-wide statistics
|
|
166
|
+
- `GET /api/v1/status` - Dashboard status with throughput
|
|
167
|
+
|
|
168
|
+
**Layout:**
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
172
|
+
│ Queen Message Queue ● Healthy 🌙 │
|
|
173
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
174
|
+
│ │
|
|
175
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────┐│
|
|
176
|
+
│ │ QUEUES │ │ PARTITIONS │ │ MESSAGES │ │ UPTIME ││
|
|
177
|
+
│ │ 53 │ │ 512 │ │ 200,000 │ │ 2h 15m ││
|
|
178
|
+
│ └──────────────┘ └──────────────┘ └──────────────┘ └─────────┘│
|
|
179
|
+
│ │
|
|
180
|
+
│ ┌──────────────────────────────────────────────────────────────┐│
|
|
181
|
+
│ │ Message Throughput (Last Hour) ││
|
|
182
|
+
│ │ ││
|
|
183
|
+
│ │ Chart: Line chart showing ingested/processed rates ││
|
|
184
|
+
│ │ X-axis: Time, Y-axis: Messages/second ││
|
|
185
|
+
│ │ Two lines: Ingested (blue), Processed (green) ││
|
|
186
|
+
│ │ ││
|
|
187
|
+
│ └──────────────────────────────────────────────────────────────┘│
|
|
188
|
+
│ │
|
|
189
|
+
│ ┌────────────────────────────┐ ┌───────────────────────────────┐│
|
|
190
|
+
│ │ Message Status │ │ System Performance ││
|
|
191
|
+
│ │ │ │ ││
|
|
192
|
+
│ │ Pending: 2 │ │ Requests/sec: 0.00 ││
|
|
193
|
+
│ │ Processing: 0 │ │ Messages/sec: 0.00 ││
|
|
194
|
+
│ │ Completed: 16,493 │ │ DB Connections: 3/3 ││
|
|
195
|
+
│ │ Failed: 0 │ │ Memory (Heap): 7.7 MB ││
|
|
196
|
+
│ │ Dead Letter: 0 │ │ CPU Time: 227ms ││
|
|
197
|
+
│ │ │ │ ││
|
|
198
|
+
│ └────────────────────────────┘ └───────────────────────────────┘│
|
|
199
|
+
│ │
|
|
200
|
+
│ ┌──────────────────────────────────────────────────────────────┐│
|
|
201
|
+
│ │ Top Queues by Activity ││
|
|
202
|
+
│ │ ││
|
|
203
|
+
│ │ Queue Name Pending Processing Completed ││
|
|
204
|
+
│ │ ───────────────────────────────────────────────────────────││
|
|
205
|
+
│ │ benchmark-queue-50 0 0 2,000 ││
|
|
206
|
+
│ │ benchmark-queue-49 0 0 2,000 ││
|
|
207
|
+
│ │ test-queue 2 0 3 ││
|
|
208
|
+
│ │ ││
|
|
209
|
+
│ └──────────────────────────────────────────────────────────────┘│
|
|
210
|
+
│ │
|
|
211
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Components:**
|
|
215
|
+
|
|
216
|
+
1. **HealthIndicator.vue**
|
|
217
|
+
- Shows server health status from `/health`
|
|
218
|
+
- Color-coded dot: green (healthy), red (unhealthy)
|
|
219
|
+
- Displays uptime
|
|
220
|
+
|
|
221
|
+
2. **MetricCard.vue**
|
|
222
|
+
- Reusable card for displaying single metrics
|
|
223
|
+
- Props: title, value, icon, trend (optional)
|
|
224
|
+
- Used for queues, partitions, messages, uptime
|
|
225
|
+
|
|
226
|
+
3. **ThroughputChart.vue**
|
|
227
|
+
- Line chart using Chart.js
|
|
228
|
+
- Data from `/api/v1/status` throughput array
|
|
229
|
+
- Two datasets: ingested and processed messages
|
|
230
|
+
|
|
231
|
+
4. **MessageStatusCard.vue**
|
|
232
|
+
- Displays message counts by status
|
|
233
|
+
- Data from `/api/v1/resources/overview`
|
|
234
|
+
|
|
235
|
+
5. **PerformanceCard.vue**
|
|
236
|
+
- Shows system performance metrics
|
|
237
|
+
- Data from `/metrics` endpoint
|
|
238
|
+
|
|
239
|
+
6. **TopQueuesTable.vue**
|
|
240
|
+
- Simple table showing top 5 queues
|
|
241
|
+
- Data from `/api/v1/resources/queues`
|
|
242
|
+
- Sortable by activity
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
### 2. Queues Page (`/queues`)
|
|
247
|
+
|
|
248
|
+
**Purpose:** List all queues with filtering, sorting, and management capabilities.
|
|
249
|
+
|
|
250
|
+
**API Routes Used:**
|
|
251
|
+
- `GET /api/v1/resources/queues` - Get all queues
|
|
252
|
+
- `GET /api/v1/resources/namespaces` - Get namespaces for filtering
|
|
253
|
+
- `POST /api/v1/configure` - Create new queue (modal)
|
|
254
|
+
- `DELETE /api/v1/resources/queues/:queue` - Delete queue
|
|
255
|
+
|
|
256
|
+
**Layout:**
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
260
|
+
│ Queues [+ New Queue] │
|
|
261
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
262
|
+
│ │
|
|
263
|
+
│ 🔍 Search queues... [Namespace ▼] [Clear Filters] │
|
|
264
|
+
│ │
|
|
265
|
+
│ ┌──────────────────────────────────────────────────────────────┐│
|
|
266
|
+
│ │ Queue Name ▲ Namespace Partitions Pending Processing Total││
|
|
267
|
+
│ ├──────────────────────────────────────────────────────────────┤│
|
|
268
|
+
│ │ test-queue - 1 2 0 0 ││
|
|
269
|
+
│ │ benchmark-q-50 benchmark 10 0 0 2,000 ││
|
|
270
|
+
│ │ benchmark-q-49 benchmark 10 0 0 2,000 ││
|
|
271
|
+
│ │ benchmark-q-48 benchmark 10 0 0 2,000 ││
|
|
272
|
+
│ │ ... ││
|
|
273
|
+
│ └──────────────────────────────────────────────────────────────┘│
|
|
274
|
+
│ │
|
|
275
|
+
│ Showing 1-20 of 53 [< 1 2 3 4 >] │
|
|
276
|
+
│ │
|
|
277
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Components:**
|
|
281
|
+
|
|
282
|
+
1. **QueueList.vue**
|
|
283
|
+
- Main component for queues page
|
|
284
|
+
- Handles filtering, sorting, pagination
|
|
285
|
+
- Data from `/api/v1/resources/queues`
|
|
286
|
+
|
|
287
|
+
2. **QueueRow.vue**
|
|
288
|
+
- Single row in the queue table
|
|
289
|
+
- Click to navigate to queue detail
|
|
290
|
+
- Shows queue statistics
|
|
291
|
+
- Action menu (delete, clear)
|
|
292
|
+
|
|
293
|
+
3. **CreateQueueModal.vue**
|
|
294
|
+
- Modal form for creating new queue
|
|
295
|
+
- Calls `POST /api/v1/configure`
|
|
296
|
+
- Fields: name, namespace, task, ttl, priority, partition
|
|
297
|
+
|
|
298
|
+
4. **FilterBar.vue**
|
|
299
|
+
- Search input
|
|
300
|
+
- Namespace dropdown (from `/api/v1/resources/namespaces`)
|
|
301
|
+
- Clear filters button
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
### 3. Queue Detail Page (`/queues/:queueName`)
|
|
306
|
+
|
|
307
|
+
**Purpose:** Deep dive into a single queue with partition details, messages, and performance.
|
|
308
|
+
|
|
309
|
+
**API Routes Used:**
|
|
310
|
+
- `GET /api/v1/resources/queues/:queue` - Queue details
|
|
311
|
+
- `GET /api/v1/status/queues/:queue` - Queue status with time range
|
|
312
|
+
- `GET /api/v1/status/queues/:queue/messages` - Queue messages
|
|
313
|
+
- `DELETE /api/v1/queues/:queue/clear` - Clear queue
|
|
314
|
+
- `POST /api/v1/push` - Push message to queue
|
|
315
|
+
|
|
316
|
+
**Layout:**
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
320
|
+
│ ← Queues / test-queue [Clear Queue] │
|
|
321
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
322
|
+
│ │
|
|
323
|
+
│ test-queue Created: Oct 15, 2025 │
|
|
324
|
+
│ Namespace: - Task: - Priority: 0 │
|
|
325
|
+
│ │
|
|
326
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────┐│
|
|
327
|
+
│ │ PENDING │ │ PROCESSING │ │ COMPLETED │ │ FAILED ││
|
|
328
|
+
│ │ 0 │ │ 0 │ │ 3 │ │ 0 ││
|
|
329
|
+
│ └──────────────┘ └──────────────┘ └──────────────┘ └─────────┘│
|
|
330
|
+
│ │
|
|
331
|
+
│ ┌──────────────────────────────────────────────────────────────┐│
|
|
332
|
+
│ │ Queue Configuration ││
|
|
333
|
+
│ │ ││
|
|
334
|
+
│ │ Lease Time: 300s Max Queue Size: 10,000 ││
|
|
335
|
+
│ │ TTL: 3,600s Retry Limit: 3 ││
|
|
336
|
+
│ │ Retry Delay: 1,000ms DLQ Enabled: No ││
|
|
337
|
+
│ │ ││
|
|
338
|
+
│ └──────────────────────────────────────────────────────────────┘│
|
|
339
|
+
│ │
|
|
340
|
+
│ ┌──────────────────────────────────────────────────────────────┐│
|
|
341
|
+
│ │ Partitions [Push Message] ││
|
|
342
|
+
│ ├──────────────────────────────────────────────────────────────┤│
|
|
343
|
+
│ │ Default ││
|
|
344
|
+
│ │ Pending: 0 Processing: 0 Completed: 3 Failed: 0 ││
|
|
345
|
+
│ │ Last Activity: Oct 15, 06:22:21 ││
|
|
346
|
+
│ │ Consumed: 3 messages in 2 batches ││
|
|
347
|
+
│ └──────────────────────────────────────────────────────────────┘│
|
|
348
|
+
│ │
|
|
349
|
+
│ ┌──────────────────────────────────────────────────────────────┐│
|
|
350
|
+
│ │ Recent Messages ││
|
|
351
|
+
│ ├──────────────────────────────────────────────────────────────┤│
|
|
352
|
+
│ │ Transaction ID Created Status ││
|
|
353
|
+
│ │ ────────────────────────────────────────────────────────────││
|
|
354
|
+
│ │ 0199e688-1857... 06:21:42 Completed ││
|
|
355
|
+
│ │ 0199e688-4d29... 06:22:01 Completed ││
|
|
356
|
+
│ │ ││
|
|
357
|
+
│ └──────────────────────────────────────────────────────────────┘│
|
|
358
|
+
│ │
|
|
359
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Components:**
|
|
363
|
+
|
|
364
|
+
1. **QueueDetailHeader.vue**
|
|
365
|
+
- Queue name and metadata
|
|
366
|
+
- Back button
|
|
367
|
+
- Action buttons (clear, delete)
|
|
368
|
+
|
|
369
|
+
2. **QueueMetrics.vue**
|
|
370
|
+
- Four metric cards for message status
|
|
371
|
+
- Data from `/api/v1/status/queues/:queue`
|
|
372
|
+
|
|
373
|
+
3. **QueueConfig.vue**
|
|
374
|
+
- Display queue configuration
|
|
375
|
+
- Grid layout for config values
|
|
376
|
+
|
|
377
|
+
4. **PartitionList.vue**
|
|
378
|
+
- List of partitions with stats
|
|
379
|
+
- Data from queue detail response
|
|
380
|
+
|
|
381
|
+
5. **RecentMessages.vue**
|
|
382
|
+
- Table of recent messages
|
|
383
|
+
- Data from `/api/v1/status/queues/:queue/messages`
|
|
384
|
+
- Link to full message view
|
|
385
|
+
|
|
386
|
+
6. **PushMessageModal.vue**
|
|
387
|
+
- Form to push new message
|
|
388
|
+
- JSON editor for payload
|
|
389
|
+
- Calls `POST /api/v1/push`
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
### 4. Consumer Groups Page (`/consumer-groups`)
|
|
394
|
+
|
|
395
|
+
**Purpose:** Monitor consumer groups, lag, and consumption patterns.
|
|
396
|
+
|
|
397
|
+
**API Routes Used:**
|
|
398
|
+
- `GET /api/v1/resources/queues` - Get queues with consumer info
|
|
399
|
+
- `GET /api/v1/status/queues` - Get detailed consumer stats
|
|
400
|
+
|
|
401
|
+
**Layout:**
|
|
402
|
+
|
|
403
|
+
```
|
|
404
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
405
|
+
│ Consumer Groups │
|
|
406
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
407
|
+
│ │
|
|
408
|
+
│ 🔍 Search groups... [Status ▼] [Clear Filters] │
|
|
409
|
+
│ │
|
|
410
|
+
│ ┌──────────────────────────────────────────────────────────────┐│
|
|
411
|
+
│ │ Group Name ▲ Topics Offset Lag Time Lag State││
|
|
412
|
+
│ ├──────────────────────────────────────────────────────────────┤│
|
|
413
|
+
│ │ worker-group-1 test-queue 0 0ms ● Stable││
|
|
414
|
+
│ │ consumer-default benchmark-q-01 1,234 5m 23s ● Stable││
|
|
415
|
+
│ │ batch-processor batch-queue 45 12s ● Stable││
|
|
416
|
+
│ │ ... ││
|
|
417
|
+
│ └──────────────────────────────────────────────────────────────┘│
|
|
418
|
+
│ │
|
|
419
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**Components:**
|
|
423
|
+
|
|
424
|
+
1. **ConsumerGroupList.vue**
|
|
425
|
+
- Table of consumer groups
|
|
426
|
+
- Derived from queue data showing active consumers
|
|
427
|
+
- Filtering and sorting
|
|
428
|
+
|
|
429
|
+
2. **ConsumerGroupRow.vue**
|
|
430
|
+
- Single consumer group entry
|
|
431
|
+
- Status badge
|
|
432
|
+
- Lag indicators
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
### 5. Messages Page (`/messages`)
|
|
437
|
+
|
|
438
|
+
**Purpose:** Browse, search, and manage individual messages across all queues.
|
|
439
|
+
|
|
440
|
+
**API Routes Used:**
|
|
441
|
+
- `GET /api/v1/messages` - List messages with filters
|
|
442
|
+
- `GET /api/v1/messages/:transactionId` - Get message detail
|
|
443
|
+
- `DELETE /api/v1/messages/:transactionId` - Delete message
|
|
444
|
+
- `POST /api/v1/messages/:transactionId/retry` - Retry message
|
|
445
|
+
- `POST /api/v1/messages/:transactionId/dlq` - Move to DLQ
|
|
446
|
+
|
|
447
|
+
**Layout:**
|
|
448
|
+
|
|
449
|
+
```
|
|
450
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
451
|
+
│ Messages │
|
|
452
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
453
|
+
│ │
|
|
454
|
+
│ 🔍 Search by ID... [Queue ▼] [Status ▼] [Clear Filters] │
|
|
455
|
+
│ │
|
|
456
|
+
│ ┌──────────────────────────────────────────────────────────────┐│
|
|
457
|
+
│ │ Transaction ID Queue Created Status ││
|
|
458
|
+
│ ├──────────────────────────────────────────────────────────────┤│
|
|
459
|
+
│ │ 0199e688-1857... test-queue Oct 15 06:21 Completed ││
|
|
460
|
+
│ │ 0199e688-4d29... test-queue Oct 15 06:22 Completed ││
|
|
461
|
+
│ │ ... ││
|
|
462
|
+
│ └──────────────────────────────────────────────────────────────┘│
|
|
463
|
+
│ │
|
|
464
|
+
│ [Message Detail Panel appears on row click] │
|
|
465
|
+
│ │
|
|
466
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
**Components:**
|
|
470
|
+
|
|
471
|
+
1. **MessageList.vue**
|
|
472
|
+
- Table of messages
|
|
473
|
+
- Click to show detail panel
|
|
474
|
+
- Pagination
|
|
475
|
+
|
|
476
|
+
2. **MessageDetailPanel.vue**
|
|
477
|
+
- Slide-out panel from right
|
|
478
|
+
- Shows full message data
|
|
479
|
+
- JSON payload viewer
|
|
480
|
+
- Action buttons (retry, delete, move to DLQ)
|
|
481
|
+
|
|
482
|
+
3. **MessageFilters.vue**
|
|
483
|
+
- Filter bar with multiple options
|
|
484
|
+
- Queue selector
|
|
485
|
+
- Status selector
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
### 6. Analytics Page (`/analytics`)
|
|
490
|
+
|
|
491
|
+
**Purpose:** Time-series analysis, trends, and system insights.
|
|
492
|
+
|
|
493
|
+
**API Routes Used:**
|
|
494
|
+
- `GET /api/v1/status/analytics` - Analytics data with time intervals
|
|
495
|
+
- `GET /api/v1/status` - Status data for time range
|
|
496
|
+
|
|
497
|
+
**Layout:**
|
|
498
|
+
|
|
499
|
+
```
|
|
500
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
501
|
+
│ Analytics │
|
|
502
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
503
|
+
│ │
|
|
504
|
+
│ [Last Hour ▼] [All Queues ▼] [Refresh] │
|
|
505
|
+
│ │
|
|
506
|
+
│ ┌──────────────────────────────────────────────────────────────┐│
|
|
507
|
+
│ │ Message Flow (Time Series) ││
|
|
508
|
+
│ │ ││
|
|
509
|
+
│ │ Stacked Area Chart: ││
|
|
510
|
+
│ │ - Ingested (blue area) ││
|
|
511
|
+
│ │ - Processed (green area) ││
|
|
512
|
+
│ │ - Failed (red line) ││
|
|
513
|
+
│ │ ││
|
|
514
|
+
│ └──────────────────────────────────────────────────────────────┘│
|
|
515
|
+
│ │
|
|
516
|
+
│ ┌────────────────────────────┐ ┌───────────────────────────────┐│
|
|
517
|
+
│ │ Top Queues by Volume │ │ Message Status Distribution ││
|
|
518
|
+
│ │ │ │ ││
|
|
519
|
+
│ │ Bar Chart: │ │ Doughnut Chart: ││
|
|
520
|
+
│ │ Queue names on X-axis │ │ - Pending (yellow) ││
|
|
521
|
+
│ │ Message count on Y-axis │ │ - Processing (blue) ││
|
|
522
|
+
│ │ │ │ - Completed (green) ││
|
|
523
|
+
│ │ │ │ - Failed (red) ││
|
|
524
|
+
│ │ │ │ - DLQ (gray) ││
|
|
525
|
+
│ │ │ │ ││
|
|
526
|
+
│ └────────────────────────────┘ └───────────────────────────────┘│
|
|
527
|
+
│ │
|
|
528
|
+
│ ┌──────────────────────────────────────────────────────────────┐│
|
|
529
|
+
│ │ Performance Metrics ││
|
|
530
|
+
│ │ ││
|
|
531
|
+
│ │ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ ││
|
|
532
|
+
│ │ │ Avg Latency │ │ Throughput │ │ Error Rate │ ││
|
|
533
|
+
│ │ │ 125ms │ │ 1,234 msg/s │ │ 0.01% │ ││
|
|
534
|
+
│ │ └────────────────┘ └────────────────┘ └────────────────┘ ││
|
|
535
|
+
│ │ ││
|
|
536
|
+
│ └──────────────────────────────────────────────────────────────┘│
|
|
537
|
+
│ │
|
|
538
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
**Components:**
|
|
542
|
+
|
|
543
|
+
1. **TimeRangeSelector.vue**
|
|
544
|
+
- Dropdown for time range selection
|
|
545
|
+
- Options: Last hour, 6 hours, 24 hours, 7 days
|
|
546
|
+
|
|
547
|
+
2. **MessageFlowChart.vue**
|
|
548
|
+
- Stacked area or line chart
|
|
549
|
+
- Data from `/api/v1/status/analytics`
|
|
550
|
+
|
|
551
|
+
3. **TopQueuesChart.vue**
|
|
552
|
+
- Horizontal bar chart
|
|
553
|
+
- Shows top queues by message volume
|
|
554
|
+
|
|
555
|
+
4. **MessageDistributionChart.vue**
|
|
556
|
+
- Doughnut chart showing status distribution
|
|
557
|
+
- Color-coded segments
|
|
558
|
+
|
|
559
|
+
5. **PerformanceMetrics.vue**
|
|
560
|
+
- Three metric cards
|
|
561
|
+
- Calculated from analytics data
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
## API Integration Map
|
|
566
|
+
|
|
567
|
+
### Component to API Endpoint Mapping
|
|
568
|
+
|
|
569
|
+
| Component | API Endpoint | Method | Purpose |
|
|
570
|
+
|-----------|-------------|--------|---------|
|
|
571
|
+
| HealthIndicator | `/health` | GET | Server health check |
|
|
572
|
+
| MetricCard (Dashboard) | `/api/v1/resources/overview` | GET | System metrics |
|
|
573
|
+
| ThroughputChart | `/api/v1/status` | GET | Throughput data |
|
|
574
|
+
| PerformanceCard | `/metrics` | GET | System performance |
|
|
575
|
+
| QueueList | `/api/v1/resources/queues` | GET | List all queues |
|
|
576
|
+
| QueueList | `/api/v1/resources/namespaces` | GET | Filter namespaces |
|
|
577
|
+
| CreateQueueModal | `/api/v1/configure` | POST | Create queue |
|
|
578
|
+
| QueueRow | `/api/v1/resources/queues/:queue` | DELETE | Delete queue |
|
|
579
|
+
| QueueDetailHeader | `/api/v1/resources/queues/:queue` | GET | Queue details |
|
|
580
|
+
| QueueMetrics | `/api/v1/status/queues/:queue` | GET | Queue status |
|
|
581
|
+
| RecentMessages | `/api/v1/status/queues/:queue/messages` | GET | Queue messages |
|
|
582
|
+
| PushMessageModal | `/api/v1/push` | POST | Push message |
|
|
583
|
+
| ClearQueueAction | `/api/v1/queues/:queue/clear` | DELETE | Clear queue |
|
|
584
|
+
| MessageList | `/api/v1/messages` | GET | List messages |
|
|
585
|
+
| MessageDetailPanel | `/api/v1/messages/:transactionId` | GET | Message detail |
|
|
586
|
+
| MessageDetailPanel | `/api/v1/messages/:transactionId` | DELETE | Delete message |
|
|
587
|
+
| MessageDetailPanel | `/api/v1/messages/:transactionId/retry` | POST | Retry message |
|
|
588
|
+
| MessageDetailPanel | `/api/v1/messages/:transactionId/dlq` | POST | Move to DLQ |
|
|
589
|
+
| ConsumerGroupList | `/api/v1/resources/queues` | GET | Consumer info |
|
|
590
|
+
| MessageFlowChart | `/api/v1/status/analytics` | GET | Analytics data |
|
|
591
|
+
| TopQueuesChart | `/api/v1/status/analytics` | GET | Queue metrics |
|
|
592
|
+
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
## File Structure
|
|
596
|
+
|
|
597
|
+
```
|
|
598
|
+
webapp/
|
|
599
|
+
├── public/
|
|
600
|
+
│ ├── index.html
|
|
601
|
+
│ └── assets/
|
|
602
|
+
│ ├── queen-logo.svg
|
|
603
|
+
│ ├── queen-logo-blue.svg
|
|
604
|
+
│ ├── queen-logo-cyan.svg
|
|
605
|
+
│ ├── queen-logo-indigo.svg
|
|
606
|
+
│ ├── queen-logo-orange.svg
|
|
607
|
+
│ ├── queen-logo-pink.svg
|
|
608
|
+
│ ├── queen-logo-purple.svg
|
|
609
|
+
│ ├── queen-logo-rose.svg
|
|
610
|
+
│ └── favicon.svg
|
|
611
|
+
│
|
|
612
|
+
├── src/
|
|
613
|
+
│ ├── main.js # App entry point
|
|
614
|
+
│ ├── App.vue # Root component with layout
|
|
615
|
+
│ │
|
|
616
|
+
│ ├── router/
|
|
617
|
+
│ │ └── index.js # Vue Router configuration
|
|
618
|
+
│ │
|
|
619
|
+
│ ├── api/
|
|
620
|
+
│ │ ├── client.js # Axios instance with base config
|
|
621
|
+
│ │ ├── health.js # Health & metrics endpoints
|
|
622
|
+
│ │ ├── queues.js # Queue management endpoints
|
|
623
|
+
│ │ ├── messages.js # Message operations endpoints
|
|
624
|
+
│ │ ├── resources.js # Resource query endpoints
|
|
625
|
+
│ │ └── analytics.js # Analytics endpoints
|
|
626
|
+
│ │
|
|
627
|
+
│ ├── composables/
|
|
628
|
+
│ │ ├── useTheme.js # Dark mode toggle
|
|
629
|
+
│ │ ├── useApi.js # API call wrapper with error handling
|
|
630
|
+
│ │ └── usePolling.js # Manual refresh helper
|
|
631
|
+
│ │
|
|
632
|
+
│ ├── components/
|
|
633
|
+
│ │ ├── layout/
|
|
634
|
+
│ │ │ ├── AppLayout.vue # Main layout with sidebar
|
|
635
|
+
│ │ │ ├── AppHeader.vue # Header with logo and controls
|
|
636
|
+
│ │ │ ├── AppSidebar.vue # Sidebar navigation
|
|
637
|
+
│ │ │ └── ThemeToggle.vue # Light/dark theme switcher
|
|
638
|
+
│ │ │
|
|
639
|
+
│ │ ├── common/
|
|
640
|
+
│ │ │ ├── MetricCard.vue # Reusable metric display card
|
|
641
|
+
│ │ │ ├── StatusBadge.vue # Status indicator badge
|
|
642
|
+
│ │ │ ├── DataTable.vue # Generic data table
|
|
643
|
+
│ │ │ ├── LoadingSpinner.vue # Loading state
|
|
644
|
+
│ │ │ ├── EmptyState.vue # Empty state placeholder
|
|
645
|
+
│ │ │ ├── ConfirmDialog.vue # Confirmation modal
|
|
646
|
+
│ │ │ └── RefreshButton.vue # Manual refresh button
|
|
647
|
+
│ │ │
|
|
648
|
+
│ │ ├── dashboard/
|
|
649
|
+
│ │ │ ├── HealthIndicator.vue
|
|
650
|
+
│ │ │ ├── ThroughputChart.vue
|
|
651
|
+
│ │ │ ├── MessageStatusCard.vue
|
|
652
|
+
│ │ │ ├── PerformanceCard.vue
|
|
653
|
+
│ │ │ └── TopQueuesTable.vue
|
|
654
|
+
│ │ │
|
|
655
|
+
│ │ ├── queues/
|
|
656
|
+
│ │ │ ├── QueueList.vue
|
|
657
|
+
│ │ │ ├── QueueRow.vue
|
|
658
|
+
│ │ │ ├── QueueFilters.vue
|
|
659
|
+
│ │ │ └── CreateQueueModal.vue
|
|
660
|
+
│ │ │
|
|
661
|
+
│ │ ├── queue-detail/
|
|
662
|
+
│ │ │ ├── QueueDetailHeader.vue
|
|
663
|
+
│ │ │ ├── QueueMetrics.vue
|
|
664
|
+
│ │ │ ├── QueueConfig.vue
|
|
665
|
+
│ │ │ ├── PartitionList.vue
|
|
666
|
+
│ │ │ ├── RecentMessages.vue
|
|
667
|
+
│ │ │ └── PushMessageModal.vue
|
|
668
|
+
│ │ │
|
|
669
|
+
│ │ ├── messages/
|
|
670
|
+
│ │ │ ├── MessageList.vue
|
|
671
|
+
│ │ │ ├── MessageDetailPanel.vue
|
|
672
|
+
│ │ │ ├── MessageFilters.vue
|
|
673
|
+
│ │ │ └── JsonViewer.vue
|
|
674
|
+
│ │ │
|
|
675
|
+
│ │ ├── consumer-groups/
|
|
676
|
+
│ │ │ ├── ConsumerGroupList.vue
|
|
677
|
+
│ │ │ └── ConsumerGroupRow.vue
|
|
678
|
+
│ │ │
|
|
679
|
+
│ │ └── analytics/
|
|
680
|
+
│ │ ├── TimeRangeSelector.vue
|
|
681
|
+
│ │ ├── MessageFlowChart.vue
|
|
682
|
+
│ │ ├── TopQueuesChart.vue
|
|
683
|
+
│ │ ├── MessageDistributionChart.vue
|
|
684
|
+
│ │ └── PerformanceMetrics.vue
|
|
685
|
+
│ │
|
|
686
|
+
│ ├── views/
|
|
687
|
+
│ │ ├── Dashboard.vue # Dashboard page
|
|
688
|
+
│ │ ├── Queues.vue # Queues list page
|
|
689
|
+
│ │ ├── QueueDetail.vue # Queue detail page
|
|
690
|
+
│ │ ├── Messages.vue # Messages page
|
|
691
|
+
│ │ ├── ConsumerGroups.vue # Consumer groups page
|
|
692
|
+
│ │ └── Analytics.vue # Analytics page
|
|
693
|
+
│ │
|
|
694
|
+
│ ├── utils/
|
|
695
|
+
│ │ ├── formatters.js # Number, date, size formatters
|
|
696
|
+
│ │ ├── constants.js # App constants
|
|
697
|
+
│ │ └── colors.js # Color scheme definitions
|
|
698
|
+
│ │
|
|
699
|
+
│ └── assets/
|
|
700
|
+
│ └── styles/
|
|
701
|
+
│ ├── main.css # Global styles
|
|
702
|
+
│ └── tailwind.css # Tailwind imports
|
|
703
|
+
│
|
|
704
|
+
├── package.json
|
|
705
|
+
├── tailwind.config.js
|
|
706
|
+
├── vite.config.js
|
|
707
|
+
├── postcss.config.js
|
|
708
|
+
└── README.md
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
---
|
|
712
|
+
|
|
713
|
+
## Implementation Plan
|
|
714
|
+
|
|
715
|
+
### Phase 1: Project Setup & Core Infrastructure (Day 1)
|
|
716
|
+
|
|
717
|
+
**1.1 Initialize Project**
|
|
718
|
+
```bash
|
|
719
|
+
npm create vite@latest webapp -- --template vue
|
|
720
|
+
cd webapp
|
|
721
|
+
npm install
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
**1.2 Install Dependencies**
|
|
725
|
+
```bash
|
|
726
|
+
npm install vue-router axios chart.js vue-chartjs
|
|
727
|
+
npm install -D tailwindcss postcss autoprefixer
|
|
728
|
+
npx tailwindcss init -p
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
**1.3 Configure Tailwind CSS**
|
|
732
|
+
|
|
733
|
+
`tailwind.config.js`:
|
|
734
|
+
```javascript
|
|
735
|
+
export default {
|
|
736
|
+
content: [
|
|
737
|
+
"./index.html",
|
|
738
|
+
"./src/**/*.{vue,js}",
|
|
739
|
+
],
|
|
740
|
+
darkMode: 'class',
|
|
741
|
+
theme: {
|
|
742
|
+
extend: {
|
|
743
|
+
colors: {
|
|
744
|
+
'queen-blue': '#3b82f6',
|
|
745
|
+
'queen-indigo': '#6366f1',
|
|
746
|
+
'queen-purple': '#8b5cf6',
|
|
747
|
+
},
|
|
748
|
+
fontFamily: {
|
|
749
|
+
sans: ['Inter', 'sans-serif'],
|
|
750
|
+
mono: ['JetBrains Mono', 'monospace'],
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
plugins: [],
|
|
755
|
+
}
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
**1.4 Setup Base Files**
|
|
759
|
+
|
|
760
|
+
`src/assets/styles/main.css`:
|
|
761
|
+
```css
|
|
762
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
|
763
|
+
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap');
|
|
764
|
+
|
|
765
|
+
@tailwind base;
|
|
766
|
+
@tailwind components;
|
|
767
|
+
@tailwind utilities;
|
|
768
|
+
|
|
769
|
+
@layer base {
|
|
770
|
+
* {
|
|
771
|
+
@apply border-gray-200 dark:border-gray-700;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
body {
|
|
775
|
+
@apply bg-white dark:bg-slate-900 text-gray-900 dark:text-gray-100;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
@layer components {
|
|
780
|
+
.card {
|
|
781
|
+
@apply bg-white dark:bg-slate-800 rounded-lg shadow-sm border p-6;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
.btn {
|
|
785
|
+
@apply px-6 py-2.5 rounded-md font-medium transition-colors;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
.btn-primary {
|
|
789
|
+
@apply bg-blue-600 hover:bg-blue-700 text-white;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.btn-secondary {
|
|
793
|
+
@apply bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
.btn-danger {
|
|
797
|
+
@apply bg-red-600 hover:bg-red-700 text-white;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.badge {
|
|
801
|
+
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.badge-success {
|
|
805
|
+
@apply bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.badge-warning {
|
|
809
|
+
@apply bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
.badge-danger {
|
|
813
|
+
@apply bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
.input {
|
|
817
|
+
@apply w-full px-4 py-2 border rounded-md bg-white dark:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-blue-500;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
**1.5 Create API Client**
|
|
823
|
+
|
|
824
|
+
`src/api/client.js`:
|
|
825
|
+
```javascript
|
|
826
|
+
import axios from 'axios';
|
|
827
|
+
|
|
828
|
+
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:6632';
|
|
829
|
+
|
|
830
|
+
const apiClient = axios.create({
|
|
831
|
+
baseURL: API_BASE_URL,
|
|
832
|
+
timeout: 30000,
|
|
833
|
+
headers: {
|
|
834
|
+
'Content-Type': 'application/json',
|
|
835
|
+
},
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// Response interceptor for error handling
|
|
839
|
+
apiClient.interceptors.response.use(
|
|
840
|
+
(response) => response,
|
|
841
|
+
(error) => {
|
|
842
|
+
if (error.response) {
|
|
843
|
+
// Server responded with error
|
|
844
|
+
console.error('API Error:', error.response.data);
|
|
845
|
+
} else if (error.request) {
|
|
846
|
+
// Request made but no response
|
|
847
|
+
console.error('Network Error:', error.message);
|
|
848
|
+
} else {
|
|
849
|
+
console.error('Error:', error.message);
|
|
850
|
+
}
|
|
851
|
+
return Promise.reject(error);
|
|
852
|
+
}
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
export default apiClient;
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
**1.6 Create API Modules**
|
|
859
|
+
|
|
860
|
+
`src/api/health.js`:
|
|
861
|
+
```javascript
|
|
862
|
+
import apiClient from './client';
|
|
863
|
+
|
|
864
|
+
export const healthApi = {
|
|
865
|
+
getHealth: () => apiClient.get('/health'),
|
|
866
|
+
getMetrics: () => apiClient.get('/metrics'),
|
|
867
|
+
};
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
`src/api/queues.js`:
|
|
871
|
+
```javascript
|
|
872
|
+
import apiClient from './client';
|
|
873
|
+
|
|
874
|
+
export const queuesApi = {
|
|
875
|
+
getQueues: (params) => apiClient.get('/api/v1/resources/queues', { params }),
|
|
876
|
+
getQueue: (queueName) => apiClient.get(`/api/v1/resources/queues/${queueName}`),
|
|
877
|
+
deleteQueue: (queueName) => apiClient.delete(`/api/v1/resources/queues/${queueName}`),
|
|
878
|
+
clearQueue: (queueName, partition) => {
|
|
879
|
+
const params = partition ? { partition } : {};
|
|
880
|
+
return apiClient.delete(`/api/v1/queues/${queueName}/clear`, { params });
|
|
881
|
+
},
|
|
882
|
+
configureQueue: (data) => apiClient.post('/api/v1/configure', data),
|
|
883
|
+
|
|
884
|
+
// Partition operations
|
|
885
|
+
getPartitions: (params) => apiClient.get('/api/v1/resources/partitions', { params }),
|
|
886
|
+
};
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
`src/api/messages.js`:
|
|
890
|
+
```javascript
|
|
891
|
+
import apiClient from './client';
|
|
892
|
+
|
|
893
|
+
export const messagesApi = {
|
|
894
|
+
getMessages: (params) => apiClient.get('/api/v1/messages', { params }),
|
|
895
|
+
getMessage: (transactionId) => apiClient.get(`/api/v1/messages/${transactionId}`),
|
|
896
|
+
deleteMessage: (transactionId) => apiClient.delete(`/api/v1/messages/${transactionId}`),
|
|
897
|
+
retryMessage: (transactionId) => apiClient.post(`/api/v1/messages/${transactionId}/retry`),
|
|
898
|
+
moveToDLQ: (transactionId) => apiClient.post(`/api/v1/messages/${transactionId}/dlq`),
|
|
899
|
+
getRelatedMessages: (transactionId) => apiClient.get(`/api/v1/messages/${transactionId}/related`),
|
|
900
|
+
|
|
901
|
+
// Push messages
|
|
902
|
+
pushMessages: (data) => apiClient.post('/api/v1/push', data),
|
|
903
|
+
|
|
904
|
+
// Pop messages
|
|
905
|
+
popMessages: (queue, partition, params) => {
|
|
906
|
+
if (partition) {
|
|
907
|
+
return apiClient.get(`/api/v1/pop/queue/${queue}/partition/${partition}`, { params });
|
|
908
|
+
}
|
|
909
|
+
return apiClient.get(`/api/v1/pop/queue/${queue}`, { params });
|
|
910
|
+
},
|
|
911
|
+
|
|
912
|
+
// Acknowledgment
|
|
913
|
+
ackMessage: (data) => apiClient.post('/api/v1/ack', data),
|
|
914
|
+
batchAck: (data) => apiClient.post('/api/v1/ack/batch', data),
|
|
915
|
+
};
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
`src/api/resources.js`:
|
|
919
|
+
```javascript
|
|
920
|
+
import apiClient from './client';
|
|
921
|
+
|
|
922
|
+
export const resourcesApi = {
|
|
923
|
+
getOverview: () => apiClient.get('/api/v1/resources/overview'),
|
|
924
|
+
getNamespaces: () => apiClient.get('/api/v1/resources/namespaces'),
|
|
925
|
+
getTasks: () => apiClient.get('/api/v1/resources/tasks'),
|
|
926
|
+
};
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
`src/api/analytics.js`:
|
|
930
|
+
```javascript
|
|
931
|
+
import apiClient from './client';
|
|
932
|
+
|
|
933
|
+
export const analyticsApi = {
|
|
934
|
+
getStatus: (params) => apiClient.get('/api/v1/status', { params }),
|
|
935
|
+
getQueues: (params) => apiClient.get('/api/v1/status/queues', { params }),
|
|
936
|
+
getQueueDetail: (queueName, params) => apiClient.get(`/api/v1/status/queues/${queueName}`, { params }),
|
|
937
|
+
getQueueMessages: (queueName, params) => apiClient.get(`/api/v1/status/queues/${queueName}/messages`, { params }),
|
|
938
|
+
getAnalytics: (params) => apiClient.get('/api/v1/status/analytics', { params }),
|
|
939
|
+
};
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
---
|
|
943
|
+
|
|
944
|
+
### Phase 2: Layout & Navigation (Day 1-2)
|
|
945
|
+
|
|
946
|
+
**2.1 Create Layout Components**
|
|
947
|
+
|
|
948
|
+
`src/App.vue`:
|
|
949
|
+
```vue
|
|
950
|
+
<template>
|
|
951
|
+
<div :class="{ 'dark': isDark }" class="min-h-screen">
|
|
952
|
+
<AppLayout>
|
|
953
|
+
<router-view />
|
|
954
|
+
</AppLayout>
|
|
955
|
+
</div>
|
|
956
|
+
</template>
|
|
957
|
+
|
|
958
|
+
<script setup>
|
|
959
|
+
import { provide } from 'vue';
|
|
960
|
+
import AppLayout from './components/layout/AppLayout.vue';
|
|
961
|
+
import { useTheme } from './composables/useTheme';
|
|
962
|
+
|
|
963
|
+
const { isDark, toggleTheme } = useTheme();
|
|
964
|
+
|
|
965
|
+
// Provide theme globally
|
|
966
|
+
provide('theme', { isDark, toggleTheme });
|
|
967
|
+
</script>
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
`src/components/layout/AppLayout.vue`:
|
|
971
|
+
```vue
|
|
972
|
+
<template>
|
|
973
|
+
<div class="flex h-screen bg-gray-50 dark:bg-slate-900">
|
|
974
|
+
<AppSidebar />
|
|
975
|
+
|
|
976
|
+
<div class="flex-1 flex flex-col overflow-hidden">
|
|
977
|
+
<AppHeader />
|
|
978
|
+
|
|
979
|
+
<main class="flex-1 overflow-y-auto p-6">
|
|
980
|
+
<slot />
|
|
981
|
+
</main>
|
|
982
|
+
</div>
|
|
983
|
+
</div>
|
|
984
|
+
</template>
|
|
985
|
+
|
|
986
|
+
<script setup>
|
|
987
|
+
import AppHeader from './AppHeader.vue';
|
|
988
|
+
import AppSidebar from './AppSidebar.vue';
|
|
989
|
+
</script>
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
`src/components/layout/AppHeader.vue`:
|
|
993
|
+
```vue
|
|
994
|
+
<template>
|
|
995
|
+
<header class="bg-white dark:bg-slate-800 border-b h-16 flex items-center px-6 justify-between">
|
|
996
|
+
<div class="flex items-center gap-4">
|
|
997
|
+
<img src="/assets/queen-logo.svg" alt="Queen" class="h-8 w-8" />
|
|
998
|
+
<h1 class="text-xl font-semibold">Queen Message Queue</h1>
|
|
999
|
+
</div>
|
|
1000
|
+
|
|
1001
|
+
<div class="flex items-center gap-4">
|
|
1002
|
+
<HealthIndicator />
|
|
1003
|
+
<ThemeToggle />
|
|
1004
|
+
</div>
|
|
1005
|
+
</header>
|
|
1006
|
+
</template>
|
|
1007
|
+
|
|
1008
|
+
<script setup>
|
|
1009
|
+
import HealthIndicator from '../dashboard/HealthIndicator.vue';
|
|
1010
|
+
import ThemeToggle from './ThemeToggle.vue';
|
|
1011
|
+
</script>
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
`src/components/layout/AppSidebar.vue`:
|
|
1015
|
+
```vue
|
|
1016
|
+
<template>
|
|
1017
|
+
<aside class="w-64 bg-white dark:bg-slate-800 border-r flex flex-col">
|
|
1018
|
+
<nav class="flex-1 p-4 space-y-1">
|
|
1019
|
+
<router-link
|
|
1020
|
+
v-for="item in navigation"
|
|
1021
|
+
:key="item.path"
|
|
1022
|
+
:to="item.path"
|
|
1023
|
+
class="flex items-center gap-3 px-4 py-3 rounded-lg transition-colors"
|
|
1024
|
+
:class="[
|
|
1025
|
+
isActive(item.path)
|
|
1026
|
+
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400'
|
|
1027
|
+
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-slate-700'
|
|
1028
|
+
]"
|
|
1029
|
+
>
|
|
1030
|
+
<component :is="item.icon" class="w-5 h-5" />
|
|
1031
|
+
<span class="font-medium">{{ item.name }}</span>
|
|
1032
|
+
</router-link>
|
|
1033
|
+
</nav>
|
|
1034
|
+
</aside>
|
|
1035
|
+
</template>
|
|
1036
|
+
|
|
1037
|
+
<script setup>
|
|
1038
|
+
import { useRoute } from 'vue-router';
|
|
1039
|
+
|
|
1040
|
+
const route = useRoute();
|
|
1041
|
+
|
|
1042
|
+
const navigation = [
|
|
1043
|
+
{ name: 'Dashboard', path: '/', icon: 'HomeIcon' },
|
|
1044
|
+
{ name: 'Queues', path: '/queues', icon: 'QueueIcon' },
|
|
1045
|
+
{ name: 'Consumer Groups', path: '/consumer-groups', icon: 'GroupIcon' },
|
|
1046
|
+
{ name: 'Messages', path: '/messages', icon: 'MessageIcon' },
|
|
1047
|
+
{ name: 'Analytics', path: '/analytics', icon: 'ChartIcon' },
|
|
1048
|
+
];
|
|
1049
|
+
|
|
1050
|
+
const isActive = (path) => {
|
|
1051
|
+
if (path === '/') return route.path === '/';
|
|
1052
|
+
return route.path.startsWith(path);
|
|
1053
|
+
};
|
|
1054
|
+
</script>
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
`src/components/layout/ThemeToggle.vue`:
|
|
1058
|
+
```vue
|
|
1059
|
+
<template>
|
|
1060
|
+
<button
|
|
1061
|
+
@click="toggleTheme"
|
|
1062
|
+
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-slate-700"
|
|
1063
|
+
aria-label="Toggle theme"
|
|
1064
|
+
>
|
|
1065
|
+
<svg v-if="isDark" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
1066
|
+
<path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/>
|
|
1067
|
+
</svg>
|
|
1068
|
+
<svg v-else class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
|
1069
|
+
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
|
|
1070
|
+
</svg>
|
|
1071
|
+
</button>
|
|
1072
|
+
</template>
|
|
1073
|
+
|
|
1074
|
+
<script setup>
|
|
1075
|
+
import { inject } from 'vue';
|
|
1076
|
+
|
|
1077
|
+
const { isDark, toggleTheme } = inject('theme');
|
|
1078
|
+
</script>
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
**2.2 Create Composables**
|
|
1082
|
+
|
|
1083
|
+
`src/composables/useTheme.js`:
|
|
1084
|
+
```javascript
|
|
1085
|
+
import { ref, watch, onMounted } from 'vue';
|
|
1086
|
+
|
|
1087
|
+
export function useTheme() {
|
|
1088
|
+
const isDark = ref(false);
|
|
1089
|
+
|
|
1090
|
+
const toggleTheme = () => {
|
|
1091
|
+
isDark.value = !isDark.value;
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
// Watch for changes and update localStorage and document class
|
|
1095
|
+
watch(isDark, (newValue) => {
|
|
1096
|
+
if (newValue) {
|
|
1097
|
+
document.documentElement.classList.add('dark');
|
|
1098
|
+
localStorage.setItem('theme', 'dark');
|
|
1099
|
+
} else {
|
|
1100
|
+
document.documentElement.classList.remove('dark');
|
|
1101
|
+
localStorage.setItem('theme', 'light');
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
// Initialize theme from localStorage or system preference
|
|
1106
|
+
onMounted(() => {
|
|
1107
|
+
const savedTheme = localStorage.getItem('theme');
|
|
1108
|
+
if (savedTheme) {
|
|
1109
|
+
isDark.value = savedTheme === 'dark';
|
|
1110
|
+
} else {
|
|
1111
|
+
isDark.value = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
return {
|
|
1116
|
+
isDark,
|
|
1117
|
+
toggleTheme,
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
`src/composables/useApi.js`:
|
|
1123
|
+
```javascript
|
|
1124
|
+
import { ref } from 'vue';
|
|
1125
|
+
|
|
1126
|
+
export function useApi(apiCall) {
|
|
1127
|
+
const data = ref(null);
|
|
1128
|
+
const error = ref(null);
|
|
1129
|
+
const loading = ref(false);
|
|
1130
|
+
|
|
1131
|
+
const execute = async (...params) => {
|
|
1132
|
+
loading.value = true;
|
|
1133
|
+
error.value = null;
|
|
1134
|
+
|
|
1135
|
+
try {
|
|
1136
|
+
const response = await apiCall(...params);
|
|
1137
|
+
data.value = response.data;
|
|
1138
|
+
return response.data;
|
|
1139
|
+
} catch (err) {
|
|
1140
|
+
error.value = err.response?.data?.error || err.message || 'An error occurred';
|
|
1141
|
+
throw err;
|
|
1142
|
+
} finally {
|
|
1143
|
+
loading.value = false;
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
const refresh = () => execute();
|
|
1148
|
+
|
|
1149
|
+
return {
|
|
1150
|
+
data,
|
|
1151
|
+
error,
|
|
1152
|
+
loading,
|
|
1153
|
+
execute,
|
|
1154
|
+
refresh,
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
```
|
|
1158
|
+
|
|
1159
|
+
**2.3 Create Router**
|
|
1160
|
+
|
|
1161
|
+
`src/router/index.js`:
|
|
1162
|
+
```javascript
|
|
1163
|
+
import { createRouter, createWebHistory } from 'vue-router';
|
|
1164
|
+
|
|
1165
|
+
const routes = [
|
|
1166
|
+
{
|
|
1167
|
+
path: '/',
|
|
1168
|
+
name: 'Dashboard',
|
|
1169
|
+
component: () => import('../views/Dashboard.vue'),
|
|
1170
|
+
},
|
|
1171
|
+
{
|
|
1172
|
+
path: '/queues',
|
|
1173
|
+
name: 'Queues',
|
|
1174
|
+
component: () => import('../views/Queues.vue'),
|
|
1175
|
+
},
|
|
1176
|
+
{
|
|
1177
|
+
path: '/queues/:queueName',
|
|
1178
|
+
name: 'QueueDetail',
|
|
1179
|
+
component: () => import('../views/QueueDetail.vue'),
|
|
1180
|
+
},
|
|
1181
|
+
{
|
|
1182
|
+
path: '/consumer-groups',
|
|
1183
|
+
name: 'ConsumerGroups',
|
|
1184
|
+
component: () => import('../views/ConsumerGroups.vue'),
|
|
1185
|
+
},
|
|
1186
|
+
{
|
|
1187
|
+
path: '/messages',
|
|
1188
|
+
name: 'Messages',
|
|
1189
|
+
component: () => import('../views/Messages.vue'),
|
|
1190
|
+
},
|
|
1191
|
+
{
|
|
1192
|
+
path: '/analytics',
|
|
1193
|
+
name: 'Analytics',
|
|
1194
|
+
component: () => import('../views/Analytics.vue'),
|
|
1195
|
+
},
|
|
1196
|
+
];
|
|
1197
|
+
|
|
1198
|
+
const router = createRouter({
|
|
1199
|
+
history: createWebHistory(),
|
|
1200
|
+
routes,
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
export default router;
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
---
|
|
1207
|
+
|
|
1208
|
+
### Phase 3: Common Components (Day 2)
|
|
1209
|
+
|
|
1210
|
+
**3.1 Utility Components**
|
|
1211
|
+
|
|
1212
|
+
`src/components/common/MetricCard.vue`:
|
|
1213
|
+
```vue
|
|
1214
|
+
<template>
|
|
1215
|
+
<div class="card">
|
|
1216
|
+
<div class="flex items-center justify-between">
|
|
1217
|
+
<div>
|
|
1218
|
+
<p class="text-sm text-gray-500 dark:text-gray-400">{{ title }}</p>
|
|
1219
|
+
<p class="text-3xl font-bold mt-1">{{ formattedValue }}</p>
|
|
1220
|
+
<p v-if="subtitle" class="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
|
1221
|
+
{{ subtitle }}
|
|
1222
|
+
</p>
|
|
1223
|
+
</div>
|
|
1224
|
+
<div v-if="icon" class="text-blue-500">
|
|
1225
|
+
<component :is="icon" class="w-10 h-10" />
|
|
1226
|
+
</div>
|
|
1227
|
+
</div>
|
|
1228
|
+
</div>
|
|
1229
|
+
</template>
|
|
1230
|
+
|
|
1231
|
+
<script setup>
|
|
1232
|
+
import { computed } from 'vue';
|
|
1233
|
+
import { formatNumber } from '../../utils/formatters';
|
|
1234
|
+
|
|
1235
|
+
const props = defineProps({
|
|
1236
|
+
title: String,
|
|
1237
|
+
value: [String, Number],
|
|
1238
|
+
subtitle: String,
|
|
1239
|
+
icon: Object,
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
const formattedValue = computed(() => {
|
|
1243
|
+
if (typeof props.value === 'number') {
|
|
1244
|
+
return formatNumber(props.value);
|
|
1245
|
+
}
|
|
1246
|
+
return props.value;
|
|
1247
|
+
});
|
|
1248
|
+
</script>
|
|
1249
|
+
```
|
|
1250
|
+
|
|
1251
|
+
`src/components/common/StatusBadge.vue`:
|
|
1252
|
+
```vue
|
|
1253
|
+
<template>
|
|
1254
|
+
<span :class="badgeClass">
|
|
1255
|
+
<span v-if="showDot" class="w-2 h-2 rounded-full mr-1.5" :class="dotClass"></span>
|
|
1256
|
+
{{ label }}
|
|
1257
|
+
</span>
|
|
1258
|
+
</template>
|
|
1259
|
+
|
|
1260
|
+
<script setup>
|
|
1261
|
+
import { computed } from 'vue';
|
|
1262
|
+
|
|
1263
|
+
const props = defineProps({
|
|
1264
|
+
status: {
|
|
1265
|
+
type: String,
|
|
1266
|
+
required: true,
|
|
1267
|
+
},
|
|
1268
|
+
showDot: {
|
|
1269
|
+
type: Boolean,
|
|
1270
|
+
default: true,
|
|
1271
|
+
},
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
const statusConfig = {
|
|
1275
|
+
healthy: { class: 'badge-success', dot: 'bg-green-500', label: 'Healthy' },
|
|
1276
|
+
stable: { class: 'badge-success', dot: 'bg-green-500', label: 'Stable' },
|
|
1277
|
+
warning: { class: 'badge-warning', dot: 'bg-yellow-500', label: 'Warning' },
|
|
1278
|
+
error: { class: 'badge-danger', dot: 'bg-red-500', label: 'Error' },
|
|
1279
|
+
unhealthy: { class: 'badge-danger', dot: 'bg-red-500', label: 'Unhealthy' },
|
|
1280
|
+
pending: { class: 'badge-warning', dot: 'bg-yellow-500', label: 'Pending' },
|
|
1281
|
+
processing: { class: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200', dot: 'bg-blue-500', label: 'Processing' },
|
|
1282
|
+
completed: { class: 'badge-success', dot: 'bg-green-500', label: 'Completed' },
|
|
1283
|
+
failed: { class: 'badge-danger', dot: 'bg-red-500', label: 'Failed' },
|
|
1284
|
+
};
|
|
1285
|
+
|
|
1286
|
+
const config = computed(() => statusConfig[props.status.toLowerCase()] || statusConfig.pending);
|
|
1287
|
+
const badgeClass = computed(() => `badge ${config.value.class}`);
|
|
1288
|
+
const dotClass = computed(() => config.value.dot);
|
|
1289
|
+
const label = computed(() => config.value.label);
|
|
1290
|
+
</script>
|
|
1291
|
+
```
|
|
1292
|
+
|
|
1293
|
+
`src/components/common/LoadingSpinner.vue`:
|
|
1294
|
+
```vue
|
|
1295
|
+
<template>
|
|
1296
|
+
<div class="flex items-center justify-center p-8">
|
|
1297
|
+
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
|
1298
|
+
</div>
|
|
1299
|
+
</template>
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
`src/components/common/RefreshButton.vue`:
|
|
1303
|
+
```vue
|
|
1304
|
+
<template>
|
|
1305
|
+
<button
|
|
1306
|
+
@click="$emit('refresh')"
|
|
1307
|
+
:disabled="loading"
|
|
1308
|
+
class="btn btn-secondary flex items-center gap-2"
|
|
1309
|
+
>
|
|
1310
|
+
<svg
|
|
1311
|
+
class="w-4 h-4"
|
|
1312
|
+
:class="{ 'animate-spin': loading }"
|
|
1313
|
+
fill="none"
|
|
1314
|
+
stroke="currentColor"
|
|
1315
|
+
viewBox="0 0 24 24"
|
|
1316
|
+
>
|
|
1317
|
+
<path
|
|
1318
|
+
stroke-linecap="round"
|
|
1319
|
+
stroke-linejoin="round"
|
|
1320
|
+
stroke-width="2"
|
|
1321
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
|
1322
|
+
/>
|
|
1323
|
+
</svg>
|
|
1324
|
+
Refresh
|
|
1325
|
+
</button>
|
|
1326
|
+
</template>
|
|
1327
|
+
|
|
1328
|
+
<script setup>
|
|
1329
|
+
defineProps({
|
|
1330
|
+
loading: Boolean,
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
defineEmits(['refresh']);
|
|
1334
|
+
</script>
|
|
1335
|
+
```
|
|
1336
|
+
|
|
1337
|
+
**3.2 Utility Functions**
|
|
1338
|
+
|
|
1339
|
+
`src/utils/formatters.js`:
|
|
1340
|
+
```javascript
|
|
1341
|
+
export function formatNumber(num) {
|
|
1342
|
+
if (num >= 1000000) {
|
|
1343
|
+
return (num / 1000000).toFixed(1) + 'M';
|
|
1344
|
+
}
|
|
1345
|
+
if (num >= 1000) {
|
|
1346
|
+
return (num / 1000).toFixed(1) + 'K';
|
|
1347
|
+
}
|
|
1348
|
+
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
export function formatBytes(bytes) {
|
|
1352
|
+
if (bytes === 0) return '0 B';
|
|
1353
|
+
const k = 1024;
|
|
1354
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
1355
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1356
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
export function formatDuration(ms) {
|
|
1360
|
+
if (ms < 1000) return `${ms}ms`;
|
|
1361
|
+
|
|
1362
|
+
const seconds = Math.floor(ms / 1000);
|
|
1363
|
+
const minutes = Math.floor(seconds / 60);
|
|
1364
|
+
const hours = Math.floor(minutes / 60);
|
|
1365
|
+
const days = Math.floor(hours / 24);
|
|
1366
|
+
|
|
1367
|
+
if (days > 0) return `${days}d ${hours % 24}h`;
|
|
1368
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
1369
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
|
1370
|
+
return `${seconds}s`;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
export function formatDate(date) {
|
|
1374
|
+
return new Date(date).toLocaleString('en-US', {
|
|
1375
|
+
month: 'short',
|
|
1376
|
+
day: 'numeric',
|
|
1377
|
+
hour: '2-digit',
|
|
1378
|
+
minute: '2-digit',
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
export function formatTime(date) {
|
|
1383
|
+
return new Date(date).toLocaleTimeString('en-US', {
|
|
1384
|
+
hour: '2-digit',
|
|
1385
|
+
minute: '2-digit',
|
|
1386
|
+
second: '2-digit',
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
---
|
|
1392
|
+
|
|
1393
|
+
### Phase 4: Dashboard Page (Day 3)
|
|
1394
|
+
|
|
1395
|
+
**4.1 Dashboard View**
|
|
1396
|
+
|
|
1397
|
+
`src/views/Dashboard.vue`:
|
|
1398
|
+
```vue
|
|
1399
|
+
<template>
|
|
1400
|
+
<div class="space-y-6">
|
|
1401
|
+
<div class="flex items-center justify-between">
|
|
1402
|
+
<h2 class="text-2xl font-bold">Dashboard</h2>
|
|
1403
|
+
<RefreshButton :loading="loading" @refresh="loadData" />
|
|
1404
|
+
</div>
|
|
1405
|
+
|
|
1406
|
+
<LoadingSpinner v-if="loading && !overview" />
|
|
1407
|
+
|
|
1408
|
+
<div v-else-if="error" class="card bg-red-50 dark:bg-red-900/20 text-red-600">
|
|
1409
|
+
<p>Error loading dashboard: {{ error }}</p>
|
|
1410
|
+
</div>
|
|
1411
|
+
|
|
1412
|
+
<template v-else>
|
|
1413
|
+
<!-- Metric Cards -->
|
|
1414
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
1415
|
+
<MetricCard
|
|
1416
|
+
title="Queues"
|
|
1417
|
+
:value="overview?.queues || 0"
|
|
1418
|
+
/>
|
|
1419
|
+
<MetricCard
|
|
1420
|
+
title="Partitions"
|
|
1421
|
+
:value="overview?.partitions || 0"
|
|
1422
|
+
/>
|
|
1423
|
+
<MetricCard
|
|
1424
|
+
title="Total Messages"
|
|
1425
|
+
:value="overview?.messages?.total || 0"
|
|
1426
|
+
/>
|
|
1427
|
+
<MetricCard
|
|
1428
|
+
title="Uptime"
|
|
1429
|
+
:value="health?.uptime || '-'"
|
|
1430
|
+
/>
|
|
1431
|
+
</div>
|
|
1432
|
+
|
|
1433
|
+
<!-- Throughput Chart -->
|
|
1434
|
+
<div class="card">
|
|
1435
|
+
<h3 class="text-lg font-semibold mb-4">Message Throughput</h3>
|
|
1436
|
+
<ThroughputChart :data="status" />
|
|
1437
|
+
</div>
|
|
1438
|
+
|
|
1439
|
+
<!-- Stats Row -->
|
|
1440
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
1441
|
+
<MessageStatusCard :data="overview?.messages" />
|
|
1442
|
+
<PerformanceCard :data="metrics" />
|
|
1443
|
+
</div>
|
|
1444
|
+
|
|
1445
|
+
<!-- Top Queues -->
|
|
1446
|
+
<div class="card">
|
|
1447
|
+
<h3 class="text-lg font-semibold mb-4">Top Queues by Activity</h3>
|
|
1448
|
+
<TopQueuesTable :queues="topQueues" />
|
|
1449
|
+
</div>
|
|
1450
|
+
</template>
|
|
1451
|
+
</div>
|
|
1452
|
+
</template>
|
|
1453
|
+
|
|
1454
|
+
<script setup>
|
|
1455
|
+
import { ref, onMounted } from 'vue';
|
|
1456
|
+
import { healthApi } from '../api/health';
|
|
1457
|
+
import { resourcesApi } from '../api/resources';
|
|
1458
|
+
import { analyticsApi } from '../api/analytics';
|
|
1459
|
+
import { queuesApi } from '../api/queues';
|
|
1460
|
+
|
|
1461
|
+
import MetricCard from '../components/common/MetricCard.vue';
|
|
1462
|
+
import LoadingSpinner from '../components/common/LoadingSpinner.vue';
|
|
1463
|
+
import RefreshButton from '../components/common/RefreshButton.vue';
|
|
1464
|
+
import ThroughputChart from '../components/dashboard/ThroughputChart.vue';
|
|
1465
|
+
import MessageStatusCard from '../components/dashboard/MessageStatusCard.vue';
|
|
1466
|
+
import PerformanceCard from '../components/dashboard/PerformanceCard.vue';
|
|
1467
|
+
import TopQueuesTable from '../components/dashboard/TopQueuesTable.vue';
|
|
1468
|
+
|
|
1469
|
+
const loading = ref(false);
|
|
1470
|
+
const error = ref(null);
|
|
1471
|
+
const overview = ref(null);
|
|
1472
|
+
const health = ref(null);
|
|
1473
|
+
const metrics = ref(null);
|
|
1474
|
+
const status = ref(null);
|
|
1475
|
+
const topQueues = ref([]);
|
|
1476
|
+
|
|
1477
|
+
async function loadData() {
|
|
1478
|
+
loading.value = true;
|
|
1479
|
+
error.value = null;
|
|
1480
|
+
|
|
1481
|
+
try {
|
|
1482
|
+
const [overviewRes, healthRes, metricsRes, statusRes, queuesRes] = await Promise.all([
|
|
1483
|
+
resourcesApi.getOverview(),
|
|
1484
|
+
healthApi.getHealth(),
|
|
1485
|
+
healthApi.getMetrics(),
|
|
1486
|
+
analyticsApi.getStatus(),
|
|
1487
|
+
queuesApi.getQueues(),
|
|
1488
|
+
]);
|
|
1489
|
+
|
|
1490
|
+
overview.value = overviewRes.data;
|
|
1491
|
+
health.value = healthRes.data;
|
|
1492
|
+
metrics.value = metricsRes.data;
|
|
1493
|
+
status.value = statusRes.data;
|
|
1494
|
+
|
|
1495
|
+
// Get top 5 queues by total messages
|
|
1496
|
+
topQueues.value = queuesRes.data.queues
|
|
1497
|
+
.sort((a, b) => (b.messages?.total || 0) - (a.messages?.total || 0))
|
|
1498
|
+
.slice(0, 5);
|
|
1499
|
+
} catch (err) {
|
|
1500
|
+
error.value = err.message;
|
|
1501
|
+
} finally {
|
|
1502
|
+
loading.value = false;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
onMounted(() => {
|
|
1507
|
+
loadData();
|
|
1508
|
+
});
|
|
1509
|
+
</script>
|
|
1510
|
+
```
|
|
1511
|
+
|
|
1512
|
+
**4.2 Dashboard Components**
|
|
1513
|
+
|
|
1514
|
+
`src/components/dashboard/HealthIndicator.vue`:
|
|
1515
|
+
```vue
|
|
1516
|
+
<template>
|
|
1517
|
+
<div class="flex items-center gap-2">
|
|
1518
|
+
<span
|
|
1519
|
+
class="w-2 h-2 rounded-full"
|
|
1520
|
+
:class="isHealthy ? 'bg-green-500' : 'bg-red-500'"
|
|
1521
|
+
></span>
|
|
1522
|
+
<span class="text-sm font-medium">{{ statusText }}</span>
|
|
1523
|
+
</div>
|
|
1524
|
+
</template>
|
|
1525
|
+
|
|
1526
|
+
<script setup>
|
|
1527
|
+
import { ref, computed, onMounted } from 'vue';
|
|
1528
|
+
import { healthApi } from '../../api/health';
|
|
1529
|
+
|
|
1530
|
+
const health = ref(null);
|
|
1531
|
+
|
|
1532
|
+
const isHealthy = computed(() => health.value?.status === 'healthy');
|
|
1533
|
+
const statusText = computed(() => isHealthy.value ? 'Healthy' : 'Unhealthy');
|
|
1534
|
+
|
|
1535
|
+
async function checkHealth() {
|
|
1536
|
+
try {
|
|
1537
|
+
const response = await healthApi.getHealth();
|
|
1538
|
+
health.value = response.data;
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
health.value = { status: 'unhealthy' };
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
onMounted(() => {
|
|
1545
|
+
checkHealth();
|
|
1546
|
+
// Check health every 30 seconds
|
|
1547
|
+
setInterval(checkHealth, 30000);
|
|
1548
|
+
});
|
|
1549
|
+
</script>
|
|
1550
|
+
```
|
|
1551
|
+
|
|
1552
|
+
`src/components/dashboard/ThroughputChart.vue`:
|
|
1553
|
+
```vue
|
|
1554
|
+
<template>
|
|
1555
|
+
<div class="h-64">
|
|
1556
|
+
<Line v-if="chartData" :data="chartData" :options="chartOptions" />
|
|
1557
|
+
<div v-else class="flex items-center justify-center h-full text-gray-500">
|
|
1558
|
+
No data available
|
|
1559
|
+
</div>
|
|
1560
|
+
</div>
|
|
1561
|
+
</template>
|
|
1562
|
+
|
|
1563
|
+
<script setup>
|
|
1564
|
+
import { computed } from 'vue';
|
|
1565
|
+
import { Line } from 'vue-chartjs';
|
|
1566
|
+
import {
|
|
1567
|
+
Chart as ChartJS,
|
|
1568
|
+
CategoryScale,
|
|
1569
|
+
LinearScale,
|
|
1570
|
+
PointElement,
|
|
1571
|
+
LineElement,
|
|
1572
|
+
Title,
|
|
1573
|
+
Tooltip,
|
|
1574
|
+
Legend,
|
|
1575
|
+
Filler,
|
|
1576
|
+
} from 'chart.js';
|
|
1577
|
+
|
|
1578
|
+
ChartJS.register(
|
|
1579
|
+
CategoryScale,
|
|
1580
|
+
LinearScale,
|
|
1581
|
+
PointElement,
|
|
1582
|
+
LineElement,
|
|
1583
|
+
Title,
|
|
1584
|
+
Tooltip,
|
|
1585
|
+
Legend,
|
|
1586
|
+
Filler
|
|
1587
|
+
);
|
|
1588
|
+
|
|
1589
|
+
const props = defineProps({
|
|
1590
|
+
data: Object,
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
const chartData = computed(() => {
|
|
1594
|
+
if (!props.data?.throughput?.length) return null;
|
|
1595
|
+
|
|
1596
|
+
const throughput = props.data.throughput;
|
|
1597
|
+
|
|
1598
|
+
return {
|
|
1599
|
+
labels: throughput.map(t => {
|
|
1600
|
+
const date = new Date(t.timestamp);
|
|
1601
|
+
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
|
1602
|
+
}),
|
|
1603
|
+
datasets: [
|
|
1604
|
+
{
|
|
1605
|
+
label: 'Ingested',
|
|
1606
|
+
data: throughput.map(t => t.ingestedPerSecond),
|
|
1607
|
+
borderColor: 'rgb(59, 130, 246)',
|
|
1608
|
+
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
1609
|
+
fill: true,
|
|
1610
|
+
tension: 0.4,
|
|
1611
|
+
},
|
|
1612
|
+
{
|
|
1613
|
+
label: 'Processed',
|
|
1614
|
+
data: throughput.map(t => t.processedPerSecond),
|
|
1615
|
+
borderColor: 'rgb(16, 185, 129)',
|
|
1616
|
+
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
|
1617
|
+
fill: true,
|
|
1618
|
+
tension: 0.4,
|
|
1619
|
+
},
|
|
1620
|
+
],
|
|
1621
|
+
};
|
|
1622
|
+
});
|
|
1623
|
+
|
|
1624
|
+
const chartOptions = {
|
|
1625
|
+
responsive: true,
|
|
1626
|
+
maintainAspectRatio: false,
|
|
1627
|
+
interaction: {
|
|
1628
|
+
mode: 'index',
|
|
1629
|
+
intersect: false,
|
|
1630
|
+
},
|
|
1631
|
+
scales: {
|
|
1632
|
+
y: {
|
|
1633
|
+
beginAtZero: true,
|
|
1634
|
+
ticks: {
|
|
1635
|
+
callback: (value) => value + ' msg/s',
|
|
1636
|
+
},
|
|
1637
|
+
},
|
|
1638
|
+
},
|
|
1639
|
+
plugins: {
|
|
1640
|
+
legend: {
|
|
1641
|
+
position: 'top',
|
|
1642
|
+
},
|
|
1643
|
+
tooltip: {
|
|
1644
|
+
callbacks: {
|
|
1645
|
+
label: (context) => {
|
|
1646
|
+
return `${context.dataset.label}: ${context.parsed.y} msg/s`;
|
|
1647
|
+
},
|
|
1648
|
+
},
|
|
1649
|
+
},
|
|
1650
|
+
},
|
|
1651
|
+
};
|
|
1652
|
+
</script>
|
|
1653
|
+
```
|
|
1654
|
+
|
|
1655
|
+
`src/components/dashboard/MessageStatusCard.vue`:
|
|
1656
|
+
```vue
|
|
1657
|
+
<template>
|
|
1658
|
+
<div class="card">
|
|
1659
|
+
<h3 class="text-lg font-semibold mb-4">Message Status</h3>
|
|
1660
|
+
<div class="space-y-3">
|
|
1661
|
+
<div class="flex items-center justify-between">
|
|
1662
|
+
<span class="text-gray-600 dark:text-gray-400">Pending</span>
|
|
1663
|
+
<span class="font-semibold">{{ formatNumber(data?.pending || 0) }}</span>
|
|
1664
|
+
</div>
|
|
1665
|
+
<div class="flex items-center justify-between">
|
|
1666
|
+
<span class="text-gray-600 dark:text-gray-400">Processing</span>
|
|
1667
|
+
<span class="font-semibold">{{ formatNumber(data?.processing || 0) }}</span>
|
|
1668
|
+
</div>
|
|
1669
|
+
<div class="flex items-center justify-between">
|
|
1670
|
+
<span class="text-gray-600 dark:text-gray-400">Completed</span>
|
|
1671
|
+
<span class="font-semibold text-green-600">{{ formatNumber(data?.completed || 0) }}</span>
|
|
1672
|
+
</div>
|
|
1673
|
+
<div class="flex items-center justify-between">
|
|
1674
|
+
<span class="text-gray-600 dark:text-gray-400">Failed</span>
|
|
1675
|
+
<span class="font-semibold text-red-600">{{ formatNumber(data?.failed || 0) }}</span>
|
|
1676
|
+
</div>
|
|
1677
|
+
<div class="flex items-center justify-between">
|
|
1678
|
+
<span class="text-gray-600 dark:text-gray-400">Dead Letter</span>
|
|
1679
|
+
<span class="font-semibold text-gray-600">{{ formatNumber(data?.deadLetter || 0) }}</span>
|
|
1680
|
+
</div>
|
|
1681
|
+
</div>
|
|
1682
|
+
</div>
|
|
1683
|
+
</template>
|
|
1684
|
+
|
|
1685
|
+
<script setup>
|
|
1686
|
+
import { formatNumber } from '../../utils/formatters';
|
|
1687
|
+
|
|
1688
|
+
defineProps({
|
|
1689
|
+
data: Object,
|
|
1690
|
+
});
|
|
1691
|
+
</script>
|
|
1692
|
+
```
|
|
1693
|
+
|
|
1694
|
+
`src/components/dashboard/PerformanceCard.vue`:
|
|
1695
|
+
```vue
|
|
1696
|
+
<template>
|
|
1697
|
+
<div class="card">
|
|
1698
|
+
<h3 class="text-lg font-semibold mb-4">System Performance</h3>
|
|
1699
|
+
<div class="space-y-3">
|
|
1700
|
+
<div class="flex items-center justify-between">
|
|
1701
|
+
<span class="text-gray-600 dark:text-gray-400">Requests/sec</span>
|
|
1702
|
+
<span class="font-semibold">{{ data?.requests?.rate?.toFixed(2) || '0.00' }}</span>
|
|
1703
|
+
</div>
|
|
1704
|
+
<div class="flex items-center justify-between">
|
|
1705
|
+
<span class="text-gray-600 dark:text-gray-400">Messages/sec</span>
|
|
1706
|
+
<span class="font-semibold">{{ data?.messages?.rate?.toFixed(2) || '0.00' }}</span>
|
|
1707
|
+
</div>
|
|
1708
|
+
<div class="flex items-center justify-between">
|
|
1709
|
+
<span class="text-gray-600 dark:text-gray-400">DB Connections</span>
|
|
1710
|
+
<span class="font-semibold">
|
|
1711
|
+
{{ data?.database?.poolSize - data?.database?.idleConnections || 0 }}/{{ data?.database?.poolSize || 0 }}
|
|
1712
|
+
</span>
|
|
1713
|
+
</div>
|
|
1714
|
+
<div class="flex items-center justify-between">
|
|
1715
|
+
<span class="text-gray-600 dark:text-gray-400">Memory (Heap)</span>
|
|
1716
|
+
<span class="font-semibold">{{ formatBytes(data?.memory?.heapUsed || 0) }}</span>
|
|
1717
|
+
</div>
|
|
1718
|
+
<div class="flex items-center justify-between">
|
|
1719
|
+
<span class="text-gray-600 dark:text-gray-400">CPU Time</span>
|
|
1720
|
+
<span class="font-semibold">{{ formatDuration((data?.cpu?.user + data?.cpu?.system) / 1000 || 0) }}</span>
|
|
1721
|
+
</div>
|
|
1722
|
+
</div>
|
|
1723
|
+
</div>
|
|
1724
|
+
</template>
|
|
1725
|
+
|
|
1726
|
+
<script setup>
|
|
1727
|
+
import { formatBytes, formatDuration } from '../../utils/formatters';
|
|
1728
|
+
|
|
1729
|
+
defineProps({
|
|
1730
|
+
data: Object,
|
|
1731
|
+
});
|
|
1732
|
+
</script>
|
|
1733
|
+
```
|
|
1734
|
+
|
|
1735
|
+
`src/components/dashboard/TopQueuesTable.vue`:
|
|
1736
|
+
```vue
|
|
1737
|
+
<template>
|
|
1738
|
+
<div class="overflow-x-auto">
|
|
1739
|
+
<table class="w-full">
|
|
1740
|
+
<thead class="text-left text-sm text-gray-500 border-b">
|
|
1741
|
+
<tr>
|
|
1742
|
+
<th class="pb-3 font-medium">Queue Name</th>
|
|
1743
|
+
<th class="pb-3 font-medium text-right">Pending</th>
|
|
1744
|
+
<th class="pb-3 font-medium text-right">Processing</th>
|
|
1745
|
+
<th class="pb-3 font-medium text-right">Completed</th>
|
|
1746
|
+
</tr>
|
|
1747
|
+
</thead>
|
|
1748
|
+
<tbody class="text-sm">
|
|
1749
|
+
<tr
|
|
1750
|
+
v-for="queue in queues"
|
|
1751
|
+
:key="queue.id"
|
|
1752
|
+
class="border-b last:border-b-0 hover:bg-gray-50 dark:hover:bg-slate-700/50 cursor-pointer"
|
|
1753
|
+
@click="navigateToQueue(queue.name)"
|
|
1754
|
+
>
|
|
1755
|
+
<td class="py-3">
|
|
1756
|
+
<div class="font-medium">{{ queue.name }}</div>
|
|
1757
|
+
<div v-if="queue.namespace" class="text-xs text-gray-500">{{ queue.namespace }}</div>
|
|
1758
|
+
</td>
|
|
1759
|
+
<td class="py-3 text-right">{{ formatNumber(queue.messages?.pending || 0) }}</td>
|
|
1760
|
+
<td class="py-3 text-right">{{ formatNumber(queue.messages?.processing || 0) }}</td>
|
|
1761
|
+
<td class="py-3 text-right">{{ formatNumber(queue.messages?.total || 0) }}</td>
|
|
1762
|
+
</tr>
|
|
1763
|
+
</tbody>
|
|
1764
|
+
</table>
|
|
1765
|
+
|
|
1766
|
+
<div v-if="!queues?.length" class="text-center py-8 text-gray-500">
|
|
1767
|
+
No queues available
|
|
1768
|
+
</div>
|
|
1769
|
+
</div>
|
|
1770
|
+
</template>
|
|
1771
|
+
|
|
1772
|
+
<script setup>
|
|
1773
|
+
import { useRouter } from 'vue-router';
|
|
1774
|
+
import { formatNumber } from '../../utils/formatters';
|
|
1775
|
+
|
|
1776
|
+
defineProps({
|
|
1777
|
+
queues: Array,
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
const router = useRouter();
|
|
1781
|
+
|
|
1782
|
+
function navigateToQueue(queueName) {
|
|
1783
|
+
router.push(`/queues/${queueName}`);
|
|
1784
|
+
}
|
|
1785
|
+
</script>
|
|
1786
|
+
```
|
|
1787
|
+
|
|
1788
|
+
---
|
|
1789
|
+
|
|
1790
|
+
### Phase 5: Remaining Pages (Day 4-5)
|
|
1791
|
+
|
|
1792
|
+
Continue implementing:
|
|
1793
|
+
- Queues page with list and filters
|
|
1794
|
+
- Queue Detail page with full information
|
|
1795
|
+
- Messages page with browsing capabilities
|
|
1796
|
+
- Consumer Groups page
|
|
1797
|
+
- Analytics page with charts
|
|
1798
|
+
|
|
1799
|
+
Each page follows similar patterns established in the Dashboard.
|
|
1800
|
+
|
|
1801
|
+
---
|
|
1802
|
+
|
|
1803
|
+
### Phase 6: Testing & Polish (Day 6)
|
|
1804
|
+
|
|
1805
|
+
1. Test all API integrations
|
|
1806
|
+
2. Verify dark mode consistency
|
|
1807
|
+
3. Test responsive layouts
|
|
1808
|
+
4. Add error boundaries
|
|
1809
|
+
5. Optimize performance
|
|
1810
|
+
6. Add loading states everywhere
|
|
1811
|
+
7. Test with real queue data
|
|
1812
|
+
|
|
1813
|
+
---
|
|
1814
|
+
|
|
1815
|
+
## Chart Configuration
|
|
1816
|
+
|
|
1817
|
+
### Chart.js Setup
|
|
1818
|
+
|
|
1819
|
+
All charts use consistent color schemes:
|
|
1820
|
+
|
|
1821
|
+
**Light Mode:**
|
|
1822
|
+
- Primary: Blue (#3b82f6)
|
|
1823
|
+
- Success: Green (#10b981)
|
|
1824
|
+
- Warning: Yellow (#f59e0b)
|
|
1825
|
+
- Danger: Red (#ef4444)
|
|
1826
|
+
|
|
1827
|
+
**Dark Mode:**
|
|
1828
|
+
- Primary: Light Blue (#60a5fa)
|
|
1829
|
+
- Success: Light Green (#34d399)
|
|
1830
|
+
- Warning: Light Yellow (#fbbf24)
|
|
1831
|
+
- Danger: Light Red (#f87171)
|
|
1832
|
+
|
|
1833
|
+
### Chart Types Used:
|
|
1834
|
+
|
|
1835
|
+
1. **Line Charts:** Throughput, time-series data
|
|
1836
|
+
2. **Bar Charts:** Queue comparisons, volume metrics
|
|
1837
|
+
3. **Doughnut Charts:** Message status distribution
|
|
1838
|
+
4. **Stacked Bar Charts:** Multi-metric comparisons
|
|
1839
|
+
|
|
1840
|
+
---
|
|
1841
|
+
|
|
1842
|
+
## Environment Configuration
|
|
1843
|
+
|
|
1844
|
+
`.env`:
|
|
1845
|
+
```
|
|
1846
|
+
VITE_API_BASE_URL=http://localhost:4000
|
|
1847
|
+
VITE_APP_TITLE=Queen Message Queue
|
|
1848
|
+
```
|
|
1849
|
+
|
|
1850
|
+
---
|
|
1851
|
+
|
|
1852
|
+
## Build & Deploy
|
|
1853
|
+
|
|
1854
|
+
`package.json` scripts:
|
|
1855
|
+
```json
|
|
1856
|
+
{
|
|
1857
|
+
"scripts": {
|
|
1858
|
+
"dev": "vite",
|
|
1859
|
+
"build": "vite build",
|
|
1860
|
+
"preview": "vite preview"
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
```
|
|
1864
|
+
|
|
1865
|
+
Build for production:
|
|
1866
|
+
```bash
|
|
1867
|
+
npm run build
|
|
1868
|
+
```
|
|
1869
|
+
|
|
1870
|
+
Output in `dist/` directory ready for deployment.
|
|
1871
|
+
|
|
1872
|
+
---
|
|
1873
|
+
|
|
1874
|
+
## Summary
|
|
1875
|
+
|
|
1876
|
+
This design creates a **professional, clean, and highly functional** web application for Queen Message Queue with:
|
|
1877
|
+
|
|
1878
|
+
✅ **Real API Integration** - Every component uses actual endpoints, no mocked data
|
|
1879
|
+
✅ **Beautiful UI** - Modern design with Tailwind, proper spacing, typography
|
|
1880
|
+
✅ **Dark Mode** - Full light/dark theme support
|
|
1881
|
+
✅ **Responsive** - Works on desktop and tablet
|
|
1882
|
+
✅ **Clear Navigation** - Intuitive sidebar with logical page organization
|
|
1883
|
+
✅ **Data Visualization** - Meaningful charts showing system health and activity
|
|
1884
|
+
✅ **Manual Refresh** - No auto-refresh, user-controlled data updates
|
|
1885
|
+
✅ **Clean Code** - Vue 3 Composition API, modular structure
|
|
1886
|
+
✅ **No Animations** - Minimal transitions, focus on content
|
|
1887
|
+
|
|
1888
|
+
The application provides complete visibility into the message queue system with intuitive interactions for managing queues, monitoring consumers, browsing messages, and analyzing system performance.
|
|
1889
|
+
|