queen-mq 0.1.0 → 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 +862 -752
- package/AUTH.md +2044 -0
- package/LICENSE.md +202 -0
- package/README.md +1705 -1051
- 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-complete-client.js +260 -0
- package/examples/test-dashboard-api.js +200 -0
- package/examples/test-traceid.js +147 -0
- package/package.json +17 -4
- 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 +300 -31
- 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/MIGRATION_ISSUES.md +174 -0
- package/src/test/README.md +203 -0
- package/src/test/advanced-pattern-tests.js +1137 -0
- package/src/test/bus-mode-tests.js +361 -0
- package/src/test/core-tests.js +342 -0
- package/src/test/edge-case-tests.js +561 -0
- package/src/test/enterprise-tests.js +637 -0
- package/src/test/partition-locking-tests.js +545 -0
- package/src/test/test-new.js +278 -0
- package/src/test/test.js +6 -3
- package/src/test/utils.js +169 -0
- 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/CACHE.md +0 -519
- package/DASHBOARD-V3.md +0 -478
- package/DASHBOARD.md +0 -382
- package/MOD_QUEUE.md +0 -453
- package/PARTITION_LOCKING_DESIGN.md +0 -989
- package/PLAN.md +0 -707
- package/QUERY_ANALSYS.md +0 -72
- package/QUEUE_BUS.md +0 -334
- package/V2-PLAN.md +0 -236
- 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/debug-namespace.js +0 -110
- package/docs/long-polling.md +0 -159
- package/docs/multi-server-cache-solutions.md +0 -185
- package/docs/performance-tuning.md +0 -222
- package/src/routes/analytics.js +0 -812
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
v-if="isOpen"
|
|
4
|
+
class="fixed inset-y-0 right-0 w-full sm:w-[32rem] bg-white dark:bg-slate-800 shadow-2xl z-40 overflow-y-auto scrollbar-thin transform transition-all duration-300 border-l border-gray-200 dark:border-gray-700"
|
|
5
|
+
:class="isOpen ? 'translate-x-0' : 'translate-x-full'"
|
|
6
|
+
>
|
|
7
|
+
<!-- Backdrop for mobile -->
|
|
8
|
+
<div
|
|
9
|
+
v-if="isOpen"
|
|
10
|
+
class="sm:hidden fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm -z-10 animate-fade-in"
|
|
11
|
+
@click="close"
|
|
12
|
+
></div>
|
|
13
|
+
|
|
14
|
+
<div class="p-6">
|
|
15
|
+
<!-- Header -->
|
|
16
|
+
<div class="flex items-start justify-between mb-6">
|
|
17
|
+
<div class="flex-1 min-w-0">
|
|
18
|
+
<h3 class="text-lg font-bold mb-1">Message Details</h3>
|
|
19
|
+
<p class="text-xs font-mono text-gray-500 dark:text-gray-400 break-all">
|
|
20
|
+
{{ message?.transactionId }}
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
<button @click="close" class="text-gray-400 hover:text-gray-600 p-1">
|
|
24
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
25
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
26
|
+
</svg>
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<LoadingSpinner v-if="loading" />
|
|
31
|
+
|
|
32
|
+
<div v-else-if="error" class="text-sm text-red-600 dark:text-red-400">
|
|
33
|
+
{{ error }}
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<template v-else-if="messageDetail">
|
|
37
|
+
<!-- Status Badge -->
|
|
38
|
+
<div class="mb-6">
|
|
39
|
+
<StatusBadge :status="messageDetail.status" />
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<!-- Info Grid -->
|
|
43
|
+
<div class="space-y-4 mb-6">
|
|
44
|
+
<div>
|
|
45
|
+
<label class="text-xs text-gray-500 dark:text-gray-400 block mb-1">Queue</label>
|
|
46
|
+
<p class="text-sm font-medium">{{ messageDetail.queue }} / {{ messageDetail.partition }}</p>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div>
|
|
50
|
+
<label class="text-xs text-gray-500 dark:text-gray-400 block mb-1">Created</label>
|
|
51
|
+
<p class="text-sm">{{ formatDate(messageDetail.createdAt) }}</p>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<div v-if="messageDetail.traceId">
|
|
55
|
+
<label class="text-xs text-gray-500 dark:text-gray-400 block mb-1">Trace ID</label>
|
|
56
|
+
<p class="text-xs font-mono break-all">{{ messageDetail.traceId }}</p>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div v-if="messageDetail.errorMessage">
|
|
60
|
+
<label class="text-xs text-gray-500 dark:text-gray-400 block mb-1">Error Message</label>
|
|
61
|
+
<p class="text-sm text-red-600 dark:text-red-400">{{ messageDetail.errorMessage }}</p>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div v-if="messageDetail.retryCount">
|
|
65
|
+
<label class="text-xs text-gray-500 dark:text-gray-400 block mb-1">Retry Count</label>
|
|
66
|
+
<p class="text-sm">{{ messageDetail.retryCount }}</p>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- Payload -->
|
|
71
|
+
<div class="mb-6">
|
|
72
|
+
<label class="text-xs text-gray-500 dark:text-gray-400 block mb-2">Payload</label>
|
|
73
|
+
<div class="bg-gray-50 dark:bg-slate-900 rounded-lg p-4 overflow-x-auto scrollbar-thin">
|
|
74
|
+
<pre class="text-xs font-mono">{{ JSON.stringify(messageDetail.payload, null, 2) }}</pre>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<!-- Actions -->
|
|
79
|
+
<div class="space-y-2">
|
|
80
|
+
<!-- Completed message info -->
|
|
81
|
+
<div v-if="messageDetail.status === 'completed'" class="card bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200 text-sm">
|
|
82
|
+
<div class="flex gap-2">
|
|
83
|
+
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
84
|
+
<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" />
|
|
85
|
+
</svg>
|
|
86
|
+
<p>This message has been successfully consumed and acknowledged.</p>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<button
|
|
91
|
+
v-if="messageDetail.status === 'dead_letter'"
|
|
92
|
+
@click="retryMessage"
|
|
93
|
+
:disabled="actionLoading"
|
|
94
|
+
class="btn btn-primary w-full"
|
|
95
|
+
>
|
|
96
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
97
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
98
|
+
</svg>
|
|
99
|
+
Retry Message
|
|
100
|
+
</button>
|
|
101
|
+
|
|
102
|
+
<button
|
|
103
|
+
v-if="messageDetail.status === 'pending'"
|
|
104
|
+
@click="moveToDLQ"
|
|
105
|
+
:disabled="actionLoading"
|
|
106
|
+
class="btn btn-secondary w-full"
|
|
107
|
+
>
|
|
108
|
+
Move to Dead Letter Queue
|
|
109
|
+
</button>
|
|
110
|
+
|
|
111
|
+
<button
|
|
112
|
+
@click="confirmDelete"
|
|
113
|
+
:disabled="actionLoading"
|
|
114
|
+
class="btn btn-danger w-full"
|
|
115
|
+
>
|
|
116
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
117
|
+
<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" />
|
|
118
|
+
</svg>
|
|
119
|
+
Delete Message
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div v-if="actionError" class="text-sm text-red-600 dark:text-red-400 mt-4">
|
|
124
|
+
{{ actionError }}
|
|
125
|
+
</div>
|
|
126
|
+
</template>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<!-- Delete Confirmation -->
|
|
130
|
+
<ConfirmDialog
|
|
131
|
+
:is-open="showDeleteConfirm"
|
|
132
|
+
title="Delete Message"
|
|
133
|
+
message="Are you sure you want to delete this message? This action cannot be undone."
|
|
134
|
+
confirm-text="Delete"
|
|
135
|
+
confirm-class="btn-danger"
|
|
136
|
+
@confirm="deleteMessage"
|
|
137
|
+
@cancel="showDeleteConfirm = false"
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
</template>
|
|
141
|
+
|
|
142
|
+
<script setup>
|
|
143
|
+
import { ref, watch } from 'vue';
|
|
144
|
+
import { messagesApi } from '../../api/messages';
|
|
145
|
+
import { formatDate } from '../../utils/formatters';
|
|
146
|
+
|
|
147
|
+
import StatusBadge from '../common/StatusBadge.vue';
|
|
148
|
+
import LoadingSpinner from '../common/LoadingSpinner.vue';
|
|
149
|
+
import ConfirmDialog from '../common/ConfirmDialog.vue';
|
|
150
|
+
|
|
151
|
+
const props = defineProps({
|
|
152
|
+
isOpen: Boolean,
|
|
153
|
+
message: Object,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const emit = defineEmits(['close', 'action-complete']);
|
|
157
|
+
|
|
158
|
+
const loading = ref(false);
|
|
159
|
+
const error = ref(null);
|
|
160
|
+
const actionLoading = ref(false);
|
|
161
|
+
const actionError = ref(null);
|
|
162
|
+
const messageDetail = ref(null);
|
|
163
|
+
const showDeleteConfirm = ref(false);
|
|
164
|
+
|
|
165
|
+
watch(() => props.message, async (newMessage) => {
|
|
166
|
+
if (newMessage) {
|
|
167
|
+
await loadMessageDetail();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
async function loadMessageDetail() {
|
|
172
|
+
if (!props.message?.transactionId) return;
|
|
173
|
+
|
|
174
|
+
loading.value = true;
|
|
175
|
+
error.value = null;
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const response = await messagesApi.getMessage(props.message.transactionId);
|
|
179
|
+
messageDetail.value = response.data;
|
|
180
|
+
} catch (err) {
|
|
181
|
+
error.value = err.response?.data?.error || err.message;
|
|
182
|
+
} finally {
|
|
183
|
+
loading.value = false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function retryMessage() {
|
|
188
|
+
actionLoading.value = true;
|
|
189
|
+
actionError.value = null;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
await messagesApi.retryMessage(messageDetail.value.transactionId);
|
|
193
|
+
emit('action-complete');
|
|
194
|
+
close();
|
|
195
|
+
} catch (err) {
|
|
196
|
+
actionError.value = err.response?.data?.error || err.message;
|
|
197
|
+
} finally {
|
|
198
|
+
actionLoading.value = false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function moveToDLQ() {
|
|
203
|
+
actionLoading.value = true;
|
|
204
|
+
actionError.value = null;
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
await messagesApi.moveToDLQ(messageDetail.value.transactionId);
|
|
208
|
+
emit('action-complete');
|
|
209
|
+
close();
|
|
210
|
+
} catch (err) {
|
|
211
|
+
actionError.value = err.response?.data?.error || err.message;
|
|
212
|
+
} finally {
|
|
213
|
+
actionLoading.value = false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function confirmDelete() {
|
|
218
|
+
showDeleteConfirm.value = true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function deleteMessage() {
|
|
222
|
+
actionLoading.value = true;
|
|
223
|
+
actionError.value = null;
|
|
224
|
+
showDeleteConfirm.value = false;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
await messagesApi.deleteMessage(messageDetail.value.transactionId);
|
|
228
|
+
emit('action-complete');
|
|
229
|
+
close();
|
|
230
|
+
} catch (err) {
|
|
231
|
+
actionError.value = err.response?.data?.error || err.message;
|
|
232
|
+
} finally {
|
|
233
|
+
actionLoading.value = false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function close() {
|
|
238
|
+
emit('close');
|
|
239
|
+
messageDetail.value = null;
|
|
240
|
+
}
|
|
241
|
+
</script>
|
|
242
|
+
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="filter-container">
|
|
3
|
+
<div class="flex flex-col sm:flex-row gap-3">
|
|
4
|
+
<!-- Search by Transaction ID -->
|
|
5
|
+
<div class="flex-1">
|
|
6
|
+
<input
|
|
7
|
+
v-model="localSearch"
|
|
8
|
+
@input="onSearchChange"
|
|
9
|
+
type="text"
|
|
10
|
+
placeholder="Search by transaction ID..."
|
|
11
|
+
class="input font-mono text-xs"
|
|
12
|
+
/>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<!-- Queue Filter -->
|
|
16
|
+
<select
|
|
17
|
+
v-model="localQueue"
|
|
18
|
+
@change="onQueueChange"
|
|
19
|
+
class="input sm:w-48"
|
|
20
|
+
>
|
|
21
|
+
<option value="">All Queues</option>
|
|
22
|
+
<option v-for="queue in queues" :key="queue.id" :value="queue.name">
|
|
23
|
+
{{ queue.name }}
|
|
24
|
+
</option>
|
|
25
|
+
</select>
|
|
26
|
+
|
|
27
|
+
<!-- Status Filter -->
|
|
28
|
+
<select
|
|
29
|
+
v-model="localStatus"
|
|
30
|
+
@change="onStatusChange"
|
|
31
|
+
class="input sm:w-40"
|
|
32
|
+
>
|
|
33
|
+
<option value="">All Status</option>
|
|
34
|
+
<option value="pending">Pending</option>
|
|
35
|
+
<option value="processing">Processing</option>
|
|
36
|
+
<option value="completed">Completed</option>
|
|
37
|
+
<option value="dead_letter">Dead Letter</option>
|
|
38
|
+
</select>
|
|
39
|
+
|
|
40
|
+
<!-- Clear Filters -->
|
|
41
|
+
<button
|
|
42
|
+
v-if="localSearch || localQueue || localStatus"
|
|
43
|
+
@click="clearFilters"
|
|
44
|
+
class="btn btn-secondary whitespace-nowrap"
|
|
45
|
+
>
|
|
46
|
+
Clear Filters
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<style scoped>
|
|
53
|
+
.filter-container {
|
|
54
|
+
background: #ffffff;
|
|
55
|
+
border: none;
|
|
56
|
+
box-shadow: none;
|
|
57
|
+
border-radius: 0.75rem;
|
|
58
|
+
padding: 1rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.dark .filter-container {
|
|
62
|
+
background: #0a0d14;
|
|
63
|
+
}
|
|
64
|
+
</style>
|
|
65
|
+
|
|
66
|
+
<script setup>
|
|
67
|
+
import { ref, watch } from 'vue';
|
|
68
|
+
|
|
69
|
+
const props = defineProps({
|
|
70
|
+
search: String,
|
|
71
|
+
queue: String,
|
|
72
|
+
status: String,
|
|
73
|
+
queues: Array,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const emit = defineEmits(['update:search', 'update:queue', 'update:status']);
|
|
77
|
+
|
|
78
|
+
const localSearch = ref(props.search || '');
|
|
79
|
+
const localQueue = ref(props.queue || '');
|
|
80
|
+
const localStatus = ref(props.status || '');
|
|
81
|
+
|
|
82
|
+
watch(() => props.search, (newVal) => {
|
|
83
|
+
localSearch.value = newVal || '';
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
watch(() => props.queue, (newVal) => {
|
|
87
|
+
localQueue.value = newVal || '';
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
watch(() => props.status, (newVal) => {
|
|
91
|
+
localStatus.value = newVal || '';
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
function onSearchChange() {
|
|
95
|
+
emit('update:search', localSearch.value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function onQueueChange() {
|
|
99
|
+
emit('update:queue', localQueue.value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function onStatusChange() {
|
|
103
|
+
emit('update:status', localStatus.value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function clearFilters() {
|
|
107
|
+
localSearch.value = '';
|
|
108
|
+
localQueue.value = '';
|
|
109
|
+
localStatus.value = '';
|
|
110
|
+
emit('update:search', '');
|
|
111
|
+
emit('update:queue', '');
|
|
112
|
+
emit('update:status', '');
|
|
113
|
+
}
|
|
114
|
+
</script>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="partition-container">
|
|
3
|
+
<h3 class="text-base font-semibold mb-4">Partitions</h3>
|
|
4
|
+
<div class="space-y-3">
|
|
5
|
+
<div
|
|
6
|
+
v-for="partition in partitions"
|
|
7
|
+
:key="partition.id"
|
|
8
|
+
class="p-4 bg-gray-50 dark:bg-slate-700/50 rounded-lg"
|
|
9
|
+
>
|
|
10
|
+
<div class="flex items-center justify-between mb-2">
|
|
11
|
+
<h4 class="font-semibold">{{ partition.name }}</h4>
|
|
12
|
+
<span class="text-xs text-gray-500">{{ formatDate(partition.createdAt) }}</span>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 text-sm">
|
|
16
|
+
<div>
|
|
17
|
+
<span class="text-gray-500 dark:text-gray-400 block">Pending</span>
|
|
18
|
+
<span class="font-semibold">{{ formatNumber(partition.messages?.pending || 0) }}</span>
|
|
19
|
+
</div>
|
|
20
|
+
<div>
|
|
21
|
+
<span class="text-gray-500 dark:text-gray-400 block">Processing</span>
|
|
22
|
+
<span class="font-semibold">{{ formatNumber(partition.messages?.processing || 0) }}</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div>
|
|
25
|
+
<span class="text-gray-500 dark:text-gray-400 block">Completed</span>
|
|
26
|
+
<span class="font-semibold text-green-600">{{ formatNumber(partition.messages?.completed || 0) }}</span>
|
|
27
|
+
</div>
|
|
28
|
+
<div>
|
|
29
|
+
<span class="text-gray-500 dark:text-gray-400 block">Failed</span>
|
|
30
|
+
<span class="font-semibold text-red-600">{{ formatNumber(partition.messages?.failed || 0) }}</span>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div v-if="partition.cursor" class="mt-3 pt-3 border-t border-gray-200 dark:border-gray-600 text-xs text-gray-600 dark:text-gray-400">
|
|
35
|
+
<div class="flex items-center justify-between">
|
|
36
|
+
<span>Consumed: {{ formatNumber(partition.cursor.totalConsumed || 0) }} messages in {{ partition.cursor.batchesConsumed || 0 }} batches</span>
|
|
37
|
+
<span v-if="partition.lastActivity">Last: {{ formatTime(partition.lastActivity) }}</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div v-if="!partitions?.length" class="text-center py-8 text-gray-500 text-sm">
|
|
44
|
+
No partitions found
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<style scoped>
|
|
50
|
+
.partition-container {
|
|
51
|
+
background: #ffffff;
|
|
52
|
+
border: none;
|
|
53
|
+
box-shadow: none;
|
|
54
|
+
border-radius: 0.75rem;
|
|
55
|
+
padding: 1rem;
|
|
56
|
+
transition: all 0.3s ease;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.dark .partition-container {
|
|
60
|
+
background: #0a0d14;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.partition-container:hover {
|
|
64
|
+
background: #fafafa;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.dark .partition-container:hover {
|
|
68
|
+
background: #0d1117;
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
71
|
+
|
|
72
|
+
<script setup>
|
|
73
|
+
import { formatNumber, formatDate, formatTime } from '../../utils/formatters';
|
|
74
|
+
|
|
75
|
+
defineProps({
|
|
76
|
+
partitions: Array,
|
|
77
|
+
});
|
|
78
|
+
</script>
|
|
79
|
+
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="isOpen" class="fixed inset-0 z-50 overflow-y-auto">
|
|
3
|
+
<div class="flex items-center justify-center min-h-screen px-4">
|
|
4
|
+
<!-- Backdrop with blur -->
|
|
5
|
+
<div
|
|
6
|
+
class="fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm transition-opacity animate-fade-in"
|
|
7
|
+
@click="close"
|
|
8
|
+
></div>
|
|
9
|
+
|
|
10
|
+
<!-- Modal -->
|
|
11
|
+
<div class="relative bg-white dark:bg-slate-800 rounded-xl shadow-2xl max-w-2xl w-full p-6 animate-scale-in border border-gray-200 dark:border-gray-700">
|
|
12
|
+
<div class="flex items-center justify-between mb-4">
|
|
13
|
+
<h3 class="text-lg font-bold">Push Message to {{ queueName }}</h3>
|
|
14
|
+
<button @click="close" class="text-gray-400 hover:text-gray-600">
|
|
15
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
16
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
17
|
+
</svg>
|
|
18
|
+
</button>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<form @submit.prevent="pushMessage" class="space-y-4">
|
|
22
|
+
<div>
|
|
23
|
+
<label class="block text-sm font-medium mb-1">Partition</label>
|
|
24
|
+
<input
|
|
25
|
+
v-model="form.partition"
|
|
26
|
+
type="text"
|
|
27
|
+
placeholder="Default"
|
|
28
|
+
class="input"
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div>
|
|
33
|
+
<label class="block text-sm font-medium mb-1">Payload (JSON) *</label>
|
|
34
|
+
<textarea
|
|
35
|
+
v-model="form.payload"
|
|
36
|
+
rows="10"
|
|
37
|
+
placeholder='{"key": "value", "message": "Hello World"}'
|
|
38
|
+
class="input font-mono text-xs"
|
|
39
|
+
required
|
|
40
|
+
></textarea>
|
|
41
|
+
<p v-if="jsonError" class="text-xs text-red-600 mt-1">{{ jsonError }}</p>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="grid grid-cols-2 gap-3">
|
|
45
|
+
<div>
|
|
46
|
+
<label class="block text-sm font-medium mb-1">TTL (seconds)</label>
|
|
47
|
+
<input
|
|
48
|
+
v-model.number="form.ttl"
|
|
49
|
+
type="number"
|
|
50
|
+
placeholder="3600"
|
|
51
|
+
class="input"
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div>
|
|
56
|
+
<label class="block text-sm font-medium mb-1">Priority</label>
|
|
57
|
+
<input
|
|
58
|
+
v-model.number="form.priority"
|
|
59
|
+
type="number"
|
|
60
|
+
placeholder="0"
|
|
61
|
+
class="input"
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div>
|
|
67
|
+
<label class="block text-sm font-medium mb-1">Trace ID (optional)</label>
|
|
68
|
+
<input
|
|
69
|
+
v-model="form.traceId"
|
|
70
|
+
type="text"
|
|
71
|
+
placeholder="trace-id-for-distributed-tracing"
|
|
72
|
+
class="input"
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div v-if="error" class="text-sm text-red-600 dark:text-red-400">
|
|
77
|
+
{{ error }}
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div class="flex gap-3 pt-2">
|
|
81
|
+
<button type="submit" :disabled="loading || !!jsonError" class="btn btn-primary flex-1">
|
|
82
|
+
<span v-if="loading">Pushing...</span>
|
|
83
|
+
<span v-else>Push Message</span>
|
|
84
|
+
</button>
|
|
85
|
+
<button type="button" @click="close" class="btn btn-secondary">
|
|
86
|
+
Cancel
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
</form>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<script setup>
|
|
96
|
+
import { ref, watch, computed } from 'vue';
|
|
97
|
+
import { messagesApi } from '../../api/messages';
|
|
98
|
+
|
|
99
|
+
const props = defineProps({
|
|
100
|
+
isOpen: Boolean,
|
|
101
|
+
queueName: String,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const emit = defineEmits(['close', 'pushed']);
|
|
105
|
+
|
|
106
|
+
const loading = ref(false);
|
|
107
|
+
const error = ref(null);
|
|
108
|
+
const form = ref({
|
|
109
|
+
partition: 'Default',
|
|
110
|
+
payload: '',
|
|
111
|
+
ttl: null,
|
|
112
|
+
priority: null,
|
|
113
|
+
traceId: '',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const jsonError = computed(() => {
|
|
117
|
+
if (!form.value.payload) return null;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
JSON.parse(form.value.payload);
|
|
121
|
+
return null;
|
|
122
|
+
} catch (e) {
|
|
123
|
+
return 'Invalid JSON format';
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
watch(() => props.isOpen, (newVal) => {
|
|
128
|
+
if (newVal) {
|
|
129
|
+
resetForm();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
function resetForm() {
|
|
134
|
+
form.value = {
|
|
135
|
+
partition: 'Default',
|
|
136
|
+
payload: '',
|
|
137
|
+
ttl: null,
|
|
138
|
+
priority: null,
|
|
139
|
+
traceId: '',
|
|
140
|
+
};
|
|
141
|
+
error.value = null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function pushMessage() {
|
|
145
|
+
loading.value = true;
|
|
146
|
+
error.value = null;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const payload = JSON.parse(form.value.payload);
|
|
150
|
+
|
|
151
|
+
const item = {
|
|
152
|
+
queue: props.queueName,
|
|
153
|
+
partition: form.value.partition || 'Default',
|
|
154
|
+
payload: payload,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (form.value.ttl) item.ttl = form.value.ttl;
|
|
158
|
+
if (form.value.priority) item.priority = form.value.priority;
|
|
159
|
+
if (form.value.traceId) item.traceId = form.value.traceId;
|
|
160
|
+
|
|
161
|
+
await messagesApi.pushMessages({ items: [item] });
|
|
162
|
+
emit('pushed');
|
|
163
|
+
close();
|
|
164
|
+
} catch (err) {
|
|
165
|
+
error.value = err.response?.data?.error || err.message;
|
|
166
|
+
} finally {
|
|
167
|
+
loading.value = false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function close() {
|
|
172
|
+
emit('close');
|
|
173
|
+
}
|
|
174
|
+
</script>
|
|
175
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="config-container">
|
|
3
|
+
<h3 class="text-base font-semibold mb-4">Queue Configuration</h3>
|
|
4
|
+
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm">
|
|
5
|
+
<div>
|
|
6
|
+
<span class="text-gray-500 dark:text-gray-400 block mb-1">Lease Time</span>
|
|
7
|
+
<span class="font-semibold">{{ config?.leaseTime || 0 }}s</span>
|
|
8
|
+
</div>
|
|
9
|
+
<div>
|
|
10
|
+
<span class="text-gray-500 dark:text-gray-400 block mb-1">TTL</span>
|
|
11
|
+
<span class="font-semibold">{{ formatDuration((config?.ttl || 0) * 1000) }}</span>
|
|
12
|
+
</div>
|
|
13
|
+
<div>
|
|
14
|
+
<span class="text-gray-500 dark:text-gray-400 block mb-1">Max Queue Size</span>
|
|
15
|
+
<span class="font-semibold">{{ formatNumber(config?.maxQueueSize || 0) }}</span>
|
|
16
|
+
</div>
|
|
17
|
+
<div>
|
|
18
|
+
<span class="text-gray-500 dark:text-gray-400 block mb-1">Retry Limit</span>
|
|
19
|
+
<span class="font-semibold">{{ config?.retryLimit || 0 }}</span>
|
|
20
|
+
</div>
|
|
21
|
+
<div>
|
|
22
|
+
<span class="text-gray-500 dark:text-gray-400 block mb-1">Retry Delay</span>
|
|
23
|
+
<span class="font-semibold">{{ config?.retryDelay || 0 }}ms</span>
|
|
24
|
+
</div>
|
|
25
|
+
<div>
|
|
26
|
+
<span class="text-gray-500 dark:text-gray-400 block mb-1">DLQ Enabled</span>
|
|
27
|
+
<span class="font-semibold">{{ config?.deadLetterQueue ? 'Yes' : 'No' }}</span>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<style scoped>
|
|
34
|
+
.config-container {
|
|
35
|
+
background: #ffffff;
|
|
36
|
+
border: none;
|
|
37
|
+
box-shadow: none;
|
|
38
|
+
border-radius: 0.75rem;
|
|
39
|
+
padding: 1rem;
|
|
40
|
+
transition: all 0.3s ease;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.dark .config-container {
|
|
44
|
+
background: #0a0d14;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.config-container:hover {
|
|
48
|
+
background: #fafafa;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.dark .config-container:hover {
|
|
52
|
+
background: #0d1117;
|
|
53
|
+
}
|
|
54
|
+
</style>
|
|
55
|
+
|
|
56
|
+
<script setup>
|
|
57
|
+
import { formatNumber, formatDuration } from '../../utils/formatters';
|
|
58
|
+
|
|
59
|
+
defineProps({
|
|
60
|
+
config: Object,
|
|
61
|
+
});
|
|
62
|
+
</script>
|
|
63
|
+
|