454 lines
15 KiB
TypeScript
454 lines
15 KiB
TypeScript
import { useState, useEffect, useMemo } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
import { IoIosTrash, IoIosPlay } from 'react-icons/io';
|
|
import { MdModeEdit } from 'react-icons/md';
|
|
|
|
import { useWorkflows, useWorkflowOperations, Workflow } from '../../hooks/useWorkflows';
|
|
import { useApiRequest } from '../../hooks/useApi';
|
|
import { useLanguage } from '../../contexts/LanguageContext';
|
|
import type { EditFieldConfig } from '../Popup/EditForm';
|
|
|
|
import type {
|
|
WorkflowsLogicReturn,
|
|
WorkflowMessageCounts,
|
|
WorkflowActionConfig,
|
|
WorkflowColumnConfig
|
|
} from './workflowsTypes';
|
|
import styles from './WorkflowsTable.module.css';
|
|
|
|
export function useWorkflowsLogic(): WorkflowsLogicReturn {
|
|
const { workflows, loading, error, refetch } = useWorkflows();
|
|
const navigate = useNavigate();
|
|
const { t } = useLanguage();
|
|
|
|
// State to track message counts for each workflow
|
|
const [workflowMessageCounts, setWorkflowMessageCounts] = useState<WorkflowMessageCounts>({});
|
|
const { request } = useApiRequest();
|
|
|
|
// Debug: Log workflow data to see the actual structure
|
|
console.log('Workflows data:', workflows);
|
|
if (workflows && workflows.length > 0) {
|
|
const firstWorkflow = workflows[0];
|
|
console.log('First workflow object:', firstWorkflow);
|
|
console.log('First workflow keys:', Object.keys(firstWorkflow));
|
|
console.log('First workflow stats:', firstWorkflow.stats);
|
|
if (firstWorkflow.stats) {
|
|
console.log('Stats keys:', Object.keys(firstWorkflow.stats));
|
|
console.log('Stats object:', firstWorkflow.stats);
|
|
}
|
|
console.log('First workflow messages array:', firstWorkflow.messages, 'length:', firstWorkflow.messages?.length);
|
|
}
|
|
|
|
const {
|
|
deleteWorkflow,
|
|
updateWorkflow,
|
|
deletingWorkflows
|
|
} = useWorkflowOperations();
|
|
|
|
// Edit modal state
|
|
const [editModalOpen, setEditModalOpen] = useState(false);
|
|
const [editingWorkflow, setEditingWorkflow] = useState<Workflow | null>(null);
|
|
|
|
// Function to fetch message count for a single workflow
|
|
const fetchMessageCount = async (workflowId: string) => {
|
|
try {
|
|
console.log(`Fetching messages for workflow: ${workflowId}`);
|
|
const messages = await request({
|
|
url: `/api/workflows/${workflowId}/messages`,
|
|
method: 'get'
|
|
});
|
|
|
|
console.log(`Messages for ${workflowId}:`, messages, 'length:', messages?.length);
|
|
const count = Array.isArray(messages) ? messages.length : 0;
|
|
console.log(`Setting message count for ${workflowId}:`, count);
|
|
setWorkflowMessageCounts(prev => ({
|
|
...prev,
|
|
[workflowId]: count
|
|
}));
|
|
} catch (error) {
|
|
console.error(`Failed to fetch message count for workflow ${workflowId}:`, error);
|
|
// Set count to 0 for failed requests
|
|
setWorkflowMessageCounts(prev => ({
|
|
...prev,
|
|
[workflowId]: 0
|
|
}));
|
|
}
|
|
};
|
|
|
|
// Effect to fetch message counts when workflows change
|
|
useEffect(() => {
|
|
if (workflows && workflows.length > 0) {
|
|
workflows.forEach(workflow => {
|
|
// Only fetch if we don't already have the count
|
|
if (!(workflow.id in workflowMessageCounts)) {
|
|
fetchMessageCount(workflow.id);
|
|
}
|
|
});
|
|
}
|
|
}, [workflows]); // Don't include workflowMessageCounts to avoid infinite loop
|
|
|
|
// Configure edit fields for workflow name editing
|
|
const editWorkflowFields: EditFieldConfig[] = useMemo(() => [
|
|
{
|
|
key: 'name',
|
|
label: t('workflows.field.name', 'Workflow Name'),
|
|
type: 'string',
|
|
editable: true,
|
|
required: true,
|
|
validator: (value: string) => {
|
|
if (!value || value.trim() === '') {
|
|
return t('workflows.validation.nameRequired', 'Workflow name cannot be empty');
|
|
}
|
|
if (value.length > 100) {
|
|
return t('workflows.validation.nameTooLong', 'Workflow name cannot exceed 100 characters');
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
], [t]);
|
|
|
|
// Configure columns for the workflows table
|
|
const columns: WorkflowColumnConfig[] = useMemo(() => [
|
|
{
|
|
key: 'id',
|
|
label: t('workflows.column.id'),
|
|
type: 'string',
|
|
width: 180,
|
|
minWidth: 150,
|
|
maxWidth: 250,
|
|
sortable: true,
|
|
filterable: true,
|
|
searchable: true,
|
|
formatter: (value: string) => (
|
|
<span className={styles.workflowId} title={value}>
|
|
{value.length > 8 ? `${value.substring(0, 8)}...` : value}
|
|
</span>
|
|
)
|
|
},
|
|
{
|
|
key: 'name',
|
|
label: t('workflows.column.name'),
|
|
type: 'string',
|
|
width: 200,
|
|
minWidth: 150,
|
|
maxWidth: 300,
|
|
sortable: true,
|
|
filterable: true,
|
|
searchable: true,
|
|
formatter: (value: string | undefined) => (
|
|
<span className={styles.workflowName}>
|
|
{value || t('workflows.unnamed')}
|
|
</span>
|
|
)
|
|
},
|
|
{
|
|
key: 'status',
|
|
label: t('workflows.column.status'),
|
|
type: 'enum',
|
|
width: 120,
|
|
minWidth: 100,
|
|
maxWidth: 150,
|
|
sortable: true,
|
|
filterable: true,
|
|
filterOptions: ['running', 'completed', 'failed', 'stopped', 'pending'],
|
|
formatter: (value: string) => (
|
|
<span className={`${styles.statusBadge} ${styles[`status-${value}`]}`}>
|
|
{t(`workflows.status.${value}`, value)}
|
|
</span>
|
|
)
|
|
},
|
|
{
|
|
key: 'currentRound',
|
|
label: t('workflows.column.round'),
|
|
type: 'number',
|
|
width: 80,
|
|
minWidth: 60,
|
|
maxWidth: 100,
|
|
sortable: true,
|
|
filterable: true,
|
|
formatter: (value: number | undefined) => (
|
|
<span className={styles.roundNumber}>
|
|
{value || 1}
|
|
</span>
|
|
)
|
|
},
|
|
{
|
|
key: 'startedAt',
|
|
label: t('workflows.column.started'),
|
|
type: 'date',
|
|
width: 140,
|
|
minWidth: 120,
|
|
maxWidth: 180,
|
|
sortable: true,
|
|
filterable: true,
|
|
formatter: (value: number | undefined) => {
|
|
if (!value) return '-';
|
|
try {
|
|
// Backend sends UTC timestamp in seconds (float), convert to milliseconds for Date constructor
|
|
const date = new Date(value * 1000);
|
|
|
|
// Check if date is valid
|
|
if (isNaN(date.getTime())) {
|
|
console.warn('Invalid startedAt date:', value);
|
|
return '-';
|
|
}
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
const hours = String(date.getHours()).padStart(2, '0');
|
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
const timezoneOffset = date.getTimezoneOffset();
|
|
const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60);
|
|
const offsetMinutes = Math.abs(timezoneOffset) % 60;
|
|
const offsetSign = timezoneOffset <= 0 ? '+' : '-';
|
|
const timezone = `GMT${offsetSign}${offsetHours}${offsetMinutes > 0 ? ':' + offsetMinutes.toString().padStart(2, '0') : ''}`;
|
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`;
|
|
} catch (error) {
|
|
console.warn('Error parsing startedAt date:', value, error);
|
|
return '-';
|
|
}
|
|
}
|
|
},
|
|
{
|
|
key: 'lastActivity',
|
|
label: t('workflows.column.lastActivity'),
|
|
type: 'date',
|
|
width: 140,
|
|
minWidth: 120,
|
|
maxWidth: 180,
|
|
sortable: true,
|
|
filterable: true,
|
|
formatter: (value: number | undefined) => {
|
|
if (!value) return '-';
|
|
try {
|
|
// Backend sends UTC timestamp in seconds (float), convert to milliseconds for Date constructor
|
|
const date = new Date(value * 1000);
|
|
|
|
// Check if date is valid
|
|
if (isNaN(date.getTime())) {
|
|
console.warn('Invalid lastActivity date:', value);
|
|
return '-';
|
|
}
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
const hours = String(date.getHours()).padStart(2, '0');
|
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
const timezoneOffset = date.getTimezoneOffset();
|
|
const offsetHours = Math.floor(Math.abs(timezoneOffset) / 60);
|
|
const offsetMinutes = Math.abs(timezoneOffset) % 60;
|
|
const offsetSign = timezoneOffset <= 0 ? '+' : '-';
|
|
const timezone = `GMT${offsetSign}${offsetHours}${offsetMinutes > 0 ? ':' + offsetMinutes.toString().padStart(2, '0') : ''}`;
|
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${timezone}`;
|
|
} catch (error) {
|
|
console.warn('Error parsing lastActivity date:', value, error);
|
|
return '-';
|
|
}
|
|
}
|
|
},
|
|
{
|
|
key: 'messages',
|
|
label: t('workflows.column.messages'),
|
|
type: 'number',
|
|
width: 100,
|
|
minWidth: 80,
|
|
maxWidth: 120,
|
|
sortable: true,
|
|
filterable: false,
|
|
formatter: (_value: any, row: any) => {
|
|
// Get message count from our fetched data, just like in Dashboard component
|
|
const workflowId = row?.id;
|
|
const messageCount = workflowId ? workflowMessageCounts[workflowId] : undefined;
|
|
|
|
console.log(`Messages formatter for ${workflowId}:`, {
|
|
workflowId,
|
|
messageCount,
|
|
hasInCache: workflowId in workflowMessageCounts,
|
|
allCounts: workflowMessageCounts
|
|
});
|
|
|
|
// Show the count if available, otherwise show loading indicator or dash
|
|
let displayValue;
|
|
if (messageCount !== undefined) {
|
|
displayValue = messageCount;
|
|
} else if (workflowId && workflows.some(w => w.id === workflowId)) {
|
|
// We're still loading this count
|
|
displayValue = '...';
|
|
} else {
|
|
displayValue = '-';
|
|
}
|
|
|
|
return (
|
|
<span className={styles.messageCount}>
|
|
{displayValue}
|
|
</span>
|
|
);
|
|
}
|
|
}
|
|
], [t, workflowMessageCounts, workflows]);
|
|
|
|
// Handle workflow actions
|
|
const handleDeleteWorkflow = async (workflow: Workflow) => {
|
|
const workflowName = workflow.name || workflow.id;
|
|
if (window.confirm(t('workflows.delete.confirm').replace('{name}', workflowName))) {
|
|
const success = await deleteWorkflow(workflow.id);
|
|
if (success) {
|
|
refetch(); // Refresh the workflows list
|
|
}
|
|
}
|
|
};
|
|
|
|
// Handle single workflow deletion for bulk delete
|
|
const handleDeleteSingle = async (workflow: Workflow) => {
|
|
const workflowName = workflow.name || workflow.id;
|
|
if (window.confirm(t('workflows.delete.confirm', 'Are you sure you want to delete "{name}"?').replace('{name}', workflowName))) {
|
|
const success = await deleteWorkflow(workflow.id);
|
|
if (success) {
|
|
refetch(); // Refresh the workflows list
|
|
} else {
|
|
console.error('Delete failed for workflow:', workflow.id);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Handle multiple workflow deletion
|
|
const handleDeleteMultiple = async (workflowsToDelete: Workflow[]) => {
|
|
const workflowCount = workflowsToDelete.length;
|
|
if (window.confirm(t('workflows.delete.confirmMultiple', 'Are you sure you want to delete {count} workflows?').replace('{count}', workflowCount.toString()))) {
|
|
// Start all delete operations simultaneously
|
|
const deletePromises = workflowsToDelete.map(async (workflow) => {
|
|
try {
|
|
const success = await deleteWorkflow(workflow.id);
|
|
return { workflowId: workflow.id, success };
|
|
} catch (error) {
|
|
console.error('Failed to delete workflow:', workflow.id, error);
|
|
return { workflowId: workflow.id, success: false };
|
|
}
|
|
});
|
|
|
|
// Wait for all deletions to complete
|
|
const results = await Promise.all(deletePromises);
|
|
|
|
// Check if any deletions failed
|
|
const failedDeletions = results.filter(result => !result.success);
|
|
if (failedDeletions.length > 0) {
|
|
console.error('Some workflow deletions failed:', failedDeletions);
|
|
}
|
|
|
|
// Refresh the workflow list regardless of individual failures
|
|
refetch();
|
|
}
|
|
};
|
|
|
|
// Handle edit workflow
|
|
const handleEditWorkflow = (workflow: Workflow) => {
|
|
setEditingWorkflow(workflow);
|
|
setEditModalOpen(true);
|
|
};
|
|
|
|
// Handle save workflow
|
|
const handleSaveWorkflow = async (updatedWorkflow: Workflow) => {
|
|
if (!editingWorkflow) return;
|
|
|
|
try {
|
|
// Call API to update workflow name
|
|
const result = await updateWorkflow(editingWorkflow.id, {
|
|
name: updatedWorkflow.name
|
|
});
|
|
|
|
if (result.success) {
|
|
// Close modal
|
|
setEditModalOpen(false);
|
|
setEditingWorkflow(null);
|
|
|
|
// Refresh workflow list
|
|
await refetch();
|
|
|
|
// Notify other components that workflows have been updated
|
|
window.dispatchEvent(new CustomEvent('workflowUpdated', {
|
|
detail: { workflowId: editingWorkflow.id, newName: updatedWorkflow.name }
|
|
}));
|
|
} else {
|
|
console.error('Failed to update workflow:', result.error);
|
|
// TODO: Show error message to user
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to update workflow:', error);
|
|
// TODO: Show error message to user
|
|
}
|
|
};
|
|
|
|
// Handle cancel edit
|
|
const handleCancelEdit = () => {
|
|
setEditModalOpen(false);
|
|
setEditingWorkflow(null);
|
|
};
|
|
|
|
// Handle play workflow - navigate to dashboard with workflow ID
|
|
const handlePlayWorkflow = (workflow: Workflow) => {
|
|
// Navigate to dashboard with workflow ID as URL parameter
|
|
navigate(`/dashboard?workflowId=${workflow.id}`);
|
|
};
|
|
|
|
// Configure action buttons
|
|
const actions: WorkflowActionConfig[] = useMemo(() => [
|
|
{
|
|
label: t('workflows.action.play'),
|
|
icon: (_row: Workflow) => {
|
|
return <IoIosPlay />;
|
|
},
|
|
onClick: (row: Workflow) => {
|
|
handlePlayWorkflow(row);
|
|
}
|
|
},
|
|
{
|
|
label: t('workflows.action.edit'),
|
|
icon: (_row: Workflow) => {
|
|
return <MdModeEdit />;
|
|
},
|
|
onClick: (row: Workflow) => {
|
|
handleEditWorkflow(row);
|
|
}
|
|
},
|
|
{
|
|
label: t('workflows.action.delete'),
|
|
icon: (_row: Workflow) => {
|
|
return <IoIosTrash />;
|
|
},
|
|
onClick: (row: Workflow) => {
|
|
if (!deletingWorkflows.has(row.id)) {
|
|
handleDeleteWorkflow(row);
|
|
}
|
|
}
|
|
},
|
|
], [t, deletingWorkflows, handleDeleteWorkflow, handleEditWorkflow, handlePlayWorkflow]);
|
|
|
|
return {
|
|
// Data
|
|
workflows,
|
|
loading,
|
|
error,
|
|
workflowMessageCounts,
|
|
editModalOpen,
|
|
editingWorkflow,
|
|
editWorkflowFields,
|
|
|
|
// Actions
|
|
handleDeleteSingle,
|
|
handleDeleteMultiple,
|
|
handleEditWorkflow,
|
|
handleSaveWorkflow,
|
|
handleCancelEdit,
|
|
handlePlayWorkflow,
|
|
|
|
// Refetch function
|
|
refetch,
|
|
|
|
// Additional data for rendering
|
|
columns: columns as any,
|
|
actions: actions as any
|
|
};
|
|
}
|