sidekiq_queue_manager 1.0.1 → 1.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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/sidekiq_queue_manager/application.js +859 -12
- data/app/assets/stylesheets/sidekiq_queue_manager/application.css +467 -7
- data/app/controllers/sidekiq_queue_manager/dashboard_controller.rb +202 -0
- data/app/services/sidekiq_queue_manager/queue_service.rb +485 -0
- data/app/views/sidekiq_queue_manager/dashboard/index.html.erb +287 -19
- data/config/routes.rb +21 -0
- data/lib/sidekiq_queue_manager/engine.rb +12 -1
- data/lib/sidekiq_queue_manager/version.rb +1 -1
- metadata +1 -1
@@ -62,7 +62,13 @@ class SidekiqQueueManagerUI {
|
|
62
62
|
retryCount: 0,
|
63
63
|
lastUpdate: null,
|
64
64
|
livePullEnabled: false,
|
65
|
-
currentTheme: this.getStoredTheme() || this.gemConfig.theme
|
65
|
+
currentTheme: this.getStoredTheme() || this.gemConfig.theme,
|
66
|
+
activeTab: 'queues',
|
67
|
+
pagination: {
|
68
|
+
scheduled: { page: 1, per_page: 25, filter: '' },
|
69
|
+
retries: { page: 1, per_page: 25, filter: '' },
|
70
|
+
dead: { page: 1, per_page: 25, filter: '' }
|
71
|
+
}
|
66
72
|
};
|
67
73
|
|
68
74
|
this.currentMenuCloseHandler = null;
|
@@ -81,6 +87,7 @@ class SidekiqQueueManagerUI {
|
|
81
87
|
this.initializeTheme();
|
82
88
|
this.cacheElements();
|
83
89
|
this.setupEventListeners();
|
90
|
+
this.setupTabSystem();
|
84
91
|
this.initializeRefreshControl();
|
85
92
|
this.loadInitialData();
|
86
93
|
this.injectActionsMenuStyles();
|
@@ -215,10 +222,19 @@ class SidekiqQueueManagerUI {
|
|
215
222
|
failed: '#sqm-failed',
|
216
223
|
busy: '#sqm-busy',
|
217
224
|
enqueued: '#sqm-enqueued',
|
225
|
+
'scheduled-jobs': '#sqm-scheduled-jobs',
|
226
|
+
'retry-jobs': '#sqm-retry-jobs',
|
227
|
+
'dead-jobs': '#sqm-dead-jobs',
|
218
228
|
totalQueues: '#sqm-total-queues',
|
219
229
|
pausedQueues: '#sqm-paused-queues',
|
220
230
|
totalJobs: '#sqm-total-jobs',
|
221
231
|
|
232
|
+
// Tab counts
|
233
|
+
'sqm-queues-count': '#sqm-queues-count',
|
234
|
+
'sqm-scheduled-count': '#sqm-scheduled-count',
|
235
|
+
'sqm-retries-count': '#sqm-retries-count',
|
236
|
+
'sqm-dead-count': '#sqm-dead-count',
|
237
|
+
|
222
238
|
// Table elements
|
223
239
|
tableBody: '#sqm-table-body',
|
224
240
|
table: '#sqm-queues-table',
|
@@ -263,6 +279,15 @@ class SidekiqQueueManagerUI {
|
|
263
279
|
}
|
264
280
|
}
|
265
281
|
|
282
|
+
addEventListener(element, event, handler) {
|
283
|
+
if (element) {
|
284
|
+
element.addEventListener(event, handler);
|
285
|
+
// Generate a unique key for tracking
|
286
|
+
const key = `dynamic_${Date.now()}_${Math.random()}`;
|
287
|
+
this.eventHandlers.set(key, { element, event, handler });
|
288
|
+
}
|
289
|
+
}
|
290
|
+
|
266
291
|
initializeRefreshControl() {
|
267
292
|
const liveToggle = this.elements.get('liveToggleBtn');
|
268
293
|
const statusText = this.elements.get('statusText');
|
@@ -491,25 +516,65 @@ class SidekiqQueueManagerUI {
|
|
491
516
|
this.updateQueuesTable(data.queues);
|
492
517
|
}
|
493
518
|
|
519
|
+
// Update tab counts
|
520
|
+
this.updateTabCounts(data);
|
521
|
+
|
494
522
|
this.updateTimestamp(data.timestamp);
|
495
523
|
}
|
496
524
|
|
497
525
|
updateGlobalStats(stats) {
|
498
|
-
const
|
499
|
-
processed: stats.processed
|
500
|
-
failed: stats.failed
|
501
|
-
busy: stats.busy
|
502
|
-
enqueued: stats.enqueued
|
526
|
+
const statsData = {
|
527
|
+
processed: stats.processed || 0,
|
528
|
+
failed: stats.failed || 0,
|
529
|
+
busy: stats.busy || 0,
|
530
|
+
enqueued: stats.enqueued || 0,
|
531
|
+
'scheduled-jobs': stats.scheduled_size || 0,
|
532
|
+
'retry-jobs': stats.retry_size || 0,
|
533
|
+
'dead-jobs': stats.dead_size || 0
|
503
534
|
};
|
504
535
|
|
505
|
-
Object.entries(
|
536
|
+
Object.entries(statsData).forEach(([key, rawValue]) => {
|
506
537
|
const element = this.elements.get(key);
|
507
538
|
if (element) {
|
508
|
-
|
539
|
+
const formattedValue = this.formatLargeNumber(rawValue);
|
540
|
+
element.textContent = formattedValue;
|
541
|
+
|
542
|
+
// Add data-length attribute for responsive font sizing
|
543
|
+
const valueLength = formattedValue.replace(/,/g, '').length;
|
544
|
+
if (valueLength >= 15) {
|
545
|
+
element.setAttribute('data-length', 'long');
|
546
|
+
} else if (valueLength >= 8) {
|
547
|
+
element.setAttribute('data-length', valueLength.toString());
|
548
|
+
} else {
|
549
|
+
element.removeAttribute('data-length');
|
550
|
+
}
|
509
551
|
}
|
510
552
|
});
|
511
553
|
}
|
512
554
|
|
555
|
+
// Format large numbers with appropriate abbreviations and locale formatting
|
556
|
+
formatLargeNumber(num) {
|
557
|
+
const number = parseInt(num) || 0;
|
558
|
+
|
559
|
+
// Handle zero and negative numbers
|
560
|
+
if (number === 0) return '0';
|
561
|
+
if (number < 0) return number.toLocaleString();
|
562
|
+
|
563
|
+
// For very large numbers, use abbreviations
|
564
|
+
if (number >= 1000000000000) { // Trillion
|
565
|
+
return (number / 1000000000000).toFixed(1).replace(/\.0$/, '') + 'T';
|
566
|
+
} else if (number >= 1000000000) { // Billion
|
567
|
+
return (number / 1000000000).toFixed(1).replace(/\.0$/, '') + 'B';
|
568
|
+
} else if (number >= 1000000) { // Million
|
569
|
+
return (number / 1000000).toFixed(1).replace(/\.0$/, '') + 'M';
|
570
|
+
} else if (number >= 100000) { // For numbers 100k+, use abbreviation
|
571
|
+
return (number / 1000).toFixed(0) + 'K';
|
572
|
+
} else {
|
573
|
+
// For smaller numbers, use locale formatting with commas
|
574
|
+
return number.toLocaleString();
|
575
|
+
}
|
576
|
+
}
|
577
|
+
|
513
578
|
updateQueuesTable(queues) {
|
514
579
|
const tbody = this.elements.get('tableBody');
|
515
580
|
if (!tbody) return;
|
@@ -519,9 +584,9 @@ class SidekiqQueueManagerUI {
|
|
519
584
|
const pausedQueues = queueArray.filter(q => q.paused).length;
|
520
585
|
|
521
586
|
// Update counters
|
522
|
-
this.updateElement('totalQueues', totalQueues
|
523
|
-
this.updateElement('pausedQueues', pausedQueues
|
524
|
-
this.updateElement('totalJobs', queueArray.reduce((sum, q) => sum + (q.size || 0), 0)
|
587
|
+
this.updateElement('totalQueues', totalQueues);
|
588
|
+
this.updateElement('pausedQueues', pausedQueues);
|
589
|
+
this.updateElement('totalJobs', queueArray.reduce((sum, q) => sum + (q.size || 0), 0));
|
525
590
|
|
526
591
|
// Update table rows
|
527
592
|
tbody.innerHTML = queueArray.map(queue => this.renderQueueRow(queue)).join('');
|
@@ -1791,7 +1856,30 @@ class SidekiqQueueManagerUI {
|
|
1791
1856
|
updateElement(key, value) {
|
1792
1857
|
const element = this.elements.get(key);
|
1793
1858
|
if (element) {
|
1794
|
-
element
|
1859
|
+
// Format numbers if the element is a stat value
|
1860
|
+
if (element.classList.contains('sqm-stat-value') || element.classList.contains('sqm-tab-count')) {
|
1861
|
+
const numValue = parseInt(value);
|
1862
|
+
if (!isNaN(numValue)) {
|
1863
|
+
const formattedValue = this.formatLargeNumber(numValue);
|
1864
|
+
element.textContent = formattedValue;
|
1865
|
+
|
1866
|
+
// Add data-length attribute for responsive font sizing (only for stat values)
|
1867
|
+
if (element.classList.contains('sqm-stat-value')) {
|
1868
|
+
const valueLength = formattedValue.replace(/,/g, '').length;
|
1869
|
+
if (valueLength >= 15) {
|
1870
|
+
element.setAttribute('data-length', 'long');
|
1871
|
+
} else if (valueLength >= 8) {
|
1872
|
+
element.setAttribute('data-length', valueLength.toString());
|
1873
|
+
} else {
|
1874
|
+
element.removeAttribute('data-length');
|
1875
|
+
}
|
1876
|
+
}
|
1877
|
+
} else {
|
1878
|
+
element.textContent = value;
|
1879
|
+
}
|
1880
|
+
} else {
|
1881
|
+
element.textContent = value;
|
1882
|
+
}
|
1795
1883
|
}
|
1796
1884
|
}
|
1797
1885
|
|
@@ -1829,6 +1917,765 @@ class SidekiqQueueManagerUI {
|
|
1829
1917
|
}
|
1830
1918
|
}
|
1831
1919
|
|
1920
|
+
// ========================================
|
1921
|
+
// Tab Management System
|
1922
|
+
// ========================================
|
1923
|
+
|
1924
|
+
setupTabSystem() {
|
1925
|
+
const tabButtons = document.querySelectorAll('.sqm-tab');
|
1926
|
+
const tabPanels = document.querySelectorAll('.sqm-tab-panel');
|
1927
|
+
|
1928
|
+
tabButtons.forEach(button => {
|
1929
|
+
this.addEventListener(button, 'click', (e) => {
|
1930
|
+
e.preventDefault();
|
1931
|
+
const tabName = button.getAttribute('data-tab');
|
1932
|
+
this.switchTab(tabName);
|
1933
|
+
});
|
1934
|
+
});
|
1935
|
+
|
1936
|
+
// Setup filter and action buttons for each tab
|
1937
|
+
this.setupScheduledJobHandlers();
|
1938
|
+
this.setupRetryJobHandlers();
|
1939
|
+
this.setupDeadJobHandlers();
|
1940
|
+
}
|
1941
|
+
|
1942
|
+
switchTab(tabName) {
|
1943
|
+
if (this.state.activeTab === tabName) return;
|
1944
|
+
|
1945
|
+
// Update active tab state
|
1946
|
+
this.state.activeTab = tabName;
|
1947
|
+
|
1948
|
+
// Update tab button states
|
1949
|
+
document.querySelectorAll('.sqm-tab').forEach(button => {
|
1950
|
+
const isActive = button.getAttribute('data-tab') === tabName;
|
1951
|
+
button.classList.toggle('sqm-tab-active', isActive);
|
1952
|
+
button.setAttribute('aria-selected', isActive);
|
1953
|
+
});
|
1954
|
+
|
1955
|
+
// Update panel visibility
|
1956
|
+
document.querySelectorAll('.sqm-tab-panel').forEach(panel => {
|
1957
|
+
const panelTab = panel.id.replace('sqm-', '').replace('-panel', '');
|
1958
|
+
const isActive = panelTab === tabName;
|
1959
|
+
panel.classList.toggle('sqm-hidden', !isActive);
|
1960
|
+
panel.classList.toggle('sqm-tab-panel-active', isActive);
|
1961
|
+
});
|
1962
|
+
|
1963
|
+
// Load data for the active tab
|
1964
|
+
this.loadTabData(tabName);
|
1965
|
+
}
|
1966
|
+
|
1967
|
+
loadTabData(tabName) {
|
1968
|
+
switch (tabName) {
|
1969
|
+
case 'queues':
|
1970
|
+
this.refreshQueues();
|
1971
|
+
break;
|
1972
|
+
case 'scheduled':
|
1973
|
+
this.loadScheduledJobs();
|
1974
|
+
break;
|
1975
|
+
case 'retries':
|
1976
|
+
this.loadRetryJobs();
|
1977
|
+
break;
|
1978
|
+
case 'dead':
|
1979
|
+
this.loadDeadJobs();
|
1980
|
+
break;
|
1981
|
+
}
|
1982
|
+
}
|
1983
|
+
|
1984
|
+
updateTabCounts(data) {
|
1985
|
+
// Update tab counts from data
|
1986
|
+
if (data.queues) {
|
1987
|
+
const queuesCount = Object.keys(data.queues).length;
|
1988
|
+
this.updateElement('sqm-queues-count', queuesCount);
|
1989
|
+
}
|
1990
|
+
|
1991
|
+
if (data.global_stats) {
|
1992
|
+
const scheduledCount = data.global_stats.scheduled_size || 0;
|
1993
|
+
const retriesCount = data.global_stats.retry_size || 0;
|
1994
|
+
const deadCount = data.global_stats.dead_size || 0;
|
1995
|
+
|
1996
|
+
this.updateElement('sqm-scheduled-count', scheduledCount);
|
1997
|
+
this.updateElement('sqm-retries-count', retriesCount);
|
1998
|
+
this.updateElement('sqm-dead-count', deadCount);
|
1999
|
+
}
|
2000
|
+
}
|
2001
|
+
|
2002
|
+
// ========================================
|
2003
|
+
// Scheduled Jobs Management
|
2004
|
+
// ========================================
|
2005
|
+
|
2006
|
+
setupScheduledJobHandlers() {
|
2007
|
+
// Filter button
|
2008
|
+
const filterBtn = document.getElementById('sqm-scheduled-apply-filter');
|
2009
|
+
const filterInput = document.getElementById('sqm-scheduled-filter');
|
2010
|
+
const clearBtn = document.getElementById('sqm-clear-scheduled-btn');
|
2011
|
+
|
2012
|
+
if (filterBtn && filterInput) {
|
2013
|
+
this.addEventListener(filterBtn, 'click', () => {
|
2014
|
+
this.state.pagination.scheduled.filter = filterInput.value;
|
2015
|
+
this.state.pagination.scheduled.page = 1;
|
2016
|
+
this.loadScheduledJobs();
|
2017
|
+
});
|
2018
|
+
|
2019
|
+
this.addEventListener(filterInput, 'keypress', (e) => {
|
2020
|
+
if (e.key === 'Enter') {
|
2021
|
+
this.state.pagination.scheduled.filter = filterInput.value;
|
2022
|
+
this.state.pagination.scheduled.page = 1;
|
2023
|
+
this.loadScheduledJobs();
|
2024
|
+
}
|
2025
|
+
});
|
2026
|
+
}
|
2027
|
+
|
2028
|
+
if (clearBtn) {
|
2029
|
+
this.addEventListener(clearBtn, 'click', async () => {
|
2030
|
+
await this.showConfirmation('Are you sure you want to clear all scheduled jobs?', () => {
|
2031
|
+
this.clearScheduledJobs();
|
2032
|
+
});
|
2033
|
+
});
|
2034
|
+
}
|
2035
|
+
}
|
2036
|
+
|
2037
|
+
async loadScheduledJobs() {
|
2038
|
+
try {
|
2039
|
+
const params = new URLSearchParams(this.state.pagination.scheduled);
|
2040
|
+
const response = await fetch(`${this.gemConfig.mountPath}/scheduled?${params}`);
|
2041
|
+
const result = await response.json();
|
2042
|
+
|
2043
|
+
if (result.success) {
|
2044
|
+
this.renderScheduledJobs(result.data);
|
2045
|
+
this.renderPagination('scheduled', result.data.pagination);
|
2046
|
+
// Update tab count
|
2047
|
+
this.updateElement('sqm-scheduled-count', result.data.total_count || 0);
|
2048
|
+
} else {
|
2049
|
+
this.showNotification(result.message, 'error');
|
2050
|
+
}
|
2051
|
+
} catch (error) {
|
2052
|
+
this.showNotification('Failed to load scheduled jobs', 'error');
|
2053
|
+
}
|
2054
|
+
}
|
2055
|
+
|
2056
|
+
renderScheduledJobs(data) {
|
2057
|
+
const tbody = document.getElementById('sqm-scheduled-table-body');
|
2058
|
+
if (!tbody) return;
|
2059
|
+
|
2060
|
+
if (!data.jobs || data.jobs.length === 0) {
|
2061
|
+
tbody.innerHTML = `
|
2062
|
+
<tr>
|
2063
|
+
<td colspan="5" style="text-align: center; padding: 2rem; color: var(--sqm-muted-foreground);">
|
2064
|
+
No scheduled jobs found
|
2065
|
+
</td>
|
2066
|
+
</tr>
|
2067
|
+
`;
|
2068
|
+
return;
|
2069
|
+
}
|
2070
|
+
|
2071
|
+
tbody.innerHTML = data.jobs.map(job => `
|
2072
|
+
<tr>
|
2073
|
+
<td>
|
2074
|
+
<div class="sqm-job-class">${job.class}</div>
|
2075
|
+
<div class="sqm-job-args" title="${JSON.stringify(job.args)}">${JSON.stringify(job.args)}</div>
|
2076
|
+
</td>
|
2077
|
+
<td>${job.queue}</td>
|
2078
|
+
<td style="text-align: right;">
|
2079
|
+
<div>${job.scheduled_at}</div>
|
2080
|
+
<div class="sqm-time-relative">${job.time_until_execution}</div>
|
2081
|
+
</td>
|
2082
|
+
<td style="text-align: right;">
|
2083
|
+
<span class="sqm-time-relative">${job.time_until_execution}</span>
|
2084
|
+
</td>
|
2085
|
+
<td style="text-align: right;">
|
2086
|
+
<div class="sqm-action-buttons">
|
2087
|
+
<button class="sqm-btn sqm-btn-sm sqm-btn-enqueue"
|
2088
|
+
onclick="window.sidekiqQueueManager.enqueueScheduledJob('${job.jid}')"
|
2089
|
+
title="Enqueue now">
|
2090
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 0.875rem; height: 0.875rem;">
|
2091
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
|
2092
|
+
</svg>
|
2093
|
+
</button>
|
2094
|
+
<button class="sqm-btn sqm-btn-sm sqm-btn-destructive"
|
2095
|
+
onclick="window.sidekiqQueueManager.deleteScheduledJob('${job.jid}')"
|
2096
|
+
title="Delete">
|
2097
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 0.875rem; height: 0.875rem;">
|
2098
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
2099
|
+
</svg>
|
2100
|
+
</button>
|
2101
|
+
</div>
|
2102
|
+
</td>
|
2103
|
+
</tr>
|
2104
|
+
`).join('');
|
2105
|
+
}
|
2106
|
+
|
2107
|
+
async enqueueScheduledJob(jobId) {
|
2108
|
+
try {
|
2109
|
+
const response = await fetch(`${this.gemConfig.mountPath}/scheduled/${jobId}/enqueue`, {
|
2110
|
+
method: 'POST',
|
2111
|
+
headers: { 'Content-Type': 'application/json' }
|
2112
|
+
});
|
2113
|
+
const result = await response.json();
|
2114
|
+
|
2115
|
+
if (result.success) {
|
2116
|
+
this.showNotification('Job enqueued successfully', 'success');
|
2117
|
+
this.loadScheduledJobs();
|
2118
|
+
} else {
|
2119
|
+
this.showNotification(result.message, 'error');
|
2120
|
+
}
|
2121
|
+
} catch (error) {
|
2122
|
+
this.showNotification('Failed to enqueue job', 'error');
|
2123
|
+
}
|
2124
|
+
}
|
2125
|
+
|
2126
|
+
async deleteScheduledJob(jobId) {
|
2127
|
+
try {
|
2128
|
+
const response = await fetch(`${this.gemConfig.mountPath}/scheduled/${jobId}`, {
|
2129
|
+
method: 'DELETE',
|
2130
|
+
headers: { 'Content-Type': 'application/json' }
|
2131
|
+
});
|
2132
|
+
const result = await response.json();
|
2133
|
+
|
2134
|
+
if (result.success) {
|
2135
|
+
this.showNotification('Job deleted successfully', 'success');
|
2136
|
+
this.loadScheduledJobs();
|
2137
|
+
} else {
|
2138
|
+
this.showNotification(result.message, 'error');
|
2139
|
+
}
|
2140
|
+
} catch (error) {
|
2141
|
+
this.showNotification('Failed to delete job', 'error');
|
2142
|
+
}
|
2143
|
+
}
|
2144
|
+
|
2145
|
+
async clearScheduledJobs() {
|
2146
|
+
try {
|
2147
|
+
const filter = this.state.pagination.scheduled.filter;
|
2148
|
+
const body = filter ? JSON.stringify({ filter }) : null;
|
2149
|
+
|
2150
|
+
const response = await fetch(`${this.gemConfig.mountPath}/scheduled/clear`, {
|
2151
|
+
method: 'POST',
|
2152
|
+
headers: { 'Content-Type': 'application/json' },
|
2153
|
+
body
|
2154
|
+
});
|
2155
|
+
const result = await response.json();
|
2156
|
+
|
2157
|
+
if (result.success) {
|
2158
|
+
this.showNotification(result.message, 'success');
|
2159
|
+
this.loadScheduledJobs();
|
2160
|
+
} else {
|
2161
|
+
this.showNotification(result.message, 'error');
|
2162
|
+
}
|
2163
|
+
} catch (error) {
|
2164
|
+
this.showNotification('Failed to clear scheduled jobs', 'error');
|
2165
|
+
}
|
2166
|
+
}
|
2167
|
+
|
2168
|
+
// ========================================
|
2169
|
+
// Retry Jobs Management
|
2170
|
+
// ========================================
|
2171
|
+
|
2172
|
+
setupRetryJobHandlers() {
|
2173
|
+
// Filter button
|
2174
|
+
const filterBtn = document.getElementById('sqm-retries-apply-filter');
|
2175
|
+
const filterInput = document.getElementById('sqm-retries-filter');
|
2176
|
+
const retryAllBtn = document.getElementById('sqm-retry-all-btn');
|
2177
|
+
const clearBtn = document.getElementById('sqm-clear-retries-btn');
|
2178
|
+
|
2179
|
+
if (filterBtn && filterInput) {
|
2180
|
+
this.addEventListener(filterBtn, 'click', () => {
|
2181
|
+
this.state.pagination.retries.filter = filterInput.value;
|
2182
|
+
this.state.pagination.retries.page = 1;
|
2183
|
+
this.loadRetryJobs();
|
2184
|
+
});
|
2185
|
+
|
2186
|
+
this.addEventListener(filterInput, 'keypress', (e) => {
|
2187
|
+
if (e.key === 'Enter') {
|
2188
|
+
this.state.pagination.retries.filter = filterInput.value;
|
2189
|
+
this.state.pagination.retries.page = 1;
|
2190
|
+
this.loadRetryJobs();
|
2191
|
+
}
|
2192
|
+
});
|
2193
|
+
}
|
2194
|
+
|
2195
|
+
if (retryAllBtn) {
|
2196
|
+
this.addEventListener(retryAllBtn, 'click', async () => {
|
2197
|
+
await this.showConfirmation('Are you sure you want to retry all jobs?', () => {
|
2198
|
+
this.retryAllJobs();
|
2199
|
+
});
|
2200
|
+
});
|
2201
|
+
}
|
2202
|
+
|
2203
|
+
if (clearBtn) {
|
2204
|
+
this.addEventListener(clearBtn, 'click', async () => {
|
2205
|
+
await this.showConfirmation('Are you sure you want to clear all retry jobs?', () => {
|
2206
|
+
this.clearRetryJobs();
|
2207
|
+
});
|
2208
|
+
});
|
2209
|
+
}
|
2210
|
+
}
|
2211
|
+
|
2212
|
+
async loadRetryJobs() {
|
2213
|
+
try {
|
2214
|
+
const params = new URLSearchParams(this.state.pagination.retries);
|
2215
|
+
const response = await fetch(`${this.gemConfig.mountPath}/retries?${params}`);
|
2216
|
+
const result = await response.json();
|
2217
|
+
|
2218
|
+
if (result.success) {
|
2219
|
+
this.renderRetryJobs(result.data);
|
2220
|
+
this.renderPagination('retries', result.data.pagination);
|
2221
|
+
// Update tab count
|
2222
|
+
this.updateElement('sqm-retries-count', result.data.total_count || 0);
|
2223
|
+
} else {
|
2224
|
+
this.showNotification(result.message, 'error');
|
2225
|
+
}
|
2226
|
+
} catch (error) {
|
2227
|
+
this.showNotification('Failed to load retry jobs', 'error');
|
2228
|
+
}
|
2229
|
+
}
|
2230
|
+
|
2231
|
+
renderRetryJobs(data) {
|
2232
|
+
const tbody = document.getElementById('sqm-retries-table-body');
|
2233
|
+
if (!tbody) return;
|
2234
|
+
|
2235
|
+
if (!data.jobs || data.jobs.length === 0) {
|
2236
|
+
tbody.innerHTML = `
|
2237
|
+
<tr>
|
2238
|
+
<td colspan="6" style="text-align: center; padding: 2rem; color: var(--sqm-muted-foreground);">
|
2239
|
+
No retry jobs found
|
2240
|
+
</td>
|
2241
|
+
</tr>
|
2242
|
+
`;
|
2243
|
+
return;
|
2244
|
+
}
|
2245
|
+
|
2246
|
+
tbody.innerHTML = data.jobs.map(job => `
|
2247
|
+
<tr>
|
2248
|
+
<td>
|
2249
|
+
<div class="sqm-job-class">${job.class}</div>
|
2250
|
+
<div class="sqm-error-preview" title="${job.error_message || 'No error message'}">${job.error_class || 'Unknown Error'}</div>
|
2251
|
+
</td>
|
2252
|
+
<td>${job.queue}</td>
|
2253
|
+
<td style="text-align: right;">
|
2254
|
+
<div>${job.failed_at || 'Unknown'}</div>
|
2255
|
+
<div class="sqm-time-relative">${job.failed_at_relative || ''}</div>
|
2256
|
+
</td>
|
2257
|
+
<td style="text-align: right;">
|
2258
|
+
<div>${job.retry_at || 'Now'}</div>
|
2259
|
+
<div class="sqm-time-relative">${job.next_retry_relative || ''}</div>
|
2260
|
+
</td>
|
2261
|
+
<td style="text-align: right;">
|
2262
|
+
<div class="sqm-retry-count">
|
2263
|
+
${job.retry_count}/${job.retry_limit}
|
2264
|
+
<div class="sqm-retry-progress">
|
2265
|
+
<div class="sqm-retry-progress-bar" style="width: ${(job.retry_count / job.retry_limit) * 100}%"></div>
|
2266
|
+
</div>
|
2267
|
+
</div>
|
2268
|
+
</td>
|
2269
|
+
<td style="text-align: right;">
|
2270
|
+
<div class="sqm-action-buttons">
|
2271
|
+
<button class="sqm-btn sqm-btn-sm sqm-btn-success"
|
2272
|
+
onclick="window.sidekiqQueueManager.retryJobNow('${job.jid}')"
|
2273
|
+
title="Retry now">
|
2274
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 0.875rem; height: 0.875rem;">
|
2275
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
|
2276
|
+
</svg>
|
2277
|
+
</button>
|
2278
|
+
<button class="sqm-btn sqm-btn-sm sqm-btn-kill"
|
2279
|
+
onclick="window.sidekiqQueueManager.killRetryJob('${job.jid}')"
|
2280
|
+
title="Move to dead queue">
|
2281
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 0.875rem; height: 0.875rem;">
|
2282
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
2283
|
+
</svg>
|
2284
|
+
</button>
|
2285
|
+
<button class="sqm-btn sqm-btn-sm sqm-btn-destructive"
|
2286
|
+
onclick="window.sidekiqQueueManager.deleteRetryJob('${job.jid}')"
|
2287
|
+
title="Delete">
|
2288
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 0.875rem; height: 0.875rem;">
|
2289
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
2290
|
+
</svg>
|
2291
|
+
</button>
|
2292
|
+
</div>
|
2293
|
+
</td>
|
2294
|
+
</tr>
|
2295
|
+
`).join('');
|
2296
|
+
}
|
2297
|
+
|
2298
|
+
async retryJobNow(jobId) {
|
2299
|
+
try {
|
2300
|
+
const response = await fetch(`${this.gemConfig.mountPath}/retries/${jobId}/retry`, {
|
2301
|
+
method: 'POST',
|
2302
|
+
headers: { 'Content-Type': 'application/json' }
|
2303
|
+
});
|
2304
|
+
const result = await response.json();
|
2305
|
+
|
2306
|
+
if (result.success) {
|
2307
|
+
this.showNotification('Job retried successfully', 'success');
|
2308
|
+
this.loadRetryJobs();
|
2309
|
+
} else {
|
2310
|
+
this.showNotification(result.message, 'error');
|
2311
|
+
}
|
2312
|
+
} catch (error) {
|
2313
|
+
this.showNotification('Failed to retry job', 'error');
|
2314
|
+
}
|
2315
|
+
}
|
2316
|
+
|
2317
|
+
async killRetryJob(jobId) {
|
2318
|
+
try {
|
2319
|
+
const response = await fetch(`${this.gemConfig.mountPath}/retries/${jobId}/kill`, {
|
2320
|
+
method: 'POST',
|
2321
|
+
headers: { 'Content-Type': 'application/json' }
|
2322
|
+
});
|
2323
|
+
const result = await response.json();
|
2324
|
+
|
2325
|
+
if (result.success) {
|
2326
|
+
this.showNotification('Job moved to dead queue', 'success');
|
2327
|
+
this.loadRetryJobs();
|
2328
|
+
} else {
|
2329
|
+
this.showNotification(result.message, 'error');
|
2330
|
+
}
|
2331
|
+
} catch (error) {
|
2332
|
+
this.showNotification('Failed to kill job', 'error');
|
2333
|
+
}
|
2334
|
+
}
|
2335
|
+
|
2336
|
+
async deleteRetryJob(jobId) {
|
2337
|
+
try {
|
2338
|
+
const response = await fetch(`${this.gemConfig.mountPath}/retries/${jobId}`, {
|
2339
|
+
method: 'DELETE',
|
2340
|
+
headers: { 'Content-Type': 'application/json' }
|
2341
|
+
});
|
2342
|
+
const result = await response.json();
|
2343
|
+
|
2344
|
+
if (result.success) {
|
2345
|
+
this.showNotification('Job deleted successfully', 'success');
|
2346
|
+
this.loadRetryJobs();
|
2347
|
+
} else {
|
2348
|
+
this.showNotification(result.message, 'error');
|
2349
|
+
}
|
2350
|
+
} catch (error) {
|
2351
|
+
this.showNotification('Failed to delete job', 'error');
|
2352
|
+
}
|
2353
|
+
}
|
2354
|
+
|
2355
|
+
async retryAllJobs() {
|
2356
|
+
try {
|
2357
|
+
const filter = this.state.pagination.retries.filter;
|
2358
|
+
const body = filter ? JSON.stringify({ filter }) : null;
|
2359
|
+
|
2360
|
+
const response = await fetch(`${this.gemConfig.mountPath}/retries/retry_all`, {
|
2361
|
+
method: 'POST',
|
2362
|
+
headers: { 'Content-Type': 'application/json' },
|
2363
|
+
body
|
2364
|
+
});
|
2365
|
+
const result = await response.json();
|
2366
|
+
|
2367
|
+
if (result.success) {
|
2368
|
+
this.showNotification(result.message, 'success');
|
2369
|
+
this.loadRetryJobs();
|
2370
|
+
} else {
|
2371
|
+
this.showNotification(result.message, 'error');
|
2372
|
+
}
|
2373
|
+
} catch (error) {
|
2374
|
+
this.showNotification('Failed to retry all jobs', 'error');
|
2375
|
+
}
|
2376
|
+
}
|
2377
|
+
|
2378
|
+
async clearRetryJobs() {
|
2379
|
+
try {
|
2380
|
+
const filter = this.state.pagination.retries.filter;
|
2381
|
+
const body = filter ? JSON.stringify({ filter }) : null;
|
2382
|
+
|
2383
|
+
const response = await fetch(`${this.gemConfig.mountPath}/retries/clear`, {
|
2384
|
+
method: 'POST',
|
2385
|
+
headers: { 'Content-Type': 'application/json' },
|
2386
|
+
body
|
2387
|
+
});
|
2388
|
+
const result = await response.json();
|
2389
|
+
|
2390
|
+
if (result.success) {
|
2391
|
+
this.showNotification(result.message, 'success');
|
2392
|
+
this.loadRetryJobs();
|
2393
|
+
} else {
|
2394
|
+
this.showNotification(result.message, 'error');
|
2395
|
+
}
|
2396
|
+
} catch (error) {
|
2397
|
+
this.showNotification('Failed to clear retry jobs', 'error');
|
2398
|
+
}
|
2399
|
+
}
|
2400
|
+
|
2401
|
+
// ========================================
|
2402
|
+
// Dead Jobs Management
|
2403
|
+
// ========================================
|
2404
|
+
|
2405
|
+
setupDeadJobHandlers() {
|
2406
|
+
// Filter button
|
2407
|
+
const filterBtn = document.getElementById('sqm-dead-apply-filter');
|
2408
|
+
const filterInput = document.getElementById('sqm-dead-filter');
|
2409
|
+
const resurrectAllBtn = document.getElementById('sqm-resurrect-all-btn');
|
2410
|
+
const clearBtn = document.getElementById('sqm-clear-dead-btn');
|
2411
|
+
|
2412
|
+
if (filterBtn && filterInput) {
|
2413
|
+
this.addEventListener(filterBtn, 'click', () => {
|
2414
|
+
this.state.pagination.dead.filter = filterInput.value;
|
2415
|
+
this.state.pagination.dead.page = 1;
|
2416
|
+
this.loadDeadJobs();
|
2417
|
+
});
|
2418
|
+
|
2419
|
+
this.addEventListener(filterInput, 'keypress', (e) => {
|
2420
|
+
if (e.key === 'Enter') {
|
2421
|
+
this.state.pagination.dead.filter = filterInput.value;
|
2422
|
+
this.state.pagination.dead.page = 1;
|
2423
|
+
this.loadDeadJobs();
|
2424
|
+
}
|
2425
|
+
});
|
2426
|
+
}
|
2427
|
+
|
2428
|
+
if (resurrectAllBtn) {
|
2429
|
+
this.addEventListener(resurrectAllBtn, 'click', async () => {
|
2430
|
+
await this.showConfirmation('Are you sure you want to resurrect all dead jobs?', () => {
|
2431
|
+
this.resurrectAllDeadJobs();
|
2432
|
+
});
|
2433
|
+
});
|
2434
|
+
}
|
2435
|
+
|
2436
|
+
if (clearBtn) {
|
2437
|
+
this.addEventListener(clearBtn, 'click', async () => {
|
2438
|
+
await this.showConfirmation('Are you sure you want to permanently delete all dead jobs?', () => {
|
2439
|
+
this.clearDeadJobs();
|
2440
|
+
});
|
2441
|
+
});
|
2442
|
+
}
|
2443
|
+
}
|
2444
|
+
|
2445
|
+
async loadDeadJobs() {
|
2446
|
+
try {
|
2447
|
+
const params = new URLSearchParams(this.state.pagination.dead);
|
2448
|
+
const response = await fetch(`${this.gemConfig.mountPath}/dead?${params}`);
|
2449
|
+
const result = await response.json();
|
2450
|
+
|
2451
|
+
if (result.success) {
|
2452
|
+
this.renderDeadJobs(result.data);
|
2453
|
+
this.renderPagination('dead', result.data.pagination);
|
2454
|
+
// Update tab count
|
2455
|
+
this.updateElement('sqm-dead-count', result.data.total_count || 0);
|
2456
|
+
} else {
|
2457
|
+
this.showNotification(result.message, 'error');
|
2458
|
+
}
|
2459
|
+
} catch (error) {
|
2460
|
+
this.showNotification('Failed to load dead jobs', 'error');
|
2461
|
+
}
|
2462
|
+
}
|
2463
|
+
|
2464
|
+
renderDeadJobs(data) {
|
2465
|
+
const tbody = document.getElementById('sqm-dead-table-body');
|
2466
|
+
if (!tbody) return;
|
2467
|
+
|
2468
|
+
if (!data.jobs || data.jobs.length === 0) {
|
2469
|
+
tbody.innerHTML = `
|
2470
|
+
<tr>
|
2471
|
+
<td colspan="6" style="text-align: center; padding: 2rem; color: var(--sqm-muted-foreground);">
|
2472
|
+
No dead jobs found
|
2473
|
+
</td>
|
2474
|
+
</tr>
|
2475
|
+
`;
|
2476
|
+
return;
|
2477
|
+
}
|
2478
|
+
|
2479
|
+
tbody.innerHTML = data.jobs.map(job => `
|
2480
|
+
<tr>
|
2481
|
+
<td>
|
2482
|
+
<div class="sqm-job-class">${job.class}</div>
|
2483
|
+
<div class="sqm-job-args" title="${JSON.stringify(job.args)}">${JSON.stringify(job.args)}</div>
|
2484
|
+
</td>
|
2485
|
+
<td>${job.queue}</td>
|
2486
|
+
<td style="text-align: right;">
|
2487
|
+
<div>${job.failed_at || 'Unknown'}</div>
|
2488
|
+
<div class="sqm-time-relative">${job.failed_at_relative || ''}</div>
|
2489
|
+
</td>
|
2490
|
+
<td style="text-align: right;">
|
2491
|
+
<span class="sqm-retry-count">${job.retry_count}</span>
|
2492
|
+
</td>
|
2493
|
+
<td style="text-align: right;">
|
2494
|
+
<div class="sqm-error-preview" title="${job.error_message || 'No error message'}">${job.error_class || 'Unknown Error'}</div>
|
2495
|
+
</td>
|
2496
|
+
<td style="text-align: right;">
|
2497
|
+
<div class="sqm-action-buttons">
|
2498
|
+
<button class="sqm-btn sqm-btn-sm sqm-btn-resurrect"
|
2499
|
+
onclick="window.sidekiqQueueManager.resurrectDeadJob('${job.jid}')"
|
2500
|
+
title="Resurrect to retry queue">
|
2501
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 0.875rem; height: 0.875rem;">
|
2502
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
|
2503
|
+
</svg>
|
2504
|
+
</button>
|
2505
|
+
<button class="sqm-btn sqm-btn-sm sqm-btn-destructive"
|
2506
|
+
onclick="window.sidekiqQueueManager.deleteDeadJob('${job.jid}')"
|
2507
|
+
title="Delete permanently">
|
2508
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 0.875rem; height: 0.875rem;">
|
2509
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
2510
|
+
</svg>
|
2511
|
+
</button>
|
2512
|
+
</div>
|
2513
|
+
</td>
|
2514
|
+
</tr>
|
2515
|
+
`).join('');
|
2516
|
+
}
|
2517
|
+
|
2518
|
+
async resurrectDeadJob(jobId) {
|
2519
|
+
try {
|
2520
|
+
const response = await fetch(`${this.gemConfig.mountPath}/dead/${jobId}/resurrect`, {
|
2521
|
+
method: 'POST',
|
2522
|
+
headers: { 'Content-Type': 'application/json' }
|
2523
|
+
});
|
2524
|
+
const result = await response.json();
|
2525
|
+
|
2526
|
+
if (result.success) {
|
2527
|
+
this.showNotification('Job resurrected successfully', 'success');
|
2528
|
+
this.loadDeadJobs();
|
2529
|
+
} else {
|
2530
|
+
this.showNotification(result.message, 'error');
|
2531
|
+
}
|
2532
|
+
} catch (error) {
|
2533
|
+
this.showNotification('Failed to resurrect job', 'error');
|
2534
|
+
}
|
2535
|
+
}
|
2536
|
+
|
2537
|
+
async deleteDeadJob(jobId) {
|
2538
|
+
try {
|
2539
|
+
const response = await fetch(`${this.gemConfig.mountPath}/dead/${jobId}`, {
|
2540
|
+
method: 'DELETE',
|
2541
|
+
headers: { 'Content-Type': 'application/json' }
|
2542
|
+
});
|
2543
|
+
const result = await response.json();
|
2544
|
+
|
2545
|
+
if (result.success) {
|
2546
|
+
this.showNotification('Job deleted permanently', 'success');
|
2547
|
+
this.loadDeadJobs();
|
2548
|
+
} else {
|
2549
|
+
this.showNotification(result.message, 'error');
|
2550
|
+
}
|
2551
|
+
} catch (error) {
|
2552
|
+
this.showNotification('Failed to delete job', 'error');
|
2553
|
+
}
|
2554
|
+
}
|
2555
|
+
|
2556
|
+
async resurrectAllDeadJobs() {
|
2557
|
+
try {
|
2558
|
+
const filter = this.state.pagination.dead.filter;
|
2559
|
+
const body = filter ? JSON.stringify({ filter }) : null;
|
2560
|
+
|
2561
|
+
const response = await fetch(`${this.gemConfig.mountPath}/dead/resurrect_all`, {
|
2562
|
+
method: 'POST',
|
2563
|
+
headers: { 'Content-Type': 'application/json' },
|
2564
|
+
body
|
2565
|
+
});
|
2566
|
+
const result = await response.json();
|
2567
|
+
|
2568
|
+
if (result.success) {
|
2569
|
+
this.showNotification(result.message, 'success');
|
2570
|
+
this.loadDeadJobs();
|
2571
|
+
} else {
|
2572
|
+
this.showNotification(result.message, 'error');
|
2573
|
+
}
|
2574
|
+
} catch (error) {
|
2575
|
+
this.showNotification('Failed to resurrect all jobs', 'error');
|
2576
|
+
}
|
2577
|
+
}
|
2578
|
+
|
2579
|
+
async clearDeadJobs() {
|
2580
|
+
try {
|
2581
|
+
const filter = this.state.pagination.dead.filter;
|
2582
|
+
const body = filter ? JSON.stringify({ filter }) : null;
|
2583
|
+
|
2584
|
+
const response = await fetch(`${this.gemConfig.mountPath}/dead/clear`, {
|
2585
|
+
method: 'POST',
|
2586
|
+
headers: { 'Content-Type': 'application/json' },
|
2587
|
+
body
|
2588
|
+
});
|
2589
|
+
const result = await response.json();
|
2590
|
+
|
2591
|
+
if (result.success) {
|
2592
|
+
this.showNotification(result.message, 'success');
|
2593
|
+
this.loadDeadJobs();
|
2594
|
+
} else {
|
2595
|
+
this.showNotification(result.message, 'error');
|
2596
|
+
}
|
2597
|
+
} catch (error) {
|
2598
|
+
this.showNotification('Failed to clear dead jobs', 'error');
|
2599
|
+
}
|
2600
|
+
}
|
2601
|
+
|
2602
|
+
// ========================================
|
2603
|
+
// Pagination System
|
2604
|
+
// ========================================
|
2605
|
+
|
2606
|
+
renderPagination(tabName, pagination) {
|
2607
|
+
const container = document.getElementById(`sqm-${tabName}-pagination`);
|
2608
|
+
if (!container || !pagination) return;
|
2609
|
+
|
2610
|
+
const { current_page, total_pages, per_page, total_jobs, has_previous, has_next } = pagination;
|
2611
|
+
|
2612
|
+
if (total_pages <= 1) {
|
2613
|
+
container.innerHTML = '';
|
2614
|
+
return;
|
2615
|
+
}
|
2616
|
+
|
2617
|
+
container.innerHTML = `
|
2618
|
+
<div class="sqm-pagination-info">
|
2619
|
+
Showing ${((current_page - 1) * per_page) + 1}-${Math.min(current_page * per_page, total_jobs)} of ${total_jobs} jobs
|
2620
|
+
</div>
|
2621
|
+
<div class="sqm-pagination-controls">
|
2622
|
+
<button class="sqm-pagination-btn" ${!has_previous ? 'disabled' : ''}
|
2623
|
+
onclick="window.sidekiqQueueManager.changePage('${tabName}', ${current_page - 1})">
|
2624
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
2625
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
2626
|
+
</svg>
|
2627
|
+
</button>
|
2628
|
+
${this.generatePageNumbers(current_page, total_pages, tabName)}
|
2629
|
+
<button class="sqm-pagination-btn" ${!has_next ? 'disabled' : ''}
|
2630
|
+
onclick="window.sidekiqQueueManager.changePage('${tabName}', ${current_page + 1})">
|
2631
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
2632
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
2633
|
+
</svg>
|
2634
|
+
</button>
|
2635
|
+
</div>
|
2636
|
+
`;
|
2637
|
+
}
|
2638
|
+
|
2639
|
+
generatePageNumbers(current, total, tabName) {
|
2640
|
+
const pages = [];
|
2641
|
+
const maxVisible = 5;
|
2642
|
+
|
2643
|
+
let start = Math.max(1, current - Math.floor(maxVisible / 2));
|
2644
|
+
let end = Math.min(total, start + maxVisible - 1);
|
2645
|
+
|
2646
|
+
if (end - start + 1 < maxVisible) {
|
2647
|
+
start = Math.max(1, end - maxVisible + 1);
|
2648
|
+
}
|
2649
|
+
|
2650
|
+
for (let i = start; i <= end; i++) {
|
2651
|
+
const isActive = i === current;
|
2652
|
+
pages.push(`
|
2653
|
+
<button class="sqm-pagination-btn ${isActive ? 'sqm-pagination-current' : ''}"
|
2654
|
+
onclick="window.sidekiqQueueManager.changePage('${tabName}', ${i})">
|
2655
|
+
${i}
|
2656
|
+
</button>
|
2657
|
+
`);
|
2658
|
+
}
|
2659
|
+
|
2660
|
+
return pages.join('');
|
2661
|
+
}
|
2662
|
+
|
2663
|
+
changePage(tabName, page) {
|
2664
|
+
this.state.pagination[tabName].page = page;
|
2665
|
+
this.loadTabData(tabName);
|
2666
|
+
}
|
2667
|
+
|
2668
|
+
// ========================================
|
2669
|
+
// Confirmation Dialog
|
2670
|
+
// ========================================
|
2671
|
+
|
2672
|
+
async showConfirmation(message, onConfirm) {
|
2673
|
+
const confirmed = await this.showCustomConfirm('Confirm Action', message);
|
2674
|
+
if (confirmed) {
|
2675
|
+
onConfirm();
|
2676
|
+
}
|
2677
|
+
}
|
2678
|
+
|
1832
2679
|
// ========================================
|
1833
2680
|
// Notification System
|
1834
2681
|
// ========================================
|