Files
minio-webui/frontend/src/components/Layout/Layout.tsx
T
Paul Nothaft f24ed78c38 feat: Add Storage Explorer with CLI terminal, alias management, and file browser
- 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)
2026-01-04 22:19:03 +01:00

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;