From 18431a8c68ca8b4b5e17bdc6035dccbf63a61ea0 Mon Sep 17 00:00:00 2001 From: mbeaulne Date: Mon, 2 Mar 2026 10:32:07 -0500 Subject: [PATCH] Pipeline filters: content highlight --- react-compiler.config.js | 1 + .../Home/PipelineSection/PipelineRow.tsx | 65 ++++++++++++++++- .../Home/PipelineSection/PipelineSection.tsx | 6 +- .../PipelineSection/usePipelineFilters.ts | 72 ++++++++++++++++++- src/components/shared/HighlightText.tsx | 27 +++++++ 5 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 src/components/shared/HighlightText.tsx 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 + ), + )} + + ); +}