95 lines
3.4 KiB
TypeScript
95 lines
3.4 KiB
TypeScript
import React, { useMemo } from 'react';
|
|
import { WorkflowStatusProps, WorkflowStatusType } from './WorkflowStatusTypes';
|
|
import styles from './WorkflowStatus.module.css';
|
|
|
|
const _STATUS_MAP: Record<string, WorkflowStatusType> = {
|
|
success: 'completed',
|
|
completed: 'completed',
|
|
started: 'started',
|
|
running: 'started',
|
|
resumed: 'resumed',
|
|
stopped: 'stopped',
|
|
failed: 'failed',
|
|
error: 'failed',
|
|
};
|
|
|
|
const extractWorkflowStatus = (logs: any[]): { status: WorkflowStatusType; round: number | null; timestamp: number } => {
|
|
if (!logs.length) return { status: null, round: null, timestamp: 0 };
|
|
|
|
const sorted = [...logs].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
|
|
|
for (const log of sorted) {
|
|
const logStatus = (log.status || '').toLowerCase();
|
|
const mapped = _STATUS_MAP[logStatus];
|
|
if (mapped) {
|
|
const roundMatch = (log.message || '').match(/\(?round\s+(\d+)\)?/i);
|
|
return { status: mapped, round: roundMatch ? parseInt(roundMatch[1], 10) : null, timestamp: log.timestamp || 0 };
|
|
}
|
|
}
|
|
|
|
return { status: null, round: null, timestamp: 0 };
|
|
};
|
|
|
|
const _formatCurrency = (amount?: number): string => {
|
|
if (amount === undefined || amount === null) return '-';
|
|
return `${amount.toFixed(2)} CHF`;
|
|
};
|
|
|
|
const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
|
|
className = '',
|
|
logs = [],
|
|
workflowStatus: workflowStatusFromApi,
|
|
currentRound: currentRoundFromApi,
|
|
isRunning,
|
|
latestStats
|
|
}) => {
|
|
// Use workflow status and round from API response, fallback to extracting from logs
|
|
const workflowStatus = useMemo(() => {
|
|
if (workflowStatusFromApi) {
|
|
const mapped = _STATUS_MAP[workflowStatusFromApi.toLowerCase()] || null;
|
|
return { status: mapped, round: currentRoundFromApi || null, timestamp: Date.now() / 1000 };
|
|
}
|
|
return extractWorkflowStatus(logs);
|
|
}, [workflowStatusFromApi, currentRoundFromApi, logs]);
|
|
|
|
// Determine if workflow is running (show spinner)
|
|
// Show spinner if explicitly running OR if status indicates running state
|
|
const showSpinner = isRunning === true || workflowStatus.status === 'started' || workflowStatus.status === 'resumed';
|
|
|
|
// Don't render if no status information and no stats (but always show if spinner should be visible)
|
|
if (!showSpinner && !workflowStatus.status && workflowStatus.round === null && !latestStats) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className={`${styles.workflowStatusContainer} ${className}`}>
|
|
{/* Status and Round Badges */}
|
|
<div className={styles.workflowStatus}>
|
|
{showSpinner && (
|
|
<div className={styles.spinner} aria-label="Workflow running" />
|
|
)}
|
|
{workflowStatus.status && (
|
|
<span className={styles.statusBadge} data-status={workflowStatus.status}>
|
|
{workflowStatus.status.charAt(0).toUpperCase() + workflowStatus.status.slice(1)}
|
|
</span>
|
|
)}
|
|
{workflowStatus.round !== null && (
|
|
<span className={styles.roundBadge}>Round {workflowStatus.round}</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Cost Display */}
|
|
{latestStats && latestStats.priceCHF !== undefined && (
|
|
<div className={styles.statsContainer}>
|
|
<div className={styles.statItem}>
|
|
<span className={styles.statLabel}>Cost:</span>
|
|
<span className={styles.statValue}>{_formatCurrency(latestStats.priceCHF)}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default WorkflowStatus;
|
|
|