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 {
|
.workflowId {
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: var(--color-text-secondary, #666);
|
color: var(--color-gray);
|
||||||
cursor: help;
|
cursor: help;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflowName {
|
.workflowName {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--color-text-primary, #333);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.statusBadge {
|
.statusBadge {
|
||||||
|
|
@ -65,21 +65,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-running {
|
.status-running {
|
||||||
background-color: #d4edda;
|
background-color: var(--color-gray);
|
||||||
color: #155724;
|
color: white;
|
||||||
border: 1px solid #c3e6cb;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-completed {
|
.status-completed {
|
||||||
background-color: #d1ecf1;
|
background-color: var(--color-secondary);
|
||||||
color: #0c5460;
|
color: white;
|
||||||
border: 1px solid #bee5eb;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-failed {
|
.status-failed {
|
||||||
background-color: #f8d7da;
|
background-color: var(--color-red);
|
||||||
color: #721c24;
|
color: white;
|
||||||
border: 1px solid #f5c6cb;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-stopped {
|
.status-stopped {
|
||||||
|
|
@ -95,11 +92,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.roundNumber {
|
.roundNumber {
|
||||||
font-weight: 600;
|
font-weight: 400;
|
||||||
color: var(--color-primary, #007bff);
|
color: var(--color-text);
|
||||||
background-color: var(--color-primary-light, #e3f2fd);
|
background-color: var(--color-bg);
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
min-width: 24px;
|
min-width: 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -108,8 +104,8 @@
|
||||||
|
|
||||||
.messageCount {
|
.messageCount {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--color-text-secondary, #666);
|
color: var(--color-text);
|
||||||
background-color: var(--color-bg-secondary, #f8f9fa);
|
background-color: var(--color-bg);
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
|
@ -139,7 +135,7 @@
|
||||||
/* Dark mode support */
|
/* Dark mode support */
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.workflowId {
|
.workflowId {
|
||||||
color: #adb5bd;
|
color: var(--color-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflowName {
|
.workflowName {
|
||||||
|
|
@ -147,43 +143,33 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-running {
|
.status-running {
|
||||||
background-color: #155724;
|
background-color: var(--color-gray);
|
||||||
color: #d4edda;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-completed {
|
.status-completed {
|
||||||
background-color: #0c5460;
|
background-color: var(--color-secondary);
|
||||||
color: #d1ecf1;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-failed {
|
.status-failed {
|
||||||
background-color: #721c24;
|
background-color: var(--color-red);
|
||||||
color: #f8d7da;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-stopped {
|
.status-stopped {
|
||||||
background-color: #495057;
|
background-color: var(--color-primary);
|
||||||
color: #f5f5f5;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-pending {
|
|
||||||
background-color: #856404;
|
|
||||||
color: #fff3cd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roundNumber {
|
.roundNumber {
|
||||||
background-color: #1e3a8a;
|
background-color: var(--color-bg);
|
||||||
color: #bfdbfe;
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.messageCount {
|
.messageCount {
|
||||||
background-color: #374151;
|
background-color: var(--color-gray);
|
||||||
color: #d1d5db;
|
color: white;
|
||||||
}
|
|
||||||
|
|
||||||
.errorState {
|
|
||||||
background-color: #2d1b1e;
|
|
||||||
border-color: #5c2b33;
|
|
||||||
color: #f5c6cb;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 { FormGenerator } from '../FormGenerator/FormGenerator';
|
||||||
import { useWorkflows, useWorkflowOperations, Workflow } from '../../hooks/useWorkflows';
|
|
||||||
import { useLanguage } from '../../contexts/LanguageContext';
|
|
||||||
import { Popup, EditForm } from '../Popup';
|
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';
|
import styles from './WorkflowsTable.module.css';
|
||||||
|
|
||||||
interface WorkflowsTableProps {
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function WorkflowsTable({ className = '' }: WorkflowsTableProps) {
|
function WorkflowsTable({ className = '' }: WorkflowsTableProps) {
|
||||||
const { workflows, loading, error, refetch } = useWorkflows();
|
const logic = useWorkflowsLogic();
|
||||||
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]);
|
|
||||||
|
|
||||||
// Show error state
|
// Show error state
|
||||||
if (error) {
|
if (logic.error) {
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.workflowsTable} ${className}`}>
|
<div className={`${styles.workflowsTable} ${className}`}>
|
||||||
<div className={styles.errorState}>
|
<div className={styles.errorState}>
|
||||||
<p>{t('workflows.error.loading')} {error}</p>
|
<p>Error loading workflows: {logic.error}</p>
|
||||||
<button onClick={refetch} className={styles.retryButton}>
|
<button onClick={logic.refetch} className={styles.retryButton}>
|
||||||
{t('workflows.button.retry')}
|
Retry
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -322,20 +26,20 @@ function WorkflowsTable({ className = '' }: WorkflowsTableProps) {
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.workflowsTable} ${className}`}>
|
<div className={`${styles.workflowsTable} ${className}`}>
|
||||||
<FormGenerator
|
<FormGenerator
|
||||||
data={workflows}
|
data={logic.workflows}
|
||||||
columns={columns}
|
columns={logic.columns}
|
||||||
searchable={true}
|
searchable={true}
|
||||||
filterable={true}
|
filterable={true}
|
||||||
sortable={true}
|
sortable={true}
|
||||||
resizable={true}
|
resizable={true}
|
||||||
pagination={true}
|
pagination={true}
|
||||||
pageSize={10}
|
pageSize={10}
|
||||||
loading={loading}
|
loading={logic.loading}
|
||||||
actions={actions}
|
actions={logic.actions}
|
||||||
onDelete={handleDeleteSingle}
|
onDelete={logic.handleDeleteSingle}
|
||||||
onDeleteMultiple={handleDeleteMultiple}
|
onDeleteMultiple={logic.handleDeleteMultiple}
|
||||||
className={styles.workflowsFormGenerator}
|
className={styles.workflowsFormGenerator}
|
||||||
onRowClick={(workflow: Workflow) => {
|
onRowClick={(workflow: any) => {
|
||||||
// TODO: Navigate to workflow detail view
|
// TODO: Navigate to workflow detail view
|
||||||
console.log('Clicked workflow:', workflow);
|
console.log('Clicked workflow:', workflow);
|
||||||
}}
|
}}
|
||||||
|
|
@ -343,19 +47,19 @@ function WorkflowsTable({ className = '' }: WorkflowsTableProps) {
|
||||||
|
|
||||||
{/* Edit Workflow Modal */}
|
{/* Edit Workflow Modal */}
|
||||||
<Popup
|
<Popup
|
||||||
isOpen={editModalOpen}
|
isOpen={logic.editModalOpen}
|
||||||
title={t('workflows.edit.title', 'Edit Workflow')}
|
title="Edit Workflow"
|
||||||
onClose={handleCancelEdit}
|
onClose={logic.handleCancelEdit}
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
{editingWorkflow && (
|
{logic.editingWorkflow && (
|
||||||
<EditForm
|
<EditForm
|
||||||
data={editingWorkflow}
|
data={logic.editingWorkflow}
|
||||||
fields={editWorkflowFields}
|
fields={logic.editWorkflowFields}
|
||||||
onSave={handleSaveWorkflow}
|
onSave={logic.handleSaveWorkflow}
|
||||||
onCancel={handleCancelEdit}
|
onCancel={logic.handleCancelEdit}
|
||||||
saveButtonText={t('common.save', 'Save')}
|
saveButtonText="Save"
|
||||||
cancelButtonText={t('common.cancel', 'Cancel')}
|
cancelButtonText="Cancel"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Popup>
|
</Popup>
|
||||||
|
|
|
||||||
|
|
@ -1 +1,3 @@
|
||||||
export { default as WorkflowsTable } from './WorkflowsTable';
|
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
|
// Get workflow ID from URL parameters
|
||||||
const workflowIdFromUrl = searchParams.get('workflowId');
|
const workflowIdFromUrl = searchParams.get('workflowId');
|
||||||
const [workflowState, workflowActions] = useWorkflowManager(workflowIdFromUrl);
|
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) => {
|
const handleWorkflowSelect = useCallback((workflowId: string) => {
|
||||||
workflowActions.loadWorkflow(workflowId);
|
workflowActions.loadWorkflow(workflowId);
|
||||||
|
|
@ -44,6 +44,18 @@ function Dashboard() {
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
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) =>
|
const getWorkflowDisplayName = (workflow: Workflow) =>
|
||||||
workflow.name || `${workflow.id.substring(0, 8)}...`;
|
workflow.name || `${workflow.id.substring(0, 8)}...`;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue