-
-
Notifications
You must be signed in to change notification settings - Fork 295
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
base: main
Are you sure you want to change the base?
Workflow Triggers #508
Changes from all commits
2c17c41
f4f3722
6cf0af1
e6dd590
5aaeb48
29f7532
7042a54
32cb20f
877cb82
60803dc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -75,6 +80,24 @@ export default function WorkflowList({ experimentInfo }) { | |
mutate: mutateWorkflows, | ||
} = useSWR(chatAPI.Endpoints.Workflows.List(), fetcher); | ||
|
||
// 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) { | ||
|
@@ -84,15 +107,39 @@ export default function WorkflowList({ experimentInfo }) { | |
} | ||
}, [workflowsData, selectedWorkflowId, newWorkflowModalOpen]); | ||
|
||
const workflows = workflowsData; | ||
// Close triggers menu when clicking outside | ||
useEffect(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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 | ||
|
@@ -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"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does position=relative accomplish here? |
||
<> | ||
{selectedWorkflow?.status != 'RUNNING' ? ( | ||
<Button | ||
|
@@ -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 />} | ||
|
@@ -252,8 +332,9 @@ export default function WorkflowList({ experimentInfo }) { | |
) : ( | ||
<WorkflowCanvas | ||
selectedWorkflow={selectedWorkflow} | ||
setNewNodeModalOpen={setNewNodeflowModalOpen} | ||
setNewNodeModalOpen={setNewNodeModalOpen} | ||
mutateWorkflows={mutateWorkflows} | ||
mutateWorkflowDetails={mutateWorkflowDetails} | ||
/> | ||
) | ||
) : ( | ||
|
@@ -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> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use useAPI going forward