Skip to content

Commit

Permalink
[UI v2] feat: Adds additional fields for automations select step
Browse files Browse the repository at this point in the history
  • Loading branch information
devinvillarosa committed Jan 10, 2025
1 parent e299e5a commit e6e6e14
Show file tree
Hide file tree
Showing 10 changed files with 401 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { ActionChangeFlowRunStateNameField } from "./action-change-flow-run-stat
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
Expand Up @@ -7,12 +7,12 @@ import {
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";
import { useFormContext } from "react-hook-form";
import { FLOW_STATES } from "./flow-states";

export const ActionChangeFlowRunStateNameField = () => {
const form = useFormContext();
const stateField = useWatch<ActionsSchema>({ name: "state" }) as FlowStates;
const form = useFormContext<ActionsSchema>();
const stateField = form.watch("state");
return (
<FormField
control={form.control}
Expand All @@ -24,6 +24,7 @@ export const ActionChangeFlowRunStateNameField = () => {
<Input
type="text"
{...field}
value={field.value ?? ""}
placeholder={FLOW_STATES[stateField]}
/>
</FormControl>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
import type { Meta, StoryObj } from "@storybook/react";

import { createFakeAutomation } from "@/mocks";
import { reactQueryDecorator } from "@/storybook/utils";
import { fn } from "@storybook/test";
import { buildApiUrl } from "@tests/utils/handlers";
import { http, HttpResponse } from "msw";
import { ActionStep } from "./action-step";

const MOCK_DATA = [
createFakeAutomation(),
createFakeAutomation(),
createFakeAutomation(),
createFakeAutomation(),
];

const meta = {
title: "Components/Automations/Wizard/ActionStep",
component: ActionStep,
args: { onSubmit: fn() },
decorators: [reactQueryDecorator],
parameters: {
msw: {
handlers: [
http.post(buildApiUrl("/automations/filter"), () => {
return HttpResponse.json(MOCK_DATA);
}),
],
},
},
} satisfies Meta;

export default meta;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import { createFakeAutomation } from "@/mocks";
import { ActionStep } from "./action-step";

import { Automation } from "@/api/automations";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { buildApiUrl, createWrapper, server } from "@tests/utils";
import { http, HttpResponse } from "msw";
import { beforeAll, describe, expect, it, vi } from "vitest";

describe("ActionStep", () => {
const mockListAutomationAPI = (automations: Array<Automation>) => {
server.use(
http.post(buildApiUrl("/automations/filter"), () => {
return HttpResponse.json(automations);
}),
);
};

beforeAll(() => {
/**
* JSDOM doesn't implement PointerEvent so we need to mock our own implementation
Expand All @@ -30,43 +42,122 @@ describe("ActionStep", () => {
window.HTMLElement.prototype.hasPointerCapture = vi.fn();
});

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

// ------------ Setup
const mockOnSubmitFn = vi.fn();
render(<ActionStep onSubmit={mockOnSubmitFn} />);
// ------------ 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" }));
// ------------ 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();
// ------------ Assert
expect(screen.getAllByText("Cancel a flow run")).toBeTruthy();
});
});

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

// ------------ Setup
const mockOnSubmitFn = vi.fn();
render(<ActionStep onSubmit={mockOnSubmitFn} />);
// ------------ 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" }),
);
// ------------ 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");
});
});

describe("action type -- automation", () => {
it("able to configure pause an automation action type", async () => {
mockListAutomationAPI([
createFakeAutomation({ name: "my automation 0" }),
createFakeAutomation({ name: "my automation 1" }),
]);

const user = userEvent.setup();

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

// ------------ Act
await user.click(
screen.getByRole("combobox", { name: /select action/i }),
);
await user.click(
screen.getByRole("option", { name: "Pause an automation" }),
);

expect(screen.getAllByText("Infer Automation")).toBeTruthy();
await user.click(
screen.getByRole("combobox", { name: /select automation to pause/i }),
);

await user.click(screen.getByRole("option", { name: "my automation 0" }));

// ------------ Assert
expect(screen.getAllByText("Pause an automation")).toBeTruthy();
expect(screen.getAllByText("my automation 0")).toBeTruthy();
});

it("able to configure resume an automation action type", async () => {
mockListAutomationAPI([
createFakeAutomation({ name: "my automation 0" }),
createFakeAutomation({ name: "my automation 1" }),
]);
const user = userEvent.setup();

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

// ------------ Act
await user.click(
screen.getByRole("combobox", { name: /select action/i }),
);
await user.click(
screen.getByRole("option", { name: "Resume an automation" }),
);

expect(screen.getAllByText("Infer Automation")).toBeTruthy();
await user.click(
screen.getByRole("combobox", { name: /select automation to resume/i }),
);

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");
await user.click(screen.getByRole("option", { name: "my automation 1" }));

// ------------ 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");
// ------------ Assert
expect(screen.getAllByText("Pause an automation")).toBeTruthy();
expect(screen.getAllByText("my automation 1")).toBeTruthy();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
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 { useEffect } from "react";
import { ActionTypeAdditionalFields } from "./action-type-additional-fields";
import { ActionsSchema } from "./action-type-schemas";
import { ActionsSchema, UNASSIGNED } from "./action-type-schemas";
import { ActionTypeSelect } from "./action-type-select";

type ActionStepProps = {
Expand All @@ -17,21 +17,64 @@ 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");
const type = form.watch("type");
// reset form values based on selected action type
useEffect(() => {
const currentActionType = form.getValues("type");
form.reset();
form.setValue("type", currentActionType);
}, [form, watchType]);
switch (type) {
case "change-flow-run-state":
form.reset({ type });
break;
case "run-deployment":
form.reset({ type, deployment_id: UNASSIGNED });
break;
case "pause-deployment":
case "resume-deployment":
form.reset({
type,
deployment_id: UNASSIGNED,
});
break;
case "pause-work-queue":
case "resume-work-queue":
form.reset({
type,
work_queue_id: UNASSIGNED,
});
break;
case "pause-work-pool":
case "resume-work-pool":
form.reset({
type,
work_pool_id: UNASSIGNED,
});
break;
case "pause-automation":
case "resume-automation":
form.reset({
type,
automation_id: UNASSIGNED,
});
break;
case "send-notification":
form.reset({ type });
break;
case "cancel-flow-run":
case "suspend-flow-run":
case "resume-flow-run":
default:
form.reset({ type });
break;
}
}, [form, type]);

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

type ActionTypeAdditionalFieldsProps = {
actionType: ActionsSchema["type"];
Expand All @@ -22,8 +23,9 @@ export const ActionTypeAdditionalFields = ({
case "resume-work-pool":
return <div>TODO Work pool</div>;
case "pause-automation":
return <AutomationsSelectStateFields action="Pause" />;
case "resume-automation":
return <div>TODO Automation</div>;
return <AutomationsSelectStateFields action="Resume" />;
case "send-notification":
return <div>TODO send notification</div>;
case "cancel-flow-run":
Expand Down
Loading

0 comments on commit e6e6e14

Please sign in to comment.