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