queen-mq 0.1.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/API.md +1116 -0
- package/CACHE.md +519 -0
- package/DASHBOARD-V3.md +478 -0
- package/DASHBOARD.md +382 -0
- package/MOD_QUEUE.md +453 -0
- package/PARTITION_LOCKING_DESIGN.md +989 -0
- package/PLAN.md +707 -0
- package/QUERY_ANALSYS.md +72 -0
- package/QUEUE_BUS.md +334 -0
- package/README.md +1495 -0
- package/V2-PLAN.md +236 -0
- package/assets/dashboard.png +0 -0
- package/dashboard/.vscode/extensions.json +3 -0
- package/dashboard/README.md +5 -0
- package/dashboard/index.html +14 -0
- package/dashboard/package-lock.json +1458 -0
- package/dashboard/package.json +25 -0
- package/dashboard/public/vite.svg +1 -0
- package/dashboard/src/App.vue +29 -0
- package/dashboard/src/assets/styles/main.css +908 -0
- package/dashboard/src/assets/vue.svg +1 -0
- package/dashboard/src/components/cards/MetricCard.vue +298 -0
- package/dashboard/src/components/charts/QueueDepthChart.vue +276 -0
- package/dashboard/src/components/charts/QueueLagChart.vue +436 -0
- package/dashboard/src/components/charts/ThroughputChart.vue +302 -0
- package/dashboard/src/components/common/ActivityFeed.vue +251 -0
- package/dashboard/src/components/layout/AppHeader.vue +208 -0
- package/dashboard/src/components/layout/AppLayout.vue +88 -0
- package/dashboard/src/components/layout/AppSidebar.vue +261 -0
- package/dashboard/src/main.js +44 -0
- package/dashboard/src/router.js +54 -0
- package/dashboard/src/services/api.js +187 -0
- package/dashboard/src/services/websocket.js +167 -0
- package/dashboard/src/utils/constants.js +56 -0
- package/dashboard/src/utils/helpers.js +118 -0
- package/dashboard/src/views/Analytics.vue +912 -0
- package/dashboard/src/views/Dashboard.vue +906 -0
- package/dashboard/src/views/Messages.vue +437 -0
- package/dashboard/src/views/QueueDetail.vue +501 -0
- package/dashboard/src/views/Queues.vue +333 -0
- package/dashboard/vite.config.js +30 -0
- package/debug-namespace.js +110 -0
- package/docs/long-polling.md +159 -0
- package/docs/multi-server-cache-solutions.md +185 -0
- package/docs/performance-tuning.md +222 -0
- package/examples/bus-mode.js +239 -0
- package/examples/continuous-consumer-optimized.js +215 -0
- package/examples/continuous-consumer.js +159 -0
- package/examples/continuous-producer.js +343 -0
- package/examples/mixed-mode.js +277 -0
- package/examples/multi-server-test.js +305 -0
- package/examples/single.js +64 -0
- package/examples/smartchat-dealyed.js +42 -0
- package/examples/smartchat.js +52 -0
- package/examples/test-cache-invalidation.js +119 -0
- package/examples/test-cache-multi-server.js +245 -0
- package/examples/test-minimal-client.js +112 -0
- package/examples/test-queue-creation-policy.js +137 -0
- package/init-db.js +20 -0
- package/package.json +36 -0
- package/src/client/client.js +291 -0
- package/src/client/index.js +6 -0
- package/src/client/queenClient.js +513 -0
- package/src/client/utils/http.js +172 -0
- package/src/client/utils/loadBalancer.js +152 -0
- package/src/client/utils/retry.js +35 -0
- package/src/config.js +215 -0
- package/src/database/connection.js +103 -0
- package/src/database/poolManager.js +192 -0
- package/src/database/schema-v2.sql +214 -0
- package/src/managers/eventManager.js +59 -0
- package/src/managers/queueManagerOptimized.js +1512 -0
- package/src/managers/resourceCache.js +96 -0
- package/src/managers/systemEventManager.js +127 -0
- package/src/routes/ack.js +26 -0
- package/src/routes/analytics.js +812 -0
- package/src/routes/configure.js +46 -0
- package/src/routes/messages.js +298 -0
- package/src/routes/pop.js +85 -0
- package/src/routes/push.js +28 -0
- package/src/routes/resources.js +296 -0
- package/src/server.js +1286 -0
- package/src/services/encryptionService.js +82 -0
- package/src/services/evictionService.js +131 -0
- package/src/services/retentionService.js +129 -0
- package/src/services/startupSync.js +35 -0
- package/src/test/test.js +4521 -0
- package/src/utils/logger.js +44 -0
- package/src/utils/uuid.js +5 -0
- package/src/websocket/wsServer.js +221 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { API_BASE_URL, API_VERSION } from '../utils/constants.js'
|
|
2
|
+
|
|
3
|
+
class ApiService {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.baseUrl = `${API_BASE_URL}${API_VERSION}`
|
|
6
|
+
this.headers = {
|
|
7
|
+
'Content-Type': 'application/json'
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async request(endpoint, options = {}) {
|
|
12
|
+
const url = endpoint.startsWith('http') ? endpoint : `${this.baseUrl}${endpoint}`
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetch(url, {
|
|
16
|
+
...options,
|
|
17
|
+
headers: {
|
|
18
|
+
...this.headers,
|
|
19
|
+
...options.headers
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
if (response.status === 204) {
|
|
24
|
+
return null // No content
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const data = await response.json()
|
|
28
|
+
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(data.error || `HTTP error! status: ${response.status}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return data
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error('API request failed:', error)
|
|
36
|
+
throw error
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// GET request
|
|
41
|
+
async get(endpoint, params = {}) {
|
|
42
|
+
const queryString = new URLSearchParams(params).toString()
|
|
43
|
+
const url = queryString ? `${endpoint}?${queryString}` : endpoint
|
|
44
|
+
return this.request(url, { method: 'GET' })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// POST request
|
|
48
|
+
async post(endpoint, data = {}) {
|
|
49
|
+
return this.request(endpoint, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
body: JSON.stringify(data)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// PUT request
|
|
56
|
+
async put(endpoint, data = {}) {
|
|
57
|
+
return this.request(endpoint, {
|
|
58
|
+
method: 'PUT',
|
|
59
|
+
body: JSON.stringify(data)
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// DELETE request
|
|
64
|
+
async delete(endpoint) {
|
|
65
|
+
return this.request(endpoint, { method: 'DELETE' })
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Health & Metrics
|
|
69
|
+
async getHealth() {
|
|
70
|
+
return this.request(`${API_BASE_URL}/health`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async getMetrics() {
|
|
74
|
+
return this.request(`${API_BASE_URL}/metrics`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Resources
|
|
78
|
+
async getSystemOverview() {
|
|
79
|
+
return this.get('/resources/overview')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async getQueues(params = {}) {
|
|
83
|
+
return this.get('/resources/queues', params)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async getQueueDetail(queueName) {
|
|
87
|
+
return this.get(`/resources/queues/${encodeURIComponent(queueName)}`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async getPartitions(params = {}) {
|
|
91
|
+
return this.get('/resources/partitions', params)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async getNamespaces() {
|
|
95
|
+
return this.get('/resources/namespaces')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async getTasks() {
|
|
99
|
+
return this.get('/resources/tasks')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Analytics
|
|
103
|
+
async getQueueAnalytics(queueName, params = {}) {
|
|
104
|
+
return this.get(`/analytics/queue/${encodeURIComponent(queueName)}`, params)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async getAllQueuesAnalytics(params = {}) {
|
|
108
|
+
return this.get('/analytics/queues', params)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async getQueueDepths() {
|
|
112
|
+
return this.get('/analytics/queue-depths')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async getThroughput() {
|
|
116
|
+
return this.get('/analytics/throughput')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async getQueueStats(params = {}) {
|
|
120
|
+
return this.get('/analytics/queue-stats', params)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async getQueueLag(params = {}) {
|
|
124
|
+
return this.get('/analytics/queue-lag', params)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Messages
|
|
128
|
+
async getMessages(params = {}) {
|
|
129
|
+
return this.get('/messages', params)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async getMessage(transactionId) {
|
|
133
|
+
return this.get(`/messages/${transactionId}`)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async getRelatedMessages(transactionId) {
|
|
137
|
+
return this.get(`/messages/${transactionId}/related`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async deleteMessage(transactionId) {
|
|
141
|
+
return this.delete(`/messages/${transactionId}`)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async retryMessage(transactionId) {
|
|
145
|
+
return this.post(`/messages/${transactionId}/retry`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async moveMessageToDLQ(transactionId) {
|
|
149
|
+
return this.post(`/messages/${transactionId}/dlq`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async clearQueue(queueName, partition = null) {
|
|
153
|
+
const params = partition ? { partition } : {}
|
|
154
|
+
return this.delete(`/queues/${encodeURIComponent(queueName)}/clear`, params)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Push & Pop
|
|
158
|
+
async pushMessages(items) {
|
|
159
|
+
return this.post('/push', { items })
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async popMessages(queue, partition = null, options = {}) {
|
|
163
|
+
const endpoint = partition
|
|
164
|
+
? `/pop/queue/${encodeURIComponent(queue)}/partition/${encodeURIComponent(partition)}`
|
|
165
|
+
: `/pop/queue/${encodeURIComponent(queue)}`
|
|
166
|
+
return this.get(endpoint, options)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Acknowledge
|
|
170
|
+
async acknowledgeMessage(transactionId, status, error = null) {
|
|
171
|
+
const data = { transactionId, status }
|
|
172
|
+
if (error) data.error = error
|
|
173
|
+
return this.post('/ack', data)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async acknowledgeBatch(acknowledgments) {
|
|
177
|
+
return this.post('/ack/batch', { acknowledgments })
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Configuration
|
|
181
|
+
async configurePartition(queue, partition = 'Default', options = {}) {
|
|
182
|
+
return this.post('/configure', { queue, partition, options })
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Export singleton instance
|
|
187
|
+
export default new ApiService()
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { WS_URL } from '../utils/constants.js'
|
|
2
|
+
|
|
3
|
+
class WebSocketService {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.ws = null
|
|
6
|
+
this.reconnectAttempts = 0
|
|
7
|
+
this.maxReconnectAttempts = 5
|
|
8
|
+
this.reconnectDelay = 1000
|
|
9
|
+
this.listeners = new Map()
|
|
10
|
+
this.isConnected = false
|
|
11
|
+
this.pingInterval = null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
connect() {
|
|
15
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
16
|
+
return Promise.resolve()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
try {
|
|
21
|
+
this.ws = new WebSocket(WS_URL)
|
|
22
|
+
|
|
23
|
+
this.ws.onopen = () => {
|
|
24
|
+
console.log('WebSocket connected')
|
|
25
|
+
this.isConnected = true
|
|
26
|
+
this.reconnectAttempts = 0
|
|
27
|
+
this.startPing()
|
|
28
|
+
this.emit('connected')
|
|
29
|
+
resolve()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.ws.onmessage = (event) => {
|
|
33
|
+
if (event.data === 'pong') {
|
|
34
|
+
return // Ignore pong messages
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const data = JSON.parse(event.data)
|
|
39
|
+
this.handleMessage(data)
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error('Failed to parse WebSocket message:', error)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.ws.onerror = (error) => {
|
|
46
|
+
console.error('WebSocket error:', error)
|
|
47
|
+
this.emit('error', error)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.ws.onclose = () => {
|
|
51
|
+
console.log('WebSocket disconnected')
|
|
52
|
+
this.isConnected = false
|
|
53
|
+
this.stopPing()
|
|
54
|
+
this.emit('disconnected')
|
|
55
|
+
this.reconnect()
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
reject(error)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
disconnect() {
|
|
64
|
+
this.stopPing()
|
|
65
|
+
if (this.ws) {
|
|
66
|
+
this.ws.close()
|
|
67
|
+
this.ws = null
|
|
68
|
+
}
|
|
69
|
+
this.isConnected = false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
reconnect() {
|
|
73
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
74
|
+
console.error('Max reconnection attempts reached')
|
|
75
|
+
this.emit('reconnect_failed')
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.reconnectAttempts++
|
|
80
|
+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1)
|
|
81
|
+
|
|
82
|
+
console.log(`Reconnecting in ${delay}ms... (attempt ${this.reconnectAttempts})`)
|
|
83
|
+
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
this.connect()
|
|
86
|
+
}, delay)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
startPing() {
|
|
90
|
+
this.pingInterval = setInterval(() => {
|
|
91
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
92
|
+
this.ws.send('ping')
|
|
93
|
+
}
|
|
94
|
+
}, 30000) // Ping every 30 seconds
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
stopPing() {
|
|
98
|
+
if (this.pingInterval) {
|
|
99
|
+
clearInterval(this.pingInterval)
|
|
100
|
+
this.pingInterval = null
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
handleMessage(data) {
|
|
105
|
+
const { event, data: eventData, timestamp } = data
|
|
106
|
+
|
|
107
|
+
// Emit event to all listeners
|
|
108
|
+
this.emit(event, { ...eventData, timestamp })
|
|
109
|
+
|
|
110
|
+
// Also emit a general 'message' event
|
|
111
|
+
this.emit('message', data)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
subscribe(queues = []) {
|
|
115
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
116
|
+
this.ws.send(JSON.stringify({
|
|
117
|
+
type: 'subscribe',
|
|
118
|
+
queues
|
|
119
|
+
}))
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
on(event, callback) {
|
|
124
|
+
if (!this.listeners.has(event)) {
|
|
125
|
+
this.listeners.set(event, [])
|
|
126
|
+
}
|
|
127
|
+
this.listeners.get(event).push(callback)
|
|
128
|
+
|
|
129
|
+
// Return unsubscribe function
|
|
130
|
+
return () => this.off(event, callback)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
off(event, callback) {
|
|
134
|
+
if (!this.listeners.has(event)) return
|
|
135
|
+
|
|
136
|
+
const callbacks = this.listeners.get(event)
|
|
137
|
+
const index = callbacks.indexOf(callback)
|
|
138
|
+
|
|
139
|
+
if (index > -1) {
|
|
140
|
+
callbacks.splice(index, 1)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (callbacks.length === 0) {
|
|
144
|
+
this.listeners.delete(event)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
emit(event, data = {}) {
|
|
149
|
+
if (!this.listeners.has(event)) return
|
|
150
|
+
|
|
151
|
+
const callbacks = this.listeners.get(event)
|
|
152
|
+
callbacks.forEach(callback => {
|
|
153
|
+
try {
|
|
154
|
+
callback(data)
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error(`Error in WebSocket listener for event ${event}:`, error)
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getConnectionStatus() {
|
|
162
|
+
return this.isConnected
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Export singleton instance
|
|
167
|
+
export default new WebSocketService()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// API Configuration
|
|
2
|
+
export const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:6632'
|
|
3
|
+
export const API_VERSION = '/api/v1'
|
|
4
|
+
export const WS_URL = import.meta.env.VITE_WS_URL || 'ws://localhost:6632/ws/dashboard'
|
|
5
|
+
|
|
6
|
+
// Refresh intervals (in milliseconds)
|
|
7
|
+
export const REFRESH_INTERVALS = {
|
|
8
|
+
DASHBOARD: 5000, // 5 seconds
|
|
9
|
+
ANALYTICS: 30000, // 30 seconds
|
|
10
|
+
QUEUE_DEPTH: 5000, // 5 seconds
|
|
11
|
+
SYSTEM_STATS: 10000 // 10 seconds
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Message statuses
|
|
15
|
+
export const MESSAGE_STATUS = {
|
|
16
|
+
PENDING: 'pending',
|
|
17
|
+
PROCESSING: 'processing',
|
|
18
|
+
COMPLETED: 'completed',
|
|
19
|
+
FAILED: 'failed',
|
|
20
|
+
DEAD_LETTER: 'dead_letter'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Status colors for UI
|
|
24
|
+
export const STATUS_COLORS = {
|
|
25
|
+
[MESSAGE_STATUS.PENDING]: '#f59e0b', // amber
|
|
26
|
+
[MESSAGE_STATUS.PROCESSING]: '#3b82f6', // blue
|
|
27
|
+
[MESSAGE_STATUS.COMPLETED]: '#10b981', // green
|
|
28
|
+
[MESSAGE_STATUS.FAILED]: '#ef4444', // red
|
|
29
|
+
[MESSAGE_STATUS.DEAD_LETTER]: '#6b7280' // gray
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Chart configuration
|
|
33
|
+
export const CHART_COLORS = [
|
|
34
|
+
'#3b82f6', // blue
|
|
35
|
+
'#10b981', // green
|
|
36
|
+
'#f59e0b', // amber
|
|
37
|
+
'#ef4444', // red
|
|
38
|
+
'#8b5cf6', // purple
|
|
39
|
+
'#ec4899', // pink
|
|
40
|
+
'#14b8a6', // teal
|
|
41
|
+
'#f97316', // orange
|
|
42
|
+
'#06b6d4', // cyan
|
|
43
|
+
'#84cc16' // lime
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
// Pagination
|
|
47
|
+
export const DEFAULT_PAGE_SIZE = 20
|
|
48
|
+
export const PAGE_SIZE_OPTIONS = [10, 20, 50, 100]
|
|
49
|
+
|
|
50
|
+
// Date formats
|
|
51
|
+
export const DATE_FORMAT = {
|
|
52
|
+
SHORT: 'MMM DD, HH:mm',
|
|
53
|
+
LONG: 'MMM DD, YYYY HH:mm:ss',
|
|
54
|
+
TIME_ONLY: 'HH:mm:ss',
|
|
55
|
+
DATE_ONLY: 'MMM DD, YYYY'
|
|
56
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Format numbers with abbreviations (1.2K, 3.4M, etc.)
|
|
2
|
+
export function formatNumber(num) {
|
|
3
|
+
if (num === null || num === undefined) return '0'
|
|
4
|
+
|
|
5
|
+
const absNum = Math.abs(num)
|
|
6
|
+
if (absNum >= 1e9) return (num / 1e9).toFixed(1) + 'B'
|
|
7
|
+
if (absNum >= 1e6) return (num / 1e6).toFixed(1) + 'M'
|
|
8
|
+
if (absNum >= 1e3) return (num / 1e3).toFixed(1) + 'K'
|
|
9
|
+
return num.toString()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Format bytes to human readable
|
|
13
|
+
export function formatBytes(bytes, decimals = 2) {
|
|
14
|
+
if (!bytes) return '0 Bytes'
|
|
15
|
+
|
|
16
|
+
const k = 1024
|
|
17
|
+
const dm = decimals < 0 ? 0 : decimals
|
|
18
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
|
19
|
+
|
|
20
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
21
|
+
|
|
22
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Format date to relative time (e.g., "2 hours ago")
|
|
26
|
+
export function formatRelativeTime(date) {
|
|
27
|
+
if (!date) return ''
|
|
28
|
+
|
|
29
|
+
const now = new Date()
|
|
30
|
+
const then = new Date(date)
|
|
31
|
+
const seconds = Math.floor((now - then) / 1000)
|
|
32
|
+
|
|
33
|
+
if (seconds < 60) return 'just now'
|
|
34
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)} min ago`
|
|
35
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago`
|
|
36
|
+
if (seconds < 604800) return `${Math.floor(seconds / 86400)} days ago`
|
|
37
|
+
|
|
38
|
+
return then.toLocaleDateString()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Format date to local string
|
|
42
|
+
export function formatDate(date, format = 'long') {
|
|
43
|
+
if (!date) return ''
|
|
44
|
+
|
|
45
|
+
const d = new Date(date)
|
|
46
|
+
|
|
47
|
+
switch (format) {
|
|
48
|
+
case 'short':
|
|
49
|
+
return d.toLocaleString('en-US', {
|
|
50
|
+
month: 'short',
|
|
51
|
+
day: 'numeric',
|
|
52
|
+
hour: '2-digit',
|
|
53
|
+
minute: '2-digit'
|
|
54
|
+
})
|
|
55
|
+
case 'time':
|
|
56
|
+
return d.toLocaleTimeString('en-US', {
|
|
57
|
+
hour: '2-digit',
|
|
58
|
+
minute: '2-digit',
|
|
59
|
+
second: '2-digit'
|
|
60
|
+
})
|
|
61
|
+
case 'date':
|
|
62
|
+
return d.toLocaleDateString('en-US', {
|
|
63
|
+
year: 'numeric',
|
|
64
|
+
month: 'short',
|
|
65
|
+
day: 'numeric'
|
|
66
|
+
})
|
|
67
|
+
default:
|
|
68
|
+
return d.toLocaleString('en-US', {
|
|
69
|
+
year: 'numeric',
|
|
70
|
+
month: 'short',
|
|
71
|
+
day: 'numeric',
|
|
72
|
+
hour: '2-digit',
|
|
73
|
+
minute: '2-digit',
|
|
74
|
+
second: '2-digit'
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Calculate percentage
|
|
80
|
+
export function calculatePercentage(value, total) {
|
|
81
|
+
if (!total || total === 0) return 0
|
|
82
|
+
return Math.round((value / total) * 100)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Get status severity for PrimeVue components
|
|
86
|
+
export function getStatusSeverity(status) {
|
|
87
|
+
const severityMap = {
|
|
88
|
+
'pending': 'warn',
|
|
89
|
+
'processing': 'info',
|
|
90
|
+
'completed': 'success',
|
|
91
|
+
'failed': 'danger',
|
|
92
|
+
'dead_letter': 'secondary'
|
|
93
|
+
}
|
|
94
|
+
return severityMap[status] || 'secondary'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Debounce function for search inputs
|
|
98
|
+
export function debounce(func, wait) {
|
|
99
|
+
let timeout
|
|
100
|
+
return function executedFunction(...args) {
|
|
101
|
+
const later = () => {
|
|
102
|
+
clearTimeout(timeout)
|
|
103
|
+
func(...args)
|
|
104
|
+
}
|
|
105
|
+
clearTimeout(timeout)
|
|
106
|
+
timeout = setTimeout(later, wait)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Deep clone object
|
|
111
|
+
export function deepClone(obj) {
|
|
112
|
+
return JSON.parse(JSON.stringify(obj))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Generate unique ID
|
|
116
|
+
export function generateId() {
|
|
117
|
+
return Date.now().toString(36) + Math.random().toString(36).substr(2)
|
|
118
|
+
}
|