Enhance schedule management UI

- Updated `HandleSchedules` component to include predefined cron expressions and improved form descriptions for better user guidance.
- Refactored `ShowSchedules` component to utilize a card layout, enhancing visual presentation and user experience.
- Added icons and tooltips for better context on schedule creation and management actions.
- Improved accessibility and responsiveness of the schedule management interface.
This commit is contained in:
Mauricio Siu 2025-05-02 03:26:05 -06:00
parent d4064805eb
commit 0ea264ea42
2 changed files with 201 additions and 75 deletions

View File

@ -6,12 +6,37 @@ import {
FormItem,
FormLabel,
FormMessage,
FormDescription,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Clock, Terminal, Info } from "lucide-react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
const commonCronExpressions = [
{ label: "Every minute", value: "* * * * *" },
{ label: "Every hour", value: "0 * * * *" },
{ label: "Every day at midnight", value: "0 0 * * *" },
{ label: "Every Sunday at midnight", value: "0 0 * * 0" },
{ label: "Every month on the 1st at midnight", value: "0 0 1 * *" },
{ label: "Every 15 minutes", value: "*/15 * * * *" },
{ label: "Every weekday at midnight", value: "0 0 * * 1-5" },
];
const formSchema = z.object({
name: z.string().min(1, "Name is required"),
@ -82,16 +107,22 @@ export const HandleSchedules = ({
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormLabel className="flex items-center gap-2">
<Clock className="w-4 h-4" />
Task Name
</FormLabel>
<FormControl>
<Input placeholder="Daily backup" {...field} />
<Input placeholder="Daily Database Backup" {...field} />
</FormControl>
<FormDescription>
A descriptive name for your scheduled task
</FormDescription>
<FormMessage />
</FormItem>
)}
@ -102,10 +133,50 @@ export const HandleSchedules = ({
name="cronExpression"
render={({ field }) => (
<FormItem>
<FormLabel>Cron Expression</FormLabel>
<FormLabel className="flex items-center gap-2">
<Clock className="w-4 h-4" />
Schedule
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Cron expression format: minute hour day month weekday
</p>
<p>Example: 0 0 * * * (daily at midnight)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<Select
onValueChange={(value) => field.onChange(value)}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select or type a cron expression" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonCronExpressions.map((expr) => (
<SelectItem key={expr.value} value={expr.value}>
{expr.label} ({expr.value})
</SelectItem>
))}
</SelectContent>
</Select>
<FormControl>
<Input placeholder="0 0 * * *" {...field} />
<Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
className="mt-2"
/>
</FormControl>
<FormDescription>
Choose a predefined schedule or enter a custom cron expression
</FormDescription>
<FormMessage />
</FormItem>
)}
@ -116,17 +187,33 @@ export const HandleSchedules = ({
name="command"
render={({ field }) => (
<FormItem>
<FormLabel>Command</FormLabel>
<FormLabel className="flex items-center gap-2">
<Terminal className="w-4 h-4" />
Command
</FormLabel>
<FormControl>
<Input placeholder="npm run backup" {...field} />
<Input
placeholder="docker exec my-container npm run backup"
{...field}
/>
</FormControl>
<FormDescription>
The command to execute in your container
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={isLoading}>
{scheduleId ? "Update" : "Create"} Schedule
<Button type="submit" disabled={isLoading} className="w-full">
{isLoading ? (
<>
<Clock className="mr-2 h-4 w-4 animate-spin" />
{scheduleId ? "Updating..." : "Creating..."}
</>
) : (
<>{scheduleId ? "Update" : "Create"} Schedule</>
)}
</Button>
</form>
</Form>

View File

@ -17,6 +17,9 @@ import {
import { api } from "@/utils/api";
import { useState } from "react";
import { HandleSchedules } from "./handle-schedules";
import { PlusCircle, Clock, Terminal, Trash2, Edit2 } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
interface Props {
applicationId: string;
@ -49,71 +52,107 @@ export const ShowSchedules = ({ applicationId }: Props) => {
};
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold">Schedules</h2>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button>Create Schedule</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
{editingSchedule ? "Edit" : "Create"} Schedule
</DialogTitle>
</DialogHeader>
<HandleSchedules
applicationId={applicationId}
onSuccess={onClose}
defaultValues={editingSchedule || undefined}
scheduleId={editingSchedule?.scheduleId}
/>
</DialogContent>
</Dialog>
</div>
{schedules && schedules.length > 0 ? (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Cron Expression</TableHead>
<TableHead>Command</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{schedules.map((schedule) => (
<TableRow key={schedule.scheduleId}>
<TableCell>{schedule.name}</TableCell>
<TableCell>{schedule.cronExpression}</TableCell>
<TableCell>{schedule.command}</TableCell>
<TableCell className="space-x-2">
<Button
variant="outline"
onClick={() => {
setEditingSchedule(schedule);
setIsOpen(true);
}}
>
Edit
</Button>
<Button
variant="destructive"
onClick={() =>
deleteSchedule({ scheduleId: schedule.scheduleId })
}
>
Delete
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<div className="text-center text-gray-500">No schedules found</div>
)}
</div>
<Card className="border px-4 shadow-none bg-transparent">
<CardHeader className="px-0">
<div className="flex justify-between items-center">
<CardTitle className="text-xl font-bold flex items-center gap-2">
<Clock className="size-4 text-muted-foreground" />
Scheduled Tasks
</CardTitle>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button className="gap-2">
<PlusCircle className="w-4 h-4" />
New Schedule
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
{editingSchedule ? "Edit" : "Create"} Schedule
</DialogTitle>
</DialogHeader>
<HandleSchedules
applicationId={applicationId}
onSuccess={onClose}
defaultValues={editingSchedule || undefined}
scheduleId={editingSchedule?.scheduleId}
/>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent className="px-0">
{schedules && schedules.length > 0 ? (
<div className="rounded-lg border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Task Name</TableHead>
<TableHead>Schedule</TableHead>
<TableHead>Command</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{schedules.map((schedule) => (
<TableRow key={schedule.scheduleId}>
<TableCell className="font-medium">
{schedule.name}
</TableCell>
<TableCell>
<Badge variant="secondary" className="font-mono">
{schedule.cronExpression}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Terminal className="w-4 h-4 text-muted-foreground" />
<code className="bg-muted px-2 py-1 rounded text-sm">
{schedule.command}
</code>
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
setEditingSchedule(schedule);
setIsOpen(true);
}}
>
<Edit2 className="w-4 h-4" />
<span className="sr-only">Edit</span>
</Button>
<Button
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive"
onClick={() =>
deleteSchedule({ scheduleId: schedule.scheduleId })
}
>
<Trash2 className="w-4 h-4" />
<span className="sr-only">Delete</span>
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="flex flex-row gap-4 items-center justify-center py-12 border rounded-lg">
<Clock className="size-6 text-muted-foreground" />
<p className="text-muted-foreground text-center">
No scheduled tasks found
</p>
</div>
)}
</CardContent>
</Card>
);
};