From e6e6e1464a2eb248b001b0e640bfa73b9fc1c993 Mon Sep 17 00:00:00 2001 From: Devin Villarosa Date: Fri, 3 Jan 2025 15:14:49 -0800 Subject: [PATCH] [UI v2] feat: Adds additional fields for automations select step --- .../action-change-flow-run-state-fields.tsx | 4 +- ...ction-change-flow-run-state-name-field.tsx | 9 +- .../action-step/action-step.stories.tsx | 21 +++ .../action-step/action-step.test.tsx | 149 ++++++++++++++---- .../action-step/action-step.tsx | 67 ++++++-- .../action-type-additional-fields.tsx | 4 +- .../action-step/action-type-schemas.ts | 90 +++++------ .../automations-select-state-fields.tsx | 141 +++++++++++++++++ ui-v2/src/components/ui/combobox/combobox.tsx | 9 +- .../ui/combobox/comboxbox.stories.tsx | 6 +- 10 files changed, 401 insertions(+), 99 deletions(-) create mode 100644 ui-v2/src/components/automations/automations-wizard/action-step/automations-select-state-fields.tsx diff --git a/ui-v2/src/components/automations/automations-wizard/action-step/action-change-flow-run-state-fields/action-change-flow-run-state-fields.tsx b/ui-v2/src/components/automations/automations-wizard/action-step/action-change-flow-run-state-fields/action-change-flow-run-state-fields.tsx index 2386e2b21035..7e552ee0b086 100644 --- a/ui-v2/src/components/automations/automations-wizard/action-step/action-change-flow-run-state-fields/action-change-flow-run-state-fields.tsx +++ b/ui-v2/src/components/automations/automations-wizard/action-step/action-change-flow-run-state-fields/action-change-flow-run-state-fields.tsx @@ -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 = () => ( -
+ <> -
+ ); diff --git a/ui-v2/src/components/automations/automations-wizard/action-step/action-change-flow-run-state-fields/action-change-flow-run-state-name-field.tsx b/ui-v2/src/components/automations/automations-wizard/action-step/action-change-flow-run-state-fields/action-change-flow-run-state-name-field.tsx index e391d77fdedb..989f6590cf2d 100644 --- a/ui-v2/src/components/automations/automations-wizard/action-step/action-change-flow-run-state-fields/action-change-flow-run-state-name-field.tsx +++ b/ui-v2/src/components/automations/automations-wizard/action-step/action-change-flow-run-state-fields/action-change-flow-run-state-name-field.tsx @@ -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({ name: "state" }) as FlowStates; + const form = useFormContext(); + const stateField = form.watch("state"); return ( { diff --git a/ui-v2/src/components/automations/automations-wizard/action-step/action-step.stories.tsx b/ui-v2/src/components/automations/automations-wizard/action-step/action-step.stories.tsx index 419811e12bbd..6704b0962e52 100644 --- a/ui-v2/src/components/automations/automations-wizard/action-step/action-step.stories.tsx +++ b/ui-v2/src/components/automations/automations-wizard/action-step/action-step.stories.tsx @@ -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; diff --git a/ui-v2/src/components/automations/automations-wizard/action-step/action-step.test.tsx b/ui-v2/src/components/automations/automations-wizard/action-step/action-step.test.tsx index 0205238fd228..3ff4483513e6 100644 --- a/ui-v2/src/components/automations/automations-wizard/action-step/action-step.test.tsx +++ b/ui-v2/src/components/automations/automations-wizard/action-step/action-step.test.tsx @@ -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) => { + 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 @@ -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(); + // ------------ Setup + const mockOnSubmitFn = vi.fn(); + render(); - // ------------ 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(); + // ------------ Setup + const mockOnSubmitFn = vi.fn(); + render(); - // ------------ 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(, { + 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(, { + 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(); + }); }); }); diff --git a/ui-v2/src/components/automations/automations-wizard/action-step/action-step.tsx b/ui-v2/src/components/automations/automations-wizard/action-step/action-step.tsx index f91bb7c7e8d2..803fcc394906 100644 --- a/ui-v2/src/components/automations/automations-wizard/action-step/action-step.tsx +++ b/ui-v2/src/components/automations/automations-wizard/action-step/action-step.tsx @@ -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 = { @@ -17,21 +17,64 @@ export const ActionStep = ({ onSubmit }: ActionStepProps) => { const form = useForm>({ 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 (
void form.handleSubmit(onSubmit)(e)}> - - - {form.formState.errors.root?.message} +
+ + + {form.formState.errors.root?.message} +
{/** nb: This button will change once we integrate it with the full wizard */}