mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
feat(onboarding): add BackupScheduleSelector component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
0d3135af94
commit
eda3c574c3
@@ -0,0 +1,186 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { AdvancedCronSelect } from "@/features/database/cron-advanced-select";
|
||||
|
||||
export type BackupScheduleValue = {
|
||||
method: "manual" | "automatic";
|
||||
cron?: string;
|
||||
};
|
||||
|
||||
const PRESETS = [
|
||||
{ label: "Every hour", cron: "0 * * * *" },
|
||||
{ label: "Every day", cron: "0 0 * * *" },
|
||||
{ label: "Every week", cron: "0 0 * * 0" },
|
||||
{ label: "Custom", cron: "custom" },
|
||||
] as const;
|
||||
|
||||
type PresetCron = (typeof PRESETS)[number]["cron"];
|
||||
|
||||
function detectPreset(cron: string | undefined): PresetCron {
|
||||
const match = PRESETS.find((p) => p.cron !== "custom" && p.cron === cron);
|
||||
return match ? match.cron : "custom";
|
||||
}
|
||||
|
||||
type BackupScheduleSelectorProps = {
|
||||
value: BackupScheduleValue;
|
||||
onChange: (value: BackupScheduleValue) => void;
|
||||
};
|
||||
|
||||
export const BackupScheduleSelector = ({
|
||||
value,
|
||||
onChange,
|
||||
}: BackupScheduleSelectorProps) => {
|
||||
const [customCron, setCustomCron] = useState<string>(
|
||||
value.cron ?? "0 0 * * *"
|
||||
);
|
||||
|
||||
const selectedPreset = detectPreset(value.cron);
|
||||
const isCustom = selectedPreset === "custom";
|
||||
|
||||
const handleMethodChange = (method: "manual" | "automatic") => {
|
||||
onChange({
|
||||
method,
|
||||
cron: method === "manual" ? undefined : (value.cron ?? "0 0 * * *"),
|
||||
});
|
||||
};
|
||||
|
||||
const handlePresetChange = (preset: PresetCron) => {
|
||||
if (preset === "custom") {
|
||||
onChange({ ...value, cron: customCron });
|
||||
} else {
|
||||
onChange({ ...value, cron: preset });
|
||||
}
|
||||
};
|
||||
|
||||
const handleCronPartChange = (
|
||||
type: "minute" | "hour" | "day-of-month" | "month" | "day-of-week",
|
||||
part: string
|
||||
) => {
|
||||
const indexMap: Record<typeof type, number> = {
|
||||
minute: 0,
|
||||
hour: 1,
|
||||
"day-of-month": 2,
|
||||
month: 3,
|
||||
"day-of-week": 4,
|
||||
};
|
||||
const parts = (customCron || "0 0 * * *").split(" ");
|
||||
parts[indexMap[type]] = part;
|
||||
const newCron = parts.join(" ");
|
||||
setCustomCron(newCron);
|
||||
onChange({ ...value, cron: newCron });
|
||||
};
|
||||
|
||||
const cronParts = (value.cron ?? customCron).split(" ");
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<RadioGroup
|
||||
value={value.method}
|
||||
onValueChange={(m) => handleMethodChange(m as "manual" | "automatic")}
|
||||
className="grid grid-cols-1 gap-3"
|
||||
>
|
||||
{(
|
||||
[
|
||||
{
|
||||
id: "manual",
|
||||
label: "Manual",
|
||||
desc: "Backups triggered manually only",
|
||||
},
|
||||
{
|
||||
id: "automatic",
|
||||
label: "Automatic",
|
||||
desc: "Scheduled via cron expression",
|
||||
},
|
||||
] as const
|
||||
).map((opt) => (
|
||||
<Label
|
||||
key={opt.id}
|
||||
htmlFor={opt.id}
|
||||
className={`flex items-center space-x-3 rounded-lg border p-4 cursor-pointer transition-colors ${
|
||||
value.method === opt.id
|
||||
? "border-primary bg-primary/5"
|
||||
: "hover:bg-muted/50"
|
||||
}`}
|
||||
>
|
||||
<RadioGroupItem value={opt.id} id={opt.id} />
|
||||
<div className="flex-1">
|
||||
<span className="font-medium">{opt.label}</span>
|
||||
<p className="text-sm text-muted-foreground">{opt.desc}</p>
|
||||
</div>
|
||||
</Label>
|
||||
))}
|
||||
</RadioGroup>
|
||||
|
||||
{value.method === "automatic" && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>Frequency</Label>
|
||||
<Select
|
||||
value={selectedPreset}
|
||||
onValueChange={(v) => handlePresetChange(v as PresetCron)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PRESETS.map((p) => (
|
||||
<SelectItem key={p.cron} value={p.cron}>
|
||||
{p.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{isCustom && (
|
||||
<div className="flex flex-col gap-2 pl-1">
|
||||
{(
|
||||
[
|
||||
{ type: "minute", label: "Minute", options: Array.from({ length: 60 }, (_, i) => String(i).padStart(2, "0")), partIdx: 0 },
|
||||
{ type: "hour", label: "Hour", options: Array.from({ length: 24 }, (_, i) => String(i).padStart(2, "0")), partIdx: 1 },
|
||||
{ type: "day-of-month", label: "Day of Month", options: Array.from({ length: 31 }, (_, i) => String(i + 1).padStart(2, "0")), partIdx: 2 },
|
||||
{ type: "month", label: "Month", options: ["01","02","03","04","05","06","07","08","09","10","11","12"], partIdx: 3 },
|
||||
{ type: "day-of-week", label: "Day of Week", options: ["0","1","2","3","4","5","6"], partIdx: 4 },
|
||||
] as const
|
||||
).map(({ type, label, options, partIdx }) => (
|
||||
<AdvancedCronSelect
|
||||
key={type}
|
||||
id={type}
|
||||
label={label}
|
||||
options={[...options]}
|
||||
type={type}
|
||||
value={cronParts[partIdx] ?? "*"}
|
||||
defaultValue={cronParts[partIdx] ?? "*"}
|
||||
onValueChange={(val) =>
|
||||
handleCronPartChange(
|
||||
type as "minute" | "hour" | "day-of-month" | "month" | "day-of-week",
|
||||
val
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-md bg-muted/50 px-3 py-2 text-xs font-mono text-muted-foreground">
|
||||
{value.cron ?? "0 0 * * *"}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user