Skip to content

Commit 9ac222f

Browse files
committed
Hook up filters to API where availiable
1 parent 06c105a commit 9ac222f

4 files changed

Lines changed: 126 additions & 43 deletions

File tree

src/components/Home/RunSection/RunSection.tsx

Lines changed: 29 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import {
2222
import { useBackend } from "@/providers/BackendProvider";
2323
import { getBackendStatusString } from "@/utils/backend";
2424
import { fetchWithErrorHandling } from "@/utils/fetchWithErrorHandling";
25+
import {
26+
filtersToApiString,
27+
parseFilterParam,
28+
} from "@/utils/pipelineRunFilterUtils";
2529

2630
import RunRow from "./RunRow";
2731

@@ -42,25 +46,12 @@ export const RunSection = ({ onEmptyList }: { onEmptyList?: () => void }) => {
4246
const isCreatedByMeDefault = useFlagValue("created-by-me-default");
4347
const dataVersion = useRef(0);
4448

45-
// Parse filter into a dictionary
46-
const parseFilter = (filter?: string): Record<string, string> => {
47-
if (!filter) return {};
48-
49-
const filterDict: Record<string, string> = {};
50-
const parts = filter.split(",");
51-
52-
for (const part of parts) {
53-
const [key, value] = part.split(":");
54-
if (key && value) {
55-
filterDict[key.trim()] = value.trim();
56-
}
57-
}
49+
// Parse filter from URL - supports both JSON (new) and key:value (legacy) formats
50+
const filters = parseFilterParam(search.filter);
51+
const createdByValue = filters.created_by;
5852

59-
return filterDict;
60-
};
61-
62-
const filterDict = parseFilter(search.filter);
63-
const createdByValue = filterDict.created_by;
53+
// Convert filters to API format (only includes supported filters)
54+
const apiFilterString = filtersToApiString(filters);
6455

6556
const [searchUser, setSearchUser] = useState(createdByValue ?? "");
6657

@@ -75,13 +66,15 @@ export const RunSection = ({ onEmptyList }: { onEmptyList?: () => void }) => {
7566

7667
const { data, isLoading, isFetching, error, isFetched } =
7768
useQuery<ListPipelineJobsResponse>({
78-
queryKey: ["runs", backendUrl, pageToken, search.filter],
69+
queryKey: ["runs", backendUrl, pageToken, apiFilterString],
7970
refetchOnWindowFocus: false,
8071
enabled: configured && available,
8172
queryFn: async () => {
8273
const u = new URL(PIPELINE_RUNS_QUERY_URL, backendUrl);
8374
if (pageToken) u.searchParams.set(PAGE_TOKEN_QUERY_KEY, pageToken);
84-
if (search.filter) u.searchParams.set(FILTER_QUERY_KEY, search.filter);
75+
// Use translated filter string (only includes API-supported filters)
76+
if (apiFilterString)
77+
u.searchParams.set(FILTER_QUERY_KEY, apiFilterString);
8578

8679
u.searchParams.set(INCLUDE_PIPELINE_NAME_QUERY_KEY, "true");
8780
u.searchParams.set(INCLUDE_EXECUTION_STATS_QUERY_KEY, "true");
@@ -108,22 +101,25 @@ export const RunSection = ({ onEmptyList }: { onEmptyList?: () => void }) => {
108101

109102
if (value) {
110103
// If there's already a created_by filter, keep it; otherwise use "created_by:me"
111-
if (!filterDict.created_by) {
104+
if (!filters.created_by) {
112105
nextSearch.filter = CREATED_BY_ME_FILTER;
113106
setSearchUser("");
114107
}
115108
} else {
116109
// Remove created_by from filter, but keep other filters
117-
const updatedFilterDict = { ...filterDict };
118-
delete updatedFilterDict.created_by;
119-
120-
// Convert back to string format
121-
const remainingFilters = Object.entries(updatedFilterDict)
122-
.map(([key, value]) => `${key}:${value}`)
123-
.join(",");
110+
const updatedFilters = { ...filters };
111+
delete updatedFilters.created_by;
112+
113+
// Check if any other filters remain
114+
const hasRemainingFilters = Object.keys(updatedFilters).some((key) => {
115+
const val = updatedFilters[key as keyof typeof updatedFilters];
116+
if (val === undefined || val === null || val === "") return false;
117+
if (Array.isArray(val) && val.length === 0) return false;
118+
return true;
119+
});
124120

125-
if (remainingFilters) {
126-
nextSearch.filter = remainingFilters;
121+
if (hasRemainingFilters) {
122+
nextSearch.filter = JSON.stringify(updatedFilters);
127123
} else {
128124
if (isCreatedByMeDefault) {
129125
nextSearch.filter = "";
@@ -143,16 +139,9 @@ export const RunSection = ({ onEmptyList }: { onEmptyList?: () => void }) => {
143139
const nextSearch: RunSectionSearch = { ...search };
144140
delete nextSearch.page_token;
145141

146-
// Create or update the created_by filter
147-
const updatedFilterDict = { ...filterDict };
148-
updatedFilterDict.created_by = searchUser.trim();
149-
150-
// Convert back to string format
151-
const newFilter = Object.entries(updatedFilterDict)
152-
.map(([key, value]) => `${key}:${value}`)
153-
.join(",");
154-
155-
nextSearch.filter = newFilter;
142+
// Create or update the created_by filter, preserving other filters
143+
const updatedFilters = { ...filters, created_by: searchUser.trim() };
144+
nextSearch.filter = JSON.stringify(updatedFilters);
156145

157146
setPreviousPageTokens([]);
158147
navigate({ to: pathname, search: nextSearch });

src/components/shared/CreatedByFilter/CreatedByFilter.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { KeyboardEvent } from "react";
2-
import { useState } from "react";
2+
import { useEffect, useState } from "react";
33

44
import { Button } from "@/components/ui/button";
55
import { Input } from "@/components/ui/input";
@@ -21,6 +21,16 @@ interface CreatedByFilterProps {
2121
export function CreatedByFilter({ value, onChange }: CreatedByFilterProps) {
2222
const [searchUser, setSearchUser] = useState(value ?? "");
2323

24+
// Sync internal state when value prop changes externally (e.g., URL navigation, badge removal)
25+
useEffect(() => {
26+
// Only sync if value is different and not "me" (don't populate input with "me")
27+
if (value !== undefined && value !== "me") {
28+
setSearchUser(value);
29+
} else if (value === undefined) {
30+
setSearchUser("");
31+
}
32+
}, [value]);
33+
2434
const isFilterActive = value !== undefined;
2535
const toggleText = value ? `Created by ${value}` : "Created by me";
2636

src/flags.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ export const ExistingFlags: ConfigFlags = {
6767
},
6868

6969
["pipeline-run-filters-bar"]: {
70-
name: "Pipeline run filters bar (UI only)",
70+
name: "Pipeline run filters bar",
7171
description:
72-
"Non-functional UI preview. This filter bar is not connected to the API and is for testing/development purposes only.",
72+
"Enable the advanced pipeline run filters bar. Note: Only 'Created by' filter is currently functional. Other filters are UI previews.",
7373
default: false,
7474
category: "beta",
7575
},

src/utils/pipelineRunFilterUtils.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import { isValidExecutionStatus } from "@/utils/executionStatus";
99
const VALID_SORT_FIELDS: SortField[] = ["created_at", "pipeline_name"];
1010
const VALID_SORT_DIRECTIONS: SortDirection[] = ["asc", "desc"];
1111

12+
/**
13+
* List of filter keys that the API currently supports.
14+
* Add keys here as the backend adds support for more filters.
15+
*/
16+
const SUPPORTED_API_FILTERS: (keyof PipelineRunFilters)[] = ["created_by"];
17+
1218
export function isRecord(value: unknown): value is Record<string, unknown> {
1319
return typeof value === "object" && value !== null && !Array.isArray(value);
1420
}
@@ -78,6 +84,84 @@ export function validateFilters(parsed: unknown): PipelineRunFilters {
7884
return filters;
7985
}
8086

87+
/**
88+
* Converts PipelineRunFilters to the API's key:value string format.
89+
* Only includes filters that the API actually supports.
90+
*
91+
* @example
92+
* // Input: { created_by: "me", status: "FAILED", annotations: [...] }
93+
* // Output: "created_by:me" (status and annotations are stripped as unsupported)
94+
*/
95+
export function filtersToApiString(
96+
filters: PipelineRunFilters,
97+
): string | undefined {
98+
const parts: string[] = [];
99+
100+
for (const key of SUPPORTED_API_FILTERS) {
101+
const value = filters[key];
102+
if (value !== undefined && value !== null && value !== "") {
103+
// Handle simple string values
104+
if (typeof value === "string") {
105+
parts.push(`${key}:${value}`);
106+
}
107+
// Future: handle arrays or complex values when API supports them
108+
}
109+
}
110+
111+
return parts.length > 0 ? parts.join(",") : undefined;
112+
}
113+
114+
/**
115+
* Parses the legacy key:value,key2:value2 format into a filter object.
116+
* Used for backwards compatibility with existing URLs.
117+
*/
118+
function parseApiStringToFilters(
119+
filterString: string | undefined,
120+
): PipelineRunFilters {
121+
if (!filterString) return {};
122+
123+
const filters: PipelineRunFilters = {};
124+
const parts = filterString.split(",");
125+
126+
for (const part of parts) {
127+
const colonIndex = part.indexOf(":");
128+
if (colonIndex === -1) continue;
129+
130+
const key = part.slice(0, colonIndex).trim();
131+
const value = part.slice(colonIndex + 1).trim();
132+
133+
if (key && value) {
134+
if (key === "created_by") {
135+
filters.created_by = value;
136+
}
137+
// Add more cases as API supports more filters
138+
}
139+
}
140+
141+
return filters;
142+
}
143+
144+
/**
145+
* Parses the filter URL param, handling both JSON format (new) and key:value format (legacy).
146+
*/
147+
export function parseFilterParam(
148+
filterParam: string | Record<string, unknown> | undefined,
149+
): PipelineRunFilters {
150+
if (!filterParam) return {};
151+
152+
// Already an object (parsed by router) - validate and return
153+
if (isRecord(filterParam)) {
154+
return validateFilters(filterParam);
155+
}
156+
157+
try {
158+
return validateFilters(JSON.parse(filterParam));
159+
} catch {
160+
// Invalid JSON - try legacy key:value format for backwards compatibility
161+
return parseApiStringToFilters(filterParam);
162+
}
163+
}
164+
81165
/**
82166
* Clean and prepare PipelineRunFilters for URL serialization.
83167
* Returns the filter object - let TanStack Router handle JSON serialization.

0 commit comments

Comments
 (0)