mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
improvement(subblocks): fixed trigger save, schedule save, time inp, text subblocks and schedule/workflow badges, can now deploy from the badge itself (#1868)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { AlertCircle, Check, Save, Trash2 } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Tooltip } from '@/components/emcn/components/tooltip/tooltip'
|
||||
import { Button } from '@/components/emcn/components'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -13,8 +13,6 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { parseCronToHumanReadable } from '@/lib/schedules/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -377,6 +375,7 @@ export function ScheduleSave({ blockId, isPreview = false, disabled = false }: S
|
||||
<div className='mt-2'>
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={handleSave}
|
||||
disabled={disabled || isPreview || isSaving || saveStatus === 'saving' || isLoadingStatus}
|
||||
className={cn(
|
||||
@@ -391,37 +390,22 @@ export function ScheduleSave({ blockId, isPreview = false, disabled = false }: S
|
||||
Saving...
|
||||
</>
|
||||
)}
|
||||
{saveStatus === 'saved' && (
|
||||
<>
|
||||
<Check className='mr-2 h-4 w-4' />
|
||||
Saved
|
||||
</>
|
||||
)}
|
||||
{saveStatus === 'idle' && (
|
||||
<>
|
||||
<Save className='mr-2 h-4 w-4' />
|
||||
{scheduleId ? 'Update Schedule' : 'Save Schedule'}
|
||||
</>
|
||||
)}
|
||||
{saveStatus === 'error' && (
|
||||
<>
|
||||
<AlertCircle className='mr-2 h-4 w-4' />
|
||||
Error
|
||||
</>
|
||||
)}
|
||||
{saveStatus === 'saved' && 'Saved'}
|
||||
{saveStatus === 'idle' && (scheduleId ? 'Update Schedule' : 'Save Schedule')}
|
||||
{saveStatus === 'error' && 'Error'}
|
||||
</Button>
|
||||
|
||||
{scheduleId && (
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
disabled={disabled || isPreview || deleteStatus === 'deleting' || isSaving}
|
||||
variant='outline'
|
||||
className='h-9 rounded-[8px] px-3 text-destructive hover:bg-destructive/10'
|
||||
className='h-9 rounded-[8px] px-3'
|
||||
>
|
||||
{deleteStatus === 'deleting' ? (
|
||||
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
|
||||
) : (
|
||||
<Trash2 className='h-4 w-4' />
|
||||
<Trash className='h-[14px] w-[14px]' />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
@@ -442,54 +426,21 @@ export function ScheduleSave({ blockId, isPreview = false, disabled = false }: S
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Badge
|
||||
variant='outline'
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-1 font-normal text-xs',
|
||||
scheduleStatus === 'disabled'
|
||||
? 'border-amber-200 bg-amber-50 text-amber-600 hover:bg-amber-100 dark:bg-amber-900/20 dark:text-amber-400'
|
||||
: 'border-green-200 bg-green-50 text-green-600 hover:bg-green-100 dark:bg-green-900/20 dark:text-green-400'
|
||||
)}
|
||||
onClick={handleToggleStatus}
|
||||
>
|
||||
<div className='relative mr-0.5 flex items-center justify-center'>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute h-3 w-3 rounded-full',
|
||||
scheduleStatus === 'disabled' ? 'bg-amber-500/20' : 'bg-green-500/20'
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
'relative h-2 w-2 rounded-full',
|
||||
scheduleStatus === 'disabled' ? 'bg-amber-500' : 'bg-green-500'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{scheduleStatus === 'active' ? 'Active' : 'Disabled'}
|
||||
</Badge>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top' className='max-w-[300px]'>
|
||||
{scheduleStatus === 'disabled' ? (
|
||||
<p className='text-sm'>Click to reactivate this schedule</p>
|
||||
) : (
|
||||
<p className='text-sm'>Click to disable this schedule</p>
|
||||
)}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{failedCount > 0 && (
|
||||
{failedCount > 0 && (
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='text-destructive text-sm'>
|
||||
⚠️ {failedCount} failed run{failedCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{savedCronExpression && (
|
||||
<p className='text-muted-foreground text-sm'>
|
||||
{parseCronToHumanReadable(savedCronExpression, scheduleTimezone || 'UTC')}
|
||||
Runs{' '}
|
||||
{parseCronToHumanReadable(
|
||||
savedCronExpression,
|
||||
scheduleTimezone || 'UTC'
|
||||
).toLowerCase()}
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
@@ -27,10 +27,10 @@ export function Text({ blockId, subBlockId, content, className }: TextProps) {
|
||||
return (
|
||||
<div
|
||||
id={`${blockId}-${subBlockId}`}
|
||||
className={`rounded-md border bg-card p-4 shadow-sm ${className || ''}`}
|
||||
className={`rounded-md border bg-[#232323] p-4 shadow-sm ${className || ''}`}
|
||||
>
|
||||
<div
|
||||
className='prose prose-sm dark:prose-invert max-w-none text-sm [&_a]:text-blue-600 [&_a]:underline [&_a]:hover:text-blue-700 [&_a]:dark:text-blue-400 [&_a]:dark:hover:text-blue-300 [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:text-xs [&_strong]:font-semibold [&_ul]:ml-5 [&_ul]:list-disc'
|
||||
className='prose prose-sm dark:prose-invert max-w-none break-words text-sm [&_a]:text-blue-600 [&_a]:underline [&_a]:hover:text-blue-700 [&_a]:dark:text-blue-400 [&_a]:dark:hover:text-blue-300 [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:text-xs [&_strong]:font-semibold [&_ul]:ml-5 [&_ul]:list-disc'
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
</div>
|
||||
@@ -40,7 +40,7 @@ export function Text({ blockId, subBlockId, content, className }: TextProps) {
|
||||
return (
|
||||
<div
|
||||
id={`${blockId}-${subBlockId}`}
|
||||
className={`whitespace-pre-wrap rounded-md border bg-card p-4 text-muted-foreground text-sm shadow-sm ${className || ''}`}
|
||||
className={`whitespace-pre-wrap break-words rounded-md border bg-[#232323] p-4 text-muted-foreground text-sm shadow-sm ${className || ''}`}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { Clock } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { Button, Input, Popover, PopoverContent, PopoverTrigger } from '@/components/emcn'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||
|
||||
@@ -91,18 +88,15 @@ export function TimeInput({
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='outline'
|
||||
disabled={isPreview || disabled}
|
||||
className={cn(
|
||||
'w-full justify-start text-left font-normal',
|
||||
!value && 'text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Clock className='mr-1 h-4 w-4' />
|
||||
{value ? formatDisplayTime(value) : <span>{placeholder || 'Select time'}</span>}
|
||||
</Button>
|
||||
<div className='relative w-full cursor-pointer'>
|
||||
<Input
|
||||
readOnly
|
||||
disabled={isPreview || disabled}
|
||||
value={value ? formatDisplayTime(value) : ''}
|
||||
placeholder={placeholder || 'Select time'}
|
||||
className={cn('cursor-pointer', !value && 'text-muted-foreground', className)}
|
||||
/>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='w-auto p-4'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
@@ -129,7 +123,7 @@ export function TimeInput({
|
||||
}}
|
||||
type='text'
|
||||
/>
|
||||
<span>:</span>
|
||||
<span className='text-[#E6E6E6]'>:</span>
|
||||
<Input
|
||||
className='w-[4rem]'
|
||||
value={minute}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { AlertCircle, Check, Copy, Save, Trash2 } from 'lucide-react'
|
||||
import { Button } from '@/components/emcn/components'
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -11,8 +12,6 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
@@ -21,6 +20,7 @@ import { useWebhookManagement } from '@/hooks/use-webhook-management'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { getTrigger, isTriggerValid } from '@/triggers'
|
||||
import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/consts'
|
||||
import { ShortInput } from '../short-input/short-input'
|
||||
|
||||
const logger = createLogger('TriggerSave')
|
||||
|
||||
@@ -45,10 +45,20 @@ export function TriggerSave({
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
const [deleteStatus, setDeleteStatus] = useState<'idle' | 'deleting'>('idle')
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
const [testUrl, setTestUrl] = useState<string | null>(null)
|
||||
const [testUrlExpiresAt, setTestUrlExpiresAt] = useState<string | null>(null)
|
||||
const [isGeneratingTestUrl, setIsGeneratingTestUrl] = useState(false)
|
||||
const [copied, setCopied] = useState<string | null>(null)
|
||||
|
||||
const storedTestUrl = useSubBlockStore((state) => state.getValue(blockId, 'testUrl'))
|
||||
const storedTestUrlExpiresAt = useSubBlockStore((state) =>
|
||||
state.getValue(blockId, 'testUrlExpiresAt')
|
||||
)
|
||||
|
||||
const isTestUrlExpired = useMemo(() => {
|
||||
if (!storedTestUrlExpiresAt) return true
|
||||
return new Date(storedTestUrlExpiresAt) < new Date()
|
||||
}, [storedTestUrlExpiresAt])
|
||||
|
||||
const testUrl = isTestUrlExpired ? null : (storedTestUrl as string | null)
|
||||
const testUrlExpiresAt = isTestUrlExpired ? null : (storedTestUrlExpiresAt as string | null)
|
||||
|
||||
const effectiveTriggerId = useMemo(() => {
|
||||
if (triggerId && isTriggerValid(triggerId)) {
|
||||
@@ -203,6 +213,13 @@ export function TriggerSave({
|
||||
validateRequiredFields,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (isTestUrlExpired && storedTestUrl) {
|
||||
useSubBlockStore.getState().setValue(blockId, 'testUrl', null)
|
||||
useSubBlockStore.getState().setValue(blockId, 'testUrlExpiresAt', null)
|
||||
}
|
||||
}, [blockId, isTestUrlExpired, storedTestUrl])
|
||||
|
||||
const handleSave = async () => {
|
||||
if (isPreview || disabled) return
|
||||
|
||||
@@ -276,8 +293,10 @@ export function TriggerSave({
|
||||
throw new Error(err?.error || 'Failed to generate test URL')
|
||||
}
|
||||
const json = await res.json()
|
||||
setTestUrl(json.url)
|
||||
setTestUrlExpiresAt(json.expiresAt)
|
||||
useSubBlockStore.getState().setValue(blockId, 'testUrl', json.url)
|
||||
useSubBlockStore.getState().setValue(blockId, 'testUrlExpiresAt', json.expiresAt)
|
||||
collaborativeSetSubblockValue(blockId, 'testUrl', json.url)
|
||||
collaborativeSetSubblockValue(blockId, 'testUrlExpiresAt', json.expiresAt)
|
||||
} catch (e) {
|
||||
logger.error('Failed to generate test webhook URL', { error: e })
|
||||
setErrorMessage(
|
||||
@@ -288,12 +307,6 @@ export function TriggerSave({
|
||||
}
|
||||
}
|
||||
|
||||
const copyToClipboard = (text: string, type: string): void => {
|
||||
navigator.clipboard.writeText(text)
|
||||
setCopied(type)
|
||||
setTimeout(() => setCopied(null), 2000)
|
||||
}
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
if (isPreview || disabled || !webhookId) return
|
||||
setShowDeleteDialog(true)
|
||||
@@ -311,12 +324,15 @@ export function TriggerSave({
|
||||
setDeleteStatus('idle')
|
||||
setSaveStatus('idle')
|
||||
setErrorMessage(null)
|
||||
setTestUrl(null)
|
||||
setTestUrlExpiresAt(null)
|
||||
|
||||
useSubBlockStore.getState().setValue(blockId, 'testUrl', null)
|
||||
useSubBlockStore.getState().setValue(blockId, 'testUrlExpiresAt', null)
|
||||
|
||||
collaborativeSetSubblockValue(blockId, 'triggerPath', '')
|
||||
collaborativeSetSubblockValue(blockId, 'webhookId', null)
|
||||
collaborativeSetSubblockValue(blockId, 'triggerConfig', null)
|
||||
collaborativeSetSubblockValue(blockId, 'testUrl', null)
|
||||
collaborativeSetSubblockValue(blockId, 'testUrlExpiresAt', null)
|
||||
|
||||
logger.info('Trigger configuration deleted successfully', {
|
||||
blockId,
|
||||
@@ -344,6 +360,7 @@ export function TriggerSave({
|
||||
<div id={`${blockId}-${subBlockId}`}>
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={handleSave}
|
||||
disabled={disabled || isProcessing}
|
||||
className={cn(
|
||||
@@ -358,37 +375,22 @@ export function TriggerSave({
|
||||
Saving...
|
||||
</>
|
||||
)}
|
||||
{saveStatus === 'saved' && (
|
||||
<>
|
||||
<Check className='mr-2 h-4 w-4' />
|
||||
Saved
|
||||
</>
|
||||
)}
|
||||
{saveStatus === 'error' && (
|
||||
<>
|
||||
<AlertCircle className='mr-2 h-4 w-4' />
|
||||
Error
|
||||
</>
|
||||
)}
|
||||
{saveStatus === 'idle' && (
|
||||
<>
|
||||
<Save className='mr-2 h-4 w-4' />
|
||||
{webhookId ? 'Update Configuration' : 'Save Configuration'}
|
||||
</>
|
||||
)}
|
||||
{saveStatus === 'saved' && 'Saved'}
|
||||
{saveStatus === 'error' && 'Error'}
|
||||
{saveStatus === 'idle' && (webhookId ? 'Update Configuration' : 'Save Configuration')}
|
||||
</Button>
|
||||
|
||||
{webhookId && (
|
||||
<Button
|
||||
variant='default'
|
||||
onClick={handleDeleteClick}
|
||||
disabled={disabled || isProcessing}
|
||||
variant='outline'
|
||||
className='h-9 rounded-[8px] px-3 text-destructive hover:bg-destructive/10'
|
||||
className='h-9 rounded-[8px] px-3'
|
||||
>
|
||||
{deleteStatus === 'deleting' ? (
|
||||
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
|
||||
) : (
|
||||
<Trash2 className='h-4 w-4' />
|
||||
<Trash className='h-[14px] w-[14px]' />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
@@ -406,7 +408,6 @@ export function TriggerSave({
|
||||
<span className='font-medium text-sm'>Test Webhook URL</span>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={generateTestUrl}
|
||||
disabled={isGeneratingTestUrl || isProcessing}
|
||||
className='h-8 rounded-[8px]'
|
||||
@@ -424,29 +425,21 @@ export function TriggerSave({
|
||||
</Button>
|
||||
</div>
|
||||
{testUrl ? (
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
readOnly
|
||||
value={testUrl}
|
||||
className='h-9 flex-1 rounded-[8px] font-mono text-xs'
|
||||
onClick={(e: React.MouseEvent<HTMLInputElement>) =>
|
||||
(e.target as HTMLInputElement).select()
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type='button'
|
||||
size='icon'
|
||||
variant='outline'
|
||||
className='h-9 w-9 rounded-[8px]'
|
||||
onClick={() => copyToClipboard(testUrl, 'testUrl')}
|
||||
>
|
||||
{copied === 'testUrl' ? (
|
||||
<Check className='h-4 w-4 text-green-500' />
|
||||
) : (
|
||||
<Copy className='h-4 w-4' />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<ShortInput
|
||||
blockId={blockId}
|
||||
subBlockId={`${subBlockId}-test-url`}
|
||||
config={{
|
||||
id: `${subBlockId}-test-url`,
|
||||
type: 'short-input',
|
||||
readOnly: true,
|
||||
showCopyButton: true,
|
||||
}}
|
||||
value={testUrl}
|
||||
readOnly={true}
|
||||
showCopyButton={true}
|
||||
disabled={isPreview || disabled}
|
||||
isPreview={isPreview}
|
||||
/>
|
||||
) : (
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
Generate a temporary URL that executes this webhook against the live (un-deployed)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Return type for the useChildDeployment hook
|
||||
@@ -7,9 +7,11 @@ export interface UseChildDeploymentReturn {
|
||||
/** The active version number of the child workflow */
|
||||
activeVersion: number | null
|
||||
/** Whether the child workflow has an active deployment */
|
||||
isDeployed: boolean
|
||||
isDeployed: boolean | null
|
||||
/** Whether the deployment information is currently being fetched */
|
||||
isLoading: boolean
|
||||
/** Function to manually refetch deployment status */
|
||||
refetch: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20,67 +22,73 @@ export interface UseChildDeploymentReturn {
|
||||
*/
|
||||
export function useChildDeployment(childWorkflowId: string | undefined): UseChildDeploymentReturn {
|
||||
const [activeVersion, setActiveVersion] = useState<number | null>(null)
|
||||
const [isDeployed, setIsDeployed] = useState<boolean>(false)
|
||||
const [isDeployed, setIsDeployed] = useState<boolean | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [refetchTrigger, setRefetchTrigger] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchActiveVersion = useCallback(async (wfId: string) => {
|
||||
let cancelled = false
|
||||
|
||||
const fetchActiveVersion = async (wfId: string) => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const res = await fetch(`/api/workflows/${wfId}/deployments`, {
|
||||
cache: 'no-store',
|
||||
headers: { 'Cache-Control': 'no-cache' },
|
||||
})
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const res = await fetch(`/api/workflows/${wfId}/deployments`, {
|
||||
cache: 'no-store',
|
||||
headers: { 'Cache-Control': 'no-cache' },
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
if (!cancelled) {
|
||||
setActiveVersion(null)
|
||||
setIsDeployed(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const json = await res.json()
|
||||
const versions = Array.isArray(json?.data?.versions)
|
||||
? json.data.versions
|
||||
: Array.isArray(json?.versions)
|
||||
? json.versions
|
||||
: []
|
||||
|
||||
const active = versions.find((v: any) => v.isActive)
|
||||
|
||||
if (!cancelled) {
|
||||
const v = active ? Number(active.version) : null
|
||||
setActiveVersion(v)
|
||||
setIsDeployed(v != null)
|
||||
}
|
||||
} catch {
|
||||
if (!res.ok) {
|
||||
if (!cancelled) {
|
||||
setActiveVersion(null)
|
||||
setIsDeployed(false)
|
||||
setIsDeployed(null)
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setIsLoading(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (childWorkflowId) {
|
||||
void fetchActiveVersion(childWorkflowId)
|
||||
} else {
|
||||
setActiveVersion(null)
|
||||
setIsDeployed(false)
|
||||
const json = await res.json()
|
||||
const versions = Array.isArray(json?.data?.versions)
|
||||
? json.data.versions
|
||||
: Array.isArray(json?.versions)
|
||||
? json.versions
|
||||
: []
|
||||
|
||||
const active = versions.find((v: any) => v.isActive)
|
||||
|
||||
if (!cancelled) {
|
||||
const v = active ? Number(active.version) : null
|
||||
setActiveVersion(v)
|
||||
setIsDeployed(v != null) // true if deployed, false if undeployed
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setActiveVersion(null)
|
||||
setIsDeployed(null)
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setIsLoading(false)
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [childWorkflowId])
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (childWorkflowId) {
|
||||
void fetchActiveVersion(childWorkflowId)
|
||||
} else {
|
||||
setActiveVersion(null)
|
||||
setIsDeployed(null)
|
||||
}
|
||||
}, [childWorkflowId, refetchTrigger, fetchActiveVersion])
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
setRefetchTrigger((prev) => prev + 1)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
activeVersion,
|
||||
isDeployed,
|
||||
isLoading,
|
||||
refetch,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@ export interface UseChildWorkflowReturn {
|
||||
/** The active version of the child workflow */
|
||||
childActiveVersion: number | null
|
||||
/** Whether the child workflow is deployed */
|
||||
childIsDeployed: boolean
|
||||
childIsDeployed: boolean | null
|
||||
/** Whether the child version information is loading */
|
||||
isLoadingChildVersion: boolean
|
||||
/** Function to manually refetch deployment status */
|
||||
refetchDeployment: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,6 +55,7 @@ export function useChildWorkflow(
|
||||
activeVersion: childActiveVersion,
|
||||
isDeployed: childIsDeployed,
|
||||
isLoading: isLoadingChildVersion,
|
||||
refetch: refetchDeployment,
|
||||
} = useChildDeployment(isWorkflowSelector ? childWorkflowId : undefined)
|
||||
|
||||
return {
|
||||
@@ -60,5 +63,6 @@ export function useChildWorkflow(
|
||||
childActiveVersion,
|
||||
childIsDeployed,
|
||||
isLoadingChildVersion,
|
||||
refetchDeployment,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Handle, type NodeProps, Position, useUpdateNodeInternals } from 'reactflow'
|
||||
import { Badge } from '@/components/emcn/components/badge/badge'
|
||||
@@ -212,13 +212,57 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
disableSchedule,
|
||||
} = useScheduleInfo(id, type, currentWorkflowId)
|
||||
|
||||
const { childWorkflowId, childIsDeployed } = useChildWorkflow(
|
||||
const { childWorkflowId, childIsDeployed, refetchDeployment } = useChildWorkflow(
|
||||
id,
|
||||
type,
|
||||
data.isPreview ?? false,
|
||||
data.subBlockValues
|
||||
)
|
||||
|
||||
const [isDeploying, setIsDeploying] = useState(false)
|
||||
const setDeploymentStatus = useWorkflowRegistry((state) => state.setDeploymentStatus)
|
||||
|
||||
const deployWorkflow = useCallback(
|
||||
async (workflowId: string) => {
|
||||
if (isDeploying) return
|
||||
|
||||
try {
|
||||
setIsDeploying(true)
|
||||
const response = await fetch(`/api/workflows/${workflowId}/deploy`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
deployChatEnabled: false,
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const responseData = await response.json()
|
||||
const isDeployedStatus = responseData.isDeployed ?? false
|
||||
const deployedAtTime = responseData.deployedAt
|
||||
? new Date(responseData.deployedAt)
|
||||
: undefined
|
||||
setDeploymentStatus(
|
||||
workflowId,
|
||||
isDeployedStatus,
|
||||
deployedAtTime,
|
||||
responseData.apiKey || ''
|
||||
)
|
||||
refetchDeployment()
|
||||
} else {
|
||||
logger.error('Failed to deploy workflow')
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error deploying workflow:', error)
|
||||
} finally {
|
||||
setIsDeploying(false)
|
||||
}
|
||||
},
|
||||
[isDeploying, setDeploymentStatus, refetchDeployment]
|
||||
)
|
||||
|
||||
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
|
||||
|
||||
/**
|
||||
@@ -604,68 +648,80 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
</div>
|
||||
<div className='flex flex-shrink-0 items-center gap-2'>
|
||||
{isWorkflowSelector && childWorkflowId && (
|
||||
<Badge
|
||||
variant='outline'
|
||||
style={{
|
||||
borderColor: childIsDeployed ? '#22C55E' : '#EF4444',
|
||||
color: childIsDeployed ? '#22C55E' : '#EF4444',
|
||||
}}
|
||||
>
|
||||
{childIsDeployed ? 'deployed' : 'undeployed'}
|
||||
</Badge>
|
||||
)}
|
||||
{!isEnabled && <Badge>Disabled</Badge>}
|
||||
|
||||
{shouldShowScheduleBadge && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Badge
|
||||
variant='outline'
|
||||
className='cursor-pointer'
|
||||
style={{
|
||||
borderColor: scheduleInfo?.isDisabled ? '#FF6600' : '#22C55E',
|
||||
color: scheduleInfo?.isDisabled ? '#FF6600' : '#22C55E',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (scheduleInfo?.id) {
|
||||
if (scheduleInfo.isDisabled) {
|
||||
reactivateSchedule(scheduleInfo.id)
|
||||
} else {
|
||||
disableSchedule(scheduleInfo.id)
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className='relative flex items-center justify-center'>
|
||||
<div
|
||||
className='absolute h-3 w-3 rounded-full'
|
||||
<>
|
||||
{typeof childIsDeployed === 'boolean' ? (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Badge
|
||||
variant='outline'
|
||||
className={!childIsDeployed ? 'cursor-pointer' : ''}
|
||||
style={{
|
||||
backgroundColor: scheduleInfo?.isDisabled
|
||||
? 'rgba(255, 102, 0, 0.2)'
|
||||
: 'rgba(34, 197, 94, 0.2)',
|
||||
borderColor: childIsDeployed ? '#22C55E' : '#EF4444',
|
||||
color: childIsDeployed ? '#22C55E' : '#EF4444',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className='relative h-2 w-2 rounded-full'
|
||||
style={{
|
||||
backgroundColor: scheduleInfo?.isDisabled ? '#FF6600' : '#22C55E',
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (!childIsDeployed && childWorkflowId && !isDeploying) {
|
||||
deployWorkflow(childWorkflowId)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{scheduleInfo?.isDisabled ? 'Disabled' : 'Scheduled'}
|
||||
>
|
||||
{isDeploying ? 'Deploying...' : childIsDeployed ? 'deployed' : 'undeployed'}
|
||||
</Badge>
|
||||
</Tooltip.Trigger>
|
||||
{!childIsDeployed && (
|
||||
<Tooltip.Content>
|
||||
<span className='text-sm'>Click to deploy</span>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
) : (
|
||||
<Badge variant='outline' style={{ visibility: 'hidden' }}>
|
||||
deployed
|
||||
</Badge>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='top' className='max-w-[300px] p-4'>
|
||||
{scheduleInfo?.isDisabled ? (
|
||||
<p className='text-sm'>
|
||||
This schedule is currently disabled. Click the badge to reactivate it.
|
||||
</p>
|
||||
) : (
|
||||
<p className='text-sm'>Click the badge to disable this schedule.</p>
|
||||
)}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!isEnabled && <Badge>disabled</Badge>}
|
||||
|
||||
{type === 'schedule' && (
|
||||
<>
|
||||
{shouldShowScheduleBadge ? (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Badge
|
||||
variant='outline'
|
||||
className={scheduleInfo?.isDisabled ? 'cursor-pointer' : ''}
|
||||
style={{
|
||||
borderColor: scheduleInfo?.isDisabled ? '#FF6600' : '#22C55E',
|
||||
color: scheduleInfo?.isDisabled ? '#FF6600' : '#22C55E',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (scheduleInfo?.id) {
|
||||
if (scheduleInfo.isDisabled) {
|
||||
reactivateSchedule(scheduleInfo.id)
|
||||
} else {
|
||||
disableSchedule(scheduleInfo.id)
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{scheduleInfo?.isDisabled ? 'disabled' : 'scheduled'}
|
||||
</Badge>
|
||||
</Tooltip.Trigger>
|
||||
{scheduleInfo?.isDisabled && (
|
||||
<Tooltip.Content>
|
||||
<span className='text-sm'>Click to reactivate</span>
|
||||
</Tooltip.Content>
|
||||
)}
|
||||
</Tooltip.Root>
|
||||
) : (
|
||||
<Badge variant='outline' style={{ visibility: 'hidden' }}>
|
||||
scheduled
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{showWebhookIndicator && (
|
||||
|
||||
Reference in New Issue
Block a user