chore: initialize insar management system v2
This commit is contained in:
@@ -0,0 +1,448 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { getPsBatches } from './api/taskBatches';
|
||||
import {
|
||||
createTimeseriesRun,
|
||||
getTimeseriesRunDetail,
|
||||
listTimeseriesRuns,
|
||||
} from './api/timeseriesProduction';
|
||||
|
||||
const card = {
|
||||
background: '#fff',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #e2e8f0',
|
||||
marginBottom: '12px',
|
||||
};
|
||||
|
||||
const STATUS_COLOR = {
|
||||
PENDING: '#64748b',
|
||||
RUNNING: '#2563eb',
|
||||
PREPARING: '#2563eb',
|
||||
PREPARED: '#16a34a',
|
||||
STACK_PREPARING: '#d97706',
|
||||
STACK_PREPARED: '#0891b2',
|
||||
MATERIALIZING: '#7c3aed',
|
||||
MATERIALIZED: '#9333ea',
|
||||
STACK_READY: '#15803d',
|
||||
STACK_RUNNING: '#ea580c',
|
||||
STACK_COMPLETED: '#16a34a',
|
||||
MINTPY_RUNNING: '#2563eb',
|
||||
MINTPY_COMPLETED: '#0f766e',
|
||||
EXPORTING: '#c2410c',
|
||||
EXPORTED: '#0891b2',
|
||||
REGISTERING: '#7c3aed',
|
||||
FAILED: '#dc2626',
|
||||
PUBLISHED: '#16a34a',
|
||||
};
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) return '-';
|
||||
try {
|
||||
return new Date(value).toLocaleString();
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function StatusPill({ value }) {
|
||||
const color = STATUS_COLOR[value] || '#64748b';
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
padding: '2px 10px',
|
||||
borderRadius: 999,
|
||||
background: `${color}14`,
|
||||
color,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: '50%',
|
||||
background: color,
|
||||
display: 'inline-block',
|
||||
}}
|
||||
/>
|
||||
{value || 'UNKNOWN'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TimeseriesProductionPanel({ readOnly = false, onJobQueued }) {
|
||||
const [batches, setBatches] = useState([]);
|
||||
const [runs, setRuns] = useState([]);
|
||||
const [selectedBatchId, setSelectedBatchId] = useState('');
|
||||
const [selectedRunId, setSelectedRunId] = useState('');
|
||||
const [selectedRunDetail, setSelectedRunDetail] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [runName, setRunName] = useState('');
|
||||
const [referenceDate, setReferenceDate] = useState('');
|
||||
const [waterMaskMode, setWaterMaskMode] = useState('synthetic_fallback');
|
||||
const [notes, setNotes] = useState('');
|
||||
|
||||
const selectedBatch = useMemo(
|
||||
() => batches.find(item => item.batch_id === selectedBatchId) || null,
|
||||
[batches, selectedBatchId]
|
||||
);
|
||||
|
||||
const loadBatches = useCallback(async () => {
|
||||
try {
|
||||
const data = await getPsBatches();
|
||||
const nextItems = Array.isArray(data) ? data : [];
|
||||
setBatches(nextItems);
|
||||
setSelectedBatchId(current => current || nextItems[0]?.batch_id || '');
|
||||
} catch {
|
||||
setBatches([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadRuns = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await listTimeseriesRuns({ limit: 20, offset: 0 });
|
||||
const nextItems = Array.isArray(data?.items) ? data.items : [];
|
||||
setRuns(nextItems);
|
||||
setSelectedRunId(current => current || nextItems[0]?.run_id || '');
|
||||
} catch {
|
||||
setRuns([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadRunDetail = useCallback(async runId => {
|
||||
if (!runId) {
|
||||
setSelectedRunDetail(null);
|
||||
return;
|
||||
}
|
||||
setDetailLoading(true);
|
||||
try {
|
||||
const detail = await getTimeseriesRunDetail(runId);
|
||||
setSelectedRunDetail(detail);
|
||||
} catch (error) {
|
||||
setSelectedRunDetail({
|
||||
error: error?.response?.data?.detail || error.message || '运行详情加载失败',
|
||||
});
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadBatches();
|
||||
loadRuns();
|
||||
}, [loadBatches, loadRuns]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(loadRuns, 10000);
|
||||
return () => clearInterval(timer);
|
||||
}, [loadRuns]);
|
||||
|
||||
useEffect(() => {
|
||||
loadRunDetail(selectedRunId);
|
||||
}, [loadRunDetail, selectedRunId]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!selectedBatchId) {
|
||||
setMessage('请先选择一个 PS 批次。');
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
setMessage('');
|
||||
try {
|
||||
const result = await createTimeseriesRun({
|
||||
batch_id: selectedBatchId,
|
||||
run_name: runName.trim() || null,
|
||||
reference_date: referenceDate.trim() || null,
|
||||
water_mask_mode: waterMaskMode,
|
||||
notes: notes.trim() || null,
|
||||
});
|
||||
setMessage(`运行已入队:${result.run_id} / task=${result.task_id}`);
|
||||
onJobQueued?.(result.task_id);
|
||||
await loadRuns();
|
||||
} catch (error) {
|
||||
setMessage(error?.response?.data?.detail || error.message || '运行提交失败');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const runData = selectedRunDetail?.run || null;
|
||||
const linkedProduct = selectedRunDetail?.product || null;
|
||||
const workflowSteps = selectedRunDetail?.workflow?.steps || [];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '16px', maxWidth: 1080 }}>
|
||||
<div style={card}>
|
||||
<strong style={{ fontSize: 14, display: 'block', marginBottom: 10 }}>SBAS 生产入口</strong>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: '#475569',
|
||||
lineHeight: 1.6,
|
||||
padding: '8px 10px',
|
||||
background: '#f8fafc',
|
||||
border: '1px solid #e2e8f0',
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
当前系统阶段已接入完整八步链路:prepare、stack_prep_initial、materialize、stack_prep_refresh、
|
||||
run_isce2_stack、run_mintpy_sbas、export_publish_bundle、register_psinsar_product。
|
||||
提交后系统会依次生成选栈 manifest、物化 LT-1 SLC、执行 ISCE2 stack、运行 MintPy SBAS、
|
||||
导出 publish bundle,并把结果注册进 PS-InSAR catalog。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={card}>
|
||||
<strong style={{ fontSize: 14, display: 'block', marginBottom: 10 }}>新建运行</strong>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 10 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#64748b', marginBottom: 4 }}>PS 批次</div>
|
||||
<select
|
||||
value={selectedBatchId}
|
||||
onChange={event => setSelectedBatchId(event.target.value)}
|
||||
disabled={readOnly || submitting}
|
||||
style={{ width: '100%', padding: '6px 8px', borderRadius: 6, border: '1px solid #cbd5e1' }}
|
||||
>
|
||||
<option value="">-- 请选择批次 --</option>
|
||||
{batches.map(item => (
|
||||
<option key={item.batch_id} value={item.batch_id}>
|
||||
{item.name || item.batch_id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#64748b', marginBottom: 4 }}>运行名称</div>
|
||||
<input
|
||||
value={runName}
|
||||
onChange={event => setRunName(event.target.value)}
|
||||
disabled={readOnly || submitting}
|
||||
placeholder="可留空,系统自动生成"
|
||||
style={{ width: '100%', padding: '6px 8px', borderRadius: 6, border: '1px solid #cbd5e1', boxSizing: 'border-box' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#64748b', marginBottom: 4 }}>参考日期</div>
|
||||
<input
|
||||
value={referenceDate}
|
||||
onChange={event => setReferenceDate(event.target.value)}
|
||||
disabled={readOnly || submitting}
|
||||
placeholder="YYYYMMDD,可留空"
|
||||
style={{ width: '100%', padding: '6px 8px', borderRadius: 6, border: '1px solid #cbd5e1', boxSizing: 'border-box' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#64748b', marginBottom: 4 }}>水体掩膜策略</div>
|
||||
<select
|
||||
value={waterMaskMode}
|
||||
onChange={event => setWaterMaskMode(event.target.value)}
|
||||
disabled={readOnly || submitting}
|
||||
style={{ width: '100%', padding: '6px 8px', borderRadius: 6, border: '1px solid #cbd5e1' }}
|
||||
>
|
||||
<option value="synthetic_fallback">synthetic_fallback</option>
|
||||
<option value="local">local</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<div style={{ fontSize: 12, color: '#64748b', marginBottom: 4 }}>备注</div>
|
||||
<textarea
|
||||
value={notes}
|
||||
onChange={event => setNotes(event.target.value)}
|
||||
disabled={readOnly || submitting}
|
||||
placeholder="记录本次实验目的、约束和特殊配置"
|
||||
style={{
|
||||
width: '100%',
|
||||
minHeight: 68,
|
||||
padding: '8px 10px',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #cbd5e1',
|
||||
boxSizing: 'border-box',
|
||||
resize: 'vertical',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedBatch && (
|
||||
<div style={{ marginTop: 10, fontSize: 12, color: '#334155', lineHeight: 1.7 }}>
|
||||
<div><strong>方向:</strong>{selectedBatch.direction || '-'}</div>
|
||||
<div><strong>影像数:</strong>{selectedBatch.total_items || 0}</div>
|
||||
<div><strong>批次状态:</strong>{selectedBatch.status || '-'}</div>
|
||||
<div><strong>更新时间:</strong>{formatDateTime(selectedBatch.updated_at)}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', marginTop: 12 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={readOnly || submitting || !selectedBatchId}
|
||||
style={{
|
||||
padding: '6px 14px',
|
||||
borderRadius: 6,
|
||||
border: 'none',
|
||||
background: readOnly ? '#cbd5e1' : '#2563eb',
|
||||
color: '#fff',
|
||||
cursor: readOnly ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
{submitting ? '提交中...' : '提交 SBAS 运行'}
|
||||
</button>
|
||||
{message && (
|
||||
<span style={{ fontSize: 12, color: message.includes('失败') ? '#dc2626' : '#166534' }}>
|
||||
{message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={card}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
|
||||
<strong style={{ fontSize: 14 }}>最近运行</strong>
|
||||
<button
|
||||
type="button"
|
||||
onClick={loadRuns}
|
||||
disabled={loading}
|
||||
style={{ padding: '4px 10px', borderRadius: 6, border: '1px solid #cbd5e1', background: '#fff', cursor: 'pointer' }}
|
||||
>
|
||||
{loading ? '刷新中...' : '刷新'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(280px, 360px) 1fr', gap: 12 }}>
|
||||
<div style={{ border: '1px solid #e2e8f0', borderRadius: 6, overflow: 'hidden' }}>
|
||||
<div style={{ padding: '8px 10px', background: '#f8fafc', fontSize: 12, fontWeight: 600 }}>
|
||||
运行列表 ({runs.length})
|
||||
</div>
|
||||
{runs.length === 0 ? (
|
||||
<div style={{ padding: '12px', fontSize: 12, color: '#94a3b8' }}>当前暂无 SBAS 运行记录。</div>
|
||||
) : (
|
||||
<div style={{ maxHeight: 420, overflowY: 'auto' }}>
|
||||
{runs.map(item => (
|
||||
<button
|
||||
key={item.run_id}
|
||||
type="button"
|
||||
onClick={() => setSelectedRunId(item.run_id)}
|
||||
style={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
border: 'none',
|
||||
borderTop: '1px solid #f1f5f9',
|
||||
background: selectedRunId === item.run_id ? '#eff6ff' : '#fff',
|
||||
padding: '10px 12px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, marginBottom: 4 }}>
|
||||
<strong style={{ fontSize: 12, color: '#0f172a', wordBreak: 'break-all' }}>
|
||||
{item.run_name || item.run_id}
|
||||
</strong>
|
||||
<span style={{ fontSize: 11, color: STATUS_COLOR[item.status] || '#64748b' }}>
|
||||
{item.status}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#64748b' }}>
|
||||
{item.reference_date || '-'} / {item.stack_size || 0} 景 / {formatDateTime(item.created_at)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ border: '1px solid #e2e8f0', borderRadius: 6, overflow: 'hidden' }}>
|
||||
<div style={{ padding: '8px 10px', background: '#f8fafc', fontSize: 12, fontWeight: 600 }}>运行详情</div>
|
||||
{!selectedRunId ? (
|
||||
<div style={{ padding: '12px', fontSize: 12, color: '#94a3b8' }}>请选择一个运行查看详情。</div>
|
||||
) : detailLoading ? (
|
||||
<div style={{ padding: '12px', fontSize: 12, color: '#94a3b8' }}>正在加载详情...</div>
|
||||
) : selectedRunDetail?.error ? (
|
||||
<div style={{ padding: '12px', fontSize: 12, color: '#dc2626' }}>{selectedRunDetail.error}</div>
|
||||
) : (
|
||||
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: 8, fontSize: 12, color: '#334155' }}>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<strong>{runData?.run_name || '-'}</strong>
|
||||
<StatusPill value={runData?.status} />
|
||||
</div>
|
||||
<div><strong>运行标识:</strong>{runData?.run_id || '-'}</div>
|
||||
<div><strong>批次标识:</strong>{runData?.batch_id || '-'}</div>
|
||||
<div><strong>参考日期:</strong>{runData?.reference_date || '-'}</div>
|
||||
<div><strong>方向:</strong>{runData?.direction || '-'}</div>
|
||||
<div><strong>影像数:</strong>{runData?.stack_size || 0}</div>
|
||||
<div><strong>水体掩膜策略:</strong>{runData?.water_mask_mode || '-'}</div>
|
||||
<div><strong>环境:</strong>{runData?.env_name || '-'} / {runData?.wsl_distro || '-'}</div>
|
||||
<div><strong>DEM:</strong>{runData?.dem_path_windows || '-'}</div>
|
||||
<div><strong>轨道池:</strong>{runData?.orbit_pool_windows || '-'}</div>
|
||||
<div><strong>工作目录:</strong>{runData?.work_root_windows || '-'}</div>
|
||||
<div><strong>发布目录:</strong>{runData?.publish_dir_windows || '-'}</div>
|
||||
<div><strong>stack manifest:</strong>{runData?.manifest_path_windows || '-'}</div>
|
||||
<div><strong>创建时间:</strong>{formatDateTime(runData?.created_at)}</div>
|
||||
<div><strong>结束时间:</strong>{formatDateTime(runData?.ended_at)}</div>
|
||||
<div><strong>输入日期:</strong>{(runData?.input_snapshot_json?.stack_dates || []).join(', ') || '-'}</div>
|
||||
<div><strong>轨道摘要:</strong>{JSON.stringify(runData?.orbit_summary_json || {}, null, 2)}</div>
|
||||
{workflowSteps.length > 0 && (
|
||||
<div style={{ marginTop: 8, paddingTop: 8, borderTop: '1px dashed #cbd5e1' }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 6 }}>工作流步骤</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{workflowSteps.map(step => (
|
||||
<div
|
||||
key={step.step_id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: 8,
|
||||
padding: '6px 8px',
|
||||
borderRadius: 6,
|
||||
background: '#f8fafc',
|
||||
border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 600, color: '#0f172a' }}>
|
||||
{step.step_name || step.step_id}
|
||||
</div>
|
||||
<div style={{ color: '#64748b', wordBreak: 'break-all' }}>
|
||||
{step.step_id}
|
||||
</div>
|
||||
{step.error && (
|
||||
<div style={{ color: '#dc2626', marginTop: 2, wordBreak: 'break-word' }}>
|
||||
{step.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
<StatusPill value={step.status} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{linkedProduct && (
|
||||
<div style={{ marginTop: 8, paddingTop: 8, borderTop: '1px dashed #cbd5e1' }}>
|
||||
<div><strong>关联产品:</strong>{linkedProduct.display_name || linkedProduct.product_id}</div>
|
||||
<div><strong>产品状态:</strong>{linkedProduct.status || '-'} / {linkedProduct.health_status || '-'}</div>
|
||||
<div><strong>发布时间:</strong>{formatDateTime(linkedProduct.published_at)}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user