frontend_nyla/src/components/Workflows/workflowsLogic.tsx

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
};
}