Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UI v2] feat: Adds Deployment Action Menu component #17015

Merged
merged 1 commit into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading