Skip to content

Commit

Permalink
enhance: Webhooks page to Workflow Triggers
Browse files Browse the repository at this point in the history
  • Loading branch information
ivyjeong13 committed Jan 7, 2025
1 parent 51275aa commit 006c683
Show file tree
Hide file tree
Showing 19 changed files with 693 additions and 228 deletions.
4 changes: 2 additions & 2 deletions ui/admin/app/components/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ const items = [
icon: BoxesIcon,
},
{
title: "Webhooks",
url: $path("/webhooks"),
title: "Workflow Triggers",
url: $path("/workflow-triggers"),
icon: WebhookIcon,
},
];
Expand Down
49 changes: 0 additions & 49 deletions ui/admin/app/components/webhooks/DeleteWebhook.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion ui/admin/app/components/webhooks/WebhookConfirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export const WebhookConfirmation = ({
<Link
as="button"
className="w-full"
to={$path("/webhooks")}
to={$path("/workflow-triggers")}
>
Continue
</Link>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { PlusIcon } from "lucide-react";
import { $path } from "safe-routes";

import { Button } from "~/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
import { Link } from "~/components/ui/link";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "~/components/ui/tooltip";

export function CreateWorkflowTrigger() {
return (
<Dialog>
<DialogTrigger>
<Button>
<PlusIcon /> Create Trigger
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create Workflow Trigger</DialogTitle>
</DialogHeader>
<DialogDescription>
Select the type of workflow trigger you want to create.
</DialogDescription>
<div className="flex flex-col w-full space-y-4">
<Tooltip>
<TooltipTrigger asChild>
<Link
to={$path("/workflow-triggers/webhooks/create")}
as="button"
variant="outline"
>
Webhook
</Link>
</TooltipTrigger>
<TooltipContent>
Set up a workflow to send real-time events.
</TooltipContent>
</Tooltip>

<Tooltip>
<TooltipTrigger asChild>
<Link
to={$path("/workflow-triggers/schedule/create")}
as="button"
variant="outline"
>
Schedule
</Link>
</TooltipTrigger>
<TooltipContent>
Set up a workflow to run on an interval.
</TooltipContent>
</Tooltip>
</div>
</DialogContent>
</Dialog>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { toast } from "sonner";
import { mutate } from "swr";

import { CronJobApiService } from "~/lib/service/api/cronjobApiService";
import { WebhookApiService } from "~/lib/service/api/webhookApiService";

import { TypographyP } from "~/components/Typography";
import { ConfirmationDialog } from "~/components/composed/ConfirmationDialog";
import { DropdownMenuItem } from "~/components/ui/dropdown-menu";
import { useConfirmationDialog } from "~/hooks/component-helpers/useConfirmationDialog";
import { useAsync } from "~/hooks/useAsync";

export function DeleteWorkflowTrigger({
id,
name,
type,
}: {
id: string;
name?: string;
type: "webhook" | "schedule";
}) {
const deleteWebhook = useAsync(WebhookApiService.deleteWebhook, {
onSuccess: () => {
mutate(WebhookApiService.getWebhooks.key());
toast.success("Webhook workflow trigger has been deleted.");
},
});

const deleteCronjob = useAsync(CronJobApiService.deleteCronJob, {
onSuccess: () => {
mutate(CronJobApiService.getCronJobs.key());
toast.success("Schedule workflow trigger has been deleted.");
},
});

const handleConfirmDelete = async () => {
if (type === "webhook") {
await deleteWebhook.executeAsync(id);
} else {
await deleteCronjob.executeAsync(id);
}
};

const { interceptAsync, dialogProps } = useConfirmationDialog();

return (
<>
<DropdownMenuItem
variant="destructive"
onClick={(e) => {
e.preventDefault();
interceptAsync(handleConfirmDelete);
}}
>
Delete
</DropdownMenuItem>

<ConfirmationDialog
{...dialogProps}
title="Delete Workflow Trigger?"
description={
<div className="flex flex-col">
<TypographyP>
Are you sure you want to delete workflow trigger:{" "}
<b>{name || id}</b>?
</TypographyP>
<TypographyP>The action cannot be undone.</TypographyP>
</div>
}
confirmProps={{
children: "Delete",
variant: "destructive",
}}
/>
</>
);
}
181 changes: 181 additions & 0 deletions ui/admin/app/components/workflow-triggers/ScheduleForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router";
import { $path } from "safe-routes";
import { toast } from "sonner";
import useSWR, { mutate } from "swr";
import { z } from "zod";

import { CronJob } from "~/lib/model/cronjobs";
import { CronJobApiService } from "~/lib/service/api/cronjobApiService";
import { WorkflowService } from "~/lib/service/api/workflowService";

import { TypographyH2 } from "~/components/Typography";
import {
ControlledCustomInput,
ControlledInput,
} from "~/components/form/controlledInputs";
import { Button } from "~/components/ui/button";
import { Form } from "~/components/ui/form";
import { ScrollArea } from "~/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { ScheduleSelection } from "~/components/workflow-triggers/shared/ScheduleSelection";
import { useAsync } from "~/hooks/useAsync";

const formSchema = z.object({
description: z.string(),
workflow: z.string().min(1, "Workflow is required"),
schedule: z.string(),
});

export type ScheduleFormValues = z.infer<typeof formSchema>;

export function ScheduleForm({ cronjob }: { cronjob?: CronJob }) {
const navigate = useNavigate();
const getWorkflows = useSWR(WorkflowService.getWorkflows.key(), () =>
WorkflowService.getWorkflows()
);

const handleSubmitSuccess = () => {
if (cronjob) {
mutate(CronJobApiService.getCronJobById(cronjob.id));
}
mutate(CronJobApiService.getCronJobs.key());
navigate($path("/workflow-triggers"));
};

const createSchedule = useAsync(CronJobApiService.createCronJob, {
onSuccess: handleSubmitSuccess,
onError: () => {
toast.error("Failed to create schedule.");
},
});

const updateSchedule = useAsync(CronJobApiService.updateCronJob, {
onSuccess: handleSubmitSuccess,
onError: () => {
toast.error("Failed to update schedule.");
},
});

const form = useForm<ScheduleFormValues>({
resolver: zodResolver(formSchema),
mode: "onChange",
defaultValues: {
description: cronjob?.description || "",
workflow: cronjob?.workflow || "",
schedule: cronjob?.schedule || "0 * * * *", // default to hourly
},
});

useEffect(() => {
if (cronjob) {
form.reset(cronjob);
}
}, [cronjob, form]);

const handleSubmit = form.handleSubmit((values: ScheduleFormValues) =>
cronjob?.id
? updateSchedule.execute(cronjob.id, values)
: createSchedule.execute(values)
);

const workflows = getWorkflows.data;
const hasCronJob = !!cronjob?.id;
const loading = createSchedule.isLoading || updateSchedule.isLoading;

return (
<ScrollArea className="h-full">
<Form {...form}>
<form
className="space-y-8 p-8 max-w-3xl mx-auto"
onSubmit={handleSubmit}
>
<TypographyH2>
{hasCronJob ? "Edit" : "Create"} Schedule
</TypographyH2>

<ControlledInput
control={form.control}
name="description"
label="Description (Optional)"
/>

<ScheduleSelection
label="Schedule"
onChange={(schedule) => {
form.setValue("schedule", schedule, {
shouldValidate: true,
shouldDirty: true,
});
}}
value={form.watch("schedule")}
/>

<ControlledCustomInput
control={form.control}
name="workflow"
label="Workflow"
description="The workflow that will be called on the interval determined by the schedule set above."
>
{({ field: { ref: _, ...field }, className }) => (
<Select
defaultValue={field.value}
onValueChange={field.onChange}
key={field.value}
>
<SelectTrigger className={className}>
<SelectValue placeholder="Select a workflow" />
</SelectTrigger>

<SelectContent>
{getWorkflowOptions()}
</SelectContent>
</Select>
)}
</ControlledCustomInput>

<Button
className="w-full"
type="submit"
disabled={loading}
loading={loading}
>
{hasCronJob ? "Update" : "Create"} Schedule
</Button>
</form>
</Form>
</ScrollArea>
);

function getWorkflowOptions() {
const workflow = form.watch("workflow");

if (getWorkflows.isLoading)
return (
<SelectItem value={workflow || "loading"} disabled>
Loading workflows...
</SelectItem>
);

if (!workflows?.length)
return (
<SelectItem value={workflow || "empty"} disabled>
No workflows found
</SelectItem>
);

return workflows.map((workflow) => (
<SelectItem key={workflow.id} value={workflow.id}>
{workflow.name}
</SelectItem>
));
}
}
Loading

0 comments on commit 006c683

Please sign in to comment.