diff --git a/ui-v2/src/api/flow-runs/use-filter-flow-runs-with-flows/index.ts b/ui-v2/src/api/flow-runs/use-filter-flow-runs-with-flows/index.ts new file mode 100644 index 000000000000..545acd5a6950 --- /dev/null +++ b/ui-v2/src/api/flow-runs/use-filter-flow-runs-with-flows/index.ts @@ -0,0 +1 @@ +export { useFilterFlowRunswithFlows } from "./use-filter-flow-runs-with-flows"; diff --git a/ui-v2/src/api/flow-runs/use-filter-flow-runs-with-flows/use-filter-flow-runs-with-flows.test.ts b/ui-v2/src/api/flow-runs/use-filter-flow-runs-with-flows/use-filter-flow-runs-with-flows.test.ts new file mode 100644 index 000000000000..a4539027a533 --- /dev/null +++ b/ui-v2/src/api/flow-runs/use-filter-flow-runs-with-flows/use-filter-flow-runs-with-flows.test.ts @@ -0,0 +1,95 @@ +import { createFakeFlow, createFakeFlowRun } from "@/mocks"; + +import type { FlowRun } from "@/api/flow-runs"; +import type { Flow } from "@/api/flows"; + +import { QueryClient } from "@tanstack/react-query"; +import { renderHook, waitFor } from "@testing-library/react"; +import { buildApiUrl, createWrapper, server } from "@tests/utils"; +import { http, HttpResponse } from "msw"; +import { describe, expect, it } from "vitest"; +import { useFilterFlowRunswithFlows } from "./use-filter-flow-runs-with-flows"; + +describe("useFilterFlowRunswithFlows", () => { + const mockFilterFlowRunsAPI = (flowRuns: Array) => { + server.use( + http.post(buildApiUrl("/flow_runs/filter"), () => { + return HttpResponse.json(flowRuns); + }), + ); + }; + + const mockFilterFlowsAPI = (flows: Array) => { + server.use( + http.post(buildApiUrl("/flows/filter"), () => { + return HttpResponse.json(flows); + }), + ); + }; + + it("returns a list with no results", async () => { + // SETUP + const queryClient = new QueryClient(); + + mockFilterFlowRunsAPI([]); + + // TEST + const { result } = renderHook( + () => useFilterFlowRunswithFlows({ offset: 0, sort: "START_TIME_ASC" }), + { wrapper: createWrapper({ queryClient }) }, + ); + + await waitFor(() => expect(result.current.status).toEqual("success")); + expect(result.current.data).toHaveLength(0); + }); + + it("returns a list with joined flows and flow runs", async () => { + // SETUP + const queryClient = new QueryClient(); + const MOCK_FLOW_RUN_0 = createFakeFlowRun({ + id: "0", + flow_id: "flow-id-0", + }); + const MOCK_FLOW_RUN_1 = createFakeFlowRun({ + id: "0", + flow_id: "flow-id-0", + }); + const MOCK_FLOW_RUN_2 = createFakeFlowRun({ + id: "0", + flow_id: "flow-id-1", + }); + const MOCK_FLOW_0 = createFakeFlow({ id: "flow-id-0" }); + const MOCK_FLOW_1 = createFakeFlow({ id: "flow-id-1" }); + + const mockFlowRuns = [MOCK_FLOW_RUN_0, MOCK_FLOW_RUN_1, MOCK_FLOW_RUN_2]; + const mockFlows = [MOCK_FLOW_0, MOCK_FLOW_1]; + mockFilterFlowRunsAPI(mockFlowRuns); + mockFilterFlowsAPI(mockFlows); + + // TEST + const { result } = renderHook( + () => useFilterFlowRunswithFlows({ offset: 0, sort: "NAME_ASC" }), + { wrapper: createWrapper({ queryClient }) }, + ); + + await waitFor(() => expect(result.current.status).toEqual("success")); + + // ASSERT + const EXPECTED = [ + { + ...MOCK_FLOW_RUN_0, + flow: MOCK_FLOW_0, + }, + { + ...MOCK_FLOW_RUN_1, + flow: MOCK_FLOW_0, + }, + { + ...MOCK_FLOW_RUN_2, + flow: MOCK_FLOW_1, + }, + ]; + + expect(result.current.data).toEqual(EXPECTED); + }); +}); diff --git a/ui-v2/src/api/flow-runs/use-filter-flow-runs-with-flows/use-filter-flow-runs-with-flows.ts b/ui-v2/src/api/flow-runs/use-filter-flow-runs-with-flows/use-filter-flow-runs-with-flows.ts new file mode 100644 index 000000000000..0bbd4a1e6ae9 --- /dev/null +++ b/ui-v2/src/api/flow-runs/use-filter-flow-runs-with-flows/use-filter-flow-runs-with-flows.ts @@ -0,0 +1,80 @@ +import { FlowRunsFilter, buildFilterFlowRunsQuery } from "@/api/flow-runs"; +import { Flow, buildListFlowsQuery } from "@/api/flows"; +import { useQuery } from "@tanstack/react-query"; +import { useMemo } from "react"; + +/** + * + * @param filter + * @returns a simplified query object that joins a flow run's pagination data with it's parent flow + */ +export const useFilterFlowRunswithFlows = (filter: FlowRunsFilter) => { + const { data: flowRunsData, error: flowRunsError } = useQuery( + buildFilterFlowRunsQuery(filter), + ); + + const flowIds = useMemo(() => { + if (!flowRunsData) { + return []; + } + return flowRunsData.map((flowRun) => flowRun.flow_id); + }, [flowRunsData]); + + const { data: flows, error: flowsError } = useQuery( + buildListFlowsQuery( + { + flows: { id: { any_: flowIds }, operator: "and_" }, + offset: 0, + sort: "CREATED_DESC", + }, + { enabled: flowIds.length > 0 }, + ), + ); + + const flowMap = useMemo(() => { + if (!flows) { + return new Map(); + } + return new Map(flows.map((flow) => [flow.id, flow])); + }, [flows]); + + // If there's no results from the query, return empty + if (flowRunsData && flowRunsData.length === 0) { + return { + status: "success" as const, + error: null, + data: [], + }; + } + + if (flowRunsData && flowMap.size > 0) { + return { + status: "success" as const, + error: null, + data: flowRunsData.map((flowRun) => { + const flow = flowMap.get(flowRun.flow_id); + if (!flow) { + throw new Error("Expecting parent flow to be found"); + } + return { + ...flowRun, + flow, + }; + }), + }; + } + + if (flowRunsError || flowsError) { + return { + status: "error" as const, + error: flowRunsError || flowsError, + data: undefined, + }; + } + + return { + status: "pending" as const, + error: null, + data: undefined, + }; +};