Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@
"dompurify": "^3.1.7",
"lucide-react": "^0.446.0",
"marked": "^14.1.3",
"next-themes": "^0.4.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.54.0",
"react-router-dom": "^6.26.2",
"sonner": "^2.0.1",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
Expand Down
28 changes: 28 additions & 0 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Outlet } from "react-router-dom";
import { ApplicationContextProvider } from "@/components/util/application-context-provider.tsx";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar.tsx";
import { MainSidebar } from "@/components/navigation/main-sidebar.tsx";
import { Toaster } from "@/components/ui/sonner.tsx"

function App() {
return (
Expand All @@ -12,6 +13,7 @@ function App() {
<SidebarInset>
<div className="flex flex-col grow">
<Outlet/>
<Toaster />
</div>
</SidebarInset>
</SidebarProvider>
Expand Down
131 changes: 124 additions & 7 deletions frontend/src/components/data-sets/data-set-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,138 @@ import { ApiClient } from "@/lib/api.ts";
import { authenticationProviderInstance } from "@/lib/authentication-provider.ts";
import { useApplicationContext } from "@/lib/use-application-context.ts";
import { useNavigate } from "react-router-dom";
import { DataSet } from "@/lib/types.ts";
import { DataSet, SyncStatus } from "@/lib/types.ts";
import { useState, useEffect } from "react";
import { CheckCircle, XCircle, Clock } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
import { SyncScheduleModal } from "@/components/data-sets/sync-schedule-modal.tsx";

const api = new ApiClient(authenticationProviderInstance);

interface SyncInfo {
lastSyncTime: Date | null;
status: string;
errorMessage?: string;
}

export function DataSetList() {
const {dataSets} = useApplicationContext()!;
const navigate = useNavigate();
const [syncInfo, setSyncInfo] = useState<SyncInfo>({
lastSyncTime: null,
status: "idle",
});
const [loading, setLoading] = useState(true);

useEffect(() => {
const fetchLastSyncInfo = async () => {
try {
const lastSyncData: SyncStatus = await api.dataSets().getLastSynchronization();
setSyncInfo({
lastSyncTime: new Date(lastSyncData.timestamp),
status: lastSyncData.status,
errorMessage: lastSyncData.error_message,
});
} catch {
setSyncInfo({
lastSyncTime: null,
status: "idle",
});
} finally {
setLoading(false);
}
};

fetchLastSyncInfo();
}, []);

const handleSyncAllSources = async () => {
await api.dataSets().syncAllSources();
}
try {
setSyncInfo((prev) => ({ ...prev, status: "syncing" }));
await api.dataSets().syncAllSources();
setSyncInfo({
lastSyncTime: new Date(),
status: "success",
});
toast.success("All sources have been synchronized successfully.");
} catch (error) {
setSyncInfo({
lastSyncTime: new Date(),
status: "error",
errorMessage: error instanceof Error ? error.message : "Unknown error occurred",
});
toast.error(error instanceof Error ? error.message : "Unknown error occurred");
}
};

const handleSyncDataSetAllSources = async (dataSet: DataSet) => {
await api.dataSets().syncDataSetAllSources(dataSet.id);
}
try {
setSyncInfo((prev) => ({ ...prev, status: "syncing" }));
await api.dataSets().syncDataSetAllSources(dataSet.id!);
setSyncInfo({
lastSyncTime: new Date(),
status: "success",
});
toast.success(`Data set ${dataSet.name} has been synchronized successfully.`);
} catch (error) {
setSyncInfo({
lastSyncTime: new Date(),
status: "error",
errorMessage: error instanceof Error ? error.message : "Unknown error occurred",
});
toast.error(error instanceof Error ? error.message : "Unknown error occurred");
}
};

const formatSyncTime = (date: Date | null) => {
if (!date) return "Never";
return date.toLocaleString();
};

return (
<>
<div className="flex flex-row justify-end items-center space-x-4 mb-4">
<Button variant="default" onClick={() => navigate('/data-sets/new') }>New Data Set</Button>
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6 gap-4">
<div className="flex flex-col">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Last synchronization:</span>
{loading ? (
<Badge variant="outline" className="flex items-center gap-1 bg-gray-50 text-gray-700 border-gray-200">
<Clock className="h-3.5 w-3.5" />
<span>Loading...</span>
</Badge>
) : syncInfo.status === "syncing" ? (
<Badge variant="outline" className="flex items-center gap-1 bg-blue-50 text-blue-700 border-blue-200">
<Clock className="h-3.5 w-3.5" />
<span>Syncing...</span>
</Badge>
) : syncInfo.status === "success" ? (
<Badge variant="outline" className="flex items-center gap-1 bg-green-50 text-green-700 border-green-200">
<CheckCircle className="h-3.5 w-3.5" />
<span>{formatSyncTime(syncInfo.lastSyncTime)}</span>
</Badge>
) : syncInfo.status === "error" ? (
<Badge
variant="outline"
className="flex items-center gap-1 bg-red-50 text-red-700 border-red-200"
title={syncInfo.errorMessage}
>
<XCircle className="h-3.5 w-3.5" />
<span>{formatSyncTime(syncInfo.lastSyncTime)}</span>
</Badge>
) : (
<Badge variant="outline" className="flex items-center gap-1">
<span>{formatSyncTime(syncInfo.lastSyncTime)}</span>
</Badge>
)}
</div>
</div>
<div className="flex flex-row justify-end items-center space-x-4">
<Button variant="default" onClick={() => navigate('/data-sets/new')}>
New Data Set
</Button>
<Button variant="default" onClick={() => handleSyncAllSources() }>Sync All</Button>
</div>
</div>
<Table>
<TableHeader>
Expand Down Expand Up @@ -54,6 +165,12 @@ export function DataSetList() {
handleSyncDataSetAllSources(item)
}} variant="secondary">Sync</Button>
</TableCell>
<TableCell>
<SyncScheduleModal
dataSetName={item.name}
dataSetId={item.id!}
/>
</TableCell>
</TableRow>
))}
</TableBody>
Expand Down
100 changes: 100 additions & 0 deletions frontend/src/components/data-sets/form/schedule-frequency-field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form.tsx";
import { UseFormReturn } from "react-hook-form";
import { Calendar } from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";

export interface ScheduleFrequencyFieldsProps {
form: UseFormReturn<any, any, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
}

export function ScheduleFrequencyFields({ form }: ScheduleFrequencyFieldsProps) {
const frequency = form.watch("frequency");
const days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];

const formDayAbbrev = form.watch("day_of_week");
const selectedDay =
days.find((day) => day.substring(0, 3).toLowerCase() === formDayAbbrev) || "Sunday";

const showDaySelection = frequency === "weekly";

const selectDay = (day: string) => {
form.setValue("day_of_week", day.substring(0, 3).toLowerCase());
};

return (
<FormField
control={form.control}
name="frequency"
render={({ field }) => (
<FormItem>
<FormLabel>Frequency</FormLabel>
<FormControl>
<div className="space-y-8 max-w-md mx-auto p-8 border rounded-xl shadow-sm bg-card">
<div className="space-y-3">
<Label htmlFor="sync-frequency" className="text-base font-medium">
Synchronization Frequency
</Label>
<Select
// Removed fallback value
value={field.value}
onValueChange={(value) => {
field.onChange(value);
}}
>
<SelectTrigger id="sync-frequency" className="w-full">
<SelectValue placeholder="Select frequency" />
</SelectTrigger>
<SelectContent>
<SelectItem value="daily">Daily</SelectItem>
<SelectItem value="weekly">Once a week</SelectItem>
<SelectItem value="monthly">Monthly</SelectItem>
</SelectContent>
</Select>
</div>

{showDaySelection && (
<div className="space-y-4">
<Label className="flex items-center gap-2 text-base font-medium">
<Calendar className="h-5 w-5" />
Select day for synchronization
</Label>

<div className="grid grid-cols-7 gap-3">
{days.map((day) => (
<button
key={day}
type="button"
onClick={() => selectDay(day)}
className={cn(
"aspect-square rounded-full flex flex-col items-center justify-center transition-all",
selectedDay === day
? "bg-primary text-primary-foreground ring-2 ring-primary ring-offset-2"
: "bg-muted hover:bg-muted/80"
)}
>
<span className="text-xs font-medium">{day.substring(0, 1)}</span>
</button>
))}
</div>

<div className="flex items-center justify-between mt-4 px-2">
<div className="text-sm font-medium">Selected day:</div>
<div className="text-sm bg-primary/10 text-primary px-3 py-1 rounded-full font-medium">
{selectedDay}
</div>
</div>
</div>
)}
</div>
</FormControl>
<FormMessage />
<FormDescription>
Specify the frequency of the synchronization.
</FormDescription>
</FormItem>
)}
/>
);
}
Loading