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, FormItem,
FormLabel, FormLabel,
FormMessage, FormMessage,
FormDescription,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; 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({ const formSchema = z.object({
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
@ -82,16 +107,22 @@ export const HandleSchedules = ({
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField <FormField
control={form.control} control={form.control}
name="name" name="name"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel className="flex items-center gap-2">
<Clock className="w-4 h-4" />
Task Name
</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Daily backup" {...field} /> <Input placeholder="Daily Database Backup" {...field} />
</FormControl> </FormControl>
<FormDescription>
A descriptive name for your scheduled task
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@ -102,10 +133,50 @@ export const HandleSchedules = ({
name="cronExpression" name="cronExpression"
render={({ field }) => ( render={({ field }) => (
<FormItem> <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> <FormControl>
<Input placeholder="0 0 * * *" {...field} /> <Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
className="mt-2"
/>
</FormControl> </FormControl>
<FormDescription>
Choose a predefined schedule or enter a custom cron expression
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@ -116,17 +187,33 @@ export const HandleSchedules = ({
name="command" name="command"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Command</FormLabel> <FormLabel className="flex items-center gap-2">
<Terminal className="w-4 h-4" />
Command
</FormLabel>
<FormControl> <FormControl>
<Input placeholder="npm run backup" {...field} /> <Input
placeholder="docker exec my-container npm run backup"
{...field}
/>
</FormControl> </FormControl>
<FormDescription>
The command to execute in your container
</FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<Button type="submit" disabled={isLoading}> <Button type="submit" disabled={isLoading} className="w-full">
{scheduleId ? "Update" : "Create"} Schedule {isLoading ? (
<>
<Clock className="mr-2 h-4 w-4 animate-spin" />
{scheduleId ? "Updating..." : "Creating..."}
</>
) : (
<>{scheduleId ? "Update" : "Create"} Schedule</>
)}
</Button> </Button>
</form> </form>
</Form> </Form>

View File

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