Skip to content

Commit

Permalink
[UI v2] feat: Adds component for change-flow-run-state option (#16611)
Browse files Browse the repository at this point in the history
Co-authored-by: Alex Streed <desertaxle@users.noreply.github.com>
  • Loading branch information
devinvillarosa and desertaxle authored Jan 6, 2025
1 parent b8a7988 commit e067622
Show file tree
Hide file tree
Showing 18 changed files with 514 additions and 169 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ActionChangeFlowRunStateMessageField } from "./action-change-flow-run-state-message-field";
import { ActionChangeFlowRunStateNameField } from "./action-change-flow-run-state-name-field";
import { ActionChangeFlowRunStateStateField } from "./action-change-flow-run-state-state-field";

export const ActionChangeFlowRunStateFields = () => (
<div>
<ActionChangeFlowRunStateStateField />
<ActionChangeFlowRunStateNameField />
<ActionChangeFlowRunStateMessageField />
</div>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Textarea } from "@/components/ui/textarea";
import { useFormContext } from "react-hook-form";

export const ActionChangeFlowRunStateMessageField = () => {
const form = useFormContext();
return (
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormLabel>Message</FormLabel>
<FormControl>
<Textarea
{...field}
placeholder="State changed by Automation <id>"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ActionsSchema } from "@/components/automations/automations-wizard/action-step/action-type-schemas";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useFormContext, useWatch } from "react-hook-form";
import { FLOW_STATES, FlowStates } from "./flow-states";

export const ActionChangeFlowRunStateNameField = () => {
const form = useFormContext();
const stateField = useWatch<ActionsSchema>({ name: "state" }) as FlowStates;
return (
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
type="text"
{...field}
placeholder={FLOW_STATES[stateField]}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useFormContext } from "react-hook-form";
import { FLOW_STATES, type FlowStates } from "./flow-states";

export const ActionChangeFlowRunStateStateField = () => {
const form = useFormContext();
return (
<FormField
control={form.control}
name="state"
render={({ field }) => (
<FormItem>
<FormLabel>State</FormLabel>
<FormControl>
<Select {...field} onValueChange={field.onChange}>
<SelectTrigger aria-label="select state">
<SelectValue placeholder="Select state" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Actions</SelectLabel>
{Object.keys(FLOW_STATES).map((key) => (
<SelectItem key={key} value={key}>
{FLOW_STATES[key as FlowStates]}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { components } from "@/api/prefect";

export const FLOW_STATES = {
COMPLETED: "Completed",
RUNNING: "Running",
SCHEDULED: "Scheduled",
PENDING: "Pending",
FAILED: "Failed",
CANCELLED: "Cancelled",
CANCELLING: "Cancelling",
CRASHED: "Crashed",
PAUSED: "Paused",
} as const satisfies Record<
components["schemas"]["StateType"],
Capitalize<Lowercase<components["schemas"]["StateType"]>>
>;
export type FlowStates = keyof typeof FLOW_STATES;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ActionChangeFlowRunStateFields } from "./action-change-flow-run-state-fields";
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Meta, StoryObj } from "@storybook/react";

import { fn } from "@storybook/test";
import { ActionStep } from "./action-step";

const meta = {
title: "Components/Automations/Wizard/ActionStep",
component: ActionStep,
args: { onSubmit: fn() },
} satisfies Meta;

export default meta;

export const story: StoryObj = { name: "ActionStep" };
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { ActionStep } from "./action-step";

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

describe("ActionStep", () => {
beforeAll(() => {
/**
* JSDOM doesn't implement PointerEvent so we need to mock our own implementation
* Default to mouse left click interaction
* https://github.com/radix-ui/primitives/issues/1822
* https://github.com/jsdom/jsdom/pull/2666
*/
class MockPointerEvent extends Event {
button: number;
ctrlKey: boolean;
pointerType: string;

constructor(type: string, props: PointerEventInit) {
super(type, props);
this.button = props.button || 0;
this.ctrlKey = props.ctrlKey || false;
this.pointerType = props.pointerType || "mouse";
}
}
window.PointerEvent = MockPointerEvent as never;
window.HTMLElement.prototype.scrollIntoView = vi.fn();
window.HTMLElement.prototype.releasePointerCapture = vi.fn();
window.HTMLElement.prototype.hasPointerCapture = vi.fn();
});

it("able to select a basic action", async () => {
const user = userEvent.setup();

// ------------ Setup
const mockOnSubmitFn = vi.fn();
render(<ActionStep onSubmit={mockOnSubmitFn} />);

// ------------ Act
await user.click(screen.getByRole("combobox", { name: /select action/i }));
await user.click(screen.getByRole("option", { name: "Cancel a flow run" }));

// ------------ Assert
expect(screen.getAllByText("Cancel a flow run")).toBeTruthy();
});

it("able to configure change flow run's state action", async () => {
const user = userEvent.setup();

// ------------ Setup
const mockOnSubmitFn = vi.fn();
render(<ActionStep onSubmit={mockOnSubmitFn} />);

// ------------ Act
await user.click(screen.getByRole("combobox", { name: /select action/i }));
await user.click(
screen.getByRole("option", { name: "Change flow run's state" }),
);

await user.click(screen.getByRole("combobox", { name: /select state/i }));
await user.click(screen.getByRole("option", { name: "Failed" }));
await user.type(screen.getByPlaceholderText("Failed"), "test name");
await user.type(screen.getByLabelText("Message"), "test message");

// ------------ Assert
expect(screen.getAllByText("Change flow run's state")).toBeTruthy();
expect(screen.getAllByText("Failed")).toBeTruthy();
expect(screen.getByLabelText("Name")).toHaveValue("test name");
expect(screen.getByLabelText("Message")).toHaveValue("test message");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Button } from "@/components/ui/button";
import { Form, FormMessage } from "@/components/ui/form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";

import { ActionTypeAdditionalFields } from "./action-type-additional-fields";
import { ActionsSchema } from "./action-type-schemas";
import { ActionTypeSelect } from "./action-type-select";

type ActionStepProps = {
onSubmit: (schema: ActionsSchema) => void;
};

export const ActionStep = ({ onSubmit }: ActionStepProps) => {
const form = useForm<z.infer<typeof ActionsSchema>>({
resolver: zodResolver(ActionsSchema),
});

// Reset form when changing action type
const watchType = form.watch("type");
useEffect(() => {
const currentActionType = form.getValues("type");
form.reset();
form.setValue("type", currentActionType);
}, [form, watchType]);

return (
<Form {...form}>
<form onSubmit={(e) => void form.handleSubmit(onSubmit)(e)}>
<ActionTypeSelect />
<ActionTypeAdditionalFields actionType={watchType} />
<FormMessage>{form.formState.errors.root?.message}</FormMessage>
{/** nb: This button will change once we integrate it with the full wizard */}
<Button className="mt-2" type="submit">
Validate
</Button>
</form>
</Form>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ActionChangeFlowRunStateFields } from "./action-change-flow-run-state-fields";
import type { ActionsSchema } from "./action-type-schemas";

type ActionTypeAdditionalFieldsProps = {
actionType: ActionsSchema["type"];
};

export const ActionTypeAdditionalFields = ({
actionType,
}: ActionTypeAdditionalFieldsProps) => {
switch (actionType) {
case "change-flow-run-state":
return <ActionChangeFlowRunStateFields />;
case "run-deployment":
case "pause-deployment":
case "resume-deployment":
return <div>TODO Deployment</div>;
case "pause-work-queue":
case "resume-work-queue":
return <div>TODO Work Queue</div>;
case "pause-work-pool":
case "resume-work-pool":
return <div>TODO Work pool</div>;
case "pause-automation":
case "resume-automation":
return <div>TODO Automation</div>;
case "send-notification":
return <div>TODO send notification</div>;
case "cancel-flow-run":
case "suspend-flow-run":
case "resume-flow-run":
default:
return null;
}
};
Loading

0 comments on commit e067622

Please sign in to comment.