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 Next run section to run tab #17245

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useFilterFlowRunswithFlows } from "./use-filter-flow-runs-with-flows";
Original file line number Diff line number Diff line change
@@ -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<FlowRun>) => {
server.use(
http.post(buildApiUrl("/flow_runs/filter"), () => {
return HttpResponse.json(flowRuns);
}),
);
};

const mockFilterFlowsAPI = (flows: Array<Flow>) => {
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);
});
});
Original file line number Diff line number Diff line change
@@ -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<string, Flow>();
}
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,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Deployment } from "@/api/deployments";
import { useFilterFlowRunswithFlows } from "@/api/flow-runs/use-filter-flow-runs-with-flows";
import { FlowRunCard } from "@/components/flow-runs/flow-run-card";
import { Typography } from "@/components/ui/typography";
import { useMemo } from "react";

type DeploymentDetailsRunsTabProps = {
deployment: Deployment;
};

export const DeploymentDetailsRunsTab = ({
deployment,
}: DeploymentDetailsRunsTabProps) => {
const nextRun = useGetNextRun(deployment);

return (
<div className="flex flex-col">
{nextRun && (
<div className="flex flex-col gap-2 border-b py-2">
<Typography variant="bodyLarge">Next Run</Typography>
<FlowRunCard flowRun={nextRun} />
</div>
)}
</div>
);
};

function useGetNextRun(deployment: Deployment) {
const { data } = useFilterFlowRunswithFlows({
deployments: { id: { any_: [deployment.id] }, operator: "and_" },
flow_runs: {
state: { name: { any_: ["Scheduled"] }, operator: "and_" },
operator: "and_",
},
sort: "NAME_ASC",
limit: 1,
offset: 0,
});

return useMemo(() => {
if (!data || !data[0]) {
return undefined;
}
return {
...data[0],
deployment,
};
}, [data, deployment]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { type JSX, useMemo } from "react";

import { DeploymentConfiguration } from "./deployment-configuration";
import { DeploymentDescription } from "./deployment-description";
import { DeploymentDetailsRunsTab } from "./deployment-details-runs-tab";
import { DeploymentDetailsUpcomingTab } from "./deployment-details-upcoming-tab";
import { DeploymentParametersTable } from "./deployment-parameters-table";

Expand Down Expand Up @@ -54,7 +55,7 @@ function useBuildTabOptions(deployment: Deployment) {
),
ViewComponent: () => (
<TabsContent value="Runs">
<div className="border border-red-400">{"<RunsView />"}</div>
<DeploymentDetailsRunsTab deployment={deployment} />
</TabsContent>
),
},
Expand All @@ -67,7 +68,7 @@ function useBuildTabOptions(deployment: Deployment) {
),
ViewComponent: () => (
<TabsContent value="Upcoming">
<DeploymentDetailsUpcomingTab deploymentId={deployment.id} />
<DeploymentDetailsUpcomingTab deployment={deployment} />
</TabsContent>
),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import { getRouteApi } from "@tanstack/react-router";

import { Deployment } from "@/api/deployments";
import { usePaginateFlowRunswithFlows } from "@/api/flow-runs/use-paginate-flow-runs-with-flows";
import { FlowRunState, SortFilters } from "@/components/flow-runs/data-table";
import {
FlowRunState,
FlowRunsFilters,
FlowRunsList,
FlowRunsPagination,
FlowRunsRowCount,
type PaginationState,
SortFilters,
} from "@/components/flow-runs/flow-runs-list";
import { useCallback, useMemo, useState } from "react";

const routeApi = getRouteApi("/deployments/deployment/$id");

type DeploymentDetailsUpcomingTabProps = {
deploymentId: string;
deployment: Deployment;
};

export const DeploymentDetailsUpcomingTab = ({
deploymentId,
deployment,
}: DeploymentDetailsUpcomingTabProps) => {
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
const [pagination, onChangePagination] = usePagination();
Expand All @@ -30,7 +32,7 @@ export const DeploymentDetailsUpcomingTab = ({
const { data } = usePaginateFlowRunswithFlows({
deployments: {
operator: "and_",
id: { any_: [deploymentId] },
id: { any_: [deployment.id] },
},
flow_runs: {
name: { like_: search || undefined },
Expand All @@ -45,10 +47,15 @@ export const DeploymentDetailsUpcomingTab = ({
sort,
});

const handleResetFilters = () => {
resetFilters();
setSelectedRows(new Set());
};
const dataWithDeployment = useMemo(() => {
if (!data) {
return undefined;
}
return {
...data,
results: data.results.map((flowRun) => ({ ...flowRun, deployment })),
};
}, [data, deployment]);

const addRow = (id: string) =>
setSelectedRows((curr) => new Set(curr).add(id));
Expand All @@ -67,12 +74,19 @@ export const DeploymentDetailsUpcomingTab = ({
}
};

const handleResetFilters = !resetFilters
? undefined
: () => {
resetFilters();
setSelectedRows(new Set());
};

return (
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<FlowRunsRowCount
count={data?.count}
results={data?.results}
count={dataWithDeployment?.count}
results={dataWithDeployment?.results}
selectedRows={selectedRows}
setSelectedRows={setSelectedRows}
/>
Expand All @@ -87,24 +101,26 @@ export const DeploymentDetailsUpcomingTab = ({
</div>

<FlowRunsList
flowRuns={data?.results}
flowRuns={dataWithDeployment?.results}
selectedRows={selectedRows}
onSelect={handleSelectRow}
onClearFilters={handleResetFilters}
/>

{data && data.results.length > 0 && (
{dataWithDeployment && dataWithDeployment.results.length > 0 && (
<FlowRunsPagination
count={dataWithDeployment.count}
pagination={pagination}
onChangePagination={onChangePagination}
pages={data.pages}
pages={dataWithDeployment.pages}
/>
)}
</div>
);
};

function useResetFilters() {
const { upcoming } = routeApi.useSearch();
const navigate = routeApi.useNavigate();
const resetFilters = useCallback(() => {
void navigate({
Expand All @@ -116,7 +132,9 @@ function useResetFilters() {
replace: true,
});
}, [navigate]);
return resetFilters;
const hasFiltersApplied = useMemo(() => Boolean(upcoming), [upcoming]);

return hasFiltersApplied ? resetFilters : undefined;
}

function usePagination() {
Expand Down
Loading