Skip to content

Commit

Permalink
[UI v2] feat: Adds automations mutations
Browse files Browse the repository at this point in the history
  • Loading branch information
devinvillarosa committed Jan 3, 2025
1 parent 488e7b4 commit 04a9bdf
Show file tree
Hide file tree
Showing 3 changed files with 297 additions and 7 deletions.
169 changes: 164 additions & 5 deletions ui-v2/src/hooks/automations.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { renderHook, waitFor } from "@testing-library/react";
import { QueryClient, useSuspenseQuery } from "@tanstack/react-query";
import { act, renderHook, waitFor } from "@testing-library/react";
import { createWrapper, server } from "@tests/utils";
import { http, HttpResponse } from "msw";
import { describe, expect, it } from "vitest";
Expand All @@ -10,12 +10,16 @@ import {
type Automation,
buildGetAutomationQuery,
buildListAutomationsQuery,
queryKeyFactory,
useCreateAutomation,
useDeleteAutomation,
useUpdateAutomation,
} from "./automations";

describe("automations queries", () => {
describe("automations queries and mutations", () => {
const seedAutomationsData = () => [
createFakeAutomation(),
createFakeAutomation(),
createFakeAutomation({ id: "0" }),
createFakeAutomation({ id: "1" }),
];

const mockFetchListAutomationsAPI = (automations: Array<Automation>) => {
Expand All @@ -35,6 +39,7 @@ describe("automations queries", () => {
};

const filter = { sort: "CREATED_DESC", offset: 0 } as const;

it("is stores automation list data", async () => {
// ------------ Mock API requests when cache is empty
const mockList = seedAutomationsData();
Expand Down Expand Up @@ -67,4 +72,158 @@ describe("automations queries", () => {
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toEqual(mockData);
});

it("useDeleteAutomation is able to call delete API and fetches updated cache", async () => {
const ID_TO_DELETE = "0";
const queryClient = new QueryClient();

// ------------ Mock API requests after queries are invalidated
const mockData = seedAutomationsData().filter(
(automation) => automation.id !== ID_TO_DELETE,
);
mockFetchListAutomationsAPI(mockData);

// ------------ Initialize cache
queryClient.setQueryData(
queryKeyFactory.list(filter),
seedAutomationsData(),
);

// ------------ Initialize hooks to test
const { result: useDeleteAutomationResult } = renderHook(
useDeleteAutomation,
{ wrapper: createWrapper({ queryClient }) },
);

const { result: useListAutomationsResult } = renderHook(
() => useSuspenseQuery(buildListAutomationsQuery()),
{ wrapper: createWrapper({ queryClient }) },
);

// ------------ Invoke mutation
act(() => useDeleteAutomationResult.current.deleteAutomation(ID_TO_DELETE));

// ------------ Assert
await waitFor(() =>
expect(useDeleteAutomationResult.current.isSuccess).toBe(true),
);
expect(useListAutomationsResult.current.data).toHaveLength(1);
});

it("useCreateAutomation() invalidates cache and fetches updated value", async () => {
const queryClient = new QueryClient();
const MOCK_NEW_DATA_ID = "2";
const NEW_AUTOMATION_DATA = createFakeAutomation({
id: MOCK_NEW_DATA_ID,
created: "2021-01-01T00:00:00Z",
updated: "2021-01-01T00:00:00Z",
});

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { id, created, updated, ...CREATE_AUTOMATION_PAYLOAD } =
NEW_AUTOMATION_DATA;

// ------------ Mock API requests after queries are invalidated
const mockData = [...seedAutomationsData(), NEW_AUTOMATION_DATA];
mockFetchListAutomationsAPI(mockData);

// ------------ Initialize cache
queryClient.setQueryData(
queryKeyFactory.list(filter),
seedAutomationsData(),
);

// ------------ Initialize hooks to test
const { result: useCreateAutomationResult } = renderHook(
useCreateAutomation,
{ wrapper: createWrapper({ queryClient }) },
);

const { result: useListAutomationsResult } = renderHook(
() => useSuspenseQuery(buildListAutomationsQuery(filter)),
{ wrapper: createWrapper({ queryClient }) },
);

// ------------ Invoke mutation
act(() =>
useCreateAutomationResult.current.createAutomation(
CREATE_AUTOMATION_PAYLOAD,
),
);

// ------------ Assert
await waitFor(() =>
expect(useCreateAutomationResult.current.isSuccess).toBe(true),
);

expect(useListAutomationsResult.current.data).toHaveLength(3);

const newAutomation = useListAutomationsResult.current.data?.find(
(automation) => automation.id === MOCK_NEW_DATA_ID,
);
expect(newAutomation).toMatchObject(NEW_AUTOMATION_DATA);
});

it("useUpdateAutomation() invalidates cache and fetches updated value", async () => {
const queryClient = new QueryClient();
const MOCK_INITIAL_DATA = seedAutomationsData();
const MOCK_EDIT_ID = "1";
const EDITED_AUTOMATION = MOCK_INITIAL_DATA.find(
(automation) => automation.id === MOCK_EDIT_ID,
);
if (!EDITED_AUTOMATION) {
throw new Error("Expected 'EDITED_AUTOMATION'");
}
EDITED_AUTOMATION.name = "Edited automation name";

const MOCK_EDITED_DATA = MOCK_INITIAL_DATA.map((automation) =>
automation.id === MOCK_EDIT_ID ? EDITED_AUTOMATION : automation,
);

// ------------ Mock API requests after queries are invalidated
mockFetchListAutomationsAPI(MOCK_EDITED_DATA);

// ------------ Initialize cache
queryClient.setQueryData(
queryKeyFactory.list(filter),
seedAutomationsData(),
);

// ------------ Initialize hooks to test
const { result: useUpdateAutomationResult } = renderHook(
useUpdateAutomation,
{
wrapper: createWrapper({ queryClient }),
},
);

const { result: useListAutomationsResult } = renderHook(
() => useSuspenseQuery(buildListAutomationsQuery(filter)),
{ wrapper: createWrapper({ queryClient }) },
);

// ------------ Invoke mutation
const { id, actions, name, description, enabled, trigger } =
EDITED_AUTOMATION;
act(() =>
useUpdateAutomationResult.current.updateAutomation({
id,
name,
description,
enabled,
trigger,
actions,
}),
);

// ------------ Assert
await waitFor(() =>
expect(useUpdateAutomationResult.current.isSuccess).toBe(true),
);

const editedAutomation = useListAutomationsResult.current.data?.find(
(automation) => automation.id === MOCK_EDIT_ID,
);
expect(editedAutomation).toMatchObject(EDITED_AUTOMATION);
});
});
133 changes: 132 additions & 1 deletion ui-v2/src/hooks/automations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { components } from "@/api/prefect";
import { getQueryService } from "@/api/service";
import { queryOptions } from "@tanstack/react-query";
import {
queryOptions,
useMutation,
useQueryClient,
} from "@tanstack/react-query";

export type Automation = components["schemas"]["Automation"];
export type AutomationsFilter =
Expand Down Expand Up @@ -58,3 +62,130 @@ export const buildGetAutomationQuery = (id: string) =>
return res.data;
},
});

// ----- ✍🏼 Mutations 🗄️
// ----------------------------

/**
* Hook for deleting an automation
*
* @returns Mutation object for deleting an automation with loading/error states and trigger function
*
* @example
* ```ts
* const { deleteAutomation } = useDeleteAutomation();
*
* // Delete an automation by id
* deleteAutomation('id-to-delete', {
* onSuccess: () => {
* // Handle successful deletion
* },
* onError: (error) => {
* console.error('Failed to delete automation:', error);
* }
* });
* ```
*/
export const useDeleteAutomation = () => {
const queryClient = useQueryClient();
const { mutate: deleteAutomation, ...rest } = useMutation({
mutationFn: (id: string) =>
getQueryService().DELETE("/automations/{id}", {
params: { path: { id } },
}),
onSuccess: () => {
// After a successful deletion, invalidate the listing queries only to refetch
return queryClient.invalidateQueries({
queryKey: queryKeyFactory.lists(),
});
},
});
return {
deleteAutomation,
...rest,
};
};

/**
* Hook for creating a new automation
*
* @returns Mutation object for creating an automation with loading/error states and trigger function
*
* @example
* ```ts
* const { createAutomation, isLoading } = useCreateAutomation();
*
* createAutomation(newAutomation, {
* onSuccess: () => {
* // Handle successful creation
* console.log('Automation created successfully');
* },
* onError: (error) => {
* // Handle error
* console.error('Failed to create automation:', error);
* }
* });
* ```
*/
export const useCreateAutomation = () => {
const queryClient = useQueryClient();
const { mutate: createAutomation, ...rest } = useMutation({
mutationFn: (body: components["schemas"]["AutomationCreate"]) =>
getQueryService().POST("/automations/", { body }),
onSuccess: () => {
// After a successful creation, invalidate the listing queries only to refetch
return queryClient.invalidateQueries({
queryKey: queryKeyFactory.lists(),
});
},
});
return {
createAutomation,
...rest,
};
};

/**
* Hook for editing an automation based on ID
*
* @returns Mutation object for editing an automation with loading/error states and trigger function
*
* @example
* ```ts
* const { updateAutomation, isLoading } = useUpdateAutomation();
*
* // Create a new task run concurrency limit
* updateAutomation('my-id', {
* onSuccess: () => {
* console.log('Automation edited successfully');
* },
* onError: (error) => {
* // Handle error
* console.error('Failed to edit automation', error);
* }
* });
* ```
*/
export const useUpdateAutomation = () => {
const queryClient = useQueryClient();
const { mutate: updateAutomation, ...rest } = useMutation({
mutationFn: ({
id,
...body
}: components["schemas"]["AutomationUpdate"] & { id: string }) =>
getQueryService().PATCH("/automations/{id}", {
body,
params: { path: { id } },
}),
onSuccess: () => {
// After a successful reset, invalidate all to get an updated list and details list
return queryClient.invalidateQueries({
queryKey: queryKeyFactory.all(),
});
},
});
return {
updateAutomation,
...rest,
};
};
2 changes: 1 addition & 1 deletion ui-v2/tests/utils/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const automationsHandlers = [
http.patch("http://localhost:4200/api/automations/:id", () => {
return new HttpResponse(null, { status: 204 });
}),
http.delete("http://localhost:4200/api/api/:id", () => {
http.delete("http://localhost:4200/api/automations/:id", () => {
return HttpResponse.json({ status: 204 });
}),
];
Expand Down

0 comments on commit 04a9bdf

Please sign in to comment.