mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-07 12:06:46 +03:00
Updated Automation & Maintainence
This commit is contained in:
335
src/components/config/AutomationSettings.tsx
Normal file
335
src/components/config/AutomationSettings.tsx
Normal file
@@ -0,0 +1,335 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Clock,
|
||||
Database,
|
||||
RefreshCw,
|
||||
Calendar,
|
||||
Activity,
|
||||
Zap,
|
||||
Info
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import type { ScheduleConfig, DatabaseCleanupConfig } from "@/types/config";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AutomationSettingsProps {
|
||||
scheduleConfig: ScheduleConfig;
|
||||
cleanupConfig: DatabaseCleanupConfig;
|
||||
onScheduleChange: (config: ScheduleConfig) => void;
|
||||
onCleanupChange: (config: DatabaseCleanupConfig) => void;
|
||||
isAutoSavingSchedule?: boolean;
|
||||
isAutoSavingCleanup?: boolean;
|
||||
}
|
||||
|
||||
const scheduleIntervals = [
|
||||
{ label: "Every hour", value: 3600 },
|
||||
{ label: "Every 2 hours", value: 7200 },
|
||||
{ label: "Every 4 hours", value: 14400 },
|
||||
{ label: "Every 8 hours", value: 28800 },
|
||||
{ label: "Every 12 hours", value: 43200 },
|
||||
{ label: "Daily", value: 86400 },
|
||||
{ label: "Every 2 days", value: 172800 },
|
||||
{ label: "Weekly", value: 604800 },
|
||||
];
|
||||
|
||||
const retentionPeriods = [
|
||||
{ label: "1 day", value: 86400 },
|
||||
{ label: "3 days", value: 259200 },
|
||||
{ label: "1 week", value: 604800 },
|
||||
{ label: "2 weeks", value: 1209600 },
|
||||
{ label: "1 month", value: 2592000 },
|
||||
{ label: "2 months", value: 5184000 },
|
||||
{ label: "3 months", value: 7776000 },
|
||||
];
|
||||
|
||||
function getCleanupInterval(retentionSeconds: number): number {
|
||||
const days = retentionSeconds / 86400;
|
||||
if (days <= 1) return 21600; // 6 hours
|
||||
if (days <= 3) return 43200; // 12 hours
|
||||
if (days <= 7) return 86400; // 24 hours
|
||||
if (days <= 30) return 172800; // 48 hours
|
||||
return 604800; // 1 week
|
||||
}
|
||||
|
||||
function getCleanupFrequencyText(retentionSeconds: number): string {
|
||||
const days = retentionSeconds / 86400;
|
||||
if (days <= 1) return "every 6 hours";
|
||||
if (days <= 3) return "every 12 hours";
|
||||
if (days <= 7) return "daily";
|
||||
if (days <= 30) return "every 2 days";
|
||||
return "weekly";
|
||||
}
|
||||
|
||||
export function AutomationSettings({
|
||||
scheduleConfig,
|
||||
cleanupConfig,
|
||||
onScheduleChange,
|
||||
onCleanupChange,
|
||||
isAutoSavingSchedule,
|
||||
isAutoSavingCleanup,
|
||||
}: AutomationSettingsProps) {
|
||||
// Update nextRun for cleanup when settings change
|
||||
useEffect(() => {
|
||||
if (cleanupConfig.enabled && !cleanupConfig.nextRun) {
|
||||
const cleanupInterval = getCleanupInterval(cleanupConfig.retentionDays);
|
||||
const nextRun = new Date(Date.now() + cleanupInterval * 1000);
|
||||
onCleanupChange({ ...cleanupConfig, nextRun });
|
||||
}
|
||||
}, [cleanupConfig.enabled, cleanupConfig.retentionDays]);
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-semibold flex items-center gap-2">
|
||||
<Zap className="h-5 w-5" />
|
||||
Automation & Maintenance
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Automatic Mirroring Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium flex items-center gap-2">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Automatic Mirroring
|
||||
</h3>
|
||||
{isAutoSavingSchedule && (
|
||||
<Activity className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
id="enable-auto-mirror"
|
||||
checked={scheduleConfig.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
onScheduleChange({ ...scheduleConfig, enabled: !!checked })
|
||||
}
|
||||
/>
|
||||
<div className="space-y-0.5 flex-1">
|
||||
<Label
|
||||
htmlFor="enable-auto-mirror"
|
||||
className="text-sm font-normal cursor-pointer"
|
||||
>
|
||||
Enable automatic repository syncing
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Periodically check GitHub for changes and mirror them to Gitea
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{scheduleConfig.enabled && (
|
||||
<div className="ml-6 space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="mirror-interval" className="text-sm">
|
||||
Sync frequency
|
||||
</Label>
|
||||
<Select
|
||||
value={scheduleConfig.interval.toString()}
|
||||
onValueChange={(value) =>
|
||||
onScheduleChange({
|
||||
...scheduleConfig,
|
||||
interval: parseInt(value, 10),
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="mirror-interval" className="mt-1.5">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{scheduleIntervals.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value.toString()}
|
||||
>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 p-3 bg-muted/50 dark:bg-muted/20 rounded-md">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
Last sync
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{scheduleConfig.lastRun
|
||||
? formatDate(scheduleConfig.lastRun)
|
||||
: "Never"}
|
||||
</span>
|
||||
</div>
|
||||
{scheduleConfig.enabled && scheduleConfig.nextRun && (
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Calendar className="h-3.5 w-3.5" />
|
||||
Next sync
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{formatDate(scheduleConfig.nextRun)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Database Cleanup Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium flex items-center gap-2">
|
||||
<Database className="h-4 w-4" />
|
||||
Database Maintenance
|
||||
</h3>
|
||||
{isAutoSavingCleanup && (
|
||||
<Activity className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
id="enable-auto-cleanup"
|
||||
checked={cleanupConfig.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
onCleanupChange({ ...cleanupConfig, enabled: !!checked })
|
||||
}
|
||||
/>
|
||||
<div className="space-y-0.5 flex-1">
|
||||
<Label
|
||||
htmlFor="enable-auto-cleanup"
|
||||
className="text-sm font-normal cursor-pointer"
|
||||
>
|
||||
Enable automatic database cleanup
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Remove old activity logs and events to optimize storage
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{cleanupConfig.enabled && (
|
||||
<div className="ml-6 space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="retention-period" className="text-sm flex items-center gap-2">
|
||||
Data retention period
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="h-3 w-3 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-xs">
|
||||
<p className="text-xs">
|
||||
Activity logs and events older than this will be removed.
|
||||
Cleanup frequency is automatically optimized based on your retention period.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
<Select
|
||||
value={cleanupConfig.retentionDays.toString()}
|
||||
onValueChange={(value) =>
|
||||
onCleanupChange({
|
||||
...cleanupConfig,
|
||||
retentionDays: parseInt(value, 10),
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="retention-period" className="mt-1.5">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{retentionPeriods.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value.toString()}
|
||||
>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{cleanupConfig.enabled && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Cleanup runs {getCleanupFrequencyText(cleanupConfig.retentionDays)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 p-3 bg-muted/50 dark:bg-muted/20 rounded-md">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
Last cleanup
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{cleanupConfig.lastRun
|
||||
? formatDate(cleanupConfig.lastRun)
|
||||
: "Never"}
|
||||
</span>
|
||||
</div>
|
||||
{cleanupConfig.enabled && cleanupConfig.nextRun && (
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Calendar className="h-3.5 w-3.5" />
|
||||
Next cleanup
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{cleanupConfig.nextRun
|
||||
? formatDate(cleanupConfig.nextRun)
|
||||
: "Calculating..."}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-blue-50/50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-900">
|
||||
<div className="flex gap-3">
|
||||
<Info className="h-4 w-4 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-blue-900 dark:text-blue-100">
|
||||
Background Operations
|
||||
</p>
|
||||
<p className="text-xs text-blue-800 dark:text-blue-200/80">
|
||||
These automated tasks run in the background to keep your mirrors up-to-date and maintain optimal database performance.
|
||||
Choose intervals that match your workflow and repository update frequency.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { GitHubConfigForm } from './GitHubConfigForm';
|
||||
import { GiteaConfigForm } from './GiteaConfigForm';
|
||||
import { ScheduleConfigForm } from './ScheduleConfigForm';
|
||||
import { DatabaseCleanupConfigForm } from './DatabaseCleanupConfigForm';
|
||||
import { AutomationSettings } from './AutomationSettings';
|
||||
import type {
|
||||
ConfigApiResponse,
|
||||
GiteaConfig,
|
||||
@@ -80,14 +79,10 @@ export function ConfigTabs() {
|
||||
const [isAutoSavingCleanup, setIsAutoSavingCleanup] = useState<boolean>(false);
|
||||
const [isAutoSavingGitHub, setIsAutoSavingGitHub] = useState<boolean>(false);
|
||||
const [isAutoSavingGitea, setIsAutoSavingGitea] = useState<boolean>(false);
|
||||
const [isAutoSavingMirrorOptions, setIsAutoSavingMirrorOptions] = useState<boolean>(false);
|
||||
const [isAutoSavingAdvancedOptions, setIsAutoSavingAdvancedOptions] = useState<boolean>(false);
|
||||
const autoSaveScheduleTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const autoSaveCleanupTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const autoSaveGitHubTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const autoSaveGiteaTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const autoSaveMirrorOptionsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const autoSaveAdvancedOptionsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const isConfigFormValid = (): boolean => {
|
||||
const { githubConfig, giteaConfig } = config;
|
||||
@@ -362,102 +357,74 @@ export function ConfigTabs() {
|
||||
}, 500); // 500ms debounce
|
||||
}, [user?.id, config.githubConfig, config.scheduleConfig, config.cleanupConfig]);
|
||||
|
||||
// Auto-save function specifically for mirror options changes
|
||||
// Auto-save function for mirror options (handled within GitHub config)
|
||||
const autoSaveMirrorOptions = useCallback(async (mirrorOptions: MirrorOptions) => {
|
||||
if (!user?.id) return;
|
||||
|
||||
// Clear any existing timeout
|
||||
if (autoSaveMirrorOptionsTimeoutRef.current) {
|
||||
clearTimeout(autoSaveMirrorOptionsTimeoutRef.current);
|
||||
}
|
||||
const reqPayload: SaveConfigApiRequest = {
|
||||
userId: user.id!,
|
||||
githubConfig: config.githubConfig,
|
||||
giteaConfig: config.giteaConfig,
|
||||
scheduleConfig: config.scheduleConfig,
|
||||
cleanupConfig: config.cleanupConfig,
|
||||
mirrorOptions: mirrorOptions,
|
||||
advancedOptions: config.advancedOptions,
|
||||
};
|
||||
|
||||
// Debounce the auto-save to prevent excessive API calls
|
||||
autoSaveMirrorOptionsTimeoutRef.current = setTimeout(async () => {
|
||||
setIsAutoSavingMirrorOptions(true);
|
||||
try {
|
||||
const response = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(reqPayload),
|
||||
});
|
||||
const result: SaveConfigApiResponse = await response.json();
|
||||
|
||||
const reqPayload: SaveConfigApiRequest = {
|
||||
userId: user.id!,
|
||||
githubConfig: config.githubConfig,
|
||||
giteaConfig: config.giteaConfig,
|
||||
scheduleConfig: config.scheduleConfig,
|
||||
cleanupConfig: config.cleanupConfig,
|
||||
mirrorOptions: mirrorOptions,
|
||||
advancedOptions: config.advancedOptions,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(reqPayload),
|
||||
});
|
||||
const result: SaveConfigApiResponse = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Silent success - no toast for auto-save
|
||||
// Invalidate config cache so other components get fresh data
|
||||
invalidateConfigCache();
|
||||
} else {
|
||||
showErrorToast(
|
||||
`Auto-save failed: ${result.message || 'Unknown error'}`,
|
||||
toast
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorToast(error, toast);
|
||||
} finally {
|
||||
setIsAutoSavingMirrorOptions(false);
|
||||
if (result.success) {
|
||||
invalidateConfigCache();
|
||||
} else {
|
||||
showErrorToast(
|
||||
`Auto-save failed: ${result.message || 'Unknown error'}`,
|
||||
toast
|
||||
);
|
||||
}
|
||||
}, 500); // 500ms debounce
|
||||
} catch (error) {
|
||||
showErrorToast(error, toast);
|
||||
}
|
||||
}, [user?.id, config.githubConfig, config.giteaConfig, config.scheduleConfig, config.cleanupConfig, config.advancedOptions]);
|
||||
|
||||
// Auto-save function specifically for advanced options changes
|
||||
// Auto-save function for advanced options (handled within GitHub config)
|
||||
const autoSaveAdvancedOptions = useCallback(async (advancedOptions: AdvancedOptions) => {
|
||||
if (!user?.id) return;
|
||||
|
||||
// Clear any existing timeout
|
||||
if (autoSaveAdvancedOptionsTimeoutRef.current) {
|
||||
clearTimeout(autoSaveAdvancedOptionsTimeoutRef.current);
|
||||
}
|
||||
const reqPayload: SaveConfigApiRequest = {
|
||||
userId: user.id!,
|
||||
githubConfig: config.githubConfig,
|
||||
giteaConfig: config.giteaConfig,
|
||||
scheduleConfig: config.scheduleConfig,
|
||||
cleanupConfig: config.cleanupConfig,
|
||||
mirrorOptions: config.mirrorOptions,
|
||||
advancedOptions: advancedOptions,
|
||||
};
|
||||
|
||||
// Debounce the auto-save to prevent excessive API calls
|
||||
autoSaveAdvancedOptionsTimeoutRef.current = setTimeout(async () => {
|
||||
setIsAutoSavingAdvancedOptions(true);
|
||||
try {
|
||||
const response = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(reqPayload),
|
||||
});
|
||||
const result: SaveConfigApiResponse = await response.json();
|
||||
|
||||
const reqPayload: SaveConfigApiRequest = {
|
||||
userId: user.id!,
|
||||
githubConfig: config.githubConfig,
|
||||
giteaConfig: config.giteaConfig,
|
||||
scheduleConfig: config.scheduleConfig,
|
||||
cleanupConfig: config.cleanupConfig,
|
||||
mirrorOptions: config.mirrorOptions,
|
||||
advancedOptions: advancedOptions,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(reqPayload),
|
||||
});
|
||||
const result: SaveConfigApiResponse = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Silent success - no toast for auto-save
|
||||
// Invalidate config cache so other components get fresh data
|
||||
invalidateConfigCache();
|
||||
} else {
|
||||
showErrorToast(
|
||||
`Auto-save failed: ${result.message || 'Unknown error'}`,
|
||||
toast
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorToast(error, toast);
|
||||
} finally {
|
||||
setIsAutoSavingAdvancedOptions(false);
|
||||
if (result.success) {
|
||||
invalidateConfigCache();
|
||||
} else {
|
||||
showErrorToast(
|
||||
`Auto-save failed: ${result.message || 'Unknown error'}`,
|
||||
toast
|
||||
);
|
||||
}
|
||||
}, 500); // 500ms debounce
|
||||
} catch (error) {
|
||||
showErrorToast(error, toast);
|
||||
}
|
||||
}, [user?.id, config.githubConfig, config.giteaConfig, config.scheduleConfig, config.cleanupConfig, config.mirrorOptions]);
|
||||
|
||||
// Cleanup timeouts on unmount
|
||||
@@ -475,12 +442,6 @@ export function ConfigTabs() {
|
||||
if (autoSaveGiteaTimeoutRef.current) {
|
||||
clearTimeout(autoSaveGiteaTimeoutRef.current);
|
||||
}
|
||||
if (autoSaveMirrorOptionsTimeoutRef.current) {
|
||||
clearTimeout(autoSaveMirrorOptionsTimeoutRef.current);
|
||||
}
|
||||
if (autoSaveAdvancedOptionsTimeoutRef.current) {
|
||||
clearTimeout(autoSaveAdvancedOptionsTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -569,20 +530,19 @@ export function ConfigTabs() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Schedule & Database Cleanup - Side by side */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="border rounded-lg p-4">
|
||||
{/* Automation & Maintenance - Full width */}
|
||||
<div className="border rounded-lg p-4">
|
||||
<Skeleton className="h-8 w-48 mb-4" />
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-6 w-40" />
|
||||
<Skeleton className="h-16 w-full" />
|
||||
<Skeleton className="h-8 w-32" />
|
||||
<Skeleton className="h-24 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="border rounded-lg p-4">
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-6 w-40" />
|
||||
<Skeleton className="h-16 w-full" />
|
||||
<Skeleton className="h-8 w-32" />
|
||||
<Skeleton className="h-24 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -692,35 +652,21 @@ export function ConfigTabs() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Schedule & Database Cleanup - Side by side */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<ScheduleConfigForm
|
||||
config={config.scheduleConfig}
|
||||
setConfig={update =>
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
scheduleConfig:
|
||||
typeof update === 'function'
|
||||
? update(prev.scheduleConfig)
|
||||
: update,
|
||||
}))
|
||||
}
|
||||
onAutoSave={autoSaveScheduleConfig}
|
||||
isAutoSaving={isAutoSavingSchedule}
|
||||
/>
|
||||
<DatabaseCleanupConfigForm
|
||||
config={config.cleanupConfig}
|
||||
setConfig={update =>
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
cleanupConfig:
|
||||
typeof update === 'function'
|
||||
? update(prev.cleanupConfig)
|
||||
: update,
|
||||
}))
|
||||
}
|
||||
onAutoSave={autoSaveCleanupConfig}
|
||||
isAutoSaving={isAutoSavingCleanup}
|
||||
{/* Automation & Maintenance - Full width */}
|
||||
<div>
|
||||
<AutomationSettings
|
||||
scheduleConfig={config.scheduleConfig}
|
||||
cleanupConfig={config.cleanupConfig}
|
||||
onScheduleChange={(newConfig) => {
|
||||
setConfig(prev => ({ ...prev, scheduleConfig: newConfig }));
|
||||
autoSaveScheduleConfig(newConfig);
|
||||
}}
|
||||
onCleanupChange={(newConfig) => {
|
||||
setConfig(prev => ({ ...prev, cleanupConfig: newConfig }));
|
||||
autoSaveCleanupConfig(newConfig);
|
||||
}}
|
||||
isAutoSavingSchedule={isAutoSavingSchedule}
|
||||
isAutoSavingCleanup={isAutoSavingCleanup}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user