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.
Files changed (135) hide show
  1. package/README.md +467 -1775
  2. package/package.json +12 -14
  3. package/src/client/client.js +664 -5
  4. package/src/managers/queueManagerOptimized.js +250 -47
  5. package/src/routes/ack.js +3 -3
  6. package/src/server.js +131 -3
  7. package/src/test/advanced-client-tests.js +761 -0
  8. package/src/test/advanced-pattern-tests.js +2 -2
  9. package/src/test/bus-mode-tests.js +6 -6
  10. package/src/test/core-tests.js +1 -1
  11. package/src/test/edge-case-tests.js +2 -2
  12. package/src/test/test-new.js +36 -1
  13. package/src/webapp-dist/assets/Analytics-DJrE4Rz2.css +1 -0
  14. package/src/webapp-dist/assets/Analytics-HEMXKmfJ.js +1 -0
  15. package/src/webapp-dist/assets/ConfirmDialog-YEoCdE7x.js +1 -0
  16. package/src/webapp-dist/assets/ConsumerGroups-CC5VJywp.css +1 -0
  17. package/src/webapp-dist/assets/ConsumerGroups-D8Z20HhZ.js +1 -0
  18. package/src/webapp-dist/assets/Dashboard-BRFOz6it.css +1 -0
  19. package/src/webapp-dist/assets/Dashboard-Bpx9I32h.js +1 -0
  20. package/src/webapp-dist/assets/LoadingSpinner-BPttXcbs.js +1 -0
  21. package/src/webapp-dist/assets/Messages-BOfVHKma.css +1 -0
  22. package/src/webapp-dist/assets/Messages-EKURCbLl.js +1 -0
  23. package/src/webapp-dist/assets/QueueDetail-BXXA7xoZ.css +1 -0
  24. package/src/webapp-dist/assets/QueueDetail-Bmxg0KiA.js +1 -0
  25. package/src/webapp-dist/assets/Queues-CvrJkrZT.css +1 -0
  26. package/src/webapp-dist/assets/Queues-yUx9_3-9.js +1 -0
  27. package/src/webapp-dist/assets/StatusBadge-C1bBtXHh.css +1 -0
  28. package/src/webapp-dist/assets/StatusBadge-C6N84XmO.js +1 -0
  29. package/src/webapp-dist/assets/analytics-BapTekLM.js +1 -0
  30. package/src/webapp-dist/assets/colors-5z6Q_kUr.js +18 -0
  31. package/src/webapp-dist/assets/index-B7w2bKT6.css +1 -0
  32. package/src/webapp-dist/assets/index-CUSnwYTH.js +31 -0
  33. package/src/webapp-dist/assets/messages-CRV6E-xU.js +1 -0
  34. package/src/webapp-dist/assets/queues-BdUbxfBC.js +1 -0
  35. package/src/webapp-dist/assets/resources-Bs9U-ceF.js +1 -0
  36. package/{webapp → src/webapp-dist}/index.html +2 -1
  37. package/API.md +0 -1226
  38. package/AUTH.md +0 -2044
  39. package/CHANGELOG_STATIC_SERVING.md +0 -213
  40. package/DEPLOYMENT.md +0 -425
  41. package/PROGRAMMATIC_SERVER.md +0 -239
  42. package/WEBAPP.md +0 -1889
  43. package/assets/dashboard-01.png +0 -0
  44. package/assets/dashboard.png +0 -0
  45. package/examples/batch-processing.js +0 -58
  46. package/examples/bus-mode.js +0 -239
  47. package/examples/continuous-consumer-optimized.js +0 -215
  48. package/examples/continuous-consumer.js +0 -159
  49. package/examples/continuous-producer.js +0 -343
  50. package/examples/mixed-mode.js +0 -277
  51. package/examples/multi-server-test.js +0 -305
  52. package/examples/programmatic-server.js +0 -58
  53. package/examples/server-with-frontend.js +0 -46
  54. package/examples/single.js +0 -64
  55. package/examples/smartchat-dealyed.js +0 -42
  56. package/examples/smartchat.js +0 -52
  57. package/examples/test-cache-invalidation.js +0 -119
  58. package/examples/test-cache-multi-server.js +0 -245
  59. package/examples/test-complete-client.js +0 -260
  60. package/examples/test-connection-recovery.js +0 -66
  61. package/examples/test-dashboard-api.js +0 -200
  62. package/examples/test-minimal-client.js +0 -112
  63. package/examples/test-queue-creation-policy.js +0 -137
  64. package/examples/test-traceid.js +0 -147
  65. package/server.log +0 -1
  66. package/test-keepalive-v2.sh +0 -22
  67. package/webapp/COLOR_GUIDE.md +0 -118
  68. package/webapp/README.md +0 -143
  69. package/webapp/package-lock.json +0 -3184
  70. package/webapp/package.json +0 -25
  71. package/webapp/postcss.config.js +0 -7
  72. package/webapp/public/assets/queen-logo-blue.svg +0 -210
  73. package/webapp/public/assets/queen-logo-cyan.svg +0 -210
  74. package/webapp/public/assets/queen-logo-indigo.svg +0 -210
  75. package/webapp/public/assets/queen-logo-orange.svg +0 -210
  76. package/webapp/public/assets/queen-logo-pink.svg +0 -210
  77. package/webapp/public/assets/queen-logo-purple.svg +0 -210
  78. package/webapp/public/assets/queen-logo-rose.svg +0 -239
  79. package/webapp/public/assets/queen-logo.svg +0 -263
  80. package/webapp/src/App.vue +0 -19
  81. package/webapp/src/api/analytics.js +0 -10
  82. package/webapp/src/api/client.js +0 -34
  83. package/webapp/src/api/consumers.js +0 -52
  84. package/webapp/src/api/health.js +0 -7
  85. package/webapp/src/api/messages.js +0 -26
  86. package/webapp/src/api/queues.js +0 -14
  87. package/webapp/src/api/resources.js +0 -8
  88. package/webapp/src/assets/styles/main.css +0 -357
  89. package/webapp/src/components/analytics/AnalyticsFilters.vue +0 -87
  90. package/webapp/src/components/analytics/AnalyticsMetrics.vue +0 -57
  91. package/webapp/src/components/analytics/MessageDistributionChart.vue +0 -111
  92. package/webapp/src/components/analytics/MessageFlowChart.vue +0 -173
  93. package/webapp/src/components/analytics/TimeRangeSelector.vue +0 -27
  94. package/webapp/src/components/analytics/TopQueuesChart.vue +0 -132
  95. package/webapp/src/components/common/ConfirmDialog.vue +0 -56
  96. package/webapp/src/components/common/LoadingSpinner.vue +0 -6
  97. package/webapp/src/components/common/MetricCard.vue +0 -43
  98. package/webapp/src/components/common/StatusBadge.vue +0 -45
  99. package/webapp/src/components/dashboard/MessageStatusCard.vue +0 -50
  100. package/webapp/src/components/dashboard/PerformanceCard.vue +0 -38
  101. package/webapp/src/components/dashboard/ThroughputChart.vue +0 -182
  102. package/webapp/src/components/dashboard/TopQueuesTable.vue +0 -53
  103. package/webapp/src/components/layout/AppLayout.vue +0 -110
  104. package/webapp/src/components/layout/AppSidebar.vue +0 -304
  105. package/webapp/src/components/messages/MessageDetailPanel.vue +0 -242
  106. package/webapp/src/components/messages/MessageFilters.vue +0 -101
  107. package/webapp/src/components/queue-detail/PartitionList.vue +0 -79
  108. package/webapp/src/components/queue-detail/PushMessageModal.vue +0 -175
  109. package/webapp/src/components/queue-detail/QueueConfig.vue +0 -63
  110. package/webapp/src/components/queue-detail/QueueDetailHeader.vue +0 -53
  111. package/webapp/src/components/queue-detail/RecentMessages.vue +0 -76
  112. package/webapp/src/components/queues/CreateQueueModal.vue +0 -193
  113. package/webapp/src/components/queues/QueueFilters.vue +0 -90
  114. package/webapp/src/composables/useApi.js +0 -34
  115. package/webapp/src/composables/useTheme.js +0 -36
  116. package/webapp/src/main.js +0 -11
  117. package/webapp/src/router/index.js +0 -42
  118. package/webapp/src/utils/colors.js +0 -96
  119. package/webapp/src/utils/formatters.js +0 -49
  120. package/webapp/src/views/Analytics.vue +0 -377
  121. package/webapp/src/views/ConsumerGroups.vue +0 -433
  122. package/webapp/src/views/Dashboard.vue +0 -418
  123. package/webapp/src/views/Messages.vue +0 -363
  124. package/webapp/src/views/QueueDetail.vue +0 -582
  125. package/webapp/src/views/Queues.vue +0 -496
  126. package/webapp/tailwind.config.js +0 -25
  127. package/webapp/vite.config.js +0 -10
  128. /package/{assets → src/webapp-dist/assets}/queen-logo-blue.svg +0 -0
  129. /package/{assets → src/webapp-dist/assets}/queen-logo-cyan.svg +0 -0
  130. /package/{assets → src/webapp-dist/assets}/queen-logo-indigo.svg +0 -0
  131. /package/{assets → src/webapp-dist/assets}/queen-logo-orange.svg +0 -0
  132. /package/{assets → src/webapp-dist/assets}/queen-logo-pink.svg +0 -0
  133. /package/{assets → src/webapp-dist/assets}/queen-logo-purple.svg +0 -0
  134. /package/{assets → src/webapp-dist/assets}/queen-logo-rose.svg +0 -0
  135. /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>