Skip to content

Commit

Permalink
[UI v2] feat: Start UX for adding and editing a deployment schedule
Browse files Browse the repository at this point in the history
  • Loading branch information
devinvillarosa committed Feb 13, 2025
1 parent 2df84e4 commit bb4d0a0
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 10 deletions.
52 changes: 50 additions & 2 deletions ui-v2/src/components/deployments/deployment-details-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,59 @@ 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<Dialogs | null>(
null,
);
const [scheduleIdToEdit, setScheduleIdToEdit] = useState("");

const { data: deployment } = useSuspenseQuery(
buildDeploymentDetailsQuery(id),
);

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 (
<>
<div className="flex flex-col gap-4">
Expand All @@ -51,7 +80,11 @@ export const DeploymentDetailsPage = ({ id }: DeploymentDetailsPageProps) => {
<DeploymentDetailsTabs deployment={deployment} />
</div>
<div className="flex flex-col gap-3">
<DeploymentSchedules deployment={deployment} />
<DeploymentSchedules
deployment={deployment}
onAddSchedule={handleAddSchedule}
onEditSchedule={handleEditSchedule}
/>

<Suspense fallback={<LoadingSkeleton numSkeletons={2} />}>
<DeploymentTriggers deployment={deployment} />
Expand All @@ -62,6 +95,21 @@ export const DeploymentDetailsPage = ({ id }: DeploymentDetailsPageProps) => {
</div>
</div>
</div>
<DeploymentScheduleDialog
deployment_id={id}
onSubmit={closeDialog}
open={showScheduleDialog === "create"}
onOpenChange={handleOpenChange}
/>
{scheduleToEdit && (
<DeploymentScheduleDialog
deployment_id={id}
onSubmit={closeDialog}
open={showScheduleDialog === "edit"}
onOpenChange={handleOpenChange}
scheduleToEdit={scheduleToEdit}
/>
)}
<DeleteConfirmationDialog {...deleteConfirmationDialogState} />
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ScheduleTypes>("interval");

const SCHEDULE_TAB_OPTIONS = [
{
value: "interval",
label: "Interval",
Component: () => <div>TODO: Interval form</div>,
},
{
value: "cron",
label: "Cron",
Component: () => <div>TODO: Cron Form</div>,
},
{ value: "rrule", label: "RRule", Component: () => <RRuleScheduleForm /> },
] as const;

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>{scheduleToEdit ? "Edit" : "Add"} Schedule</DialogTitle>
</DialogHeader>

<Tabs defaultValue={SCHEDULE_TAB_OPTIONS[0].value} value={scheduleTab}>
<TabsList>
{SCHEDULE_TAB_OPTIONS.map(({ value, label }) => (
<TabsTrigger
key={value}
value={value}
onClick={() => setScheduleTab(value)}
>
{label}
</TabsTrigger>
))}
</TabsList>
{SCHEDULE_TAB_OPTIONS.map(({ value, Component }) => (
<TabsContent key={value} value={value}>
<Component />
</TabsContent>
))}
</Tabs>
</DialogContent>
</Dialog>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DeploymentScheduleDialog } from "./deployment-schedule-dialog";
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col gap-4">
<Typography>
Sorry, modifying RRule schedules via the UI is currently unsupported;
select a different schedule type above or modify your schedule in code.
</Typography>

<DialogFooter>
<DialogTrigger asChild>
<Button variant="outline">Close</Button>
</DialogTrigger>
<Button disabled>Save</Button>
</DialogFooter>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,22 @@ import type { DeploymentSchedule } from "./types";

type DeploymentScheduleItemProps = {
deploymentSchedule: DeploymentSchedule;
onEditSchedule: (scheduleId: string) => void;
};

export const DeploymentScheduleItem = ({
deploymentSchedule,
onEditSchedule,
}: DeploymentScheduleItemProps) => {
return (
<Card className="p-3 flex items-center justify-between">
<Typography>{getScheduleTitle(deploymentSchedule)}</Typography>
<div className="flex items-center gap-2">
<ScheduleToggleSwitch deploymentSchedule={deploymentSchedule} />
<ScheduleActionMenu deploymentSchedule={deploymentSchedule} />
<ScheduleActionMenu
deploymentSchedule={deploymentSchedule}
onEditSchedule={onEditSchedule}
/>
</div>
</Card>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand All @@ -35,10 +39,11 @@ export const DeploymentSchedules = ({
<DeploymentScheduleItem
key={schedule.id}
deploymentSchedule={schedule}
onEditSchedule={onEditSchedule}
/>
))}
<div>
<Button size="sm">
<Button size="sm" onClick={onAddSchedule}>
<Icon id="Plus" className="mr-2 h-4 w-4" /> Schedule
</Button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -28,7 +28,10 @@ describe("ScheduleActionMenu", () => {
render(
<>
<Toaster />
<ScheduleActionMenu deploymentSchedule={MOCK_DEPLOYMENT_SCHEDULE} />
<ScheduleActionMenu
deploymentSchedule={MOCK_DEPLOYMENT_SCHEDULE}
onEditSchedule={vi.fn()}
/>
</>,
{ wrapper: createWrapper() },
);
Expand All @@ -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(
<>
<Toaster />
<ScheduleActionMenu
deploymentSchedule={MOCK_DEPLOYMENT_SCHEDULE}
onEditSchedule={mockOnEditScheduleFn}
/>
</>,
{ 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(
<>
<Toaster />
<ScheduleActionMenu deploymentSchedule={MOCK_DEPLOYMENT_SCHEDULE} />
<ScheduleActionMenu
deploymentSchedule={MOCK_DEPLOYMENT_SCHEDULE}
onEditSchedule={vi.fn()}
/>
</>,
{ wrapper: createWrapper() },
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -26,9 +28,8 @@ export const ScheduleActionMenu = ({
toast({ title: "ID copied" });
};

const handleEdit = () => {};

const handleDelete = () => confirmDelete(deploymentSchedule);
const handleEdit = () => onEditSchedule(deploymentSchedule.id);

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down

0 comments on commit bb4d0a0

Please # to comment.