queen-mq 0.1.0
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 +1116 -0
- package/CACHE.md +519 -0
- package/DASHBOARD-V3.md +478 -0
- package/DASHBOARD.md +382 -0
- package/MOD_QUEUE.md +453 -0
- package/PARTITION_LOCKING_DESIGN.md +989 -0
- package/PLAN.md +707 -0
- package/QUERY_ANALSYS.md +72 -0
- package/QUEUE_BUS.md +334 -0
- package/README.md +1495 -0
- package/V2-PLAN.md +236 -0
- package/assets/dashboard.png +0 -0
- package/dashboard/.vscode/extensions.json +3 -0
- package/dashboard/README.md +5 -0
- package/dashboard/index.html +14 -0
- package/dashboard/package-lock.json +1458 -0
- package/dashboard/package.json +25 -0
- package/dashboard/public/vite.svg +1 -0
- package/dashboard/src/App.vue +29 -0
- package/dashboard/src/assets/styles/main.css +908 -0
- package/dashboard/src/assets/vue.svg +1 -0
- package/dashboard/src/components/cards/MetricCard.vue +298 -0
- package/dashboard/src/components/charts/QueueDepthChart.vue +276 -0
- package/dashboard/src/components/charts/QueueLagChart.vue +436 -0
- package/dashboard/src/components/charts/ThroughputChart.vue +302 -0
- package/dashboard/src/components/common/ActivityFeed.vue +251 -0
- package/dashboard/src/components/layout/AppHeader.vue +208 -0
- package/dashboard/src/components/layout/AppLayout.vue +88 -0
- package/dashboard/src/components/layout/AppSidebar.vue +261 -0
- package/dashboard/src/main.js +44 -0
- package/dashboard/src/router.js +54 -0
- package/dashboard/src/services/api.js +187 -0
- package/dashboard/src/services/websocket.js +167 -0
- package/dashboard/src/utils/constants.js +56 -0
- package/dashboard/src/utils/helpers.js +118 -0
- package/dashboard/src/views/Analytics.vue +912 -0
- package/dashboard/src/views/Dashboard.vue +906 -0
- package/dashboard/src/views/Messages.vue +437 -0
- package/dashboard/src/views/QueueDetail.vue +501 -0
- package/dashboard/src/views/Queues.vue +333 -0
- package/dashboard/vite.config.js +30 -0
- package/debug-namespace.js +110 -0
- package/docs/long-polling.md +159 -0
- package/docs/multi-server-cache-solutions.md +185 -0
- package/docs/performance-tuning.md +222 -0
- package/examples/bus-mode.js +239 -0
- package/examples/continuous-consumer-optimized.js +215 -0
- package/examples/continuous-consumer.js +159 -0
- package/examples/continuous-producer.js +343 -0
- package/examples/mixed-mode.js +277 -0
- package/examples/multi-server-test.js +305 -0
- package/examples/single.js +64 -0
- package/examples/smartchat-dealyed.js +42 -0
- package/examples/smartchat.js +52 -0
- package/examples/test-cache-invalidation.js +119 -0
- package/examples/test-cache-multi-server.js +245 -0
- package/examples/test-minimal-client.js +112 -0
- package/examples/test-queue-creation-policy.js +137 -0
- package/init-db.js +20 -0
- package/package.json +36 -0
- package/src/client/client.js +291 -0
- package/src/client/index.js +6 -0
- package/src/client/queenClient.js +513 -0
- package/src/client/utils/http.js +172 -0
- package/src/client/utils/loadBalancer.js +152 -0
- package/src/client/utils/retry.js +35 -0
- package/src/config.js +215 -0
- package/src/database/connection.js +103 -0
- package/src/database/poolManager.js +192 -0
- package/src/database/schema-v2.sql +214 -0
- package/src/managers/eventManager.js +59 -0
- package/src/managers/queueManagerOptimized.js +1512 -0
- package/src/managers/resourceCache.js +96 -0
- package/src/managers/systemEventManager.js +127 -0
- package/src/routes/ack.js +26 -0
- package/src/routes/analytics.js +812 -0
- package/src/routes/configure.js +46 -0
- package/src/routes/messages.js +298 -0
- package/src/routes/pop.js +85 -0
- package/src/routes/push.js +28 -0
- package/src/routes/resources.js +296 -0
- package/src/server.js +1286 -0
- package/src/services/encryptionService.js +82 -0
- package/src/services/evictionService.js +131 -0
- package/src/services/retentionService.js +129 -0
- package/src/services/startupSync.js +35 -0
- package/src/test/test.js +4521 -0
- package/src/utils/logger.js +44 -0
- package/src/utils/uuid.js +5 -0
- package/src/websocket/wsServer.js +221 -0
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="dashboard">
|
|
3
|
+
<!-- Date/Time Filter Bar -->
|
|
4
|
+
<div class="filter-bar">
|
|
5
|
+
<div class="filter-group">
|
|
6
|
+
<label>Time Range:</label>
|
|
7
|
+
<Select
|
|
8
|
+
v-model="selectedTimeRange"
|
|
9
|
+
:options="timeRangeOptions"
|
|
10
|
+
optionLabel="label"
|
|
11
|
+
optionValue="value"
|
|
12
|
+
placeholder="Select time range"
|
|
13
|
+
@change="onTimeRangeChange"
|
|
14
|
+
/>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="filter-group" v-if="selectedTimeRange === 'custom'">
|
|
17
|
+
<label>From:</label>
|
|
18
|
+
<Calendar
|
|
19
|
+
v-model="fromDateTime"
|
|
20
|
+
showTime
|
|
21
|
+
:showIcon="true"
|
|
22
|
+
dateFormat="yy-mm-dd"
|
|
23
|
+
placeholder="Start date/time"
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="filter-group" v-if="selectedTimeRange === 'custom'">
|
|
27
|
+
<label>To:</label>
|
|
28
|
+
<Calendar
|
|
29
|
+
v-model="toDateTime"
|
|
30
|
+
showTime
|
|
31
|
+
:showIcon="true"
|
|
32
|
+
dateFormat="yy-mm-dd"
|
|
33
|
+
placeholder="End date/time"
|
|
34
|
+
/>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="filter-group">
|
|
37
|
+
<label>Queue:</label>
|
|
38
|
+
<Select
|
|
39
|
+
v-model="selectedQueue"
|
|
40
|
+
:options="queueOptions || []"
|
|
41
|
+
optionLabel="label"
|
|
42
|
+
optionValue="value"
|
|
43
|
+
placeholder="All queues"
|
|
44
|
+
:showClear="true"
|
|
45
|
+
@change="refreshData"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="filter-group">
|
|
49
|
+
<label>Namespace:</label>
|
|
50
|
+
<Select
|
|
51
|
+
v-model="selectedNamespace"
|
|
52
|
+
:options="namespaceOptions || []"
|
|
53
|
+
optionLabel="label"
|
|
54
|
+
optionValue="value"
|
|
55
|
+
placeholder="All namespaces"
|
|
56
|
+
:showClear="true"
|
|
57
|
+
@change="refreshData"
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="filter-group">
|
|
61
|
+
<Button
|
|
62
|
+
label="Apply Filters"
|
|
63
|
+
icon="pi pi-filter"
|
|
64
|
+
@click="refreshData"
|
|
65
|
+
:loading="loading"
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- Metric Cards -->
|
|
71
|
+
<div class="metrics-grid">
|
|
72
|
+
<MetricCard
|
|
73
|
+
title="Total Messages"
|
|
74
|
+
:value="metrics.totalMessages"
|
|
75
|
+
icon="pi pi-envelope"
|
|
76
|
+
color="primary"
|
|
77
|
+
:loading="loading"
|
|
78
|
+
:sparklineData="sparklineData.messages"
|
|
79
|
+
/>
|
|
80
|
+
<MetricCard
|
|
81
|
+
title="Pending"
|
|
82
|
+
:value="metrics.pending"
|
|
83
|
+
icon="pi pi-clock"
|
|
84
|
+
color="warning"
|
|
85
|
+
:loading="loading"
|
|
86
|
+
:sparklineData="sparklineData.pending"
|
|
87
|
+
/>
|
|
88
|
+
<MetricCard
|
|
89
|
+
title="Processing"
|
|
90
|
+
:value="metrics.processing"
|
|
91
|
+
icon="pi pi-spin pi-spinner"
|
|
92
|
+
color="info"
|
|
93
|
+
:loading="loading"
|
|
94
|
+
:sparklineData="sparklineData.processing"
|
|
95
|
+
/>
|
|
96
|
+
<MetricCard
|
|
97
|
+
title="Completed Today"
|
|
98
|
+
:value="metrics.completedToday"
|
|
99
|
+
icon="pi pi-check-circle"
|
|
100
|
+
color="success"
|
|
101
|
+
:loading="loading"
|
|
102
|
+
:sparklineData="sparklineData.completed"
|
|
103
|
+
/>
|
|
104
|
+
<MetricCard
|
|
105
|
+
title="Failed Today"
|
|
106
|
+
:value="metrics.failedToday"
|
|
107
|
+
icon="pi pi-times-circle"
|
|
108
|
+
color="danger"
|
|
109
|
+
:loading="loading"
|
|
110
|
+
:sparklineData="sparklineData.failed"
|
|
111
|
+
/>
|
|
112
|
+
<MetricCard
|
|
113
|
+
title="Dead Letter"
|
|
114
|
+
:value="metrics.deadLetter"
|
|
115
|
+
icon="pi pi-exclamation-triangle"
|
|
116
|
+
color="danger"
|
|
117
|
+
:loading="loading"
|
|
118
|
+
:sparklineData="sparklineData.deadLetter"
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<!-- Charts Row -->
|
|
123
|
+
<div class="charts-row">
|
|
124
|
+
<ThroughputChart
|
|
125
|
+
title="Message Throughput (Last Hour)"
|
|
126
|
+
:data="throughputData"
|
|
127
|
+
:loading="loading"
|
|
128
|
+
@refresh="refreshData"
|
|
129
|
+
/>
|
|
130
|
+
|
|
131
|
+
<QueueDepthChart
|
|
132
|
+
title="Queue Depths"
|
|
133
|
+
:data="queueDepthData"
|
|
134
|
+
:loading="loading"
|
|
135
|
+
@refresh="refreshData"
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<!-- Queue List and Activity Feed -->
|
|
140
|
+
<div class="bottom-row">
|
|
141
|
+
<div class="card-v3">
|
|
142
|
+
<div class="card-header">
|
|
143
|
+
<h3>Top Queues by Activity</h3>
|
|
144
|
+
<Button
|
|
145
|
+
label="View All"
|
|
146
|
+
class="btn-secondary"
|
|
147
|
+
@click="$router.push('/queues')"
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<DataTable
|
|
152
|
+
:value="topQueues"
|
|
153
|
+
:loading="loading"
|
|
154
|
+
responsiveLayout="scroll"
|
|
155
|
+
:rows="5"
|
|
156
|
+
class="dark-table-v3"
|
|
157
|
+
>
|
|
158
|
+
<Column field="name" header="Queue">
|
|
159
|
+
<template #body="{ data }">
|
|
160
|
+
<div class="queue-name-cell">
|
|
161
|
+
<div class="queue-icon">Q</div>
|
|
162
|
+
<span>{{ data.name }}</span>
|
|
163
|
+
</div>
|
|
164
|
+
</template>
|
|
165
|
+
</Column>
|
|
166
|
+
<Column field="namespace" header="Namespace">
|
|
167
|
+
<template #body="{ data }">
|
|
168
|
+
<Tag v-if="data.namespace" :value="data.namespace" class="namespace-tag" />
|
|
169
|
+
<span v-else class="text-muted">-</span>
|
|
170
|
+
</template>
|
|
171
|
+
</Column>
|
|
172
|
+
<Column field="pending" header="Pending">
|
|
173
|
+
<template #body="{ data }">
|
|
174
|
+
<span class="status-pending">{{ data.pending }}</span>
|
|
175
|
+
</template>
|
|
176
|
+
</Column>
|
|
177
|
+
<Column field="processing" header="Processing">
|
|
178
|
+
<template #body="{ data }">
|
|
179
|
+
<span class="status-processing">{{ data.processing }}</span>
|
|
180
|
+
</template>
|
|
181
|
+
</Column>
|
|
182
|
+
<Column field="throughput" header="Rate">
|
|
183
|
+
<template #body="{ data }">
|
|
184
|
+
<span class="rate-value">{{ data.throughput }} msg/min</span>
|
|
185
|
+
</template>
|
|
186
|
+
</Column>
|
|
187
|
+
<Column>
|
|
188
|
+
<template #body="{ data }">
|
|
189
|
+
<Button
|
|
190
|
+
icon="pi pi-eye"
|
|
191
|
+
class="p-button-text p-button-sm action-btn"
|
|
192
|
+
@click="$router.push(`/queues/${data.name}`)"
|
|
193
|
+
v-tooltip="'View Details'"
|
|
194
|
+
/>
|
|
195
|
+
</template>
|
|
196
|
+
</Column>
|
|
197
|
+
</DataTable>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div class="activity-feed">
|
|
201
|
+
<div class="activity-header">
|
|
202
|
+
<h3>Live Activity</h3>
|
|
203
|
+
<div class="live-indicator">
|
|
204
|
+
<span class="pulse-dot"></span>
|
|
205
|
+
<span>Live</span>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<ActivityFeed :events="activityEvents" />
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
</template>
|
|
214
|
+
|
|
215
|
+
<script setup>
|
|
216
|
+
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
|
217
|
+
import { useToast } from 'primevue/usetoast'
|
|
218
|
+
import Button from 'primevue/button'
|
|
219
|
+
import DataTable from 'primevue/datatable'
|
|
220
|
+
import Column from 'primevue/column'
|
|
221
|
+
import Tag from 'primevue/tag'
|
|
222
|
+
import Select from 'primevue/select'
|
|
223
|
+
import Calendar from 'primevue/calendar'
|
|
224
|
+
|
|
225
|
+
import MetricCard from '../components/cards/MetricCard.vue'
|
|
226
|
+
import ThroughputChart from '../components/charts/ThroughputChart.vue'
|
|
227
|
+
import QueueDepthChart from '../components/charts/QueueDepthChart.vue'
|
|
228
|
+
import ActivityFeed from '../components/common/ActivityFeed.vue'
|
|
229
|
+
|
|
230
|
+
import api from '../services/api.js'
|
|
231
|
+
import websocket from '../services/websocket.js'
|
|
232
|
+
import { formatNumber } from '../utils/helpers.js'
|
|
233
|
+
|
|
234
|
+
// Simple UUID generator for unique event IDs
|
|
235
|
+
const generateEventId = () => {
|
|
236
|
+
return Date.now().toString(36) + Math.random().toString(36).substr(2, 9)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const toast = useToast()
|
|
240
|
+
const loading = ref(false)
|
|
241
|
+
|
|
242
|
+
// Filter state
|
|
243
|
+
const selectedTimeRange = ref('1h')
|
|
244
|
+
const fromDateTime = ref(null)
|
|
245
|
+
const toDateTime = ref(null)
|
|
246
|
+
const selectedQueue = ref(null)
|
|
247
|
+
const selectedNamespace = ref(null)
|
|
248
|
+
const selectedTask = ref(null)
|
|
249
|
+
|
|
250
|
+
// Filter options
|
|
251
|
+
const timeRangeOptions = ref([
|
|
252
|
+
{ label: 'Last Hour', value: '1h' },
|
|
253
|
+
{ label: 'Last 6 Hours', value: '6h' },
|
|
254
|
+
{ label: 'Last 24 Hours', value: '24h' },
|
|
255
|
+
{ label: 'Last 7 Days', value: '7d' },
|
|
256
|
+
{ label: 'Last 30 Days', value: '30d' },
|
|
257
|
+
{ label: 'Custom Range', value: 'custom' }
|
|
258
|
+
])
|
|
259
|
+
|
|
260
|
+
const queueOptions = ref([])
|
|
261
|
+
const namespaceOptions = ref([])
|
|
262
|
+
|
|
263
|
+
// Metrics data
|
|
264
|
+
const metrics = ref({
|
|
265
|
+
totalMessages: 0,
|
|
266
|
+
pending: 0,
|
|
267
|
+
processing: 0,
|
|
268
|
+
completedToday: 0,
|
|
269
|
+
failedToday: 0,
|
|
270
|
+
deadLetter: 0
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// Sparkline data for metric cards
|
|
274
|
+
const sparklineData = ref({
|
|
275
|
+
messages: [],
|
|
276
|
+
pending: [],
|
|
277
|
+
processing: [],
|
|
278
|
+
completed: [],
|
|
279
|
+
failed: [],
|
|
280
|
+
deadLetter: []
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
// Chart data - Initialize with empty structure
|
|
284
|
+
const throughputData = ref({
|
|
285
|
+
labels: [],
|
|
286
|
+
datasets: [
|
|
287
|
+
{ label: 'Incoming', data: [] },
|
|
288
|
+
{ label: 'Completed', data: [] },
|
|
289
|
+
{ label: 'Failed', data: [] },
|
|
290
|
+
{ label: 'Dead Letter', data: [] }
|
|
291
|
+
]
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
const queueDepthData = ref({
|
|
295
|
+
labels: [],
|
|
296
|
+
datasets: [
|
|
297
|
+
{ label: 'Pending', data: [] },
|
|
298
|
+
{ label: 'Processing', data: [] }
|
|
299
|
+
]
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
// Queue distribution data
|
|
303
|
+
const queueDistribution = ref([])
|
|
304
|
+
|
|
305
|
+
// Table data
|
|
306
|
+
const topQueues = ref([])
|
|
307
|
+
|
|
308
|
+
// Activity feed
|
|
309
|
+
const activityEvents = ref([])
|
|
310
|
+
const maxActivityEvents = 20
|
|
311
|
+
|
|
312
|
+
// Generate random sparkline data (for demo)
|
|
313
|
+
const generateSparklineData = () => {
|
|
314
|
+
const generateArray = (base, variance) => {
|
|
315
|
+
return Array.from({ length: 10 }, () =>
|
|
316
|
+
base + Math.floor(Math.random() * variance - variance / 2)
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
sparklineData.value = {
|
|
321
|
+
messages: generateArray(1000, 200),
|
|
322
|
+
pending: generateArray(50, 20),
|
|
323
|
+
processing: generateArray(30, 10),
|
|
324
|
+
completed: generateArray(800, 150),
|
|
325
|
+
failed: generateArray(10, 5),
|
|
326
|
+
deadLetter: generateArray(5, 3)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Calculate queue distribution
|
|
331
|
+
const calculateQueueDistribution = (queues) => {
|
|
332
|
+
// Use mock data if no real queues
|
|
333
|
+
if (!queues || queues.length === 0) {
|
|
334
|
+
queues = [
|
|
335
|
+
{ name: 'email-queue', pending: 120, processing: 30 },
|
|
336
|
+
{ name: 'notification-queue', pending: 85, processing: 20 },
|
|
337
|
+
{ name: 'analytics-queue', pending: 200, processing: 45 },
|
|
338
|
+
{ name: 'payment-queue', pending: 45, processing: 10 },
|
|
339
|
+
{ name: 'report-queue', pending: 30, processing: 5 }
|
|
340
|
+
]
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const colors = ['#ec4899', '#10b981', '#f59e0b', '#3b82f6', '#8b5cf6', '#ef4444']
|
|
344
|
+
const total = queues.reduce((sum, q) => sum + (q.pending || 0) + (q.processing || 0), 0)
|
|
345
|
+
|
|
346
|
+
queueDistribution.value = queues
|
|
347
|
+
.slice(0, 6)
|
|
348
|
+
.map((queue, index) => {
|
|
349
|
+
const count = (queue.pending || 0) + (queue.processing || 0)
|
|
350
|
+
return {
|
|
351
|
+
name: queue.name,
|
|
352
|
+
count,
|
|
353
|
+
percentage: total > 0 ? Math.round((count / total) * 100) : 0,
|
|
354
|
+
color: colors[index % colors.length]
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
.filter(item => item.percentage > 0)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Fetch dashboard data
|
|
361
|
+
const fetchData = async () => {
|
|
362
|
+
try {
|
|
363
|
+
loading.value = true
|
|
364
|
+
|
|
365
|
+
// Build filter parameters
|
|
366
|
+
const filters = {}
|
|
367
|
+
|
|
368
|
+
// Handle time range
|
|
369
|
+
if (selectedTimeRange.value === 'custom') {
|
|
370
|
+
if (fromDateTime.value) filters.fromDateTime = fromDateTime.value.toISOString()
|
|
371
|
+
if (toDateTime.value) filters.toDateTime = toDateTime.value.toISOString()
|
|
372
|
+
} else {
|
|
373
|
+
// Calculate from/to based on selected range
|
|
374
|
+
const now = new Date()
|
|
375
|
+
const from = new Date()
|
|
376
|
+
|
|
377
|
+
switch (selectedTimeRange.value) {
|
|
378
|
+
case '1h':
|
|
379
|
+
from.setHours(from.getHours() - 1)
|
|
380
|
+
break
|
|
381
|
+
case '6h':
|
|
382
|
+
from.setHours(from.getHours() - 6)
|
|
383
|
+
break
|
|
384
|
+
case '24h':
|
|
385
|
+
from.setDate(from.getDate() - 1)
|
|
386
|
+
break
|
|
387
|
+
case '7d':
|
|
388
|
+
from.setDate(from.getDate() - 7)
|
|
389
|
+
break
|
|
390
|
+
case '30d':
|
|
391
|
+
from.setDate(from.getDate() - 30)
|
|
392
|
+
break
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
filters.fromDateTime = from.toISOString()
|
|
396
|
+
filters.toDateTime = now.toISOString()
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Add other filters
|
|
400
|
+
if (selectedQueue.value) filters.queue = selectedQueue.value
|
|
401
|
+
if (selectedNamespace.value) filters.namespace = selectedNamespace.value
|
|
402
|
+
if (selectedTask.value) filters.task = selectedTask.value
|
|
403
|
+
|
|
404
|
+
// Fetch all data in parallel with filters
|
|
405
|
+
const [overview, throughput, depths, queues] = await Promise.all([
|
|
406
|
+
api.getSystemOverview(),
|
|
407
|
+
api.getThroughput(filters),
|
|
408
|
+
api.getQueueDepths(filters),
|
|
409
|
+
api.getQueues(filters)
|
|
410
|
+
])
|
|
411
|
+
|
|
412
|
+
// Update metrics
|
|
413
|
+
if (overview) {
|
|
414
|
+
metrics.value = {
|
|
415
|
+
totalMessages: overview.messages?.total || 0,
|
|
416
|
+
pending: overview.messages?.pending || 0,
|
|
417
|
+
processing: overview.messages?.processing || 0,
|
|
418
|
+
completedToday: overview.messages?.completed || 0,
|
|
419
|
+
failedToday: overview.messages?.failed || 0,
|
|
420
|
+
deadLetter: overview.messages?.deadLetter || 0
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Process throughput data for chart - always process to show mock data if empty
|
|
425
|
+
processThroughputData(throughput?.throughput || [])
|
|
426
|
+
|
|
427
|
+
// Process queue depths for chart - always process to show mock data if empty
|
|
428
|
+
processQueueDepthData(depths?.depths || [])
|
|
429
|
+
|
|
430
|
+
// Process top queues and distribution - always process to show mock data if empty
|
|
431
|
+
processTopQueues(queues?.queues || [])
|
|
432
|
+
calculateQueueDistribution(queues?.queues || [])
|
|
433
|
+
|
|
434
|
+
// Generate sparkline data
|
|
435
|
+
generateSparklineData()
|
|
436
|
+
|
|
437
|
+
} catch (error) {
|
|
438
|
+
console.error('Failed to fetch dashboard data:', error)
|
|
439
|
+
toast.add({
|
|
440
|
+
severity: 'error',
|
|
441
|
+
summary: 'Error',
|
|
442
|
+
detail: 'Failed to load dashboard data',
|
|
443
|
+
life: 3000
|
|
444
|
+
})
|
|
445
|
+
} finally {
|
|
446
|
+
loading.value = false
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Process throughput data for chart
|
|
451
|
+
const processThroughputData = (data) => {
|
|
452
|
+
// Use empty array if no data available
|
|
453
|
+
if (!data || data.length === 0) {
|
|
454
|
+
data = []
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Take last 12 data points for hourly view and reverse to show chronological order (oldest to newest)
|
|
458
|
+
const recentData = data.reverse().slice(-100)
|
|
459
|
+
|
|
460
|
+
throughputData.value = {
|
|
461
|
+
labels: recentData.map(item => {
|
|
462
|
+
const date = new Date(item.timestamp)
|
|
463
|
+
return date.toLocaleTimeString('en-US', {
|
|
464
|
+
hour: 'numeric',
|
|
465
|
+
minute: '2-digit'
|
|
466
|
+
})
|
|
467
|
+
}),
|
|
468
|
+
datasets: [
|
|
469
|
+
{
|
|
470
|
+
label: 'Incoming Messages',
|
|
471
|
+
data: recentData.map(item => item.incoming?.messagesPerMinute || 0)
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
label: 'Completed Messages',
|
|
475
|
+
data: recentData.map(item => item.completed?.messagesPerMinute || 0)
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
label: 'Failed Messages',
|
|
479
|
+
data: recentData.map(item => item.failed?.messagesPerMinute || 0)
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
label: 'Dead Letter Messages',
|
|
483
|
+
data: recentData.map(item => item.deadLetter?.messagesPerMinute || 0)
|
|
484
|
+
}
|
|
485
|
+
]
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Process queue depth data for chart
|
|
490
|
+
const processQueueDepthData = (data) => {
|
|
491
|
+
// Use empty array if no data available
|
|
492
|
+
if (!data || data.length === 0) {
|
|
493
|
+
data = []
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Filter out queues with no messages and sort by total messages, take top 8
|
|
497
|
+
const filtered = data.filter(item => (item.depth || 0) + (item.processing || 0) > 0)
|
|
498
|
+
const sorted = [...filtered]
|
|
499
|
+
.sort((a, b) => (b.depth + b.processing) - (a.depth + a.processing))
|
|
500
|
+
.slice(0, 8)
|
|
501
|
+
|
|
502
|
+
queueDepthData.value = {
|
|
503
|
+
labels: sorted.map(item => item.queue),
|
|
504
|
+
datasets: [
|
|
505
|
+
{
|
|
506
|
+
label: 'Pending',
|
|
507
|
+
data: sorted.map(item => item.depth || 0)
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
label: 'Processing',
|
|
511
|
+
data: sorted.map(item => item.processing || 0)
|
|
512
|
+
}
|
|
513
|
+
]
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Process top queues for table
|
|
518
|
+
const processTopQueues = (queues) => {
|
|
519
|
+
// Use empty array if no real queues
|
|
520
|
+
if (!queues || queues.length === 0) {
|
|
521
|
+
queues = []
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
topQueues.value = queues
|
|
525
|
+
.map(queue => ({
|
|
526
|
+
name: queue.name,
|
|
527
|
+
namespace: queue.namespace,
|
|
528
|
+
pending: queue.stats?.pending || 0,
|
|
529
|
+
processing: queue.stats?.processing || 0,
|
|
530
|
+
throughput: 0 // No throughput data available
|
|
531
|
+
}))
|
|
532
|
+
.sort((a, b) => (b.pending + b.processing) - (a.pending + a.processing))
|
|
533
|
+
.slice(0, 5)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Handle WebSocket events
|
|
537
|
+
const handleWebSocketEvent = (wsMessage) => {
|
|
538
|
+
// WebSocket message format: { event: "message.pushed", data: {...}, timestamp: "..." }
|
|
539
|
+
// ActivityFeed expects: { event: "message.pushed", data: {...}, timestamp: Date }
|
|
540
|
+
|
|
541
|
+
// Add to activity feed in the format ActivityFeed expects
|
|
542
|
+
activityEvents.value.unshift({
|
|
543
|
+
id: generateEventId(), // Use unique ID generator to avoid duplicate keys
|
|
544
|
+
event: wsMessage.event || 'unknown',
|
|
545
|
+
data: wsMessage.data || {},
|
|
546
|
+
timestamp: wsMessage.timestamp ? new Date(wsMessage.timestamp) : new Date()
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
// Limit activity events
|
|
550
|
+
if (activityEvents.value.length > maxActivityEvents) {
|
|
551
|
+
activityEvents.value = activityEvents.value.slice(0, maxActivityEvents)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Update metrics based on event type
|
|
555
|
+
const eventType = wsMessage.event
|
|
556
|
+
if (eventType === 'message.pushed') {
|
|
557
|
+
metrics.value.pending++
|
|
558
|
+
metrics.value.totalMessages++
|
|
559
|
+
} else if (eventType === 'message.popped') {
|
|
560
|
+
metrics.value.pending = Math.max(0, metrics.value.pending - 1)
|
|
561
|
+
metrics.value.processing++
|
|
562
|
+
} else if (eventType === 'message.completed' || eventType === 'message.acknowledged') {
|
|
563
|
+
metrics.value.processing = Math.max(0, metrics.value.processing - 1)
|
|
564
|
+
metrics.value.completedToday++
|
|
565
|
+
} else if (eventType === 'message.failed') {
|
|
566
|
+
metrics.value.processing = Math.max(0, metrics.value.processing - 1)
|
|
567
|
+
metrics.value.failedToday++
|
|
568
|
+
} else if (eventType === 'message.dead_letter') {
|
|
569
|
+
metrics.value.deadLetter++
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Refresh data
|
|
574
|
+
const refreshData = () => {
|
|
575
|
+
fetchData()
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Lifecycle
|
|
579
|
+
// Function to handle time range change
|
|
580
|
+
const onTimeRangeChange = () => {
|
|
581
|
+
if (selectedTimeRange.value !== 'custom') {
|
|
582
|
+
fromDateTime.value = null
|
|
583
|
+
toDateTime.value = null
|
|
584
|
+
refreshData()
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Function to load filter options
|
|
589
|
+
const loadFilterOptions = async () => {
|
|
590
|
+
try {
|
|
591
|
+
const [queues, namespaces] = await Promise.all([
|
|
592
|
+
api.getQueues(),
|
|
593
|
+
api.getNamespaces()
|
|
594
|
+
])
|
|
595
|
+
|
|
596
|
+
// Format queue options
|
|
597
|
+
if (queues?.queues && Array.isArray(queues.queues)) {
|
|
598
|
+
queueOptions.value = queues.queues.map(q => ({
|
|
599
|
+
label: q.queue || 'Unknown',
|
|
600
|
+
value: q.queue || ''
|
|
601
|
+
}))
|
|
602
|
+
} else {
|
|
603
|
+
queueOptions.value = []
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Format namespace options - handle both array and object responses
|
|
607
|
+
if (namespaces) {
|
|
608
|
+
const namespaceList = Array.isArray(namespaces) ? namespaces : (namespaces.namespaces || [])
|
|
609
|
+
if (Array.isArray(namespaceList)) {
|
|
610
|
+
namespaceOptions.value = namespaceList.map(ns => ({
|
|
611
|
+
label: ns.namespace || 'Default',
|
|
612
|
+
value: ns.namespace || ''
|
|
613
|
+
}))
|
|
614
|
+
} else {
|
|
615
|
+
namespaceOptions.value = []
|
|
616
|
+
}
|
|
617
|
+
} else {
|
|
618
|
+
namespaceOptions.value = []
|
|
619
|
+
}
|
|
620
|
+
} catch (error) {
|
|
621
|
+
console.error('Failed to load filter options:', error)
|
|
622
|
+
// Ensure arrays are set even on error
|
|
623
|
+
queueOptions.value = []
|
|
624
|
+
namespaceOptions.value = []
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
onMounted(() => {
|
|
629
|
+
loadFilterOptions()
|
|
630
|
+
fetchData()
|
|
631
|
+
|
|
632
|
+
// Subscribe to WebSocket events
|
|
633
|
+
websocket.on('message', handleWebSocketEvent)
|
|
634
|
+
|
|
635
|
+
// Auto-refresh every 30 seconds
|
|
636
|
+
const refreshInterval = setInterval(() => {
|
|
637
|
+
fetchData()
|
|
638
|
+
}, 30000)
|
|
639
|
+
|
|
640
|
+
// Store interval ID for cleanup
|
|
641
|
+
window.dashboardRefreshInterval = refreshInterval
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
onUnmounted(() => {
|
|
645
|
+
// Clean up WebSocket listener
|
|
646
|
+
websocket.off('message', handleWebSocketEvent)
|
|
647
|
+
|
|
648
|
+
// Clear refresh interval
|
|
649
|
+
if (window.dashboardRefreshInterval) {
|
|
650
|
+
clearInterval(window.dashboardRefreshInterval)
|
|
651
|
+
}
|
|
652
|
+
})
|
|
653
|
+
</script>
|
|
654
|
+
|
|
655
|
+
<style scoped>
|
|
656
|
+
.dashboard {
|
|
657
|
+
padding: 0;
|
|
658
|
+
width: 100%;
|
|
659
|
+
max-width: 1600px;
|
|
660
|
+
margin: 0 auto;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/* Filter Bar */
|
|
664
|
+
.filter-bar {
|
|
665
|
+
display: flex;
|
|
666
|
+
gap: 1rem;
|
|
667
|
+
padding: 1rem;
|
|
668
|
+
background: linear-gradient(135deg, #1e1e2e 0%, #2a2a3e 100%);
|
|
669
|
+
border-radius: 12px;
|
|
670
|
+
margin-bottom: 1.5rem;
|
|
671
|
+
flex-wrap: wrap;
|
|
672
|
+
align-items: flex-end;
|
|
673
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
.filter-group {
|
|
677
|
+
display: flex;
|
|
678
|
+
flex-direction: column;
|
|
679
|
+
gap: 0.5rem;
|
|
680
|
+
min-width: 150px;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.filter-group label {
|
|
684
|
+
font-size: 0.875rem;
|
|
685
|
+
color: #a0a0a0;
|
|
686
|
+
font-weight: 500;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.filter-group :deep(.p-select),
|
|
690
|
+
.filter-group :deep(.p-calendar) {
|
|
691
|
+
background: rgba(255, 255, 255, 0.05);
|
|
692
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
693
|
+
border-radius: 8px;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
.filter-group :deep(.p-select:hover),
|
|
697
|
+
.filter-group :deep(.p-calendar:hover) {
|
|
698
|
+
border-color: rgba(236, 72, 153, 0.5);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.filter-group :deep(.p-select-label),
|
|
702
|
+
.filter-group :deep(.p-inputtext) {
|
|
703
|
+
color: #ffffff;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.filter-group :deep(.p-button) {
|
|
707
|
+
background: linear-gradient(135deg, #ec4899 0%, #8b5cf6 100%);
|
|
708
|
+
border: none;
|
|
709
|
+
border-radius: 8px;
|
|
710
|
+
padding: 0.75rem 1.5rem;
|
|
711
|
+
font-weight: 500;
|
|
712
|
+
transition: all 0.3s ease;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.filter-group :deep(.p-button:hover) {
|
|
716
|
+
transform: translateY(-2px);
|
|
717
|
+
box-shadow: 0 4px 12px rgba(236, 72, 153, 0.3);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/* Header removed - no longer needed */
|
|
721
|
+
|
|
722
|
+
.metrics-grid {
|
|
723
|
+
display: grid;
|
|
724
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
725
|
+
gap: 1rem;
|
|
726
|
+
margin-bottom: 1.5rem;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.charts-row {
|
|
730
|
+
display: grid;
|
|
731
|
+
grid-template-columns: 1fr 1fr;
|
|
732
|
+
gap: 1.5rem;
|
|
733
|
+
margin-bottom: 1.5rem;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.bottom-row {
|
|
737
|
+
display: grid;
|
|
738
|
+
grid-template-columns: 1.5fr 1fr;
|
|
739
|
+
gap: 1.5rem;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.card-v3 {
|
|
743
|
+
background: transparent;
|
|
744
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
745
|
+
border-radius: 16px;
|
|
746
|
+
padding: 1.5rem;
|
|
747
|
+
position: relative;
|
|
748
|
+
overflow: hidden;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.card-header {
|
|
752
|
+
display: flex;
|
|
753
|
+
justify-content: space-between;
|
|
754
|
+
align-items: center;
|
|
755
|
+
margin-bottom: 1.5rem;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.card-header h3 {
|
|
759
|
+
font-size: 1.125rem;
|
|
760
|
+
font-weight: 600;
|
|
761
|
+
color: var(--surface-700);
|
|
762
|
+
margin: 0;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/* Queue table styling */
|
|
766
|
+
.queue-name-cell {
|
|
767
|
+
display: flex;
|
|
768
|
+
align-items: center;
|
|
769
|
+
gap: 0.75rem;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
.queue-icon {
|
|
773
|
+
width: 32px;
|
|
774
|
+
height: 32px;
|
|
775
|
+
border-radius: 8px;
|
|
776
|
+
background: var(--gradient-primary);
|
|
777
|
+
display: flex;
|
|
778
|
+
align-items: center;
|
|
779
|
+
justify-content: center;
|
|
780
|
+
color: white;
|
|
781
|
+
font-weight: 600;
|
|
782
|
+
font-size: 0.875rem;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
.namespace-tag {
|
|
786
|
+
background: rgba(236, 72, 153, 0.15);
|
|
787
|
+
color: var(--primary-500);
|
|
788
|
+
border: 1px solid rgba(236, 72, 153, 0.3);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
.rate-value {
|
|
792
|
+
color: var(--primary-500);
|
|
793
|
+
font-weight: 600;
|
|
794
|
+
font-size: 0.875rem;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
.action-btn {
|
|
798
|
+
color: var(--surface-500) !important;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
.action-btn:hover {
|
|
802
|
+
background: rgba(236, 72, 153, 0.1) !important;
|
|
803
|
+
color: var(--primary-500) !important;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/* Activity feed styling */
|
|
807
|
+
.activity-feed {
|
|
808
|
+
background: transparent;
|
|
809
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
810
|
+
border-radius: 16px;
|
|
811
|
+
padding: 1.5rem;
|
|
812
|
+
max-height: 500px;
|
|
813
|
+
overflow: hidden;
|
|
814
|
+
display: flex;
|
|
815
|
+
flex-direction: column;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
.activity-header {
|
|
819
|
+
display: flex;
|
|
820
|
+
justify-content: space-between;
|
|
821
|
+
align-items: center;
|
|
822
|
+
margin-bottom: 1.5rem;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
.activity-header h3 {
|
|
826
|
+
font-size: 1.125rem;
|
|
827
|
+
font-weight: 600;
|
|
828
|
+
color: var(--surface-700);
|
|
829
|
+
margin: 0;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.live-indicator {
|
|
833
|
+
display: flex;
|
|
834
|
+
align-items: center;
|
|
835
|
+
gap: 0.5rem;
|
|
836
|
+
color: var(--success-color);
|
|
837
|
+
font-size: 0.875rem;
|
|
838
|
+
font-weight: 500;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
.pulse-dot {
|
|
842
|
+
width: 8px;
|
|
843
|
+
height: 8px;
|
|
844
|
+
border-radius: 50%;
|
|
845
|
+
background: var(--success-color);
|
|
846
|
+
animation: pulse 2s infinite;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/* Distribution card is already styled in main.css */
|
|
850
|
+
|
|
851
|
+
/* Override DataTable styles for dark theme */
|
|
852
|
+
:deep(.dark-table-v3) {
|
|
853
|
+
background: transparent !important;
|
|
854
|
+
border: none !important;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
:deep(.dark-table-v3 .p-datatable-thead > tr > th) {
|
|
858
|
+
background: var(--surface-0) !important;
|
|
859
|
+
color: var(--surface-400) !important;
|
|
860
|
+
border-color: rgba(255, 255, 255, 0.05) !important;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
:deep(.dark-table-v3 .p-datatable-tbody > tr) {
|
|
864
|
+
background: transparent !important;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
:deep(.dark-table-v3 .p-datatable-tbody > tr:hover) {
|
|
868
|
+
background: rgba(236, 72, 153, 0.05) !important;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
:deep(.dark-table-v3 .p-datatable-tbody > tr > td) {
|
|
872
|
+
color: var(--surface-600) !important;
|
|
873
|
+
border-color: rgba(255, 255, 255, 0.03) !important;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/* Responsive */
|
|
877
|
+
@media (max-width: 1024px) {
|
|
878
|
+
.charts-row {
|
|
879
|
+
grid-template-columns: 1fr;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
.bottom-row {
|
|
883
|
+
grid-template-columns: 1fr;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
.metrics-grid {
|
|
887
|
+
grid-template-columns: repeat(2, 1fr);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
@media (max-width: 768px) {
|
|
892
|
+
.dashboard-header {
|
|
893
|
+
flex-direction: column;
|
|
894
|
+
align-items: flex-start;
|
|
895
|
+
gap: 1rem;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
.metrics-grid {
|
|
899
|
+
grid-template-columns: 1fr;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
.activity-feed {
|
|
903
|
+
max-height: 400px;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
</style>
|