Skip to content

Commit

Permalink
[UI v2] feat: Adds flow run button to deployment details page (#17051)
Browse files Browse the repository at this point in the history
  • Loading branch information
devinvillarosa authored Feb 10, 2025
1 parent 26f354b commit b1908af
Show file tree
Hide file tree
Showing 7 changed files with 271 additions and 2 deletions.
3 changes: 2 additions & 1 deletion ui-v2/src/components/deployments/deployment-details-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { DeploymentDetailsTabs } from "./deployment-details-tabs";
import { DeploymentFlowLink } from "./deployment-flow-link";
import { DeploymentMetadata } from "./deployment-metadata";
import { DeploymentTriggers } from "./deployment-triggers";
import { RunFlowButton } from "./run-flow-button";
import { useDeleteDeploymentConfirmationDialog } from "./use-delete-deployment-confirmation-dialog";

type DeploymentDetailsPageProps = {
Expand All @@ -28,7 +29,7 @@ export const DeploymentDetailsPage = ({ id }: DeploymentDetailsPageProps) => {
<DeploymentFlowLink flowId={data.flow_id} />
</div>
<div className="flex align-middle gap-2">
<div className="border border-red-400">{"<RunButton />"}</div>
<RunFlowButton deployment={data} />
<DeploymentActionMenu
id={id}
onDelete={() => confirmDelete(data, { shouldNavigate: true })}
Expand Down
1 change: 1 addition & 0 deletions ui-v2/src/components/deployments/run-flow-button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { RunFlowButton } from "./run-flow-button";
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
reactQueryDecorator,
routerDecorator,
toastDecorator,
} from "@/storybook/utils";
import type { Meta, StoryObj } from "@storybook/react";

import { createFakeAutomation, createFakeDeployment } from "@/mocks";
import { buildApiUrl } from "@tests/utils/handlers";
import { http, HttpResponse } from "msw";
import { RunFlowButton } from "./run-flow-button";

const meta = {
title: "Components/Deployments/RunFlowButton",
component: RunFlowButton,
decorators: [toastDecorator, routerDecorator, reactQueryDecorator],
args: { deployment: createFakeDeployment() },
parameters: {
msw: {
handlers: [
http.post(buildApiUrl("/deployments/:id/create_flow_run"), () => {
return HttpResponse.json(createFakeAutomation());
}),
],
},
},
} satisfies Meta<typeof RunFlowButton>;

export default meta;

export const story: StoryObj = { name: "RunFlowButton" };
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Toaster } from "@/components/ui/toaster";
import { createFakeDeployment, createFakeFlowRun } from "@/mocks";
import { QueryClient } from "@tanstack/react-query";
import {
RouterProvider,
createMemoryHistory,
createRootRoute,
createRouter,
} from "@tanstack/react-router";
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { buildApiUrl, createWrapper, server } from "@tests/utils";
import { http, HttpResponse } from "msw";
import { describe, expect, it } from "vitest";
import { RunFlowButton, type RunFlowButtonProps } from "./run-flow-button";

describe("RunFlowButton", () => {
// Wraps component in test with a Tanstack router provider
const RunFlowButtonRouter = (props: RunFlowButtonProps) => {
const rootRoute = createRootRoute({
component: () => (
<>
<Toaster />
<RunFlowButton {...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("calls quick run option", async () => {
// ------------ Setup
const MOCK_DEPLOYMENT = createFakeDeployment();
const MOCK_FLOW_RUN_RESPONSE = createFakeFlowRun();
server.use(
http.post(buildApiUrl("/deployments/:id/create_flow_run"), () => {
return HttpResponse.json(MOCK_FLOW_RUN_RESPONSE);
}),
);
const user = userEvent.setup();
render(<RunFlowButtonRouter deployment={MOCK_DEPLOYMENT} />, {
wrapper: createWrapper(),
});

// ------------ Act
await user.click(screen.getByRole("button", { name: "Run", hidden: true }));
await user.click(screen.getByRole("menuitem", { name: "Quick run" }));

// ------------ Assert
const list = screen.getByRole("list");
expect(within(list).getByRole("status")).toBeVisible();
expect(
screen.getByRole("button", {
name: /view run/i,
}),
).toBeVisible();
});

it("custom run option is a link with deployment parameters", async () => {
// ------------ Setup
const MOCK_DEPLOYMENT = createFakeDeployment({
id: "0",
parameters: {
// @ts-expect-error Need to update schema type
paramKey: "paramValue",
},
});
const user = userEvent.setup();
render(<RunFlowButtonRouter deployment={MOCK_DEPLOYMENT} />, {
wrapper: createWrapper(),
});

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

await user.click(screen.getByRole("button", { name: "Run" }));

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

// Validates URL has search parameters with deployment parameters
expect(screen.getByRole("link", { name: "Custom run" })).toHaveAttribute(
"href",
"/deployments/deployment/0/run?parameters=%7B%22paramKey%22%3A%22paramValue%22%7D",
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Deployment } from "@/api/deployments";
import { useDeploymentCreateFlowRun } from "@/api/flow-runs";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Icon } from "@/components/ui/icons";
import { useToast } from "@/hooks/use-toast";
import { Link } from "@tanstack/react-router";

const DEPLOYMENT_QUICK_RUN_PAYLOAD = {
state: {
type: "SCHEDULED",
message: "Run from the Prefect UI with defaults",
state_details: {
deferred: false,
untrackable_result: false,
pause_reschedule: false,
},
},
} as const;

export type RunFlowButtonProps = {
deployment: Deployment;
};

export const RunFlowButton = ({ deployment }: RunFlowButtonProps) => {
const { toast } = useToast();
const { createDeploymentFlowRun, isPending } = useDeploymentCreateFlowRun();

const handleClickQuickRun = (id: string) => {
createDeploymentFlowRun(
{
id,
...DEPLOYMENT_QUICK_RUN_PAYLOAD,
},
{
onSuccess: (res) => {
toast({
action: (
<Link to="/runs/flow-run/$id" params={{ id: res.id }}>
<Button size="sm">View run</Button>
</Link>
),
description: (
<p>
<span className="font-bold">{res.name}</span> scheduled to start{" "}
<span className="font-bold">now</span>
</p>
),
});
},
onError: (error) => {
const message =
error.message || "Unknown error while creating flow run.";
console.error(message);
},
},
);
};

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button loading={isPending}>
Run <Icon className="ml-1 h-4 w-4" id="Play" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuGroup>
<DropdownMenuItem onClick={() => handleClickQuickRun(deployment.id)}>
Quick run
</DropdownMenuItem>
<Link
to="/deployments/deployment/$id/run"
params={{ id: deployment.id }}
search={{ parameters: deployment.parameters }}
>
<DropdownMenuItem>Custom run</DropdownMenuItem>
</Link>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};
31 changes: 30 additions & 1 deletion ui-v2/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { Route as ConcurrencyLimitsConcurrencyLimitIdImport } from './routes/con
import { Route as BlocksBlockIdImport } from './routes/blocks/block.$id'
import { Route as AutomationsAutomationIdImport } from './routes/automations/automation.$id'
import { Route as ArtifactsKeyKeyImport } from './routes/artifacts/key.$key'
import { Route as DeploymentsDeploymentIdRunImport } from './routes/deployments/deployment_.$id.run'
import { Route as DeploymentsDeploymentIdEditImport } from './routes/deployments/deployment_.$id.edit'
import { Route as DeploymentsDeploymentIdDuplicateImport } from './routes/deployments/deployment_.$id.duplicate'
import { Route as AutomationsAutomationIdEditImport } from './routes/automations/automation.$id.edit'
Expand Down Expand Up @@ -194,6 +195,15 @@ const ArtifactsKeyKeyRoute = ArtifactsKeyKeyImport.update({
path: '/artifacts/key/$key',
getParentRoute: () => rootRoute,
} as any)

const DeploymentsDeploymentIdRunRoute = DeploymentsDeploymentIdRunImport.update(
{
id: '/deployments/deployment_/$id/run',
path: '/deployments/deployment/$id/run',
getParentRoute: () => rootRoute,
} as any,
)

const DeploymentsDeploymentIdEditRoute =
DeploymentsDeploymentIdEditImport.update({
id: '/deployments/deployment_/$id/edit',
Expand Down Expand Up @@ -422,6 +432,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DeploymentsDeploymentIdEditImport
parentRoute: typeof rootRoute
}
'/deployments/deployment_/$id/run': {
id: '/deployments/deployment_/$id/run'
path: '/deployments/deployment/$id/run'
fullPath: '/deployments/deployment/$id/run'
preLoaderRoute: typeof DeploymentsDeploymentIdRunImport
parentRoute: typeof rootRoute
}
'/work-pools/work-pool/$workPoolName/queue/$workQueueName': {
id: '/work-pools/work-pool/$workPoolName/queue/$workQueueName'
path: '/queue/$workQueueName'
Expand Down Expand Up @@ -507,6 +524,7 @@ export interface FileRoutesByFullPath {
'/automations/automation/$id/edit': typeof AutomationsAutomationIdEditRoute
'/deployments/deployment/$id/duplicate': typeof DeploymentsDeploymentIdDuplicateRoute
'/deployments/deployment/$id/edit': typeof DeploymentsDeploymentIdEditRoute
'/deployments/deployment/$id/run': typeof DeploymentsDeploymentIdRunRoute
'/work-pools/work-pool/$workPoolName/queue/$workQueueName': typeof WorkPoolsWorkPoolWorkPoolNameQueueWorkQueueNameRoute
}

Expand Down Expand Up @@ -538,6 +556,7 @@ export interface FileRoutesByTo {
'/automations/automation/$id/edit': typeof AutomationsAutomationIdEditRoute
'/deployments/deployment/$id/duplicate': typeof DeploymentsDeploymentIdDuplicateRoute
'/deployments/deployment/$id/edit': typeof DeploymentsDeploymentIdEditRoute
'/deployments/deployment/$id/run': typeof DeploymentsDeploymentIdRunRoute
'/work-pools/work-pool/$workPoolName/queue/$workQueueName': typeof WorkPoolsWorkPoolWorkPoolNameQueueWorkQueueNameRoute
}

Expand Down Expand Up @@ -571,6 +590,7 @@ export interface FileRoutesById {
'/automations/automation/$id/edit': typeof AutomationsAutomationIdEditRoute
'/deployments/deployment_/$id/duplicate': typeof DeploymentsDeploymentIdDuplicateRoute
'/deployments/deployment_/$id/edit': typeof DeploymentsDeploymentIdEditRoute
'/deployments/deployment_/$id/run': typeof DeploymentsDeploymentIdRunRoute
'/work-pools/work-pool/$workPoolName/queue/$workQueueName': typeof WorkPoolsWorkPoolWorkPoolNameQueueWorkQueueNameRoute
}

Expand Down Expand Up @@ -605,6 +625,7 @@ export interface FileRouteTypes {
| '/automations/automation/$id/edit'
| '/deployments/deployment/$id/duplicate'
| '/deployments/deployment/$id/edit'
| '/deployments/deployment/$id/run'
| '/work-pools/work-pool/$workPoolName/queue/$workQueueName'
fileRoutesByTo: FileRoutesByTo
to:
Expand Down Expand Up @@ -635,6 +656,7 @@ export interface FileRouteTypes {
| '/automations/automation/$id/edit'
| '/deployments/deployment/$id/duplicate'
| '/deployments/deployment/$id/edit'
| '/deployments/deployment/$id/run'
| '/work-pools/work-pool/$workPoolName/queue/$workQueueName'
id:
| '__root__'
Expand Down Expand Up @@ -666,6 +688,7 @@ export interface FileRouteTypes {
| '/automations/automation/$id/edit'
| '/deployments/deployment_/$id/duplicate'
| '/deployments/deployment_/$id/edit'
| '/deployments/deployment_/$id/run'
| '/work-pools/work-pool/$workPoolName/queue/$workQueueName'
fileRoutesById: FileRoutesById
}
Expand Down Expand Up @@ -695,6 +718,7 @@ export interface RootRouteChildren {
WorkPoolsWorkPoolWorkPoolNameRoute: typeof WorkPoolsWorkPoolWorkPoolNameRouteWithChildren
DeploymentsDeploymentIdDuplicateRoute: typeof DeploymentsDeploymentIdDuplicateRoute
DeploymentsDeploymentIdEditRoute: typeof DeploymentsDeploymentIdEditRoute
DeploymentsDeploymentIdRunRoute: typeof DeploymentsDeploymentIdRunRoute
}

const rootRouteChildren: RootRouteChildren = {
Expand Down Expand Up @@ -724,6 +748,7 @@ const rootRouteChildren: RootRouteChildren = {
WorkPoolsWorkPoolWorkPoolNameRouteWithChildren,
DeploymentsDeploymentIdDuplicateRoute: DeploymentsDeploymentIdDuplicateRoute,
DeploymentsDeploymentIdEditRoute: DeploymentsDeploymentIdEditRoute,
DeploymentsDeploymentIdRunRoute: DeploymentsDeploymentIdRunRoute,
}

export const routeTree = rootRoute
Expand Down Expand Up @@ -759,7 +784,8 @@ export const routeTree = rootRoute
"/runs/task-run/$id",
"/work-pools/work-pool/$workPoolName",
"/deployments/deployment_/$id/duplicate",
"/deployments/deployment_/$id/edit"
"/deployments/deployment_/$id/edit",
"/deployments/deployment_/$id/run"
]
},
"/": {
Expand Down Expand Up @@ -861,6 +887,9 @@ export const routeTree = rootRoute
"/deployments/deployment_/$id/edit": {
"filePath": "deployments/deployment_.$id.edit.tsx"
},
"/deployments/deployment_/$id/run": {
"filePath": "deployments/deployment_.$id.run.tsx"
},
"/work-pools/work-pool/$workPoolName/queue/$workQueueName": {
"filePath": "work-pools/work-pool.$workPoolName.queue.$workQueueName.tsx",
"parent": "/work-pools/work-pool/$workPoolName"
Expand Down
23 changes: 23 additions & 0 deletions ui-v2/src/routes/deployments/deployment_.$id.run.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createFileRoute } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";

// nb: Revisit search params to determine if we're decoding the parameters correctly. Or if there are stricter typings
// We'll know stricter types as we write more of the webapp

/**
* Schema for validating URL search parameters for the create automation page.
* @property actions used designate how to pre-populate the fields
*/
const searchParams = z
.object({ parameters: z.record(z.unknown()).optional() })
.optional();

export const Route = createFileRoute("/deployments/deployment_/$id/run")({
validateSearch: zodValidator(searchParams),
component: RouteComponent,
});

function RouteComponent() {
return "🚧🚧 Pardon our dust! 🚧🚧";
}

0 comments on commit b1908af

Please sign in to comment.