Persist selections across pages, add Select All for Granite Loader
- Selections persist when paging through the device table - Select All button fetches all hostnames for the active sheet - Granite Loader button fetches full device data for all selected (not just current page) before opening the modal - Clear selection button when items are selected across pages - Page checkbox adds to existing selection instead of replacing
This commit is contained in:
@@ -68,6 +68,7 @@ export default function GraniteHygieneSection({ activeTeam, teamScope: _teamScop
|
||||
setActiveSheet(sheetName);
|
||||
setDevicesPage(1);
|
||||
setDevicesSearch('');
|
||||
setSelectedDevices(new Set());
|
||||
fetchDevices(sheetName, 1, '');
|
||||
}
|
||||
};
|
||||
@@ -83,6 +84,45 @@ export default function GraniteHygieneSection({ activeTeam, teamScope: _teamScop
|
||||
fetchDevices(activeSheet, 1, val);
|
||||
};
|
||||
|
||||
// Select all devices for the active sheet (fetches all hostnames)
|
||||
const selectAll = async () => {
|
||||
if (!activeSheet) return;
|
||||
try {
|
||||
const params = new URLSearchParams({ sheet: activeSheet, page: '1', page_size: '10000' });
|
||||
if (activeTeam) params.set('team', activeTeam);
|
||||
const res = await fetch(`${API_BASE}/compliance/supplemental/items?${params}`, { credentials: 'include' });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setSelectedDevices(new Set((data.devices || []).map(d => d.hostname)));
|
||||
}
|
||||
} catch (_err) { /* silent */ }
|
||||
};
|
||||
|
||||
// Open Loader with all selected devices (fetches full data for all selected)
|
||||
const openLoaderWithSelected = async () => {
|
||||
if (selectedDevices.size === 0) return;
|
||||
// If all selected are on current page, use local data
|
||||
const allOnPage = [...selectedDevices].every(h => devices.some(d => d.hostname === h));
|
||||
if (allOnPage) {
|
||||
setShowLoaderModal(true);
|
||||
return;
|
||||
}
|
||||
// Fetch all selected devices from API
|
||||
try {
|
||||
const params = new URLSearchParams({ sheet: activeSheet, page: '1', page_size: '10000' });
|
||||
if (activeTeam) params.set('team', activeTeam);
|
||||
const res = await fetch(`${API_BASE}/compliance/supplemental/items?${params}`, { credentials: 'include' });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const allDevices = (data.devices || []).filter(d => selectedDevices.has(d.hostname));
|
||||
setAllSelectedDeviceData(allDevices);
|
||||
}
|
||||
} catch (_err) { /* silent */ }
|
||||
setShowLoaderModal(true);
|
||||
};
|
||||
|
||||
const [allSelectedDeviceData, setAllSelectedDeviceData] = useState(null);
|
||||
|
||||
// Don't render if no data
|
||||
if (!summary || (summary.sheets && summary.sheets.length === 0)) {
|
||||
if (loading) return null;
|
||||
@@ -170,7 +210,7 @@ export default function GraniteHygieneSection({ activeTeam, teamScope: _teamScop
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
{selectedDevices.size > 0 && (
|
||||
<button
|
||||
onClick={() => setShowLoaderModal(true)}
|
||||
onClick={openLoaderWithSelected}
|
||||
style={styles.loaderBtn}
|
||||
>
|
||||
<FileSpreadsheet style={{ width: '12px', height: '12px' }} />
|
||||
@@ -208,10 +248,15 @@ export default function GraniteHygieneSection({ activeTeam, teamScope: _teamScop
|
||||
if (devices.every(d => selectedDevices.has(d.hostname))) {
|
||||
setSelectedDevices(new Set());
|
||||
} else {
|
||||
setSelectedDevices(new Set(devices.map(d => d.hostname)));
|
||||
setSelectedDevices(prev => {
|
||||
const next = new Set(prev);
|
||||
devices.forEach(d => next.add(d.hostname));
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}}
|
||||
style={{ accentColor: '#14B8A6', cursor: 'pointer' }}
|
||||
title={`Select page (${devices.length})`}
|
||||
/>
|
||||
</th>
|
||||
<th style={styles.th}>Hostname</th>
|
||||
@@ -281,6 +326,20 @@ export default function GraniteHygieneSection({ activeTeam, teamScope: _teamScop
|
||||
>
|
||||
Next →
|
||||
</button>
|
||||
<button
|
||||
onClick={selectAll}
|
||||
style={{ ...styles.pageBtn, color: '#14B8A6', borderColor: 'rgba(20,184,166,0.4)' }}
|
||||
>
|
||||
Select All ({devicesTotal})
|
||||
</button>
|
||||
{selectedDevices.size > 0 && selectedDevices.size !== devices.length && (
|
||||
<button
|
||||
onClick={() => setSelectedDevices(new Set())}
|
||||
style={{ ...styles.pageBtn, color: '#64748B' }}
|
||||
>
|
||||
Clear ({selectedDevices.size})
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -302,13 +361,16 @@ export default function GraniteHygieneSection({ activeTeam, teamScope: _teamScop
|
||||
{/* Granite Loader Modal */}
|
||||
<LoaderModal
|
||||
isOpen={showLoaderModal}
|
||||
onClose={() => { setShowLoaderModal(false); setSelectedDevices(new Set()); }}
|
||||
initialDevices={showLoaderModal ? devices.filter(d => selectedDevices.has(d.hostname)).map(d => ({
|
||||
ip_address: d.ip_address || '',
|
||||
hostname: d.hostname || '',
|
||||
host_id: null,
|
||||
equip_inst_id: d.equip_inst_id || '',
|
||||
})) : null}
|
||||
onClose={() => { setShowLoaderModal(false); setAllSelectedDeviceData(null); }}
|
||||
initialDevices={showLoaderModal ? (() => {
|
||||
const source = allSelectedDeviceData || devices.filter(d => selectedDevices.has(d.hostname));
|
||||
return source.map(d => ({
|
||||
ip_address: d.ip_address || '',
|
||||
hostname: d.hostname || '',
|
||||
host_id: null,
|
||||
equip_inst_id: d.equip_inst_id || '',
|
||||
}));
|
||||
})() : null}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user