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.
Files changed (139) hide show
  1. package/API.md +1226 -0
  2. package/AUTH.md +2044 -0
  3. package/LICENSE.md +202 -0
  4. package/README.md +276 -53
  5. package/WEBAPP.md +1889 -0
  6. package/assets/dashboard-01.png +0 -0
  7. package/assets/queen-logo-blue.svg +210 -0
  8. package/assets/queen-logo-cyan.svg +210 -0
  9. package/assets/queen-logo-indigo.svg +210 -0
  10. package/assets/queen-logo-orange.svg +210 -0
  11. package/assets/queen-logo-pink.svg +210 -0
  12. package/assets/queen-logo-purple.svg +210 -0
  13. package/assets/queen-logo-rose.svg +239 -0
  14. package/assets/queen-logo.svg +263 -0
  15. package/examples/batch-processing.js +58 -0
  16. package/examples/test-dashboard-api.js +200 -0
  17. package/package.json +4 -2
  18. package/server.log +1 -0
  19. package/src/benchmark/consumer.js +207 -0
  20. package/src/benchmark/consumer_multi.js +216 -0
  21. package/src/benchmark/producer.js +75 -0
  22. package/src/benchmark/producer_multi.js +115 -0
  23. package/src/client/client.js +192 -16
  24. package/src/client/queenClient.js +5 -0
  25. package/src/cluster-server.js +242 -0
  26. package/src/config.js +19 -5
  27. package/src/database/connection.js +42 -16
  28. package/src/database/poolManager.js +7 -0
  29. package/src/database/schema-v2.sql +194 -130
  30. package/src/managers/queueManagerOptimized.js +823 -933
  31. package/src/managers/systemEventManager.js +8 -3
  32. package/src/routes/messages.js +127 -57
  33. package/src/routes/pop.js +27 -43
  34. package/src/routes/resources.js +61 -27
  35. package/src/routes/status.js +1037 -0
  36. package/src/server.js +308 -272
  37. package/src/services/evictionService.js +57 -28
  38. package/src/services/retentionService.js +44 -11
  39. package/src/test/advanced-pattern-tests.js +5 -5
  40. package/src/test/bus-mode-tests.js +24 -11
  41. package/src/test/edge-case-tests.js +12 -5
  42. package/src/test/enterprise-tests.js +48 -15
  43. package/src/test/utils.js +1 -1
  44. package/src/utils/streaming.js +231 -0
  45. package/src/utils/uuid.js +2 -2
  46. package/src/websocket/wsServer.js +10 -3
  47. package/test-keepalive-v2.sh +22 -0
  48. package/webapp/COLOR_GUIDE.md +118 -0
  49. package/webapp/README.md +143 -0
  50. package/webapp/index.html +14 -0
  51. package/webapp/package-lock.json +3184 -0
  52. package/webapp/package.json +25 -0
  53. package/webapp/postcss.config.js +7 -0
  54. package/webapp/public/assets/queen-logo-blue.svg +210 -0
  55. package/webapp/public/assets/queen-logo-cyan.svg +210 -0
  56. package/webapp/public/assets/queen-logo-indigo.svg +210 -0
  57. package/webapp/public/assets/queen-logo-orange.svg +210 -0
  58. package/webapp/public/assets/queen-logo-pink.svg +210 -0
  59. package/webapp/public/assets/queen-logo-purple.svg +210 -0
  60. package/webapp/public/assets/queen-logo-rose.svg +239 -0
  61. package/webapp/public/assets/queen-logo.svg +263 -0
  62. package/webapp/src/App.vue +19 -0
  63. package/webapp/src/api/analytics.js +10 -0
  64. package/webapp/src/api/client.js +29 -0
  65. package/webapp/src/api/consumers.js +52 -0
  66. package/webapp/src/api/health.js +7 -0
  67. package/webapp/src/api/messages.js +26 -0
  68. package/webapp/src/api/queues.js +14 -0
  69. package/webapp/src/api/resources.js +8 -0
  70. package/webapp/src/assets/styles/main.css +357 -0
  71. package/webapp/src/components/analytics/AnalyticsFilters.vue +87 -0
  72. package/webapp/src/components/analytics/AnalyticsMetrics.vue +57 -0
  73. package/webapp/src/components/analytics/MessageDistributionChart.vue +111 -0
  74. package/webapp/src/components/analytics/MessageFlowChart.vue +173 -0
  75. package/webapp/src/components/analytics/TimeRangeSelector.vue +27 -0
  76. package/webapp/src/components/analytics/TopQueuesChart.vue +132 -0
  77. package/webapp/src/components/common/ConfirmDialog.vue +56 -0
  78. package/webapp/src/components/common/LoadingSpinner.vue +6 -0
  79. package/webapp/src/components/common/MetricCard.vue +43 -0
  80. package/webapp/src/components/common/StatusBadge.vue +45 -0
  81. package/webapp/src/components/dashboard/MessageStatusCard.vue +50 -0
  82. package/webapp/src/components/dashboard/PerformanceCard.vue +38 -0
  83. package/webapp/src/components/dashboard/ThroughputChart.vue +182 -0
  84. package/webapp/src/components/dashboard/TopQueuesTable.vue +53 -0
  85. package/webapp/src/components/layout/AppLayout.vue +110 -0
  86. package/webapp/src/components/layout/AppSidebar.vue +304 -0
  87. package/webapp/src/components/messages/MessageDetailPanel.vue +242 -0
  88. package/webapp/src/components/messages/MessageFilters.vue +114 -0
  89. package/webapp/src/components/queue-detail/PartitionList.vue +79 -0
  90. package/webapp/src/components/queue-detail/PushMessageModal.vue +175 -0
  91. package/webapp/src/components/queue-detail/QueueConfig.vue +63 -0
  92. package/webapp/src/components/queue-detail/QueueDetailHeader.vue +53 -0
  93. package/webapp/src/components/queue-detail/RecentMessages.vue +76 -0
  94. package/webapp/src/components/queues/CreateQueueModal.vue +193 -0
  95. package/webapp/src/components/queues/QueueFilters.vue +90 -0
  96. package/webapp/src/composables/useApi.js +34 -0
  97. package/webapp/src/composables/useTheme.js +36 -0
  98. package/webapp/src/main.js +11 -0
  99. package/webapp/src/router/index.js +42 -0
  100. package/webapp/src/utils/colors.js +96 -0
  101. package/webapp/src/utils/formatters.js +49 -0
  102. package/webapp/src/views/Analytics.vue +377 -0
  103. package/webapp/src/views/ConsumerGroups.vue +433 -0
  104. package/webapp/src/views/Dashboard.vue +418 -0
  105. package/webapp/src/views/Messages.vue +361 -0
  106. package/webapp/src/views/QueueDetail.vue +582 -0
  107. package/webapp/src/views/Queues.vue +496 -0
  108. package/webapp/tailwind.config.js +25 -0
  109. package/webapp/vite.config.js +10 -0
  110. package/dashboard/.vscode/extensions.json +0 -3
  111. package/dashboard/README.md +0 -5
  112. package/dashboard/index.html +0 -14
  113. package/dashboard/package-lock.json +0 -1458
  114. package/dashboard/package.json +0 -25
  115. package/dashboard/public/vite.svg +0 -1
  116. package/dashboard/src/App.vue +0 -29
  117. package/dashboard/src/assets/styles/main.css +0 -908
  118. package/dashboard/src/assets/vue.svg +0 -1
  119. package/dashboard/src/components/cards/MetricCard.vue +0 -298
  120. package/dashboard/src/components/charts/QueueDepthChart.vue +0 -276
  121. package/dashboard/src/components/charts/QueueLagChart.vue +0 -436
  122. package/dashboard/src/components/charts/ThroughputChart.vue +0 -302
  123. package/dashboard/src/components/common/ActivityFeed.vue +0 -251
  124. package/dashboard/src/components/layout/AppHeader.vue +0 -208
  125. package/dashboard/src/components/layout/AppLayout.vue +0 -88
  126. package/dashboard/src/components/layout/AppSidebar.vue +0 -261
  127. package/dashboard/src/main.js +0 -44
  128. package/dashboard/src/router.js +0 -54
  129. package/dashboard/src/services/api.js +0 -187
  130. package/dashboard/src/services/websocket.js +0 -167
  131. package/dashboard/src/utils/constants.js +0 -56
  132. package/dashboard/src/utils/helpers.js +0 -118
  133. package/dashboard/src/views/Analytics.vue +0 -912
  134. package/dashboard/src/views/Dashboard.vue +0 -906
  135. package/dashboard/src/views/Messages.vue +0 -437
  136. package/dashboard/src/views/QueueDetail.vue +0 -501
  137. package/dashboard/src/views/Queues.vue +0 -333
  138. package/dashboard/vite.config.js +0 -30
  139. 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>