Polish populated workbench states
This commit is contained in:
@@ -604,3 +604,13 @@ Added:
|
|||||||
- Preserved existing dataset detail loading, map layer state, export actions and API client behavior.
|
- Preserved existing dataset detail loading, map layer state, export actions and API client behavior.
|
||||||
- Added regression coverage for dataset quick actions and inspector navigation wiring.
|
- Added regression coverage for dataset quick actions and inspector navigation wiring.
|
||||||
- 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 54 populated-state UI polish (2026-06-17)
|
||||||
|
|
||||||
|
- Ran the live demo/export workflow against Tower and audited populated Data and Exports states.
|
||||||
|
- Changed the Data workspace to keep Project and AOI side by side while giving the Dataset catalog a full-width row.
|
||||||
|
- Compacted the Exports history to show the latest 10 artifacts by default with an explicit show-all toggle.
|
||||||
|
- Kept a visible Export Preview panel even before preview content is selected.
|
||||||
|
- Shortened displayed export paths while preserving the full path in the element title.
|
||||||
|
- Added regression coverage for populated-state Data layout, export limiting and export preview empty state.
|
||||||
|
- No API contracts, migrations, product capabilities, live provider fetching or AI/model dependency changes were introduced.
|
||||||
|
|||||||
@@ -31,8 +31,11 @@ def test_export_center_uses_artifact_actions_and_cards() -> None:
|
|||||||
assert 'className="latest-export-card"' in export_center
|
assert 'className="latest-export-card"' 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 "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
|
||||||
assert "onExportSegmentationRun" in export_center
|
assert "onExportSegmentationRun" in export_center
|
||||||
assert "download-only HTML artifact" in export_center
|
assert "download-only HTML artifact" in export_center
|
||||||
assert 'className="export-preview-panel"' in export_preview
|
assert 'className="export-preview-panel"' in export_preview
|
||||||
|
assert "No export preview selected." in export_preview
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ def test_dataset_panel_exposes_map_and_export_quick_actions() -> None:
|
|||||||
assert "disabled={!(dataset.dataset_type === 'vector' || dataset.dataset_type === 'geojson')}" in panel
|
assert "disabled={!(dataset.dataset_type === 'vector' || dataset.dataset_type === 'geojson')}" in panel
|
||||||
|
|
||||||
|
|
||||||
|
def test_data_workspace_keeps_catalog_wide_enough_for_populated_state() -> None:
|
||||||
|
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert ".workspace-grid-data {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n}" in css
|
||||||
|
assert ".workspace-grid-data > section:nth-child(3)" in css
|
||||||
|
assert "grid-column: 1 / -1" in css
|
||||||
|
assert ".dataset-card .button-row" in css
|
||||||
|
|
||||||
|
|
||||||
def test_app_wires_dataset_quick_actions_to_existing_workspaces() -> None:
|
def test_app_wires_dataset_quick_actions_to_existing_workspaces() -> None:
|
||||||
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|||||||
@@ -2175,3 +2175,27 @@ Limitations:
|
|||||||
|
|
||||||
Next recommended pass:
|
Next recommended pass:
|
||||||
- Verify dataset quick actions with demo data on Tower, then improve populated map/detail readability if needed.
|
- Verify dataset quick actions with demo data on Tower, then improve populated map/detail readability if needed.
|
||||||
|
|
||||||
|
## Sprint 54 populated-state UI polish (2026-06-17)
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
- Ran the live demo/export workflow against the Tower deployment to inspect real populated workbench states.
|
||||||
|
- Adjusted the Data workspace so Project and AOI remain side by side while the Dataset catalog spans the full row for readable populated dataset cards.
|
||||||
|
- Made dataset-card action rows responsive so Map/Exports/detail actions do not crowd or clip on populated cards.
|
||||||
|
- Limited the Exports artifact history to the latest 10 entries by default with an explicit show-all toggle.
|
||||||
|
- Kept the Export Preview panel visible even before an artifact is selected, avoiding a blank middle column in the Exports workspace.
|
||||||
|
- Shortened displayed export storage paths while keeping the full path available in the title attribute.
|
||||||
|
- Added regression coverage for the populated Data layout and Exports populated-state behavior.
|
||||||
|
|
||||||
|
Tested:
|
||||||
|
- `bash scripts/verify_demo_export_workflow.sh http://192.168.10.150:1202`
|
||||||
|
- Local Vite visual audit using the live Tower API proxy on `http://127.0.0.1:5176`
|
||||||
|
|
||||||
|
Open:
|
||||||
|
- Run full readiness, deploy Tower, then verify the polished populated states on `http://192.168.10.150:1202`.
|
||||||
|
|
||||||
|
Limitations:
|
||||||
|
- This pass remains UI polish only. It does not add features, change API contracts, alter migrations, fetch live providers or enable new AI models.
|
||||||
|
|
||||||
|
Next recommended pass:
|
||||||
|
- Add export history filtering or retention controls if artifact history continues to grow during demo runs.
|
||||||
|
|||||||
+2
-1
@@ -323,4 +323,5 @@ This file now starts with the current implementation status. Older preparation/b
|
|||||||
- [x] Polish QA/QC and Exports workspaces.
|
- [x] Polish QA/QC and Exports workspaces.
|
||||||
- [x] Add selected-object inspector detail tabs for project, AOI, dataset, QA check, export and AI run context.
|
- [x] Add selected-object inspector detail tabs for project, AOI, dataset, QA check, export and AI run context.
|
||||||
- [x] Improve map/dataset selection ergonomics from the workbench canvas and inspector.
|
- [x] Improve map/dataset selection ergonomics from the workbench canvas and inspector.
|
||||||
- [ ] Improve populated map/detail readability after a demo workflow run.
|
- [x] Improve populated Data/Exports readability after a demo workflow run.
|
||||||
|
- [ ] Add export history filtering or retention controls for long-running demo environments.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
import type { DatasetCreateResponse, ExportCreateResponse, ExportRead } from '../../types'
|
import type { DatasetCreateResponse, ExportCreateResponse, ExportRead } from '../../types'
|
||||||
|
|
||||||
interface ExportCenterProps {
|
interface ExportCenterProps {
|
||||||
@@ -34,6 +35,14 @@ function canPreviewJson(item: ExportRead): boolean {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compactPath(value: string): string {
|
||||||
|
const parts = value.split('/').filter(Boolean)
|
||||||
|
if (parts.length <= 2) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return parts.slice(-2).join('/')
|
||||||
|
}
|
||||||
|
|
||||||
export function ExportCenter({
|
export function ExportCenter({
|
||||||
selectedProjectId,
|
selectedProjectId,
|
||||||
selectedDataset,
|
selectedDataset,
|
||||||
@@ -53,9 +62,12 @@ export function ExportCenter({
|
|||||||
onPreviewContent,
|
onPreviewContent,
|
||||||
onDownload,
|
onDownload,
|
||||||
}: ExportCenterProps): JSX.Element {
|
}: ExportCenterProps): JSX.Element {
|
||||||
|
const [showAllExports, setShowAllExports] = useState(false)
|
||||||
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 hiddenExportCount = Math.max(exports.length - visibleExports.length, 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="export-center" data-testid="export-center">
|
<section className="export-center" data-testid="export-center">
|
||||||
@@ -126,8 +138,18 @@ 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 ? (
|
||||||
|
<div className="list-limit-banner">
|
||||||
|
<span>
|
||||||
|
Showing {visibleExports.length} latest exports. {hiddenExportCount > 0 ? `${hiddenExportCount} older artifacts hidden.` : 'All artifacts shown.'}
|
||||||
|
</span>
|
||||||
|
<button type="button" className="secondary-action" onClick={() => setShowAllExports((value) => !value)}>
|
||||||
|
{showAllExports ? 'Show latest 10' : 'Show all exports'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<ul className="export-list">
|
<ul className="export-list">
|
||||||
{exports.map((item) => (
|
{visibleExports.map((item) => (
|
||||||
<li className="export-card" key={item.id}>
|
<li className="export-card" key={item.id}>
|
||||||
<div className="export-card-header">
|
<div className="export-card-header">
|
||||||
<div>
|
<div>
|
||||||
@@ -138,7 +160,7 @@ export function ExportCenter({
|
|||||||
</div>
|
</div>
|
||||||
<span className={item.status === 'ready' ? 'status-badge status-badge-ready' : 'status-badge'}>{item.status}</span>
|
<span className={item.status === 'ready' ? 'status-badge status-badge-ready' : 'status-badge'}>{item.status}</span>
|
||||||
</div>
|
</div>
|
||||||
<p>{item.storage_path}</p>
|
<p title={item.storage_path}>{compactPath(item.storage_path)}</p>
|
||||||
<div className="button-row">
|
<div className="button-row">
|
||||||
{canPreviewJson(item) ? (
|
{canPreviewJson(item) ? (
|
||||||
<button type="button" className="secondary-action" onClick={() => onPreviewContent(item.id)}>
|
<button type="button" className="secondary-action" onClick={() => onPreviewContent(item.id)}>
|
||||||
|
|||||||
@@ -2,11 +2,7 @@ interface ExportPreviewProps {
|
|||||||
content: Record<string, unknown> | null
|
content: Record<string, unknown> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExportPreview({ content }: ExportPreviewProps): JSX.Element | null {
|
export function ExportPreview({ content }: ExportPreviewProps): JSX.Element {
|
||||||
if (!content) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="export-preview-panel">
|
<section className="export-preview-panel">
|
||||||
<div className="panel-title-row">
|
<div className="panel-title-row">
|
||||||
@@ -16,7 +12,14 @@ export function ExportPreview({ content }: ExportPreviewProps): JSX.Element | nu
|
|||||||
</div>
|
</div>
|
||||||
<span className="count-pill">preview</span>
|
<span className="count-pill">preview</span>
|
||||||
</div>
|
</div>
|
||||||
<pre className="job-result">{JSON.stringify(content, null, 2)}</pre>
|
{content ? (
|
||||||
|
<pre className="job-result">{JSON.stringify(content, null, 2)}</pre>
|
||||||
|
) : (
|
||||||
|
<div className="empty-state">
|
||||||
|
<strong>No export preview selected.</strong>
|
||||||
|
<p>Use Preview JSON content on a JSON or GeoJSON artifact to inspect the stored payload here.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -679,7 +679,7 @@ section li strong + div {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workspace-grid-data {
|
.workspace-grid-data {
|
||||||
grid-template-columns: minmax(16rem, 0.75fr) minmax(18rem, 0.95fr) minmax(22rem, 1.35fr);
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-grid-analysis,
|
.workspace-grid-analysis,
|
||||||
@@ -701,6 +701,10 @@ section li strong + div {
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-grid-data > section:nth-child(3) {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
.workspace-grid-ai > section {
|
.workspace-grid-ai > section {
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
}
|
}
|
||||||
@@ -943,6 +947,16 @@ button.entity-card {
|
|||||||
margin-top: 0.7rem;
|
margin-top: 0.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dataset-card .button-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataset-card .button-row button {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.button-row {
|
.button-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -1068,6 +1082,29 @@ button.entity-card {
|
|||||||
background: var(--accent-soft);
|
background: var(--accent-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-limit-banner {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 0.7rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.8rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.62rem 0.72rem;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-limit-banner span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.86rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-limit-banner button {
|
||||||
|
width: auto;
|
||||||
|
min-height: 2.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
.export-list {
|
.export-list {
|
||||||
margin-top: 0.8rem;
|
margin-top: 0.8rem;
|
||||||
}
|
}
|
||||||
@@ -1311,7 +1348,8 @@ button.entity-card {
|
|||||||
.quality-summary-grid,
|
.quality-summary-grid,
|
||||||
.quality-score-row,
|
.quality-score-row,
|
||||||
.export-action-grid,
|
.export-action-grid,
|
||||||
.inspector-action-bar {
|
.inspector-action-bar,
|
||||||
|
.list-limit-banner {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user