From 212be9ae4864a7a4ee1ff148ad044782541c3d18 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 17 Jun 2026 23:26:27 +0200 Subject: [PATCH] Add export history controls --- CHANGELOG.md | 10 +++ .../test_sprint51_quality_export_polish.py | 10 ++- docs/CODEX_EXECUTION_LOG.md | 27 ++++++ docs/TODO.md | 3 +- .../src/components/exports/ExportCenter.tsx | 90 +++++++++++++++++-- frontend/src/styles/app.css | 24 ++++- 6 files changed, 156 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddb7eca7..a6946647 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -625,3 +625,13 @@ Added: - Reset page scroll on workspace changes so workspaces open from their heading instead of inheriting stale scroll positions. - Added regression coverage for the standard-desktop layout and workspace scroll reset. - No API contracts, migrations, product capabilities, live provider fetching or AI/model dependency changes were introduced. + +## Sprint 56 export history controls (2026-06-17) + +- Added frontend-only export history search across export type, status, id and storage path. +- Added export type and status filters based on the currently loaded export records. +- Made the existing latest-10 export limiter operate on filtered results instead of the whole export list. +- Added a no-match empty state and reset view action for filtered export history. +- Slightly compacted export action buttons so filters are visible earlier on standard desktop viewports. +- Added regression coverage for filtering controls, filtered list limiting and the no-match state. +- No API contracts, migrations, product capabilities, live provider fetching or AI/model dependency changes were introduced. diff --git a/backend/tests/test_sprint51_quality_export_polish.py b/backend/tests/test_sprint51_quality_export_polish.py index cfe9b1af..c9c7a8dc 100644 --- a/backend/tests/test_sprint51_quality_export_polish.py +++ b/backend/tests/test_sprint51_quality_export_polish.py @@ -29,9 +29,17 @@ def test_export_center_uses_artifact_actions_and_cards() -> None: assert 'className="export-center"' in export_center assert 'className="export-action-grid"' in export_center assert 'className="latest-export-card"' in export_center + assert 'className="export-history-controls"' in export_center assert 'className="export-list"' in export_center assert 'className="export-card"' in export_center - assert "visibleExports = showAllExports ? exports : exports.slice(0, 10)" in export_center + assert "exportTypeFilter" in export_center + assert "exportStatusFilter" in export_center + assert "exportSearchQuery" in export_center + assert "exportMatchesSearch" in export_center + assert "filteredExports = useMemo" in export_center + assert "visibleExports = showAllExports ? filteredExports : filteredExports.slice(0, 10)" in export_center + assert "No exports match the current filters." in export_center + assert "Reset view" in export_center assert "Show all exports" in export_center assert "onExportDataset" in export_center assert "onExportDetectionRun" in export_center diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 1f6b3752..3027c0b4 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -2240,3 +2240,30 @@ Limitations: Next recommended pass: - Continue with export history filtering or retention controls if repeated demo runs keep growing artifact history. + +## Sprint 56 export history controls (2026-06-17) + +Changed: +- Added frontend-only search for export type, status, id and storage path in the Exports workspace. +- Added export type and status filters generated from the loaded export records. +- Made the latest-10 limiter apply after filtering so large histories stay manageable without hiding matching records unexpectedly. +- Added a clear no-match empty state and reset view action. +- Slightly compacted export action buttons so the history controls are visible earlier on standard desktop viewports. +- Added regression coverage for the export history controls, filtered list limiting and no-match state. + +Tested: +- `cd frontend && npm run typecheck` +- `cd backend && python -m pytest tests/test_sprint51_quality_export_polish.py -q` +- `cd frontend && npm run build` +- Local Browser visual audit against `http://127.0.0.1:5178` using the live Tower API proxy. +- Verified Exports history controls render with 50 live artifacts and a `report` search narrows the list to 17 matching artifacts. +- Browser console error/warning check returned no entries. + +Open: +- Run full readiness, deploy Tower and verify the live Exports filters on `http://192.168.10.150:1202`. + +Limitations: +- This pass remains frontend UI hardening only. It does not add backend filtering, retention deletion, API changes, migrations, live provider fetching or AI model behavior. + +Next recommended pass: +- Consider a safe export retention/cleanup command if the artifact table keeps growing beyond demo needs. diff --git a/docs/TODO.md b/docs/TODO.md index 67468d4a..c42720d5 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -325,4 +325,5 @@ This file now starts with the current implementation status. Older preparation/b - [x] Improve map/dataset selection ergonomics from the workbench canvas and inspector. - [x] Improve populated Data/Exports readability after a demo workflow run. - [x] Improve live visual shell width, scroll behavior and Map workspace layout at 1280px. -- [ ] Add export history filtering or retention controls for long-running demo environments. +- [x] Add export history filtering controls for long-running demo environments. +- [ ] Add a safe export retention/cleanup command for demo environments. diff --git a/frontend/src/components/exports/ExportCenter.tsx b/frontend/src/components/exports/ExportCenter.tsx index 0d3ffc48..7e60aefb 100644 --- a/frontend/src/components/exports/ExportCenter.tsx +++ b/frontend/src/components/exports/ExportCenter.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useMemo, useState } from 'react' import type { DatasetCreateResponse, ExportCreateResponse, ExportRead } from '../../types' interface ExportCenterProps { @@ -43,6 +43,16 @@ function compactPath(value: string): string { return parts.slice(-2).join('/') } +function exportMatchesSearch(item: ExportRead, query: string): boolean { + const normalizedQuery = query.trim().toLowerCase() + if (!normalizedQuery) { + return true + } + return [item.export_type, item.status, item.id, item.storage_path] + .filter(Boolean) + .some((value) => String(value).toLowerCase().includes(normalizedQuery)) +} + export function ExportCenter({ selectedProjectId, selectedDataset, @@ -63,11 +73,26 @@ export function ExportCenter({ onDownload, }: ExportCenterProps): JSX.Element { const [showAllExports, setShowAllExports] = useState(false) + const [exportTypeFilter, setExportTypeFilter] = useState('all') + const [exportStatusFilter, setExportStatusFilter] = useState('all') + const [exportSearchQuery, setExportSearchQuery] = useState('') const canExportDataset = Boolean(selectedDataset && isVectorDatasetType(selectedDataset.dataset_type)) const geojsonExports = exports.filter((item) => item.export_type.includes('geojson')).length const reportExports = exports.filter((item) => item.export_type === 'project_report_html').length - const visibleExports = showAllExports ? exports : exports.slice(0, 10) - const hiddenExportCount = Math.max(exports.length - visibleExports.length, 0) + const exportTypes = useMemo(() => Array.from(new Set(exports.map((item) => item.export_type))).sort(), [exports]) + const exportStatuses = useMemo(() => Array.from(new Set(exports.map((item) => item.status))).sort(), [exports]) + const filteredExports = useMemo( + () => + exports.filter((item) => { + const typeMatches = exportTypeFilter === 'all' || item.export_type === exportTypeFilter + const statusMatches = exportStatusFilter === 'all' || item.status === exportStatusFilter + return typeMatches && statusMatches && exportMatchesSearch(item, exportSearchQuery) + }), + [exports, exportTypeFilter, exportStatusFilter, exportSearchQuery], + ) + const visibleExports = showAllExports ? filteredExports : filteredExports.slice(0, 10) + const hiddenExportCount = Math.max(filteredExports.length - visibleExports.length, 0) + const hasActiveExportFilters = exportTypeFilter !== 'all' || exportStatusFilter !== 'all' || exportSearchQuery.trim().length > 0 return (
@@ -138,16 +163,71 @@ export function ExportCenter({

Create metadata, GeoJSON or HTML report artifacts once the active project has data to hand off.

) : null} - {exports.length > 10 ? ( + {exports.length > 0 ? ( +
+ + + + +
+ ) : null} + {filteredExports.length > 10 ? (
- Showing {visibleExports.length} latest exports. {hiddenExportCount > 0 ? `${hiddenExportCount} older artifacts hidden.` : 'All artifacts shown.'} + Showing {visibleExports.length} of {filteredExports.length} matching exports. + {hiddenExportCount > 0 ? ` ${hiddenExportCount} older artifacts hidden.` : ' All matching artifacts shown.'}
) : null} + {exports.length > 0 && filteredExports.length === 0 ? ( +
+ No exports match the current filters. +

Clear the search, type or status filter to return to the latest artifacts.

+
+ ) : null}