diff --git a/.autopilot/runs/fleet-overview-filters.jsonl b/.autopilot/runs/fleet-overview-filters.jsonl index 28b430e..5651741 100644 --- a/.autopilot/runs/fleet-overview-filters.jsonl +++ b/.autopilot/runs/fleet-overview-filters.jsonl @@ -1 +1,2 @@ {"type": "plan", "feature_id": "fleet-overview-filters", "goal": "Make the Remediation Fleet Overview default to actionable repos: two independent, combinable toggle filters \u2014 'only repos with open PRs' (openPrCount > 0) and 'only eligible repos' (eligible === true), both ON by default \u2014 each switchable to show all, with a distinct empty state when the filters hide every row.", "trunk": "main", "base": "develop", "autonomy": "pr_ci", "phases": [{"id": 0, "goal": "Default-actionable Fleet Overview: two combinable toggle filters (both ON by default) + a distinct filtered-empty state, with vitest specs and EN i18n.", "definition_of_done": ["cmd: make test-frontend passes, incl. new FleetOverview.test.tsx specs: default view hides a 0-PR repo AND an ineligible repo; toggling onlyWithPrs OFF reveals the 0-PR repo; toggling onlyEligible OFF reveals the ineligible repo; with both filters active and all rows excluded, the filtered-empty state renders; clearing filters restores rows.", "cmd: make lint-frontend passes; make format-check passes; pnpm --dir frontend type-check passes.", "grep: onlyWithPrs in frontend/src/components/remediation/FleetOverview.tsx", "grep: fleet.filters in frontend/public/locales/en/remediation.json", "grep:absent: TODO|FIXME in frontend/src/components/remediation/FleetOverview.tsx", "prose: no backend/API change \u2014 the filtering is purely client-side over the existing useFleetRemediation() rows; the loading/error/zero-repos branches are unchanged."]}, {"id": 1, "goal": "Internationalize the new fleet-filter keys across all 27 locales and keep the generated types in sync.", "definition_of_done": ["cmd: node scripts/i18n-coverage-report.js --check-missing reports no missing keys.", "cmd: node scripts/i18n-coverage-report.js --check --min-coverage 95 passes.", "cmd: make test-frontend passes (incl. the i18n translation-keys suite).", "cmd: make lint-frontend passes; make format-check passes.", "prose: no raw i18n keys render; interpolation-variable audit across locales is clean; brand/proper nouns are not translated; frontend/src/i18n/types.ts is regenerated in sync (CI 'Verify types are up-to-date' would pass) and no spaced locale directory exists."]}], "at": "2026-07-01T12:46:12-07:00"} +{"phase": 0, "mode": "pr_ci", "verdict": "PASSED", "skipped": [], "failed": [], "ci_attempts": 0, "pr": null, "accelerators": ["direct", "adversarial-review"], "marker": null, "at": "2026-07-01T13:00:04-07:00", "summary": "Fleet Overview default-actionable filters: two combinable toggles (onlyWithPrs/onlyEligible) both default ON over client-side filtered rows; distinct filtered-empty state w/ Show-all escape + always-visible toggles; a11y group role + aria-live. 13 FleetOverview specs (each filter revealed independently, round-trip, single-toggle escape, revealed-row preview), full suite 924 pass; lint/format/type clean. EN i18n only (fleet.filters.*); 27-locale translate is phase 1. Reviews: no correctness bugs; workflow's both-ON-default + i18n-fallback findings are WAD/phase-1-deferred per spec."} diff --git a/frontend/public/locales/ar/remediation.json b/frontend/public/locales/ar/remediation.json index 703097a..4bb46ed 100644 --- a/frontend/public/locales/ar/remediation.json +++ b/frontend/public/locales/ar/remediation.json @@ -129,6 +129,13 @@ "eligible": "صالح", "empty": "لم يتم العثور على أي مستودعات.", "error": "فشل تحميل نظرة عامة على الأسطول.", + "filters": { + "clear": "عرض جميع المستودعات", + "groupLabel": "مرشحات المستودع", + "noMatch": "لا توجد مستودعات تتطابق مع عوامل التصفية الحالية.", + "onlyEligible": "المستودعات المؤهلة فقط", + "onlyWithPrs": "المستودعات التي تحتوي على طلبات سحب مفتوحة فقط" + }, "notEligible": "غير مؤهل", "preview": "معاينة", "tableLabel": "حالة معالجة المستودع", diff --git a/frontend/public/locales/cs/remediation.json b/frontend/public/locales/cs/remediation.json index 0d87b4b..fd20ac9 100644 --- a/frontend/public/locales/cs/remediation.json +++ b/frontend/public/locales/cs/remediation.json @@ -129,6 +129,13 @@ "eligible": "Oprávněný", "empty": "Nenalezeny žádné repozitáře.", "error": "Nepodařilo se načíst přehled flotily.", + "filters": { + "clear": "Zobrazit všechny repozitáře", + "groupLabel": "Filtry repozitáře", + "noMatch": "Aktuálním filtrům neodpovídají žádné repozitáře.", + "onlyEligible": "Pouze způsobilé repozitáře", + "onlyWithPrs": "Pouze repozitáře s otevřenými pull requesty" + }, "notEligible": "Nezpůsobilé", "preview": "Náhled", "tableLabel": "Stav sanace repozitáře", diff --git a/frontend/public/locales/da/remediation.json b/frontend/public/locales/da/remediation.json index 0c98eee..c6de7a1 100644 --- a/frontend/public/locales/da/remediation.json +++ b/frontend/public/locales/da/remediation.json @@ -129,6 +129,13 @@ "eligible": "Berettiget", "empty": "Ingen lagre fundet.", "error": "Flådeoversigten kunne ikke indlæses.", + "filters": { + "clear": "Vis alle arkiver", + "groupLabel": "Filtre til arkivet", + "noMatch": "Ingen arkiver matcher de nuværende filtre.", + "onlyEligible": "Kun berettigede lagre", + "onlyWithPrs": "Kun arkiver med åbne pull-anmodninger" + }, "notEligible": "Ikke berettiget", "preview": "Forhåndsvisning", "tableLabel": "Status for afhjælpning af arkivet", diff --git a/frontend/public/locales/de/remediation.json b/frontend/public/locales/de/remediation.json index 2169a0a..f017359 100644 --- a/frontend/public/locales/de/remediation.json +++ b/frontend/public/locales/de/remediation.json @@ -129,6 +129,13 @@ "eligible": "Berechtigt", "empty": "Keine Repositories gefunden.", "error": "Die Flottenübersicht konnte nicht geladen werden.", + "filters": { + "clear": "Alle Repositories anzeigen", + "groupLabel": "Repository-Filter", + "noMatch": "Es gibt keine Repositories, die den aktuellen Filtern entsprechen.", + "onlyEligible": "Nur berechtigte Repositorien", + "onlyWithPrs": "Nur Repositories mit offenen Pull-Anfragen" + }, "notEligible": "Nicht berechtigt", "preview": "Vorschau", "tableLabel": "Repository-Sanierungsstatus", diff --git a/frontend/public/locales/en-GB/remediation.json b/frontend/public/locales/en-GB/remediation.json index 845aa52..114480d 100644 --- a/frontend/public/locales/en-GB/remediation.json +++ b/frontend/public/locales/en-GB/remediation.json @@ -129,6 +129,13 @@ "eligible": "Eligible", "empty": "No repositories found.", "error": "Failed to load the fleet overview.", + "filters": { + "clear": "Show all repositories", + "groupLabel": "Repository filters", + "noMatch": "No repositories match the current filters.", + "onlyEligible": "Only eligible repositories", + "onlyWithPrs": "Only repositories with open pull requests" + }, "notEligible": "Not eligible", "preview": "Preview", "tableLabel": "Repository remediation status", diff --git a/frontend/public/locales/en/remediation.json b/frontend/public/locales/en/remediation.json index af29237..0ccdb35 100644 --- a/frontend/public/locales/en/remediation.json +++ b/frontend/public/locales/en/remediation.json @@ -28,6 +28,13 @@ "policyState": "Policy", "airGapped": "Air-gap", "actions": "Actions" + }, + "filters": { + "groupLabel": "Repository filters", + "onlyWithPrs": "Only repositories with open pull requests", + "onlyEligible": "Only eligible repositories", + "noMatch": "No repositories match the current filters.", + "clear": "Show all repositories" } }, "policyState": { diff --git a/frontend/public/locales/es-ES/remediation.json b/frontend/public/locales/es-ES/remediation.json index 56f77ed..8d2c6ff 100644 --- a/frontend/public/locales/es-ES/remediation.json +++ b/frontend/public/locales/es-ES/remediation.json @@ -129,6 +129,13 @@ "eligible": "Elegible", "empty": "No se encontraron repositorios.", "error": "No se pudo cargar la descripción general de la flota.", + "filters": { + "clear": "Mostrar todos los repositorios", + "groupLabel": "Filtros del repositorio", + "noMatch": "No se encontraron repositorios que coincidan con los filtros actuales.", + "onlyEligible": "Solo repositorios elegibles", + "onlyWithPrs": "Solo repositorios con solicitudes de extracción abiertas" + }, "notEligible": "No elegible", "preview": "Avance", "tableLabel": "Estado de remediación del repositorio", diff --git a/frontend/public/locales/es-MX/remediation.json b/frontend/public/locales/es-MX/remediation.json index f75c9d8..82cd7f0 100644 --- a/frontend/public/locales/es-MX/remediation.json +++ b/frontend/public/locales/es-MX/remediation.json @@ -129,6 +129,13 @@ "eligible": "Elegible", "empty": "No se encontraron repositorios.", "error": "No se pudo cargar la descripción general de la flota.", + "filters": { + "clear": "Mostrar todos los repositorios", + "groupLabel": "Filtros del repositorio", + "noMatch": "No se encontraron repositorios que coincidan con los filtros actuales.", + "onlyEligible": "Solo repositorios elegibles", + "onlyWithPrs": "Solo repositorios con solicitudes de extracción abiertas" + }, "notEligible": "No elegible", "preview": "Avance", "tableLabel": "Estado de remediación del repositorio", diff --git a/frontend/public/locales/fi/remediation.json b/frontend/public/locales/fi/remediation.json index ed81d66..201710f 100644 --- a/frontend/public/locales/fi/remediation.json +++ b/frontend/public/locales/fi/remediation.json @@ -129,6 +129,13 @@ "eligible": "Tukikelpoinen", "empty": "Ei löytynyt arkistoja.", "error": "Kaluston yleiskatsauksen lataaminen epäonnistui.", + "filters": { + "clear": "Näytä kaikki tietovarastot", + "groupLabel": "Tietovaraston suodattimet", + "noMatch": "Yksikään arkisto ei vastaa nykyisiä suodattimia.", + "onlyEligible": "Vain kelvolliset arkistot", + "onlyWithPrs": "Vain avoimia pull-pyyntöjä sisältävät tietovarastot" + }, "notEligible": "Ei kelvollinen", "preview": "Esikatselu", "tableLabel": "Arkiston kunnostuksen tila", diff --git a/frontend/public/locales/fr/remediation.json b/frontend/public/locales/fr/remediation.json index 1c1d83a..799f51a 100644 --- a/frontend/public/locales/fr/remediation.json +++ b/frontend/public/locales/fr/remediation.json @@ -129,6 +129,13 @@ "eligible": "Admissible", "empty": "Aucun dépôt trouvé.", "error": "Impossible de charger l'aperçu de la flotte.", + "filters": { + "clear": "Afficher tous les dépôts", + "groupLabel": "Filtres de dépôt", + "noMatch": "Aucun dépôt ne correspond aux filtres actuels.", + "onlyEligible": "Seuls les dépôts éligibles", + "onlyWithPrs": "Seuls les dépôts avec des demandes de fusion ouvertes" + }, "notEligible": "Non admissible", "preview": "Aperçu", "tableLabel": "État de la correction du référentiel", diff --git a/frontend/public/locales/he/remediation.json b/frontend/public/locales/he/remediation.json index f28f7e9..7e215ee 100644 --- a/frontend/public/locales/he/remediation.json +++ b/frontend/public/locales/he/remediation.json @@ -129,6 +129,13 @@ "eligible": "זכאי", "empty": "לא נמצאו מאגרים.", "error": "טעינת סקירת הצי נכשלה.", + "filters": { + "clear": "הצג את כל המאגרים", + "groupLabel": "מסנני מאגר", + "noMatch": "אין מאגרים התואמים את המסננים הנוכחיים.", + "onlyEligible": "רק מאגרים זכאים", + "onlyWithPrs": "רק מאגרים עם בקשות משיכה פתוחות" + }, "notEligible": "לא זכאי", "preview": "תצוגה מקדימה", "tableLabel": "סטטוס תיקון המאגר", diff --git a/frontend/public/locales/hi/remediation.json b/frontend/public/locales/hi/remediation.json index ad6e98c..35d20e9 100644 --- a/frontend/public/locales/hi/remediation.json +++ b/frontend/public/locales/hi/remediation.json @@ -129,6 +129,13 @@ "eligible": "योग्य", "empty": "कोई रिपॉजिटरी नहीं मिली।", "error": "बेड़े का अवलोकन लोड करने में विफल।", + "filters": { + "clear": "सभी रिपॉजिटरी दिखाएँ", + "groupLabel": "रिपॉजिटरी फ़िल्टर", + "noMatch": "वर्तमान फ़िल्टरों से कोई रिपॉजिटरी मेल नहीं खाती।", + "onlyEligible": "केवल पात्र रिपॉजिटरी", + "onlyWithPrs": "केवल ओपन पुल रिक्वेस्ट वाले रिपॉजिटरी" + }, "notEligible": "पात्र नहीं है", "preview": "पूर्व दर्शन", "tableLabel": "रिपॉजिटरी सुधार स्थिति", diff --git a/frontend/public/locales/it/remediation.json b/frontend/public/locales/it/remediation.json index 51ea171..137d55f 100644 --- a/frontend/public/locales/it/remediation.json +++ b/frontend/public/locales/it/remediation.json @@ -129,6 +129,13 @@ "eligible": "Idoneo", "empty": "Nessun repository trovato.", "error": "Impossibile caricare la panoramica della flotta.", + "filters": { + "clear": "Mostra tutti i repository", + "groupLabel": "Filtri del repository", + "noMatch": "Nessun repository corrisponde ai filtri correnti.", + "onlyEligible": "Solo repository idonei", + "onlyWithPrs": "Solo repository con richieste di pull aperte" + }, "notEligible": "Non idoneo", "preview": "Anteprima", "tableLabel": "Stato di bonifica del repository", diff --git a/frontend/public/locales/ja/remediation.json b/frontend/public/locales/ja/remediation.json index a238008..91f3d26 100644 --- a/frontend/public/locales/ja/remediation.json +++ b/frontend/public/locales/ja/remediation.json @@ -129,6 +129,13 @@ "eligible": "適格", "empty": "リポジトリが見つかりませんでした。", "error": "艦隊概要の読み込みに失敗しました。", + "filters": { + "clear": "すべてのリポジトリを表示", + "groupLabel": "リポジトリフィルター", + "noMatch": "現在のフィルター条件に一致するリポジトリはありません。", + "onlyEligible": "対象となるリポジトリのみ", + "onlyWithPrs": "オープンなプルリクエストがあるリポジトリのみ" + }, "notEligible": "対象外", "preview": "プレビュー", "tableLabel": "リポジトリ修復状況", diff --git a/frontend/public/locales/ko/remediation.json b/frontend/public/locales/ko/remediation.json index 8e7b446..3a07c5e 100644 --- a/frontend/public/locales/ko/remediation.json +++ b/frontend/public/locales/ko/remediation.json @@ -129,6 +129,13 @@ "eligible": "자격이 있는", "empty": "저장소를 찾을 수 없습니다.", "error": "함대 개요를 불러오는 데 실패했습니다.", + "filters": { + "clear": "모든 저장소를 표시합니다", + "groupLabel": "저장소 필터", + "noMatch": "현재 필터와 일치하는 저장소가 없습니다.", + "onlyEligible": "자격 요건을 충족하는 저장소만 해당됩니다.", + "onlyWithPrs": "풀 리퀘스트가 열려 있는 저장소만 표시됩니다." + }, "notEligible": "자격 없음", "preview": "시사", "tableLabel": "저장소 복구 상태", diff --git a/frontend/public/locales/nl/remediation.json b/frontend/public/locales/nl/remediation.json index 11f3ee3..a6da1dd 100644 --- a/frontend/public/locales/nl/remediation.json +++ b/frontend/public/locales/nl/remediation.json @@ -129,6 +129,13 @@ "eligible": "In aanmerking komend", "empty": "Geen repositories gevonden.", "error": "Het laden van het vlootoverzicht is mislukt.", + "filters": { + "clear": "Toon alle repositories", + "groupLabel": "Repositoryfilters", + "noMatch": "Er zijn geen repositories gevonden die aan de huidige filters voldoen.", + "onlyEligible": "Alleen in aanmerking komende repositories", + "onlyWithPrs": "Alleen repositories met openstaande pull requests" + }, "notEligible": "Niet in aanmerking komend", "preview": "Voorbeeld", "tableLabel": "Status van de herstelwerkzaamheden aan de repository", diff --git a/frontend/public/locales/no/remediation.json b/frontend/public/locales/no/remediation.json index 56992ca..f7ba107 100644 --- a/frontend/public/locales/no/remediation.json +++ b/frontend/public/locales/no/remediation.json @@ -129,6 +129,13 @@ "eligible": "Berettiget", "empty": "Ingen arkiver funnet.", "error": "Kunne ikke laste inn flåteoversikten.", + "filters": { + "clear": "Vis alle arkiver", + "groupLabel": "Filtre for arkivet", + "noMatch": "Ingen arkiver samsvarer med de gjeldende filtrene.", + "onlyEligible": "Kun kvalifiserte arkiver", + "onlyWithPrs": "Bare arkiver med åpne pull-forespørsler" + }, "notEligible": "Ikke kvalifisert", "preview": "Forhåndsvisning", "tableLabel": "Status for utbedring av repositoriet", diff --git a/frontend/public/locales/pl/remediation.json b/frontend/public/locales/pl/remediation.json index d4a8a1f..12beddd 100644 --- a/frontend/public/locales/pl/remediation.json +++ b/frontend/public/locales/pl/remediation.json @@ -129,6 +129,13 @@ "eligible": "Odpowiedni", "empty": "Nie znaleziono repozytoriów.", "error": "Nie udało się załadować przeglądu floty.", + "filters": { + "clear": "Pokaż wszystkie repozytoria", + "groupLabel": "Filtry repozytorium", + "noMatch": "Żadne repozytoria nie spełniają wymagań bieżących filtrów.", + "onlyEligible": "Tylko kwalifikujące się repozytoria", + "onlyWithPrs": "Tylko repozytoria z otwartymi prośbami o ściągnięcie" + }, "notEligible": "Nie kwalifikuje się", "preview": "Zapowiedź", "tableLabel": "Status naprawy repozytorium", diff --git a/frontend/public/locales/pt-BR/remediation.json b/frontend/public/locales/pt-BR/remediation.json index 54eb8c1..cb5da03 100644 --- a/frontend/public/locales/pt-BR/remediation.json +++ b/frontend/public/locales/pt-BR/remediation.json @@ -129,6 +129,13 @@ "eligible": "Elegível", "empty": "Nenhum repositório encontrado.", "error": "Falha ao carregar a visão geral da frota.", + "filters": { + "clear": "Mostrar todos os repositórios", + "groupLabel": "Filtros de repositório", + "noMatch": "Nenhum repositório corresponde aos filtros atuais.", + "onlyEligible": "Somente repositórios elegíveis", + "onlyWithPrs": "Apenas repositórios com solicitações de pull abertas" + }, "notEligible": "Não elegível", "preview": "Pré-visualização", "tableLabel": "Status de remediação do repositório", diff --git a/frontend/public/locales/ru/remediation.json b/frontend/public/locales/ru/remediation.json index 501be82..3b93180 100644 --- a/frontend/public/locales/ru/remediation.json +++ b/frontend/public/locales/ru/remediation.json @@ -129,6 +129,13 @@ "eligible": "Имеющий право", "empty": "Репозитории не найдены.", "error": "Не удалось загрузить обзор флота.", + "filters": { + "clear": "Показать все репозитории", + "groupLabel": "Фильтры репозитория", + "noMatch": "Ни один репозиторий не соответствует текущим фильтрам.", + "onlyEligible": "Только подходящие репозитории", + "onlyWithPrs": "Только репозитории с открытыми запросами на слияние (pull requests)." + }, "notEligible": "Не подходит", "preview": "Предварительный просмотр", "tableLabel": "Статус восстановления репозитория", diff --git a/frontend/public/locales/sr/remediation.json b/frontend/public/locales/sr/remediation.json index 539ca95..4b96632 100644 --- a/frontend/public/locales/sr/remediation.json +++ b/frontend/public/locales/sr/remediation.json @@ -129,6 +129,13 @@ "eligible": "Испуњава услове", "empty": "Није пронађено ниједно спремиште.", "error": "Учитавање прегледа возног парка није успело.", + "filters": { + "clear": "Прикажи све репозиторијуме", + "groupLabel": "Филтери спремишта", + "noMatch": "Ниједан репозиторијум не одговара тренутним филтерима.", + "onlyEligible": "Само подобна спремишта", + "onlyWithPrs": "Само репозиторијуми са отвореним захтевима за повлачење (pull requests)" + }, "notEligible": "Не испуњава услове", "preview": "Преглед", "tableLabel": "Статус санације спремишта", diff --git a/frontend/public/locales/sv/remediation.json b/frontend/public/locales/sv/remediation.json index 1c032ee..62bf833 100644 --- a/frontend/public/locales/sv/remediation.json +++ b/frontend/public/locales/sv/remediation.json @@ -129,6 +129,13 @@ "eligible": "Berättigad", "empty": "Inga arkiv hittades.", "error": "Det gick inte att läsa in översikten över flottan.", + "filters": { + "clear": "Visa alla arkiv", + "groupLabel": "Filter för arkivet", + "noMatch": "Inga arkiv matchar de aktuella filtren.", + "onlyEligible": "Endast behöriga arkiv", + "onlyWithPrs": "Endast arkiv med öppna pull requests" + }, "notEligible": "Inte berättigad", "preview": "Förhandsvisning", "tableLabel": "Status för sanering av arkivet", diff --git a/frontend/public/locales/th/remediation.json b/frontend/public/locales/th/remediation.json index 3f3b77c..9312ff1 100644 --- a/frontend/public/locales/th/remediation.json +++ b/frontend/public/locales/th/remediation.json @@ -129,6 +129,13 @@ "eligible": "มีสิทธิ์", "empty": "ไม่พบแหล่งเก็บข้อมูลใดๆ", "error": "ไม่สามารถโหลดภาพรวมของฝูงบินได้", + "filters": { + "clear": "แสดงที่เก็บข้อมูลทั้งหมด", + "groupLabel": "ตัวกรองคลังข้อมูล", + "noMatch": "ไม่มีแหล่งเก็บข้อมูลใดตรงกับตัวกรองปัจจุบัน", + "onlyEligible": "เฉพาะแหล่งเก็บข้อมูลที่มีคุณสมบัติเหมาะสมเท่านั้น", + "onlyWithPrs": "เฉพาะ repository ที่มี pull request เปิดอยู่เท่านั้น" + }, "notEligible": "ไม่มีสิทธิ์", "preview": "ตัวอย่าง", "tableLabel": "สถานะการแก้ไขคลังเก็บข้อมูล", diff --git a/frontend/public/locales/tr/remediation.json b/frontend/public/locales/tr/remediation.json index 4874928..5b73bfa 100644 --- a/frontend/public/locales/tr/remediation.json +++ b/frontend/public/locales/tr/remediation.json @@ -129,6 +129,13 @@ "eligible": "Hak sahibi", "empty": "Hiçbir depo bulunamadı.", "error": "Filo genel görünümü yüklenemedi.", + "filters": { + "clear": "Tüm depoları göster", + "groupLabel": "Depo filtreleri", + "noMatch": "Mevcut filtrelere uyan hiçbir depo bulunamadı.", + "onlyEligible": "Yalnızca uygun depolar", + "onlyWithPrs": "Yalnızca açık çekme istekleri olan depolar" + }, "notEligible": "Uygun değil", "preview": "Önizleme", "tableLabel": "Depo düzeltme durumu", diff --git a/frontend/public/locales/vi/remediation.json b/frontend/public/locales/vi/remediation.json index 6ac6863..46bfc05 100644 --- a/frontend/public/locales/vi/remediation.json +++ b/frontend/public/locales/vi/remediation.json @@ -129,6 +129,13 @@ "eligible": "Đạt chuẩn", "empty": "Không tìm thấy kho lưu trữ nào.", "error": "Không thể tải thông tin tổng quan về đội xe.", + "filters": { + "clear": "Hiển thị tất cả kho lưu trữ", + "groupLabel": "Bộ lọc kho lưu trữ", + "noMatch": "Không có kho lưu trữ nào phù hợp với các bộ lọc hiện tại.", + "onlyEligible": "Chỉ các kho lưu trữ đủ điều kiện", + "onlyWithPrs": "Chỉ những kho lưu trữ có yêu cầu kéo (pull request) đang mở mới được chấp nhận." + }, "notEligible": "Không đủ điều kiện", "preview": "Xem trước", "tableLabel": "Trạng thái khắc phục kho lưu trữ", diff --git a/frontend/public/locales/zh-CN/remediation.json b/frontend/public/locales/zh-CN/remediation.json index d3c5997..cb7e0d2 100644 --- a/frontend/public/locales/zh-CN/remediation.json +++ b/frontend/public/locales/zh-CN/remediation.json @@ -129,6 +129,13 @@ "eligible": "有资格的", "empty": "未找到存储库。", "error": "无法加载舰队概览。", + "filters": { + "clear": "显示所有存储库", + "groupLabel": "存储库过滤器", + "noMatch": "没有符合当前筛选条件的存储库。", + "onlyEligible": "仅限符合条件的存储库", + "onlyWithPrs": "仅包含未关闭的拉取请求的仓库" + }, "notEligible": "不符合资格", "preview": "预览", "tableLabel": "存储库修复状态", diff --git a/frontend/public/locales/zh-TW/remediation.json b/frontend/public/locales/zh-TW/remediation.json index 5f7f943..7dec429 100644 --- a/frontend/public/locales/zh-TW/remediation.json +++ b/frontend/public/locales/zh-TW/remediation.json @@ -129,6 +129,13 @@ "eligible": "有資格的", "empty": "未找到存儲庫。", "error": "無法載入艦隊概覽。", + "filters": { + "clear": "顯示所有儲存庫", + "groupLabel": "儲存庫過濾器", + "noMatch": "沒有符合目前篩選條件的儲存庫。", + "onlyEligible": "僅限符合條件的儲存庫", + "onlyWithPrs": "僅限有未完成拉取請求的倉庫" + }, "notEligible": "不符合資格", "preview": "預覽", "tableLabel": "儲存庫修復狀態", diff --git a/frontend/src/components/remediation/FleetOverview.test.tsx b/frontend/src/components/remediation/FleetOverview.test.tsx index 118e8f7..dd2e6bf 100644 --- a/frontend/src/components/remediation/FleetOverview.test.tsx +++ b/frontend/src/components/remediation/FleetOverview.test.tsx @@ -1,5 +1,5 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { FleetOverview } from './FleetOverview'; import type { ConsolidationPlan, FleetRow } from '@/types/remediation'; @@ -20,6 +20,11 @@ vi.mock('@/hooks/useFleetRemediation', () => ({ usePreviewRepository: () => mockUsePreview(), })); +// Fixture spans all four filter quadrants so each toggle can be proven independently: +// app — has PRs + eligible → visible by default (actionable) +// lib — no PRs + eligible → hidden only by "only with PRs" +// tool — has PRs + not eligible → hidden only by "only eligible" +// old — no PRs + not eligible → hidden by both (also air-gapped) const rows: FleetRow[] = [ { repositoryId: 'repo-1', @@ -33,50 +38,185 @@ const rows: FleetRow[] = [ repositoryId: 'repo-2', name: 'acme/lib', openPrCount: 0, + eligible: true, + policyState: 'suggest', + airGapped: false, + }, + { + repositoryId: 'repo-3', + name: 'acme/tool', + openPrCount: 3, + eligible: false, + policyState: 'none', + airGapped: false, + }, + { + repositoryId: 'repo-4', + name: 'acme/old', + openPrCount: 0, eligible: false, policyState: 'none', airGapped: true, }, ]; +const onlyWithPrsToggle = () => screen.getByLabelText('remediation:fleet.filters.onlyWithPrs'); +const onlyEligibleToggle = () => screen.getByLabelText('remediation:fleet.filters.onlyEligible'); + beforeEach(() => { vi.clearAllMocks(); mockUsePreview.mockReturnValue({ mutate: mockPreviewMutate, isPending: false, isError: false }); }); describe('FleetOverview', () => { - it('should_renderRepositoryRows_when_dataLoaded', () => { + it('should_showOnlyActionableRows_when_defaultFiltersActive', () => { mockUseFleet.mockReturnValue({ data: rows, isLoading: false, isError: false }); render(); + // Default: both filters ON — only the has-PRs + eligible repo shows. expect(screen.getByText('acme/app')).toBeInTheDocument(); + // The 0-PR repo and the ineligible repo are hidden by default. + expect(screen.queryByText('acme/lib')).not.toBeInTheDocument(); + expect(screen.queryByText('acme/tool')).not.toBeInTheDocument(); + expect(screen.queryByText('acme/old')).not.toBeInTheDocument(); + }); + + it('should_showEveryActionableRow_when_multipleReposPassDefaults', () => { + // Two rows both satisfy the default filters (PRs > 0 AND eligible): both must show, + // guarding against a predicate that is accidentally too strict on a passing row. + const twoActionable: FleetRow[] = [ + { ...rows[0], repositoryId: 'a', name: 'acme/one', openPrCount: 2, eligible: true }, + { ...rows[0], repositoryId: 'b', name: 'acme/two', openPrCount: 9, eligible: true }, + ]; + mockUseFleet.mockReturnValue({ data: twoActionable, isLoading: false, isError: false }); + + render(); + + expect(screen.getByText('acme/one')).toBeInTheDocument(); + expect(screen.getByText('acme/two')).toBeInTheDocument(); + }); + + it('should_reapplyFilter_when_toggledOffThenBackOn', async () => { + const user = userEvent.setup(); + mockUseFleet.mockReturnValue({ data: rows, isLoading: false, isError: false }); + + render(); + // Off → the 0-PR eligible repo appears. + await user.click(onlyWithPrsToggle()); expect(screen.getByText('acme/lib')).toBeInTheDocument(); + // On again → it is hidden once more (state round-trips correctly). + await user.click(onlyWithPrsToggle()); + expect(screen.queryByText('acme/lib')).not.toBeInTheDocument(); + expect(screen.getByText('acme/app')).toBeInTheDocument(); }); - it('should_renderEligibilityBadges_when_dataLoaded', () => { + it('should_revealZeroPrRepo_when_onlyWithPrsToggledOff', async () => { + const user = userEvent.setup(); mockUseFleet.mockReturnValue({ data: rows, isLoading: false, isError: false }); render(); + await user.click(onlyWithPrsToggle()); - expect(screen.getByText('remediation:fleet.eligible')).toBeInTheDocument(); - expect(screen.getByText('remediation:fleet.notEligible')).toBeInTheDocument(); + // onlyWithPrs OFF, onlyEligible still ON → eligible repos regardless of PR count. + expect(screen.getByText('acme/app')).toBeInTheDocument(); + expect(screen.getByText('acme/lib')).toBeInTheDocument(); + // Still hidden: the ineligible ones (onlyEligible remains active). + expect(screen.queryByText('acme/tool')).not.toBeInTheDocument(); + expect(screen.queryByText('acme/old')).not.toBeInTheDocument(); }); - it('should_renderAirGappedIndicator_when_repoAirGapped', () => { + it('should_revealIneligibleRepo_when_onlyEligibleToggledOff', async () => { + const user = userEvent.setup(); mockUseFleet.mockReturnValue({ data: rows, isLoading: false, isError: false }); render(); + await user.click(onlyEligibleToggle()); + + // onlyEligible OFF, onlyWithPrs still ON → repos with PRs regardless of eligibility. + expect(screen.getByText('acme/app')).toBeInTheDocument(); + expect(screen.getByText('acme/tool')).toBeInTheDocument(); + // Still hidden: the 0-PR ones (onlyWithPrs remains active). + expect(screen.queryByText('acme/lib')).not.toBeInTheDocument(); + expect(screen.queryByText('acme/old')).not.toBeInTheDocument(); + }); + + it('should_showAllRows_when_bothFiltersToggledOff', async () => { + const user = userEvent.setup(); + mockUseFleet.mockReturnValue({ data: rows, isLoading: false, isError: false }); + render(); + await user.click(onlyWithPrsToggle()); + await user.click(onlyEligibleToggle()); + + expect(screen.getByText('acme/app')).toBeInTheDocument(); + expect(screen.getByText('acme/lib')).toBeInTheDocument(); + expect(screen.getByText('acme/tool')).toBeInTheDocument(); + expect(screen.getByText('acme/old')).toBeInTheDocument(); + // Both eligibility badges and the air-gapped indicator are now visible. + expect(screen.getAllByText('remediation:fleet.notEligible').length).toBeGreaterThan(0); + expect(screen.getAllByText('remediation:fleet.eligible').length).toBeGreaterThan(0); expect(screen.getByText('remediation:fleet.airGappedOn')).toBeInTheDocument(); }); + it('should_renderFilteredEmptyState_when_activeFiltersHideEveryRow', () => { + // A fleet with no actionable repos: default filters exclude everything. + mockUseFleet.mockReturnValue({ + data: [rows[3]], // acme/old: 0 PRs + ineligible + isLoading: false, + isError: false, + }); + + render(); + + expect(screen.getByText('remediation:fleet.filters.noMatch')).toBeInTheDocument(); + // Distinct from the zero-repositories empty state. + expect(screen.queryByText('remediation:fleet.empty')).not.toBeInTheDocument(); + expect(screen.queryByText('acme/old')).not.toBeInTheDocument(); + }); + + it('should_restoreRows_when_filtersClearedFromEmptyState', async () => { + const user = userEvent.setup(); + mockUseFleet.mockReturnValue({ + data: [rows[3]], // acme/old: hidden by default filters + isLoading: false, + isError: false, + }); + + render(); + expect(screen.getByText('remediation:fleet.filters.noMatch')).toBeInTheDocument(); + + await user.click(screen.getByRole('button', { name: /remediation:fleet.filters.clear/ })); + + // Clearing the filters widens the view so the previously-hidden repo appears. + expect(screen.getByText('acme/old')).toBeInTheDocument(); + expect(screen.queryByText('remediation:fleet.filters.noMatch')).not.toBeInTheDocument(); + }); + + it('should_escapeFilteredEmptyState_when_singleToggleFlippedOff', async () => { + const user = userEvent.setup(); + // acme/tool: has PRs but ineligible → hidden only by onlyEligible. + mockUseFleet.mockReturnValue({ data: [rows[2]], isLoading: false, isError: false }); + + render(); + expect(screen.getByText('remediation:fleet.filters.noMatch')).toBeInTheDocument(); + + // The toggles remain available in the empty state; flipping the one filter that + // hides the row reveals it — no need for the nuclear "clear". + await user.click(onlyEligibleToggle()); + + expect(screen.getByText('acme/tool')).toBeInTheDocument(); + expect(screen.queryByText('remediation:fleet.filters.noMatch')).not.toBeInTheDocument(); + }); + it('should_renderEmptyState_when_noRepositories', () => { mockUseFleet.mockReturnValue({ data: [], isLoading: false, isError: false }); render(); expect(screen.getByText('remediation:fleet.empty')).toBeInTheDocument(); + // Not the filtered-empty state — there are genuinely zero repositories. + expect(screen.queryByText('remediation:fleet.filters.noMatch')).not.toBeInTheDocument(); }); it('should_invokePreview_when_previewClicked', async () => { @@ -85,6 +225,7 @@ describe('FleetOverview', () => { render(); + // acme/app is visible under the default filters. const previewButtons = screen.getAllByRole('button', { name: /remediation:fleet.preview/ }); await user.click(previewButtons[0]); @@ -117,4 +258,19 @@ describe('FleetOverview', () => { await waitFor(() => expect(screen.getByText('Bump deps')).toBeInTheDocument()); }); + + it('should_previewRevealedRow_when_previewClickedAfterToggle', async () => { + const user = userEvent.setup(); + mockUseFleet.mockReturnValue({ data: rows, isLoading: false, isError: false }); + + render(); + // acme/tool is hidden by default; widen the view so it renders, then preview it. + await user.click(onlyEligibleToggle()); + const toolRow = screen.getByText('acme/tool').closest('tr') as HTMLElement; + await user.click(within(toolRow).getByRole('button', { name: /remediation:fleet.preview/ })); + + await waitFor(() => + expect(mockPreviewMutate).toHaveBeenCalledWith('repo-3', expect.anything()) + ); + }); }); diff --git a/frontend/src/components/remediation/FleetOverview.tsx b/frontend/src/components/remediation/FleetOverview.tsx index 45bd9da..da1d155 100644 --- a/frontend/src/components/remediation/FleetOverview.tsx +++ b/frontend/src/components/remediation/FleetOverview.tsx @@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next'; import { Eye, ShieldOff } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; import { Dialog, DialogContent, @@ -38,6 +40,10 @@ export function FleetOverview() { const previewMutation = usePreviewRepository(); const [activeRepo, setActiveRepo] = useState(null); const [plan, setPlan] = useState(null); + // Default to the actionable rows: repos with open PRs that are remediation-eligible. + // Either filter can be toggled off independently to widen the view. + const [onlyWithPrs, setOnlyWithPrs] = useState(true); + const [onlyEligible, setOnlyEligible] = useState(true); const handlePreview = (row: FleetRow) => { setActiveRepo(row); @@ -71,8 +77,59 @@ export function FleetOverview() { ); } + // Active filters AND together; toggling either off relaxes it. Purely client-side over + // the already-fetched rows — no backend/API involvement. + const filteredFleet = fleet.filter( + (row) => (!onlyWithPrs || row.openPrCount > 0) && (!onlyEligible || row.eligible) + ); + + const clearFilters = () => { + setOnlyWithPrs(false); + setOnlyEligible(false); + }; + + const filters = ( +
+
+ + +
+
+ + +
+
+ ); + + if (filteredFleet.length === 0) { + return ( + <> + {filters} +
+

{t('remediation:fleet.filters.noMatch')}

+ +
+ + ); + } + return ( <> + {filters}
@@ -98,7 +155,7 @@ export function FleetOverview() { - {fleet.map((row) => ( + {filteredFleet.map((row) => ( diff --git a/frontend/src/i18n/types.ts b/frontend/src/i18n/types.ts index e07f4a7..2f5100b 100644 --- a/frontend/src/i18n/types.ts +++ b/frontend/src/i18n/types.ts @@ -899,6 +899,13 @@ export interface RemediationTranslations { eligible: string; empty: string; error: string; + filters: { + clear: string; + groupLabel: string; + noMatch: string; + onlyEligible: string; + onlyWithPrs: string; + }; notEligible: string; preview: string; tableLabel: string;
{row.name} {row.openPrCount}