diff --git a/react-compiler.config.js b/react-compiler.config.js
index 8f1ee6021..394e9692f 100644
--- a/react-compiler.config.js
+++ b/react-compiler.config.js
@@ -51,6 +51,7 @@ export const REACT_COMPILER_ENABLED_DIRS = [
"src/components/shared/ReactFlow/FlowControls/StackingControls.tsx",
"src/components/shared/ReactFlow/FlowCanvas/FlexNode",
"src/components/shared/ReactFlow/FlowCanvas/TaskNode/ArgumentsEditor/DynamicDataDropdown.tsx",
+ "src/components/shared/HighlightText.tsx",
// 11-20 useCallback/useMemo
// "src/components/ui", // 12
diff --git a/src/components/Home/PipelineSection/PipelineRow.tsx b/src/components/Home/PipelineSection/PipelineRow.tsx
index 960cdde03..8aab64abe 100644
--- a/src/components/Home/PipelineSection/PipelineRow.tsx
+++ b/src/components/Home/PipelineSection/PipelineRow.tsx
@@ -2,13 +2,16 @@ import { useNavigate } from "@tanstack/react-router";
import { type MouseEvent } from "react";
import { ConfirmationDialog } from "@/components/shared/Dialogs";
+import { HighlightText } from "@/components/shared/HighlightText";
import { PipelineRunInfoCondensed } from "@/components/shared/PipelineRunDisplay/PipelineRunInfoCondensed";
import { PipelineRunsList } from "@/components/shared/PipelineRunDisplay/PipelineRunsList";
import { usePipelineRuns } from "@/components/shared/PipelineRunDisplay/usePipelineRuns";
import { withSuspenseWrapper } from "@/components/shared/SuspenseWrapper";
+import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Icon } from "@/components/ui/icon";
+import { BlockStack, InlineStack } from "@/components/ui/layout";
import {
Popover,
PopoverContent,
@@ -22,6 +25,8 @@ import { deletePipeline } from "@/services/pipelineService";
import type { ComponentReferenceWithSpec } from "@/utils/componentStore";
import { formatDate } from "@/utils/date";
+import type { MatchedField } from "./usePipelineFilters";
+
interface PipelineRowProps {
url?: string;
componentRef?: ComponentReferenceWithSpec;
@@ -30,6 +35,10 @@ interface PipelineRowProps {
onDelete?: () => void;
isSelected?: boolean;
onSelect?: (checked: boolean) => void;
+ searchQuery?: string;
+ matchedFields?: MatchedField[];
+ componentQuery?: string;
+ matchedComponentNames?: string[];
}
const PipelineRow = withSuspenseWrapper(
@@ -39,6 +48,10 @@ const PipelineRow = withSuspenseWrapper(
onDelete,
isSelected = false,
onSelect,
+ searchQuery,
+ matchedFields,
+ componentQuery,
+ matchedComponentNames,
}: PipelineRowProps) => {
const navigate = useNavigate();
@@ -90,7 +103,17 @@ const PipelineRow = withSuspenseWrapper(
/>
- {name}
+
+
+
+
+
+
@@ -193,6 +216,46 @@ const PipelineRunsButton = withSuspenseWrapper(
},
);
+function MatchBadges({
+ matchedFields,
+ matchedComponentNames,
+ searchQuery,
+ componentQuery,
+}: {
+ matchedFields?: MatchedField[];
+ matchedComponentNames?: string[];
+ searchQuery?: string;
+ componentQuery?: string;
+}) {
+ const hasFields = matchedFields && matchedFields.length > 0;
+ const hasComponents =
+ matchedComponentNames && matchedComponentNames.length > 0;
+
+ if (!hasFields && !hasComponents) return null;
+
+ return (
+
+ {matchedFields?.map((field) => (
+
+ {field.label}:{" "}
+
+
+ ))}
+ {matchedComponentNames?.map((compName) => (
+
+
+
+
+ ))}
+
+ );
+}
+
function formatModificationTime(modificationTime: Date | undefined) {
return modificationTime ? formatDate(modificationTime) : "N/A";
}
diff --git a/src/components/Home/PipelineSection/PipelineSection.tsx b/src/components/Home/PipelineSection/PipelineSection.tsx
index 34a2d0f88..10861a450 100644
--- a/src/components/Home/PipelineSection/PipelineSection.tsx
+++ b/src/components/Home/PipelineSection/PipelineSection.tsx
@@ -204,7 +204,7 @@ export const PipelineSection = withSuspenseWrapper(() => {
)}
- {paginatedPipelines.map(([name, fileEntry]) => (
+ {paginatedPipelines.map(([name, fileEntry, matchMetadata]) => (
{
onDelete={fetchUserPipelines}
isSelected={selectedPipelines.has(name)}
onSelect={(checked) => handleSelectPipeline(name, checked)}
+ searchQuery={matchMetadata.searchQuery}
+ matchedFields={matchMetadata.matchedFields}
+ componentQuery={matchMetadata.componentQuery}
+ matchedComponentNames={matchMetadata.matchedComponentNames}
/>
))}
diff --git a/src/components/Home/PipelineSection/usePipelineFilters.ts b/src/components/Home/PipelineSection/usePipelineFilters.ts
index 78b6c6e53..decf2267f 100644
--- a/src/components/Home/PipelineSection/usePipelineFilters.ts
+++ b/src/components/Home/PipelineSection/usePipelineFilters.ts
@@ -7,7 +7,19 @@ import type { ComponentFileEntry } from "@/utils/componentStore";
export type PipelineSortField = "modified_at" | "name";
export type PipelineSortDirection = "asc" | "desc";
-type PipelineEntry = [string, ComponentFileEntry];
+export interface MatchedField {
+ label: string;
+ value: string;
+}
+
+interface PipelineMatchMetadata {
+ searchQuery: string;
+ matchedFields: MatchedField[];
+ componentQuery: string;
+ matchedComponentNames: string[];
+}
+
+type PipelineEntry = [string, ComponentFileEntry, PipelineMatchMetadata];
export interface FilterBarProps {
searchQuery: string;
@@ -64,6 +76,55 @@ function matchesSearch(
);
}
+function getMatchMetadata(
+ fileEntry: ComponentFileEntry,
+ searchQuery: string,
+ componentQuery: string,
+): PipelineMatchMetadata {
+ const matchedFields: MatchedField[] = [];
+ if (searchQuery) {
+ const q = searchQuery.toLowerCase();
+ const spec = fileEntry.componentRef.spec;
+ const desc = spec.description ?? "";
+ if (desc.toLowerCase().includes(q)) {
+ matchedFields.push({ label: "Description", value: desc });
+ }
+ const author = spec.metadata?.annotations?.author ?? "";
+ if (author.toLowerCase().includes(q)) {
+ matchedFields.push({ label: "Author", value: author });
+ }
+ const rawNotes = spec.metadata?.annotations?.["notes"];
+ const notes = typeof rawNotes === "string" ? rawNotes : "";
+ if (notes.toLowerCase().includes(q)) {
+ matchedFields.push({ label: "Note", value: notes });
+ }
+ }
+
+ const matchedComponentNames: string[] = [];
+ const impl = fileEntry.componentRef.spec.implementation;
+ if (componentQuery && isGraphImplementation(impl)) {
+ const normalizedQuery = componentQuery.toLowerCase();
+ const seen = new Set();
+ for (const task of Object.values(impl.graph.tasks)) {
+ const refName = task.componentRef.name ?? "";
+ const specName = task.componentRef.spec?.name ?? "";
+ for (const name of [refName, specName]) {
+ if (!name || seen.has(name)) continue;
+ if (!name.toLowerCase().includes(normalizedQuery)) continue;
+ seen.add(name);
+ matchedComponentNames.push(name);
+ }
+ }
+ }
+
+ return {
+ searchQuery,
+ matchedFields,
+ componentQuery,
+ matchedComponentNames,
+ };
+}
+
function matchesDateRange(
fileEntry: ComponentFileEntry,
dateRange: DateRange | undefined,
@@ -117,7 +178,14 @@ export function usePipelineFilters(pipelines: Map) {
(new Date(entryA.modificationTime).getTime() -
new Date(entryB.modificationTime).getTime())
);
- });
+ })
+ .map(
+ ([name, fileEntry]): PipelineEntry => [
+ name,
+ fileEntry,
+ getMatchMetadata(fileEntry, searchQuery, componentQuery),
+ ],
+ );
const filterKey = [
searchQuery,
diff --git a/src/components/shared/HighlightText.tsx b/src/components/shared/HighlightText.tsx
new file mode 100644
index 000000000..8babb2654
--- /dev/null
+++ b/src/components/shared/HighlightText.tsx
@@ -0,0 +1,27 @@
+interface HighlightTextProps {
+ text: string;
+ query: string | undefined;
+ className?: string;
+}
+
+function escapeRegex(str: string): string {
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+export function HighlightText({ text, query, className }: HighlightTextProps) {
+ if (!query) return {text};
+
+ const parts = text.split(new RegExp(`(${escapeRegex(query)})`, "gi"));
+
+ return (
+
+ {parts.map((part, i) =>
+ part.toLowerCase() === query.toLowerCase() ? (
+ {part}
+ ) : (
+ part
+ ),
+ )}
+
+ );
+}