feat(pbs): Implement sub-tabs for multiple PBS instances

This commit is contained in:
courtmanr@gmail.com
2025-05-20 15:59:19 +01:00
parent 599b943bda
commit 449a217a08
3 changed files with 170 additions and 319 deletions
+7 -1
View File
@@ -354,7 +354,13 @@
</div>
</div>
<div id="pbs" class="tab-content hidden bg-white dark:bg-gray-800 p-4 border border-t-0 border-gray-300 dark:border-gray-700 rounded-b-lg shadow-sm">
<div id="pbs-instances-container" class="space-y-6">
<!-- Sub-tab navigation -->
<div id="pbs-sub-tabs-nav" class="flex border-b border-gray-200 dark:border-gray-700 mb-4">
<!-- Sub-tab buttons will be dynamically inserted here by JavaScript -->
</div>
<!-- Sub-tab content panels -->
<div id="pbs-sub-tabs-content" class="space-y-6">
<!-- Content for each PBS instance will be dynamically inserted here -->
<p id="pbs-loading-message" class="text-gray-500 dark:text-gray-400">Loading PBS data...</p>
</div>
</div>
+17 -1
View File
@@ -2,6 +2,7 @@ PulseApp.state = (() => {
const savedSortState = JSON.parse(localStorage.getItem('pulseSortState')) || {};
const savedFilterState = JSON.parse(localStorage.getItem('pulseFilterState')) || {};
const savedThresholdState = JSON.parse(localStorage.getItem('pulseThresholdState')) || {};
const savedPbsShowDetails = JSON.parse(localStorage.getItem('pulsePbsShowDetails')) || {};
let internalState = {
nodesData: [],
@@ -35,7 +36,8 @@ PulseApp.state = (() => {
},
activeLogSessions: {},
thresholdLogEntries: [],
activeLoggingThresholds: null
activeLoggingThresholds: null,
pbsShowDetails: savedPbsShowDetails
};
// Initialize thresholdState by merging saved state with defaults
@@ -81,6 +83,10 @@ PulseApp.state = (() => {
localStorage.setItem('pulseSortState', JSON.stringify(stateToSave));
}
function savePbsShowDetailsState() {
localStorage.setItem('pulsePbsShowDetails', JSON.stringify(internalState.pbsShowDetails));
}
return {
get: (key) => internalState[key],
set: (key, value) => {
@@ -134,6 +140,16 @@ PulseApp.state = (() => {
},
clearDashboardHistoryEntry: (guestId) => {
delete internalState.dashboardHistory[guestId];
},
getPbShowDetailsState: (instanceId, defaultValue) => {
if (typeof internalState.pbsShowDetails[instanceId] === 'boolean') {
return internalState.pbsShowDetails[instanceId];
}
return defaultValue;
},
setPbShowDetailsState: (instanceId, value) => {
internalState.pbsShowDetails[instanceId] = !!value;
savePbsShowDetailsState();
}
};
})();
+146 -317
View File
@@ -102,7 +102,10 @@ PulseApp.ui.pbs = (() => {
PBS_RECENT_VERIFY_TASKS_TBODY: 'pbs-recent-verify-tasks-tbody-',
PBS_RECENT_SYNC_TASKS_TBODY: 'pbs-recent-sync-tasks-tbody-',
PBS_RECENT_PRUNEGC_TASKS_TBODY: 'pbs-recent-prunegc-tasks-tbody-',
PBS_INSTANCES_CONTAINER: 'pbs-instances-container',
PBS_INSTANCES_CONTAINER: 'pbs-instances-container', // Kept for reference, not primary use now
PBS_SUB_TABS_NAV: 'pbs-sub-tabs-nav',
PBS_SUB_TABS_CONTENT: 'pbs-sub-tabs-content',
PBS_SUB_TAB_CONTENT_PANEL_PREFIX: 'pbs-instance-content-'
};
const DATA_ATTRIBUTES = {
@@ -355,7 +358,7 @@ PulseApp.ui.pbs = (() => {
headerDiv.className = `${CSS_CLASSES.FLEX} ${CSS_CLASSES.JUSTIFY_BETWEEN} ${CSS_CLASSES.ITEMS_CENTER} ${CSS_CLASSES.MB3}`;
const instanceTitleElement = document.createElement('h3');
instanceTitleElement.className = `${CSS_CLASSES.TEXT_LG} ${CSS_CLASSES.FONT_SEMIBOLD} ${CSS_CLASSES.TEXT_GRAY_800_DARK_GRAY_200} ${CSS_CLASSES.FLEX} ${CSS_CLASSES.ITEMS_CENTER}`;
instanceTitleElement.appendChild(_createHealthBadgeHTML(overallHealth, healthTitle));
// instanceTitleElement.appendChild(_createHealthBadgeHTML(overallHealth, healthTitle));
instanceTitleElement.appendChild(document.createTextNode(instanceName));
headerDiv.appendChild(instanceTitleElement);
return headerDiv;
@@ -396,340 +399,166 @@ PulseApp.ui.pbs = (() => {
};
function updatePbsInfo(pbsArray) {
const container = document.getElementById(ID_PREFIXES.PBS_INSTANCES_CONTAINER);
if (!container) {
console.error(`PBS container element #${ID_PREFIXES.PBS_INSTANCES_CONTAINER} not found!`);
return;
}
const pbsSubTabsNav = document.getElementById(ID_PREFIXES.PBS_SUB_TABS_NAV);
const pbsSubTabsContent = document.getElementById(ID_PREFIXES.PBS_SUB_TABS_CONTENT);
const pbsLoadingMessageEl = document.getElementById('pbs-loading-message');
if (!pbsArray || pbsArray.length === 0) {
container.innerHTML = '';
const placeholder = document.createElement('p');
placeholder.className = CSS_CLASSES.TEXT_GRAY_500_DARK_TEXT_GRAY_400_P4_TEXT_CENTER_TEXT_SM;
placeholder.textContent = 'Proxmox Backup Server integration is not configured.';
container.appendChild(placeholder);
return;
}
const loadingMessage = document.getElementById('pbs-loading-message'); // Assuming this ID is unique and used elsewhere
if (loadingMessage) {
loadingMessage.remove();
}
const notConfiguredBanner = container.querySelector('.pbs-not-configured-banner'); // Assuming this class is unique
if (notConfiguredBanner) {
notConfiguredBanner.remove();
}
const currentInstanceIds = new Set();
const _createPbsTaskHealthTable = (instanceId, pbsInstanceData) => {
const sectionDiv = document.createElement('div');
sectionDiv.id = ID_PREFIXES.PBS_SUMMARIES_SECTION + instanceId;
// Add a general class for styling this new section if needed, e.g., 'pbs-task-health-section'
sectionDiv.className = `pbs-task-health-section ${CSS_CLASSES.MB3}`; // Added margin-bottom for spacing
const heading = document.createElement('h4');
heading.className = `${CSS_CLASSES.TEXT_MD} ${CSS_CLASSES.FONT_SEMIBOLD} ${CSS_CLASSES.MB2} ${CSS_CLASSES.TEXT_GRAY_700_DARK_GRAY_300}`;
heading.textContent = 'PBS Task Health (7d)';
sectionDiv.appendChild(heading);
const tableContainer = document.createElement('div');
// Make the tableContainer itself look more like a card
tableContainer.className = `${CSS_CLASSES.OVERFLOW_X_AUTO} ${CSS_CLASSES.BORDER_GRAY_200_DARK_BORDER_GRAY_700} ${CSS_CLASSES.ROUNDED} ${CSS_CLASSES.BG_GRAY_50_DARK_BG_GRAY_800_30} p-3`; // Added background and padding
const table = document.createElement('table');
table.className = `${CSS_CLASSES.MIN_W_FULL} ${CSS_CLASSES.TEXT_SM}`;
// Removed DIVIDE_Y from table itself, will rely on cell padding and borders if needed
const thead = document.createElement('thead');
thead.className = `${CSS_CLASSES.BG_GRAY_100_DARK_BG_GRAY_800}`; // Added background to header
const headerRow = document.createElement('tr');
headerRow.className = `${CSS_CLASSES.TEXT_XS} ${CSS_CLASSES.FONT_MEDIUM} ${CSS_CLASSES.TRACKING_WIDER} ${CSS_CLASSES.TEXT_LEFT} ${CSS_CLASSES.TEXT_GRAY_600_UPPERCASE_DARK_TEXT_GRAY_300} ${CSS_CLASSES.BORDER_B_GRAY_300_DARK_BORDER_GRAY_600}`;
const headers = ['Task Type', 'Status', 'Last Successful Run', 'Last Failure'];
headers.forEach(text => {
const th = document.createElement('th');
th.scope = 'col';
th.className = `${CSS_CLASSES.P1_PX2} ${CSS_CLASSES.TEXT_LEFT}`; // Ensure left alignment for headers
th.textContent = text;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
const tbody = document.createElement('tbody');
tbody.className = `pbs-task-health-tbody`;
// Data for the table rows
const taskHealthData = [
{ title: 'Backups', data: pbsInstanceData.backupTasks },
{ title: 'Verification', data: pbsInstanceData.verificationTasks },
{ title: 'Sync', data: pbsInstanceData.syncTasks },
{ title: 'Prune/GC', data: pbsInstanceData.pruneTasks }
];
taskHealthData.forEach(taskItem => {
const summary = taskItem.data?.summary || {};
const ok = summary.ok ?? '-';
const failed = summary.failed ?? 0; // Default to 0 for comparison
const total = summary.total ?? '-';
const lastOk = PulseApp.utils.formatPbsTimestamp(summary.lastOk);
const lastFailed = PulseApp.utils.formatPbsTimestamp(summary.lastFailed);
const row = tbody.insertRow();
// Remove left border highlighting - rely on text styling in Status cell
// if (failed > 0) {
// row.className = CSS_CLASSES.BORDER_L_4_RED_500_DARK_RED_400;
// } else {
// row.className = CSS_CLASSES.BORDER_L_4_TRANSPARENT;
// }
// Ensure row still has its bottom border for separation
row.className = 'initial-row-class-if-any'; // Clear previous BORDER_L classes
row.classList.add(...CSS_CLASSES.BORDER_B_GRAY_200_DARK_GRAY_700.split(' '));
const cellTaskType = row.insertCell();
cellTaskType.className = `${CSS_CLASSES.P1_PX2} ${CSS_CLASSES.FONT_SEMIBOLD} ${CSS_CLASSES.TEXT_GRAY_800_DARK_GRAY_200}`;
cellTaskType.textContent = taskItem.title;
const cellStatus = row.insertCell();
cellStatus.className = CSS_CLASSES.P1_PX2;
const okHtml = `<span class="${CSS_CLASSES.TEXT_GREEN_600_DARK_GREEN_400}">${ok} OK</span>`;
const failHtml = `<span class="${failed > 0 ? CSS_CLASSES.FONT_BOLD + ' ' + CSS_CLASSES.TEXT_RED_600_DARK_RED_400 : CSS_CLASSES.TEXT_GRAY_600_DARK_GRAY_400}">${failed} Fail</span>`;
cellStatus.innerHTML = `${okHtml} / ${failHtml}`;
const cellLastOk = row.insertCell();
cellLastOk.className = `${CSS_CLASSES.P1_PX2} ${CSS_CLASSES.TEXT_GRAY_600_DARK_GRAY_400}`;
cellLastOk.textContent = lastOk;
const cellLastFail = row.insertCell();
cellLastFail.className = `${CSS_CLASSES.P1_PX2} ${CSS_CLASSES.TEXT_GRAY_600_DARK_GRAY_400}`;
cellLastFail.textContent = lastFailed;
});
table.appendChild(tbody);
tableContainer.appendChild(table);
sectionDiv.appendChild(tableContainer);
return sectionDiv;
};
const _createTaskTableElement = (tableId, title, idColumnHeader) => {
const fragment = document.createDocumentFragment();
const heading = document.createElement('h4');
heading.className = `${CSS_CLASSES.TEXT_MD} ${CSS_CLASSES.FONT_SEMIBOLD} ${CSS_CLASSES.MB2} ${CSS_CLASSES.TEXT_GRAY_700_DARK_GRAY_300}`;
heading.textContent = `Recent ${title} Tasks`;
fragment.appendChild(heading);
const tableContainer = document.createElement('div');
tableContainer.className = `${CSS_CLASSES.OVERFLOW_X_AUTO} ${CSS_CLASSES.BORDER_GRAY_200_DARK_BORDER_GRAY_700} ${CSS_CLASSES.ROUNDED}`;
const table = document.createElement('table');
table.id = tableId;
table.className = `${CSS_CLASSES.MIN_W_FULL} ${CSS_CLASSES.DIVIDE_Y_GRAY_200_DARK_DIVIDE_GRAY_700} ${CSS_CLASSES.TEXT_SM}`;
const thead = document.createElement('thead');
thead.className = `${CSS_CLASSES.STICKY} ${CSS_CLASSES.TOP_0} ${CSS_CLASSES.Z_10} ${CSS_CLASSES.BG_GRAY_100_DARK_BG_GRAY_800}`;
const headerRow = document.createElement('tr');
headerRow.className = `${CSS_CLASSES.TEXT_XS} ${CSS_CLASSES.FONT_MEDIUM} ${CSS_CLASSES.TRACKING_WIDER} ${CSS_CLASSES.TEXT_LEFT} ${CSS_CLASSES.TEXT_GRAY_600_UPPERCASE_DARK_TEXT_GRAY_300} ${CSS_CLASSES.BORDER_B_GRAY_300_DARK_BORDER_GRAY_600}`;
const headers = [idColumnHeader, 'Status', 'Start Time', 'Duration', 'UPID'];
headers.forEach(text => {
const th = document.createElement('th');
th.scope = 'col';
th.className = CSS_CLASSES.P1_PX2;
th.textContent = text;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
const tbody = document.createElement('tbody');
tbody.id = tableId.replace('-table-', '-tbody-');
tbody.className = `${CSS_CLASSES.PBS_TASK_TBODY} ${CSS_CLASSES.DIVIDE_Y_GRAY_200_DARK_DIVIDE_GRAY_700}`;
table.appendChild(tbody);
tableContainer.appendChild(table);
fragment.appendChild(tableContainer);
const toggleButtonContainer = document.createElement('div');
toggleButtonContainer.id = tableId.replace('-table', '-toggle-container');
toggleButtonContainer.className = `pbs-toggle-button-container ${CSS_CLASSES.PT1} ${CSS_CLASSES.TEXT_RIGHT}`;
const showMoreButton = document.createElement('button');
showMoreButton.className = `${CSS_CLASSES.PBS_SHOW_MORE} ${CSS_CLASSES.TEXT_BLUE_600_HOVER_BLUE_800_DARK_BLUE_400_DARK_HOVER_BLUE_300} ${CSS_CLASSES.HIDDEN}`;
showMoreButton.textContent = 'Show More';
toggleButtonContainer.appendChild(showMoreButton);
const noTasksMessage = document.createElement('p');
noTasksMessage.className = `${CSS_CLASSES.PBS_NO_TASKS} ${CSS_CLASSES.TEXT_XS} ${CSS_CLASSES.TEXT_GRAY_400} dark:text-gray-500 ${CSS_CLASSES.HIDDEN} ${CSS_CLASSES.ITALIC}`;
noTasksMessage.textContent = 'No recent tasks found.';
toggleButtonContainer.appendChild(noTasksMessage);
fragment.appendChild(toggleButtonContainer);
return fragment;
};
const _createSummariesSectionElement = (instanceId, pbsInstanceData) => {
// This function is now a wrapper for _createPbsTaskHealthTable
// The old grid layout is replaced by the single table.
return _createPbsTaskHealthTable(instanceId, pbsInstanceData);
};
const _createAllTaskSectionsContainer = (instanceId) => {
const container = document.createElement('div'); // Or DocumentFragment
container.className = CSS_CLASSES.SPACE_Y_4; // Add some spacing between task sections
const taskDefinitions = [
{ type: 'backup', title: 'Backup', idCol: 'Guest', tableIdPrefix: ID_PREFIXES.PBS_RECENT_BACKUP_TASKS_TABLE },
{ type: 'verify', title: 'Verification', idCol: 'Guest/Group', tableIdPrefix: ID_PREFIXES.PBS_RECENT_VERIFY_TASKS_TABLE },
{ type: 'sync', title: 'Sync', idCol: 'Job ID', tableIdPrefix: ID_PREFIXES.PBS_RECENT_SYNC_TASKS_TABLE },
{ type: 'prunegc', title: 'Prune/GC', idCol: 'Datastore/Group', tableIdPrefix: ID_PREFIXES.PBS_RECENT_PRUNEGC_TASKS_TABLE }
];
taskDefinitions.forEach(def => {
const taskSection = document.createElement('div');
taskSection.className = CSS_CLASSES.PBS_TASK_SECTION;
taskSection.dataset.taskType = def.type;
taskSection.appendChild(_createTaskTableElement(def.tableIdPrefix + instanceId, def.title, def.idCol));
container.appendChild(taskSection);
});
return container;
};
const _createPbsInstanceElement = (pbsInstanceData, instanceId, instanceName, overallHealth, healthTitle, showDetails, statusText) => {
const instanceWrapper = document.createElement('div');
instanceWrapper.className = `${CSS_CLASSES.PBS_INSTANCE_SECTION} ${CSS_CLASSES.BORDER_GRAY_200_DARK_BORDER_GRAY_700} ${CSS_CLASSES.ROUNDED} p-4 mb-4 bg-gray-50/30 dark:bg-gray-800/30`;
instanceWrapper.id = ID_PREFIXES.PBS_INSTANCE + instanceId;
instanceWrapper.appendChild(_createInstanceHeaderDiv(instanceName, overallHealth, healthTitle));
const detailsContainer = document.createElement('div');
detailsContainer.className = `${CSS_CLASSES.PBS_INSTANCE_DETAILS} ${CSS_CLASSES.SPACE_Y_4} ${showDetails ? '' : CSS_CLASSES.HIDDEN}`;
detailsContainer.id = ID_PREFIXES.PBS_DETAILS + instanceId;
detailsContainer.appendChild(_createDatastoreSectionElement(instanceId));
detailsContainer.appendChild(_createSummariesSectionElement(instanceId, pbsInstanceData));
detailsContainer.appendChild(_createAllTaskSectionsContainer(instanceId));
instanceWrapper.appendChild(detailsContainer);
// Populate dynamic content after structure is built
const dsTableBodyElement = instanceWrapper.querySelector(`#${ID_PREFIXES.PBS_DS_TBODY}${instanceId}`);
_populateDsTableBody(dsTableBodyElement, pbsInstanceData.datastores, statusText, showDetails);
_populateInstanceTaskSections(detailsContainer, instanceId, pbsInstanceData, statusText, showDetails);
return instanceWrapper;
};
pbsArray.forEach((pbsInstance, index) => {
const rawInstanceId = pbsInstance.pbsEndpointId || `instance-${index}`;
const instanceId = PulseApp.utils.sanitizeForId(rawInstanceId);
const instanceName = pbsInstance.pbsInstanceName || `PBS Instance ${index + 1}`;
const instanceElementId = ID_PREFIXES.PBS_INSTANCE + instanceId;
currentInstanceIds.add(instanceElementId);
let instanceWrapper = document.getElementById(instanceElementId);
let detailsContainer, dsTableBody, instanceTitleElement;
let statusText = 'Loading...';
let showDetails = false;
switch (pbsInstance.status) {
case 'configured':
statusText = `Configured, attempting connection...`;
break;
case 'ok':
statusText = `Status: OK`;
showDetails = true;
break;
case 'error':
statusText = `Error: ${pbsInstance.errorMessage || 'Connection failed'}`;
break;
default:
statusText = `Status: ${pbsInstance.status || 'Unknown'}`;
break;
if (!pbsSubTabsNav || !pbsSubTabsContent) {
console.error('[PBS UI] Sub-tab navigation or content area not found in HTML.');
if (pbsLoadingMessageEl) pbsLoadingMessageEl.textContent = 'UI Error: PBS tab structure missing.';
return;
}
let overallHealth = 'ok';
let healthTitle = 'OK';
if (pbsInstance.status === 'error') {
overallHealth = 'error';
healthTitle = `Error: ${pbsInstance.errorMessage || 'Connection failed'}`;
} else if (pbsInstance.status !== 'ok') {
overallHealth = 'warning';
healthTitle = 'Connecting or unknown status';
} else {
const highUsageDatastore = (pbsInstance.datastores || []).find(ds => {
const totalBytes = ds.total || 0;
const usedBytes = ds.used || 0;
const usagePercent = totalBytes > 0 ? Math.round((usedBytes / totalBytes) * 100) : 0;
return usagePercent > 85;
});
if (highUsageDatastore) {
overallHealth = 'warning';
healthTitle = `Warning: Datastore ${highUsageDatastore.name} usage high (${Math.round((highUsageDatastore.used / highUsageDatastore.total) * 100)}%)`;
pbsSubTabsNav.innerHTML = '';
pbsSubTabsContent.innerHTML = '';
const validPbsInstances = pbsArray ? pbsArray.filter(p => p && p.status !== 'error' && p.status !== 'pending_initialization') : [];
if (!pbsArray || pbsArray.length === 0 || validPbsInstances.length === 0) {
let message = 'No PBS instances configured or all are currently unavailable.';
if (!pbsArray || pbsArray.length === 0) {
message = 'No PBS instances configured.';
} else if (validPbsInstances.length === 0) {
message = 'Could not retrieve data from any configured PBS instances.';
}
const messageElement = document.createElement('p');
messageElement.textContent = message;
messageElement.className = `${CSS_CLASSES.TEXT_GRAY_500_DARK_GRAY_400_P4_TEXT_CENTER_TEXT_SM}`;
pbsSubTabsContent.appendChild(messageElement);
return;
}
pbsArray.forEach((pbsInstanceData, index) => {
if (!pbsInstanceData || !pbsInstanceData.pbsEndpointId) {
console.warn('[PBS UI] Skipping PBS instance due to missing data or endpoint ID:', pbsInstanceData);
return;
}
const instanceId = pbsInstanceData.pbsEndpointId;
const instanceName = pbsInstanceData.pbsInstanceName || `PBS ${index + 1}`;
const tabButton = document.createElement('button');
tabButton.textContent = instanceName;
tabButton.setAttribute('data-pbs-instance-id', instanceId);
tabButton.className = `pbs-sub-tab-button py-2 px-4 -mb-px border-b-2 border-transparent hover:text-blue-600 dark:hover:text-blue-400 hover:border-blue-500 dark:hover:border-blue-400 focus:outline-none text-sm font-medium text-gray-500 dark:text-gray-400`;
const contentPanel = document.createElement('div');
contentPanel.id = `${ID_PREFIXES.PBS_SUB_TAB_CONTENT_PANEL_PREFIX}${instanceId}`;
contentPanel.className = `pbs-instance-content-panel ${CSS_CLASSES.SPACE_Y_4}`;
if (index !== 0) {
contentPanel.classList.add(CSS_CLASSES.HIDDEN);
}
if (overallHealth !== 'error') {
const hasFailures = [
pbsInstance.backupTasks,
pbsInstance.verificationTasks,
pbsInstance.syncTasks,
pbsInstance.pruneTasks
].some(taskGroup => (taskGroup?.summary?.failed ?? 0) > 0);
const overallHealth = pbsInstanceData.status === 'ok' ? 'ok' : (pbsInstanceData.status || 'error');
const healthTitle = pbsInstanceData.status === 'ok' ? 'Instance OK' : `Instance status: ${pbsInstanceData.status || 'Unknown'}`;
const showDetails = PulseApp.state.getPbShowDetailsState(instanceId, true);
const statusText = (pbsInstanceData.status !== 'ok' && pbsInstanceData.status !== 'pending_initialization') ? `Instance reported status: ${pbsInstanceData.status}` : '';
if (hasFailures) {
overallHealth = 'error';
healthTitle = 'Error: One or more recent tasks failed';
_createPbsInstanceElement(pbsInstanceData, instanceId, instanceName, overallHealth, healthTitle, showDetails, statusText, contentPanel);
pbsSubTabsNav.appendChild(tabButton);
pbsSubTabsContent.appendChild(contentPanel);
// Add Click Listener to Tab Button
tabButton.addEventListener('click', () => {
// Deactivate all other tabs
pbsSubTabsNav.querySelectorAll('.pbs-sub-tab-button').forEach(btn => {
btn.classList.remove('text-blue-600', 'dark:text-blue-300', 'border-blue-500', 'dark:border-blue-400', 'font-semibold');
btn.classList.add('text-gray-500', 'dark:text-gray-400', 'border-transparent');
});
// Activate clicked tab
tabButton.classList.add('text-blue-600', 'dark:text-blue-300', 'border-blue-500', 'dark:border-blue-400', 'font-semibold');
tabButton.classList.remove('text-gray-500', 'dark:text-gray-400', 'border-transparent');
// Hide all content panels
pbsSubTabsContent.querySelectorAll('.pbs-instance-content-panel').forEach(panel => {
panel.classList.add(CSS_CLASSES.HIDDEN);
});
// Show clicked tab's content panel
const targetPanelId = `${ID_PREFIXES.PBS_SUB_TAB_CONTENT_PANEL_PREFIX}${tabButton.getAttribute('data-pbs-instance-id')}`;
const targetPanel = document.getElementById(targetPanelId);
if (targetPanel) {
targetPanel.classList.remove(CSS_CLASSES.HIDDEN);
}
}
});
});
// Activate the first tab by default if tabs were created
if (pbsSubTabsNav.firstChild && typeof pbsSubTabsNav.firstChild.click === 'function') {
pbsSubTabsNav.firstChild.click();
}
}
if (instanceWrapper) {
detailsContainer = instanceWrapper.querySelector(`#${ID_PREFIXES.PBS_DETAILS}${instanceId}`);
instanceTitleElement = instanceWrapper.querySelector('h3');
if (instanceTitleElement) {
instanceTitleElement.innerHTML = ''; // Clear existing content
instanceTitleElement.appendChild(_createHealthBadgeHTML(overallHealth, healthTitle));
instanceTitleElement.appendChild(document.createTextNode(instanceName));
}
const _createAllTaskSectionsContainer = (instanceId) => {
// ... existing implementation ...
};
if (detailsContainer) {
dsTableBody = detailsContainer.querySelector(`#${ID_PREFIXES.PBS_DS_TBODY}${instanceId}`);
_populateDsTableBody(dsTableBody, pbsInstance.datastores, statusText, showDetails);
// MODIFIED: Added targetPanelElement argument, appends to it, returns instanceSection
const _createPbsInstanceElement = (pbsInstanceData, instanceId, instanceName, overallHealth, healthTitle, showDetails, statusText, targetPanelElement) => {
if (!targetPanelElement) {
console.error(`[PBS UI _createPbsInstanceElement] No targetPanelElement provided for instance ${instanceName} (${instanceId})`);
return null;
}
targetPanelElement.innerHTML = ''; // Clear the target panel first
const summariesSection = detailsContainer.querySelector(`#${ID_PREFIXES.PBS_SUMMARIES_SECTION}${instanceId}`);
if (summariesSection) {
summariesSection.innerHTML = '';
summariesSection.appendChild(_createPbsTaskHealthTable(instanceId, pbsInstance));
}
const instanceSection = document.createElement('div');
instanceSection.id = `${ID_PREFIXES.PBS_INSTANCE}${instanceId}`;
instanceSection.className = `${CSS_CLASSES.PBS_INSTANCE_SECTION} ${CSS_CLASSES.BORDER_GRAY_200_DARK_BORDER_GRAY_700} ${CSS_CLASSES.ROUNDED} ${CSS_CLASSES.P3} ${CSS_CLASSES.BG_GRAY_100_50_DARK_BG_GRAY_700_50}`;
instanceSection.classList.toggle(CSS_CLASSES.BORDER_L_4_RED_500_DARK_RED_400, overallHealth === 'error');
instanceSection.classList.toggle(CSS_CLASSES.BORDER_L_4_TRANSPARENT, overallHealth !== 'error');
_populateInstanceTaskSections(detailsContainer, instanceId, pbsInstance, statusText, showDetails);
detailsContainer.classList.toggle(CSS_CLASSES.HIDDEN, !showDetails);
}
const headerDiv = _createInstanceHeaderDiv(instanceName, overallHealth, healthTitle);
instanceSection.appendChild(headerDiv);
const detailsContainer = document.createElement('div');
detailsContainer.id = `${ID_PREFIXES.PBS_DETAILS}${instanceId}`;
detailsContainer.className = CSS_CLASSES.PBS_INSTANCE_DETAILS;
detailsContainer.classList.toggle(CSS_CLASSES.HIDDEN, !showDetails);
if (overallHealth === 'error' && statusText) {
const errorPara = document.createElement('p');
errorPara.className = `${CSS_CLASSES.TEXT_RED_500_DARK_RED_400} ${CSS_CLASSES.TEXT_SM} ${CSS_CLASSES.MB2}`;
errorPara.textContent = statusText;
detailsContainer.appendChild(errorPara);
} else if (pbsInstanceData && pbsInstanceData.status === 'ok') {
const dsSection = _createDatastoreSectionElement(instanceId);
_populateDsTableBody(dsSection.querySelector('tbody'), pbsInstanceData.datastores || [], statusText, showDetails);
detailsContainer.appendChild(dsSection);
const taskHealthTableSection = _createPbsTaskHealthTable(instanceId, pbsInstanceData);
detailsContainer.appendChild(taskHealthTableSection);
const allTaskSectionsContainer = _createAllTaskSectionsContainer(instanceId);
_populateInstanceTaskSections(allTaskSectionsContainer, instanceId, pbsInstanceData, statusText, showDetails);
detailsContainer.appendChild(allTaskSectionsContainer);
} else if (pbsInstanceData && (pbsInstanceData.status === 'pending_initialization' || !pbsInstanceData.status)) {
const pendingMsg = document.createElement('p');
pendingMsg.textContent = `Data for ${instanceName} is still loading or initializing...`;
pendingMsg.className = `${CSS_CLASSES.TEXT_GRAY_500_DARK_GRAY_400} ${CSS_CLASSES.TEXT_SM}`;
detailsContainer.appendChild(pendingMsg);
} else {
instanceWrapper = _createPbsInstanceElement(pbsInstance, instanceId, instanceName, overallHealth, healthTitle, showDetails, statusText);
container.appendChild(instanceWrapper);
const noDataMsg = document.createElement('p');
noDataMsg.textContent = `No detailed data available for ${instanceName}.`;
noDataMsg.className = `${CSS_CLASSES.TEXT_GRAY_500_DARK_GRAY_400} ${CSS_CLASSES.TEXT_SM}`;
detailsContainer.appendChild(noDataMsg);
}
});
container.querySelectorAll(`.${CSS_CLASSES.PBS_INSTANCE_SECTION}`).forEach(el => {
if (!currentInstanceIds.has(el.id)) {
el.remove();
}
});
}
instanceSection.appendChild(detailsContainer);
targetPanelElement.appendChild(instanceSection);
return instanceSection;
};
function initPbsEventListeners() {
// Event listeners for "Show More" buttons are now handled within populatePbsTaskTable
// to ensure they are attached correctly when tables are dynamically populated.
// This function is kept if other PBS-specific global event listeners are needed in the future.
const pbsInstancesContainer = document.getElementById(ID_PREFIXES.PBS_INSTANCES_CONTAINER);
if (!pbsInstancesContainer) {
console.warn(`PBS instances container #${ID_PREFIXES.PBS_INSTANCES_CONTAINER} not found. Some UI interactions might not work.`);
}
// const pbsInstancesContainer = document.getElementById(ID_PREFIXES.PBS_INSTANCES_CONTAINER);
// if (!pbsInstancesContainer) {
// console.warn(`PBS instances container #${ID_PREFIXES.PBS_INSTANCES_CONTAINER} not found. Some UI interactions might not work.`);
// }
}
return {