Skip to content

Workflow Triggers #508

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,45 +28,50 @@ import {
PlusCircleIcon,
Trash2Icon,
WorkflowIcon,
Zap,
ZapIcon,
} from 'lucide-react';
import { useEffect, useState } from 'react';
import { useEffect, useState, useRef } from 'react';

import * as chatAPI from '../../../../lib/transformerlab-api-sdk';
import useSWR from 'swr';
import NewWorkflowModal from './NewWorkflowModal';
import NewNodeModal from './NewNodeModal';
import WorkflowCanvas from './WorkflowCanvas';
import WorkflowTriggersMenu from '../WorkflowTriggers/WorkflowTriggersMenu';
import { TriggerConfig, Workflow } from '../../../../types/workflow';

function ShowCode({ code }) {
const config = code?.config;
const fetcher = (url: string) => fetch(url).then((res) => res.json());

if (!config) {
return <></>;
}

let parsedConfig = {};

try {
parsedConfig = JSON.parse(config);
} catch (e) {}
interface ShowCodeProps {
code: {
config?: string;
};
}

function ShowCode({ code }: ShowCodeProps) {
return (
<Box
sx={{ width: '100%', backgroundColor: '#F7F9FB', overflow: 'scroll' }}
p={4}
>
<pre>{JSON.stringify(parsedConfig, null, 2)}</pre>
<Box sx={{ width: '100%', p: 2, overflow: 'auto' }}>
<pre>{JSON.stringify(code, null, 2)}</pre>
</Box>
);
}

const fetcher = (url: any) => fetch(url).then((res) => res.json());
interface WorkflowListProps {
experimentInfo: {
id: string;
name: string;
};
}

export default function WorkflowList({ experimentInfo }) {
const [selectedWorkflowId, setSelectedWorkflowId] = useState(null);
export default function WorkflowList({ experimentInfo }: WorkflowListProps) {
const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | null>(null);
const [newWorkflowModalOpen, setNewWorkflowModalOpen] = useState(false);
const [newNodeflowModalOpen, setNewNodeflowModalOpen] = useState(false);
const [newNodeModalOpen, setNewNodeModalOpen] = useState(false);
const [viewCodeMode, setViewCodeMode] = useState(false);
const [triggersMenuOpen, setTriggersMenuOpen] = useState(false);
const [selectedWorkflowDetails, setSelectedWorkflowDetails] = useState<Workflow | null>(null);
const triggersMenuRef = useRef<HTMLDivElement>(null);

const {
data: workflowsData,
Expand All @@ -75,6 +80,24 @@ export default function WorkflowList({ experimentInfo }) {
mutate: mutateWorkflows,
} = useSWR(chatAPI.Endpoints.Workflows.List(), fetcher);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use useAPI going forward

// Fetch detailed workflow data when a workflow is selected
const {
data: workflowDetailsData,
error: workflowDetailsError,
isLoading: isLoadingDetails,
mutate: mutateWorkflowDetails,
} = useSWR(
selectedWorkflowId ? chatAPI.Endpoints.Workflows.GetDetails(selectedWorkflowId) : null,
fetcher
);

// Fetch predefined triggers from backend
const {
data: predefinedTriggersData,
error: predefinedTriggersError,
isLoading: isLoadingPredefinedTriggers,
} = useSWR(chatAPI.Endpoints.Workflows.GetPredefinedTriggers(), fetcher);

// select the first workflow available:
useEffect(() => {
if (workflowsData && workflowsData.length > 0) {
Expand All @@ -84,15 +107,39 @@ export default function WorkflowList({ experimentInfo }) {
}
}, [workflowsData, selectedWorkflowId, newWorkflowModalOpen]);

const workflows = workflowsData;
// Close triggers menu when clicking outside
useEffect(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you should have to manage clickoutside etc. This should be managed by MUI Joy in the Menu Component

const handleClickOutside = (event: MouseEvent) => {
if (triggersMenuRef.current && !triggersMenuRef.current.contains(event.target as Node)) {
setTriggersMenuOpen(false);
}
};

if (triggersMenuOpen) {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}
}, [triggersMenuOpen]);

const selectedWorkflow = workflows?.find(
(workflow) => workflow.id === selectedWorkflowId,
const workflows = workflowsData as Workflow[];

const selectedWorkflow = workflowDetailsData || workflows?.find(
(workflow: Workflow) => workflow.id === selectedWorkflowId,
);

async function runWorkflow(workflowId: string) {
await fetch(chatAPI.Endpoints.Workflows.RunWorkflow(workflowId));
}

const handleTriggerConfigurationChange = (newConfigs: TriggerConfig[]) => {
// Update the workflow details with new trigger configs
mutateWorkflowDetails();
// Also refresh the workflows list in case it affects the list view
mutateWorkflows();
};

return (
<>
<NewWorkflowModal
Expand All @@ -106,13 +153,16 @@ export default function WorkflowList({ experimentInfo }) {
/>
{selectedWorkflow && (
<NewNodeModal
open={newNodeflowModalOpen}
open={newNodeModalOpen}
onClose={() => {
setNewNodeflowModalOpen(false);
setNewNodeModalOpen(false);
mutateWorkflows();
mutateWorkflowDetails();
}}
selectedWorkflow={selectedWorkflow}
experimentInfo={experimentInfo}
mutateWorkflows={mutateWorkflows}
mutateWorkflowDetails={mutateWorkflowDetails}
/>
)}
<Box
Expand Down Expand Up @@ -169,7 +219,7 @@ export default function WorkflowList({ experimentInfo }) {
<Typography level="title-lg">
Workflow {selectedWorkflow?.name}
</Typography>
<Box pl={2} display="flex" flexDirection="row" gap={1}>
<Box pl={2} display="flex" flexDirection="row" gap={1} position="relative">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does position=relative accomplish here?

<>
{selectedWorkflow?.status != 'RUNNING' ? (
<Button
Expand All @@ -184,10 +234,40 @@ export default function WorkflowList({ experimentInfo }) {
Running
</Button>
)}
<Button
variant="outlined"
disabled={!selectedWorkflow || isLoadingDetails || isLoadingPredefinedTriggers}
startDecorator={<Zap />}
onClick={() => setTriggersMenuOpen(!triggersMenuOpen)}
loading={isLoadingDetails || isLoadingPredefinedTriggers}
sx={{
minWidth: '120px',
position: 'relative'
}}
>
Set Triggers
</Button>
{triggersMenuOpen && selectedWorkflow && predefinedTriggersData && (
<Box
ref={triggersMenuRef}
sx={{
position: 'absolute',
right: 0,
top: '100%',
zIndex: 1200
}}
>
<WorkflowTriggersMenu
workflowId={selectedWorkflow.id}
currentTriggerConfigs={selectedWorkflow.trigger_configs || []}
predefinedTriggers={predefinedTriggersData}
onConfigurationChange={handleTriggerConfigurationChange}
/>
</Box>
)}
<IconButton
variant="plain"
disabled={!selectedWorkflow}
// startDecorator={<BookOpenIcon />}
onClick={() => setViewCodeMode(!viewCodeMode)}
>
{viewCodeMode ? <WorkflowIcon /> : <BracesIcon />}
Expand Down Expand Up @@ -252,8 +332,9 @@ export default function WorkflowList({ experimentInfo }) {
) : (
<WorkflowCanvas
selectedWorkflow={selectedWorkflow}
setNewNodeModalOpen={setNewNodeflowModalOpen}
setNewNodeModalOpen={setNewNodeModalOpen}
mutateWorkflows={mutateWorkflows}
mutateWorkflowDetails={mutateWorkflowDetails}
/>
)
) : (
Expand All @@ -266,4 +347,4 @@ export default function WorkflowList({ experimentInfo }) {
</Box>
</>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {
Box,
Switch,
Typography,
useColorScheme,
} from '@mui/joy';
import { TriggerBlueprint } from '../../../../types/workflow';

interface TriggerControlRowProps {
triggerBlueprint: TriggerBlueprint;
isEnabled: boolean;
onToggleChange: (triggerType: string, newIsEnabledState: boolean) => void;
}

export default function TriggerControlRow({
triggerBlueprint,
isEnabled,
onToggleChange,
}: TriggerControlRowProps) {
const { mode } = useColorScheme();

// Determine toggle color based on active state
// Using 'success' color for enabled state in both light and dark modes
// This provides consistent and clear visual feedback
const toggleColor = isEnabled ? 'success' : 'neutral';

return (
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
p: 2,
borderBottom: '1px solid',
borderColor: 'divider',
'&:last-child': {
borderBottom: 'none',
},
'&:hover': {
bgcolor: 'background.level1',
},
}}
>
<Box sx={{ flex: 1, mr: 2 }}>
<Typography
level="title-sm"
sx={{
fontWeight: 'bold',
mb: 0.5,
}}
>
{triggerBlueprint.name}
</Typography>
<Typography
level="body-sm"
sx={{
color: 'text.secondary',
lineHeight: 1.4,
}}
>
{triggerBlueprint.description}
</Typography>
</Box>
<Switch
checked={isEnabled}
onChange={(event) => onToggleChange(triggerBlueprint.trigger_type, event.target.checked)}
color={toggleColor}
/>
</Box>
);
}
Loading