Skip to content

Commit bb82610

Browse files
committed
Add new public JS utility and UI files
1 parent caf6a2b commit bb82610

File tree

6 files changed

+621
-0
lines changed

6 files changed

+621
-0
lines changed

src/public/js/debounce.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Debounce utility function
2+
function debounce(func, wait) {
3+
let timeout;
4+
return function executedFunction(...args) {
5+
const later = () => {
6+
clearTimeout(timeout);
7+
func(...args);
8+
};
9+
clearTimeout(timeout);
10+
timeout = setTimeout(later, wait);
11+
};
12+
}
13+
14+
// Export for use in other modules
15+
if (typeof PulseApp !== 'undefined') {
16+
PulseApp.utils = PulseApp.utils || {};
17+
PulseApp.utils.debounce = debounce;
18+
}

src/public/js/ui/empty-states.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Empty state UI components
2+
PulseApp.ui = PulseApp.ui || {};
3+
4+
PulseApp.ui.emptyStates = (() => {
5+
6+
function createEmptyState(type, context = {}) {
7+
const emptyStates = {
8+
'no-guests': {
9+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="mx-auto mb-4 text-gray-300 dark:text-gray-600">
10+
<rect width="20" height="14" x="2" y="5" rx="2" ry="2"/>
11+
<line x1="2" y1="10" x2="22" y2="10"/>
12+
</svg>`,
13+
title: 'No Virtual Machines or Containers',
14+
message: 'No VMs or containers are currently configured on this node.',
15+
actions: []
16+
},
17+
'no-results': {
18+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="mx-auto mb-4 text-gray-300 dark:text-gray-600">
19+
<circle cx="11" cy="11" r="8"/>
20+
<path d="m21 21-4.35-4.35"/>
21+
<line x1="11" y1="8" x2="11" y2="14"/>
22+
<line x1="8" y1="11" x2="14" y2="11"/>
23+
</svg>`,
24+
title: 'No Matching Results',
25+
message: _buildFilterMessage(context),
26+
actions: [{
27+
text: 'Clear Filters',
28+
onclick: 'PulseApp.ui.common.resetDashboardView()'
29+
}]
30+
},
31+
'no-storage': {
32+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="mx-auto mb-4 text-gray-300 dark:text-gray-600">
33+
<ellipse cx="12" cy="5" rx="9" ry="3"/>
34+
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/>
35+
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
36+
</svg>`,
37+
title: 'No Storage Configured',
38+
message: 'No storage repositories are configured on this system.',
39+
actions: []
40+
},
41+
'no-backups': {
42+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="mx-auto mb-4 text-gray-300 dark:text-gray-600">
43+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
44+
<path d="m9 12 2 2 4-4"/>
45+
</svg>`,
46+
title: 'No Backups Found',
47+
message: context.filtered ? 'No backups match your current filters.' : 'No backup data is available yet.',
48+
actions: context.filtered ? [{
49+
text: 'Clear Filters',
50+
onclick: 'PulseApp.ui.backups.resetBackupsView()'
51+
}] : []
52+
},
53+
'no-pbs': {
54+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="mx-auto mb-4 text-gray-300 dark:text-gray-600">
55+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
56+
<line x1="12" y1="8" x2="12" y2="12"/>
57+
<line x1="12" y1="16" x2="12.01" y2="16"/>
58+
</svg>`,
59+
title: 'No Proxmox Backup Servers',
60+
message: 'No PBS instances are configured or available.',
61+
actions: []
62+
},
63+
'loading': {
64+
icon: `<div class="mx-auto mb-4">
65+
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
66+
</div>`,
67+
title: 'Loading...',
68+
message: 'Fetching data from the server.',
69+
actions: []
70+
},
71+
'error': {
72+
icon: `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="mx-auto mb-4 text-red-400 dark:text-red-600">
73+
<circle cx="12" cy="12" r="10"/>
74+
<line x1="12" y1="8" x2="12" y2="12"/>
75+
<line x1="12" y1="16" x2="12.01" y2="16"/>
76+
</svg>`,
77+
title: 'Error Loading Data',
78+
message: context.error || 'Failed to load data. Please try again.',
79+
actions: [{
80+
text: 'Retry',
81+
onclick: 'location.reload()'
82+
}]
83+
}
84+
};
85+
86+
const state = emptyStates[type] || emptyStates['no-results'];
87+
88+
return `
89+
<div class="flex flex-col items-center justify-center py-12 px-4">
90+
${state.icon}
91+
<h3 class="text-lg font-medium text-gray-700 dark:text-gray-300 mb-2">${state.title}</h3>
92+
<p class="text-sm text-gray-700 dark:text-gray-300 text-center max-w-md mb-6">${state.message}</p>
93+
${state.actions.length > 0 ? `
94+
<div class="flex gap-3">
95+
${state.actions.map(action => `
96+
<button onclick="${action.onclick}" class="px-4 py-2 bg-blue-500 text-white text-sm rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition-colors">
97+
${action.text}
98+
</button>
99+
`).join('')}
100+
</div>
101+
` : ''}
102+
</div>
103+
`;
104+
}
105+
106+
function _buildFilterMessage(context) {
107+
const filters = [];
108+
109+
if (context.filterType && context.filterType !== 'all') {
110+
filters.push(`Type: ${context.filterType.toUpperCase()}`);
111+
}
112+
113+
if (context.filterStatus && context.filterStatus !== 'all') {
114+
filters.push(`Status: ${context.filterStatus}`);
115+
}
116+
117+
if (context.searchTerms && context.searchTerms.length > 0) {
118+
filters.push(`Search: "${context.searchTerms.join(', ')}"`);
119+
}
120+
121+
if (context.thresholds && context.thresholds.length > 0) {
122+
filters.push(`Thresholds: ${context.thresholds.join(', ')}`);
123+
}
124+
125+
if (filters.length === 0) {
126+
return 'No items match your current view.';
127+
}
128+
129+
return `No items found matching: ${filters.join(' • ')}`;
130+
}
131+
132+
function createTableEmptyState(type, context, colspan) {
133+
const emptyStateHtml = createEmptyState(type, context);
134+
return `<tr><td colspan="${colspan}" class="p-0">${emptyStateHtml}</td></tr>`;
135+
}
136+
137+
return {
138+
createEmptyState,
139+
createTableEmptyState
140+
};
141+
})();

src/public/js/ui/loading-skeletons.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Loading skeleton components for better perceived performance
2+
PulseApp.ui = PulseApp.ui || {};
3+
4+
PulseApp.ui.loadingSkeletons = (() => {
5+
6+
function createTableRowSkeleton(columns = 11) {
7+
const cells = [];
8+
for (let i = 0; i < columns; i++) {
9+
cells.push(`
10+
<td class="p-1 px-2">
11+
<div class="h-4 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
12+
</td>
13+
`);
14+
}
15+
16+
return `
17+
<tr class="border-b border-gray-200 dark:border-gray-700">
18+
${cells.join('')}
19+
</tr>
20+
`;
21+
}
22+
23+
function createTableSkeleton(rows = 5, columns = 11) {
24+
const skeletonRows = [];
25+
for (let i = 0; i < rows; i++) {
26+
skeletonRows.push(createTableRowSkeleton(columns));
27+
}
28+
29+
return skeletonRows.join('');
30+
}
31+
32+
function createNodeCardSkeleton() {
33+
return `
34+
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-sm border border-gray-200 dark:border-gray-700">
35+
<div class="flex items-center justify-between mb-3">
36+
<div class="h-6 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
37+
<div class="h-5 w-16 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
38+
</div>
39+
<div class="space-y-2">
40+
<div class="flex justify-between items-center">
41+
<div class="h-4 w-12 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
42+
<div class="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
43+
</div>
44+
<div class="h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
45+
</div>
46+
<div class="space-y-2 mt-2">
47+
<div class="flex justify-between items-center">
48+
<div class="h-4 w-16 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
49+
<div class="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
50+
</div>
51+
<div class="h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
52+
</div>
53+
</div>
54+
`;
55+
}
56+
57+
function createStorageRowSkeleton() {
58+
return `
59+
<tr class="transition-all duration-150">
60+
<td class="p-1 px-2">
61+
<div class="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
62+
</td>
63+
<td class="p-1 px-2">
64+
<div class="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
65+
</td>
66+
<td class="p-1 px-2">
67+
<div class="h-4 w-16 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
68+
</td>
69+
<td class="p-1 px-2">
70+
<div class="h-4 w-12 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
71+
</td>
72+
<td class="p-1 px-2">
73+
<div class="h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
74+
</td>
75+
<td class="p-1 px-2">
76+
<div class="h-4 w-16 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
77+
</td>
78+
<td class="p-1 px-2">
79+
<div class="h-4 w-16 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
80+
</td>
81+
</tr>
82+
`;
83+
}
84+
85+
function createBackupRowSkeleton() {
86+
return `
87+
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
88+
<td class="p-2 px-3">
89+
<div class="h-4 w-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
90+
</td>
91+
<td class="p-2 px-3">
92+
<div class="h-4 w-16 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
93+
</td>
94+
<td class="p-2 px-3">
95+
<div class="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
96+
</td>
97+
<td class="p-2 px-3">
98+
<div class="h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
99+
</td>
100+
<td class="p-2 px-3">
101+
<div class="h-4 w-12 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
102+
</td>
103+
<td class="p-2 px-3">
104+
<div class="h-5 w-16 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse"></div>
105+
</td>
106+
</tr>
107+
`;
108+
}
109+
110+
function showTableSkeleton(tableElement, rows = 5, columns = 11) {
111+
if (!tableElement) return;
112+
113+
const tbody = tableElement.querySelector('tbody');
114+
if (tbody) {
115+
tbody.innerHTML = createTableSkeleton(rows, columns);
116+
}
117+
}
118+
119+
function showNodeCardsSkeleton(container, count = 3) {
120+
if (!container) return;
121+
122+
const skeletons = [];
123+
for (let i = 0; i < count; i++) {
124+
skeletons.push(createNodeCardSkeleton());
125+
}
126+
127+
container.innerHTML = `
128+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
129+
${skeletons.join('')}
130+
</div>
131+
`;
132+
}
133+
134+
return {
135+
createTableRowSkeleton,
136+
createTableSkeleton,
137+
createNodeCardSkeleton,
138+
createStorageRowSkeleton,
139+
createBackupRowSkeleton,
140+
showTableSkeleton,
141+
showNodeCardsSkeleton
142+
};
143+
})();

0 commit comments

Comments
 (0)