- Fix FiTestTube import error by replacing with FiZap (which exists in react-icons) - Integrate S3 Configuration as a tab in Music Storage page - Remove standalone S3 Configuration route and header button - Clean up unused imports and routes - Improve organization by grouping S3 functionality with Music Storage The S3 Configuration is now accessible as a tab within the Music Storage page, and the import error has been resolved.
481 lines
16 KiB
TypeScript
481 lines
16 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
VStack,
|
|
HStack,
|
|
Text,
|
|
Heading,
|
|
FormControl,
|
|
FormLabel,
|
|
Input,
|
|
Button,
|
|
useToast,
|
|
Alert,
|
|
AlertIcon,
|
|
AlertTitle,
|
|
AlertDescription,
|
|
Card,
|
|
CardBody,
|
|
CardHeader,
|
|
Spinner,
|
|
Divider,
|
|
Badge,
|
|
Icon,
|
|
Switch,
|
|
FormHelperText,
|
|
} from '@chakra-ui/react';
|
|
import { FiCheck, FiX, FiSettings, FiZap, FiSave } from 'react-icons/fi';
|
|
|
|
interface S3Config {
|
|
endpoint: string;
|
|
region: string;
|
|
accessKeyId: string;
|
|
secretAccessKey: string;
|
|
bucketName: string;
|
|
useSSL: boolean;
|
|
}
|
|
|
|
interface TestResult {
|
|
success: boolean;
|
|
message: string;
|
|
details?: any;
|
|
}
|
|
|
|
export const S3Configuration: React.FC = () => {
|
|
const [config, setConfig] = useState<S3Config>({
|
|
endpoint: '',
|
|
region: 'us-east-1',
|
|
accessKeyId: '',
|
|
secretAccessKey: '',
|
|
bucketName: '',
|
|
useSSL: true,
|
|
});
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [isTesting, setIsTesting] = useState(false);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [testResult, setTestResult] = useState<TestResult | null>(null);
|
|
const [currentConfig, setCurrentConfig] = useState<S3Config | null>(null);
|
|
const toast = useToast();
|
|
|
|
// Load current configuration on component mount
|
|
useEffect(() => {
|
|
loadCurrentConfig();
|
|
}, []);
|
|
|
|
const loadCurrentConfig = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
const response = await fetch('/api/config/s3');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setCurrentConfig(data.config);
|
|
setConfig(data.config);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading S3 config:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleInputChange = (field: keyof S3Config, value: string | boolean) => {
|
|
setConfig(prev => ({
|
|
...prev,
|
|
[field]: value,
|
|
}));
|
|
};
|
|
|
|
const testConnection = async () => {
|
|
setIsTesting(true);
|
|
setTestResult(null);
|
|
|
|
try {
|
|
const response = await fetch('/api/config/s3/test', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(config),
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (response.ok) {
|
|
setTestResult({
|
|
success: true,
|
|
message: 'Connection successful!',
|
|
details: result,
|
|
});
|
|
toast({
|
|
title: 'Connection Test Successful',
|
|
description: 'S3 bucket connection is working properly',
|
|
status: 'success',
|
|
duration: 5000,
|
|
isClosable: true,
|
|
});
|
|
} else {
|
|
setTestResult({
|
|
success: false,
|
|
message: result.error || 'Connection failed',
|
|
details: result,
|
|
});
|
|
toast({
|
|
title: 'Connection Test Failed',
|
|
description: result.error || 'Failed to connect to S3 bucket',
|
|
status: 'error',
|
|
duration: 5000,
|
|
isClosable: true,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error testing S3 connection:', error);
|
|
setTestResult({
|
|
success: false,
|
|
message: 'Network error or server unavailable',
|
|
});
|
|
toast({
|
|
title: 'Connection Test Failed',
|
|
description: 'Network error or server unavailable',
|
|
status: 'error',
|
|
duration: 5000,
|
|
isClosable: true,
|
|
});
|
|
} finally {
|
|
setIsTesting(false);
|
|
}
|
|
};
|
|
|
|
const saveConfiguration = async () => {
|
|
setIsSaving(true);
|
|
|
|
try {
|
|
const response = await fetch('/api/config/s3', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(config),
|
|
});
|
|
|
|
if (response.ok) {
|
|
setCurrentConfig(config);
|
|
toast({
|
|
title: 'Configuration Saved',
|
|
description: 'S3 configuration has been saved successfully',
|
|
status: 'success',
|
|
duration: 3000,
|
|
isClosable: true,
|
|
});
|
|
} else {
|
|
const error = await response.json();
|
|
throw new Error(error.error || 'Failed to save configuration');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving S3 config:', error);
|
|
toast({
|
|
title: 'Save Failed',
|
|
description: error instanceof Error ? error.message : 'Failed to save configuration',
|
|
status: 'error',
|
|
duration: 5000,
|
|
isClosable: true,
|
|
});
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
const hasChanges = () => {
|
|
if (!currentConfig) return true;
|
|
return JSON.stringify(config) !== JSON.stringify(currentConfig);
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Box p={8}>
|
|
<VStack spacing={4} align="center">
|
|
<Spinner size="xl" />
|
|
<Text>Loading S3 configuration...</Text>
|
|
</VStack>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box p={8} maxW="800px" mx="auto">
|
|
<VStack spacing={8} align="stretch">
|
|
{/* Header */}
|
|
<VStack spacing={2} align="start">
|
|
<HStack spacing={3}>
|
|
<Icon as={FiSettings} w={6} h={6} color="blue.400" />
|
|
<Heading size="lg" color="white">S3 Configuration</Heading>
|
|
</HStack>
|
|
<Text color="gray.400">
|
|
Configure your S3-compatible storage connection for music file storage and playback.
|
|
</Text>
|
|
</VStack>
|
|
|
|
{/* Configuration Form */}
|
|
<Card bg="gray.800" borderColor="gray.700">
|
|
<CardHeader>
|
|
<Heading size="md" color="white">Connection Settings</Heading>
|
|
</CardHeader>
|
|
<CardBody>
|
|
<VStack spacing={6} align="stretch">
|
|
{/* Endpoint */}
|
|
<FormControl>
|
|
<FormLabel color="white">S3 Endpoint</FormLabel>
|
|
<Input
|
|
value={config.endpoint}
|
|
onChange={(e) => handleInputChange('endpoint', e.target.value)}
|
|
placeholder="https://s3.amazonaws.com or http://localhost:9000 for MinIO"
|
|
bg="gray.700"
|
|
borderColor="gray.600"
|
|
color="white"
|
|
_placeholder={{ color: 'gray.400' }}
|
|
_focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
|
|
/>
|
|
<FormHelperText color="gray.400">
|
|
For AWS S3, use: https://s3.amazonaws.com. For MinIO: http://localhost:9000
|
|
</FormHelperText>
|
|
</FormControl>
|
|
|
|
{/* Region */}
|
|
<FormControl>
|
|
<FormLabel color="white">Region</FormLabel>
|
|
<Input
|
|
value={config.region}
|
|
onChange={(e) => handleInputChange('region', e.target.value)}
|
|
placeholder="us-east-1"
|
|
bg="gray.700"
|
|
borderColor="gray.600"
|
|
color="white"
|
|
_placeholder={{ color: 'gray.400' }}
|
|
_focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
|
|
/>
|
|
<FormHelperText color="gray.400">
|
|
AWS region (e.g., us-east-1, eu-west-1) or 'us-east-1' for MinIO
|
|
</FormHelperText>
|
|
</FormControl>
|
|
|
|
{/* Access Key */}
|
|
<FormControl>
|
|
<FormLabel color="white">Access Key ID</FormLabel>
|
|
<Input
|
|
value={config.accessKeyId}
|
|
onChange={(e) => handleInputChange('accessKeyId', e.target.value)}
|
|
placeholder="Your S3 access key"
|
|
bg="gray.700"
|
|
borderColor="gray.600"
|
|
color="white"
|
|
_placeholder={{ color: 'gray.400' }}
|
|
_focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
|
|
/>
|
|
</FormControl>
|
|
|
|
{/* Secret Key */}
|
|
<FormControl>
|
|
<FormLabel color="white">Secret Access Key</FormLabel>
|
|
<Input
|
|
type="password"
|
|
value={config.secretAccessKey}
|
|
onChange={(e) => handleInputChange('secretAccessKey', e.target.value)}
|
|
placeholder="Your S3 secret key"
|
|
bg="gray.700"
|
|
borderColor="gray.600"
|
|
color="white"
|
|
_placeholder={{ color: 'gray.400' }}
|
|
_focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
|
|
/>
|
|
</FormControl>
|
|
|
|
{/* Bucket Name */}
|
|
<FormControl>
|
|
<FormLabel color="white">Bucket Name</FormLabel>
|
|
<Input
|
|
value={config.bucketName}
|
|
onChange={(e) => handleInputChange('bucketName', e.target.value)}
|
|
placeholder="music-files"
|
|
bg="gray.700"
|
|
borderColor="gray.600"
|
|
color="white"
|
|
_placeholder={{ color: 'gray.400' }}
|
|
_focus={{ borderColor: 'blue.400', boxShadow: 'none' }}
|
|
/>
|
|
<FormHelperText color="gray.400">
|
|
The S3 bucket where music files will be stored
|
|
</FormHelperText>
|
|
</FormControl>
|
|
|
|
{/* Use SSL */}
|
|
<FormControl display="flex" alignItems="center">
|
|
<FormLabel htmlFor="use-ssl" mb="0" color="white">
|
|
Use SSL/TLS
|
|
</FormLabel>
|
|
<Switch
|
|
id="use-ssl"
|
|
isChecked={config.useSSL}
|
|
onChange={(e) => handleInputChange('useSSL', e.target.checked)}
|
|
colorScheme="blue"
|
|
/>
|
|
<FormHelperText color="gray.400" ml={3}>
|
|
Enable for HTTPS connections (recommended for production)
|
|
</FormHelperText>
|
|
</FormControl>
|
|
</VStack>
|
|
</CardBody>
|
|
</Card>
|
|
|
|
{/* Test Connection */}
|
|
<Card bg="gray.800" borderColor="gray.700">
|
|
<CardHeader>
|
|
<HStack spacing={3}>
|
|
<Icon as={FiZap} w={5} h={5} color="blue.400" />
|
|
<Heading size="md" color="white">Test Connection</Heading>
|
|
</HStack>
|
|
</CardHeader>
|
|
<CardBody>
|
|
<VStack spacing={4} align="stretch">
|
|
<Text color="gray.400">
|
|
Test your S3 configuration to ensure it's working properly before saving.
|
|
</Text>
|
|
|
|
<Button
|
|
leftIcon={isTesting ? <Spinner size="sm" /> : <FiZap />}
|
|
onClick={testConnection}
|
|
isLoading={isTesting}
|
|
loadingText="Testing..."
|
|
colorScheme="blue"
|
|
_hover={{ bg: "blue.700" }}
|
|
>
|
|
Test Connection
|
|
</Button>
|
|
|
|
{testResult && (
|
|
<Alert
|
|
status={testResult.success ? 'success' : 'error'}
|
|
variant="subtle"
|
|
flexDirection="column"
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
textAlign="center"
|
|
height="auto"
|
|
py={4}
|
|
>
|
|
<AlertIcon boxSize="24px" />
|
|
<AlertTitle mt={2} mb={1} fontSize="lg">
|
|
{testResult.success ? 'Connection Successful' : 'Connection Failed'}
|
|
</AlertTitle>
|
|
<AlertDescription maxWidth="sm">
|
|
{testResult.message}
|
|
</AlertDescription>
|
|
{testResult.details && (
|
|
<Box mt={2} p={3} bg="gray.700" borderRadius="md" fontSize="sm">
|
|
<Text color="gray.300" fontWeight="bold">Details:</Text>
|
|
<Text color="gray.400" fontFamily="mono">
|
|
{JSON.stringify(testResult.details, null, 2)}
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
</Alert>
|
|
)}
|
|
</VStack>
|
|
</CardBody>
|
|
</Card>
|
|
|
|
{/* Save Configuration */}
|
|
<Card bg="gray.800" borderColor="gray.700">
|
|
<CardHeader>
|
|
<HStack spacing={3}>
|
|
<Icon as={FiSave} w={5} h={5} color="green.400" />
|
|
<Heading size="md" color="white">Save Configuration</Heading>
|
|
</HStack>
|
|
</CardHeader>
|
|
<CardBody>
|
|
<VStack spacing={4} align="stretch">
|
|
<Text color="gray.400">
|
|
Save your S3 configuration to use it for music file storage and playback.
|
|
</Text>
|
|
|
|
<HStack spacing={3}>
|
|
<Button
|
|
leftIcon={isSaving ? <Spinner size="sm" /> : <FiSave />}
|
|
onClick={saveConfiguration}
|
|
isLoading={isSaving}
|
|
loadingText="Saving..."
|
|
colorScheme="green"
|
|
isDisabled={!hasChanges()}
|
|
_hover={{ bg: "green.700" }}
|
|
>
|
|
Save Configuration
|
|
</Button>
|
|
|
|
{currentConfig && (
|
|
<Badge colorScheme="blue" variant="subtle" bg="blue.900" color="blue.200">
|
|
Configuration Loaded
|
|
</Badge>
|
|
)}
|
|
</HStack>
|
|
|
|
{!hasChanges() && currentConfig && (
|
|
<Alert status="info" variant="subtle">
|
|
<AlertIcon />
|
|
<Text color="gray.300">No changes to save</Text>
|
|
</Alert>
|
|
)}
|
|
</VStack>
|
|
</CardBody>
|
|
</Card>
|
|
|
|
{/* Help Section */}
|
|
<Card bg="gray.800" borderColor="gray.700">
|
|
<CardHeader>
|
|
<Heading size="md" color="white">Configuration Help</Heading>
|
|
</CardHeader>
|
|
<CardBody>
|
|
<VStack spacing={4} align="stretch">
|
|
<Text color="gray.400">
|
|
<strong>For AWS S3:</strong>
|
|
</Text>
|
|
<Box pl={4} borderLeft="2px" borderColor="gray.600">
|
|
<Text color="gray.400" fontSize="sm">
|
|
• Endpoint: https://s3.amazonaws.com<br/>
|
|
• Region: Your AWS region (e.g., us-east-1)<br/>
|
|
• Access Key: Your AWS access key<br/>
|
|
• Secret Key: Your AWS secret key<br/>
|
|
• Bucket: Your S3 bucket name
|
|
</Text>
|
|
</Box>
|
|
|
|
<Text color="gray.400" mt={4}>
|
|
<strong>For MinIO (Local Development):</strong>
|
|
</Text>
|
|
<Box pl={4} borderLeft="2px" borderColor="gray.600">
|
|
<Text color="gray.400" fontSize="sm">
|
|
• Endpoint: http://localhost:9000<br/>
|
|
• Region: us-east-1<br/>
|
|
• Access Key: minioadmin<br/>
|
|
• Secret Key: minioadmin<br/>
|
|
• Bucket: Create a bucket named 'music-files'
|
|
</Text>
|
|
</Box>
|
|
|
|
<Text color="gray.400" mt={4}>
|
|
<strong>For Other S3-Compatible Services:</strong>
|
|
</Text>
|
|
<Box pl={4} borderLeft="2px" borderColor="gray.600">
|
|
<Text color="gray.400" fontSize="sm">
|
|
• Endpoint: Your service's endpoint URL<br/>
|
|
• Region: Your service's region<br/>
|
|
• Access Key: Your service access key<br/>
|
|
• Secret Key: Your service secret key<br/>
|
|
• Bucket: Your bucket name
|
|
</Text>
|
|
</Box>
|
|
</VStack>
|
|
</CardBody>
|
|
</Card>
|
|
</VStack>
|
|
</Box>
|
|
);
|
|
};
|