feat(localization): add i18next extraction helper & refactor backup configuration component to tsx
This commit is contained in:
@@ -25,15 +25,8 @@ interface AdminSidebarProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface NavItem {
|
||||
nameKey: string;
|
||||
href: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
permission?: string;
|
||||
}
|
||||
|
||||
const navigation: NavItem[] = [
|
||||
{ nameKey: 'navigation.dashboard', href: '/admin/dashboard', icon: LayoutDashboard },
|
||||
const navigation = [
|
||||
{ nameKey: 'navigation.dashboard', href: '/admin/dashboard', icon: LayoutDashboard, permission: false },
|
||||
{ nameKey: 'navigation.events', href: '/admin/events', icon: Calendar, permission: 'events.view' },
|
||||
{ nameKey: 'navigation.archives', href: '/admin/archives', icon: Archive, permission: 'archives.view' },
|
||||
{ nameKey: 'admin.analytics', href: '/admin/analytics', icon: BarChart3, permission: 'analytics.view' },
|
||||
@@ -44,7 +37,7 @@ const navigation: NavItem[] = [
|
||||
{ nameKey: 'navigation.backup', href: '/admin/backup', icon: HardDrive, permission: 'backup.view' },
|
||||
{ nameKey: 'navigation.cmsPages', href: '/admin/cms', icon: FileText, permission: 'cms.view' },
|
||||
{ nameKey: 'navigation.users', href: '/admin/users', icon: Users, permission: 'users.view' },
|
||||
];
|
||||
] as const;
|
||||
|
||||
export const AdminSidebar: React.FC<AdminSidebarProps> = ({ isOpen, onClose }) => {
|
||||
const location = useLocation();
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import type { ComponentType } from 'react';
|
||||
|
||||
export const BackupConfiguration: ComponentType<any>;
|
||||
+53
-31
@@ -5,18 +5,11 @@ import {
|
||||
Server,
|
||||
Cloud,
|
||||
HardDrive,
|
||||
Clock,
|
||||
Calendar,
|
||||
Shield,
|
||||
AlertCircle,
|
||||
Info,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Wifi,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Loader2,
|
||||
FolderOpen,
|
||||
Database,
|
||||
Image,
|
||||
FileArchive
|
||||
@@ -24,26 +17,58 @@ import {
|
||||
import { toast } from 'react-toastify';
|
||||
import { Button, Card, Input } from '../common';
|
||||
|
||||
export const BackupConfiguration = ({ config, onSave, isSaving }) => {
|
||||
interface BackupFormData {
|
||||
backup_enabled: boolean;
|
||||
backup_destination_type: 'local' | 'rsync' | 's3';
|
||||
backup_destination_path: string;
|
||||
backup_rsync_host: string;
|
||||
backup_rsync_user: string;
|
||||
backup_rsync_path: string;
|
||||
backup_rsync_ssh_key: string;
|
||||
backup_s3_endpoint: string;
|
||||
backup_s3_bucket: string;
|
||||
backup_s3_access_key: string;
|
||||
backup_s3_secret_key: string;
|
||||
backup_s3_region: string;
|
||||
backup_schedule: string;
|
||||
backup_schedule_cron: string;
|
||||
backup_retention_days: number;
|
||||
backup_include_database: boolean;
|
||||
backup_include_photos: boolean;
|
||||
backup_include_archives: boolean;
|
||||
backup_include_thumbnails: boolean;
|
||||
backup_include_temp: boolean;
|
||||
backup_compression: boolean;
|
||||
backup_encryption: boolean;
|
||||
backup_encryption_passphrase: string;
|
||||
}
|
||||
|
||||
interface BackupConfigurationProps {
|
||||
config?: Partial<BackupFormData>;
|
||||
onSave: (data: BackupFormData) => void;
|
||||
isSaving: boolean;
|
||||
}
|
||||
|
||||
export const BackupConfiguration: React.FC<BackupConfigurationProps> = ({ config, onSave, isSaving }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
const destinationTypes = [
|
||||
{
|
||||
id: 'local',
|
||||
id: 'local' as const,
|
||||
name: t('backup.configuration.destinationTypes.local.name'),
|
||||
icon: HardDrive,
|
||||
description: t('backup.configuration.destinationTypes.local.description'),
|
||||
fields: ['backup_destination_path']
|
||||
},
|
||||
{
|
||||
id: 'rsync',
|
||||
id: 'rsync' as const,
|
||||
name: t('backup.configuration.destinationTypes.rsync.name'),
|
||||
icon: Server,
|
||||
description: t('backup.configuration.destinationTypes.rsync.description'),
|
||||
fields: ['backup_rsync_host', 'backup_rsync_user', 'backup_rsync_path', 'backup_rsync_ssh_key']
|
||||
},
|
||||
{
|
||||
id: 's3',
|
||||
id: 's3' as const,
|
||||
name: t('backup.configuration.destinationTypes.s3.name'),
|
||||
icon: Cloud,
|
||||
description: t('backup.configuration.destinationTypes.s3.description'),
|
||||
@@ -57,8 +82,8 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
|
||||
{ value: 'weekly', label: t('backup.configuration.schedule.options.weekly') },
|
||||
{ value: 'custom', label: t('backup.configuration.schedule.options.custom') }
|
||||
];
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
|
||||
const [formData, setFormData] = useState<BackupFormData>({
|
||||
backup_enabled: false,
|
||||
backup_destination_type: 'local',
|
||||
backup_destination_path: '',
|
||||
@@ -101,33 +126,32 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
const handleChange = (field, value) => {
|
||||
const handleChange = <K extends keyof BackupFormData>(field: K, value: BackupFormData[K]) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Validate required fields
|
||||
const destinationType = destinationTypes.find(t => t.id === formData.backup_destination_type);
|
||||
const missingFields = [];
|
||||
|
||||
|
||||
const destinationType = destinationTypes.find(dt => dt.id === formData.backup_destination_type);
|
||||
const missingFields: string[] = [];
|
||||
|
||||
if (formData.backup_enabled && destinationType) {
|
||||
destinationType.fields.forEach(field => {
|
||||
if (!formData[field] && !field.includes('optional')) {
|
||||
if (!formData[field as keyof BackupFormData] && !field.includes('optional')) {
|
||||
missingFields.push(field);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
toast.error(t('backup.configuration.messages.requiredFields'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
onSave(formData);
|
||||
};
|
||||
|
||||
@@ -138,14 +162,12 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
toast.success(t('backup.configuration.messages.connectionSuccess'));
|
||||
} catch (error) {
|
||||
toast.error(t('backup.configuration.messages.connectionFailed') + ': ' + error.message);
|
||||
toast.error(t('backup.configuration.messages.connectionFailed') + ': ' + (error as Error).message);
|
||||
} finally {
|
||||
setTestingConnection(false);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedDestination = destinationTypes.find(t => t.id === formData.backup_destination_type);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Enable/Disable Toggle */}
|
||||
@@ -399,7 +421,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
|
||||
{/* Schedule Configuration */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('backup.configuration.schedule.title')}</h3>
|
||||
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
||||
@@ -456,7 +478,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
|
||||
{/* Backup Content Selection */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('backup.configuration.whatToBackup.title')}</h3>
|
||||
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
@@ -527,7 +549,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
|
||||
{/* Advanced Options */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('backup.configuration.advancedOptions.title')}</h3>
|
||||
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
@@ -608,4 +630,4 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
import type { ComponentType } from 'react';
|
||||
|
||||
export const BackupDashboard: ComponentType<any>;
|
||||
+76
-26
@@ -8,11 +8,9 @@ import {
|
||||
Clock,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
TrendingUp,
|
||||
Shield,
|
||||
Server,
|
||||
Cloud,
|
||||
Calendar,
|
||||
Play,
|
||||
Loader2,
|
||||
AlertTriangle,
|
||||
@@ -21,7 +19,59 @@ import {
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
import { Card, Button } from '../common';
|
||||
|
||||
const StatCard = ({ icon: Icon, label, value, color = 'blue', subtext }) => (
|
||||
export type HealthStatus = 'excellent' | 'good' | 'warning' | 'critical';
|
||||
export type BackupDestinationType = 's3' | 'rsync' | 'local';
|
||||
|
||||
interface BackupStatistics {
|
||||
total_size?: number;
|
||||
files_processed?: number;
|
||||
database_backed_up?: boolean;
|
||||
photos_backed_up?: number;
|
||||
total_photos?: number;
|
||||
archives_backed_up?: number;
|
||||
photo_count?: number;
|
||||
}
|
||||
|
||||
interface BackupRecord {
|
||||
id: number;
|
||||
status: 'completed' | 'failed' | 'running';
|
||||
backup_type: string;
|
||||
created_at: string;
|
||||
duration_seconds: number;
|
||||
statistics?: BackupStatistics;
|
||||
}
|
||||
|
||||
interface BackupStatus {
|
||||
lastBackup?: BackupRecord;
|
||||
totalBackups?: number;
|
||||
recentBackups?: BackupRecord[];
|
||||
}
|
||||
|
||||
interface BackupConfig {
|
||||
backup_destination_type?: BackupDestinationType;
|
||||
backup_enabled?: boolean;
|
||||
backup_s3_bucket?: string;
|
||||
backup_destination_path?: string;
|
||||
backup_rsync_host?: string;
|
||||
backup_retention_days?: number;
|
||||
}
|
||||
|
||||
interface StatCardProps {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
label: string;
|
||||
value: string | number;
|
||||
color?: string;
|
||||
subtext?: string;
|
||||
}
|
||||
|
||||
interface BackupDashboardProps {
|
||||
status?: BackupStatus;
|
||||
config?: BackupConfig;
|
||||
onRunBackup: () => void;
|
||||
isBackupRunning: boolean;
|
||||
}
|
||||
|
||||
const StatCard: React.FC<StatCardProps> = ({ icon: Icon, label, value, color = 'blue', subtext }) => (
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
@@ -38,7 +88,7 @@ const StatCard = ({ icon: Icon, label, value, color = 'blue', subtext }) => (
|
||||
</Card>
|
||||
);
|
||||
|
||||
const formatBytes = (bytes) => {
|
||||
const formatBytes = (bytes: number): string => {
|
||||
if (!bytes) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
@@ -46,28 +96,34 @@ const formatBytes = (bytes) => {
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }) => {
|
||||
const healthColors: Record<HealthStatus, string> = {
|
||||
excellent: 'green',
|
||||
good: 'blue',
|
||||
warning: 'amber',
|
||||
critical: 'red',
|
||||
};
|
||||
|
||||
export const BackupDashboard: React.FC<BackupDashboardProps> = ({ status, config, onRunBackup, isBackupRunning }) => {
|
||||
const { t } = useTranslation();
|
||||
const lastBackup = status?.lastBackup;
|
||||
const statistics = lastBackup?.statistics || {};
|
||||
const statistics = lastBackup?.statistics ?? {};
|
||||
const isConfigured = config && config.backup_destination_type;
|
||||
const isEnabled = config?.backup_enabled;
|
||||
|
||||
// Calculate backup health score
|
||||
const getHealthScore = () => {
|
||||
const getHealthScore = (): { score: number; status: HealthStatus; message: string } => {
|
||||
if (!lastBackup) return { score: 0, status: 'critical', message: t('backup.dashboard.healthMessages.noBackups') };
|
||||
|
||||
const hoursSinceBackup = (Date.now() - new Date(lastBackup.created_at)) / (1000 * 60 * 60);
|
||||
|
||||
|
||||
const hoursSinceBackup = (Date.now() - new Date(lastBackup.created_at).getTime()) / (1000 * 60 * 60);
|
||||
|
||||
if (lastBackup.status === 'failed') {
|
||||
return { score: 0, status: 'critical', message: t('backup.dashboard.healthMessages.lastBackupFailed') };
|
||||
}
|
||||
|
||||
|
||||
if (hoursSinceBackup < 24) {
|
||||
return { score: 100, status: 'excellent', message: t('backup.dashboard.healthMessages.upToDate') };
|
||||
} else if (hoursSinceBackup < 48) {
|
||||
return { score: 75, status: 'good', message: t('backup.dashboard.healthMessages.recent') };
|
||||
} else if (hoursSinceBackup < 168) { // 1 week
|
||||
} else if (hoursSinceBackup < 168) {
|
||||
return { score: 50, status: 'warning', message: t('backup.dashboard.healthMessages.gettingOld') };
|
||||
} else {
|
||||
return { score: 25, status: 'critical', message: t('backup.dashboard.healthMessages.outdated') };
|
||||
@@ -75,12 +131,6 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
|
||||
};
|
||||
|
||||
const health = getHealthScore();
|
||||
const healthColors = {
|
||||
excellent: 'green',
|
||||
good: 'blue',
|
||||
warning: 'amber',
|
||||
critical: 'red'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -109,7 +159,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
|
||||
{t(`backup.dashboard.healthStatus.${health.status}`)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="relative w-24 h-24">
|
||||
<svg className="w-24 h-24 transform -rotate-90">
|
||||
@@ -137,7 +187,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
|
||||
<span className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{health.score}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex-1">
|
||||
<p className="text-neutral-700 dark:text-neutral-300 font-medium">{health.message}</p>
|
||||
{lastBackup && (
|
||||
@@ -145,7 +195,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
|
||||
Last successful backup: {formatDistanceToNow(new Date(lastBackup.created_at), { addSuffix: true })}
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
<Button
|
||||
onClick={onRunBackup}
|
||||
disabled={!isConfigured || !isEnabled || isBackupRunning}
|
||||
@@ -177,7 +227,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
|
||||
color="blue"
|
||||
subtext={lastBackup ? `${t('backup.dashboard.stats.last')}: ${format(new Date(lastBackup.created_at), 'PP')}` : t('backup.dashboard.stats.noBackupsYet')}
|
||||
/>
|
||||
|
||||
|
||||
<StatCard
|
||||
icon={HardDrive}
|
||||
label={t('backup.dashboard.stats.backupSize')}
|
||||
@@ -185,7 +235,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
|
||||
color="green"
|
||||
subtext={`${statistics.files_processed || 0} ${t('backup.dashboard.stats.files')}`}
|
||||
/>
|
||||
|
||||
|
||||
<StatCard
|
||||
icon={Clock}
|
||||
label={t('backup.dashboard.stats.lastDuration')}
|
||||
@@ -193,7 +243,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
|
||||
color="purple"
|
||||
subtext={lastBackup ? format(new Date(lastBackup.created_at), 'p') : ''}
|
||||
/>
|
||||
|
||||
|
||||
<StatCard
|
||||
icon={Shield}
|
||||
label={t('backup.dashboard.stats.backupStatus')}
|
||||
@@ -324,4 +374,4 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -19,7 +19,7 @@ const RATING_OPTIONS = [
|
||||
{ value: 3, label: 'filter.threeStarsPlus' },
|
||||
{ value: 4, label: 'filter.fourStarsPlus' },
|
||||
{ value: 5, label: 'filter.fiveStarsOnly' },
|
||||
];
|
||||
] as const;
|
||||
|
||||
export const PhotoFilterPanel: React.FC<PhotoFilterPanelProps> = ({
|
||||
filters,
|
||||
|
||||
Reference in New Issue
Block a user