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.
Files changed (160) hide show
  1. package/API.md +862 -752
  2. package/AUTH.md +2044 -0
  3. package/LICENSE.md +202 -0
  4. package/README.md +1705 -1051
  5. package/WEBAPP.md +1889 -0
  6. package/assets/dashboard-01.png +0 -0
  7. package/assets/queen-logo-blue.svg +210 -0
  8. package/assets/queen-logo-cyan.svg +210 -0
  9. package/assets/queen-logo-indigo.svg +210 -0
  10. package/assets/queen-logo-orange.svg +210 -0
  11. package/assets/queen-logo-pink.svg +210 -0
  12. package/assets/queen-logo-purple.svg +210 -0
  13. package/assets/queen-logo-rose.svg +239 -0
  14. package/assets/queen-logo.svg +263 -0
  15. package/examples/batch-processing.js +58 -0
  16. package/examples/test-complete-client.js +260 -0
  17. package/examples/test-dashboard-api.js +200 -0
  18. package/examples/test-traceid.js +147 -0
  19. package/package.json +17 -4
  20. package/server.log +1 -0
  21. package/src/benchmark/consumer.js +207 -0
  22. package/src/benchmark/consumer_multi.js +216 -0
  23. package/src/benchmark/producer.js +75 -0
  24. package/src/benchmark/producer_multi.js +115 -0
  25. package/src/client/client.js +300 -31
  26. package/src/client/queenClient.js +5 -0
  27. package/src/cluster-server.js +242 -0
  28. package/src/config.js +19 -5
  29. package/src/database/connection.js +42 -16
  30. package/src/database/poolManager.js +7 -0
  31. package/src/database/schema-v2.sql +194 -130
  32. package/src/managers/queueManagerOptimized.js +823 -933
  33. package/src/managers/systemEventManager.js +8 -3
  34. package/src/routes/messages.js +127 -57
  35. package/src/routes/pop.js +27 -43
  36. package/src/routes/resources.js +61 -27
  37. package/src/routes/status.js +1037 -0
  38. package/src/server.js +308 -272
  39. package/src/services/evictionService.js +57 -28
  40. package/src/services/retentionService.js +44 -11
  41. package/src/test/MIGRATION_ISSUES.md +174 -0
  42. package/src/test/README.md +203 -0
  43. package/src/test/advanced-pattern-tests.js +1137 -0
  44. package/src/test/bus-mode-tests.js +361 -0
  45. package/src/test/core-tests.js +342 -0
  46. package/src/test/edge-case-tests.js +561 -0
  47. package/src/test/enterprise-tests.js +637 -0
  48. package/src/test/partition-locking-tests.js +545 -0
  49. package/src/test/test-new.js +278 -0
  50. package/src/test/test.js +6 -3
  51. package/src/test/utils.js +169 -0
  52. package/src/utils/streaming.js +231 -0
  53. package/src/utils/uuid.js +2 -2
  54. package/src/websocket/wsServer.js +10 -3
  55. package/test-keepalive-v2.sh +22 -0
  56. package/webapp/COLOR_GUIDE.md +118 -0
  57. package/webapp/README.md +143 -0
  58. package/webapp/index.html +14 -0
  59. package/webapp/package-lock.json +3184 -0
  60. package/webapp/package.json +25 -0
  61. package/webapp/postcss.config.js +7 -0
  62. package/webapp/public/assets/queen-logo-blue.svg +210 -0
  63. package/webapp/public/assets/queen-logo-cyan.svg +210 -0
  64. package/webapp/public/assets/queen-logo-indigo.svg +210 -0
  65. package/webapp/public/assets/queen-logo-orange.svg +210 -0
  66. package/webapp/public/assets/queen-logo-pink.svg +210 -0
  67. package/webapp/public/assets/queen-logo-purple.svg +210 -0
  68. package/webapp/public/assets/queen-logo-rose.svg +239 -0
  69. package/webapp/public/assets/queen-logo.svg +263 -0
  70. package/webapp/src/App.vue +19 -0
  71. package/webapp/src/api/analytics.js +10 -0
  72. package/webapp/src/api/client.js +29 -0
  73. package/webapp/src/api/consumers.js +52 -0
  74. package/webapp/src/api/health.js +7 -0
  75. package/webapp/src/api/messages.js +26 -0
  76. package/webapp/src/api/queues.js +14 -0
  77. package/webapp/src/api/resources.js +8 -0
  78. package/webapp/src/assets/styles/main.css +357 -0
  79. package/webapp/src/components/analytics/AnalyticsFilters.vue +87 -0
  80. package/webapp/src/components/analytics/AnalyticsMetrics.vue +57 -0
  81. package/webapp/src/components/analytics/MessageDistributionChart.vue +111 -0
  82. package/webapp/src/components/analytics/MessageFlowChart.vue +173 -0
  83. package/webapp/src/components/analytics/TimeRangeSelector.vue +27 -0
  84. package/webapp/src/components/analytics/TopQueuesChart.vue +132 -0
  85. package/webapp/src/components/common/ConfirmDialog.vue +56 -0
  86. package/webapp/src/components/common/LoadingSpinner.vue +6 -0
  87. package/webapp/src/components/common/MetricCard.vue +43 -0
  88. package/webapp/src/components/common/StatusBadge.vue +45 -0
  89. package/webapp/src/components/dashboard/MessageStatusCard.vue +50 -0
  90. package/webapp/src/components/dashboard/PerformanceCard.vue +38 -0
  91. package/webapp/src/components/dashboard/ThroughputChart.vue +182 -0
  92. package/webapp/src/components/dashboard/TopQueuesTable.vue +53 -0
  93. package/webapp/src/components/layout/AppLayout.vue +110 -0
  94. package/webapp/src/components/layout/AppSidebar.vue +304 -0
  95. package/webapp/src/components/messages/MessageDetailPanel.vue +242 -0
  96. package/webapp/src/components/messages/MessageFilters.vue +114 -0
  97. package/webapp/src/components/queue-detail/PartitionList.vue +79 -0
  98. package/webapp/src/components/queue-detail/PushMessageModal.vue +175 -0
  99. package/webapp/src/components/queue-detail/QueueConfig.vue +63 -0
  100. package/webapp/src/components/queue-detail/QueueDetailHeader.vue +53 -0
  101. package/webapp/src/components/queue-detail/RecentMessages.vue +76 -0
  102. package/webapp/src/components/queues/CreateQueueModal.vue +193 -0
  103. package/webapp/src/components/queues/QueueFilters.vue +90 -0
  104. package/webapp/src/composables/useApi.js +34 -0
  105. package/webapp/src/composables/useTheme.js +36 -0
  106. package/webapp/src/main.js +11 -0
  107. package/webapp/src/router/index.js +42 -0
  108. package/webapp/src/utils/colors.js +96 -0
  109. package/webapp/src/utils/formatters.js +49 -0
  110. package/webapp/src/views/Analytics.vue +377 -0
  111. package/webapp/src/views/ConsumerGroups.vue +433 -0
  112. package/webapp/src/views/Dashboard.vue +418 -0
  113. package/webapp/src/views/Messages.vue +361 -0
  114. package/webapp/src/views/QueueDetail.vue +582 -0
  115. package/webapp/src/views/Queues.vue +496 -0
  116. package/webapp/tailwind.config.js +25 -0
  117. package/webapp/vite.config.js +10 -0
  118. package/CACHE.md +0 -519
  119. package/DASHBOARD-V3.md +0 -478
  120. package/DASHBOARD.md +0 -382
  121. package/MOD_QUEUE.md +0 -453
  122. package/PARTITION_LOCKING_DESIGN.md +0 -989
  123. package/PLAN.md +0 -707
  124. package/QUERY_ANALSYS.md +0 -72
  125. package/QUEUE_BUS.md +0 -334
  126. package/V2-PLAN.md +0 -236
  127. package/dashboard/.vscode/extensions.json +0 -3
  128. package/dashboard/README.md +0 -5
  129. package/dashboard/index.html +0 -14
  130. package/dashboard/package-lock.json +0 -1458
  131. package/dashboard/package.json +0 -25
  132. package/dashboard/public/vite.svg +0 -1
  133. package/dashboard/src/App.vue +0 -29
  134. package/dashboard/src/assets/styles/main.css +0 -908
  135. package/dashboard/src/assets/vue.svg +0 -1
  136. package/dashboard/src/components/cards/MetricCard.vue +0 -298
  137. package/dashboard/src/components/charts/QueueDepthChart.vue +0 -276
  138. package/dashboard/src/components/charts/QueueLagChart.vue +0 -436
  139. package/dashboard/src/components/charts/ThroughputChart.vue +0 -302
  140. package/dashboard/src/components/common/ActivityFeed.vue +0 -251
  141. package/dashboard/src/components/layout/AppHeader.vue +0 -208
  142. package/dashboard/src/components/layout/AppLayout.vue +0 -88
  143. package/dashboard/src/components/layout/AppSidebar.vue +0 -261
  144. package/dashboard/src/main.js +0 -44
  145. package/dashboard/src/router.js +0 -54
  146. package/dashboard/src/services/api.js +0 -187
  147. package/dashboard/src/services/websocket.js +0 -167
  148. package/dashboard/src/utils/constants.js +0 -56
  149. package/dashboard/src/utils/helpers.js +0 -118
  150. package/dashboard/src/views/Analytics.vue +0 -912
  151. package/dashboard/src/views/Dashboard.vue +0 -906
  152. package/dashboard/src/views/Messages.vue +0 -437
  153. package/dashboard/src/views/QueueDetail.vue +0 -501
  154. package/dashboard/src/views/Queues.vue +0 -333
  155. package/dashboard/vite.config.js +0 -30
  156. package/debug-namespace.js +0 -110
  157. package/docs/long-polling.md +0 -159
  158. package/docs/multi-server-cache-solutions.md +0 -185
  159. package/docs/performance-tuning.md +0 -222
  160. package/src/routes/analytics.js +0 -812
@@ -1,501 +0,0 @@
1
- <template>
2
- <div class="queue-detail">
3
- <div class="detail-header">
4
- <div class="header-info">
5
- <Button
6
- icon="pi pi-arrow-left"
7
- class="p-button-text back-btn"
8
- @click="$router.push('/queues')"
9
- v-tooltip="'Back to Queues'"
10
- />
11
- <h1>{{ queueName }}</h1>
12
- </div>
13
- <div class="header-actions">
14
- <Select
15
- v-model="selectedTimeRange"
16
- :options="timeRangeOptions || []"
17
- optionLabel="label"
18
- optionValue="value"
19
- placeholder="Time range"
20
- @change="onTimeRangeChange"
21
- class="time-filter"
22
- />
23
- <Button
24
- label="Refresh"
25
- icon="pi pi-refresh"
26
- class="btn-primary"
27
- @click="fetchQueueDetails"
28
- :loading="loading"
29
- />
30
- </div>
31
- </div>
32
-
33
- <!-- Queue Information Card -->
34
- <div class="card-v3 info-card">
35
- <h3 class="card-title">Queue Information</h3>
36
- <div class="info-grid">
37
- <div class="info-item">
38
- <span class="info-label">Namespace:</span>
39
- <span class="info-value">{{ queueInfo.namespace || '-' }}</span>
40
- </div>
41
- <div class="info-item">
42
- <span class="info-label">Task:</span>
43
- <span class="info-value">{{ queueInfo.task || '-' }}</span>
44
- </div>
45
- <div class="info-item">
46
- <span class="info-label">Created:</span>
47
- <span class="info-value">{{ formatDate(queueInfo.created) }}</span>
48
- </div>
49
- <div class="info-item">
50
- <span class="info-label">Total Messages:</span>
51
- <span class="info-value highlight">{{ queueInfo.totalMessages || 0 }}</span>
52
- </div>
53
- </div>
54
- </div>
55
-
56
- <!-- Partitions Table -->
57
- <div class="card-v3">
58
- <h3 class="card-title">Partitions</h3>
59
- <DataTable
60
- :value="partitions"
61
- :loading="loading"
62
- class="dark-table-v3"
63
- >
64
- <Column field="name" header="PARTITION NAME">
65
- <template #body="{ data }">
66
- <span class="partition-name">{{ data.name }}</span>
67
- </template>
68
- </Column>
69
- <Column field="priority" header="PRIORITY">
70
- <template #body="{ data }">
71
- <span class="priority-value">{{ data.priority }}</span>
72
- </template>
73
- </Column>
74
- <Column header="STATS">
75
- <template #body="{ data }">
76
- <div class="stats-badges">
77
- <span class="status-pending">{{ data.pending || 0 }} pending</span>
78
- <span class="status-processing">{{ data.processing || 0 }} processing</span>
79
- <span class="status-completed">{{ data.completed || 0 }} completed</span>
80
- </div>
81
- </template>
82
- </Column>
83
- </DataTable>
84
- </div>
85
-
86
- <!-- Message Activity Chart -->
87
- <div class="chart-container" style="margin-top: 1.5rem;">
88
- <div class="chart-header">
89
- <h3 class="chart-title">Message Activity</h3>
90
- <Button
91
- icon="pi pi-refresh"
92
- class="p-button-text p-button-sm"
93
- @click="fetchQueueDetails"
94
- v-tooltip="'Refresh'"
95
- />
96
- </div>
97
- <ThroughputChart :data="activityData" :loading="loading" />
98
- </div>
99
- </div>
100
- </template>
101
-
102
- <script setup>
103
- import { ref, onMounted, computed, watch } from 'vue'
104
- import { useRoute } from 'vue-router'
105
- import { useToast } from 'primevue/usetoast'
106
- import DataTable from 'primevue/datatable'
107
- import Column from 'primevue/column'
108
- import Button from 'primevue/button'
109
- import Select from 'primevue/select'
110
- import ThroughputChart from '../components/charts/ThroughputChart.vue'
111
- import api from '../services/api.js'
112
-
113
- const route = useRoute()
114
- const toast = useToast()
115
- const loading = ref(false)
116
-
117
- const queueName = computed(() => route.params.name || 'Unknown Queue')
118
-
119
- // Time range filter
120
- const selectedTimeRange = ref('1h')
121
- const timeRangeOptions = ref([
122
- { label: 'Last Hour', value: '1h' },
123
- { label: 'Last 6 Hours', value: '6h' },
124
- { label: 'Last 24 Hours', value: '24h' },
125
- { label: 'Last 7 Days', value: '7d' },
126
- { label: 'Last 30 Days', value: '30d' }
127
- ])
128
-
129
- const queueInfo = ref({
130
- namespace: null,
131
- task: null,
132
- created: null,
133
- totalMessages: 0
134
- })
135
-
136
- const partitions = ref([])
137
-
138
- const activityData = ref({
139
- labels: [],
140
- datasets: []
141
- })
142
-
143
- const fetchQueueDetails = async () => {
144
- try {
145
- loading.value = true
146
-
147
- // Build time range filter
148
- const filters = {}
149
- const now = new Date()
150
- const from = new Date()
151
-
152
- switch (selectedTimeRange.value) {
153
- case '1h':
154
- from.setHours(from.getHours() - 1)
155
- break
156
- case '6h':
157
- from.setHours(from.getHours() - 6)
158
- break
159
- case '24h':
160
- from.setDate(from.getDate() - 1)
161
- break
162
- case '7d':
163
- from.setDate(from.getDate() - 7)
164
- break
165
- case '30d':
166
- from.setDate(from.getDate() - 30)
167
- break
168
- }
169
-
170
- filters.fromDateTime = from.toISOString()
171
- filters.toDateTime = now.toISOString()
172
-
173
- // Fetch real queue data from API with filters
174
- const [queueData, analyticsData, partitionData] = await Promise.all([
175
- api.getQueueDetail(queueName.value).catch(() => null),
176
- api.getQueueAnalytics(queueName.value, filters).catch(() => null),
177
- api.getPartitions({ queue: queueName.value }).catch(() => null)
178
- ])
179
-
180
- // Update queue info
181
- if (queueData) {
182
- queueInfo.value = {
183
- namespace: queueData.namespace || null,
184
- task: queueData.task || null,
185
- created: queueData.created || new Date().toISOString(),
186
- totalMessages: queueData.stats?.total ||
187
- (queueData.stats?.pending || 0) +
188
- (queueData.stats?.processing || 0) +
189
- (queueData.stats?.completed || 0) +
190
- (queueData.stats?.failed || 0)
191
- }
192
- }
193
-
194
- // Process partition data
195
- if (partitionData && partitionData.partitions) {
196
- partitions.value = partitionData.partitions.map(p => ({
197
- name: p.name || p.id || 'Unknown',
198
- priority: p.priority || 0,
199
- pending: p.stats?.pending || 0,
200
- processing: p.stats?.processing || 0,
201
- completed: p.stats?.completed || 0
202
- }))
203
- } else {
204
- // Default partitions if no data
205
- partitions.value = [
206
- { name: 'Default', priority: 0, pending: 0, processing: 0, completed: 0 }
207
- ]
208
- }
209
-
210
- // Process throughput data for activity chart
211
- if (analyticsData && analyticsData.throughput && analyticsData.throughput.length > 0) {
212
- const recentData = analyticsData.throughput.slice(-12)
213
-
214
- activityData.value = {
215
- labels: recentData.map(item => {
216
- const date = new Date(item.timestamp)
217
- return date.toLocaleTimeString('en-US', {
218
- hour: 'numeric',
219
- minute: '2-digit'
220
- })
221
- }),
222
- datasets: [
223
- {
224
- label: 'Incoming Messages',
225
- data: recentData.map(item => item.incoming || 0)
226
- },
227
- {
228
- label: 'Completed Messages',
229
- data: recentData.map(item => item.completed || 0)
230
- },
231
- {
232
- label: 'Failed Messages',
233
- data: recentData.map(item => item.failed || 0)
234
- }
235
- ]
236
- }
237
- } else {
238
- // Generate placeholder data if no real data available
239
- const now = new Date()
240
- const placeholderData = []
241
- for (let i = 11; i >= 0; i--) {
242
- const time = new Date(now.getTime() - i * 5 * 60 * 1000)
243
- placeholderData.push({
244
- timestamp: time.toISOString(),
245
- incoming: 0,
246
- completed: 0,
247
- failed: 0
248
- })
249
- }
250
-
251
- activityData.value = {
252
- labels: placeholderData.map(item => {
253
- const date = new Date(item.timestamp)
254
- return date.toLocaleTimeString('en-US', {
255
- hour: 'numeric',
256
- minute: '2-digit'
257
- })
258
- }),
259
- datasets: [
260
- {
261
- label: 'Incoming Messages',
262
- data: placeholderData.map(() => 0)
263
- },
264
- {
265
- label: 'Completed Messages',
266
- data: placeholderData.map(() => 0)
267
- },
268
- {
269
- label: 'Failed Messages',
270
- data: placeholderData.map(() => 0)
271
- }
272
- ]
273
- }
274
- }
275
-
276
- } catch (error) {
277
- console.error('Failed to fetch queue details:', error)
278
- toast.add({
279
- severity: 'error',
280
- summary: 'Error',
281
- detail: 'Failed to load queue details',
282
- life: 3000
283
- })
284
- } finally {
285
- loading.value = false
286
- }
287
- }
288
-
289
- const formatDate = (dateString) => {
290
- if (!dateString) return '-'
291
- const date = new Date(dateString)
292
- return date.toLocaleString()
293
- }
294
-
295
- // Handle time range change
296
- const onTimeRangeChange = () => {
297
- fetchQueueDetails()
298
- }
299
-
300
- // Watch for queue name changes
301
- watch(() => route.params.name, (newName) => {
302
- if (newName) {
303
- fetchQueueDetails()
304
- }
305
- })
306
-
307
- onMounted(() => {
308
- fetchQueueDetails()
309
- })
310
- </script>
311
-
312
- <style scoped>
313
- .queue-detail {
314
- padding: 0;
315
- }
316
-
317
- .detail-header {
318
- display: flex;
319
- justify-content: space-between;
320
- align-items: center;
321
- margin-bottom: 1.5rem;
322
- }
323
-
324
- .header-actions {
325
- display: flex;
326
- gap: 1rem;
327
- align-items: center;
328
- }
329
-
330
- .time-filter {
331
- min-width: 150px;
332
- }
333
-
334
- .time-filter :deep(.p-select) {
335
- background: rgba(255, 255, 255, 0.05);
336
- border: 1px solid rgba(255, 255, 255, 0.1);
337
- border-radius: 8px;
338
- }
339
-
340
- .time-filter :deep(.p-select:hover) {
341
- border-color: rgba(236, 72, 153, 0.5);
342
- }
343
-
344
- .time-filter :deep(.p-select-label) {
345
- color: #ffffff;
346
- }
347
-
348
- .header-info {
349
- display: flex;
350
- align-items: center;
351
- gap: 1rem;
352
- }
353
-
354
- .header-info h1 {
355
- font-size: 1.5rem;
356
- font-weight: 600;
357
- color: var(--surface-700);
358
- margin: 0;
359
- }
360
-
361
- .back-btn {
362
- color: var(--surface-500) !important;
363
- }
364
-
365
- .back-btn:hover {
366
- background: rgba(236, 72, 153, 0.1) !important;
367
- color: var(--primary-500) !important;
368
- }
369
-
370
- .card-v3 {
371
- background: transparent;
372
- border: 1px solid rgba(255, 255, 255, 0.1);
373
- border-radius: 16px;
374
- padding: 1.5rem;
375
- position: relative;
376
- overflow: hidden;
377
- margin-bottom: 1.5rem;
378
- }
379
-
380
- .card-title {
381
- font-size: 1.125rem;
382
- font-weight: 600;
383
- color: var(--surface-700);
384
- margin: 0 0 1.5rem 0;
385
- }
386
-
387
- .info-card {
388
- margin-bottom: 1.5rem;
389
- }
390
-
391
- .info-grid {
392
- display: grid;
393
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
394
- gap: 1.5rem;
395
- }
396
-
397
- .info-item {
398
- display: flex;
399
- flex-direction: column;
400
- gap: 0.5rem;
401
- }
402
-
403
- .info-label {
404
- font-size: 0.75rem;
405
- color: var(--surface-400);
406
- text-transform: uppercase;
407
- letter-spacing: 0.05em;
408
- font-weight: 500;
409
- }
410
-
411
- .info-value {
412
- font-size: 1rem;
413
- color: var(--surface-600);
414
- font-weight: 500;
415
- }
416
-
417
- .info-value.highlight {
418
- color: var(--primary-500);
419
- font-size: 1.25rem;
420
- font-weight: 600;
421
- }
422
-
423
- .partition-name {
424
- color: var(--surface-600);
425
- font-weight: 500;
426
- }
427
-
428
- .priority-value {
429
- color: var(--surface-500);
430
- }
431
-
432
- .stats-badges {
433
- display: flex;
434
- gap: 0.75rem;
435
- }
436
-
437
- .chart-container {
438
- background: transparent;
439
- border: 1px solid rgba(255, 255, 255, 0.1);
440
- border-radius: 16px;
441
- padding: 1.5rem;
442
- height: 400px;
443
- display: flex;
444
- flex-direction: column;
445
- }
446
-
447
- .chart-header {
448
- display: flex;
449
- justify-content: space-between;
450
- align-items: center;
451
- margin-bottom: 1.5rem;
452
- }
453
-
454
- .chart-title {
455
- font-size: 1.125rem;
456
- font-weight: 600;
457
- color: var(--surface-700);
458
- margin: 0;
459
- }
460
-
461
- /* DataTable dark theme overrides */
462
- :deep(.dark-table-v3) {
463
- background: transparent !important;
464
- border: none !important;
465
- }
466
-
467
- :deep(.dark-table-v3 .p-datatable-thead > tr > th) {
468
- background: transparent !important;
469
- color: var(--surface-400) !important;
470
- border-color: rgba(255, 255, 255, 0.1) !important;
471
- text-transform: uppercase;
472
- font-size: 0.75rem;
473
- letter-spacing: 0.05em;
474
- }
475
-
476
- :deep(.dark-table-v3 .p-datatable-tbody > tr) {
477
- background: transparent !important;
478
- }
479
-
480
- :deep(.dark-table-v3 .p-datatable-tbody > tr:hover) {
481
- background: rgba(236, 72, 153, 0.05) !important;
482
- }
483
-
484
- :deep(.dark-table-v3 .p-datatable-tbody > tr > td) {
485
- color: var(--surface-600) !important;
486
- border-color: rgba(255, 255, 255, 0.05) !important;
487
- }
488
-
489
- /* Responsive */
490
- @media (max-width: 768px) {
491
- .info-grid {
492
- grid-template-columns: repeat(2, 1fr);
493
- }
494
- }
495
-
496
- @media (max-width: 480px) {
497
- .info-grid {
498
- grid-template-columns: 1fr;
499
- }
500
- }
501
- </style>