queen-mq 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +467 -1775
- package/package.json +12 -14
- package/src/client/client.js +664 -5
- package/src/managers/queueManagerOptimized.js +250 -47
- package/src/routes/ack.js +3 -3
- package/src/server.js +131 -3
- package/src/test/advanced-client-tests.js +761 -0
- package/src/test/advanced-pattern-tests.js +2 -2
- package/src/test/bus-mode-tests.js +6 -6
- package/src/test/core-tests.js +1 -1
- package/src/test/edge-case-tests.js +2 -2
- package/src/test/test-new.js +36 -1
- package/src/webapp-dist/assets/Analytics-DJrE4Rz2.css +1 -0
- package/src/webapp-dist/assets/Analytics-HEMXKmfJ.js +1 -0
- package/src/webapp-dist/assets/ConfirmDialog-YEoCdE7x.js +1 -0
- package/src/webapp-dist/assets/ConsumerGroups-CC5VJywp.css +1 -0
- package/src/webapp-dist/assets/ConsumerGroups-D8Z20HhZ.js +1 -0
- package/src/webapp-dist/assets/Dashboard-BRFOz6it.css +1 -0
- package/src/webapp-dist/assets/Dashboard-Bpx9I32h.js +1 -0
- package/src/webapp-dist/assets/LoadingSpinner-BPttXcbs.js +1 -0
- package/src/webapp-dist/assets/Messages-BOfVHKma.css +1 -0
- package/src/webapp-dist/assets/Messages-EKURCbLl.js +1 -0
- package/src/webapp-dist/assets/QueueDetail-BXXA7xoZ.css +1 -0
- package/src/webapp-dist/assets/QueueDetail-Bmxg0KiA.js +1 -0
- package/src/webapp-dist/assets/Queues-CvrJkrZT.css +1 -0
- package/src/webapp-dist/assets/Queues-yUx9_3-9.js +1 -0
- package/src/webapp-dist/assets/StatusBadge-C1bBtXHh.css +1 -0
- package/src/webapp-dist/assets/StatusBadge-C6N84XmO.js +1 -0
- package/src/webapp-dist/assets/analytics-BapTekLM.js +1 -0
- package/src/webapp-dist/assets/colors-5z6Q_kUr.js +18 -0
- package/src/webapp-dist/assets/index-B7w2bKT6.css +1 -0
- package/src/webapp-dist/assets/index-CUSnwYTH.js +31 -0
- package/src/webapp-dist/assets/messages-CRV6E-xU.js +1 -0
- package/src/webapp-dist/assets/queues-BdUbxfBC.js +1 -0
- package/src/webapp-dist/assets/resources-Bs9U-ceF.js +1 -0
- package/{webapp → src/webapp-dist}/index.html +2 -1
- package/API.md +0 -1226
- package/AUTH.md +0 -2044
- package/CHANGELOG_STATIC_SERVING.md +0 -213
- package/DEPLOYMENT.md +0 -425
- package/PROGRAMMATIC_SERVER.md +0 -239
- package/WEBAPP.md +0 -1889
- package/assets/dashboard-01.png +0 -0
- package/assets/dashboard.png +0 -0
- package/examples/batch-processing.js +0 -58
- package/examples/bus-mode.js +0 -239
- package/examples/continuous-consumer-optimized.js +0 -215
- package/examples/continuous-consumer.js +0 -159
- package/examples/continuous-producer.js +0 -343
- package/examples/mixed-mode.js +0 -277
- package/examples/multi-server-test.js +0 -305
- package/examples/programmatic-server.js +0 -58
- package/examples/server-with-frontend.js +0 -46
- package/examples/single.js +0 -64
- package/examples/smartchat-dealyed.js +0 -42
- package/examples/smartchat.js +0 -52
- package/examples/test-cache-invalidation.js +0 -119
- package/examples/test-cache-multi-server.js +0 -245
- package/examples/test-complete-client.js +0 -260
- package/examples/test-connection-recovery.js +0 -66
- package/examples/test-dashboard-api.js +0 -200
- package/examples/test-minimal-client.js +0 -112
- package/examples/test-queue-creation-policy.js +0 -137
- package/examples/test-traceid.js +0 -147
- package/server.log +0 -1
- package/test-keepalive-v2.sh +0 -22
- package/webapp/COLOR_GUIDE.md +0 -118
- package/webapp/README.md +0 -143
- package/webapp/package-lock.json +0 -3184
- package/webapp/package.json +0 -25
- package/webapp/postcss.config.js +0 -7
- package/webapp/public/assets/queen-logo-blue.svg +0 -210
- package/webapp/public/assets/queen-logo-cyan.svg +0 -210
- package/webapp/public/assets/queen-logo-indigo.svg +0 -210
- package/webapp/public/assets/queen-logo-orange.svg +0 -210
- package/webapp/public/assets/queen-logo-pink.svg +0 -210
- package/webapp/public/assets/queen-logo-purple.svg +0 -210
- package/webapp/public/assets/queen-logo-rose.svg +0 -239
- package/webapp/public/assets/queen-logo.svg +0 -263
- package/webapp/src/App.vue +0 -19
- package/webapp/src/api/analytics.js +0 -10
- package/webapp/src/api/client.js +0 -34
- package/webapp/src/api/consumers.js +0 -52
- package/webapp/src/api/health.js +0 -7
- package/webapp/src/api/messages.js +0 -26
- package/webapp/src/api/queues.js +0 -14
- package/webapp/src/api/resources.js +0 -8
- package/webapp/src/assets/styles/main.css +0 -357
- package/webapp/src/components/analytics/AnalyticsFilters.vue +0 -87
- package/webapp/src/components/analytics/AnalyticsMetrics.vue +0 -57
- package/webapp/src/components/analytics/MessageDistributionChart.vue +0 -111
- package/webapp/src/components/analytics/MessageFlowChart.vue +0 -173
- package/webapp/src/components/analytics/TimeRangeSelector.vue +0 -27
- package/webapp/src/components/analytics/TopQueuesChart.vue +0 -132
- package/webapp/src/components/common/ConfirmDialog.vue +0 -56
- package/webapp/src/components/common/LoadingSpinner.vue +0 -6
- package/webapp/src/components/common/MetricCard.vue +0 -43
- package/webapp/src/components/common/StatusBadge.vue +0 -45
- package/webapp/src/components/dashboard/MessageStatusCard.vue +0 -50
- package/webapp/src/components/dashboard/PerformanceCard.vue +0 -38
- package/webapp/src/components/dashboard/ThroughputChart.vue +0 -182
- package/webapp/src/components/dashboard/TopQueuesTable.vue +0 -53
- package/webapp/src/components/layout/AppLayout.vue +0 -110
- package/webapp/src/components/layout/AppSidebar.vue +0 -304
- package/webapp/src/components/messages/MessageDetailPanel.vue +0 -242
- package/webapp/src/components/messages/MessageFilters.vue +0 -101
- package/webapp/src/components/queue-detail/PartitionList.vue +0 -79
- package/webapp/src/components/queue-detail/PushMessageModal.vue +0 -175
- package/webapp/src/components/queue-detail/QueueConfig.vue +0 -63
- package/webapp/src/components/queue-detail/QueueDetailHeader.vue +0 -53
- package/webapp/src/components/queue-detail/RecentMessages.vue +0 -76
- package/webapp/src/components/queues/CreateQueueModal.vue +0 -193
- package/webapp/src/components/queues/QueueFilters.vue +0 -90
- package/webapp/src/composables/useApi.js +0 -34
- package/webapp/src/composables/useTheme.js +0 -36
- package/webapp/src/main.js +0 -11
- package/webapp/src/router/index.js +0 -42
- package/webapp/src/utils/colors.js +0 -96
- package/webapp/src/utils/formatters.js +0 -49
- package/webapp/src/views/Analytics.vue +0 -377
- package/webapp/src/views/ConsumerGroups.vue +0 -433
- package/webapp/src/views/Dashboard.vue +0 -418
- package/webapp/src/views/Messages.vue +0 -363
- package/webapp/src/views/QueueDetail.vue +0 -582
- package/webapp/src/views/Queues.vue +0 -496
- package/webapp/tailwind.config.js +0 -25
- package/webapp/vite.config.js +0 -10
- /package/{assets → src/webapp-dist/assets}/queen-logo-blue.svg +0 -0
- /package/{assets → src/webapp-dist/assets}/queen-logo-cyan.svg +0 -0
- /package/{assets → src/webapp-dist/assets}/queen-logo-indigo.svg +0 -0
- /package/{assets → src/webapp-dist/assets}/queen-logo-orange.svg +0 -0
- /package/{assets → src/webapp-dist/assets}/queen-logo-pink.svg +0 -0
- /package/{assets → src/webapp-dist/assets}/queen-logo-purple.svg +0 -0
- /package/{assets → src/webapp-dist/assets}/queen-logo-rose.svg +0 -0
- /package/{assets → src/webapp-dist/assets}/queen-logo.svg +0 -0
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="chart-container">
|
|
3
|
-
<Bar v-if="chartData" :data="chartData" :options="chartOptions" />
|
|
4
|
-
<div v-else class="flex items-center justify-center h-full text-gray-500 text-sm">
|
|
5
|
-
No data available
|
|
6
|
-
</div>
|
|
7
|
-
</div>
|
|
8
|
-
</template>
|
|
9
|
-
|
|
10
|
-
<script setup>
|
|
11
|
-
import { computed } from 'vue';
|
|
12
|
-
import { Bar } from 'vue-chartjs';
|
|
13
|
-
import {
|
|
14
|
-
Chart as ChartJS,
|
|
15
|
-
CategoryScale,
|
|
16
|
-
LinearScale,
|
|
17
|
-
BarElement,
|
|
18
|
-
Title,
|
|
19
|
-
Tooltip,
|
|
20
|
-
Legend,
|
|
21
|
-
} from 'chart.js';
|
|
22
|
-
import { colors } from '../../utils/colors';
|
|
23
|
-
|
|
24
|
-
ChartJS.register(
|
|
25
|
-
CategoryScale,
|
|
26
|
-
LinearScale,
|
|
27
|
-
BarElement,
|
|
28
|
-
Title,
|
|
29
|
-
Tooltip,
|
|
30
|
-
Legend
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
const props = defineProps({
|
|
34
|
-
data: Object,
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const chartData = computed(() => {
|
|
38
|
-
if (!props.data?.throughput?.length) return null;
|
|
39
|
-
|
|
40
|
-
// Reverse the array since API returns data in descending order
|
|
41
|
-
const throughput = [...props.data.throughput].reverse();
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
labels: throughput.map(t => {
|
|
45
|
-
const date = new Date(t.timestamp);
|
|
46
|
-
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
|
47
|
-
}),
|
|
48
|
-
datasets: [
|
|
49
|
-
{
|
|
50
|
-
label: 'Ingested',
|
|
51
|
-
data: throughput.map(t => t.ingestedPerSecond),
|
|
52
|
-
backgroundColor: createGradientBars('rose'),
|
|
53
|
-
borderColor: 'transparent',
|
|
54
|
-
borderWidth: 0,
|
|
55
|
-
borderRadius: 4,
|
|
56
|
-
barPercentage: 0.9,
|
|
57
|
-
categoryPercentage: 0.95,
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
label: 'Processed',
|
|
61
|
-
data: throughput.map(t => t.processedPerSecond),
|
|
62
|
-
backgroundColor: createGradientBars('purple'),
|
|
63
|
-
borderColor: 'transparent',
|
|
64
|
-
borderWidth: 0,
|
|
65
|
-
borderRadius: 4,
|
|
66
|
-
barPercentage: 0.9,
|
|
67
|
-
categoryPercentage: 0.95,
|
|
68
|
-
},
|
|
69
|
-
],
|
|
70
|
-
};
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// Create gradient for bars
|
|
74
|
-
function createGradientBars(color) {
|
|
75
|
-
return (context) => {
|
|
76
|
-
const chart = context.chart;
|
|
77
|
-
const {ctx, chartArea} = chart;
|
|
78
|
-
|
|
79
|
-
if (!chartArea) {
|
|
80
|
-
return color === 'rose' ? colors.charts.ingested.border : colors.charts.processed.border;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const gradient = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
|
|
84
|
-
|
|
85
|
-
if (color === 'rose') {
|
|
86
|
-
gradient.addColorStop(0, 'rgba(244, 63, 94, 0.9)');
|
|
87
|
-
gradient.addColorStop(1, 'rgba(244, 63, 94, 0.5)');
|
|
88
|
-
} else if (color === 'purple') {
|
|
89
|
-
gradient.addColorStop(0, 'rgba(168, 85, 247, 0.9)');
|
|
90
|
-
gradient.addColorStop(1, 'rgba(168, 85, 247, 0.5)');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return gradient;
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const chartOptions = {
|
|
98
|
-
responsive: true,
|
|
99
|
-
maintainAspectRatio: false,
|
|
100
|
-
interaction: {
|
|
101
|
-
mode: 'index',
|
|
102
|
-
intersect: false,
|
|
103
|
-
},
|
|
104
|
-
scales: {
|
|
105
|
-
x: {
|
|
106
|
-
stacked: false,
|
|
107
|
-
grid: {
|
|
108
|
-
display: false,
|
|
109
|
-
},
|
|
110
|
-
border: {
|
|
111
|
-
display: false,
|
|
112
|
-
},
|
|
113
|
-
ticks: {
|
|
114
|
-
maxRotation: 0,
|
|
115
|
-
autoSkipPadding: 20,
|
|
116
|
-
font: {
|
|
117
|
-
size: 10,
|
|
118
|
-
},
|
|
119
|
-
color: '#9ca3af',
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
y: {
|
|
123
|
-
stacked: false,
|
|
124
|
-
beginAtZero: true,
|
|
125
|
-
border: {
|
|
126
|
-
display: false,
|
|
127
|
-
},
|
|
128
|
-
grid: {
|
|
129
|
-
display: false,
|
|
130
|
-
},
|
|
131
|
-
ticks: {
|
|
132
|
-
font: {
|
|
133
|
-
size: 11,
|
|
134
|
-
},
|
|
135
|
-
color: '#9ca3af',
|
|
136
|
-
padding: 8,
|
|
137
|
-
callback: (value) => value.toFixed(1),
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
plugins: {
|
|
142
|
-
legend: {
|
|
143
|
-
display: true,
|
|
144
|
-
position: 'top',
|
|
145
|
-
align: 'end',
|
|
146
|
-
labels: {
|
|
147
|
-
boxWidth: 12,
|
|
148
|
-
boxHeight: 12,
|
|
149
|
-
padding: 15,
|
|
150
|
-
usePointStyle: true,
|
|
151
|
-
pointStyle: 'circle',
|
|
152
|
-
font: {
|
|
153
|
-
size: 12,
|
|
154
|
-
weight: '500',
|
|
155
|
-
},
|
|
156
|
-
color: '#6b7280',
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
tooltip: {
|
|
160
|
-
enabled: true,
|
|
161
|
-
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
|
162
|
-
padding: 12,
|
|
163
|
-
cornerRadius: 8,
|
|
164
|
-
titleFont: {
|
|
165
|
-
size: 13,
|
|
166
|
-
weight: '600',
|
|
167
|
-
},
|
|
168
|
-
bodyFont: {
|
|
169
|
-
size: 12,
|
|
170
|
-
},
|
|
171
|
-
bodySpacing: 6,
|
|
172
|
-
usePointStyle: true,
|
|
173
|
-
callbacks: {
|
|
174
|
-
title: (items) => items[0].label,
|
|
175
|
-
label: (context) => {
|
|
176
|
-
return ` ${context.dataset.label}: ${context.parsed.y.toFixed(2)} msg/s`;
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
</script>
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="table-container scrollbar-thin">
|
|
3
|
-
<table class="table">
|
|
4
|
-
<thead>
|
|
5
|
-
<tr>
|
|
6
|
-
<th>Queue Name</th>
|
|
7
|
-
<th class="text-right">Pending</th>
|
|
8
|
-
<th class="text-right hidden sm:table-cell">Processing</th>
|
|
9
|
-
<th class="text-right">Completed</th>
|
|
10
|
-
</tr>
|
|
11
|
-
</thead>
|
|
12
|
-
<tbody>
|
|
13
|
-
<tr
|
|
14
|
-
v-for="queue in queues"
|
|
15
|
-
:key="queue.id"
|
|
16
|
-
class="cursor-pointer"
|
|
17
|
-
@click="navigateToQueue(queue.name)"
|
|
18
|
-
>
|
|
19
|
-
<td>
|
|
20
|
-
<div class="font-medium text-gray-900 dark:text-gray-100">{{ queue.name }}</div>
|
|
21
|
-
<div v-if="queue.namespace" class="text-xs text-gray-500 dark:text-gray-400">
|
|
22
|
-
{{ queue.namespace }}
|
|
23
|
-
</div>
|
|
24
|
-
</td>
|
|
25
|
-
<td class="text-right font-medium">{{ formatNumber(queue.messages?.pending || 0) }}</td>
|
|
26
|
-
<td class="text-right font-medium hidden sm:table-cell">{{ formatNumber(queue.messages?.processing || 0) }}</td>
|
|
27
|
-
<td class="text-right font-medium text-green-600 dark:text-green-400">
|
|
28
|
-
{{ formatNumber(queue.messages?.total || 0) }}
|
|
29
|
-
</td>
|
|
30
|
-
</tr>
|
|
31
|
-
</tbody>
|
|
32
|
-
</table>
|
|
33
|
-
|
|
34
|
-
<div v-if="!queues?.length" class="text-center py-8 text-gray-500 text-sm">
|
|
35
|
-
No queues available
|
|
36
|
-
</div>
|
|
37
|
-
</div>
|
|
38
|
-
</template>
|
|
39
|
-
|
|
40
|
-
<script setup>
|
|
41
|
-
import { useRouter } from 'vue-router';
|
|
42
|
-
import { formatNumber } from '../../utils/formatters';
|
|
43
|
-
|
|
44
|
-
defineProps({
|
|
45
|
-
queues: Array,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const router = useRouter();
|
|
49
|
-
|
|
50
|
-
function navigateToQueue(queueName) {
|
|
51
|
-
router.push(`/queues/${queueName}`);
|
|
52
|
-
}
|
|
53
|
-
</script>
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="flex h-screen bg-gray-100 dark:bg-[#0d1117] overflow-hidden">
|
|
3
|
-
<!-- Mobile overlay -->
|
|
4
|
-
<div
|
|
5
|
-
v-if="isSidebarOpen && isMobile"
|
|
6
|
-
class="fixed inset-0 bg-black bg-opacity-50 z-20 lg:hidden"
|
|
7
|
-
@click="closeSidebar"
|
|
8
|
-
></div>
|
|
9
|
-
|
|
10
|
-
<!-- Sidebar -->
|
|
11
|
-
<transition
|
|
12
|
-
enter-active-class="transition-all duration-300"
|
|
13
|
-
leave-active-class="transition-all duration-300"
|
|
14
|
-
enter-from-class="-translate-x-full lg:translate-x-0"
|
|
15
|
-
leave-to-class="-translate-x-full lg:translate-x-0"
|
|
16
|
-
>
|
|
17
|
-
<aside
|
|
18
|
-
v-show="isSidebarOpen || !isMobile"
|
|
19
|
-
:class="[
|
|
20
|
-
'fixed lg:relative inset-y-0 left-0 z-30 lg:z-0',
|
|
21
|
-
isCollapsed && !isMobile ? 'w-16' : 'w-64',
|
|
22
|
-
'bg-white dark:bg-[#0a0d14]',
|
|
23
|
-
'transform transition-all duration-300',
|
|
24
|
-
'flex flex-col'
|
|
25
|
-
]"
|
|
26
|
-
>
|
|
27
|
-
<AppSidebar
|
|
28
|
-
:is-collapsed="isCollapsed && !isMobile"
|
|
29
|
-
@close="closeSidebar"
|
|
30
|
-
@toggle-collapse="toggleCollapse"
|
|
31
|
-
@toggle-sidebar-mobile="toggleSidebar"
|
|
32
|
-
/>
|
|
33
|
-
</aside>
|
|
34
|
-
</transition>
|
|
35
|
-
|
|
36
|
-
<!-- Main content - full height, no header -->
|
|
37
|
-
<div class="flex-1 flex flex-col overflow-hidden min-w-0">
|
|
38
|
-
<!-- Hamburger for mobile (floating) -->
|
|
39
|
-
<button
|
|
40
|
-
v-if="isMobile && !isSidebarOpen"
|
|
41
|
-
@click="toggleSidebar"
|
|
42
|
-
class="lg:hidden fixed top-4 left-4 z-10 p-2 rounded-lg bg-white dark:bg-slate-800 shadow-lg hover:bg-gray-100 dark:hover:bg-slate-700 transition-colors"
|
|
43
|
-
aria-label="Toggle menu"
|
|
44
|
-
>
|
|
45
|
-
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
46
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
|
47
|
-
</svg>
|
|
48
|
-
</button>
|
|
49
|
-
|
|
50
|
-
<main class="flex-1 overflow-y-auto scrollbar-thin">
|
|
51
|
-
<slot />
|
|
52
|
-
</main>
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
</template>
|
|
56
|
-
|
|
57
|
-
<script setup>
|
|
58
|
-
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
|
59
|
-
import { useRoute } from 'vue-router';
|
|
60
|
-
import AppSidebar from './AppSidebar.vue';
|
|
61
|
-
|
|
62
|
-
const route = useRoute();
|
|
63
|
-
const isSidebarOpen = ref(false);
|
|
64
|
-
const isCollapsed = ref(false);
|
|
65
|
-
const isMobile = ref(false);
|
|
66
|
-
|
|
67
|
-
function checkMobile() {
|
|
68
|
-
isMobile.value = window.innerWidth < 1024;
|
|
69
|
-
if (!isMobile.value) {
|
|
70
|
-
isSidebarOpen.value = false;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function toggleSidebar() {
|
|
75
|
-
isSidebarOpen.value = !isSidebarOpen.value;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function toggleCollapse() {
|
|
79
|
-
if (!isMobile.value) {
|
|
80
|
-
isCollapsed.value = !isCollapsed.value;
|
|
81
|
-
localStorage.setItem('sidebarCollapsed', isCollapsed.value ? 'true' : 'false');
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function closeSidebar() {
|
|
86
|
-
if (isMobile.value) {
|
|
87
|
-
isSidebarOpen.value = false;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Close mobile sidebar on route change
|
|
92
|
-
watch(() => route.path, () => {
|
|
93
|
-
closeSidebar();
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
onMounted(() => {
|
|
97
|
-
checkMobile();
|
|
98
|
-
window.addEventListener('resize', checkMobile);
|
|
99
|
-
|
|
100
|
-
// Restore collapsed state
|
|
101
|
-
const savedState = localStorage.getItem('sidebarCollapsed');
|
|
102
|
-
if (savedState === 'true') {
|
|
103
|
-
isCollapsed.value = true;
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
onUnmounted(() => {
|
|
108
|
-
window.removeEventListener('resize', checkMobile);
|
|
109
|
-
});
|
|
110
|
-
</script>
|
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="flex flex-col h-full overflow-hidden">
|
|
3
|
-
<!-- Logo section -->
|
|
4
|
-
<div class="p-3 flex-shrink-0 mb-2">
|
|
5
|
-
<div
|
|
6
|
-
v-if="!isCollapsed"
|
|
7
|
-
class="flex items-center gap-3"
|
|
8
|
-
>
|
|
9
|
-
<div class="relative">
|
|
10
|
-
<img src="/assets/queen-logo-rose.svg" alt="Queen" class="h-8 w-8 flex-shrink-0 transition-transform hover:scale-110" />
|
|
11
|
-
<div class="absolute inset-0 bg-rose-500 opacity-0 blur-xl transition-opacity hover:opacity-20 -z-10"></div>
|
|
12
|
-
</div>
|
|
13
|
-
<div class="min-w-0">
|
|
14
|
-
<h1 class="text-base font-bold text-gray-900 dark:text-gray-100 truncate">Queen</h1>
|
|
15
|
-
<p class="text-xs text-gray-500 dark:text-gray-400 truncate">Message Queue</p>
|
|
16
|
-
</div>
|
|
17
|
-
</div>
|
|
18
|
-
<div
|
|
19
|
-
v-else
|
|
20
|
-
class="flex items-center justify-center relative"
|
|
21
|
-
>
|
|
22
|
-
<img src="/assets/queen-logo-rose.svg" alt="Queen" class="h-8 w-8 transition-transform hover:scale-110" />
|
|
23
|
-
<div class="absolute inset-0 bg-rose-500 opacity-0 blur-xl transition-opacity hover:opacity-20 -z-10"></div>
|
|
24
|
-
</div>
|
|
25
|
-
</div>
|
|
26
|
-
|
|
27
|
-
<!-- Navigation -->
|
|
28
|
-
<nav class="flex-1 p-3 space-y-1 overflow-y-auto overflow-x-hidden scrollbar-thin">
|
|
29
|
-
<router-link
|
|
30
|
-
v-for="item in navigation"
|
|
31
|
-
:key="item.path"
|
|
32
|
-
:to="item.path"
|
|
33
|
-
@click="$emit('close')"
|
|
34
|
-
:title="item.name"
|
|
35
|
-
class="flex items-center rounded-lg transition-all duration-200 text-sm group relative"
|
|
36
|
-
:class="[
|
|
37
|
-
isActive(item.path) && !isCollapsed
|
|
38
|
-
? 'bg-rose-500/8 dark:bg-rose-500/10 text-rose-600 dark:text-rose-400 font-medium'
|
|
39
|
-
: isActive(item.path) && isCollapsed
|
|
40
|
-
? 'text-rose-600 dark:text-rose-400 font-medium'
|
|
41
|
-
: 'text-gray-700 dark:text-gray-300 hover:bg-black/5 dark:hover:bg-white/5',
|
|
42
|
-
isCollapsed ? 'px-3 py-3 justify-center' : 'px-3 py-2.5 gap-3'
|
|
43
|
-
]"
|
|
44
|
-
>
|
|
45
|
-
<div :class="[
|
|
46
|
-
'flex items-center justify-center rounded-md transition-all flex-shrink-0',
|
|
47
|
-
isActive(item.path) ? 'w-8 h-8 bg-gradient-to-br from-rose-500 to-purple-500 text-white shadow-sm' : 'w-5 h-5'
|
|
48
|
-
]">
|
|
49
|
-
<svg
|
|
50
|
-
class="transition-transform group-hover:scale-110"
|
|
51
|
-
:class="isActive(item.path) ? 'w-4 h-4' : 'w-5 h-5'"
|
|
52
|
-
fill="none"
|
|
53
|
-
stroke="currentColor"
|
|
54
|
-
viewBox="0 0 24 24"
|
|
55
|
-
>
|
|
56
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="item.iconPath" />
|
|
57
|
-
</svg>
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
<span
|
|
61
|
-
v-if="!isCollapsed"
|
|
62
|
-
class="whitespace-nowrap overflow-hidden text-ellipsis"
|
|
63
|
-
>
|
|
64
|
-
{{ item.name }}
|
|
65
|
-
</span>
|
|
66
|
-
|
|
67
|
-
<!-- Tooltip for collapsed state -->
|
|
68
|
-
<div
|
|
69
|
-
v-if="isCollapsed"
|
|
70
|
-
class="absolute left-full ml-2 px-3 py-1.5 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded-lg opacity-0 pointer-events-none group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 shadow-lg"
|
|
71
|
-
>
|
|
72
|
-
{{ item.name }}
|
|
73
|
-
</div>
|
|
74
|
-
</router-link>
|
|
75
|
-
</nav>
|
|
76
|
-
|
|
77
|
-
<!-- Utilities Section (Desktop: theme, health, refresh, collapse) -->
|
|
78
|
-
<div class="flex-shrink-0 hidden lg:block">
|
|
79
|
-
<div class="h-px bg-gradient-to-r from-transparent via-gray-300 dark:via-gray-700 to-transparent mb-2"></div>
|
|
80
|
-
|
|
81
|
-
<div class="p-3 space-y-1">
|
|
82
|
-
<!-- Refresh Button -->
|
|
83
|
-
<button
|
|
84
|
-
v-if="showRefresh"
|
|
85
|
-
@click="handleRefresh"
|
|
86
|
-
:disabled="isRefreshing"
|
|
87
|
-
:title="isCollapsed ? 'Refresh' : ''"
|
|
88
|
-
class="w-full flex items-center rounded-lg transition-all duration-200 text-sm text-gray-700 dark:text-gray-300 hover:bg-black/5 dark:hover:bg-white/5 group relative"
|
|
89
|
-
:class="isCollapsed ? 'px-3 py-2.5 justify-center' : 'px-3 py-2.5 gap-3'"
|
|
90
|
-
>
|
|
91
|
-
<svg
|
|
92
|
-
class="w-5 h-5 flex-shrink-0 transition-transform"
|
|
93
|
-
:class="{ 'animate-spin': isRefreshing, 'group-hover:rotate-90': !isRefreshing }"
|
|
94
|
-
fill="none"
|
|
95
|
-
stroke="currentColor"
|
|
96
|
-
viewBox="0 0 24 24"
|
|
97
|
-
>
|
|
98
|
-
<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" />
|
|
99
|
-
</svg>
|
|
100
|
-
<span v-if="!isCollapsed">Refresh</span>
|
|
101
|
-
|
|
102
|
-
<div
|
|
103
|
-
v-if="isCollapsed"
|
|
104
|
-
class="absolute left-full ml-2 px-3 py-1.5 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded-lg opacity-0 pointer-events-none group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 shadow-lg"
|
|
105
|
-
>
|
|
106
|
-
Refresh
|
|
107
|
-
</div>
|
|
108
|
-
</button>
|
|
109
|
-
|
|
110
|
-
<!-- Theme Toggle -->
|
|
111
|
-
<button
|
|
112
|
-
@click="toggleTheme"
|
|
113
|
-
:title="isCollapsed ? (isDark ? 'Light mode' : 'Dark mode') : ''"
|
|
114
|
-
class="w-full flex items-center rounded-lg transition-all duration-200 text-sm text-gray-700 dark:text-gray-300 hover:bg-black/5 dark:hover:bg-white/5 group relative"
|
|
115
|
-
:class="isCollapsed ? 'px-3 py-2.5 justify-center' : 'px-3 py-2.5 gap-3'"
|
|
116
|
-
>
|
|
117
|
-
<svg class="w-5 h-5 flex-shrink-0" fill="currentColor" viewBox="0 0 24 24">
|
|
118
|
-
<path v-if="isDark" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
|
|
119
|
-
<path v-else d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" opacity="0.5"/>
|
|
120
|
-
</svg>
|
|
121
|
-
<span v-if="!isCollapsed">{{ isDark ? 'Light Mode' : 'Dark Mode' }}</span>
|
|
122
|
-
|
|
123
|
-
<div
|
|
124
|
-
v-if="isCollapsed"
|
|
125
|
-
class="absolute left-full ml-2 px-3 py-1.5 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded-lg opacity-0 pointer-events-none group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 shadow-lg"
|
|
126
|
-
>
|
|
127
|
-
{{ isDark ? 'Light mode' : 'Dark mode' }}
|
|
128
|
-
</div>
|
|
129
|
-
</button>
|
|
130
|
-
|
|
131
|
-
<!-- Health Status -->
|
|
132
|
-
<div
|
|
133
|
-
:title="isCollapsed ? (isHealthy ? 'System Healthy' : 'System Error') : ''"
|
|
134
|
-
class="w-full flex items-center rounded-lg text-sm group relative"
|
|
135
|
-
:class="[
|
|
136
|
-
isHealthy ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400',
|
|
137
|
-
isCollapsed ? 'px-3 py-2.5 justify-center' : 'px-3 py-2.5 gap-3'
|
|
138
|
-
]"
|
|
139
|
-
>
|
|
140
|
-
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
141
|
-
<path v-if="isHealthy" 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" />
|
|
142
|
-
<path v-else stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
143
|
-
</svg>
|
|
144
|
-
<span v-if="!isCollapsed" class="font-medium">{{ isHealthy ? 'Healthy' : 'Error' }}</span>
|
|
145
|
-
|
|
146
|
-
<div
|
|
147
|
-
v-if="isCollapsed"
|
|
148
|
-
class="absolute left-full ml-2 px-3 py-1.5 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded-lg opacity-0 pointer-events-none group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 shadow-lg"
|
|
149
|
-
>
|
|
150
|
-
{{ isHealthy ? 'System Healthy' : 'System Error' }}
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
153
|
-
|
|
154
|
-
<!-- Collapse Button -->
|
|
155
|
-
<button
|
|
156
|
-
@click="$emit('toggle-collapse')"
|
|
157
|
-
class="w-full flex items-center rounded-lg transition-all duration-200 text-sm text-gray-700 dark:text-gray-300 hover:bg-black/5 dark:hover:bg-white/5 group relative"
|
|
158
|
-
:class="isCollapsed ? 'px-3 py-2.5 justify-center' : 'px-3 py-2.5 gap-3'"
|
|
159
|
-
:title="isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'"
|
|
160
|
-
>
|
|
161
|
-
<svg
|
|
162
|
-
class="w-5 h-5 transition-transform duration-300 flex-shrink-0"
|
|
163
|
-
:class="{ 'rotate-180': isCollapsed }"
|
|
164
|
-
fill="none"
|
|
165
|
-
stroke="currentColor"
|
|
166
|
-
viewBox="0 0 24 24"
|
|
167
|
-
>
|
|
168
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
|
|
169
|
-
</svg>
|
|
170
|
-
<span v-if="!isCollapsed" class="whitespace-nowrap">Collapse</span>
|
|
171
|
-
|
|
172
|
-
<div
|
|
173
|
-
v-if="isCollapsed"
|
|
174
|
-
class="absolute left-full ml-2 px-3 py-1.5 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded-lg opacity-0 pointer-events-none group-hover:opacity-100 transition-opacity whitespace-nowrap z-50 shadow-lg"
|
|
175
|
-
>
|
|
176
|
-
Expand sidebar
|
|
177
|
-
</div>
|
|
178
|
-
</button>
|
|
179
|
-
</div>
|
|
180
|
-
</div>
|
|
181
|
-
|
|
182
|
-
<!-- Mobile utilities (theme and health in header area when sidebar open) -->
|
|
183
|
-
<div v-if="isMobile && isSidebarOpen" class="lg:hidden p-3 border-t border-gray-200 dark:border-gray-700">
|
|
184
|
-
<div class="flex gap-2">
|
|
185
|
-
<button
|
|
186
|
-
@click="toggleTheme"
|
|
187
|
-
class="flex-1 btn btn-secondary text-xs py-2"
|
|
188
|
-
>
|
|
189
|
-
{{ isDark ? '☀️ Light' : '🌙 Dark' }}
|
|
190
|
-
</button>
|
|
191
|
-
<div class="px-3 py-2 bg-gray-100 dark:bg-slate-700 rounded-lg text-xs font-medium flex items-center gap-2">
|
|
192
|
-
<span :class="isHealthy ? 'text-green-600' : 'text-red-600'">
|
|
193
|
-
{{ isHealthy ? '✓' : '✗' }}
|
|
194
|
-
</span>
|
|
195
|
-
{{ isHealthy ? 'Healthy' : 'Error' }}
|
|
196
|
-
</div>
|
|
197
|
-
</div>
|
|
198
|
-
</div>
|
|
199
|
-
</div>
|
|
200
|
-
</template>
|
|
201
|
-
|
|
202
|
-
<script setup>
|
|
203
|
-
import { ref, inject, computed, onMounted } from 'vue';
|
|
204
|
-
import { useRoute } from 'vue-router';
|
|
205
|
-
import { healthApi } from '../../api/health';
|
|
206
|
-
|
|
207
|
-
defineProps({
|
|
208
|
-
isCollapsed: {
|
|
209
|
-
type: Boolean,
|
|
210
|
-
default: false,
|
|
211
|
-
},
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
const emit = defineEmits(['close', 'toggle-collapse', 'toggle-sidebar-mobile']);
|
|
215
|
-
|
|
216
|
-
const route = useRoute();
|
|
217
|
-
const { isDark, toggleTheme } = inject('theme');
|
|
218
|
-
|
|
219
|
-
// Health check
|
|
220
|
-
const health = ref(null);
|
|
221
|
-
const isHealthy = computed(() => health.value?.status === 'healthy');
|
|
222
|
-
const isRefreshing = ref(false);
|
|
223
|
-
|
|
224
|
-
async function checkHealth() {
|
|
225
|
-
try {
|
|
226
|
-
const response = await healthApi.getHealth();
|
|
227
|
-
health.value = response.data;
|
|
228
|
-
} catch (error) {
|
|
229
|
-
health.value = { status: 'unhealthy' };
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Refresh logic
|
|
234
|
-
const showRefresh = computed(() => {
|
|
235
|
-
return ['/', '/queues', '/consumer-groups', '/messages', '/analytics'].includes(route.path);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
function handleRefresh() {
|
|
239
|
-
isRefreshing.value = true;
|
|
240
|
-
|
|
241
|
-
const refreshCallbacks = window.refreshCallbacks || new Map();
|
|
242
|
-
const callback = refreshCallbacks.get(route.path);
|
|
243
|
-
|
|
244
|
-
if (callback) {
|
|
245
|
-
Promise.resolve(callback())
|
|
246
|
-
.finally(() => {
|
|
247
|
-
setTimeout(() => {
|
|
248
|
-
isRefreshing.value = false;
|
|
249
|
-
}, 300);
|
|
250
|
-
});
|
|
251
|
-
} else {
|
|
252
|
-
setTimeout(() => {
|
|
253
|
-
isRefreshing.value = false;
|
|
254
|
-
}, 300);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
onMounted(() => {
|
|
259
|
-
checkHealth();
|
|
260
|
-
setInterval(checkHealth, 30000);
|
|
261
|
-
|
|
262
|
-
// Initialize global callbacks if not exists
|
|
263
|
-
if (!window.refreshCallbacks) {
|
|
264
|
-
window.refreshCallbacks = new Map();
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
window.registerRefreshCallback = (path, callback) => {
|
|
268
|
-
window.refreshCallbacks.set(path, callback);
|
|
269
|
-
};
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
const navigation = [
|
|
273
|
-
{
|
|
274
|
-
name: 'Dashboard',
|
|
275
|
-
path: '/',
|
|
276
|
-
iconPath: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6'
|
|
277
|
-
},
|
|
278
|
-
{
|
|
279
|
-
name: 'Queues',
|
|
280
|
-
path: '/queues',
|
|
281
|
-
iconPath: 'M4 6h16M4 10h16M4 14h16M4 18h16'
|
|
282
|
-
},
|
|
283
|
-
{
|
|
284
|
-
name: 'Consumer Groups',
|
|
285
|
-
path: '/consumer-groups',
|
|
286
|
-
iconPath: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z'
|
|
287
|
-
},
|
|
288
|
-
{
|
|
289
|
-
name: 'Messages',
|
|
290
|
-
path: '/messages',
|
|
291
|
-
iconPath: 'M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z'
|
|
292
|
-
},
|
|
293
|
-
{
|
|
294
|
-
name: 'Analytics',
|
|
295
|
-
path: '/analytics',
|
|
296
|
-
iconPath: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z'
|
|
297
|
-
},
|
|
298
|
-
];
|
|
299
|
-
|
|
300
|
-
const isActive = (path) => {
|
|
301
|
-
if (path === '/') return route.path === '/';
|
|
302
|
-
return route.path.startsWith(path);
|
|
303
|
-
};
|
|
304
|
-
</script>
|