Skip to content

Commit d347b8c

Browse files
TheodoreSpeaksTheodore Li
andauthored
Feat/add mothership manual workflow runs (#3520)
* Add run and open workflow buttons in workflow preview * Send log request message after manual workflow run * Make edges in embedded workflow non-editable * Change chat to pass in log as additional context * Revert "Change chat to pass in log as additional context" This reverts commit e957dff. * Revert "Send log request message after manual workflow run" This reverts commit 0fb9275. * Move run and workflow icons to tab bar * Simplify boolean condition --------- Co-authored-by: Theodore Li <theo@sim.ai>
1 parent 10e8eed commit d347b8c

File tree

6 files changed

+137
-36
lines changed

6 files changed

+137
-36
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export { ResourceContent } from './resource-content'
1+
export { EmbeddedWorkflowActions, ResourceContent } from './resource-content'
22
export { ResourceTabs } from './resource-tabs'
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { ResourceContent } from './resource-content'
1+
export { EmbeddedWorkflowActions, ResourceContent } from './resource-content'

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
'use client'
22

3-
import { lazy, Suspense, useMemo } from 'react'
4-
import { Skeleton } from '@/components/emcn'
3+
import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react'
4+
import { Square } from 'lucide-react'
5+
import { useRouter } from 'next/navigation'
6+
import { Button, PlayOutline, Skeleton, Tooltip } from '@/components/emcn'
7+
import { WorkflowIcon } from '@/components/icons'
58
import {
69
FileViewer,
710
type PreviewMode,
811
} from '@/app/workspace/[workspaceId]/files/components/file-viewer'
12+
import { useWorkspacePermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
913
import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
14+
import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution'
15+
import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/hooks'
1016
import { Table } from '@/app/workspace/[workspaceId]/tables/[tableId]/components'
1117
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
18+
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
19+
import { useCurrentWorkflowExecution } from '@/stores/execution'
20+
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
1221

1322
const Workflow = lazy(() => import('@/app/workspace/[workspaceId]/w/[workflowId]/workflow'))
1423

@@ -58,6 +67,88 @@ export function ResourceContent({ workspaceId, resource, previewMode }: Resource
5867
}
5968
}
6069

70+
interface EmbeddedWorkflowActionsProps {
71+
workspaceId: string
72+
workflowId: string
73+
}
74+
75+
export function EmbeddedWorkflowActions({
76+
workspaceId,
77+
workflowId,
78+
}: EmbeddedWorkflowActionsProps) {
79+
const router = useRouter()
80+
const { navigateToSettings } = useSettingsNavigation()
81+
const { userPermissions: effectivePermissions } = useWorkspacePermissionsContext()
82+
const setActiveWorkflow = useWorkflowRegistry((state) => state.setActiveWorkflow)
83+
const { handleRunWorkflow, handleCancelExecution } = useWorkflowExecution()
84+
const { isExecuting } = useCurrentWorkflowExecution()
85+
const { usageExceeded } = useUsageLimits()
86+
87+
useEffect(() => {
88+
setActiveWorkflow(workflowId)
89+
}, [setActiveWorkflow, workflowId])
90+
91+
const isRunButtonDisabled = !isExecuting && (!effectivePermissions.canRead && !effectivePermissions.isLoading)
92+
93+
const handleRun = useCallback(async () => {
94+
if (isExecuting) {
95+
await handleCancelExecution()
96+
return
97+
}
98+
99+
if (usageExceeded) {
100+
navigateToSettings({ section: 'subscription' })
101+
return
102+
}
103+
104+
await handleRunWorkflow()
105+
}, [handleCancelExecution, handleRunWorkflow, isExecuting, navigateToSettings, usageExceeded])
106+
107+
const handleOpenWorkflow = useCallback(() => {
108+
router.push(`/workspace/${workspaceId}/w/${workflowId}`)
109+
}, [router, workspaceId, workflowId])
110+
111+
return (
112+
<>
113+
<Tooltip.Root>
114+
<Tooltip.Trigger asChild>
115+
<Button
116+
variant='subtle'
117+
onClick={handleOpenWorkflow}
118+
className='shrink-0 bg-transparent px-[8px] py-[5px] text-[12px]'
119+
aria-label='Open workflow'
120+
>
121+
<WorkflowIcon className='h-[16px] w-[16px] text-[var(--text-icon)]' />
122+
</Button>
123+
</Tooltip.Trigger>
124+
<Tooltip.Content side='bottom'>
125+
<p>Open Workflow</p>
126+
</Tooltip.Content>
127+
</Tooltip.Root>
128+
<Tooltip.Root>
129+
<Tooltip.Trigger asChild>
130+
<Button
131+
variant='subtle'
132+
onClick={() => void handleRun()}
133+
disabled={isRunButtonDisabled}
134+
className='shrink-0 bg-transparent px-[8px] py-[5px] text-[12px]'
135+
aria-label={isExecuting ? 'Stop workflow' : 'Run workflow'}
136+
>
137+
{isExecuting ? (
138+
<Square className='h-[16px] w-[16px] text-[var(--text-icon)]' />
139+
) : (
140+
<PlayOutline className='h-[16px] w-[16px] text-[var(--text-icon)]' />
141+
)}
142+
</Button>
143+
</Tooltip.Trigger>
144+
<Tooltip.Content side='bottom'>
145+
<p>{isExecuting ? 'Stop' : 'Run'}</p>
146+
</Tooltip.Content>
147+
</Tooltip.Root>
148+
</>
149+
)
150+
}
151+
61152
interface EmbeddedFileProps {
62153
workspaceId: string
63154
fileId: string

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import type { ElementType, SVGProps } from 'react'
3+
import type { ElementType, ReactNode, SVGProps } from 'react'
44
import { Button, Tooltip } from '@/components/emcn'
55
import { PanelLeft, Table as TableIcon } from '@/components/emcn/icons'
66
import { WorkflowIcon } from '@/components/icons'
@@ -48,6 +48,7 @@ interface ResourceTabsProps {
4848
onCollapse: () => void
4949
previewMode?: PreviewMode
5050
onCyclePreviewMode?: () => void
51+
actions?: ReactNode
5152
}
5253

5354
const RESOURCE_ICONS: Record<Exclude<MothershipResourceType, 'file'>, ElementType> = {
@@ -73,6 +74,7 @@ export function ResourceTabs({
7374
onCollapse,
7475
previewMode,
7576
onCyclePreviewMode,
77+
actions,
7678
}: ResourceTabsProps) {
7779
return (
7880
<div className='flex shrink-0 items-center border-[var(--border)] border-b px-[16px] py-[8.5px]'>
@@ -118,26 +120,29 @@ export function ResourceTabs({
118120
)
119121
})}
120122
</div>
121-
{previewMode && onCyclePreviewMode && (
122-
<Tooltip.Root>
123-
<Tooltip.Trigger asChild>
124-
<Button
125-
variant='subtle'
126-
onClick={onCyclePreviewMode}
127-
className='ml-auto shrink-0 bg-transparent px-[8px] py-[5px] text-[12px]'
128-
aria-label='Cycle preview mode'
129-
>
130-
<PreviewModeIcon
131-
mode={previewMode}
132-
className='h-[16px] w-[16px] text-[var(--text-icon)]'
133-
/>
134-
</Button>
135-
</Tooltip.Trigger>
136-
<Tooltip.Content side='bottom'>
137-
<p>Preview mode</p>
138-
</Tooltip.Content>
139-
</Tooltip.Root>
140-
)}
123+
<div className='ml-auto flex shrink-0 items-center gap-[6px]'>
124+
{actions}
125+
{previewMode && onCyclePreviewMode && (
126+
<Tooltip.Root>
127+
<Tooltip.Trigger asChild>
128+
<Button
129+
variant='subtle'
130+
onClick={onCyclePreviewMode}
131+
className='shrink-0 bg-transparent px-[8px] py-[5px] text-[12px]'
132+
aria-label='Cycle preview mode'
133+
>
134+
<PreviewModeIcon
135+
mode={previewMode}
136+
className='h-[16px] w-[16px] text-[var(--text-icon)]'
137+
/>
138+
</Button>
139+
</Tooltip.Trigger>
140+
<Tooltip.Content side='bottom'>
141+
<p>Preview mode</p>
142+
</Tooltip.Content>
143+
</Tooltip.Root>
144+
)}
145+
</div>
141146
</div>
142147
)
143148
}

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/mothership-view.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { cn } from '@/lib/core/utils/cn'
55
import { getFileExtension } from '@/lib/uploads/utils/file-utils'
66
import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer'
77
import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
8-
import { ResourceContent, ResourceTabs } from './components'
8+
import { EmbeddedWorkflowActions, ResourceContent, ResourceTabs } from './components'
99

1010
const PREVIEWABLE_EXTENSIONS = new Set(['md', 'html', 'htm', 'csv'])
1111
const PREVIEW_ONLY_EXTENSIONS = new Set(['html', 'htm'])
@@ -53,6 +53,11 @@ export function MothershipView({
5353
const isActivePreviewable =
5454
active?.type === 'file' && PREVIEWABLE_EXTENSIONS.has(getFileExtension(active.title))
5555

56+
const headerActions =
57+
active?.type === 'workflow' ? (
58+
<EmbeddedWorkflowActions workspaceId={workspaceId} workflowId={active.id} />
59+
) : null
60+
5661
return (
5762
<div
5863
className={cn(
@@ -67,6 +72,7 @@ export function MothershipView({
6772
activeId={active?.id ?? null}
6873
onSelect={onSelectResource}
6974
onCollapse={onCollapse}
75+
actions={headerActions}
7076
previewMode={isActivePreviewable ? previewMode : undefined}
7177
onCyclePreviewMode={isActivePreviewable ? handleCyclePreview : undefined}
7278
/>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,8 @@ const WorkflowContent = React.memo(
379379

380380
const showTrainingModal = useCopilotTrainingStore((state) => state.showModal)
381381

382-
const { handleRunFromBlock, handleRunUntilBlock } = useWorkflowExecution()
382+
const { handleRunFromBlock, handleRunUntilBlock, handleRunWorkflow, handleCancelExecution } =
383+
useWorkflowExecution()
383384

384385
const snapToGridSize = useSnapToGridSize()
385386
const snapToGrid = snapToGridSize > 0
@@ -612,7 +613,6 @@ const WorkflowContent = React.memo(
612613

613614
const { userPermissions, workspacePermissions, permissionsError } =
614615
useWorkspacePermissionsContext()
615-
616616
/** Returns read-only permissions when viewing snapshot, otherwise user permissions. */
617617
const effectivePermissions = useMemo(() => {
618618
if (currentWorkflow.isSnapshotView) {
@@ -625,7 +625,6 @@ const WorkflowContent = React.memo(
625625
}
626626
return userPermissions
627627
}, [userPermissions, currentWorkflow.isSnapshotView])
628-
629628
const {
630629
collaborativeBatchAddEdges,
631630
collaborativeBatchRemoveEdges,
@@ -3836,9 +3835,9 @@ const WorkflowContent = React.memo(
38363835
edges={edgesWithSelection}
38373836
onNodesChange={onNodesChange}
38383837
onEdgesChange={onEdgesChange}
3839-
onConnect={effectivePermissions.canEdit ? onConnect : undefined}
3840-
onConnectStart={effectivePermissions.canEdit ? onConnectStart : undefined}
3841-
onConnectEnd={effectivePermissions.canEdit ? onConnectEnd : undefined}
3838+
onConnect={!embedded && effectivePermissions.canEdit ? onConnect : undefined}
3839+
onConnectStart={!embedded && effectivePermissions.canEdit ? onConnectStart : undefined}
3840+
onConnectEnd={!embedded && effectivePermissions.canEdit ? onConnectEnd : undefined}
38423841
nodeTypes={nodeTypes}
38433842
edgeTypes={edgeTypes}
38443843
onMouseDown={handleCanvasMouseDown}
@@ -3859,7 +3858,7 @@ const WorkflowContent = React.memo(
38593858
connectionLineStyle={connectionLineStyle}
38603859
connectionLineType={ConnectionLineType.SmoothStep}
38613860
onPaneClick={onPaneClick}
3862-
onEdgeClick={onEdgeClick}
3861+
onEdgeClick={embedded ? undefined : onEdgeClick}
38633862
onNodeClick={handleNodeClick}
38643863
onPaneContextMenu={handlePaneContextMenu}
38653864
onNodeContextMenu={handleNodeContextMenu}
@@ -3876,8 +3875,8 @@ const WorkflowContent = React.memo(
38763875
nodesDraggable={!embedded && effectivePermissions.canEdit}
38773876
draggable={false}
38783877
noWheelClassName='allow-scroll'
3879-
edgesFocusable={true}
3880-
edgesUpdatable={effectivePermissions.canEdit}
3878+
edgesFocusable={!embedded}
3879+
edgesUpdatable={!embedded && effectivePermissions.canEdit}
38813880
className={`workflow-container h-full bg-[var(--bg)] transition-opacity duration-150 ${reactFlowStyles} ${isCanvasReady ? 'opacity-100' : 'opacity-0'} ${isHandMode ? 'canvas-mode-hand' : 'canvas-mode-cursor'}`}
38823881
onNodeDrag={effectivePermissions.canEdit ? onNodeDrag : undefined}
38833882
onNodeDragStop={effectivePermissions.canEdit ? onNodeDragStop : undefined}
@@ -3985,7 +3984,7 @@ const WorkflowContent = React.memo(
39853984
{!embedded && <DiffControls />}
39863985
</div>
39873986

3988-
{!embedded && <Terminal />}
3987+
<Terminal />
39893988
</div>
39903989

39913990
{!embedded && <Panel />}

0 commit comments

Comments
 (0)