Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "frontend",
"version": "1.5.11",
"version": "1.5.15",
"private": true,
"dependencies": {
"@emotion/react": "^11.14.0",
Expand Down
18 changes: 18 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Dashboard from './components/Dashboard/Dashboard';
import MyBookingsPage from './components/Booking/MyBookingsPage';
import NotificationsPage from './components/Notifications/NotificationsPage';
import SiteManagement from './components/Admin/SiteManagement';
import IsoManagement from './components/Admin/IsoManagement';
import { AuthContext } from './context/AuthContext';
import { SiteContext } from './context/SiteContext';
import { Box, CircularProgress, Typography } from '@mui/material';
Expand Down Expand Up @@ -94,6 +95,9 @@ const LayoutWrapper = ({ element, currentSection, requiredRole }) => {
case 'sites':
navigate('/sites');
break;
case 'isos': // <-- AGGIUNTO: Caso per la navigazione ISO
navigate('/isos');
break;
default:
navigate('/');
}
Expand Down Expand Up @@ -164,6 +168,20 @@ const App = () => {
/>
}
/>

{/* --- NUOVA ROTTA GESTIONE ISO --- */}
<Route
path="/isos"
element={
<LayoutWrapper
element={<IsoManagement />}
currentSection="isos"
requiredRole={SiteRoles.SITE_ADMIN}
/>
}
/>
{/* ------------------------------- */}

<Route
path="/profile"
element={
Expand Down
16 changes: 15 additions & 1 deletion src/components/Admin/AdminPanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import PeopleIcon from '@mui/icons-material/People';
import CategoryIcon from '@mui/icons-material/Category';
import ReceiptIcon from '@mui/icons-material/Receipt';
import WebhookIcon from '@mui/icons-material/Webhook';
import DesktopWindowsIcon from '@mui/icons-material/DesktopWindows'; // <-- AGGIUNTO: Icona per gli OS
import ResourceManagement from './ResourceManagement';
import UserManagement from './UserManagement';
import ResourceTypeManagement from './ResourceTypeManagement';
import AuditLogsManagement from './AuditLogsManagement';
import WebhookManagement from './WebhookManagement';
import IsoManagement from './IsoManagement'; // <-- AGGIUNTO: Import del componente creato nello Step 1

const AdminPanel = () => {
const { t } = useTranslation();
Expand Down Expand Up @@ -59,6 +61,13 @@ const AdminPanel = () => {
icon={<ReceiptIcon />}
sx={{ flexGrow: 1 }}
/>
{/* --- NUOVA TAB GESTIONE OS --- */}
<Tab
label="GESTIONE OS" // Nota: Puoi aggiungere 'adminPanel.isoManagement' al tuo file i18n
icon={<DesktopWindowsIcon />}
sx={{ flexGrow: 1 }}
/>
{/* ---------------------------- */}
<Tab
label={t('adminPanel.webhooks')}
icon={<WebhookIcon />}
Expand All @@ -80,7 +89,12 @@ const AdminPanel = () => {
)}
{currentTab === 2 && <UserManagement />}
{currentTab === 3 && <AuditLogsManagement />}
{currentTab === 4 && <WebhookManagement />}

{/* --- NUOVO CONTENUTO TAB ISO --- */}
{currentTab === 4 && <IsoManagement />}

{/* Scaliamo l'indice dei Webhook a 5 */}
{currentTab === 5 && <WebhookManagement />}
</Box>
</Paper>
</Box>
Expand Down
234 changes: 234 additions & 0 deletions src/components/Admin/IsoManagement.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
Box, Paper, Table, TableBody, TableCell, TableContainer,
TableHead, TableRow, Typography, TextField, Button,
IconButton, Tooltip, Card, CardContent, Chip
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
import DesktopWindowsIcon from '@mui/icons-material/DesktopWindows';
import LinkIcon from '@mui/icons-material/Link';
import { fetchAllIsosAdmin, saveIso, deleteIso } from '../../services/isoService';
import useApiError from '../../hooks/useApiError';

// Regex per feedback visivo immediato (inizia con http o https)
const URL_REGEX = /^(https?:\/\/)/;

const IsoManagement = () => {
const { t } = useTranslation();
const [isos, setIsos] = useState([]);

// Nota: 'name' è l'ID testuale (es. ubuntu), 'id' è quello numerico del DB (null per nuovi)
const [newIso, setNewIso] = useState({
name: '',
displayName: '',
imageUrl: '',
checksumUrl: '',
checksumType: 'sha256', // Default
active: true
});

const { withErrorHandling } = useApiError();
const [isSubmitting, setIsSubmitting] = useState(false);

const loadIsos = async () => {
await withErrorHandling(async () => {
const data = await fetchAllIsosAdmin();
setIsos(data);
});
};

useEffect(() => {
loadIsos();
}, []);

const handleSave = async (e) => {
e.preventDefault();

// Validazione Frontend pre-invio
if (newIso.imageUrl && !URL_REGEX.test(newIso.imageUrl)) {
alert(t('isoManagement.alertUrlInvalid'));
return;
}

setIsSubmitting(true);
try {
await withErrorHandling(async () => {
await saveIso(newIso);
// Reset form
setNewIso({ name: '', displayName: '', imageUrl: '', checksumUrl: '', checksumType: 'sha256', active: true });
await loadIsos();
});
} finally {
setIsSubmitting(false);
}
};

const handleDelete = async (id) => {
if (window.confirm(t('isoManagement.confirmDelete'))) {
await withErrorHandling(async () => {
await deleteIso(id);
await loadIsos();
});
}
};

// Helper per validazione visiva
const isUrlInvalid = (url) => url.length > 0 && !URL_REGEX.test(url);

return (
<Box sx={{ p: 3 }}>
<Typography variant="h5" gutterBottom sx={{ display: 'flex', alignItems: 'center', mb: 1, fontWeight: 'bold', color: 'primary.main' }}>
<DesktopWindowsIcon sx={{ mr: 1 }} />
{t('isoManagement.title')}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
{t('isoManagement.subtitle')}
</Typography>

{/* FORM DI AGGIUNTA */}
<Card variant="outlined" sx={{ mb: 4, borderRadius: 2, borderStyle: 'dashed' }}>
<CardContent>
<Typography variant="subtitle2" fontWeight="bold" gutterBottom color="primary">
{t('isoManagement.addIsoTitle')}
</Typography>
<Box component="form" onSubmit={handleSave} sx={{ mt: 2 }}>
{/* Prima riga: Identificativi */}
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 2fr' }, gap: 2, mb: 2 }}>
<TextField
size="small"
label={t('isoManagement.internalIdLabel')}
value={newIso.name}
onChange={e => setNewIso({...newIso, name: e.target.value})}
required
helperText={t('isoManagement.internalIdHelper')}
disabled={isSubmitting}
/>
<TextField
size="small"
label={t('isoManagement.displayNameLabel')}
value={newIso.displayName}
onChange={e => setNewIso({...newIso, displayName: e.target.value})}
required
helperText={t('isoManagement.displayNameHelper')}
disabled={isSubmitting}
/>
</Box>

{/* Seconda riga: URL */}
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '2fr 1fr' }, gap: 2, mb: 2 }}>
<TextField
size="small"
label={t('isoManagement.imageUrlLabel')}
value={newIso.imageUrl}
onChange={e => setNewIso({...newIso, imageUrl: e.target.value})}
required
placeholder="http://192.168.1.50/images/ubuntu.qcow2"
error={isUrlInvalid(newIso.imageUrl)}
helperText={isUrlInvalid(newIso.imageUrl) ? t('isoManagement.urlMustStartWithHttp') : t('isoManagement.urlDirectFile')}
disabled={isSubmitting}
InputProps={{ endAdornment: <LinkIcon color="action" fontSize="small" /> }}
/>
<TextField
size="small"
label={t('isoManagement.checksumUrlLabel')}
value={newIso.checksumUrl}
onChange={e => setNewIso({...newIso, checksumUrl: e.target.value})}
placeholder="http://.../SHA256SUMS"
error={isUrlInvalid(newIso.checksumUrl)}
helperText={isUrlInvalid(newIso.checksumUrl) ? t('isoManagement.urlMustStartWithHttp') : t('isoManagement.optionalRecommended')}
disabled={isSubmitting}
/>
</Box>

<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button
type="submit"
variant="contained"
startIcon={isSubmitting ? <CircularProgress size={20} color="inherit"/> : <AddCircleOutlineIcon />}
disabled={isSubmitting || isUrlInvalid(newIso.imageUrl)}
sx={{ px: 4, py: 1 }}
>
{isSubmitting ? t('isoManagement.verifyingAndSaving') : t('isoManagement.addIsoButton')}
</Button>
</Box>
</Box>
</CardContent>
</Card>

{/* TABELLA */}
<TableContainer component={Paper} variant="outlined" sx={{ borderRadius: 2 }}>
<Table sx={{ minWidth: 800 }}>
<TableHead sx={{ bgcolor: 'action.hover' }}>
<TableRow>
<TableCell sx={{ fontWeight: 'bold' }}>{t('isoManagement.table.id')}</TableCell>
<TableCell sx={{ fontWeight: 'bold' }}>{t('isoManagement.table.name')}</TableCell>
<TableCell sx={{ fontWeight: 'bold' }}>{t('isoManagement.table.urlConfig')}</TableCell>
<TableCell sx={{ fontWeight: 'bold' }} align="center">{t('isoManagement.table.status')}</TableCell>
<TableCell sx={{ fontWeight: 'bold' }} align="right">{t('isoManagement.table.actions')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{isos.length === 0 ? (
<TableRow>
<TableCell colSpan={5} align="center" sx={{ py: 4, color: 'text.secondary' }}>
{t('isoManagement.noImagesConfigured')}
</TableCell>
</TableRow>
) : (
isos.map((iso) => (
<TableRow key={iso.id} hover>
<TableCell>
<Chip label={iso.name} size="small" variant="outlined" sx={{ fontFamily: 'monospace', fontWeight: 'bold' }} />
</TableCell>
<TableCell sx={{ fontWeight: 'medium' }}>{iso.displayName}</TableCell>
<TableCell>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
<Tooltip title={iso.imageUrl}>
<Typography variant="caption" sx={{
fontFamily: 'monospace',
bgcolor: 'action.selected',
p: 0.5, borderRadius: 1,
display: 'flex', alignItems: 'center', width: 'fit-content', maxWidth: 300,
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'
}}>
<Box component="span" sx={{ mr: 1, fontSize: '10px' }}>💿</Box>
{iso.imageUrl || t('isoManagement.missingUrl')}
</Typography>
</Tooltip>
{iso.checksumUrl && (
<Tooltip title={iso.checksumUrl}>
<Typography variant="caption" sx={{ fontFamily: 'monospace', color: 'text.secondary', display: 'flex', alignItems: 'center' }}>
<Box component="span" sx={{ mr: 1, fontSize: '10px' }}>🛡️</Box> {t('isoManagement.checksumOk')}
</Typography>
</Tooltip>
)}
</Box>
</TableCell>
<TableCell align="center">
{iso.imageUrl ? (
<Chip label={t('isoManagement.statusActive')} color="success" size="small" variant="filled" />
) : (
<Chip label={t('isoManagement.statusIncomplete')} color="error" size="small" />
)}
</TableCell>
<TableCell align="right">
<IconButton color="error" onClick={() => handleDelete(iso.id)}>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
</Box>
);
};

// Componente dummy per CircularProgress se non importato (ma è in MUI)
const CircularProgress = ({size}) => <Box sx={{ width: size, height: size, borderRadius: '50%', border: '2px solid white', borderTopColor: 'transparent', animation: 'spin 1s linear infinite' }} />;

export default IsoManagement;
28 changes: 12 additions & 16 deletions src/components/Admin/ResourceManagement.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,20 +87,6 @@ const ResourceManagement = ({ onSwitchToResourceType }) => {
loadData();
}, [needsRefresh, withErrorHandling, t, currentSite]);

// Helper function to get status priority for sorting
const getStatusPriority = (status) => {
switch(status) {
case ResourceStatus.ACTIVE:
return 1; // First priority
case ResourceStatus.MAINTENANCE:
return 2; // Second priority
case ResourceStatus.UNAVAILABLE:
return 3; // Last priority
default:
return 4; // Unknown status lowest priority
}
};

// Filter resources based on search, type, and status
const filteredResources = resources.filter(resource => {
const matchesSearch = searchTerm === '' ||
Expand All @@ -115,9 +101,19 @@ const ResourceManagement = ({ onSwitchToResourceType }) => {

return matchesSearch && matchesType && matchesStatus;
})
// Sort resources by status priority
// Sort resources by type name, then by resource name alphabetically
.sort((a, b) => {
return getStatusPriority(a.status) - getStatusPriority(b.status);
// 1. Ordine alfabetico per Tipo (es. "Server" prima di "Switch")
const typeA = resourceTypes.find(t => t.id === a.typeId)?.name || '';
const typeB = resourceTypes.find(t => t.id === b.typeId)?.name || '';
const typeDiff = typeA.localeCompare(typeB);

if (typeDiff !== 0) return typeDiff;

// 2. Ordine alfabetico per Nome della risorsa (a parità di Tipo)
const nameA = a.name || '';
const nameB = b.name || '';
return nameA.localeCompare(nameB);
});

const handleViewModeChange = (event, newMode) => {
Expand Down
Loading