Improve DInSAR production frontend monitoring and catalog layout

This commit is contained in:
2026-05-03 16:49:00 +08:00
parent 3474e25839
commit 2d65bc7ce9
2 changed files with 181 additions and 22 deletions
+157 -22
View File
@@ -2,7 +2,7 @@
import { listEngines, listRuns, previewPyintInputAssets, submitRun } from './api/dinsarProduction';
import { getJobLog } from './api/idl';
import { clearTaskLogs, deleteTaskLog, getActiveTasks, getTaskLogs } from './api/tasks';
import { clearTaskLogs, deleteTaskLog, getActiveTasks, getRecentTasks, getTaskLogs } from './api/tasks';
const card = {
background: '#fff',
@@ -40,7 +40,7 @@ const ENGINE_LABEL = {
const TASK_TYPE_LABEL = {
ISCE2_RUN: 'ISCE2生产',
PYINT_RUN: 'PyINT生产',
PYINT_RUN: 'PyINT/Gamma生产',
IDL_RUN_DINSAR: 'ENVI生产',
};
@@ -104,6 +104,96 @@ function formatStatus(status) {
return STATUS_LABEL[status] || status || '-';
}
function taskTypeToEngine(taskType) {
if (taskType === 'ISCE2_RUN') return 'isce2';
if (taskType === 'PYINT_RUN') return 'pyint';
if (taskType === 'IDL_RUN_DINSAR') return 'sarscape';
return '';
}
function taskStatusToRunStatus(status) {
const normalized = String(status || '').toUpperCase();
if (normalized === 'COMPLETED') return 'success';
if (normalized === 'FAILED') return 'failed';
if (normalized === 'CANCELLED' || normalized === 'CANCELED') return 'cancelled';
if (normalized === 'RUNNING') return 'running';
return 'pending';
}
function epochSeconds(value) {
if (value == null || value === '') return null;
if (typeof value === 'number') return value;
const parsed = Date.parse(value);
return Number.isNaN(parsed) ? null : Math.floor(parsed / 1000);
}
function taskToRunRow(task) {
const taskId = task?.task_id || '';
return {
run_id: taskId,
record_type: 'task',
log_source: 'task',
product_family: 'dinsar',
engine: taskTypeToEngine(task?.task_type),
profile_code: '',
status: taskStatusToRunStatus(task?.status),
raw_status: task?.status,
started_at: epochSeconds(task?.started_at || task?.created_at),
ended_at: epochSeconds(task?.ended_at),
task_id: taskId,
task_type: task?.task_type,
workflow_run_id: '',
root_dir: '',
publish_root_dir: '',
message: task?.message || task?.task_name || '',
total_items: null,
completed_items: null,
failed_items: null,
skipped_items: null,
};
}
function mergeRunRows(productionRuns, recentTasks, limit = 20) {
const rows = (productionRuns || []).map(run => ({
...run,
record_type: run?.record_type || 'run',
log_source: run?.log_source || 'run',
}));
const representedTaskIds = new Set(
rows
.map(run => String(run?.task_id || '').trim())
.filter(Boolean),
);
(recentTasks || []).forEach(task => {
if (!['ISCE2_RUN', 'PYINT_RUN', 'IDL_RUN_DINSAR'].includes(task?.task_type)) {
return;
}
const taskId = String(task?.task_id || '').trim();
if (!taskId || representedTaskIds.has(taskId)) {
return;
}
rows.push(taskToRunRow(task));
representedTaskIds.add(taskId);
});
return rows
.sort((a, b) => Number(b?.started_at || 0) - Number(a?.started_at || 0))
.slice(0, limit);
}
function formatTaskLogContent(taskId, logs) {
if (!Array.isArray(logs) || logs.length === 0) {
return '暂无任务日志。';
}
const body = logs.map(log => {
const timestamp = String(log.timestamp || '').replace('T', ' ').replace('Z', '');
const level = log.level || 'INFO';
return `${timestamp} [${level}] ${log.message || ''}`;
}).join('\n');
return `task_id=${taskId}\n${body}`;
}
function formatPyintDemMode(mode) {
return PYINT_DEM_MODE_LABEL[mode] || mode || '-';
}
@@ -390,6 +480,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
const [logModal, setLogModal] = useState({ open: false, runId: '', content: '', loading: false });
const [activeTask, setActiveTask] = useState(null);
const [recentTask, setRecentTask] = useState(null);
const [taskLogs, setTaskLogs] = useState([]);
const [taskLogsLoading, setTaskLogsLoading] = useState(false);
const [taskLogActionLoading, setTaskLogActionLoading] = useState(false);
@@ -402,13 +493,13 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
const currentParamSections = buildParamSections(currentParamSchema);
const currentDefaultTimeoutSec = Number(currentEngineObj?.default_timeout_seconds || 0) || 0;
const currentParamHelpText = selectedEngine === 'pyint'
? '这些参数影响 PyINT 的多视、并行度以及是否执行解缠/地理编码。建议先直接使用默认值,优先确认当前任务目录里的 LT-1 原始压缩包是否能被正常识别。'
? 'PyINT/Gamma 默认按目标网格尺寸自动换算多视,逻辑与 ENVI/SARscape 自定义流程一致;通常只需要设置目标网格、并行度是否执行解缠/地理编码。'
: selectedEngine === 'isce2'
? '这些参数现在按执行、交付、增强分组展示。结果异常时,优先尝试关闭增强项,再回看基础几何和配对质量。'
: '这些参数影响当前引擎的生产模板。建议先使用默认值,只有在结果边界、噪声或几何表现异常时再逐项调整。';
const pyintPreviewBlocksSubmit = selectedEngine === 'pyint' && pyintPreview && pyintPreview.allow_submit === false;
const latestRunWithTask = runs.find(run => run?.task_id) || null;
const monitoredTask = activeTask || (
const monitoredTask = activeTask || recentTask || (
latestRunWithTask
? {
task_id: latestRunWithTask.task_id,
@@ -439,18 +530,23 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
}
}, []);
const loadRuns = useCallback(async () => {
setRunsLoading(true);
const loadRuns = useCallback(async (options = {}) => {
const silent = !!options.silent;
if (!silent) setRunsLoading(true);
try {
const data = await listRuns(20);
const nextRuns = data.runs || [];
const [runData, taskData] = await Promise.all([
listRuns(20),
getRecentTasks(['ISCE2_RUN', 'PYINT_RUN', 'IDL_RUN_DINSAR'], [], 20, 0),
]);
const recentTasks = Array.isArray(taskData) ? taskData : (taskData?.tasks || []);
const nextRuns = mergeRunRows(runData.runs || [], recentTasks, 20);
setRuns(nextRuns);
return nextRuns;
} catch {
setRuns([]);
return [];
} finally {
setRunsLoading(false);
if (!silent) setRunsLoading(false);
}
}, []);
@@ -467,19 +563,33 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
}
}, []);
const loadTaskLogs = useCallback(async taskId => {
const loadRecentTask = useCallback(async () => {
try {
const data = await getRecentTasks(['ISCE2_RUN', 'PYINT_RUN', 'IDL_RUN_DINSAR'], [], 1, 0);
const tasks = Array.isArray(data) ? data : (data?.tasks || []);
const relevantTask = tasks[0] || null;
setRecentTask(relevantTask);
return relevantTask;
} catch {
setRecentTask(null);
return null;
}
}, []);
const loadTaskLogs = useCallback(async (taskId, options = {}) => {
const silent = !!options.silent;
if (!taskId) {
setTaskLogs([]);
return;
}
setTaskLogsLoading(true);
if (!silent) setTaskLogsLoading(true);
try {
const data = await getTaskLogs(taskId, 50, 0);
const data = await getTaskLogs(taskId, 120, 0);
setTaskLogs(data?.logs || []);
} catch {
setTaskLogs([]);
} finally {
setTaskLogsLoading(false);
if (!silent) setTaskLogsLoading(false);
}
}, []);
@@ -519,20 +629,34 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
}
}, [logTaskId, loadTaskLogs, taskLogActionLoading, taskLogs.length]);
const refreshMonitor = useCallback(async () => {
const [nextRuns, nextActiveTask] = await Promise.all([
loadRuns(),
const refreshMonitor = useCallback(async (options = {}) => {
const silent = !!options.silent;
const [nextRuns, nextActiveTask, nextRecentTask] = await Promise.all([
loadRuns({ silent }),
loadActiveTask(),
loadRecentTask(),
]);
const fallbackTaskId = nextActiveTask?.task_id || nextRuns.find(run => run?.task_id)?.task_id || '';
await loadTaskLogs(fallbackTaskId);
}, [loadActiveTask, loadRuns, loadTaskLogs]);
const fallbackTaskId =
nextActiveTask?.task_id
|| nextRecentTask?.task_id
|| nextRuns.find(run => run?.task_id)?.task_id
|| '';
await loadTaskLogs(fallbackTaskId, { silent });
}, [loadActiveTask, loadRecentTask, loadRuns, loadTaskLogs]);
useEffect(() => {
loadEngines();
refreshMonitor();
}, [loadEngines, refreshMonitor]);
useEffect(() => {
const intervalMs = activeTask ? 5000 : 15000;
const timer = window.setInterval(() => {
refreshMonitor({ silent: true });
}, intervalMs);
return () => window.clearInterval(timer);
}, [activeTask, refreshMonitor]);
useEffect(() => {
if (currentProfiles.length > 0) {
setSelectedProfile(currentProfiles[0].code);
@@ -647,9 +771,20 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
}
};
const handleViewLog = async runId => {
const handleViewLog = async run => {
const runId = typeof run === 'string' ? run : (run?.run_id || run?.task_id || '');
setLogModal({ open: true, runId, content: '', loading: true });
try {
if (typeof run !== 'string' && run?.log_source === 'task' && run?.task_id) {
const data = await getTaskLogs(run.task_id, 200, 0);
setLogModal({
open: true,
runId,
content: formatTaskLogContent(run.task_id, data?.logs || []),
loading: false,
});
return;
}
const data = await getJobLog(runId);
setLogModal({ open: true, runId, content: data.content || '', loading: false });
} catch {
@@ -1389,7 +1524,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
</thead>
<tbody>
{runs.map(run => (
<tr key={run.run_id} style={{ borderBottom: '1px solid #f1f5f9' }}>
<tr key={`${run.record_type || 'run'}-${run.run_id}`} style={{ borderBottom: '1px solid #f1f5f9' }}>
<td style={{ padding: '4px 8px', fontFamily: 'monospace', fontSize: 11 }}>{run.run_id}</td>
<td style={{ padding: '4px 8px' }}>{formatEngineLabel(run.engine)}</td>
<td
@@ -1405,7 +1540,7 @@ export default function DinsarProductionPanel({ readOnly = false, onJobQueued })
</td>
<td style={{ padding: '4px 8px' }}>
<button
onClick={() => handleViewLog(run.run_id)}
onClick={() => handleViewLog(run)}
style={{
fontSize: 11,
padding: '2px 8px',