Add export history controls
This commit is contained in:
@@ -625,3 +625,13 @@ Added:
|
|||||||
- Reset page scroll on workspace changes so workspaces open from their heading instead of inheriting stale scroll positions.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -29,9 +29,17 @@ def test_export_center_uses_artifact_actions_and_cards() -> None:
|
|||||||
assert 'className="export-center"' in export_center
|
assert 'className="export-center"' in export_center
|
||||||
assert 'className="export-action-grid"' in export_center
|
assert 'className="export-action-grid"' in export_center
|
||||||
assert 'className="latest-export-card"' 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-list"' in export_center
|
||||||
assert 'className="export-card"' 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 "Show all exports" in export_center
|
||||||
assert "onExportDataset" in export_center
|
assert "onExportDataset" in export_center
|
||||||
assert "onExportDetectionRun" in export_center
|
assert "onExportDetectionRun" in export_center
|
||||||
|
|||||||
@@ -2240,3 +2240,30 @@ Limitations:
|
|||||||
|
|
||||||
Next recommended pass:
|
Next recommended pass:
|
||||||
- Continue with export history filtering or retention controls if repeated demo runs keep growing artifact history.
|
- 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.
|
||||||
|
|||||||
+2
-1
@@ -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 map/dataset selection ergonomics from the workbench canvas and inspector.
|
||||||
- [x] Improve populated Data/Exports readability after a demo workflow run.
|
- [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.
|
- [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.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import type { DatasetCreateResponse, ExportCreateResponse, ExportRead } from '../../types'
|
import type { DatasetCreateResponse, ExportCreateResponse, ExportRead } from '../../types'
|
||||||
|
|
||||||
interface ExportCenterProps {
|
interface ExportCenterProps {
|
||||||
@@ -43,6 +43,16 @@ function compactPath(value: string): string {
|
|||||||
return parts.slice(-2).join('/')
|
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({
|
export function ExportCenter({
|
||||||
selectedProjectId,
|
selectedProjectId,
|
||||||
selectedDataset,
|
selectedDataset,
|
||||||
@@ -63,11 +73,26 @@ export function ExportCenter({
|
|||||||
onDownload,
|
onDownload,
|
||||||
}: ExportCenterProps): JSX.Element {
|
}: ExportCenterProps): JSX.Element {
|
||||||
const [showAllExports, setShowAllExports] = useState(false)
|
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 canExportDataset = Boolean(selectedDataset && isVectorDatasetType(selectedDataset.dataset_type))
|
||||||
const geojsonExports = exports.filter((item) => item.export_type.includes('geojson')).length
|
const geojsonExports = exports.filter((item) => item.export_type.includes('geojson')).length
|
||||||
const reportExports = exports.filter((item) => item.export_type === 'project_report_html').length
|
const reportExports = exports.filter((item) => item.export_type === 'project_report_html').length
|
||||||
const visibleExports = showAllExports ? exports : exports.slice(0, 10)
|
const exportTypes = useMemo(() => Array.from(new Set(exports.map((item) => item.export_type))).sort(), [exports])
|
||||||
const hiddenExportCount = Math.max(exports.length - visibleExports.length, 0)
|
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 (
|
return (
|
||||||
<section className="export-center" data-testid="export-center">
|
<section className="export-center" data-testid="export-center">
|
||||||
@@ -138,16 +163,71 @@ export function ExportCenter({
|
|||||||
<p>Create metadata, GeoJSON or HTML report artifacts once the active project has data to hand off.</p>
|
<p>Create metadata, GeoJSON or HTML report artifacts once the active project has data to hand off.</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{exports.length > 10 ? (
|
{exports.length > 0 ? (
|
||||||
|
<div className="export-history-controls" aria-label="Export history filters">
|
||||||
|
<label>
|
||||||
|
Search exports
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
placeholder="Type, id or path"
|
||||||
|
value={exportSearchQuery}
|
||||||
|
onChange={(event) => setExportSearchQuery(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Type
|
||||||
|
<select value={exportTypeFilter} onChange={(event) => setExportTypeFilter(event.target.value)}>
|
||||||
|
<option value="all">All types</option>
|
||||||
|
{exportTypes.map((type) => (
|
||||||
|
<option key={type} value={type}>
|
||||||
|
{type}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Status
|
||||||
|
<select value={exportStatusFilter} onChange={(event) => setExportStatusFilter(event.target.value)}>
|
||||||
|
<option value="all">All statuses</option>
|
||||||
|
{exportStatuses.map((status) => (
|
||||||
|
<option key={status} value={status}>
|
||||||
|
{status}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="secondary-action"
|
||||||
|
onClick={() => {
|
||||||
|
setExportSearchQuery('')
|
||||||
|
setExportTypeFilter('all')
|
||||||
|
setExportStatusFilter('all')
|
||||||
|
setShowAllExports(false)
|
||||||
|
}}
|
||||||
|
disabled={!hasActiveExportFilters && !showAllExports}
|
||||||
|
>
|
||||||
|
Reset view
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{filteredExports.length > 10 ? (
|
||||||
<div className="list-limit-banner">
|
<div className="list-limit-banner">
|
||||||
<span>
|
<span>
|
||||||
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.'}
|
||||||
</span>
|
</span>
|
||||||
<button type="button" className="secondary-action" onClick={() => setShowAllExports((value) => !value)}>
|
<button type="button" className="secondary-action" onClick={() => setShowAllExports((value) => !value)}>
|
||||||
{showAllExports ? 'Show latest 10' : 'Show all exports'}
|
{showAllExports ? 'Show latest 10' : 'Show all exports'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{exports.length > 0 && filteredExports.length === 0 ? (
|
||||||
|
<div className="empty-state">
|
||||||
|
<strong>No exports match the current filters.</strong>
|
||||||
|
<p>Clear the search, type or status filter to return to the latest artifacts.</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<ul className="export-list">
|
<ul className="export-list">
|
||||||
{visibleExports.map((item) => (
|
{visibleExports.map((item) => (
|
||||||
<li className="export-card" key={item.id}>
|
<li className="export-card" key={item.id}>
|
||||||
|
|||||||
@@ -1075,7 +1075,7 @@ button.entity-card {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.export-action-grid button {
|
.export-action-grid button {
|
||||||
min-height: 3.2rem;
|
min-height: 2.8rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1087,6 +1087,27 @@ button.entity-card {
|
|||||||
background: var(--accent-soft);
|
background: var(--accent-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.export-history-controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(14rem, 1.2fr) minmax(10rem, 0.9fr) minmax(9rem, 0.8fr) auto;
|
||||||
|
gap: 0.65rem;
|
||||||
|
align-items: end;
|
||||||
|
margin-top: 0.8rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.72rem;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-history-controls label {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-history-controls button {
|
||||||
|
min-height: 2.35rem;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.list-limit-banner {
|
.list-limit-banner {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
@@ -1378,6 +1399,7 @@ button.entity-card {
|
|||||||
.quality-summary-grid,
|
.quality-summary-grid,
|
||||||
.quality-score-row,
|
.quality-score-row,
|
||||||
.export-action-grid,
|
.export-action-grid,
|
||||||
|
.export-history-controls,
|
||||||
.inspector-action-bar,
|
.inspector-action-bar,
|
||||||
.list-limit-banner {
|
.list-limit-banner {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
Reference in New Issue
Block a user