Refactor BlocklistsPage into section components and fix frontend lint issues
This commit is contained in:
175
frontend/src/components/blocklist/BlocklistScheduleSection.tsx
Normal file
175
frontend/src/components/blocklist/BlocklistScheduleSection.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { Button, Field, Input, MessageBar, MessageBarBody, Select, Spinner, Text } from "@fluentui/react-components";
|
||||
import { PlayRegular } from "@fluentui/react-icons";
|
||||
import { useCommonSectionStyles } from "../../theme/commonStyles";
|
||||
import { useSchedule } from "../../hooks/useBlocklist";
|
||||
import { useBlocklistStyles } from "./blocklistStyles";
|
||||
import type { ScheduleConfig, ScheduleFrequency } from "../../types/blocklist";
|
||||
|
||||
const FREQUENCY_LABELS: Record<ScheduleFrequency, string> = {
|
||||
hourly: "Every N hours",
|
||||
daily: "Daily",
|
||||
weekly: "Weekly",
|
||||
};
|
||||
|
||||
const DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
||||
|
||||
interface ScheduleSectionProps {
|
||||
onRunImport: () => void;
|
||||
runImportRunning: boolean;
|
||||
}
|
||||
|
||||
export function BlocklistScheduleSection({ onRunImport, runImportRunning }: ScheduleSectionProps): React.JSX.Element {
|
||||
const styles = useBlocklistStyles();
|
||||
const sectionStyles = useCommonSectionStyles();
|
||||
const { info, loading, error, saveSchedule } = useSchedule();
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saveMsg, setSaveMsg] = useState<string | null>(null);
|
||||
|
||||
const config = info?.config ?? {
|
||||
frequency: "daily" as ScheduleFrequency,
|
||||
interval_hours: 24,
|
||||
hour: 3,
|
||||
minute: 0,
|
||||
day_of_week: 0,
|
||||
};
|
||||
|
||||
const [draft, setDraft] = useState<ScheduleConfig>(config);
|
||||
|
||||
const handleSave = useCallback((): void => {
|
||||
setSaving(true);
|
||||
saveSchedule(draft)
|
||||
.then(() => {
|
||||
setSaveMsg("Schedule saved.");
|
||||
setSaving(false);
|
||||
setTimeout(() => { setSaveMsg(null); }, 3000);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
setSaveMsg(err instanceof Error ? err.message : "Failed to save schedule");
|
||||
setSaving(false);
|
||||
});
|
||||
}, [draft, saveSchedule]);
|
||||
|
||||
return (
|
||||
<div className={sectionStyles.section}>
|
||||
<div className={sectionStyles.sectionHeader}>
|
||||
<Text size={500} weight="semibold">
|
||||
Import Schedule
|
||||
</Text>
|
||||
<Button icon={<PlayRegular />} appearance="secondary" onClick={onRunImport} disabled={runImportRunning}>
|
||||
{runImportRunning ? <Spinner size="tiny" /> : "Run Now"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<MessageBar intent="error">
|
||||
<MessageBarBody>{error}</MessageBarBody>
|
||||
</MessageBar>
|
||||
)}
|
||||
{saveMsg && (
|
||||
<MessageBar intent={saveMsg === "Schedule saved." ? "success" : "error"}>
|
||||
<MessageBarBody>{saveMsg}</MessageBarBody>
|
||||
</MessageBar>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className={styles.centred}>
|
||||
<Spinner label="Loading schedule…" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.scheduleForm}>
|
||||
<Field label="Frequency" className={styles.scheduleField}>
|
||||
<Select
|
||||
value={draft.frequency}
|
||||
onChange={(_ev, d) => { setDraft((p) => ({ ...p, frequency: d.value as ScheduleFrequency })); }}
|
||||
>
|
||||
{(["hourly", "daily", "weekly"] as ScheduleFrequency[]).map((f) => (
|
||||
<option key={f} value={f}>
|
||||
{FREQUENCY_LABELS[f]}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
{draft.frequency === "hourly" && (
|
||||
<Field label="Every (hours)" className={styles.scheduleField}>
|
||||
<Input
|
||||
type="number"
|
||||
value={String(draft.interval_hours)}
|
||||
onChange={(_ev, d) => { setDraft((p) => ({ ...p, interval_hours: Math.max(1, parseInt(d.value, 10) || 1) })); }}
|
||||
min={1}
|
||||
max={168}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{draft.frequency !== "hourly" && (
|
||||
<>
|
||||
{draft.frequency === "weekly" && (
|
||||
<Field label="Day of week" className={styles.scheduleField}>
|
||||
<Select
|
||||
value={String(draft.day_of_week)}
|
||||
onChange={(_ev, d) => { setDraft((p) => ({ ...p, day_of_week: parseInt(d.value, 10) })); }}
|
||||
>
|
||||
{DAYS.map((day, i) => (
|
||||
<option key={day} value={i}>
|
||||
{day}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field label="Hour (UTC)" className={styles.scheduleField}>
|
||||
<Select
|
||||
value={String(draft.hour)}
|
||||
onChange={(_ev, d) => { setDraft((p) => ({ ...p, hour: parseInt(d.value, 10) })); }}
|
||||
>
|
||||
{Array.from({ length: 24 }, (_, i) => (
|
||||
<option key={i} value={i}>
|
||||
{String(i).padStart(2, "0")}:00
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
<Field label="Minute" className={styles.scheduleField}>
|
||||
<Select
|
||||
value={String(draft.minute)}
|
||||
onChange={(_ev, d) => { setDraft((p) => ({ ...p, minute: parseInt(d.value, 10) })); }}
|
||||
>
|
||||
{[0, 15, 30, 45].map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{String(m).padStart(2, "0")}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button appearance="primary" onClick={handleSave} disabled={saving} style={{ alignSelf: "flex-end" }}>
|
||||
{saving ? <Spinner size="tiny" /> : "Save Schedule"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={styles.metaRow}>
|
||||
<div className={styles.metaItem}>
|
||||
<Text size={200} weight="semibold">
|
||||
Last run
|
||||
</Text>
|
||||
<Text size={200}>{info?.last_run_at ?? "Never"}</Text>
|
||||
</div>
|
||||
<div className={styles.metaItem}>
|
||||
<Text size={200} weight="semibold">
|
||||
Next run
|
||||
</Text>
|
||||
<Text size={200}>{info?.next_run_at ?? "Not scheduled"}</Text>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user