Skip to content

Commit

Permalink
[UI v2] feat: Adds Deployment Action Menu component (#17015)
Browse files Browse the repository at this point in the history
  • Loading branch information
devinvillarosa authored Feb 7, 2025
1 parent 57635cf commit a84945e
Show file tree
Hide file tree
Showing 8 changed files with 284 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { routerDecorator, toastDecorator } from "@/storybook/utils";
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";

import { DeploymentActionMenu } from "./deployment-action-menu";

const meta = {
title: "Components/Deployments/DeploymentActionMenu",
component: DeploymentActionMenu,
decorators: [toastDecorator, routerDecorator],
args: {
id: "my-id",
onDelete: fn(),
},
} satisfies Meta<typeof DeploymentActionMenu>;

export default meta;

export const story: StoryObj = { name: "DeploymentActionMenu" };
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Toaster } from "@/components/ui/toaster";

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";

import { QueryClient } from "@tanstack/react-query";
import {
RouterProvider,
createMemoryHistory,
createRootRoute,
createRouter,
} from "@tanstack/react-router";
import {
DeploymentActionMenu,
type DeploymentActionMenuProps,
} from "./deployment-action-menu";

describe("DeploymentActionMenu", () => {
// Wraps component in test with a Tanstack router provider
const DeploymentActionMenuRouter = (props: DeploymentActionMenuProps) => {
const rootRoute = createRootRoute({
component: () => <DeploymentActionMenu {...props} />,
});

const router = createRouter({
routeTree: rootRoute,
history: createMemoryHistory({
initialEntries: ["/"],
}),
context: { queryClient: new QueryClient() },
});
// @ts-expect-error - Type error from using a test router
return <RouterProvider router={router} />;
};

it("copies the id", async () => {
// ------------ Setup
const user = userEvent.setup();
render(
<>
<Toaster />
<DeploymentActionMenuRouter id="my-id" onDelete={vi.fn()} />
</>,
);

// ------------ 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 ", async () => {
// ------------ Setup
const user = userEvent.setup();
const mockOnDeleteFn = vi.fn();

render(<DeploymentActionMenuRouter id="my-id" onDelete={mockOnDeleteFn} />);

// ------------ Act

await user.click(
screen.getByRole("button", { name: /open menu/i, hidden: true }),
);
await user.click(screen.getByRole("menuitem", { name: /delete/i }));

// ------------ Assert
expect(mockOnDeleteFn).toHaveBeenCalledOnce();
});

it("edit option is visible", async () => {
const user = userEvent.setup();

// ------------ Setup
render(<DeploymentActionMenuRouter id="my-id" onDelete={vi.fn()} />);

// ------------ Act

await user.click(
screen.getByRole("button", { name: /open menu/i, hidden: true }),
);

// ------------ Assert
expect(screen.getByRole("menuitem", { name: /edit/i })).toBeVisible();
});

it("duplicate option is visible", async () => {
const user = userEvent.setup();

// ------------ Setup
render(<DeploymentActionMenuRouter id="my-id" onDelete={vi.fn()} />);

// ------------ Act

await user.click(
screen.getByRole("button", { name: /open menu/i, hidden: true }),
);

// ------------ Assert
expect(screen.getByRole("menuitem", { name: /duplicate/i })).toBeVisible();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Icon } from "@/components/ui/icons";
import { useToast } from "@/hooks/use-toast";
import { Link } from "@tanstack/react-router";

export type DeploymentActionMenuProps = {
id: string;
onDelete: () => void;
};

export const DeploymentActionMenu = ({
id,
onDelete,
}: DeploymentActionMenuProps) => {
const { toast } = useToast();

const handleCopyId = (_id: string) => {
void navigator.clipboard.writeText(_id);
toast({ title: "ID copied" });
};

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(id)}>
Copy ID
</DropdownMenuItem>
<Link to="/deployments/deployment/$id/edit" params={{ id }}>
<DropdownMenuItem>Edit</DropdownMenuItem>
</Link>
<DropdownMenuItem onClick={onDelete}>Delete</DropdownMenuItem>
<Link to="/deployments/deployment/$id/duplicate" params={{ id }}>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
</Link>
</DropdownMenuContent>
</DropdownMenu>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DeploymentActionMenu } from "./deployment-action-menu";
53 changes: 33 additions & 20 deletions ui-v2/src/components/deployments/deployment-details-page.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,54 @@
import { buildDeploymentDetailsQuery } from "@/api/deployments";
import { DeleteConfirmationDialog } from "@/components/ui/delete-confirmation-dialog";
import { useSuspenseQuery } from "@tanstack/react-query";

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 { useDeleteDeploymentConfirmationDialog } from "./use-delete-deployment-confirmation-dialog";

type DeploymentDetailsPageProps = {
id: string;
};

export const DeploymentDetailsPage = ({ id }: DeploymentDetailsPageProps) => {
const { data } = useSuspenseQuery(buildDeploymentDetailsQuery(id));
const [deleteConfirmationDialogState, confirmDelete] =
useDeleteDeploymentConfirmationDialog();

return (
<div className="flex flex-col gap-4">
<div className="flex align-middle justify-between">
<div className="flex flex-col gap-2">
<DeploymentDetailsHeader deployment={data} />
<DeploymentFlowLink flowId={data.flow_id} />
<>
<div className="flex flex-col gap-4">
<div className="flex align-middle justify-between">
<div className="flex flex-col gap-2">
<DeploymentDetailsHeader deployment={data} />
<DeploymentFlowLink flowId={data.flow_id} />
</div>
<div className="flex align-middle gap-2">
<div className="border border-red-400">{"<RunButton />"}</div>
<DeploymentActionMenu
id={id}
onDelete={() => confirmDelete(data, { shouldNavigate: true })}
/>
</div>
</div>
<div className="flex align-middle gap-2">
<div className="border border-red-400">{"<RunButton />"}</div>
<div className="border border-red-400">{"<Actions />"}</div>
<div className="grid gap-4" style={{ gridTemplateColumns: "3fr 1fr" }}>
<div className="flex flex-col gap-5">
<DeploymentDetailsTabs />
</div>
<div className="flex flex-col gap-3">
<div className="border border-red-400">
{"<SchedulesSection />"}
</div>
<div className="border border-red-400">{"<TriggerSection />"}</div>
<hr />
<DeploymentMetadata deployment={data} />
</div>
</div>
</div>
<div className="grid gap-4" style={{ gridTemplateColumns: "3fr 1fr" }}>
<div className="flex flex-col gap-5">
<DeploymentDetailsTabs />
</div>
<div className="flex flex-col gap-3">
<div className="border border-red-400">{"<SchedulesSection />"}</div>
<div className="border border-red-400">{"<TriggerSection />"}</div>
<hr />
<DeploymentMetadata deployment={data} />
</div>
</div>
</div>
<DeleteConfirmationDialog {...deleteConfirmationDialogState} />
</>
);
};
Loading

0 comments on commit a84945e

Please sign in to comment.