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

Devan/eng 1244 artifactskeyname #17030

Merged
merged 8 commits into from
Feb 7, 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
349 changes: 349 additions & 0 deletions ui-v2/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions ui-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@radix-ui/react-hover-card": "^1.1.5",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-menubar": "^1.1.5",
"@radix-ui/react-popover": "^1.1.5",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.5",
Expand Down Expand Up @@ -60,6 +61,7 @@
"react-hook-form": "^7.54.2",
"react-markdown": "^9.0.3",
"recharts": "^2.15.1",
"remark-gfm": "^4.0.0",
"rrule": "^2.8.1",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
Expand All @@ -79,6 +81,7 @@
"@storybook/test": "^8.4.2",
"@storybook/theming": "^8.4.7",
"@storybook/types": "^8.5.3",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.66.0",
"@tanstack/eslint-plugin-router": "^1.99.3",
"@tanstack/router-devtools": "^1.99.0",
Expand Down
4 changes: 4 additions & 0 deletions ui-v2/src/api/artifacts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { components } from "../prefect";
import { getQueryService } from "../service";

export type Artifact = components["schemas"]["Artifact"];
export type ArtifactWithFlowRunAndTaskRun = Artifact & {
flow_run?: components["schemas"]["FlowRun"];
task_run?: components["schemas"]["TaskRun"];
};

export type ArtifactsFilter =
components["schemas"]["Body_read_artifacts_artifacts_filter_post"];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { buildListFlowRunsQuery } from "@/api/flow-runs";
import { buildListTaskRunsQuery } from "@/api/task-runs";
import { useQueries, useQuery } from "@tanstack/react-query";
import {
ArtifactWithFlowRunAndTaskRun,
ArtifactsFilter,
buildGetArtifactQuery,
buildListArtifactsQuery,
} from "..";

export const useFilterArtifactsFlowTaskRuns = (filter: ArtifactsFilter) => {
const { data: artifacts } = useQuery(buildListArtifactsQuery(filter));

const flowRunIds = artifacts
?.map((artifact) => artifact.flow_run_id)
.filter((val) => val != null && val !== undefined);
const taskRunIds = artifacts
?.map((artifact) => artifact.task_run_id)
.filter((val) => val != null && val !== undefined);

const { flowRuns, taskRuns } = useQueries({
queries: [
buildListFlowRunsQuery({
flow_runs: {
operator: "and_",
id: {
any_: flowRunIds,
},
},
sort: "ID_DESC",
offset: 0,
}),
buildListTaskRunsQuery({
task_runs: {
operator: "and_",
id: {
any_: taskRunIds,
},
},
sort: "ID_DESC",
offset: 0,
}),
],
combine: (data) => {
const [flowRuns, taskRuns] = data;
return {
flowRuns: flowRuns.data,
taskRuns: taskRuns.data,
};
},
});

const artifactsWithMetadata = artifacts?.map((artifact) => {
const flowRun = flowRuns?.find(
(flowRun) => flowRun.id === artifact.flow_run_id,
);
const taskRun = taskRuns?.find(
(taskRun) => taskRun.id === artifact.task_run_id,
);

return {
...artifact,
flow_run: flowRun,
task_run: taskRun,
};
});

return artifactsWithMetadata as ArtifactWithFlowRunAndTaskRun[];
};

export const useGetArtifactFlowTaskRuns = (artifactId: string) => {
const { data: artifact } = useQuery(buildGetArtifactQuery(artifactId));

const flowRunId = artifact?.flow_run_id;
const taskRunId = artifact?.task_run_id;

const { flowRuns, taskRuns } = useQueries({
queries: [
buildListFlowRunsQuery({
flow_runs: {
operator: "and_",
id: {
any_: [flowRunId ?? ""],
},
},
sort: "ID_DESC",
offset: 0,
}),
buildListTaskRunsQuery({
task_runs: {
operator: "and_",
id: {
any_: [taskRunId ?? ""],
},
},
sort: "ID_DESC",
offset: 0,
}),
],
combine: (data) => {
const [flowRuns, taskRuns] = data;
return {
flowRuns: flowRuns.data,
taskRuns: taskRuns.data,
};
},
});

return {
artifact,
flowRun: flowRuns?.[0],
taskRun: taskRuns?.[0],
} as ArtifactWithFlowRunAndTaskRun;
};
65 changes: 65 additions & 0 deletions ui-v2/src/api/task-runs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { queryOptions } from "@tanstack/react-query";
import { components } from "../prefect";
import { getQueryService } from "../service";

export type TaskRun = components["schemas"]["TaskRun"];

export type TaskRunsFilter =
components["schemas"]["Body_read_task_runs_task_runs_filter_post"];

/**
* Query key factory for task-related queries
*
* @property {function} all - Returns base key for all task run queries
* @property {function} lists - Returns key for all list-type task run queries
* @property {function} list - Generates key for a specific filtered task run query
*
* ```
* all => ['task']
* lists => ['task', 'list']
* list => ['task', 'list', { ...filter }]
* ```
*/
export const queryKeyFactory = {
all: () => ["taskRuns"] as const,
lists: () => [...queryKeyFactory.all(), "list"] as const,
list: (filter: TaskRunsFilter) =>
[...queryKeyFactory.lists(), filter] as const,
};

/**
* Builds a query configuration for fetching filtered task runs
*
* @param filter - Filter parameters for the task runs query.
* @returns Query configuration object for use with TanStack Query
*
* @example
* ```ts
* const { data, isLoading, error } = useQuery(buildListTaskRunsQuery({
* offset: 0,
* sort: "CREATED_DESC",
* task_runs: {
* name: { like_: "my-task-run" }
* }
* }));
* ```
*/
export const buildListTaskRunsQuery = (
filter: TaskRunsFilter = {
sort: "ID_DESC",
offset: 0,
},
refetchInterval: number = 30_000,
) => {
return queryOptions({
queryKey: queryKeyFactory.list(filter),
queryFn: async () => {
const res = await getQueryService().POST("/task_runs/filter", {
body: filter,
});
return res.data ?? [];
},
staleTime: 1000,
refetchInterval,
});
};
72 changes: 72 additions & 0 deletions ui-v2/src/api/task-runs/task-runs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { createFakeTaskRun } from "@/mocks";
import { QueryClient, useSuspenseQuery } 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 { TaskRun, TaskRunsFilter, buildListTaskRunsQuery } from ".";

describe("task runs api", () => {
const mockFetchTaskRunsAPI = (taskRuns: Array<TaskRun>) => {
server.use(
http.post(buildApiUrl("/task_runs/filter"), () => {
return HttpResponse.json(taskRuns);
}),
);
};

describe("taskRunsQueryParams", () => {
it("fetches paginated task runs with default parameters", async () => {
const taskRun = createFakeTaskRun();
mockFetchTaskRunsAPI([taskRun]);

const queryClient = new QueryClient();
const { result } = renderHook(
() => useSuspenseQuery(buildListTaskRunsQuery()),
{ wrapper: createWrapper({ queryClient }) },
);

await waitFor(() => {
expect(result.current.data).toEqual([taskRun]);
});
});

it("fetches paginated task runs with custom search parameters", async () => {
const taskRun = createFakeTaskRun();
mockFetchTaskRunsAPI([taskRun]);

const filter: TaskRunsFilter = {
offset: 0,
limit: 10,
sort: "ID_DESC" as const,
task_runs: {
operator: "and_" as const,
name: { like_: "test-task-run" },
},
};

const queryClient = new QueryClient();
const { result } = renderHook(
() => useSuspenseQuery(buildListTaskRunsQuery(filter)),
{ wrapper: createWrapper({ queryClient }) },
);

await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data).toEqual([taskRun]);
});

it("uses the provided refetch interval", () => {
const taskRun = createFakeTaskRun();
mockFetchTaskRunsAPI([taskRun]);

const customRefetchInterval = 60_000; // 1 minute

const { refetchInterval } = buildListTaskRunsQuery(
{ sort: "ID_DESC", offset: 0 },
customRefetchInterval,
);

expect(refetchInterval).toBe(customRefetchInterval);
});
});
});
34 changes: 29 additions & 5 deletions ui-v2/src/components/artifacts/artifact-card.test.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
import { Artifact } from "@/api/artifacts";
import { createFakeArtifact } from "@/mocks";
import { QueryClient } from "@tanstack/react-query";
import {
RouterProvider,
createMemoryHistory,
createRootRoute,
createRouter,
} from "@tanstack/react-router";
import { render } from "@testing-library/react";
import { createWrapper } from "@tests/utils";
import { describe, expect, it } from "vitest";
import { ArtifactCard } from "./artifact-card";
import { ArtifactCard, ArtifactsCardProps } from "./artifact-card";

// Wraps component in test with a Tanstack router provider
const ArtifactsCardRouter = (props: ArtifactsCardProps) => {
const rootRoute = createRootRoute({
component: () => <ArtifactCard {...props} />,
});

const router = createRouter({
routeTree: rootRoute,
history: createMemoryHistory({
initialEntries: ["/"],
}),
context: { queryClient: new QueryClient() },
});
// @ts-expect-error - Type error from using a test router
return <RouterProvider router={router} />;
};

describe("Artifacts Card", () => {
it("renders artifact card with description", () => {
const artifact: Artifact = createFakeArtifact({
description: "This is a description",
});
const { getByText } = render(<ArtifactCard artifact={artifact} />, {
const { getByText } = render(<ArtifactsCardRouter artifact={artifact} />, {
wrapper: createWrapper(),
});

Expand All @@ -21,7 +45,7 @@ describe("Artifacts Card", () => {
const artifact = createFakeArtifact({
updated: "2021-09-01T12:00:00Z",
});
const { getByText } = render(<ArtifactCard artifact={artifact} />, {
const { getByText } = render(<ArtifactsCardRouter artifact={artifact} />, {
wrapper: createWrapper(),
});

Expand All @@ -33,7 +57,7 @@ describe("Artifacts Card", () => {
const artifact = createFakeArtifact({
key: "test-key",
});
const { getByText } = render(<ArtifactCard artifact={artifact} />, {
const { getByText } = render(<ArtifactsCardRouter artifact={artifact} />, {
wrapper: createWrapper(),
});

Expand All @@ -44,7 +68,7 @@ describe("Artifacts Card", () => {
const artifact = createFakeArtifact({
type: "test-type",
});
const { getByText } = render(<ArtifactCard artifact={artifact} />, {
const { getByText } = render(<ArtifactsCardRouter artifact={artifact} />, {
wrapper: createWrapper(),
});

Expand Down
Loading
Loading