diff --git a/ui-v2/src/hooks/global-concurrency-limits.test.tsx b/ui-v2/src/hooks/global-concurrency-limits.test.tsx new file mode 100644 index 0000000000000..fb7d11de63c5d --- /dev/null +++ b/ui-v2/src/hooks/global-concurrency-limits.test.tsx @@ -0,0 +1,119 @@ +import { + QueryClient, + QueryClientProvider, + useQuery, +} from "@tanstack/react-query"; +import { renderHook, waitFor } from "@testing-library/react"; +import { http, HttpResponse } from "msw"; +import { describe, expect, it } from "vitest"; + +import { + type GlobalConcurrencyLimit, + buildGetGlobalConcurrencyLimitQuery, + buildListGlobalConcurrencyLimitsQuery, +} from "./global-concurrency-limits"; + +import { server } from "../../tests/mocks/node"; + +describe("global concurrency limits hooks", () => { + const seedGlobalConcurrencyLimits = () => [ + { + id: "0", + created: "2021-01-01T00:00:00Z", + updated: "2021-01-01T00:00:00Z", + active: false, + name: "global concurrency limit 0", + limit: 0, + active_slots: 0, + slot_decay_per_second: 0, + }, + ]; + + const seedGlobalConcurrencyLimitDetails = () => ({ + id: "0", + created: "2021-01-01T00:00:00Z", + updated: "2021-01-01T00:00:00Z", + active: false, + name: "global concurrency limit 0", + limit: 0, + active_slots: 0, + slot_decay_per_second: 0, + }); + + const mockFetchGlobalConcurrencyLimitsAPI = ( + globalConcurrencyLimits: Array, + ) => { + server.use( + http.post( + "http://localhost:4200/api/v2/concurrency_limits/filter", + () => { + return HttpResponse.json(globalConcurrencyLimits); + }, + ), + ); + }; + + const mockFetchGlobalConcurrencyLimitDetailsAPI = ( + globalConcurrencyLimit: GlobalConcurrencyLimit, + ) => { + server.use( + http.get( + "http://localhost:4200/api/v2/concurrency_limits/:id_or_name", + () => { + return HttpResponse.json(globalConcurrencyLimit); + }, + ), + ); + }; + + const createQueryWrapper = ({ queryClient = new QueryClient() }) => { + const QueryWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + return QueryWrapper; + }; + + const filter = { + offset: 0, + }; + + /** + * Data Management: + * - Asserts global concurrency limit list data is fetched based on the APIs invoked for the hook + */ + it("is stores list data into the appropriate list query when using useQuery()", async () => { + // ------------ Mock API requests when cache is empty + const mockList = seedGlobalConcurrencyLimits(); + mockFetchGlobalConcurrencyLimitsAPI(mockList); + + // ------------ Initialize hooks to test + const { result } = renderHook( + () => useQuery(buildListGlobalConcurrencyLimitsQuery(filter)), + { wrapper: createQueryWrapper({}) }, + ); + + // ------------ Assert + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(result.current.data).toEqual(mockList); + }); + + /** + * Data Management: + * - Asserts global concurrency limit details data is fetched based on the APIs invoked for the hook + */ + it("is stores details data into the appropriate details query when using useQuery()", async () => { + // ------------ Mock API requests when cache is empty + const mockDetails = seedGlobalConcurrencyLimitDetails(); + mockFetchGlobalConcurrencyLimitDetailsAPI(mockDetails); + + // ------------ Initialize hooks to test + const { result } = renderHook( + () => useQuery(buildGetGlobalConcurrencyLimitQuery(mockDetails.id)), + { wrapper: createQueryWrapper({}) }, + ); + + // ------------ Assert + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(result.current.data).toEqual(mockDetails); + }); +}); diff --git a/ui-v2/src/hooks/global-concurrency-limits.ts b/ui-v2/src/hooks/global-concurrency-limits.ts new file mode 100644 index 0000000000000..9015a30ad0149 --- /dev/null +++ b/ui-v2/src/hooks/global-concurrency-limits.ts @@ -0,0 +1,63 @@ +import type { components } from "@/api/prefect"; +import { getQueryService } from "@/api/service"; +import { queryOptions } from "@tanstack/react-query"; + +export type GlobalConcurrencyLimit = + components["schemas"]["GlobalConcurrencyLimitResponse"]; +export type GlobalConcurrencyLimitsFilter = + components["schemas"]["Body_read_all_concurrency_limits_v2_v2_concurrency_limits_filter_post"]; + +/** + * ``` + * 🏗️ Variable queries construction 👷 + * all => ['global-concurrency-limits'] // key to match ['global-concurrency-limits', ... + * list => ['global-concurrency-limits', 'list'] // key to match ['global-concurrency-limits', 'list', ... + * ['global-concurrency-limits', 'list', { ...filter1 }] + * ['global-concurrency-limits', 'list', { ...filter2 }] + * details => ['global-concurrency-limits', 'details'] // key to match ['global-concurrency-limits', 'details', ...] + * ['global-concurrency-limits', 'details', { ...globalConcurrencyLimit1 }] + * ['global-concurrency-limits', 'details', { ...globalConcurrencyLimit2 }] + * ``` + * */ +const queryKeyFactory = { + all: () => ["global-concurrency-limits"] as const, + lists: () => [...queryKeyFactory.all(), "list"] as const, + list: (filter: GlobalConcurrencyLimitsFilter) => + [...queryKeyFactory.lists(), filter] as const, + details: () => [...queryKeyFactory.all(), "details"] as const, + detail: (id_or_name: string) => + [...queryKeyFactory.details(), id_or_name] as const, +}; + +// ----- 🔑 Queries 🗄️ +// ---------------------------- +export const buildListGlobalConcurrencyLimitsQuery = ( + filter: GlobalConcurrencyLimitsFilter, +) => + queryOptions({ + queryKey: queryKeyFactory.list(filter), + queryFn: async () => { + const res = await getQueryService().POST( + "/v2/concurrency_limits/filter", + { body: filter }, + ); + return res.data ?? []; + }, + }); + +export const buildGetGlobalConcurrencyLimitQuery = (id_or_name: string) => + queryOptions({ + queryKey: queryKeyFactory.detail(id_or_name), + queryFn: async () => { + const res = await getQueryService().GET( + "/v2/concurrency_limits/{id_or_name}", + { params: { path: { id_or_name } } }, + ); + return res.data ?? null; + }, + }); + +// ----- ✍🏼 Mutations 🗄️ +// ---------------------------- + +// TODO: