Polish export handoff artifacts
This commit is contained in:
@@ -51,6 +51,17 @@ function formatExportType(value: string): string {
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
function formatExportCreated(value?: string | null): string {
|
||||
if (!value) {
|
||||
return 'created time unavailable'
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function getLatestExportByType(exports: ExportRead[], exportType: string): ExportRead | null {
|
||||
return exports.find((item) => item.export_type === exportType) ?? null
|
||||
}
|
||||
|
||||
function exportMatchesSearch(item: ExportRead, query: string): boolean {
|
||||
const normalizedQuery = query.trim().toLowerCase()
|
||||
if (!normalizedQuery) {
|
||||
@@ -90,6 +101,41 @@ export function ExportCenter({
|
||||
const handoffStatus = selectedProjectId ? 'project selected' : 'select project'
|
||||
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 latestHandoffArtifacts = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'project_report_html',
|
||||
label: 'Project report',
|
||||
detail: 'HTML report for human review',
|
||||
item: getLatestExportByType(exports, 'project_report_html'),
|
||||
},
|
||||
{
|
||||
key: 'project_metadata_json',
|
||||
label: 'Project metadata',
|
||||
detail: 'Machine-readable project inventory',
|
||||
item: getLatestExportByType(exports, 'project_metadata_json'),
|
||||
},
|
||||
{
|
||||
key: 'dataset_geojson',
|
||||
label: 'Dataset GeoJSON',
|
||||
detail: 'Selected vector/reference layer',
|
||||
item: getLatestExportByType(exports, 'dataset_geojson'),
|
||||
},
|
||||
{
|
||||
key: 'detection_geojson',
|
||||
label: 'Detection GeoJSON',
|
||||
detail: 'Persisted detection geometries',
|
||||
item: getLatestExportByType(exports, 'detection_geojson'),
|
||||
},
|
||||
{
|
||||
key: 'segmentation_geojson',
|
||||
label: 'Segmentation GeoJSON',
|
||||
detail: 'Persisted segmentation polygons',
|
||||
item: getLatestExportByType(exports, 'segmentation_geojson'),
|
||||
},
|
||||
],
|
||||
[exports],
|
||||
)
|
||||
const filteredExports = useMemo(
|
||||
() =>
|
||||
exports.filter((item) => {
|
||||
@@ -161,6 +207,42 @@ export function ExportCenter({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="latest-artifact-surface" aria-label="Latest handoff artifacts">
|
||||
<div>
|
||||
<h3>Latest handoff artifacts</h3>
|
||||
<p className="muted">Open the newest artifact of each handoff type without searching the full export history.</p>
|
||||
</div>
|
||||
<div className="latest-artifact-grid">
|
||||
{latestHandoffArtifacts.map((artifact) => {
|
||||
const item = artifact.item
|
||||
return (
|
||||
<div className={item ? 'latest-artifact-card' : 'latest-artifact-card latest-artifact-card-empty'} key={artifact.key}>
|
||||
<span>{artifact.label}</span>
|
||||
<strong>{item ? item.status : 'No artifact yet'}</strong>
|
||||
<p>{item ? compactPath(item.storage_path) : artifact.detail}</p>
|
||||
{item ? (
|
||||
<>
|
||||
<small>{formatExportCreated(item.created_at)}</small>
|
||||
<div className="latest-artifact-actions">
|
||||
{canPreviewJson(item) ? (
|
||||
<button type="button" className="secondary-action" onClick={() => onPreviewContent(item.id)}>
|
||||
Preview
|
||||
</button>
|
||||
) : (
|
||||
<span className="status-badge">download-only</span>
|
||||
)}
|
||||
<button type="button" className="primary-action" onClick={() => onDownload(item.id)}>
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="export-actions-surface" aria-label="Export artifact actions">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
|
||||
@@ -1831,6 +1831,7 @@ button.entity-card {
|
||||
|
||||
.export-summary-surface,
|
||||
.export-handoff-surface,
|
||||
.latest-artifact-surface,
|
||||
.export-actions-surface,
|
||||
.export-history-surface,
|
||||
.system-provider-capability-surface {
|
||||
@@ -1849,6 +1850,10 @@ button.entity-card {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.latest-artifact-surface {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.export-center-shell .quality-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
|
||||
@@ -1977,6 +1982,69 @@ button.entity-card {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.latest-artifact-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.latest-artifact-card {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.36rem;
|
||||
border: 1px solid rgba(15, 118, 110, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 0.72rem;
|
||||
background: linear-gradient(180deg, #f8fcfa, #ffffff);
|
||||
}
|
||||
|
||||
.latest-artifact-card-empty {
|
||||
border-style: dashed;
|
||||
border-color: var(--line);
|
||||
background: var(--panel-soft);
|
||||
}
|
||||
|
||||
.latest-artifact-card span {
|
||||
color: var(--muted);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 850;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.latest-artifact-card strong {
|
||||
overflow: hidden;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.24;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.latest-artifact-card p,
|
||||
.latest-artifact-card small {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.35;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.latest-artifact-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.45rem;
|
||||
align-items: center;
|
||||
margin-top: 0.18rem;
|
||||
}
|
||||
|
||||
.latest-artifact-actions button,
|
||||
.latest-artifact-actions .status-badge {
|
||||
width: 100%;
|
||||
min-height: 2.1rem;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.export-type-badge {
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user