Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UI v2] feat: Adds automations mutations #16577

Merged
merged 1 commit into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 162 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,156 @@ 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
Loading