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
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="page-flat">
|
|
3
|
+
<div class="py-4 px-3">
|
|
4
|
+
<div class="space-y-2.5">
|
|
5
|
+
<LoadingSpinner v-if="loading && !queueData" />
|
|
6
|
+
|
|
7
|
+
<div v-else-if="error" class="error-flat">
|
|
8
|
+
<p><strong>Error loading queue:</strong> {{ error }}</p>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<template v-else-if="queueData">
|
|
12
|
+
<!-- Queue Header Card -->
|
|
13
|
+
<div class="header-section">
|
|
14
|
+
<div class="flex items-center justify-between mb-2">
|
|
15
|
+
<div class="flex items-center gap-3">
|
|
16
|
+
<button @click="goBack" class="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition-colors">
|
|
17
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
18
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
|
19
|
+
</svg>
|
|
20
|
+
</button>
|
|
21
|
+
<h2 class="text-xl font-bold gradient-text">{{ statusData?.queue?.name }}</h2>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="flex gap-2">
|
|
25
|
+
<button @click="showPushModal = true" class="btn btn-primary">
|
|
26
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
27
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
28
|
+
</svg>
|
|
29
|
+
Push Message
|
|
30
|
+
</button>
|
|
31
|
+
<button @click="confirmClear" class="btn btn-secondary">
|
|
32
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
33
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
34
|
+
</svg>
|
|
35
|
+
Clear Queue
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="flex flex-wrap gap-x-6 gap-y-1 text-sm text-gray-600 dark:text-gray-400 mb-1">
|
|
41
|
+
<span><span class="text-gray-500 dark:text-gray-500">Namespace:</span> {{ statusData?.queue?.namespace || '-' }}</span>
|
|
42
|
+
<span><span class="text-gray-500 dark:text-gray-500">Task:</span> {{ statusData?.queue?.task || '-' }}</span>
|
|
43
|
+
<span><span class="text-gray-500 dark:text-gray-500">Priority:</span> {{ statusData?.queue?.priority || 0 }}</span>
|
|
44
|
+
<span class="text-xs text-gray-500">Created: {{ formatDate(statusData?.queue?.createdAt) }}</span>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<!-- Status Metrics -->
|
|
49
|
+
<div class="grid grid-cols-2 lg:grid-cols-4 gap-2">
|
|
50
|
+
<div class="metric-flat">
|
|
51
|
+
<div class="flex items-start gap-3">
|
|
52
|
+
<div class="metric-icon-flat bg-yellow-500/10 dark:bg-yellow-500/20">
|
|
53
|
+
<svg class="w-5 h-5 text-yellow-600 dark:text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
54
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
55
|
+
</svg>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="flex-1">
|
|
58
|
+
<p class="metric-label">Pending</p>
|
|
59
|
+
<p class="metric-value-flat">{{ formatNumber(calculatedPending) }}</p>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="metric-flat">
|
|
64
|
+
<div class="flex items-start gap-3">
|
|
65
|
+
<div class="metric-icon-flat bg-purple-500/10 dark:bg-purple-500/20">
|
|
66
|
+
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
67
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4v.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
68
|
+
</svg>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="flex-1">
|
|
71
|
+
<p class="metric-label">Processing</p>
|
|
72
|
+
<p class="metric-value-flat">{{ formatNumber(statusData?.totals?.messages?.processing || 0) }}</p>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="metric-flat">
|
|
77
|
+
<div class="flex items-start gap-3">
|
|
78
|
+
<div class="metric-icon-flat bg-green-500/10 dark:bg-green-500/20">
|
|
79
|
+
<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
80
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
81
|
+
</svg>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="flex-1">
|
|
84
|
+
<p class="metric-label">Completed</p>
|
|
85
|
+
<p class="metric-value-flat text-green-600 dark:text-green-400">{{ formatNumber(statusData?.totals?.messages?.completed || 0) }}</p>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="metric-flat">
|
|
90
|
+
<div class="flex items-start gap-3">
|
|
91
|
+
<div class="metric-icon-flat bg-red-500/10 dark:bg-red-500/20">
|
|
92
|
+
<svg class="w-5 h-5 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
93
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
94
|
+
</svg>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="flex-1">
|
|
97
|
+
<p class="metric-label">Failed</p>
|
|
98
|
+
<p class="metric-value-flat text-red-600 dark:text-red-400">{{ formatNumber(statusData?.totals?.messages?.failed || 0) }}</p>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<!-- Queue Configuration -->
|
|
105
|
+
<div class="config-section">
|
|
106
|
+
<div class="flex items-center gap-2 mb-3">
|
|
107
|
+
<div class="w-7 h-7 rounded-lg bg-gradient-to-br from-blue-500/10 to-cyan-500/20 flex items-center justify-center">
|
|
108
|
+
<svg class="w-4 h-4 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
109
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
|
110
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
111
|
+
</svg>
|
|
112
|
+
</div>
|
|
113
|
+
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Queue Configuration</h3>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="grid grid-cols-2 md:grid-cols-3 gap-x-6 gap-y-3 text-sm">
|
|
116
|
+
<div>
|
|
117
|
+
<span class="text-gray-500 dark:text-gray-400 block mb-0.5">Lease Time</span>
|
|
118
|
+
<span class="font-semibold">{{ statusData?.queue?.config?.leaseTime || 0 }}s</span>
|
|
119
|
+
</div>
|
|
120
|
+
<div>
|
|
121
|
+
<span class="text-gray-500 dark:text-gray-400 block mb-0.5">TTL</span>
|
|
122
|
+
<span class="font-semibold">{{ formatDuration((statusData?.queue?.config?.ttl || 0) * 1000) }}</span>
|
|
123
|
+
</div>
|
|
124
|
+
<div>
|
|
125
|
+
<span class="text-gray-500 dark:text-gray-400 block mb-0.5">Max Queue Size</span>
|
|
126
|
+
<span class="font-semibold">{{ formatNumber(statusData?.queue?.config?.maxQueueSize || 0) }}</span>
|
|
127
|
+
</div>
|
|
128
|
+
<div>
|
|
129
|
+
<span class="text-gray-500 dark:text-gray-400 block mb-0.5">Retry Limit</span>
|
|
130
|
+
<span class="font-semibold">{{ statusData?.queue?.config?.retryLimit || 0 }}</span>
|
|
131
|
+
</div>
|
|
132
|
+
<div>
|
|
133
|
+
<span class="text-gray-500 dark:text-gray-400 block mb-0.5">Retry Delay</span>
|
|
134
|
+
<span class="font-semibold">{{ statusData?.queue?.config?.retryDelay || 0 }}ms</span>
|
|
135
|
+
</div>
|
|
136
|
+
<div>
|
|
137
|
+
<span class="text-gray-500 dark:text-gray-400 block mb-0.5">DLQ Enabled</span>
|
|
138
|
+
<span class="font-semibold">{{ statusData?.queue?.config?.deadLetterQueue ? 'Yes' : 'No' }}</span>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<!-- Partitions Table -->
|
|
144
|
+
<div class="partition-section">
|
|
145
|
+
<div class="flex items-center justify-between mb-2">
|
|
146
|
+
<div class="flex items-center gap-2">
|
|
147
|
+
<div class="w-7 h-7 rounded-lg bg-gradient-to-br from-indigo-500/10 to-violet-500/20 flex items-center justify-center">
|
|
148
|
+
<svg class="w-4 h-4 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
149
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
|
150
|
+
</svg>
|
|
151
|
+
</div>
|
|
152
|
+
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Partitions</h3>
|
|
153
|
+
</div>
|
|
154
|
+
<input
|
|
155
|
+
v-model="partitionSearch"
|
|
156
|
+
type="text"
|
|
157
|
+
placeholder="Search partitions..."
|
|
158
|
+
class="input w-48"
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div class="table-container scrollbar-thin">
|
|
163
|
+
<table class="table">
|
|
164
|
+
<thead>
|
|
165
|
+
<tr>
|
|
166
|
+
<th @click="sortPartitions('name')" class="cursor-pointer hover:text-rose-600 dark:hover:text-rose-400 transition-colors">
|
|
167
|
+
<div class="flex items-center gap-1">
|
|
168
|
+
Partition
|
|
169
|
+
<svg class="w-3 h-3 transition-transform" :class="getPartitionSortClass('name')" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
170
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
171
|
+
</svg>
|
|
172
|
+
</div>
|
|
173
|
+
</th>
|
|
174
|
+
<th @click="sortPartitions('pending')" class="text-right cursor-pointer hover:text-rose-600 dark:hover:text-rose-400 transition-colors">
|
|
175
|
+
<div class="flex items-center justify-end gap-1">
|
|
176
|
+
Pending
|
|
177
|
+
<svg class="w-3 h-3 transition-transform" :class="getPartitionSortClass('pending')" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
178
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
179
|
+
</svg>
|
|
180
|
+
</div>
|
|
181
|
+
</th>
|
|
182
|
+
<th @click="sortPartitions('processing')" class="text-right cursor-pointer hover:text-rose-600 dark:hover:text-rose-400 transition-colors">
|
|
183
|
+
<div class="flex items-center justify-end gap-1">
|
|
184
|
+
Processing
|
|
185
|
+
<svg class="w-3 h-3 transition-transform" :class="getPartitionSortClass('processing')" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
186
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
187
|
+
</svg>
|
|
188
|
+
</div>
|
|
189
|
+
</th>
|
|
190
|
+
<th @click="sortPartitions('completed')" class="text-right cursor-pointer hover:text-rose-600 dark:hover:text-rose-400 transition-colors">
|
|
191
|
+
<div class="flex items-center justify-end gap-1">
|
|
192
|
+
Completed
|
|
193
|
+
<svg class="w-3 h-3 transition-transform" :class="getPartitionSortClass('completed')" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
194
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
195
|
+
</svg>
|
|
196
|
+
</div>
|
|
197
|
+
</th>
|
|
198
|
+
<th @click="sortPartitions('failed')" class="text-right cursor-pointer hover:text-rose-600 dark:hover:text-rose-400 transition-colors">
|
|
199
|
+
<div class="flex items-center justify-end gap-1">
|
|
200
|
+
Failed
|
|
201
|
+
<svg class="w-3 h-3 transition-transform" :class="getPartitionSortClass('failed')" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
202
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
203
|
+
</svg>
|
|
204
|
+
</div>
|
|
205
|
+
</th>
|
|
206
|
+
<th class="text-right">Consumed</th>
|
|
207
|
+
</tr>
|
|
208
|
+
</thead>
|
|
209
|
+
<tbody>
|
|
210
|
+
<tr v-for="partition in sortedPartitions" :key="partition.id">
|
|
211
|
+
<td>
|
|
212
|
+
<div class="font-medium">{{ partition.name }}</div>
|
|
213
|
+
<div v-if="partition.lastActivity" class="text-xs text-gray-500 dark:text-gray-400">
|
|
214
|
+
Last: {{ formatTime(partition.lastActivity) }}
|
|
215
|
+
</div>
|
|
216
|
+
</td>
|
|
217
|
+
<td class="text-right font-medium">{{ formatNumber(partition.messages?.pending || 0) }}</td>
|
|
218
|
+
<td class="text-right font-medium">{{ formatNumber(partition.messages?.processing || 0) }}</td>
|
|
219
|
+
<td class="text-right font-medium text-green-600 dark:text-green-400">{{ formatNumber(partition.messages?.completed || 0) }}</td>
|
|
220
|
+
<td class="text-right font-medium text-red-600 dark:text-red-400">{{ formatNumber(partition.messages?.failed || 0) }}</td>
|
|
221
|
+
<td class="text-right">
|
|
222
|
+
<div class="text-sm">{{ formatNumber(partition.cursor?.totalConsumed || 0) }}</div>
|
|
223
|
+
<div class="text-xs text-gray-500 dark:text-gray-400">
|
|
224
|
+
{{ partition.cursor?.batchesConsumed || 0 }} batches
|
|
225
|
+
</div>
|
|
226
|
+
</td>
|
|
227
|
+
</tr>
|
|
228
|
+
</tbody>
|
|
229
|
+
</table>
|
|
230
|
+
|
|
231
|
+
<div v-if="!sortedPartitions.length" class="text-center py-8 text-gray-500 text-sm">
|
|
232
|
+
No partitions found
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</template>
|
|
237
|
+
|
|
238
|
+
<!-- Push Message Modal -->
|
|
239
|
+
<PushMessageModal
|
|
240
|
+
:is-open="showPushModal"
|
|
241
|
+
:queue-name="queueName"
|
|
242
|
+
@close="showPushModal = false"
|
|
243
|
+
@pushed="onMessagePushed"
|
|
244
|
+
/>
|
|
245
|
+
|
|
246
|
+
<!-- Clear Queue Confirmation -->
|
|
247
|
+
<ConfirmDialog
|
|
248
|
+
:is-open="showClearConfirm"
|
|
249
|
+
title="Clear Queue"
|
|
250
|
+
:message="`Are you sure you want to clear all messages from '${queueName}'? This action cannot be undone.`"
|
|
251
|
+
confirm-text="Clear Queue"
|
|
252
|
+
confirm-class="btn-danger"
|
|
253
|
+
@confirm="clearQueue"
|
|
254
|
+
@cancel="showClearConfirm = false"
|
|
255
|
+
/>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</template>
|
|
260
|
+
|
|
261
|
+
<script setup>
|
|
262
|
+
import { ref, computed, onMounted } from 'vue';
|
|
263
|
+
import { useRoute, useRouter } from 'vue-router';
|
|
264
|
+
import { queuesApi } from '../api/queues';
|
|
265
|
+
import { analyticsApi } from '../api/analytics';
|
|
266
|
+
import { formatNumber, formatDate, formatTime, formatDuration } from '../utils/formatters';
|
|
267
|
+
|
|
268
|
+
import LoadingSpinner from '../components/common/LoadingSpinner.vue';
|
|
269
|
+
import ConfirmDialog from '../components/common/ConfirmDialog.vue';
|
|
270
|
+
import PushMessageModal from '../components/queue-detail/PushMessageModal.vue';
|
|
271
|
+
|
|
272
|
+
const route = useRoute();
|
|
273
|
+
const router = useRouter();
|
|
274
|
+
const queueName = computed(() => route.params.queueName);
|
|
275
|
+
|
|
276
|
+
const loading = ref(false);
|
|
277
|
+
const error = ref(null);
|
|
278
|
+
const queueData = ref(null);
|
|
279
|
+
const statusData = ref(null);
|
|
280
|
+
const partitionSearch = ref('');
|
|
281
|
+
const partitionSortColumn = ref('name');
|
|
282
|
+
const partitionSortDirection = ref('asc');
|
|
283
|
+
|
|
284
|
+
const showPushModal = ref(false);
|
|
285
|
+
const showClearConfirm = ref(false);
|
|
286
|
+
|
|
287
|
+
const calculatedPending = computed(() => {
|
|
288
|
+
if (!statusData.value?.totals?.messages) return 0;
|
|
289
|
+
|
|
290
|
+
const total = queueData.value?.totals?.total || 0;
|
|
291
|
+
const completed = statusData.value.totals.messages.completed || 0;
|
|
292
|
+
const failed = statusData.value.totals.messages.failed || 0;
|
|
293
|
+
const processing = statusData.value.totals.messages.processing || 0;
|
|
294
|
+
|
|
295
|
+
return Math.max(0, total - completed - failed - processing);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const sortedPartitions = computed(() => {
|
|
299
|
+
let partitions = statusData.value?.partitions || [];
|
|
300
|
+
|
|
301
|
+
// Filter by search
|
|
302
|
+
if (partitionSearch.value) {
|
|
303
|
+
const search = partitionSearch.value.toLowerCase();
|
|
304
|
+
partitions = partitions.filter(p => p.name.toLowerCase().includes(search));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Sort
|
|
308
|
+
return [...partitions].sort((a, b) => {
|
|
309
|
+
let aVal, bVal;
|
|
310
|
+
|
|
311
|
+
switch (partitionSortColumn.value) {
|
|
312
|
+
case 'name':
|
|
313
|
+
aVal = a.name.toLowerCase();
|
|
314
|
+
bVal = b.name.toLowerCase();
|
|
315
|
+
break;
|
|
316
|
+
case 'pending':
|
|
317
|
+
aVal = a.messages?.pending || 0;
|
|
318
|
+
bVal = b.messages?.pending || 0;
|
|
319
|
+
break;
|
|
320
|
+
case 'processing':
|
|
321
|
+
aVal = a.messages?.processing || 0;
|
|
322
|
+
bVal = b.messages?.processing || 0;
|
|
323
|
+
break;
|
|
324
|
+
case 'completed':
|
|
325
|
+
aVal = a.messages?.completed || 0;
|
|
326
|
+
bVal = b.messages?.completed || 0;
|
|
327
|
+
break;
|
|
328
|
+
case 'failed':
|
|
329
|
+
aVal = a.messages?.failed || 0;
|
|
330
|
+
bVal = b.messages?.failed || 0;
|
|
331
|
+
break;
|
|
332
|
+
default:
|
|
333
|
+
return 0;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (aVal < bVal) return partitionSortDirection.value === 'asc' ? -1 : 1;
|
|
337
|
+
if (aVal > bVal) return partitionSortDirection.value === 'asc' ? 1 : -1;
|
|
338
|
+
return 0;
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
function sortPartitions(column) {
|
|
343
|
+
if (partitionSortColumn.value === column) {
|
|
344
|
+
partitionSortDirection.value = partitionSortDirection.value === 'asc' ? 'desc' : 'asc';
|
|
345
|
+
} else {
|
|
346
|
+
partitionSortColumn.value = column;
|
|
347
|
+
partitionSortDirection.value = 'asc';
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function getPartitionSortClass(column) {
|
|
352
|
+
if (partitionSortColumn.value !== column) {
|
|
353
|
+
return 'opacity-30';
|
|
354
|
+
}
|
|
355
|
+
return partitionSortDirection.value === 'asc' ? '' : 'rotate-180';
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function goBack() {
|
|
359
|
+
router.push('/queues');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function loadData() {
|
|
363
|
+
loading.value = true;
|
|
364
|
+
error.value = null;
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const [queueRes, statusRes] = await Promise.all([
|
|
368
|
+
queuesApi.getQueue(queueName.value),
|
|
369
|
+
analyticsApi.getQueueDetail(queueName.value),
|
|
370
|
+
]);
|
|
371
|
+
|
|
372
|
+
queueData.value = queueRes.data;
|
|
373
|
+
statusData.value = statusRes.data;
|
|
374
|
+
} catch (err) {
|
|
375
|
+
error.value = err.message;
|
|
376
|
+
console.error('Queue detail error:', err);
|
|
377
|
+
} finally {
|
|
378
|
+
loading.value = false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function confirmClear() {
|
|
383
|
+
showClearConfirm.value = true;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function clearQueue() {
|
|
387
|
+
try {
|
|
388
|
+
await queuesApi.clearQueue(queueName.value);
|
|
389
|
+
showClearConfirm.value = false;
|
|
390
|
+
await loadData();
|
|
391
|
+
} catch (err) {
|
|
392
|
+
error.value = err.message;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function onMessagePushed() {
|
|
397
|
+
await loadData();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
onMounted(() => {
|
|
401
|
+
loadData();
|
|
402
|
+
});
|
|
403
|
+
</script>
|
|
404
|
+
|
|
405
|
+
<style scoped>
|
|
406
|
+
.page-flat {
|
|
407
|
+
min-height: 100%;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.header-section {
|
|
411
|
+
background: transparent;
|
|
412
|
+
padding: 0;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.config-section {
|
|
416
|
+
background: #ffffff;
|
|
417
|
+
border: none;
|
|
418
|
+
box-shadow: none;
|
|
419
|
+
border-radius: 0.75rem;
|
|
420
|
+
padding: 1rem;
|
|
421
|
+
transition: all 0.3s ease;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.dark .config-section {
|
|
425
|
+
background: #0a0d14;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.config-section:hover {
|
|
429
|
+
background: #fafafa;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.dark .config-section:hover {
|
|
433
|
+
background: #0d1117;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.partition-section {
|
|
437
|
+
background: #ffffff;
|
|
438
|
+
border: none;
|
|
439
|
+
box-shadow: none;
|
|
440
|
+
border-radius: 0.75rem;
|
|
441
|
+
padding: 1rem;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.dark .partition-section {
|
|
445
|
+
background: #0a0d14;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* Flat inputs */
|
|
449
|
+
.partition-section :deep(.input) {
|
|
450
|
+
background: transparent;
|
|
451
|
+
border: 1px solid rgba(156, 163, 175, 0.15);
|
|
452
|
+
transition: all 0.2s ease;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.partition-section :deep(.input:hover) {
|
|
456
|
+
border-color: rgba(156, 163, 175, 0.25);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.partition-section :deep(.input:focus) {
|
|
460
|
+
background: rgba(244, 63, 94, 0.02);
|
|
461
|
+
border-color: rgba(244, 63, 94, 0.4);
|
|
462
|
+
box-shadow: 0 0 0 3px rgba(244, 63, 94, 0.05);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.dark .partition-section :deep(.input) {
|
|
466
|
+
border-color: rgba(156, 163, 175, 0.1);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.dark .partition-section :deep(.input:focus) {
|
|
470
|
+
background: rgba(244, 63, 94, 0.03);
|
|
471
|
+
border-color: rgba(244, 63, 94, 0.5);
|
|
472
|
+
box-shadow: 0 0 0 3px rgba(244, 63, 94, 0.08);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/* Flat table styling */
|
|
476
|
+
.partition-section :deep(.table) {
|
|
477
|
+
border-collapse: separate;
|
|
478
|
+
border-spacing: 0;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.partition-section :deep(.table thead) {
|
|
482
|
+
background: transparent;
|
|
483
|
+
border-bottom: 1px solid rgba(156, 163, 175, 0.08);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.dark .partition-section :deep(.table thead) {
|
|
487
|
+
border-bottom-color: rgba(156, 163, 175, 0.1);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.partition-section :deep(.table th) {
|
|
491
|
+
padding: 1rem 1rem;
|
|
492
|
+
font-weight: 600;
|
|
493
|
+
letter-spacing: 0.05em;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.partition-section :deep(.table tbody tr) {
|
|
497
|
+
border: none;
|
|
498
|
+
transition: all 0.15s ease;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.partition-section :deep(.table tbody tr:nth-child(even)) {
|
|
502
|
+
background: rgba(0, 0, 0, 0.015);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.dark .partition-section :deep(.table tbody tr:nth-child(even)) {
|
|
506
|
+
background: rgba(255, 255, 255, 0.02);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.partition-section :deep(.table tbody tr:hover) {
|
|
510
|
+
background: rgba(244, 63, 94, 0.03);
|
|
511
|
+
box-shadow: inset 3px 0 0 0 rgba(244, 63, 94, 0.6);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.dark .partition-section :deep(.table tbody tr:hover) {
|
|
515
|
+
background: rgba(244, 63, 94, 0.05);
|
|
516
|
+
box-shadow: inset 3px 0 0 0 rgba(244, 63, 94, 0.8);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.partition-section :deep(.table td) {
|
|
520
|
+
padding: 0.875rem 1rem;
|
|
521
|
+
border: none;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.metric-flat {
|
|
525
|
+
background: #ffffff;
|
|
526
|
+
border: none;
|
|
527
|
+
box-shadow: none;
|
|
528
|
+
border-radius: 0.75rem;
|
|
529
|
+
padding: 1rem;
|
|
530
|
+
transition: all 0.3s ease;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.dark .metric-flat {
|
|
534
|
+
background: #0a0d14;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.metric-flat:hover {
|
|
538
|
+
background: #fafafa;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.dark .metric-flat:hover {
|
|
542
|
+
background: #0d1117;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.metric-icon-flat {
|
|
546
|
+
width: 2rem;
|
|
547
|
+
height: 2rem;
|
|
548
|
+
border-radius: 0.5rem;
|
|
549
|
+
display: flex;
|
|
550
|
+
align-items: center;
|
|
551
|
+
justify-content: center;
|
|
552
|
+
flex-shrink: 0;
|
|
553
|
+
transition: all 0.3s ease;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.metric-flat:hover .metric-icon-flat {
|
|
557
|
+
transform: scale(1.05);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.metric-value-flat {
|
|
561
|
+
font-size: 1.5rem;
|
|
562
|
+
font-weight: 700;
|
|
563
|
+
line-height: 1.1;
|
|
564
|
+
margin-top: 0.125rem;
|
|
565
|
+
background: linear-gradient(135deg, #f43f5e 0%, #ec4899 50%, #a855f7 100%);
|
|
566
|
+
-webkit-background-clip: text;
|
|
567
|
+
-webkit-text-fill-color: transparent;
|
|
568
|
+
background-clip: text;
|
|
569
|
+
letter-spacing: -0.02em;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.error-flat {
|
|
573
|
+
background: transparent;
|
|
574
|
+
color: #dc2626;
|
|
575
|
+
font-size: 0.875rem;
|
|
576
|
+
padding: 1rem;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.dark .error-flat {
|
|
580
|
+
color: #fca5a5;
|
|
581
|
+
}
|
|
582
|
+
</style>
|