-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
enhance: Webhooks page to Workflow Triggers
- Loading branch information
1 parent
51275aa
commit 006c683
Showing
19 changed files
with
693 additions
and
228 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 69 additions & 0 deletions
69
ui/admin/app/components/workflow-triggers/CreateWorkflowTrigger.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
77 changes: 77 additions & 0 deletions
77
ui/admin/app/components/workflow-triggers/DeleteWorkflowTrigger.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
181
ui/admin/app/components/workflow-triggers/ScheduleForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
)); | ||
} | ||
} |
Oops, something went wrong.