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,433 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="page-flat">
|
|
3
|
+
<div class="py-4 px-3">
|
|
4
|
+
<div class="space-y-2.5">
|
|
5
|
+
<!-- Info Card -->
|
|
6
|
+
<div class="info-banner">
|
|
7
|
+
<div class="flex gap-3">
|
|
8
|
+
<svg class="w-5 h-5 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
9
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
10
|
+
</svg>
|
|
11
|
+
<div class="text-sm">
|
|
12
|
+
<p class="font-semibold mb-1">Consumer Groups Overview</p>
|
|
13
|
+
<p class="text-purple-800 dark:text-purple-200">
|
|
14
|
+
Consumer groups enable multiple consumers to process messages in parallel without duplication.
|
|
15
|
+
Each group maintains its own consumption cursor per partition.
|
|
16
|
+
</p>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<!-- Filters -->
|
|
22
|
+
<div class="filter-flat">
|
|
23
|
+
<div class="flex flex-col sm:flex-row gap-3">
|
|
24
|
+
<div class="flex-1">
|
|
25
|
+
<input
|
|
26
|
+
v-model="searchQuery"
|
|
27
|
+
type="text"
|
|
28
|
+
placeholder="Search consumer groups..."
|
|
29
|
+
class="input"
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<select v-model="statusFilter" class="input sm:w-48">
|
|
34
|
+
<option value="">All States</option>
|
|
35
|
+
<option value="Stable">Stable</option>
|
|
36
|
+
<option value="Lagging">Lagging</option>
|
|
37
|
+
<option value="Dead">Dead</option>
|
|
38
|
+
</select>
|
|
39
|
+
|
|
40
|
+
<button
|
|
41
|
+
v-if="searchQuery || statusFilter"
|
|
42
|
+
@click="clearFilters"
|
|
43
|
+
class="btn btn-secondary whitespace-nowrap"
|
|
44
|
+
>
|
|
45
|
+
Clear Filters
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<LoadingSpinner v-if="loading && !consumerGroups.length" />
|
|
51
|
+
|
|
52
|
+
<div v-else-if="error" class="error-flat">
|
|
53
|
+
<p><strong>Error loading consumer groups:</strong> {{ error }}</p>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<!-- Consumer Groups Stats -->
|
|
57
|
+
<div class="grid grid-cols-2 lg:grid-cols-4 gap-2">
|
|
58
|
+
<div class="metric-flat">
|
|
59
|
+
<div class="flex items-start gap-3">
|
|
60
|
+
<div class="metric-icon-flat bg-green-500/10 dark:bg-green-500/20">
|
|
61
|
+
<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
62
|
+
<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" />
|
|
63
|
+
</svg>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="flex-1">
|
|
66
|
+
<p class="metric-label">Active Groups</p>
|
|
67
|
+
<p class="metric-value-flat">{{ stats.activeGroups }}</p>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="metric-flat">
|
|
72
|
+
<div class="flex items-start gap-3">
|
|
73
|
+
<div class="metric-icon-flat bg-purple-500/10 dark:bg-purple-500/20">
|
|
74
|
+
<svg class="w-5 h-5 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
75
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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" />
|
|
76
|
+
</svg>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="flex-1">
|
|
79
|
+
<p class="metric-label">Total Consumers</p>
|
|
80
|
+
<p class="metric-value-flat">{{ stats.totalConsumers }}</p>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="metric-flat">
|
|
85
|
+
<div class="flex items-start gap-3">
|
|
86
|
+
<div class="metric-icon-flat bg-rose-500/10 dark:bg-rose-500/20">
|
|
87
|
+
<svg class="w-5 h-5 text-rose-600 dark:text-rose-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
88
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
|
89
|
+
</svg>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="flex-1">
|
|
92
|
+
<p class="metric-label">Topics Monitored</p>
|
|
93
|
+
<p class="metric-value-flat">{{ stats.totalTopics }}</p>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="metric-flat">
|
|
98
|
+
<div class="flex items-start gap-3">
|
|
99
|
+
<div class="metric-icon-flat bg-orange-500/10 dark:bg-orange-500/20">
|
|
100
|
+
<svg class="w-5 h-5 text-orange-600 dark:text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
101
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
102
|
+
</svg>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="flex-1">
|
|
105
|
+
<p class="metric-label">Avg Lag</p>
|
|
106
|
+
<p class="metric-value-flat">{{ stats.avgLag }}</p>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<!-- Consumer Groups Table -->
|
|
113
|
+
<div class="table-section">
|
|
114
|
+
<div class="table-container scrollbar-thin">
|
|
115
|
+
<table class="table">
|
|
116
|
+
<thead>
|
|
117
|
+
<tr>
|
|
118
|
+
<th>Consumer Group</th>
|
|
119
|
+
<th class="hidden md:table-cell">Topics</th>
|
|
120
|
+
<th class="text-right">Members</th>
|
|
121
|
+
<th class="text-right hidden lg:table-cell">Offset Lag</th>
|
|
122
|
+
<th class="text-right">Time Lag</th>
|
|
123
|
+
<th class="text-right">State</th>
|
|
124
|
+
</tr>
|
|
125
|
+
</thead>
|
|
126
|
+
<tbody>
|
|
127
|
+
<tr v-for="group in filteredGroups" :key="group.name">
|
|
128
|
+
<td>
|
|
129
|
+
<div class="font-medium text-gray-900 dark:text-gray-100">{{ group.name }}</div>
|
|
130
|
+
<div class="text-xs text-gray-500 dark:text-gray-400 md:hidden">
|
|
131
|
+
{{ group.topics.length }} topic{{ group.topics.length !== 1 ? 's' : '' }}
|
|
132
|
+
</div>
|
|
133
|
+
</td>
|
|
134
|
+
<td class="hidden md:table-cell">
|
|
135
|
+
<div class="flex flex-wrap gap-1">
|
|
136
|
+
<span
|
|
137
|
+
v-for="topic in group.topics.slice(0, 2)"
|
|
138
|
+
:key="topic"
|
|
139
|
+
class="badge badge-info text-xs"
|
|
140
|
+
>
|
|
141
|
+
{{ topic }}
|
|
142
|
+
</span>
|
|
143
|
+
<span v-if="group.topics.length > 2" class="text-xs text-gray-500">
|
|
144
|
+
+{{ group.topics.length - 2 }} more
|
|
145
|
+
</span>
|
|
146
|
+
</div>
|
|
147
|
+
</td>
|
|
148
|
+
<td class="text-right font-medium">{{ group.members }}</td>
|
|
149
|
+
<td class="text-right font-medium hidden lg:table-cell">
|
|
150
|
+
{{ formatNumber(group.totalLag) }}
|
|
151
|
+
</td>
|
|
152
|
+
<td class="text-right font-medium">
|
|
153
|
+
{{ group.maxTimeLag > 0 ? formatDuration(group.maxTimeLag) : '0ms' }}
|
|
154
|
+
</td>
|
|
155
|
+
<td class="text-right">
|
|
156
|
+
<StatusBadge :status="group.state" />
|
|
157
|
+
</td>
|
|
158
|
+
</tr>
|
|
159
|
+
</tbody>
|
|
160
|
+
</table>
|
|
161
|
+
|
|
162
|
+
<div v-if="!filteredGroups.length && !loading" class="text-center py-12 text-gray-500 text-sm">
|
|
163
|
+
<svg class="w-12 h-12 mx-auto mb-3 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
164
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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" />
|
|
165
|
+
</svg>
|
|
166
|
+
<p>No consumer groups found</p>
|
|
167
|
+
<p class="text-xs mt-2">Consumer groups are created automatically when consumers connect with a consumerGroup parameter.</p>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</template>
|
|
175
|
+
|
|
176
|
+
<script setup>
|
|
177
|
+
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
|
178
|
+
import { consumersApi } from '../api/consumers';
|
|
179
|
+
import { formatNumber, formatDuration } from '../utils/formatters';
|
|
180
|
+
|
|
181
|
+
import StatusBadge from '../components/common/StatusBadge.vue';
|
|
182
|
+
import LoadingSpinner from '../components/common/LoadingSpinner.vue';
|
|
183
|
+
|
|
184
|
+
const loading = ref(false);
|
|
185
|
+
const error = ref(null);
|
|
186
|
+
const consumerGroups = ref([]);
|
|
187
|
+
const searchQuery = ref('');
|
|
188
|
+
const statusFilter = ref('');
|
|
189
|
+
|
|
190
|
+
const filteredGroups = computed(() => {
|
|
191
|
+
let filtered = consumerGroups.value;
|
|
192
|
+
|
|
193
|
+
if (searchQuery.value) {
|
|
194
|
+
const search = searchQuery.value.toLowerCase();
|
|
195
|
+
filtered = filtered.filter(g =>
|
|
196
|
+
g.name.toLowerCase().includes(search) ||
|
|
197
|
+
g.topics.some(t => t.toLowerCase().includes(search))
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (statusFilter.value) {
|
|
202
|
+
filtered = filtered.filter(g => g.state === statusFilter.value);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return filtered;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const stats = computed(() => {
|
|
209
|
+
const groups = consumerGroups.value;
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
activeGroups: groups.filter(g => g.state === 'Stable').length,
|
|
213
|
+
totalConsumers: groups.reduce((sum, g) => sum + g.members, 0),
|
|
214
|
+
totalTopics: new Set(groups.flatMap(g => g.topics)).size,
|
|
215
|
+
avgLag: groups.length > 0
|
|
216
|
+
? Math.round(groups.reduce((sum, g) => sum + g.totalLag, 0) / groups.length)
|
|
217
|
+
: 0,
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
function clearFilters() {
|
|
222
|
+
searchQuery.value = '';
|
|
223
|
+
statusFilter.value = '';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function loadData() {
|
|
227
|
+
loading.value = true;
|
|
228
|
+
error.value = null;
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
const groups = await consumersApi.getConsumerGroups();
|
|
232
|
+
consumerGroups.value = groups;
|
|
233
|
+
} catch (err) {
|
|
234
|
+
error.value = err.message;
|
|
235
|
+
console.error('Consumer groups error:', err);
|
|
236
|
+
} finally {
|
|
237
|
+
loading.value = false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
onMounted(() => {
|
|
242
|
+
loadData();
|
|
243
|
+
|
|
244
|
+
if (window.registerRefreshCallback) {
|
|
245
|
+
window.registerRefreshCallback('/consumer-groups', loadData);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
onUnmounted(() => {
|
|
250
|
+
if (window.registerRefreshCallback) {
|
|
251
|
+
window.registerRefreshCallback('/consumer-groups', null);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
</script>
|
|
255
|
+
|
|
256
|
+
<style scoped>
|
|
257
|
+
.page-flat {
|
|
258
|
+
min-height: 100%;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.info-banner {
|
|
262
|
+
background: #ffffff;
|
|
263
|
+
border: none;
|
|
264
|
+
box-shadow: none;
|
|
265
|
+
border-radius: 0.75rem;
|
|
266
|
+
padding: 1rem;
|
|
267
|
+
color: inherit;
|
|
268
|
+
border-left: 3px solid rgba(168, 85, 247, 0.3);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.dark .info-banner {
|
|
272
|
+
background: #0a0d14;
|
|
273
|
+
border-left-color: rgba(168, 85, 247, 0.5);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.filter-flat {
|
|
277
|
+
background: #ffffff;
|
|
278
|
+
border: none;
|
|
279
|
+
box-shadow: none;
|
|
280
|
+
border-radius: 0.75rem;
|
|
281
|
+
padding: 1rem;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.dark .filter-flat {
|
|
285
|
+
background: #0a0d14;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.filter-flat :deep(.input) {
|
|
289
|
+
background: transparent;
|
|
290
|
+
border: 1px solid rgba(156, 163, 175, 0.15);
|
|
291
|
+
transition: all 0.2s ease;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.filter-flat :deep(.input:hover) {
|
|
295
|
+
border-color: rgba(156, 163, 175, 0.25);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.filter-flat :deep(.input:focus) {
|
|
299
|
+
background: rgba(244, 63, 94, 0.02);
|
|
300
|
+
border-color: rgba(244, 63, 94, 0.4);
|
|
301
|
+
box-shadow: 0 0 0 3px rgba(244, 63, 94, 0.05);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.dark .filter-flat :deep(.input) {
|
|
305
|
+
border-color: rgba(156, 163, 175, 0.1);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.dark .filter-flat :deep(.input:focus) {
|
|
309
|
+
background: rgba(244, 63, 94, 0.03);
|
|
310
|
+
border-color: rgba(244, 63, 94, 0.5);
|
|
311
|
+
box-shadow: 0 0 0 3px rgba(244, 63, 94, 0.08);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.metric-flat {
|
|
315
|
+
background: #ffffff;
|
|
316
|
+
border: none;
|
|
317
|
+
box-shadow: none;
|
|
318
|
+
border-radius: 0.75rem;
|
|
319
|
+
padding: 1rem;
|
|
320
|
+
transition: all 0.3s ease;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.dark .metric-flat {
|
|
324
|
+
background: #0a0d14;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.metric-flat:hover {
|
|
328
|
+
background: #fafafa;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.dark .metric-flat:hover {
|
|
332
|
+
background: #0d1117;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.metric-value-flat {
|
|
336
|
+
font-size: 1.5rem;
|
|
337
|
+
font-weight: 700;
|
|
338
|
+
line-height: 1.1;
|
|
339
|
+
margin-top: 0.125rem;
|
|
340
|
+
background: linear-gradient(135deg, #f43f5e 0%, #ec4899 50%, #a855f7 100%);
|
|
341
|
+
-webkit-background-clip: text;
|
|
342
|
+
-webkit-text-fill-color: transparent;
|
|
343
|
+
background-clip: text;
|
|
344
|
+
letter-spacing: -0.02em;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.table-section {
|
|
348
|
+
background: #ffffff;
|
|
349
|
+
border: none;
|
|
350
|
+
box-shadow: none;
|
|
351
|
+
border-radius: 0.75rem;
|
|
352
|
+
padding: 1rem 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.dark .table-section {
|
|
356
|
+
background: #0a0d14;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.metric-icon-flat {
|
|
360
|
+
width: 2rem;
|
|
361
|
+
height: 2rem;
|
|
362
|
+
border-radius: 0.5rem;
|
|
363
|
+
display: flex;
|
|
364
|
+
align-items: center;
|
|
365
|
+
justify-content: center;
|
|
366
|
+
flex-shrink: 0;
|
|
367
|
+
transition: all 0.3s ease;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.metric-flat:hover .metric-icon-flat {
|
|
371
|
+
transform: scale(1.05);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/* Flat table styling */
|
|
375
|
+
.table-section :deep(.table) {
|
|
376
|
+
border-collapse: separate;
|
|
377
|
+
border-spacing: 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.table-section :deep(.table thead) {
|
|
381
|
+
background: transparent;
|
|
382
|
+
border-bottom: 1px solid rgba(156, 163, 175, 0.08);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.dark .table-section :deep(.table thead) {
|
|
386
|
+
border-bottom-color: rgba(156, 163, 175, 0.1);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.table-section :deep(.table th) {
|
|
390
|
+
padding: 1rem 1rem;
|
|
391
|
+
font-weight: 600;
|
|
392
|
+
letter-spacing: 0.05em;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.table-section :deep(.table tbody tr) {
|
|
396
|
+
border: none;
|
|
397
|
+
transition: all 0.15s ease;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.table-section :deep(.table tbody tr:nth-child(even)) {
|
|
401
|
+
background: rgba(0, 0, 0, 0.015);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.dark .table-section :deep(.table tbody tr:nth-child(even)) {
|
|
405
|
+
background: rgba(255, 255, 255, 0.02);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.table-section :deep(.table tbody tr:hover) {
|
|
409
|
+
background: rgba(244, 63, 94, 0.03);
|
|
410
|
+
box-shadow: inset 3px 0 0 0 rgba(244, 63, 94, 0.6);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.dark .table-section :deep(.table tbody tr:hover) {
|
|
414
|
+
background: rgba(244, 63, 94, 0.05);
|
|
415
|
+
box-shadow: inset 3px 0 0 0 rgba(244, 63, 94, 0.8);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.table-section :deep(.table td) {
|
|
419
|
+
padding: 0.875rem 1rem;
|
|
420
|
+
border: none;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.error-flat {
|
|
424
|
+
background: transparent;
|
|
425
|
+
color: #dc2626;
|
|
426
|
+
font-size: 0.875rem;
|
|
427
|
+
padding: 1rem;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.dark .error-flat {
|
|
431
|
+
color: #fca5a5;
|
|
432
|
+
}
|
|
433
|
+
</style>
|