8000 Add query builder for log filtering (#17693) · CodersSampling/prefect@922b45a · GitHub
[go: up one dir, main page]

Skip to content

Commit 922b45a

Browse files
authored
Add query builder for log filtering (PrefectHQ#17693)
1 parent c51af96 commit 922b45a

File tree

3 files changed

+169
-0
lines changed

3 files changed

+169
-0
lines changed

ui-v2/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,6 @@ oss_schema.json
2929
*.tsbuildinfo
3030
*storybook.log
3131
*.orig
32+
33+
# Log code
34+
!src/api/logs/

ui-v2/src/api/logs/index.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { components } from "@/api/prefect";
2+
import { queryOptions } from "@tanstack/react-query";
3+
import { getQueryService } from "../service";
4+
5+
type LogsFilter = components["schemas"]["Body_read_logs_logs_filter_post"];
6+
7+
/**
8+
* Query key factory for logs-related queries
9+
*
10+
* @property {function} all - Returns base key for all flow queries
11+
* @property {function} lists - Returns key for all list-type flow queries
12+
* @property {function} list - Generates key for a specific filtered flow query
13+
*
14+
* ```
15+
* all => ['logs']
16+
* lists => ['logs', 'list']
17+
* list => ['logs', 'list', { ...filter }]
18+
* ```
19+
*/
20+
export const queryKeyFactory = {
21+
all: () => ["logs"] as const,
22+
lists: () => [...queryKeyFactory.all(), "list"] as const,
23+
list: (filter: LogsFilter) => [...queryKeyFactory.lists(), filter] as const,
24+
};
25+
26+
/**
27+
* Builds a query configuration for fetching filtered logs
28+
*
29+
* @param filter - Filter options for the logs query including:
30+
* - limit: Number of logs to fetch
31+
* - offset: Starting index of the logs
32+
* - sort: Sort order for the logs
33+
* - logs: Filter criteria for the logs
34+
* @returns Query configuration object for use with TanStack Query
35+
*
36+
* @example
37+
* ```ts
38+
* const query = useQuery(buildFilterLogsQuery({
39+
* logs: {
40+
* level: { ge_: 0 },
41+
* task_run_id: { any_: ["f96e6054-65f7-4921-b2a5-d8827c72b708"] }
42+
* },
43+
* sort: "TIMESTAMP_ASC",
44+
* offset: 0,
45+
* limit: 200
46+
* }));
47+
* ```
48+
*/
49+
export const buildFilterLogsQuery = (filter: LogsFilter) =>
50+
queryOptions({
51+
queryKey: queryKeyFactory.list(filter),
52+
queryFn: async () => {
53+
const res = await getQueryService().POST("/logs/filter", {
54+
body: filter,
55+
});
56+
return res.data ?? [];
57+
},
58+
});

ui-v2/src/api/logs/logs.test.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import type { components } from "@/api/prefect";
2+
import { QueryClient, useSuspenseQuery } from "@tanstack/react-query";
3+
import { renderHook, waitFor } from "@testing-library/react";
4+
import { buildApiUrl, createWrapper, server } from "@tests/utils";
5+
import { http, HttpResponse } from "msw";
6+
import { describe, expect, it } from "vitest";
7+
import { buildFilterLogsQuery, queryKeyFactory } from ".";
8+
9+
type Log = components["schemas"]["Log"];
10+
11+
describe("logs api", () => {
12+
const mockFetchLogsAPI = (logs: Array<Log>) => {
13+
server.use(
14+
http.post(buildApiUrl("/logs/filter"), () => {
15+
return HttpResponse.json(logs);
16+
}),
17+
);
18+
};
19+
20+
describe("queryKeyFactory", () => {
21+
it("generates correct query keys", () => {
22+
expect(queryKeyFactory.all()).toEqual(["logs"]);
23+
expect(queryKeyFactory.lists()).toEqual(["logs", "list"]);
24+
25+
const filter = {
26+
logs: { operator: "and_", level: { ge_: 0 } },
27+
sort: "TIMESTAMP_ASC",
28+
offset: 0,
29+
limit: 100,
30+
} as const;
31+
32+
expect(queryKeyFactory.list(filter)).toEqual(["logs", "list", filter]);
33+
});
34+
});
35+
36+
describe("buildFilterLogsQuery", () => {
37+
it("fetches logs with default parameters", async () => {
38+
const mockLogs = [
39+
{ id: "1", level: 20, message: "Test log 1" },
40+
{ id: "2", level: 30, message: "Test log 2" },
41+
] as Log[];
42+
43+
mockFetchLogsAPI(mockLogs);
44+
45+
const queryClient = new QueryClient();
46+
const { result } = renderHook(
47+
() =>
48+
useSuspenseQuery(
49+
buildFilterLogsQuery({
50+
offset: 0,
51+
sort: "TIMESTAMP_ASC",
52+
limit: 100,
53+
}),
54+
),
55+
{ wrapper: createWrapper({ queryClient }) },
56+
);
57+
58+
await waitFor(() => {
59+
expect(result.current.data).toEqual(mockLogs);
60+
});
61+
});
62+
63+
it("fetches logs with custom filter parameters", async () => {
64+
const mockLogs = [{ id: "1", level: 40, message: "Error log" }] as Log[];
65+
66+
mockFetchLogsAPI(mockLogs);
67+
68+
const filter = {
69+
logs: {
70+
operator: "and_",
71+
level: { ge_: 40 },
72+
},
73+
sort: "TIMESTAMP_DESC",
74+
offset: 0,
75+
limit: 50,
76+
} as const;
77+
78+
const queryClient = new QueryClient();
79+
const { result } = renderHook(
80+
() => useSuspenseQuery(buildFilterLogsQuery(filter)),
81+
{ wrapper: createWrapper({ queryClient }) },
82+
);
83+
84+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
85+
expect(result.current.data).toEqual(mockLogs);
86+
});
87+
88+
it("handles empty response from API", async () => {
89+
mockFetchLogsAPI([]);
90+
91+
const queryClient = new QueryClient();
92+
const { result } = renderHook(
93+
() =>
94+
useSuspenseQuery(
95+
buildFilterLogsQuery({
96+
offset: 0,
97+
sort: "TIMESTAMP_ASC",
98+
limit: 100,
99+
}),
100+
),
101+
{ wrapper: createWrapper({ queryClient }) },
102+
);
103+
104+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
105+
expect(result.current.data).toEqual([]);
106+
});
107+
});
108+
});

0 commit comments

Comments
 (0)
0