-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[UI v2] feat: Adds deployment schedule list (except for adding end ed…
…iting UX) (#17081)
- Loading branch information
1 parent
704995b
commit 039693f
Showing
13 changed files
with
477 additions
and
3 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
25 changes: 25 additions & 0 deletions
25
ui-v2/src/components/deployments/deployment-schedules/deployment-schedule-item.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,25 @@ | ||
import { Card } from "@/components/ui/card"; | ||
import { Typography } from "@/components/ui/typography"; | ||
|
||
import { getScheduleTitle } from "./get-schedule-title"; | ||
import { ScheduleActionMenu } from "./schedule-action-menu"; | ||
import { ScheduleToggleSwitch } from "./schedule-toggle-switch"; | ||
import type { DeploymentSchedule } from "./types"; | ||
|
||
type DeploymentScheduleItemProps = { | ||
deploymentSchedule: DeploymentSchedule; | ||
}; | ||
|
||
export const DeploymentScheduleItem = ({ | ||
deploymentSchedule, | ||
}: 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} /> | ||
</div> | ||
</Card> | ||
); | ||
}; |
60 changes: 60 additions & 0 deletions
60
ui-v2/src/components/deployments/deployment-schedules/deployment-schedules.stories.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,60 @@ | ||
import { | ||
reactQueryDecorator, | ||
routerDecorator, | ||
toastDecorator, | ||
} from "@/storybook/utils"; | ||
import type { Meta, StoryObj } from "@storybook/react"; | ||
|
||
import { createFakeDeployment } from "@/mocks"; | ||
import { faker } from "@faker-js/faker"; | ||
import { DeploymentSchedules } from "./deployment-schedules"; | ||
|
||
const baseDeploymentSchedule = { | ||
id: faker.string.uuid(), | ||
created: faker.date.recent().toISOString(), | ||
updated: faker.date.recent().toISOString(), | ||
deployment_id: faker.string.uuid(), | ||
active: true, | ||
max_scheduled_runs: null, | ||
}; | ||
|
||
const MOCK_DEPLOYMENT = createFakeDeployment({ | ||
schedules: [ | ||
{ | ||
...baseDeploymentSchedule, | ||
schedule: { | ||
cron: "1 * * * *", | ||
timezone: "UTC", | ||
day_or: true, | ||
}, | ||
}, | ||
{ | ||
...baseDeploymentSchedule, | ||
schedule: { | ||
cron: "1 * * * *", | ||
timezone: "UTC", | ||
day_or: true, | ||
}, | ||
}, | ||
{ | ||
...baseDeploymentSchedule, | ||
schedule: { | ||
rrule: "FREQ=DAILY;COUNT=5", | ||
timezone: "UTC", | ||
}, | ||
}, | ||
], | ||
}); | ||
|
||
const meta = { | ||
title: "Components/Deployments/DeploymentSchedules", | ||
component: DeploymentSchedules, | ||
decorators: [toastDecorator, routerDecorator, reactQueryDecorator], | ||
args: { | ||
deployment: MOCK_DEPLOYMENT, | ||
}, | ||
} satisfies Meta<typeof DeploymentSchedules>; | ||
|
||
export default meta; | ||
|
||
export const story: StoryObj = { name: "DeploymentSchedules" }; |
48 changes: 48 additions & 0 deletions
48
ui-v2/src/components/deployments/deployment-schedules/deployment-schedules.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,48 @@ | ||
import { Deployment } from "@/api/deployments"; | ||
import { Button } from "@/components/ui/button"; | ||
import { Icon } from "@/components/ui/icons"; | ||
import { useMemo } from "react"; | ||
import { DeploymentScheduleItem } from "./deployment-schedule-item"; | ||
|
||
type DeploymentSchedulesProps = { | ||
deployment: Deployment; | ||
}; | ||
|
||
export const DeploymentSchedules = ({ | ||
deployment, | ||
}: DeploymentSchedulesProps) => { | ||
// nb: Need to sort by created, because API re-arranges order per last update | ||
const deploymentSchedulesSorted = useMemo(() => { | ||
if (!deployment.schedules) { | ||
return []; | ||
} | ||
return deployment.schedules.sort((a, b) => { | ||
if (!a.created) { | ||
return -1; | ||
} | ||
if (!b.created) { | ||
return 1; | ||
} | ||
return Date.parse(a.created) - Date.parse(b.created); | ||
}); | ||
}, [deployment.schedules]); | ||
|
||
return ( | ||
<div className="flex flex-col gap-1"> | ||
<div className="text-sm text-muted-foreground">Schedules</div> | ||
<div className="flex flex-col gap-2"> | ||
{deploymentSchedulesSorted.map((schedule) => ( | ||
<DeploymentScheduleItem | ||
key={schedule.id} | ||
deploymentSchedule={schedule} | ||
/> | ||
))} | ||
<div> | ||
<Button size="sm"> | ||
<Icon id="Plus" className="mr-2 h-4 w-4" /> Schedule | ||
</Button> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; |
68 changes: 68 additions & 0 deletions
68
ui-v2/src/components/deployments/deployment-schedules/get-schedule-title.test.ts
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,68 @@ | ||
import { faker } from "@faker-js/faker"; | ||
import { describe, expect, it } from "vitest"; | ||
import { getScheduleTitle } from "./get-schedule-title"; | ||
import { DeploymentSchedule } from "./types"; | ||
|
||
describe("getScheduleTitle()", () => { | ||
const baseDeploymentSchedule = { | ||
id: faker.string.uuid(), | ||
created: faker.date.recent().toISOString(), | ||
updated: faker.date.recent().toISOString(), | ||
deployment_id: faker.string.uuid(), | ||
active: true, | ||
max_scheduled_runs: null, | ||
}; | ||
|
||
it("returns an interval formatted title", () => { | ||
const mockDeploymentSchedule: DeploymentSchedule = { | ||
...baseDeploymentSchedule, | ||
schedule: { | ||
interval: 3600, | ||
anchor_date: faker.date.recent().toISOString(), | ||
timezone: "UTC", | ||
}, | ||
}; | ||
|
||
// TEST | ||
const RESULT = getScheduleTitle(mockDeploymentSchedule); | ||
|
||
// ASSERT | ||
const EXPECTED = "Every 1 hour"; | ||
expect(RESULT).toEqual(EXPECTED); | ||
}); | ||
|
||
it("returns a cron formatted title", () => { | ||
const mockDeploymentSchedule: DeploymentSchedule = { | ||
...baseDeploymentSchedule, | ||
schedule: { | ||
cron: "1 * * * *", | ||
timezone: "UTC", | ||
day_or: true, | ||
}, | ||
}; | ||
|
||
// TEST | ||
const RESULT = getScheduleTitle(mockDeploymentSchedule); | ||
|
||
// ASSERT | ||
const EXPECTED = "At 1 minutes past the hour"; | ||
expect(RESULT).toEqual(EXPECTED); | ||
}); | ||
|
||
it("returns a rrule formatted title", () => { | ||
const mockDeploymentSchedule: DeploymentSchedule = { | ||
...baseDeploymentSchedule, | ||
schedule: { | ||
rrule: "FREQ=DAILY;COUNT=5", | ||
timezone: "UTC", | ||
}, | ||
}; | ||
|
||
// TEST | ||
const RESULT = getScheduleTitle(mockDeploymentSchedule); | ||
|
||
// ASSERT | ||
const EXPECTED = "every day for 5 times"; | ||
expect(RESULT).toEqual(EXPECTED); | ||
}); | ||
}); |
15 changes: 15 additions & 0 deletions
15
ui-v2/src/components/deployments/deployment-schedules/get-schedule-title.ts
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,15 @@ | ||
import cronstrue from "cronstrue"; | ||
import humanizeDuration from "humanize-duration"; | ||
import { rrulestr } from "rrule"; | ||
import type { DeploymentSchedule } from "./types"; | ||
|
||
export const getScheduleTitle = (deploymentSchedule: DeploymentSchedule) => { | ||
const { schedule } = deploymentSchedule; | ||
if ("interval" in schedule) { | ||
return `Every ${humanizeDuration(schedule.interval * 1_000)}`; | ||
} | ||
if ("cron" in schedule) { | ||
return cronstrue.toString(schedule.cron); | ||
} | ||
return rrulestr(schedule.rrule).toText(); | ||
}; |
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 @@ | ||
export { DeploymentSchedules } from "./deployment-schedules"; |
73 changes: 73 additions & 0 deletions
73
ui-v2/src/components/deployments/deployment-schedules/schedule-action-menu.test.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,73 @@ | ||
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 { createWrapper } from "@tests/utils"; | ||
import { ScheduleActionMenu } from "./schedule-action-menu"; | ||
|
||
const MOCK_DEPLOYMENT_SCHEDULE = { | ||
id: faker.string.uuid(), | ||
created: faker.date.recent().toISOString(), | ||
updated: faker.date.recent().toISOString(), | ||
deployment_id: faker.string.uuid(), | ||
active: true, | ||
max_scheduled_runs: null, | ||
schedule: { | ||
cron: "1 * * * *", | ||
timezone: "UTC", | ||
day_or: true, | ||
}, | ||
}; | ||
|
||
describe("ScheduleActionMenu", () => { | ||
it("copies the id", async () => { | ||
// ------------ Setup | ||
const user = userEvent.setup(); | ||
render( | ||
<> | ||
<Toaster /> | ||
<ScheduleActionMenu deploymentSchedule={MOCK_DEPLOYMENT_SCHEDULE} /> | ||
</>, | ||
{ wrapper: createWrapper() }, | ||
); | ||
|
||
// ------------ Act | ||
await user.click( | ||
screen.getByRole("button", { name: /open menu/i, hidden: true }), | ||
); | ||
await user.click(screen.getByRole("menuitem", { name: "Copy ID" })); | ||
|
||
// ------------ Assert | ||
expect(screen.getByText("ID copied")).toBeVisible(); | ||
}); | ||
|
||
it("calls delete option and deletes schedule", async () => { | ||
// ------------ Setup | ||
const user = userEvent.setup(); | ||
render( | ||
<> | ||
<Toaster /> | ||
<ScheduleActionMenu deploymentSchedule={MOCK_DEPLOYMENT_SCHEDULE} /> | ||
</>, | ||
{ wrapper: createWrapper() }, | ||
); | ||
// ------------ Act | ||
|
||
await user.click( | ||
screen.getByRole("button", { name: /open menu/i, hidden: true }), | ||
); | ||
await user.click(screen.getByRole("menuitem", { name: /delete/i })); | ||
|
||
await user.click(screen.getByRole("button", { name: /delete/i })); | ||
|
||
// ------------ Assert | ||
await waitFor(() => | ||
expect(screen.getByText("Schedule deleted")).toBeVisible(), | ||
); | ||
expect( | ||
screen.queryByRole("heading", { name: /delete schedule/i }), | ||
).not.toBeInTheDocument(); | ||
}); | ||
}); |
54 changes: 54 additions & 0 deletions
54
ui-v2/src/components/deployments/deployment-schedules/schedule-action-menu.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,54 @@ | ||
import { Button } from "@/components/ui/button"; | ||
import { DeleteConfirmationDialog } from "@/components/ui/delete-confirmation-dialog"; | ||
import { | ||
DropdownMenu, | ||
DropdownMenuContent, | ||
DropdownMenuItem, | ||
DropdownMenuLabel, | ||
DropdownMenuTrigger, | ||
} from "@/components/ui/dropdown-menu"; | ||
import { Icon } from "@/components/ui/icons"; | ||
import { useToast } from "@/hooks/use-toast"; | ||
import { DeploymentSchedule } from "./types"; | ||
import { useDeleteSchedule } from "./use-delete-schedule"; | ||
|
||
type ScheduleActionMenuProps = { | ||
deploymentSchedule: DeploymentSchedule; | ||
}; | ||
|
||
export const ScheduleActionMenu = ({ | ||
deploymentSchedule, | ||
}: ScheduleActionMenuProps) => { | ||
const { toast } = useToast(); | ||
const [dialogState, confirmDelete] = useDeleteSchedule(); | ||
const handleCopyId = (id: string) => { | ||
void navigator.clipboard.writeText(id); | ||
toast({ title: "ID copied" }); | ||
}; | ||
|
||
const handleEdit = () => {}; | ||
|
||
const handleDelete = () => confirmDelete(deploymentSchedule); | ||
|
||
return ( | ||
<> | ||
<DropdownMenu> | ||
<DropdownMenuTrigger asChild> | ||
<Button variant="outline" className="h-8 w-8 p-0"> | ||
<span className="sr-only">Open menu</span> | ||
<Icon id="MoreVertical" className="h-4 w-4" /> | ||
</Button> | ||
</DropdownMenuTrigger> | ||
<DropdownMenuContent align="end"> | ||
<DropdownMenuLabel>Actions</DropdownMenuLabel> | ||
<DropdownMenuItem onClick={() => handleCopyId(deploymentSchedule.id)}> | ||
Copy ID | ||
</DropdownMenuItem> | ||
<DropdownMenuItem onClick={handleEdit}>Edit</DropdownMenuItem> | ||
<DropdownMenuItem onClick={handleDelete}>Delete</DropdownMenuItem> | ||
</DropdownMenuContent> | ||
</DropdownMenu> | ||
<DeleteConfirmationDialog {...dialogState} /> | ||
</> | ||
); | ||
}; |
Oops, something went wrong.