From bb4d0a00ec71fecea6cb43ea7219a6fb7c1b3945 Mon Sep 17 00:00:00 2001 From: Devin Villarosa Date: Mon, 10 Feb 2025 15:01:17 -0800 Subject: [PATCH] [UI v2] feat: Start UX for adding and editing a deployment schedule --- .../deployments/deployment-details-page.tsx | 52 +++++++++++++- .../deployment-schedule-dialog.tsx | 70 +++++++++++++++++++ .../deployment-schedule-dialog/index.ts | 1 + .../rrule-schedule-form.tsx | 22 ++++++ .../deployment-schedule-item.tsx | 7 +- .../deployment-schedules.tsx | 7 +- .../schedule-action-menu.test.tsx | 37 +++++++++- .../schedule-action-menu.tsx | 5 +- .../ui/timezone-select/timezone-select.tsx | 2 +- 9 files changed, 193 insertions(+), 10 deletions(-) create mode 100644 ui-v2/src/components/deployments/deployment-schedules/deployment-schedule-dialog/deployment-schedule-dialog.tsx create mode 100644 ui-v2/src/components/deployments/deployment-schedules/deployment-schedule-dialog/index.ts create mode 100644 ui-v2/src/components/deployments/deployment-schedules/deployment-schedule-dialog/rrule-schedule-form.tsx diff --git a/ui-v2/src/components/deployments/deployment-details-page.tsx b/ui-v2/src/components/deployments/deployment-details-page.tsx index f6f2f0b8aae4..5e5d9cf13175 100644 --- a/ui-v2/src/components/deployments/deployment-details-page.tsx +++ b/ui-v2/src/components/deployments/deployment-details-page.tsx @@ -2,23 +2,31 @@ import { buildDeploymentDetailsQuery } from "@/api/deployments"; import { DeleteConfirmationDialog } from "@/components/ui/delete-confirmation-dialog"; import { Skeleton } from "@/components/ui/skeleton"; import { useSuspenseQuery } from "@tanstack/react-query"; -import { Suspense } from "react"; +import { Suspense, useMemo, useState } from "react"; import { DeploymentActionMenu } from "./deployment-action-menu"; import { DeploymentDetailsHeader } from "./deployment-details-header"; import { DeploymentDetailsTabs } from "./deployment-details-tabs"; import { DeploymentFlowLink } from "./deployment-flow-link"; import { DeploymentMetadata } from "./deployment-metadata"; +import { DeploymentScheduleDialog } from "./deployment-schedules/deployment-schedule-dialog"; import { DeploymentSchedules } from "./deployment-schedules/deployment-schedules"; import { DeploymentTriggers } from "./deployment-triggers"; import { RunFlowButton } from "./run-flow-button"; import { useDeleteDeploymentConfirmationDialog } from "./use-delete-deployment-confirmation-dialog"; +type Dialogs = "create" | "edit"; + type DeploymentDetailsPageProps = { id: string; }; export const DeploymentDetailsPage = ({ id }: DeploymentDetailsPageProps) => { + const [showScheduleDialog, setShowScheduleDialog] = useState( + null, + ); + const [scheduleIdToEdit, setScheduleIdToEdit] = useState(""); + const { data: deployment } = useSuspenseQuery( buildDeploymentDetailsQuery(id), ); @@ -26,6 +34,27 @@ export const DeploymentDetailsPage = ({ id }: DeploymentDetailsPageProps) => { const [deleteConfirmationDialogState, confirmDelete] = useDeleteDeploymentConfirmationDialog(); + const scheduleToEdit = useMemo(() => { + if (!deployment.schedules) { + return undefined; + } + return deployment.schedules.find( + (schedule) => schedule.id === scheduleIdToEdit, + ); + }, [deployment.schedules, scheduleIdToEdit]); + + const handleAddSchedule = () => setShowScheduleDialog("create"); + const handleEditSchedule = (scheduleId: string) => { + setScheduleIdToEdit(scheduleId); + setShowScheduleDialog("edit"); + }; + const closeDialog = () => setShowScheduleDialog(null); + const handleOpenChange = (open: boolean) => { + if (!open) { + closeDialog(); + } + }; + return ( <>
@@ -51,7 +80,11 @@ export const DeploymentDetailsPage = ({ id }: DeploymentDetailsPageProps) => {
- + }> @@ -62,6 +95,21 @@ export const DeploymentDetailsPage = ({ id }: DeploymentDetailsPageProps) => {
+ + {scheduleToEdit && ( + + )} ); diff --git a/ui-v2/src/components/deployments/deployment-schedules/deployment-schedule-dialog/deployment-schedule-dialog.tsx b/ui-v2/src/components/deployments/deployment-schedules/deployment-schedule-dialog/deployment-schedule-dialog.tsx new file mode 100644 index 000000000000..777134861edd --- /dev/null +++ b/ui-v2/src/components/deployments/deployment-schedules/deployment-schedule-dialog/deployment-schedule-dialog.tsx @@ -0,0 +1,70 @@ +import type { DeploymentSchedule } from "@/components/deployments/deployment-schedules/types"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +import { useState } from "react"; +import { RRuleScheduleForm } from "./rrule-schedule-form"; + +type ScheduleTypes = "interval" | "cron" | "rrule"; + +type DeploymentScheduleDialogProps = { + onOpenChange: (open: boolean) => void; + open: boolean; + scheduleToEdit?: DeploymentSchedule; +}; + +export const DeploymentScheduleDialog = ({ + onOpenChange, + open, + scheduleToEdit, +}: DeploymentScheduleDialogProps) => { + const [scheduleTab, setScheduleTab] = useState("interval"); + + const SCHEDULE_TAB_OPTIONS = [ + { + value: "interval", + label: "Interval", + Component: () =>
TODO: Interval form
, + }, + { + value: "cron", + label: "Cron", + Component: () =>
TODO: Cron Form
, + }, + { value: "rrule", label: "RRule", Component: () => }, + ] as const; + + return ( + + + + {scheduleToEdit ? "Edit" : "Add"} Schedule + + + + + {SCHEDULE_TAB_OPTIONS.map(({ value, label }) => ( + setScheduleTab(value)} + > + {label} + + ))} + + {SCHEDULE_TAB_OPTIONS.map(({ value, Component }) => ( + + + + ))} + + + + ); +}; diff --git a/ui-v2/src/components/deployments/deployment-schedules/deployment-schedule-dialog/index.ts b/ui-v2/src/components/deployments/deployment-schedules/deployment-schedule-dialog/index.ts new file mode 100644 index 000000000000..d31c25dbd4a6 --- /dev/null +++ b/ui-v2/src/components/deployments/deployment-schedules/deployment-schedule-dialog/index.ts @@ -0,0 +1 @@ +export { DeploymentScheduleDialog } from "./deployment-schedule-dialog"; diff --git a/ui-v2/src/components/deployments/deployment-schedules/deployment-schedule-dialog/rrule-schedule-form.tsx b/ui-v2/src/components/deployments/deployment-schedules/deployment-schedule-dialog/rrule-schedule-form.tsx new file mode 100644 index 000000000000..29105894172b --- /dev/null +++ b/ui-v2/src/components/deployments/deployment-schedules/deployment-schedule-dialog/rrule-schedule-form.tsx @@ -0,0 +1,22 @@ +import { Button } from "@/components/ui/button"; +import { DialogFooter } from "@/components/ui/dialog"; +import { Typography } from "@/components/ui/typography"; +import { DialogTrigger } from "@radix-ui/react-dialog"; + +export const RRuleScheduleForm = () => { + return ( +
+ + Sorry, modifying RRule schedules via the UI is currently unsupported; + select a different schedule type above or modify your schedule in code. + + + + + + + + +
+ ); +}; diff --git a/ui-v2/src/components/deployments/deployment-schedules/deployment-schedule-item.tsx b/ui-v2/src/components/deployments/deployment-schedules/deployment-schedule-item.tsx index d5a5057a2cd9..810a3fcc432d 100644 --- a/ui-v2/src/components/deployments/deployment-schedules/deployment-schedule-item.tsx +++ b/ui-v2/src/components/deployments/deployment-schedules/deployment-schedule-item.tsx @@ -8,17 +8,22 @@ import type { DeploymentSchedule } from "./types"; type DeploymentScheduleItemProps = { deploymentSchedule: DeploymentSchedule; + onEditSchedule: (scheduleId: string) => void; }; export const DeploymentScheduleItem = ({ deploymentSchedule, + onEditSchedule, }: DeploymentScheduleItemProps) => { return ( {getScheduleTitle(deploymentSchedule)}
- +
); diff --git a/ui-v2/src/components/deployments/deployment-schedules/deployment-schedules.tsx b/ui-v2/src/components/deployments/deployment-schedules/deployment-schedules.tsx index 27f23c9c5988..ec2fabb7fbb0 100644 --- a/ui-v2/src/components/deployments/deployment-schedules/deployment-schedules.tsx +++ b/ui-v2/src/components/deployments/deployment-schedules/deployment-schedules.tsx @@ -5,11 +5,15 @@ import { useMemo } from "react"; import { DeploymentScheduleItem } from "./deployment-schedule-item"; type DeploymentSchedulesProps = { + onAddSchedule: () => void; + onEditSchedule: (scheduleId: string) => void; deployment: Deployment; }; export const DeploymentSchedules = ({ deployment, + onAddSchedule, + onEditSchedule, }: DeploymentSchedulesProps) => { // nb: Need to sort by created, because API re-arranges order per last update const deploymentSchedulesSorted = useMemo(() => { @@ -35,10 +39,11 @@ export const DeploymentSchedules = ({ ))}
-
diff --git a/ui-v2/src/components/deployments/deployment-schedules/schedule-action-menu.test.tsx b/ui-v2/src/components/deployments/deployment-schedules/schedule-action-menu.test.tsx index b95bb02c69bb..99560ff8552d 100644 --- a/ui-v2/src/components/deployments/deployment-schedules/schedule-action-menu.test.tsx +++ b/ui-v2/src/components/deployments/deployment-schedules/schedule-action-menu.test.tsx @@ -2,7 +2,7 @@ import { Toaster } from "@/components/ui/toaster"; import { faker } from "@faker-js/faker"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { createWrapper } from "@tests/utils"; import { ScheduleActionMenu } from "./schedule-action-menu"; @@ -28,7 +28,10 @@ describe("ScheduleActionMenu", () => { render( <> - + , { wrapper: createWrapper() }, ); @@ -43,13 +46,41 @@ describe("ScheduleActionMenu", () => { expect(screen.getByText("ID copied")).toBeVisible(); }); + it("calls edit option", async () => { + // ------------ Setup + const user = userEvent.setup(); + const mockOnEditScheduleFn = vi.fn(); + render( + <> + + + , + { wrapper: createWrapper() }, + ); + // ------------ Act + + await user.click( + screen.getByRole("button", { name: /open menu/i, hidden: true }), + ); + await user.click(screen.getByRole("menuitem", { name: /edit/i })); + + // ------------ Assert + expect(mockOnEditScheduleFn).toBeCalledWith(MOCK_DEPLOYMENT_SCHEDULE.id); + }); + it("calls delete option and deletes schedule", async () => { // ------------ Setup const user = userEvent.setup(); render( <> - + , { wrapper: createWrapper() }, ); diff --git a/ui-v2/src/components/deployments/deployment-schedules/schedule-action-menu.tsx b/ui-v2/src/components/deployments/deployment-schedules/schedule-action-menu.tsx index 7cc1b1d10e00..2c678178c9ae 100644 --- a/ui-v2/src/components/deployments/deployment-schedules/schedule-action-menu.tsx +++ b/ui-v2/src/components/deployments/deployment-schedules/schedule-action-menu.tsx @@ -14,10 +14,12 @@ import { useDeleteSchedule } from "./use-delete-schedule"; type ScheduleActionMenuProps = { deploymentSchedule: DeploymentSchedule; + onEditSchedule: (scheduleId: string) => void; }; export const ScheduleActionMenu = ({ deploymentSchedule, + onEditSchedule, }: ScheduleActionMenuProps) => { const { toast } = useToast(); const [dialogState, confirmDelete] = useDeleteSchedule(); @@ -26,9 +28,8 @@ export const ScheduleActionMenu = ({ toast({ title: "ID copied" }); }; - const handleEdit = () => {}; - const handleDelete = () => confirmDelete(deploymentSchedule); + const handleEdit = () => onEditSchedule(deploymentSchedule.id); return ( <> diff --git a/ui-v2/src/components/ui/timezone-select/timezone-select.tsx b/ui-v2/src/components/ui/timezone-select/timezone-select.tsx index c6c4cac8a65f..7c6ca1e1a50f 100644 --- a/ui-v2/src/components/ui/timezone-select/timezone-select.tsx +++ b/ui-v2/src/components/ui/timezone-select/timezone-select.tsx @@ -38,7 +38,7 @@ const TIMEZONES = Intl.supportedValuesOf("timeZone") const ALL_TIMEZONES = [...SUGGESTED_TIMEZONES, ...TIMEZONES]; type TimezoneSelectProps = { - selectedValue: string | undefined; + selectedValue: string | undefined | null; onSelect: (value: string) => void; };