f24ed78c38
- Add CLI terminal for executing mc commands from the web interface - Command history with up/down arrow navigation - Dark theme with monospace font - Auto-scroll output and loading states - Add alias management page - List all MinIO aliases with connection status - Add, edit, delete aliases - Test connection functionality - Add file browser for navigating bucket contents - Breadcrumb navigation (alias > bucket > path) - File/folder table with size, type, last modified - Upload files (drag-and-drop support) - Download, rename, delete operations - Create new folders - Multi-select for batch delete Backend: - New API routes: /api/terminal, /api/aliases, /api/browser - Multer middleware for file uploads - Extended minio.service.js with file operations Frontend: - New Explorer component with Aliases/Terminal tabs - Zustand stores for terminal and explorer state - i18n translations (English and German)
260 lines
7.7 KiB
TypeScript
260 lines
7.7 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
|
import {
|
|
Box,
|
|
Drawer,
|
|
AppBar,
|
|
Toolbar,
|
|
List,
|
|
Typography,
|
|
Divider,
|
|
IconButton,
|
|
ListItem,
|
|
ListItemButton,
|
|
ListItemIcon,
|
|
ListItemText,
|
|
Avatar,
|
|
Menu,
|
|
MenuItem,
|
|
Tooltip,
|
|
Select,
|
|
FormControl,
|
|
} from '@mui/material';
|
|
import {
|
|
Menu as MenuIcon,
|
|
ChevronLeft as ChevronLeftIcon,
|
|
Dashboard as DashboardIcon,
|
|
Storage as StorageIcon,
|
|
FolderOpen as FolderOpenIcon,
|
|
People as PeopleIcon,
|
|
Policy as PolicyIcon,
|
|
Assessment as AssessmentIcon,
|
|
Logout as LogoutIcon,
|
|
AccountCircle as AccountCircleIcon,
|
|
Language as LanguageIcon,
|
|
} from '@mui/icons-material';
|
|
import { useTranslation } from 'react-i18next';
|
|
import useAuthStore from '../../store/authStore';
|
|
|
|
const drawerWidth = 240;
|
|
|
|
interface NavItem {
|
|
text: string;
|
|
icon: React.ReactElement;
|
|
path: string;
|
|
translationKey: string;
|
|
}
|
|
|
|
const Layout: React.FC = () => {
|
|
const { t, i18n } = useTranslation(['common']);
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const { user, logout } = useAuthStore();
|
|
const [open, setOpen] = useState(true);
|
|
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
|
|
|
const navItems: NavItem[] = [
|
|
{ text: 'Dashboard', icon: <DashboardIcon />, path: '/', translationKey: 'navigation.dashboard' },
|
|
{ text: 'Buckets', icon: <StorageIcon />, path: '/buckets', translationKey: 'navigation.buckets' },
|
|
{ text: 'Explorer', icon: <FolderOpenIcon />, path: '/explorer', translationKey: 'navigation.explorer' },
|
|
{ text: 'Users', icon: <PeopleIcon />, path: '/users', translationKey: 'navigation.users' },
|
|
{ text: 'Policies', icon: <PolicyIcon />, path: '/policies', translationKey: 'navigation.policies' },
|
|
{ text: 'Reports', icon: <AssessmentIcon />, path: '/reports', translationKey: 'navigation.reports' },
|
|
];
|
|
|
|
const handleDrawerToggle = () => {
|
|
setOpen(!open);
|
|
};
|
|
|
|
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
|
setAnchorEl(event.currentTarget);
|
|
};
|
|
|
|
const handleMenuClose = () => {
|
|
setAnchorEl(null);
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
await logout();
|
|
navigate('/login');
|
|
};
|
|
|
|
const handleLanguageChange = (event: any) => {
|
|
i18n.changeLanguage(event.target.value);
|
|
};
|
|
|
|
return (
|
|
<Box sx={{ display: 'flex', width: '100%' }}>
|
|
<AppBar
|
|
position="fixed"
|
|
sx={{
|
|
width: `calc(100% - ${open ? drawerWidth : 0}px)`,
|
|
ml: `${open ? drawerWidth : 0}px`,
|
|
transition: (theme) =>
|
|
theme.transitions.create(['margin', 'width'], {
|
|
easing: theme.transitions.easing.sharp,
|
|
duration: theme.transitions.duration.leavingScreen,
|
|
}),
|
|
}}
|
|
>
|
|
<Toolbar>
|
|
<IconButton
|
|
color="inherit"
|
|
aria-label="toggle drawer"
|
|
onClick={handleDrawerToggle}
|
|
edge="start"
|
|
sx={{ mr: 2 }}
|
|
>
|
|
{open ? <ChevronLeftIcon /> : <MenuIcon />}
|
|
</IconButton>
|
|
|
|
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
|
|
{t('common:appName')}
|
|
</Typography>
|
|
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
<FormControl size="small" sx={{ minWidth: 120 }}>
|
|
<Select
|
|
value={i18n.language}
|
|
onChange={handleLanguageChange}
|
|
displayEmpty
|
|
sx={{
|
|
color: 'white',
|
|
'& .MuiOutlinedInput-notchedOutline': {
|
|
borderColor: 'rgba(255, 255, 255, 0.5)',
|
|
},
|
|
'&:hover .MuiOutlinedInput-notchedOutline': {
|
|
borderColor: 'rgba(255, 255, 255, 0.8)',
|
|
},
|
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
|
borderColor: 'white',
|
|
},
|
|
'& .MuiSvgIcon-root': {
|
|
color: 'white',
|
|
},
|
|
}}
|
|
startAdornment={<LanguageIcon sx={{ mr: 1, color: 'white' }} />}
|
|
>
|
|
<MenuItem value="de">Deutsch</MenuItem>
|
|
<MenuItem value="en">English</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
|
|
<Tooltip title="Account">
|
|
<IconButton
|
|
onClick={handleMenuOpen}
|
|
size="small"
|
|
sx={{ ml: 2 }}
|
|
aria-controls={Boolean(anchorEl) ? 'account-menu' : undefined}
|
|
aria-haspopup="true"
|
|
aria-expanded={Boolean(anchorEl) ? 'true' : undefined}
|
|
>
|
|
<Avatar sx={{ width: 32, height: 32 }}>
|
|
<AccountCircleIcon />
|
|
</Avatar>
|
|
</IconButton>
|
|
</Tooltip>
|
|
</Box>
|
|
|
|
<Menu
|
|
anchorEl={anchorEl}
|
|
id="account-menu"
|
|
open={Boolean(anchorEl)}
|
|
onClose={handleMenuClose}
|
|
onClick={handleMenuClose}
|
|
PaperProps={{
|
|
elevation: 0,
|
|
sx: {
|
|
overflow: 'visible',
|
|
filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
|
|
mt: 1.5,
|
|
'& .MuiAvatar-root': {
|
|
width: 32,
|
|
height: 32,
|
|
ml: -0.5,
|
|
mr: 1,
|
|
},
|
|
},
|
|
}}
|
|
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
|
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
|
>
|
|
<MenuItem disabled>
|
|
<Typography variant="body2">
|
|
Logged in as {user?.role || 'Admin'}
|
|
</Typography>
|
|
</MenuItem>
|
|
<Divider />
|
|
<MenuItem onClick={handleLogout}>
|
|
<ListItemIcon>
|
|
<LogoutIcon fontSize="small" />
|
|
</ListItemIcon>
|
|
{t('common:navigation.logout')}
|
|
</MenuItem>
|
|
</Menu>
|
|
</Toolbar>
|
|
</AppBar>
|
|
|
|
<Drawer
|
|
sx={{
|
|
width: drawerWidth,
|
|
flexShrink: 0,
|
|
'& .MuiDrawer-paper': {
|
|
width: drawerWidth,
|
|
boxSizing: 'border-box',
|
|
},
|
|
}}
|
|
variant="persistent"
|
|
anchor="left"
|
|
open={open}
|
|
>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
padding: (theme) => theme.spacing(0, 1),
|
|
...((theme) => theme.mixins.toolbar),
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
<Typography variant="h6" noWrap component="div">
|
|
MinIO Manager
|
|
</Typography>
|
|
</Box>
|
|
<Divider />
|
|
<List>
|
|
{navItems.map((item) => (
|
|
<ListItem key={item.text} disablePadding>
|
|
<ListItemButton
|
|
selected={location.pathname === item.path || location.pathname.startsWith(item.path + '/')}
|
|
onClick={() => navigate(item.path)}
|
|
>
|
|
<ListItemIcon>{item.icon}</ListItemIcon>
|
|
<ListItemText primary={t(`common:${item.translationKey}`)} />
|
|
</ListItemButton>
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
</Drawer>
|
|
|
|
<Box
|
|
component="main"
|
|
sx={{
|
|
flexGrow: 1,
|
|
padding: 3,
|
|
transition: (theme) =>
|
|
theme.transitions.create('margin', {
|
|
easing: theme.transitions.easing.sharp,
|
|
duration: theme.transitions.duration.leavingScreen,
|
|
}),
|
|
marginLeft: open ? 0 : `-${drawerWidth}px`,
|
|
mt: 8,
|
|
}}
|
|
>
|
|
<Outlet />
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default Layout; |