From 95740f352dafb05194958f05fa59f7118d48e70b Mon Sep 17 00:00:00 2001 From: Renato Guimaraes Date: Wed, 17 Jun 2026 11:26:42 -0300 Subject: [PATCH] feat(compare): filter the ranking table by repository name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the search input already used by the /[tenant]/repos list so the compare table can be narrowed by name. The input only appears once the org has more than five repos (same threshold as repo-list) to avoid clutter on small tenants. Best/worst highlights still reference the full set — the filter narrows the visible rows, not the comparison universe. Co-Authored-By: Claude Opus 4.7 (1M context) --- platform/lib/translations.ts | 4 ++ .../src/app/[tenant]/compare/compare-view.tsx | 57 ++++++++++++++++--- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/platform/lib/translations.ts b/platform/lib/translations.ts index c272ff6..cbd3460 100644 --- a/platform/lib/translations.ts +++ b/platform/lib/translations.ts @@ -1299,6 +1299,8 @@ export const translations = { ranking: "Repository Ranking", sortBy: "Sort by", sortHint: "Click a column header to sort", + searchPlaceholder: "Filter by repository...", + noMatches: "No repositories match “{query}”.", columns: { repository: "Repository", stabilization: "Stabilization", @@ -2695,6 +2697,8 @@ export const translations = { ranking: "Ranking de Repositórios", sortBy: "Ordenar por", sortHint: "Clique no cabeçalho de uma coluna para ordenar", + searchPlaceholder: "Filtrar por repositório...", + noMatches: "Nenhum repositório corresponde a “{query}”.", columns: { repository: "Repositório", stabilization: "Estabilização", diff --git a/platform/src/app/[tenant]/compare/compare-view.tsx b/platform/src/app/[tenant]/compare/compare-view.tsx index 266f918..b47fdf1 100644 --- a/platform/src/app/[tenant]/compare/compare-view.tsx +++ b/platform/src/app/[tenant]/compare/compare-view.tsx @@ -2,6 +2,8 @@ import { useMemo, useState } from "react"; +import { Search } from "lucide-react"; + import { Sparkline } from "@/components/charts/Sparkline"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { useTranslation } from "@/hooks/useTranslation"; @@ -9,6 +11,10 @@ import { cn } from "@/lib/utils"; import type { RepoSummary } from "@/types/temporal"; import { healthIndicator } from "@/types/temporal"; +// Show the search input only when there are enough repos for it to be useful. +// Mirrors the threshold used by RepoList on /[tenant]/repos. +const SEARCH_MIN_REPOS = 5; + interface CompareViewProps { repos: RepoSummary[]; } @@ -211,6 +217,7 @@ export function CompareView({ repos }: CompareViewProps) { key: "stabilization_ratio", dir: "desc", }); + const [query, setQuery] = useState(""); function handleSort(key: SortKey) { setSort((prev) => @@ -220,8 +227,10 @@ export function CompareView({ repos }: CompareViewProps) { ); } - // Best/worst highlights are independent of the chosen sort — they reflect the - // extreme across the whole set. + // Best/worst highlights span the whole set, NOT the filtered view: the user + // is filtering to focus, not to redefine the comparison universe. A repo + // with 60% stabilization stays highlighted yellow even when it's the only + // one matching the filter. const allStab = repos .map((r) => r.stabilization_ratio) .filter((v): v is number => v !== null); @@ -245,8 +254,13 @@ export function CompareView({ repos }: CompareViewProps) { .filter((v): v is number => v !== null && v > 0); const hasAnyAI = allAI.length > 0; + const filtered = useMemo(() => { + const q = query.trim().toLowerCase(); + return q ? repos.filter((r) => r.name.toLowerCase().includes(q)) : repos; + }, [repos, query]); + const sorted = useMemo(() => { - const arr = [...repos]; + const arr = [...filtered]; arr.sort((a, b) => { const av = sortValue(a, sort.key); const bv = sortValue(b, sort.key); @@ -261,7 +275,7 @@ export function CompareView({ repos }: CompareViewProps) { return sort.dir === "asc" ? cmp : -cmp; }); return arr; - }, [repos, sort]); + }, [filtered, sort]); if (repos.length === 0) { return ( @@ -289,14 +303,41 @@ export function CompareView({ repos }: CompareViewProps) { { key: "health", label: t("compare.columns.health") }, ]; + const showSearch = repos.length > SEARCH_MIN_REPOS; + const noMatches = + showSearch && query.trim().length > 0 && sorted.length === 0; + return ( - + {t("compare.ranking")} + {showSearch && ( +
+ + setQuery(e.target.value)} + placeholder={t("compare.searchPlaceholder")} + aria-label={t("compare.searchPlaceholder")} + className="w-full rounded-md border border-border bg-card py-2 pl-9 pr-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring" + /> +
+ )}
+ {noMatches && ( +

+ {t("compare.noMatches", { query: query.trim() })} +

+ )} {/* Desktop / tablet: table */} -
+
@@ -416,7 +457,9 @@ export function CompareView({ repos }: CompareViewProps) { {/* Mobile: sort control + card stack */} -
+