finalized workflows page
This commit is contained in:
parent
41a3b8f40e
commit
43951c280a
6 changed files with 580 additions and 363 deletions
|
|
@ -44,13 +44,13 @@
|
|||
.workflowId {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
color: var(--color-text-secondary, #666);
|
||||
color: var(--color-gray);
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.workflowName {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary, #333);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.statusBadge {
|
||||
|
|
@ -65,21 +65,18 @@
|
|||
}
|
||||
|
||||
.status-running {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
background-color: var(--color-gray);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
background-color: var(--color-secondary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
background-color: var(--color-red);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-stopped {
|
||||
|
|
@ -95,11 +92,10 @@
|
|||
}
|
||||
|
||||
.roundNumber {
|
||||
font-weight: 600;
|
||||
color: var(--color-primary, #007bff);
|
||||
background-color: var(--color-primary-light, #e3f2fd);
|
||||
font-weight: 400;
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9em;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
|
|
@ -108,8 +104,8 @@
|
|||
|
||||
.messageCount {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary, #666);
|
||||
background-color: var(--color-bg-secondary, #f8f9fa);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9em;
|
||||
|
|
@ -139,7 +135,7 @@
|
|||
/* Dark mode support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.workflowId {
|
||||
color: #adb5bd;
|
||||
color: var(--color-gray);
|
||||
}
|
||||
|
||||
.workflowName {
|
||||
|
|
@ -147,43 +143,33 @@
|
|||
}
|
||||
|
||||
.status-running {
|
||||
background-color: #155724;
|
||||
color: #d4edda;
|
||||
background-color: var(--color-gray);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background-color: #0c5460;
|
||||
color: #d1ecf1;
|
||||
background-color: var(--color-secondary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background-color: #721c24;
|
||||
color: #f8d7da;
|
||||
background-color: var(--color-red);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-stopped {
|
||||
background-color: #495057;
|
||||
color: #f5f5f5;
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: #856404;
|
||||
color: #fff3cd;
|
||||
}
|
||||
|
||||
.roundNumber {
|
||||
background-color: #1e3a8a;
|
||||
color: #bfdbfe;
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.messageCount {
|
||||
background-color: #374151;
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.errorState {
|
||||
background-color: #2d1b1e;
|
||||
border-color: #5c2b33;
|
||||
color: #f5c6cb;
|
||||
background-color: var(--color-gray);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,318 +1,22 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { IoIosTrash, IoIosPlay } from 'react-icons/io';
|
||||
import { MdModeEdit } from 'react-icons/md';
|
||||
|
||||
import { FormGenerator, ColumnConfig } from '../FormGenerator/FormGenerator';
|
||||
import { useWorkflows, useWorkflowOperations, Workflow } from '../../hooks/useWorkflows';
|
||||
import { useLanguage } from '../../contexts/LanguageContext';
|
||||
import { FormGenerator } from '../FormGenerator/FormGenerator';
|
||||
import { Popup, EditForm } from '../Popup';
|
||||
import type { EditFieldConfig } from '../Popup/EditForm';
|
||||
import { useWorkflowsLogic } from './workflowsLogic';
|
||||
import { WorkflowsTableProps } from './workflowsTypes';
|
||||
import styles from './WorkflowsTable.module.css';
|
||||
|
||||
interface WorkflowsTableProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function WorkflowsTable({ className = '' }: WorkflowsTableProps) {
|
||||
const { workflows, loading, error, refetch } = useWorkflows();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Debug: Log workflow data to see the actual structure
|
||||
console.log('Workflows data:', workflows);
|
||||
const {
|
||||
deleteWorkflow,
|
||||
updateWorkflow,
|
||||
deletingWorkflows
|
||||
} = useWorkflowOperations();
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Edit modal state
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [editingWorkflow, setEditingWorkflow] = useState<Workflow | null>(null);
|
||||
|
||||
// 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: ColumnConfig[] = 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: string | undefined) => {
|
||||
if (!value) return '-';
|
||||
try {
|
||||
const date = new Date(value);
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'lastActivity',
|
||||
label: t('workflows.column.lastActivity'),
|
||||
type: 'date',
|
||||
width: 140,
|
||||
minWidth: 120,
|
||||
maxWidth: 180,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
formatter: (value: string | undefined) => {
|
||||
if (!value) return '-';
|
||||
try {
|
||||
const date = new Date(value);
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'messages',
|
||||
label: t('workflows.column.messages'),
|
||||
type: 'number',
|
||||
width: 100,
|
||||
minWidth: 80,
|
||||
maxWidth: 120,
|
||||
sortable: true,
|
||||
filterable: false,
|
||||
formatter: (value: any[] | undefined) => (
|
||||
<span className={styles.messageCount}>
|
||||
{value?.length || 0}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
], [t]);
|
||||
|
||||
// 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();
|
||||
} 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 = 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]);
|
||||
const logic = useWorkflowsLogic();
|
||||
|
||||
// Show error state
|
||||
if (error) {
|
||||
if (logic.error) {
|
||||
return (
|
||||
<div className={`${styles.workflowsTable} ${className}`}>
|
||||
<div className={styles.errorState}>
|
||||
<p>{t('workflows.error.loading')} {error}</p>
|
||||
<button onClick={refetch} className={styles.retryButton}>
|
||||
{t('workflows.button.retry')}
|
||||
<p>Error loading workflows: {logic.error}</p>
|
||||
<button onClick={logic.refetch} className={styles.retryButton}>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -322,20 +26,20 @@ function WorkflowsTable({ className = '' }: WorkflowsTableProps) {
|
|||
return (
|
||||
<div className={`${styles.workflowsTable} ${className}`}>
|
||||
<FormGenerator
|
||||
data={workflows}
|
||||
columns={columns}
|
||||
data={logic.workflows}
|
||||
columns={logic.columns}
|
||||
searchable={true}
|
||||
filterable={true}
|
||||
sortable={true}
|
||||
resizable={true}
|
||||
pagination={true}
|
||||
pageSize={10}
|
||||
loading={loading}
|
||||
actions={actions}
|
||||
onDelete={handleDeleteSingle}
|
||||
onDeleteMultiple={handleDeleteMultiple}
|
||||
loading={logic.loading}
|
||||
actions={logic.actions}
|
||||
onDelete={logic.handleDeleteSingle}
|
||||
onDeleteMultiple={logic.handleDeleteMultiple}
|
||||
className={styles.workflowsFormGenerator}
|
||||
onRowClick={(workflow: Workflow) => {
|
||||
onRowClick={(workflow: any) => {
|
||||
// TODO: Navigate to workflow detail view
|
||||
console.log('Clicked workflow:', workflow);
|
||||
}}
|
||||
|
|
@ -343,19 +47,19 @@ function WorkflowsTable({ className = '' }: WorkflowsTableProps) {
|
|||
|
||||
{/* Edit Workflow Modal */}
|
||||
<Popup
|
||||
isOpen={editModalOpen}
|
||||
title={t('workflows.edit.title', 'Edit Workflow')}
|
||||
onClose={handleCancelEdit}
|
||||
isOpen={logic.editModalOpen}
|
||||
title="Edit Workflow"
|
||||
onClose={logic.handleCancelEdit}
|
||||
size="small"
|
||||
>
|
||||
{editingWorkflow && (
|
||||
{logic.editingWorkflow && (
|
||||
<EditForm
|
||||
data={editingWorkflow}
|
||||
fields={editWorkflowFields}
|
||||
onSave={handleSaveWorkflow}
|
||||
onCancel={handleCancelEdit}
|
||||
saveButtonText={t('common.save', 'Save')}
|
||||
cancelButtonText={t('common.cancel', 'Cancel')}
|
||||
data={logic.editingWorkflow}
|
||||
fields={logic.editWorkflowFields}
|
||||
onSave={logic.handleSaveWorkflow}
|
||||
onCancel={logic.handleCancelEdit}
|
||||
saveButtonText="Save"
|
||||
cancelButtonText="Cancel"
|
||||
/>
|
||||
)}
|
||||
</Popup>
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
export { default as WorkflowsTable } from './WorkflowsTable';
|
||||
export { useWorkflowsLogic } from './workflowsLogic';
|
||||
export * from './workflowsTypes';
|
||||
454
src/components/Workflows/workflowsLogic.tsx
Normal file
454
src/components/Workflows/workflowsLogic.tsx
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
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: string | number | undefined) => {
|
||||
if (!value) return '-';
|
||||
try {
|
||||
let date: Date;
|
||||
|
||||
// Handle Unix timestamp (as string or number)
|
||||
if (typeof value === 'string' && /^\d+$/.test(value)) {
|
||||
// Unix timestamp as string
|
||||
date = new Date(parseInt(value) * 1000);
|
||||
} else if (typeof value === 'number') {
|
||||
// Unix timestamp as number
|
||||
date = new Date(value * 1000);
|
||||
} else {
|
||||
// ISO string or other date format
|
||||
date = new Date(value);
|
||||
}
|
||||
|
||||
// Check if date is valid
|
||||
if (isNaN(date.getTime())) {
|
||||
console.warn('Invalid startedAt date:', value);
|
||||
return '-';
|
||||
}
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
} 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: string | number | undefined) => {
|
||||
if (!value) return '-';
|
||||
try {
|
||||
let date: Date;
|
||||
|
||||
// Handle Unix timestamp (as string or number)
|
||||
if (typeof value === 'string' && /^\d+$/.test(value)) {
|
||||
// Unix timestamp as string
|
||||
date = new Date(parseInt(value) * 1000);
|
||||
} else if (typeof value === 'number') {
|
||||
// Unix timestamp as number
|
||||
date = new Date(value * 1000);
|
||||
} else {
|
||||
// ISO string or other date format
|
||||
date = new Date(value);
|
||||
}
|
||||
|
||||
// Check if date is valid
|
||||
if (isNaN(date.getTime())) {
|
||||
console.warn('Invalid lastActivity date:', value);
|
||||
return '-';
|
||||
}
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
} 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
|
||||
};
|
||||
}
|
||||
59
src/components/Workflows/workflowsTypes.ts
Normal file
59
src/components/Workflows/workflowsTypes.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import React from 'react';
|
||||
import { Workflow } from '../../hooks/useWorkflows';
|
||||
|
||||
export interface WorkflowsTableProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowMessageCounts {
|
||||
[workflowId: string]: number;
|
||||
}
|
||||
|
||||
export interface WorkflowEditState {
|
||||
editModalOpen: boolean;
|
||||
editingWorkflow: Workflow | null;
|
||||
}
|
||||
|
||||
export interface WorkflowsLogicReturn {
|
||||
// Data
|
||||
workflows: Workflow[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
workflowMessageCounts: WorkflowMessageCounts;
|
||||
editModalOpen: boolean;
|
||||
editingWorkflow: Workflow | null;
|
||||
editWorkflowFields: any[];
|
||||
columns: any[];
|
||||
actions: any[];
|
||||
|
||||
// Actions
|
||||
handleDeleteSingle: (workflow: Workflow) => Promise<void>;
|
||||
handleDeleteMultiple: (workflows: Workflow[]) => Promise<void>;
|
||||
handleEditWorkflow: (workflow: Workflow) => void;
|
||||
handleSaveWorkflow: (updatedWorkflow: Workflow) => Promise<void>;
|
||||
handleCancelEdit: () => void;
|
||||
handlePlayWorkflow: (workflow: Workflow) => void;
|
||||
|
||||
// Refetch function
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
export interface WorkflowActionConfig {
|
||||
label: string;
|
||||
icon: (row: Workflow) => React.ReactElement;
|
||||
onClick: (row: Workflow) => void;
|
||||
}
|
||||
|
||||
export interface WorkflowColumnConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
type: string;
|
||||
width: number;
|
||||
minWidth: number;
|
||||
maxWidth: number;
|
||||
sortable: boolean;
|
||||
filterable: boolean;
|
||||
searchable?: boolean;
|
||||
filterOptions?: string[];
|
||||
formatter: (value: any, row?: any) => React.ReactElement | string;
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ function Dashboard() {
|
|||
// Get workflow ID from URL parameters
|
||||
const workflowIdFromUrl = searchParams.get('workflowId');
|
||||
const [workflowState, workflowActions] = useWorkflowManager(workflowIdFromUrl);
|
||||
const { workflows, loading: workflowsLoading, error: workflowsError } = useWorkflows();
|
||||
const { workflows, loading: workflowsLoading, error: workflowsError, refetch: refetchWorkflows } = useWorkflows();
|
||||
|
||||
const handleWorkflowSelect = useCallback((workflowId: string) => {
|
||||
workflowActions.loadWorkflow(workflowId);
|
||||
|
|
@ -44,6 +44,18 @@ function Dashboard() {
|
|||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
// Listen for workflow updates from other components (like WorkflowsTable)
|
||||
useEffect(() => {
|
||||
const handleWorkflowUpdated = (event: CustomEvent) => {
|
||||
console.log('Dashboard received workflow update event:', event.detail);
|
||||
// Refetch workflows to update the dropdown
|
||||
refetchWorkflows();
|
||||
};
|
||||
|
||||
window.addEventListener('workflowUpdated', handleWorkflowUpdated as EventListener);
|
||||
return () => window.removeEventListener('workflowUpdated', handleWorkflowUpdated as EventListener);
|
||||
}, [refetchWorkflows]);
|
||||
|
||||
const getWorkflowDisplayName = (workflow: Workflow) =>
|
||||
workflow.name || `${workflow.id.substring(0, 8)}...`;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue