fix: Resolve API connection issues and add Quick Start features
- Fix API URL configuration to use relative paths (fixes localhost:8080 error) - Add Quick Start Wizard to dashboard for guided bucket/user setup - Create bash scripts for automated bucket and user creation - Add quickstart.sh for interactive setup experience - Update documentation with correct ports and new features - Improve user onboarding with step-by-step wizard This addresses: - Connection refused errors when creating users/policies - WebSocket connection issues - Need for easy one-step setup process - Port configuration clarity in documentation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -8,14 +8,17 @@ import {
|
||||
LinearProgress,
|
||||
Alert,
|
||||
Paper,
|
||||
Button,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Storage as StorageIcon,
|
||||
People as PeopleIcon,
|
||||
Folder as FolderIcon,
|
||||
Speed as SpeedIcon,
|
||||
Rocket as RocketIcon,
|
||||
} from '@mui/icons-material';
|
||||
import api, { handleApiError } from '../../services/api';
|
||||
import QuickStartWizard from './QuickStartWizard';
|
||||
|
||||
interface DashboardStats {
|
||||
totalBuckets: number;
|
||||
@@ -65,6 +68,7 @@ const Dashboard: React.FC = () => {
|
||||
const [stats, setStats] = useState<DashboardStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [wizardOpen, setWizardOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboardData();
|
||||
@@ -112,12 +116,25 @@ const Dashboard: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Dashboard
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" paragraph>
|
||||
Welcome to MinIO WebUI. Monitor your storage infrastructure at a glance.
|
||||
</Typography>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Dashboard
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary" paragraph>
|
||||
Welcome to MinIO WebUI. Monitor your storage infrastructure at a glance.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<RocketIcon />}
|
||||
onClick={() => setWizardOpen(true)}
|
||||
size="large"
|
||||
>
|
||||
Quick Start
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
@@ -189,6 +206,15 @@ const Dashboard: React.FC = () => {
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<QuickStartWizard
|
||||
open={wizardOpen}
|
||||
onClose={() => setWizardOpen(false)}
|
||||
onComplete={() => {
|
||||
setWizardOpen(false);
|
||||
loadDashboardData();
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,490 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Stepper,
|
||||
Step,
|
||||
StepLabel,
|
||||
TextField,
|
||||
Box,
|
||||
Typography,
|
||||
Alert,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Paper,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
Chip,
|
||||
LinearProgress,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Storage,
|
||||
Person,
|
||||
Security,
|
||||
Check,
|
||||
ContentCopy,
|
||||
Visibility,
|
||||
VisibilityOff,
|
||||
Folder,
|
||||
VpnKey,
|
||||
Assignment,
|
||||
} from '@mui/icons-material';
|
||||
import userService from '../../services/userService';
|
||||
import policyService from '../../services/policyService';
|
||||
import api from '../../services/api';
|
||||
|
||||
interface QuickStartWizardProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
interface SetupData {
|
||||
bucketName: string;
|
||||
userName: string;
|
||||
userPassword: string;
|
||||
policyType: 'readonly' | 'readwrite' | 'writeonly';
|
||||
}
|
||||
|
||||
const QuickStartWizard: React.FC<QuickStartWizardProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onComplete,
|
||||
}) => {
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [setupData, setSetupData] = useState<SetupData>({
|
||||
bucketName: '',
|
||||
userName: '',
|
||||
userPassword: '',
|
||||
policyType: 'readwrite',
|
||||
});
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [completedSetup, setCompletedSetup] = useState<any>(null);
|
||||
|
||||
const steps = ['Create Bucket', 'Create User', 'Set Permissions', 'Review & Complete'];
|
||||
|
||||
const generatePassword = () => {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
|
||||
let password = '';
|
||||
for (let i = 0; i < 16; i++) {
|
||||
password += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
setSetupData({ ...setupData, userPassword: password });
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
setError('');
|
||||
if (activeStep === 0 && !setupData.bucketName) {
|
||||
setError('Please enter a bucket name');
|
||||
return;
|
||||
}
|
||||
if (activeStep === 1) {
|
||||
if (!setupData.userName) {
|
||||
setError('Please enter a username');
|
||||
return;
|
||||
}
|
||||
if (!setupData.userPassword) {
|
||||
setError('Please enter or generate a password');
|
||||
return;
|
||||
}
|
||||
const validation = userService.validatePassword(setupData.userPassword);
|
||||
if (!validation.valid) {
|
||||
setError(validation.errors[0]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (activeStep === steps.length - 1) {
|
||||
handleComplete();
|
||||
} else {
|
||||
setActiveStep((prevStep) => prevStep + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setActiveStep((prevStep) => prevStep - 1);
|
||||
setError('');
|
||||
};
|
||||
|
||||
const handleComplete = async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
// Step 1: Create bucket
|
||||
await api.post('/buckets', { name: setupData.bucketName });
|
||||
|
||||
// Step 2: Create user
|
||||
await userService.createUser({
|
||||
accessKey: setupData.userName,
|
||||
secretKey: setupData.userPassword,
|
||||
});
|
||||
|
||||
// Step 3: Create policy
|
||||
const policyName = `${setupData.bucketName}-${setupData.policyType}-policy`;
|
||||
const policyJson = policyService.generatePolicyFromTemplate(
|
||||
`bucket${setupData.policyType.charAt(0).toUpperCase() + setupData.policyType.slice(1).replace('only', 'Only')}`,
|
||||
setupData.bucketName
|
||||
);
|
||||
|
||||
await policyService.createPolicy({
|
||||
name: policyName,
|
||||
policy: policyJson,
|
||||
});
|
||||
|
||||
// Step 4: Attach policy to user
|
||||
await policyService.attachPolicy({
|
||||
policyName: policyName,
|
||||
userName: setupData.userName,
|
||||
});
|
||||
|
||||
setCompletedSetup({
|
||||
bucket: setupData.bucketName,
|
||||
user: setupData.userName,
|
||||
password: setupData.userPassword,
|
||||
policy: policyName,
|
||||
});
|
||||
|
||||
setActiveStep(steps.length);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to complete setup');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyCredentials = () => {
|
||||
const credentials = `MinIO Credentials\n\nBucket: ${completedSetup.bucket}\nUsername: ${completedSetup.user}\nPassword: ${completedSetup.password}\nPolicy: ${completedSetup.policy}`;
|
||||
navigator.clipboard.writeText(credentials);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!loading) {
|
||||
onClose();
|
||||
// Reset wizard state
|
||||
setActiveStep(0);
|
||||
setSetupData({
|
||||
bucketName: '',
|
||||
userName: '',
|
||||
userPassword: '',
|
||||
policyType: 'readwrite',
|
||||
});
|
||||
setError('');
|
||||
setCompletedSetup(null);
|
||||
}
|
||||
};
|
||||
|
||||
const getStepContent = (step: number) => {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
Let's start by creating a bucket to store your objects.
|
||||
</Typography>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="normal"
|
||||
label="Bucket Name"
|
||||
fullWidth
|
||||
value={setupData.bucketName}
|
||||
onChange={(e) => setSetupData({ ...setupData, bucketName: e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '') })}
|
||||
helperText="Use lowercase letters, numbers, and hyphens only"
|
||||
error={!!error && activeStep === 0}
|
||||
/>
|
||||
<Alert severity="info" sx={{ mt: 2 }}>
|
||||
A bucket is a container for storing your files and objects in MinIO.
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
|
||||
case 1:
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
Create a user who will have access to this bucket.
|
||||
</Typography>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="normal"
|
||||
label="Username"
|
||||
fullWidth
|
||||
value={setupData.userName}
|
||||
onChange={(e) => setSetupData({ ...setupData, userName: e.target.value })}
|
||||
error={!!error && activeStep === 1}
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
label="Password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
fullWidth
|
||||
value={setupData.userPassword}
|
||||
onChange={(e) => setSetupData({ ...setupData, userPassword: e.target.value })}
|
||||
error={!!error && activeStep === 1}
|
||||
helperText={error || "Min 8 chars, uppercase, lowercase, and number required"}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={() => setShowPassword(!showPassword)} edge="end">
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={generatePassword}
|
||||
sx={{ mt: 1 }}
|
||||
fullWidth
|
||||
>
|
||||
Generate Secure Password
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
case 2:
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
Choose what permissions this user should have for the bucket.
|
||||
</Typography>
|
||||
<FormControl fullWidth margin="normal">
|
||||
<InputLabel>Access Level</InputLabel>
|
||||
<Select
|
||||
value={setupData.policyType}
|
||||
onChange={(e) => setSetupData({ ...setupData, policyType: e.target.value as any })}
|
||||
label="Access Level"
|
||||
>
|
||||
<MenuItem value="readonly">
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Security color="info" fontSize="small" />
|
||||
Read Only - Can view and download files
|
||||
</Box>
|
||||
</MenuItem>
|
||||
<MenuItem value="writeonly">
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Security color="warning" fontSize="small" />
|
||||
Write Only - Can upload and delete files
|
||||
</Box>
|
||||
</MenuItem>
|
||||
<MenuItem value="readwrite">
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Security color="success" fontSize="small" />
|
||||
Read & Write - Full access to the bucket
|
||||
</Box>
|
||||
</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Alert severity="info" sx={{ mt: 2 }}>
|
||||
Policies control what actions users can perform on buckets and objects.
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
|
||||
case 3:
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
Review your configuration before creating:
|
||||
</Typography>
|
||||
<Paper variant="outlined" sx={{ p: 2, mt: 2 }}>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Folder color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Bucket"
|
||||
secondary={setupData.bucketName}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Person color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="User"
|
||||
secondary={setupData.userName}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<VpnKey color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Password"
|
||||
secondary="••••••••••••••••"
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<Security color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary="Access Level"
|
||||
secondary={
|
||||
<Chip
|
||||
label={setupData.policyType}
|
||||
size="small"
|
||||
color={
|
||||
setupData.policyType === 'readwrite' ? 'success' :
|
||||
setupData.policyType === 'readonly' ? 'info' : 'warning'
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const renderCompletionScreen = () => (
|
||||
<Box>
|
||||
<Box textAlign="center" py={3}>
|
||||
<Check sx={{ fontSize: 64, color: 'success.main' }} />
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Setup Complete!
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Your bucket and user have been created successfully.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Paper variant="outlined" sx={{ p: 3, mt: 2, bgcolor: 'grey.50' }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<Assignment sx={{ verticalAlign: 'middle', mr: 1 }} />
|
||||
Your Credentials
|
||||
</Typography>
|
||||
<Box sx={{ fontFamily: 'monospace', mt: 2 }}>
|
||||
<Typography variant="body2">
|
||||
<strong>Bucket:</strong> {completedSetup.bucket}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
<strong>Username:</strong> {completedSetup.user}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
<strong>Password:</strong> {completedSetup.password}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
<strong>Policy:</strong> {completedSetup.policy}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<ContentCopy />}
|
||||
onClick={handleCopyCredentials}
|
||||
fullWidth
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
Copy Credentials
|
||||
</Button>
|
||||
</Paper>
|
||||
|
||||
<Alert severity="warning" sx={{ mt: 2 }}>
|
||||
Save these credentials securely. The password cannot be retrieved later.
|
||||
</Alert>
|
||||
|
||||
<Typography variant="h6" sx={{ mt: 3, mb: 1 }}>
|
||||
Next Steps:
|
||||
</Typography>
|
||||
<List dense>
|
||||
<ListItem>
|
||||
<ListItemText primary="1. Use MinIO CLI or SDK to connect with these credentials" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="2. Start uploading files to your bucket" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemText primary="3. Configure additional users or policies as needed" />
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Storage color="primary" />
|
||||
Quick Start Wizard
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{error && activeStep < steps.length && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{activeStep < steps.length ? (
|
||||
<>
|
||||
<Stepper activeStep={activeStep} sx={{ mb: 3 }}>
|
||||
{steps.map((label) => (
|
||||
<Step key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
{getStepContent(activeStep)}
|
||||
</>
|
||||
) : (
|
||||
renderCompletionScreen()
|
||||
)}
|
||||
|
||||
{loading && <LinearProgress sx={{ mt: 2 }} />}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{activeStep < steps.length ? (
|
||||
<>
|
||||
<Button onClick={handleClose} disabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleBack}
|
||||
disabled={activeStep === 0 || loading}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleNext}
|
||||
disabled={loading}
|
||||
>
|
||||
{activeStep === steps.length - 1 ? 'Complete Setup' : 'Next'}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
onComplete();
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuickStartWizard;
|
||||
Reference in New Issue
Block a user