mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 01:48:04 +08:00
Feat/environment variables in workflow (#6515)
Co-authored-by: JzoNg <jzongcode@gmail.com>
This commit is contained in:
@ -396,3 +396,4 @@ export const PARAMETER_EXTRACTOR_COMMON_STRUCT: Var[] = [
|
||||
|
||||
export const WORKFLOW_DATA_UPDATE = 'WORKFLOW_DATA_UPDATE'
|
||||
export const CUSTOM_NODE = 'custom'
|
||||
export const DSL_EXPORT_CHECK = 'DSL_EXPORT_CHECK'
|
||||
|
||||
85
web/app/components/workflow/dsl-export-confirm-modal.tsx
Normal file
85
web/app/components/workflow/dsl-export-confirm-modal.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiCloseLine, RiLock2Line } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
|
||||
export type DSLExportConfirmModalProps = {
|
||||
envList: EnvironmentVariable[]
|
||||
onConfirm: (state: boolean) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const DSLExportConfirmModal = ({
|
||||
envList = [],
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: DSLExportConfirmModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [exportSecrets, setExportSecrets] = useState<boolean>(false)
|
||||
|
||||
const submit = () => {
|
||||
onConfirm(exportSecrets)
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={true}
|
||||
onClose={() => { }}
|
||||
className={cn('max-w-[480px] w-[480px]')}
|
||||
>
|
||||
<div className='relative pb-6 title-2xl-semi-bold text-text-primary'>{t('workflow.env.export.title')}</div>
|
||||
<div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onClose}>
|
||||
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='relative'>
|
||||
<table className='w-full border-separate border-spacing-0 border border-divider-regular radius-md shadow-xs'>
|
||||
<thead className='system-xs-medium-uppercase text-text-tertiary'>
|
||||
<tr>
|
||||
<td width={220} className='h-7 pl-3 border-r border-b border-divider-regular'>NAME</td>
|
||||
<td className='h-7 pl-3 border-b border-divider-regular'>VALUE</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{envList.map((env, index) => (
|
||||
<tr key={env.name}>
|
||||
<td className={cn('h-7 pl-3 border-r system-xs-medium', index + 1 !== envList.length && 'border-b')}>
|
||||
<div className='flex gap-1 items-center w-[200px]'>
|
||||
<Env className='shrink-0 w-4 h-4 text-util-colors-violet-violet-600' />
|
||||
<div className='text-text-primary truncate'>{env.name}</div>
|
||||
<div className='shrink-0 text-text-tertiary'>Secret</div>
|
||||
<RiLock2Line className='shrink-0 w-3 h-3 text-text-tertiary' />
|
||||
</div>
|
||||
</td>
|
||||
<td className={cn('h-7 pl-3', index + 1 !== envList.length && 'border-b')}>
|
||||
<div className='system-xs-regular text-text-secondary truncate'>{env.value}</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className='mt-4 flex gap-2'>
|
||||
<Checkbox
|
||||
className='shrink-0'
|
||||
checked={exportSecrets}
|
||||
onCheck={() => setExportSecrets(!exportSecrets)}
|
||||
/>
|
||||
<div className='text-text-primary system-sm-medium cursor-pointer' onClick={() => setExportSecrets(!exportSecrets)}>{t('workflow.env.export.checkbox')}</div>
|
||||
</div>
|
||||
<div className='flex flex-row-reverse pt-6'>
|
||||
<Button className='ml-2' variant='primary' onClick={submit}>{exportSecrets ? t('workflow.env.export.export') : t('workflow.env.export.ignore')}</Button>
|
||||
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default DSLExportConfirmModal
|
||||
@ -57,22 +57,15 @@ const WorkflowChecklist = ({
|
||||
<PortalToFollowElemTrigger onClick={() => !disabled && setOpen(v => !v)}>
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex items-center justify-center p-0.5 w-8 h-8 rounded-lg border-[0.5px] border-gray-200 bg-white shadow-xs',
|
||||
'relative ml-0.5 flex items-center justify-center w-7 h-7 rounded-md',
|
||||
disabled && 'opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
group flex items-center justify-center w-full h-full rounded-md cursor-pointer
|
||||
hover:bg-primary-50
|
||||
${open && 'bg-primary-50'}
|
||||
`}
|
||||
className={cn('group flex items-center justify-center w-full h-full rounded-md cursor-pointer hover:bg-state-accent-hover', open && 'bg-state-accent-hover')}
|
||||
>
|
||||
<RiListCheck3
|
||||
className={`
|
||||
w-4 h-4 group-hover:text-primary-600
|
||||
${open ? 'text-primary-600' : 'text-gray-500'}`
|
||||
}
|
||||
className={cn('w-4 h-4 group-hover:text-components-button-secondary-accent-text', open ? 'text-components-button-secondary-accent-text' : 'text-components-button-ghost-text')}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
|
||||
22
web/app/components/workflow/header/env-button.tsx
Normal file
22
web/app/components/workflow/header/env-button.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { memo } from 'react'
|
||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const EnvButton = () => {
|
||||
const setShowEnvPanel = useStore(s => s.setShowEnvPanel)
|
||||
const setShowDebugAndPreviewPanel = useStore(s => s.setShowDebugAndPreviewPanel)
|
||||
|
||||
const handleClick = () => {
|
||||
setShowEnvPanel(true)
|
||||
setShowDebugAndPreviewPanel(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('relative flex items-center justify-center p-0.5 w-8 h-8 rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs cursor-pointer hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover')} onClick={handleClick}>
|
||||
<Env className='w-4 h-4 text-components-button-secondary-text' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(EnvButton)
|
||||
@ -4,6 +4,7 @@ import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { RiApps2AddLine } from '@remixicon/react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
@ -30,8 +31,7 @@ import EditingTitle from './editing-title'
|
||||
import RunningTitle from './running-title'
|
||||
import RestoringTitle from './restoring-title'
|
||||
import ViewHistory from './view-history'
|
||||
import Checklist from './checklist'
|
||||
import { Grid01 } from '@/app/components/base/icons/src/vender/line/layout'
|
||||
import EnvButton from './env-button'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { publishWorkflow } from '@/service/workflow'
|
||||
@ -44,10 +44,7 @@ const Header: FC = () => {
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const appSidebarExpand = useAppStore(s => s.appSidebarExpand)
|
||||
const appID = appDetail?.id
|
||||
const {
|
||||
nodesReadOnly,
|
||||
getNodesReadOnly,
|
||||
} = useNodesReadOnly()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const publishedAt = useStore(s => s.publishedAt)
|
||||
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
|
||||
const toolPublished = useStore(s => s.toolPublished)
|
||||
@ -167,14 +164,12 @@ const Header: FC = () => {
|
||||
</div>
|
||||
{
|
||||
normal && (
|
||||
<div className='flex items-center'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<EnvButton />
|
||||
<div className='w-[1px] h-3.5 bg-gray-200'></div>
|
||||
<RunAndHistory />
|
||||
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
|
||||
<Button
|
||||
className='mr-2'
|
||||
onClick={handleShowFeatures}
|
||||
>
|
||||
<Grid01 className='w-4 h-4 mr-1 text-gray-500' />
|
||||
<Button className='text-components-button-secondary-text' onClick={handleShowFeatures}>
|
||||
<RiApps2AddLine className='w-4 h-4 mr-1 text-components-button-secondary-text' />
|
||||
{t('workflow.common.features')}
|
||||
</Button>
|
||||
<AppPublisher
|
||||
@ -188,11 +183,9 @@ const Header: FC = () => {
|
||||
onPublish,
|
||||
onRestore: onStartRestoring,
|
||||
onToggle: onPublisherToggle,
|
||||
crossAxisOffset: 53,
|
||||
crossAxisOffset: 4,
|
||||
}}
|
||||
/>
|
||||
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
|
||||
<Checklist disabled={nodesReadOnly} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -215,10 +208,8 @@ const Header: FC = () => {
|
||||
{
|
||||
restoring && (
|
||||
<div className='flex items-center'>
|
||||
<Button
|
||||
onClick={handleShowFeatures}
|
||||
>
|
||||
<Grid01 className='w-4 h-4 mr-1 text-gray-500' />
|
||||
<Button className='text-components-button-secondary-text' onClick={handleShowFeatures}>
|
||||
<RiApps2AddLine className='w-4 h-4 mr-1 text-components-button-secondary-text' />
|
||||
{t('workflow.common.features')}
|
||||
</Button>
|
||||
<div className='mx-2 w-[1px] h-3.5 bg-gray-200'></div>
|
||||
|
||||
@ -3,21 +3,22 @@ import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiLoader2Line,
|
||||
RiPlayLargeFill,
|
||||
RiPlayLargeLine,
|
||||
} from '@remixicon/react'
|
||||
import { useStore } from '../store'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesReadOnly,
|
||||
useWorkflowRun,
|
||||
useWorkflowStartRun,
|
||||
} from '../hooks'
|
||||
import { WorkflowRunningStatus } from '../types'
|
||||
import ViewHistory from './view-history'
|
||||
import Checklist from './checklist'
|
||||
import cn from '@/utils/classnames'
|
||||
import {
|
||||
StopCircle,
|
||||
} from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { MessagePlay } from '@/app/components/base/icons/src/vender/line/communication'
|
||||
|
||||
const RunMode = memo(() => {
|
||||
const { t } = useTranslation()
|
||||
@ -30,9 +31,9 @@ const RunMode = memo(() => {
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center px-1.5 h-7 rounded-md text-[13px] font-medium text-primary-600',
|
||||
'hover:bg-primary-50 cursor-pointer',
|
||||
isRunning && 'bg-primary-50 !cursor-not-allowed',
|
||||
'flex items-center px-2.5 h-7 rounded-md text-[13px] font-medium text-components-button-secondary-accent-text',
|
||||
'hover:bg-state-accent-hover cursor-pointer',
|
||||
isRunning && 'bg-state-accent-hover !cursor-not-allowed',
|
||||
)}
|
||||
onClick={() => handleWorkflowStartRunInWorkflow()}
|
||||
>
|
||||
@ -46,7 +47,7 @@ const RunMode = memo(() => {
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<RiPlayLargeFill className='mr-1 w-4 h-4' />
|
||||
<RiPlayLargeLine className='mr-1 w-4 h-4' />
|
||||
{t('workflow.common.run')}
|
||||
</>
|
||||
)
|
||||
@ -58,7 +59,7 @@ const RunMode = memo(() => {
|
||||
className='flex items-center justify-center ml-0.5 w-7 h-7 cursor-pointer hover:bg-black/5 rounded-md'
|
||||
onClick={() => handleStopRun(workflowRunningData?.task_id || '')}
|
||||
>
|
||||
<StopCircle className='w-4 h-4 text-gray-500' />
|
||||
<StopCircle className='w-4 h-4 text-components-button-ghost-text' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -74,12 +75,12 @@ const PreviewMode = memo(() => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center px-1.5 h-7 rounded-md text-[13px] font-medium text-primary-600',
|
||||
'hover:bg-primary-50 cursor-pointer',
|
||||
'flex items-center px-2.5 h-7 rounded-md text-[13px] font-medium text-components-button-secondary-accent-text',
|
||||
'hover:bg-state-accent-hover cursor-pointer',
|
||||
)}
|
||||
onClick={() => handleWorkflowStartRunInChatflow()}
|
||||
>
|
||||
<MessagePlay className='mr-1 w-4 h-4' />
|
||||
<RiPlayLargeLine className='mr-1 w-4 h-4' />
|
||||
{t('workflow.common.debugAndPreview')}
|
||||
</div>
|
||||
)
|
||||
@ -88,17 +89,19 @@ PreviewMode.displayName = 'PreviewMode'
|
||||
|
||||
const RunAndHistory: FC = () => {
|
||||
const isChatMode = useIsChatMode()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
|
||||
return (
|
||||
<div className='flex items-center px-0.5 h-8 rounded-lg border-[0.5px] border-gray-200 bg-white shadow-xs'>
|
||||
<div className='flex items-center px-0.5 h-8 rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs'>
|
||||
{
|
||||
!isChatMode && <RunMode />
|
||||
}
|
||||
{
|
||||
isChatMode && <PreviewMode />
|
||||
}
|
||||
<div className='mx-0.5 w-[0.5px] h-8 bg-gray-200'></div>
|
||||
<div className='mx-0.5 w-[1px] h-3.5 bg-divider-regular'></div>
|
||||
<ViewHistory />
|
||||
<Checklist disabled={nodesReadOnly} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -103,16 +103,13 @@ const ViewHistory = ({
|
||||
popupContent={t('workflow.common.viewRunHistory')}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-center w-7 h-7 rounded-md hover:bg-black/5 cursor-pointer
|
||||
${open && 'bg-primary-50'}
|
||||
`}
|
||||
className={cn('group flex items-center justify-center w-7 h-7 rounded-md hover:bg-state-accent-hover cursor-pointer', open && 'bg-state-accent-hover')}
|
||||
onClick={() => {
|
||||
setCurrentLogItem()
|
||||
setShowMessageLogModal(false)
|
||||
}}
|
||||
>
|
||||
<ClockPlay className={`w-4 h-4 ${open ? 'text-primary-600' : 'text-gray-500'}`} />
|
||||
<ClockPlay className={cn('w-4 h-4 group-hover:text-components-button-secondary-accent-text', open ? 'text-components-button-secondary-accent-text' : 'text-components-button-ghost-text')} />
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
)
|
||||
@ -170,6 +167,7 @@ const ViewHistory = ({
|
||||
workflowStore.setState({
|
||||
historyWorkflowData: item,
|
||||
showInputsPanel: false,
|
||||
showEnvPanel: false,
|
||||
})
|
||||
handleBackupDraft()
|
||||
setOpen(false)
|
||||
|
||||
@ -14,3 +14,4 @@ export * from './use-panel-interactions'
|
||||
export * from './use-workflow-start-run'
|
||||
export * from './use-nodes-layout'
|
||||
export * from './use-workflow-history'
|
||||
export * from './use-workflow-variables'
|
||||
|
||||
@ -31,6 +31,7 @@ export const useNodesSyncDraft = () => {
|
||||
const [x, y, zoom] = transform
|
||||
const {
|
||||
appId,
|
||||
environmentVariables,
|
||||
syncWorkflowDraftHash,
|
||||
} = workflowStore.getState()
|
||||
|
||||
@ -80,6 +81,7 @@ export const useNodesSyncDraft = () => {
|
||||
sensitive_word_avoidance: features.moderation,
|
||||
file_upload: features.file,
|
||||
},
|
||||
environment_variables: environmentVariables,
|
||||
hash: syncWorkflowDraftHash,
|
||||
},
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import { WORKFLOW_DATA_UPDATE } from '../constants'
|
||||
import { DSL_EXPORT_CHECK, WORKFLOW_DATA_UPDATE } from '../constants'
|
||||
import type { WorkflowDataUpdator } from '../types'
|
||||
import {
|
||||
initialEdges,
|
||||
@ -66,11 +66,18 @@ export const useWorkflowUpdate = () => {
|
||||
appId,
|
||||
setSyncWorkflowDraftHash,
|
||||
setIsSyncingWorkflowDraft,
|
||||
setEnvironmentVariables,
|
||||
setEnvSecrets,
|
||||
} = workflowStore.getState()
|
||||
setIsSyncingWorkflowDraft(true)
|
||||
fetchWorkflowDraft(`/apps/${appId}/workflows/draft`).then((response) => {
|
||||
handleUpdateWorkflowCanvas(response.graph as WorkflowDataUpdator)
|
||||
setSyncWorkflowDraftHash(response.hash)
|
||||
setEnvSecrets((response.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => {
|
||||
acc[env.id] = env.value
|
||||
return acc
|
||||
}, {} as Record<string, string>))
|
||||
setEnvironmentVariables(response.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [])
|
||||
}).finally(() => setIsSyncingWorkflowDraft(false))
|
||||
}, [handleUpdateWorkflowCanvas, workflowStore])
|
||||
|
||||
@ -83,12 +90,13 @@ export const useWorkflowUpdate = () => {
|
||||
export const useDSL = () => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [exporting, setExporting] = useState(false)
|
||||
const { doSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
|
||||
const handleExportDSL = useCallback(async () => {
|
||||
const handleExportDSL = useCallback(async (include = false) => {
|
||||
if (!appDetail)
|
||||
return
|
||||
|
||||
@ -98,7 +106,10 @@ export const useDSL = () => {
|
||||
try {
|
||||
setExporting(true)
|
||||
await doSyncWorkflowDraft()
|
||||
const { data } = await exportAppConfig(appDetail.id)
|
||||
const { data } = await exportAppConfig({
|
||||
appID: appDetail.id,
|
||||
include,
|
||||
})
|
||||
const a = document.createElement('a')
|
||||
const file = new Blob([data], { type: 'application/yaml' })
|
||||
a.href = URL.createObjectURL(file)
|
||||
@ -113,7 +124,30 @@ export const useDSL = () => {
|
||||
}
|
||||
}, [appDetail, notify, t, doSyncWorkflowDraft, exporting])
|
||||
|
||||
const exportCheck = useCallback(async () => {
|
||||
if (!appDetail)
|
||||
return
|
||||
try {
|
||||
const workflowDraft = await fetchWorkflowDraft(`/apps/${appDetail?.id}/workflows/draft`)
|
||||
const list = (workflowDraft.environment_variables || []).filter(env => env.value_type === 'secret')
|
||||
if (list.length === 0) {
|
||||
handleExportDSL()
|
||||
return
|
||||
}
|
||||
eventEmitter?.emit({
|
||||
type: DSL_EXPORT_CHECK,
|
||||
payload: {
|
||||
data: list,
|
||||
},
|
||||
} as any)
|
||||
}
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: t('app.exportFailed') })
|
||||
}
|
||||
}, [appDetail, eventEmitter, handleExportDSL, notify, t])
|
||||
|
||||
return {
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@ export const useWorkflowRun = () => {
|
||||
const {
|
||||
backupDraft,
|
||||
setBackupDraft,
|
||||
environmentVariables,
|
||||
} = workflowStore.getState()
|
||||
const { features } = featuresStore!.getState()
|
||||
|
||||
@ -50,6 +51,7 @@ export const useWorkflowRun = () => {
|
||||
edges,
|
||||
viewport: getViewport(),
|
||||
features,
|
||||
environmentVariables,
|
||||
})
|
||||
doSyncWorkflowDraft()
|
||||
}
|
||||
@ -59,6 +61,7 @@ export const useWorkflowRun = () => {
|
||||
const {
|
||||
backupDraft,
|
||||
setBackupDraft,
|
||||
setEnvironmentVariables,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (backupDraft) {
|
||||
@ -67,12 +70,14 @@ export const useWorkflowRun = () => {
|
||||
edges,
|
||||
viewport,
|
||||
features,
|
||||
environmentVariables,
|
||||
} = backupDraft
|
||||
handleUpdateWorkflowCanvas({
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
})
|
||||
setEnvironmentVariables(environmentVariables)
|
||||
featuresStore!.setState({ features })
|
||||
setBackupDraft(undefined)
|
||||
}
|
||||
@ -522,6 +527,7 @@ export const useWorkflowRun = () => {
|
||||
})
|
||||
featuresStore?.setState({ features: publishedWorkflow.features })
|
||||
workflowStore.getState().setPublishedAt(publishedWorkflow.created_at)
|
||||
workflowStore.getState().setEnvironmentVariables(publishedWorkflow.environment_variables || [])
|
||||
}
|
||||
}, [featuresStore, handleUpdateWorkflowCanvas, workflowStore])
|
||||
|
||||
|
||||
@ -39,8 +39,11 @@ export const useWorkflowStartRun = () => {
|
||||
showDebugAndPreviewPanel,
|
||||
setShowDebugAndPreviewPanel,
|
||||
setShowInputsPanel,
|
||||
setShowEnvPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
setShowEnvPanel(false)
|
||||
|
||||
if (showDebugAndPreviewPanel) {
|
||||
handleCancelDebugAndPreviewPanel()
|
||||
return
|
||||
@ -63,8 +66,11 @@ export const useWorkflowStartRun = () => {
|
||||
showDebugAndPreviewPanel,
|
||||
setShowDebugAndPreviewPanel,
|
||||
setHistoryWorkflowData,
|
||||
setShowEnvPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
setShowEnvPanel(false)
|
||||
|
||||
if (showDebugAndPreviewPanel)
|
||||
handleCancelDebugAndPreviewPanel()
|
||||
else
|
||||
|
||||
69
web/app/components/workflow/hooks/use-workflow-variables.ts
Normal file
69
web/app/components/workflow/hooks/use-workflow-variables.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from '../store'
|
||||
import { getVarType, toNodeAvailableVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
ValueSelector,
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
export const useWorkflowVariables = () => {
|
||||
const { t } = useTranslation()
|
||||
const environmentVariables = useStore(s => s.environmentVariables)
|
||||
|
||||
const getNodeAvailableVars = useCallback(({
|
||||
parentNode,
|
||||
beforeNodes,
|
||||
isChatMode,
|
||||
filterVar,
|
||||
hideEnv,
|
||||
}: {
|
||||
parentNode?: Node | null
|
||||
beforeNodes: Node[]
|
||||
isChatMode: boolean
|
||||
filterVar: (payload: Var, selector: ValueSelector) => boolean
|
||||
hideEnv?: boolean
|
||||
}): NodeOutPutVar[] => {
|
||||
return toNodeAvailableVars({
|
||||
parentNode,
|
||||
t,
|
||||
beforeNodes,
|
||||
isChatMode,
|
||||
environmentVariables: hideEnv ? [] : environmentVariables,
|
||||
filterVar,
|
||||
})
|
||||
}, [environmentVariables, t])
|
||||
|
||||
const getCurrentVariableType = useCallback(({
|
||||
parentNode,
|
||||
valueSelector,
|
||||
isIterationItem,
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
isConstant,
|
||||
}: {
|
||||
valueSelector: ValueSelector
|
||||
parentNode?: Node | null
|
||||
isIterationItem?: boolean
|
||||
availableNodes: any[]
|
||||
isChatMode: boolean
|
||||
isConstant?: boolean
|
||||
}) => {
|
||||
return getVarType({
|
||||
parentNode,
|
||||
valueSelector,
|
||||
isIterationItem,
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
isConstant,
|
||||
environmentVariables,
|
||||
})
|
||||
}, [environmentVariables])
|
||||
|
||||
return {
|
||||
getNodeAvailableVars,
|
||||
getCurrentVariableType,
|
||||
}
|
||||
}
|
||||
@ -471,8 +471,14 @@ export const useWorkflowInit = () => {
|
||||
const handleGetInitialWorkflowData = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetchWorkflowDraft(`/apps/${appDetail.id}/workflows/draft`)
|
||||
|
||||
setData(res)
|
||||
workflowStore.setState({
|
||||
envSecrets: (res.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => {
|
||||
acc[env.id] = env.value
|
||||
return acc
|
||||
}, {} as Record<string, string>),
|
||||
environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [],
|
||||
})
|
||||
setSyncWorkflowDraftHash(res.hash)
|
||||
setIsLoading(false)
|
||||
}
|
||||
@ -491,6 +497,7 @@ export const useWorkflowInit = () => {
|
||||
features: {
|
||||
retriever_resource: { enabled: true },
|
||||
},
|
||||
environment_variables: [],
|
||||
},
|
||||
}).then((res) => {
|
||||
workflowStore.getState().setDraftUpdatedAt(res.updated_at)
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { setAutoFreeze } from 'immer'
|
||||
import {
|
||||
@ -30,6 +31,7 @@ import 'reactflow/dist/style.css'
|
||||
import './style.css'
|
||||
import type {
|
||||
Edge,
|
||||
EnvironmentVariable,
|
||||
Node,
|
||||
} from './types'
|
||||
import { WorkflowContextProvider } from './context'
|
||||
@ -62,6 +64,7 @@ import PanelContextmenu from './panel-contextmenu'
|
||||
import NodeContextmenu from './node-contextmenu'
|
||||
import SyncingDataModal from './syncing-data-modal'
|
||||
import UpdateDSLModal from './update-dsl-modal'
|
||||
import DSLExportConfirmModal from './dsl-export-confirm-modal'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
@ -74,6 +77,7 @@ import {
|
||||
} from './utils'
|
||||
import {
|
||||
CUSTOM_NODE,
|
||||
DSL_EXPORT_CHECK,
|
||||
ITERATION_CHILDREN_Z_INDEX,
|
||||
WORKFLOW_DATA_UPDATE,
|
||||
} from './constants'
|
||||
@ -114,6 +118,7 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||
const nodeAnimation = useStore(s => s.nodeAnimation)
|
||||
const showConfirm = useStore(s => s.showConfirm)
|
||||
const showImportDSLModal = useStore(s => s.showImportDSLModal)
|
||||
|
||||
const {
|
||||
setShowConfirm,
|
||||
setControlPromptEditorRerenderKey,
|
||||
@ -127,6 +132,8 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||
const { workflowReadOnly } = useWorkflowReadOnly()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
|
||||
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
|
||||
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
@ -148,6 +155,8 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||
|
||||
setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
|
||||
}
|
||||
if (v.type === DSL_EXPORT_CHECK)
|
||||
setSecretEnvList(v.payload.data as EnvironmentVariable[])
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@ -330,6 +339,15 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
secretEnvList.length > 0 && (
|
||||
<DSLExportConfirmModal
|
||||
envList={secretEnvList}
|
||||
onConfirm={handleExportDSL}
|
||||
onClose={() => setSecretEnvList([])}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<ReactFlow
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
|
||||
@ -5,12 +5,12 @@ import {
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from '../../../store'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodeDataUpdate,
|
||||
useWorkflow,
|
||||
useWorkflowVariables,
|
||||
} from '../../../hooks'
|
||||
import type {
|
||||
ValueSelector,
|
||||
@ -20,7 +20,6 @@ import type {
|
||||
import { useVariableAssigner } from '../../variable-assigner/hooks'
|
||||
import { filterVar } from '../../variable-assigner/utils'
|
||||
import AddVariablePopup from './add-variable-popup'
|
||||
import { toNodeAvailableVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
|
||||
type AddVariablePopupWithPositionProps = {
|
||||
nodeId: string
|
||||
@ -30,7 +29,6 @@ const AddVariablePopupWithPosition = ({
|
||||
nodeId,
|
||||
nodeData,
|
||||
}: AddVariablePopupWithPositionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const ref = useRef(null)
|
||||
const showAssignVariablePopup = useStore(s => s.showAssignVariablePopup)
|
||||
const setShowAssignVariablePopup = useStore(s => s.setShowAssignVariablePopup)
|
||||
@ -38,6 +36,7 @@ const AddVariablePopupWithPosition = ({
|
||||
const { handleAddVariableInAddVariablePopupWithPosition } = useVariableAssigner()
|
||||
const isChatMode = useIsChatMode()
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const { getNodeAvailableVars } = useWorkflowVariables()
|
||||
|
||||
const outputType = useMemo(() => {
|
||||
if (!showAssignVariablePopup)
|
||||
@ -55,9 +54,8 @@ const AddVariablePopupWithPosition = ({
|
||||
if (!showAssignVariablePopup)
|
||||
return []
|
||||
|
||||
return toNodeAvailableVars({
|
||||
return getNodeAvailableVars({
|
||||
parentNode: showAssignVariablePopup.parentNode,
|
||||
t,
|
||||
beforeNodes: [
|
||||
...getBeforeNodesInSameBranch(showAssignVariablePopup.nodeId),
|
||||
{
|
||||
@ -65,10 +63,16 @@ const AddVariablePopupWithPosition = ({
|
||||
data: showAssignVariablePopup.nodeData,
|
||||
} as any,
|
||||
],
|
||||
hideEnv: true,
|
||||
isChatMode,
|
||||
filterVar: filterVar(outputType as VarType),
|
||||
})
|
||||
}, [getBeforeNodesInSameBranch, isChatMode, showAssignVariablePopup, t, outputType])
|
||||
.map(node => ({
|
||||
...node,
|
||||
vars: node.isStartNode ? node.vars.filter(v => !v.variable.startsWith('sys.')) : node.vars,
|
||||
}))
|
||||
.filter(item => item.vars.length > 0)
|
||||
}, [showAssignVariablePopup, getNodeAvailableVars, getBeforeNodesInSameBranch, isChatMode, outputType])
|
||||
|
||||
useClickAway(() => {
|
||||
if (nodeData._holdAddVariablePopup) {
|
||||
|
||||
@ -5,9 +5,10 @@ import cn from 'classnames'
|
||||
import { useWorkflow } from '../../../hooks'
|
||||
import { BlockEnum } from '../../../types'
|
||||
import { VarBlockIcon } from '../../../block-icon'
|
||||
import { getNodeInfoById, isSystemVar } from './variable/utils'
|
||||
import { getNodeInfoById, isENV, isSystemVar } from './variable/utils'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
type Props = {
|
||||
nodeId: string
|
||||
value: string
|
||||
@ -40,25 +41,29 @@ const ReadonlyInputWithSelectVar: FC<Props> = ({
|
||||
|
||||
const value = vars[index].split('.')
|
||||
const isSystem = isSystemVar(value)
|
||||
const isEnv = isENV(value)
|
||||
const node = (isSystem ? startNode : getNodeInfoById(availableNodes, value[0]))?.data
|
||||
const varName = `${isSystem ? 'sys.' : ''}${value[value.length - 1]}`
|
||||
|
||||
return (<span key={index}>
|
||||
<span className='relative top-[-3px] leading-[16px]'>{str}</span>
|
||||
<div className=' inline-flex h-[16px] items-center px-1.5 rounded-[5px] bg-white'>
|
||||
<div className='flex items-center'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={node?.type || BlockEnum.Start}
|
||||
/>
|
||||
{!isEnv && (
|
||||
<div className='flex items-center'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={node?.type || BlockEnum.Start}
|
||||
/>
|
||||
</div>
|
||||
<div className='max-w-[60px] mx-0.5 text-xs font-medium text-gray-700 truncate' title={node?.title}>{node?.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</div>
|
||||
<div className='max-w-[60px] mx-0.5 text-xs font-medium text-gray-700 truncate' title={node?.title}>{node?.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex items-center text-primary-600'>
|
||||
<Variable02 className='w-3.5 h-3.5' />
|
||||
<div className='max-w-[50px] ml-0.5 text-xs font-medium truncate' title={varName}>{varName}</div>
|
||||
{!isEnv && <Variable02 className='shrink-0 w-3.5 h-3.5' />}
|
||||
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
<div className={cn('max-w-[50px] ml-0.5 text-xs font-medium truncate', isEnv && 'text-gray-900')} title={varName}>{varName}</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>)
|
||||
|
||||
@ -10,7 +10,9 @@ import type {
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type VariableTagProps = {
|
||||
valueSelector: ValueSelector
|
||||
@ -27,36 +29,40 @@ const VariableTag = ({
|
||||
|
||||
return nodes.find(node => node.id === valueSelector[0])
|
||||
}, [nodes, valueSelector])
|
||||
const isEnv = isENV(valueSelector)
|
||||
|
||||
const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.')
|
||||
|
||||
return (
|
||||
<div className='inline-flex items-center px-1.5 max-w-full h-6 text-xs rounded-md border-[0.5px] border-[rgba(16, 2440,0.08)] bg-white shadow-xs'>
|
||||
{
|
||||
node && (
|
||||
<VarBlockIcon
|
||||
className='shrink-0 mr-0.5 text-[#354052]'
|
||||
type={node!.data.type}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{!isEnv && (
|
||||
<>
|
||||
{node && (
|
||||
<VarBlockIcon
|
||||
className='shrink-0 mr-0.5 text-text-secondary'
|
||||
type={node!.data.type}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className='max-w-[60px] truncate text-text-secondary font-medium'
|
||||
title={node?.data.title}
|
||||
>
|
||||
{node?.data.title}
|
||||
</div>
|
||||
<Line3 className='shrink-0 mx-0.5' />
|
||||
<Variable02 className='shrink-0 mr-0.5 w-3.5 h-3.5 text-text-accent' />
|
||||
</>
|
||||
)}
|
||||
{isEnv && <Env className='shrink-0 mr-0.5 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
<div
|
||||
className='max-w-[60px] truncate text-[#354052] font-medium'
|
||||
title={node?.data.title}
|
||||
>
|
||||
{node?.data.title}
|
||||
</div>
|
||||
<Line3 className='shrink-0 mx-0.5' />
|
||||
<Variable02 className='shrink-0 mr-0.5 w-3.5 h-3.5 text-[#155AEF]' />
|
||||
<div
|
||||
className='truncate text-[#155AEF] font-medium'
|
||||
className={cn('truncate text-text-accent font-medium', isEnv && 'text-text-secondary')}
|
||||
title={variableName}
|
||||
>
|
||||
{variableName}
|
||||
</div>
|
||||
{
|
||||
varType && (
|
||||
<div className='shrink-0 ml-0.5 text-[#676F83]'>{capitalize(varType)}</div>
|
||||
<div className='shrink-0 ml-0.5 text-text-tertiary'>{capitalize(varType)}</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -15,7 +15,7 @@ import type { ParameterExtractorNodeType } from '../../../parameter-extractor/ty
|
||||
import type { IterationNodeType } from '../../../iteration/types'
|
||||
import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types'
|
||||
import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
|
||||
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { EnvironmentVariable, Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/types'
|
||||
import {
|
||||
HTTP_REQUEST_OUTPUT_STRUCT,
|
||||
@ -34,6 +34,10 @@ export const isSystemVar = (valueSelector: ValueSelector) => {
|
||||
return valueSelector[0] === 'sys' || valueSelector[1] === 'sys'
|
||||
}
|
||||
|
||||
export const isENV = (valueSelector: ValueSelector) => {
|
||||
return valueSelector[0] === 'env'
|
||||
}
|
||||
|
||||
const inputVarTypeToVarType = (type: InputVarType): VarType => {
|
||||
if (type === InputVarType.number)
|
||||
return VarType.number
|
||||
@ -59,7 +63,11 @@ const findExceptVarInObject = (obj: any, filterVar: (payload: Var, selector: Val
|
||||
return res
|
||||
}
|
||||
|
||||
const formatItem = (item: any, isChatMode: boolean, filterVar: (payload: Var, selector: ValueSelector) => boolean): NodeOutPutVar => {
|
||||
const formatItem = (
|
||||
item: any,
|
||||
isChatMode: boolean,
|
||||
filterVar: (payload: Var, selector: ValueSelector) => boolean,
|
||||
): NodeOutPutVar => {
|
||||
const { id, data } = item
|
||||
|
||||
const res: NodeOutPutVar = {
|
||||
@ -226,6 +234,16 @@ const formatItem = (item: any, isChatMode: boolean, filterVar: (payload: Var, se
|
||||
]
|
||||
break
|
||||
}
|
||||
|
||||
case 'env': {
|
||||
res.vars = data.envList.map((env: EnvironmentVariable) => {
|
||||
return {
|
||||
variable: `env.${env.name}`,
|
||||
type: env.value_type,
|
||||
}
|
||||
}) as Var[]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const selector = [id]
|
||||
@ -246,16 +264,30 @@ const formatItem = (item: any, isChatMode: boolean, filterVar: (payload: Var, se
|
||||
|
||||
return res
|
||||
}
|
||||
export const toNodeOutputVars = (nodes: any[], isChatMode: boolean, filterVar = (_payload: Var, _selector: ValueSelector) => true): NodeOutPutVar[] => {
|
||||
const res = nodes
|
||||
.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node.data.type))
|
||||
.map((node) => {
|
||||
return {
|
||||
...formatItem(node, isChatMode, filterVar),
|
||||
isStartNode: node.data.type === BlockEnum.Start,
|
||||
}
|
||||
})
|
||||
.filter(item => item.vars.length > 0)
|
||||
export const toNodeOutputVars = (
|
||||
nodes: any[],
|
||||
isChatMode: boolean,
|
||||
filterVar = (_payload: Var, _selector: ValueSelector) => true,
|
||||
environmentVariables: EnvironmentVariable[] = [],
|
||||
): NodeOutPutVar[] => {
|
||||
// ENV_NODE data format
|
||||
const ENV_NODE = {
|
||||
id: 'env',
|
||||
data: {
|
||||
title: 'ENVIRONMENT',
|
||||
type: 'env',
|
||||
envList: environmentVariables,
|
||||
},
|
||||
}
|
||||
const res = [
|
||||
...nodes.filter(node => SUPPORT_OUTPUT_VARS_NODE.includes(node.data.type)),
|
||||
...(environmentVariables.length > 0 ? [ENV_NODE] : []),
|
||||
].map((node) => {
|
||||
return {
|
||||
...formatItem(node, isChatMode, filterVar),
|
||||
isStartNode: node.data.type === BlockEnum.Start,
|
||||
}
|
||||
}).filter(item => item.vars.length > 0)
|
||||
return res
|
||||
}
|
||||
|
||||
@ -313,6 +345,7 @@ export const getVarType = ({
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
isConstant,
|
||||
environmentVariables = [],
|
||||
}:
|
||||
{
|
||||
valueSelector: ValueSelector
|
||||
@ -321,11 +354,17 @@ export const getVarType = ({
|
||||
availableNodes: any[]
|
||||
isChatMode: boolean
|
||||
isConstant?: boolean
|
||||
environmentVariables?: EnvironmentVariable[]
|
||||
}): VarType => {
|
||||
if (isConstant)
|
||||
return VarType.string
|
||||
|
||||
const beforeNodesOutputVars = toNodeOutputVars(availableNodes, isChatMode)
|
||||
const beforeNodesOutputVars = toNodeOutputVars(
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
undefined,
|
||||
environmentVariables,
|
||||
)
|
||||
|
||||
const isIterationInnerVar = parentNode?.data.type === BlockEnum.Iteration
|
||||
if (isIterationItem) {
|
||||
@ -346,6 +385,7 @@ export const getVarType = ({
|
||||
return VarType.number
|
||||
}
|
||||
const isSystem = isSystemVar(valueSelector)
|
||||
const isEnv = isENV(valueSelector)
|
||||
const startNode = availableNodes.find((node: any) => {
|
||||
return node.data.type === BlockEnum.Start
|
||||
})
|
||||
@ -358,7 +398,7 @@ export const getVarType = ({
|
||||
|
||||
let type: VarType = VarType.string
|
||||
let curr: any = targetVar.vars
|
||||
if (isSystem) {
|
||||
if (isSystem || isEnv) {
|
||||
return curr.find((v: any) => v.variable === (valueSelector as ValueSelector).join('.'))?.type
|
||||
}
|
||||
else {
|
||||
@ -383,6 +423,7 @@ export const toNodeAvailableVars = ({
|
||||
t,
|
||||
beforeNodes,
|
||||
isChatMode,
|
||||
environmentVariables,
|
||||
filterVar,
|
||||
}: {
|
||||
parentNode?: Node | null
|
||||
@ -390,9 +431,16 @@ export const toNodeAvailableVars = ({
|
||||
// to get those nodes output vars
|
||||
beforeNodes: Node[]
|
||||
isChatMode: boolean
|
||||
// env
|
||||
environmentVariables?: EnvironmentVariable[]
|
||||
filterVar: (payload: Var, selector: ValueSelector) => boolean
|
||||
}): NodeOutPutVar[] => {
|
||||
const beforeNodesOutputVars = toNodeOutputVars(beforeNodes, isChatMode, filterVar)
|
||||
const beforeNodesOutputVars = toNodeOutputVars(
|
||||
beforeNodes,
|
||||
isChatMode,
|
||||
filterVar,
|
||||
environmentVariables,
|
||||
)
|
||||
const isInIteration = parentNode?.data.type === BlockEnum.Iteration
|
||||
if (isInIteration) {
|
||||
const iterationNode: any = parentNode
|
||||
@ -402,6 +450,7 @@ export const toNodeAvailableVars = ({
|
||||
valueSelector: iterationNode?.data.iterator_selector || [],
|
||||
availableNodes: beforeNodes,
|
||||
isChatMode,
|
||||
environmentVariables,
|
||||
})
|
||||
const iterationVar = {
|
||||
nodeId: iterationNode?.id,
|
||||
@ -493,7 +542,7 @@ export const getNodeUsedVars = (node: Node): ValueSelector[] => {
|
||||
case BlockEnum.IfElse: {
|
||||
res = (data as IfElseNodeType).conditions?.map((c) => {
|
||||
return c.variable_selector
|
||||
})
|
||||
}) || []
|
||||
break
|
||||
}
|
||||
case BlockEnum.Code: {
|
||||
|
||||
@ -9,12 +9,13 @@ import {
|
||||
import produce from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import VarReferencePopup from './var-reference-popup'
|
||||
import { getNodeInfoById, getVarType, isSystemVar, toNodeAvailableVars } from './utils'
|
||||
import { getNodeInfoById, isENV, isSystemVar } from './utils'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
@ -24,6 +25,7 @@ import {
|
||||
import {
|
||||
useIsChatMode,
|
||||
useWorkflow,
|
||||
useWorkflowVariables,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
|
||||
@ -71,6 +73,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const { getTreeLeafNodes, getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const { getCurrentVariableType, getNodeAvailableVars } = useWorkflowVariables()
|
||||
const availableNodes = useMemo(() => {
|
||||
return passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranch(nodeId))
|
||||
}, [getBeforeNodesInSameBranch, getTreeLeafNodes, nodeId, onlyLeafNodeVar, passedInAvailableNodes])
|
||||
@ -97,16 +100,15 @@ const VarReferencePicker: FC<Props> = ({
|
||||
if (availableVars)
|
||||
return availableVars
|
||||
|
||||
const vars = toNodeAvailableVars({
|
||||
const vars = getNodeAvailableVars({
|
||||
parentNode: iterationNode,
|
||||
t,
|
||||
beforeNodes: availableNodes,
|
||||
isChatMode,
|
||||
filterVar,
|
||||
})
|
||||
|
||||
return vars
|
||||
}, [iterationNode, availableNodes, isChatMode, filterVar, availableVars, t])
|
||||
}, [iterationNode, availableNodes, isChatMode, filterVar, availableVars, getNodeAvailableVars])
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
useEffect(() => {
|
||||
@ -201,7 +203,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||
onChange([], varKindType)
|
||||
}, [onChange, varKindType])
|
||||
|
||||
const type = getVarType({
|
||||
const type = getCurrentVariableType({
|
||||
parentNode: iterationNode,
|
||||
valueSelector: value as ValueSelector,
|
||||
availableNodes,
|
||||
@ -209,6 +211,8 @@ const VarReferencePicker: FC<Props> = ({
|
||||
isConstant: !!isConstant,
|
||||
})
|
||||
|
||||
const isEnv = isENV(value as ValueSelector)
|
||||
|
||||
// 8(left/right-padding) + 14(icon) + 4 + 14 + 2 = 42 + 17 buff
|
||||
const availableWidth = triggerWidth - 56
|
||||
const [maxNodeNameWidth, maxVarNameWidth, maxTypeWidth] = (() => {
|
||||
@ -276,7 +280,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||
{hasValue
|
||||
? (
|
||||
<>
|
||||
{isShowNodeName && (
|
||||
{isShowNodeName && !isEnv && (
|
||||
<div className='flex items-center'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
@ -292,7 +296,8 @@ const VarReferencePicker: FC<Props> = ({
|
||||
)}
|
||||
<div className='flex items-center text-primary-600'>
|
||||
{!hasValue && <Variable02 className='w-3.5 h-3.5' />}
|
||||
<div className='ml-0.5 text-xs font-medium truncate' title={varName} style={{
|
||||
{isEnv && <Env className='w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
<div className={cn('ml-0.5 text-xs font-medium truncate', isEnv && '!text-gray-900')} title={varName} style={{
|
||||
maxWidth: maxVarNameWidth,
|
||||
}}>{varName}</div>
|
||||
</div>
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { checkKeys } from '@/utils/var'
|
||||
|
||||
type ObjectChildrenProps = {
|
||||
@ -48,6 +49,8 @@ const Item: FC<ItemProps> = ({
|
||||
itemWidth,
|
||||
}) => {
|
||||
const isObj = itemData.type === VarType.object && itemData.children && itemData.children.length > 0
|
||||
const isSys = itemData.variable.startsWith('sys.')
|
||||
const isEnv = itemData.variable.startsWith('env.')
|
||||
const itemRef = useRef(null)
|
||||
const [isItemHovering, setIsItemHovering] = useState(false)
|
||||
const _ = useHover(itemRef, {
|
||||
@ -76,7 +79,7 @@ const Item: FC<ItemProps> = ({
|
||||
}, [isHovering])
|
||||
const handleChosen = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (itemData.variable.startsWith('sys.')) { // system variable
|
||||
if (isSys || isEnv) { // system variable or environment variable
|
||||
onChange([...objPath, ...itemData.variable.split('.')], itemData)
|
||||
}
|
||||
else {
|
||||
@ -101,8 +104,9 @@ const Item: FC<ItemProps> = ({
|
||||
onClick={handleChosen}
|
||||
>
|
||||
<div className='flex items-center w-0 grow'>
|
||||
<Variable02 className='shrink-0 w-3.5 h-3.5 text-primary-500' />
|
||||
<div title={itemData.variable} className='ml-1 w-0 grow truncate text-[13px] font-normal text-gray-900'>{itemData.variable}</div>
|
||||
{!isEnv && <Variable02 className='shrink-0 w-3.5 h-3.5 text-primary-500' />}
|
||||
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
<div title={itemData.variable} className='ml-1 w-0 grow truncate text-[13px] font-normal text-gray-900'>{!isEnv ? itemData.variable : itemData.variable.replace('env.', '')}</div>
|
||||
</div>
|
||||
<div className='ml-1 shrink-0 text-xs font-normal text-gray-500 capitalize'>{itemData.type}</div>
|
||||
{isObj && (
|
||||
@ -205,8 +209,9 @@ const VarReferenceVars: FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
|
||||
const filteredVars = vars.filter((v) => {
|
||||
const children = v.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.'))
|
||||
const children = v.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.') || v.variable.startsWith('env.'))
|
||||
return children.length > 0
|
||||
}).filter((node) => {
|
||||
if (!searchText)
|
||||
@ -217,7 +222,7 @@ const VarReferenceVars: FC<Props> = ({
|
||||
})
|
||||
return children.length > 0
|
||||
}).map((node) => {
|
||||
let vars = node.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.'))
|
||||
let vars = node.vars.filter(v => checkKeys([v.variable], false).isValid || v.variable.startsWith('sys.') || v.variable.startsWith('env.'))
|
||||
if (searchText) {
|
||||
const searchTextLower = searchText.toLowerCase()
|
||||
if (!node.title.toLowerCase().includes(searchTextLower))
|
||||
@ -229,6 +234,7 @@ const VarReferenceVars: FC<Props> = ({
|
||||
vars,
|
||||
}
|
||||
})
|
||||
|
||||
const [isFocus, {
|
||||
setFalse: setBlur,
|
||||
setTrue: setFocus,
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useNodeInfo from './use-node-info'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useWorkflow,
|
||||
useWorkflowVariables,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { toNodeAvailableVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
type Params = {
|
||||
onlyLeafNodeVar?: boolean
|
||||
@ -18,9 +17,8 @@ const useAvailableVarList = (nodeId: string, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: () => true,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { getTreeLeafNodes, getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const { getNodeAvailableVars } = useWorkflowVariables()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const availableNodes = onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranch(nodeId)
|
||||
@ -29,9 +27,8 @@ const useAvailableVarList = (nodeId: string, {
|
||||
parentNode: iterationNode,
|
||||
} = useNodeInfo(nodeId)
|
||||
|
||||
const availableVars = toNodeAvailableVars({
|
||||
const availableVars = getNodeAvailableVars({
|
||||
parentNode: iterationNode,
|
||||
t,
|
||||
beforeNodes: availableNodes,
|
||||
isChatMode,
|
||||
filterVar,
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
useNodeDataUpdate,
|
||||
useWorkflow,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { getNodeInfoById, isSystemVar, toNodeOutputVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import { getNodeInfoById, isENV, isSystemVar, toNodeOutputVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
|
||||
import type { CommonNodeType, InputVar, ValueSelector, Var, Variable } from '@/app/components/workflow/types'
|
||||
import { BlockEnum, InputVarType, NodeRunningStatus, VarType } from '@/app/components/workflow/types'
|
||||
@ -329,7 +329,7 @@ const useOneStepRun = <T>({
|
||||
if (!variables)
|
||||
return []
|
||||
|
||||
const varInputs = variables.map((item) => {
|
||||
const varInputs = variables.filter(item => !isENV(item.value_selector)).map((item) => {
|
||||
const originalVar = getVar(item.value_selector)
|
||||
if (!originalVar) {
|
||||
return {
|
||||
|
||||
@ -161,7 +161,7 @@ const useConfig = (id: string, payload: CodeNodeType) => {
|
||||
})
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.string, VarType.number, VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject].includes(varPayload.type)
|
||||
return [VarType.string, VarType.number, VarType.secret, VarType.object, VarType.array, VarType.arrayNumber, VarType.arrayString, VarType.arrayObject].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
// single run
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import type { EndNodeType } from './types'
|
||||
import type { NodeProps, Variable } from '@/app/components/workflow/types'
|
||||
import { getVarType, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import { isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useWorkflow,
|
||||
useWorkflowVariables,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
const Node: FC<NodeProps<EndNodeType>> = ({
|
||||
@ -18,6 +21,7 @@ const Node: FC<NodeProps<EndNodeType>> = ({
|
||||
}) => {
|
||||
const { getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const availableNodes = getBeforeNodesInSameBranch(id)
|
||||
const { getCurrentVariableType } = useWorkflowVariables()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const startNode = availableNodes.find((node: any) => {
|
||||
@ -39,8 +43,9 @@ const Node: FC<NodeProps<EndNodeType>> = ({
|
||||
{filteredOutputs.map(({ value_selector }, index) => {
|
||||
const node = getNode(value_selector[0])
|
||||
const isSystem = isSystemVar(value_selector)
|
||||
const isEnv = isENV(value_selector)
|
||||
const varName = isSystem ? `sys.${value_selector[value_selector.length - 1]}` : value_selector[value_selector.length - 1]
|
||||
const varType = getVarType({
|
||||
const varType = getCurrentVariableType({
|
||||
valueSelector: value_selector,
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
@ -48,17 +53,22 @@ const Node: FC<NodeProps<EndNodeType>> = ({
|
||||
return (
|
||||
<div key={index} className='flex items-center h-6 justify-between bg-gray-100 rounded-md px-1 space-x-1 text-xs font-normal text-gray-700'>
|
||||
<div className='flex items-center text-xs font-medium text-gray-500'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={node?.data.type || BlockEnum.Start}
|
||||
/>
|
||||
</div>
|
||||
<div className='max-w-[75px] truncate'>{node?.data.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
{!isEnv && (
|
||||
<>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={node?.data.type || BlockEnum.Start}
|
||||
/>
|
||||
</div>
|
||||
<div className='max-w-[75px] truncate'>{node?.data.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</>
|
||||
)}
|
||||
<div className='flex items-center text-primary-600'>
|
||||
<Variable02 className='w-3.5 h-3.5' />
|
||||
<div className='max-w-[50px] ml-0.5 text-xs font-medium truncate'>{varName}</div>
|
||||
{!isEnv && <Variable02 className='shrink-0 w-3.5 h-3.5 text-primary-500' />}
|
||||
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
<div className={cn('max-w-[50px] ml-0.5 text-xs font-medium truncate', isEnv && '!max-w-[70px] text-gray-900')}>{varName}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='text-xs font-normal text-gray-700'>
|
||||
|
||||
@ -42,7 +42,7 @@ const ApiInput: FC<Props> = ({
|
||||
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number].includes(varPayload.type)
|
||||
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -1,17 +1,23 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import React, { useCallback } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import produce from 'immer'
|
||||
import type { Authorization as AuthorizationPayloadType } from '../../types'
|
||||
import { APIType, AuthorizationType } from '../../types'
|
||||
import RadioGroup from './radio-group'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import type { Var } from '@/app/components/workflow/types'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.http.authorization'
|
||||
|
||||
type Props = {
|
||||
nodeId: string
|
||||
payload: AuthorizationPayloadType
|
||||
onChange: (payload: AuthorizationPayloadType) => void
|
||||
isShow: boolean
|
||||
@ -31,6 +37,7 @@ const Field = ({ title, isRequired, children }: { title: string; isRequired?: bo
|
||||
}
|
||||
|
||||
const Authorization: FC<Props> = ({
|
||||
nodeId,
|
||||
payload,
|
||||
onChange,
|
||||
isShow,
|
||||
@ -38,6 +45,14 @@ const Authorization: FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isFocus, setIsFocus] = useState(false)
|
||||
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
const [tempPayload, setTempPayload] = React.useState<AuthorizationPayloadType>(payload)
|
||||
const handleAuthTypeChange = useCallback((type: string) => {
|
||||
const newPayload = produce(tempPayload, (draft: AuthorizationPayloadType) => {
|
||||
@ -80,6 +95,19 @@ const Authorization: FC<Props> = ({
|
||||
}
|
||||
}, [tempPayload, setTempPayload])
|
||||
|
||||
const handleAPIKeyChange = useCallback((str: string) => {
|
||||
const newPayload = produce(tempPayload, (draft: AuthorizationPayloadType) => {
|
||||
if (!draft.config) {
|
||||
draft.config = {
|
||||
type: APIType.basic,
|
||||
api_key: '',
|
||||
}
|
||||
}
|
||||
draft.config.api_key = str
|
||||
})
|
||||
setTempPayload(newPayload)
|
||||
}, [tempPayload, setTempPayload])
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
onChange(tempPayload)
|
||||
onHide()
|
||||
@ -128,12 +156,19 @@ const Authorization: FC<Props> = ({
|
||||
)}
|
||||
|
||||
<Field title={t(`${i18nPrefix}.api-key-title`)} isRequired>
|
||||
<input
|
||||
type='text'
|
||||
className='w-full h-8 leading-8 px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px] placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
|
||||
value={tempPayload.config?.api_key || ''}
|
||||
onChange={handleAPIKeyOrHeaderChange('api_key')}
|
||||
/>
|
||||
<div className='flex'>
|
||||
<Input
|
||||
instanceId='http-api-key'
|
||||
className={cn(isFocus ? 'shadow-xs bg-gray-50 border-gray-300' : 'bg-gray-100 border-gray-100', 'w-0 grow rounded-lg px-3 py-[6px] border')}
|
||||
value={tempPayload.config?.api_key || ''}
|
||||
onChange={handleAPIKeyChange}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodesWithParent}
|
||||
onFocusChange={setIsFocus}
|
||||
placeholder={' '}
|
||||
placeholderClassName='!leading-[21px]'
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -44,7 +44,7 @@ const EditBody: FC<Props> = ({
|
||||
const { availableVars, availableNodes } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number].includes(varPayload.type)
|
||||
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ const InputItem: FC<Props> = ({
|
||||
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number].includes(varPayload.type)
|
||||
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -125,6 +125,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
|
||||
</div>
|
||||
{(isShowAuthorization && !readOnly) && (
|
||||
<AuthorizationModal
|
||||
nodeId={id}
|
||||
isShow
|
||||
onHide={hideAuthorization}
|
||||
payload={inputs.authorization}
|
||||
|
||||
@ -103,7 +103,7 @@ const useConfig = (id: string, payload: HttpNodeType) => {
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.string, VarType.number].includes(varPayload.type)
|
||||
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
// single run
|
||||
|
||||
@ -9,8 +9,9 @@ import {
|
||||
isComparisonOperatorNeedTranslate,
|
||||
} from '../utils'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import cn from '@/utils/classnames'
|
||||
import { isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import { isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
|
||||
type ConditionValueProps = {
|
||||
variableSelector: string[]
|
||||
@ -32,7 +33,7 @@ const ConditionValue = ({
|
||||
return ''
|
||||
|
||||
return value.replace(/{{#([^#]*)#}}/g, (a, b) => {
|
||||
const arr = b.split('.')
|
||||
const arr: string[] = b.split('.')
|
||||
if (isSystemVar(arr))
|
||||
return `{{${b}}}`
|
||||
|
||||
@ -42,7 +43,8 @@ const ConditionValue = ({
|
||||
|
||||
return (
|
||||
<div className='flex items-center px-1 h-6 rounded-md bg-workflow-block-parma-bg'>
|
||||
<Variable02 className='shrink-0 mr-1 w-3.5 h-3.5 text-text-accent' />
|
||||
{!isENV(variableSelector) && <Variable02 className='shrink-0 mr-1 w-3.5 h-3.5 text-text-accent' />}
|
||||
{isENV(variableSelector) && <Env className='shrink-0 mr-1 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
<div
|
||||
className={cn(
|
||||
'shrink-0 truncate text-xs font-medium text-text-accent',
|
||||
|
||||
@ -328,11 +328,11 @@ const useConfig = (id: string, payload: LLMNodeType) => {
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const filterInputVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.number, VarType.string].includes(varPayload.type)
|
||||
return [VarType.number, VarType.string, VarType.secret].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.arrayObject, VarType.array, VarType.string].includes(varPayload.type)
|
||||
return [VarType.arrayObject, VarType.array, VarType.number, VarType.string, VarType.secret].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
const {
|
||||
|
||||
@ -40,7 +40,7 @@ const InputVarList: FC<Props> = ({
|
||||
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number].includes(varPayload.type)
|
||||
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
const paramType = (type: string) => {
|
||||
|
||||
@ -19,8 +19,8 @@ import {
|
||||
import { filterVar } from '../utils'
|
||||
import AddVariable from './add-variable'
|
||||
import NodeVariableItem from './node-variable-item'
|
||||
import { isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import cn from '@/utils/classnames'
|
||||
import { isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.variableAssigner'
|
||||
type GroupItem = {
|
||||
@ -55,7 +55,7 @@ const NodeGroupItem = ({
|
||||
const group = item.variableAssignerNodeData.advanced_settings?.groups.find(group => group.groupId === item.targetHandleId)
|
||||
return group?.output_type || ''
|
||||
}, [item.variableAssignerNodeData, item.targetHandleId, groupEnabled])
|
||||
const availableVars = getAvailableVars(item.variableAssignerNodeId, item.targetHandleId, filterVar(outputType as VarType))
|
||||
const availableVars = getAvailableVars(item.variableAssignerNodeId, item.targetHandleId, filterVar(outputType as VarType), true)
|
||||
const showSelectionBorder = useMemo(() => {
|
||||
if (groupEnabled && enteringNodePayload?.nodeId === item.variableAssignerNodeId) {
|
||||
if (hoveringAssignVariableGroupId)
|
||||
@ -123,12 +123,14 @@ const NodeGroupItem = ({
|
||||
{
|
||||
!!item.variables.length && item.variables.map((variable = [], index) => {
|
||||
const isSystem = isSystemVar(variable)
|
||||
const isEnv = isENV(variable)
|
||||
const node = isSystem ? nodes.find(node => node.data.type === BlockEnum.Start) : nodes.find(node => node.id === variable[0])
|
||||
const varName = isSystem ? `sys.${variable[variable.length - 1]}` : variable.slice(1).join('.')
|
||||
|
||||
return (
|
||||
<NodeVariableItem
|
||||
key={index}
|
||||
isEnv={isEnv}
|
||||
node={node as Node}
|
||||
varName={varName}
|
||||
showBorder={showSelectedBorder || showSelectionBorder}
|
||||
|
||||
@ -3,15 +3,18 @@ import cn from '@/utils/classnames'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
type NodeVariableItemProps = {
|
||||
isEnv: boolean
|
||||
node: Node
|
||||
varName: string
|
||||
showBorder?: boolean
|
||||
}
|
||||
const NodeVariableItem = ({
|
||||
isEnv,
|
||||
node,
|
||||
varName,
|
||||
showBorder,
|
||||
@ -21,19 +24,22 @@ const NodeVariableItem = ({
|
||||
'relative flex items-center mt-0.5 h-6 bg-gray-100 rounded-md px-1 text-xs font-normal text-gray-700',
|
||||
showBorder && '!bg-black/[0.02]',
|
||||
)}>
|
||||
<div className='flex items-center'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={node?.data.type || BlockEnum.Start}
|
||||
/>
|
||||
{!isEnv && (
|
||||
<div className='flex items-center'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={node?.data.type || BlockEnum.Start}
|
||||
/>
|
||||
</div>
|
||||
<div className='max-w-[85px] truncate mx-0.5 text-xs font-medium text-gray-700' title={node?.data.title}>{node?.data.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</div>
|
||||
<div className='max-w-[85px] truncate mx-0.5 text-xs font-medium text-gray-700' title={node?.data.title}>{node?.data.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex items-center text-primary-600'>
|
||||
<Variable02 className='w-3.5 h-3.5' />
|
||||
<div className='max-w-[75px] truncate ml-0.5 text-xs font-medium' title={varName}>{varName}</div>
|
||||
{!isEnv && <Variable02 className='shrink-0 w-3.5 h-3.5 text-primary-500' />}
|
||||
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
<div className={cn('max-w-[75px] truncate ml-0.5 text-xs font-medium', isEnv && 'text-gray-900')} title={varName}>{varName}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -3,13 +3,13 @@ import {
|
||||
useNodes,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { uniqBy } from 'lodash-es'
|
||||
import produce from 'immer'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodeDataUpdate,
|
||||
useWorkflow,
|
||||
useWorkflowVariables,
|
||||
} from '../../hooks'
|
||||
import type {
|
||||
Node,
|
||||
@ -21,7 +21,6 @@ import type {
|
||||
VarGroupItem,
|
||||
VariableAssignerNodeType,
|
||||
} from './types'
|
||||
import { toNodeAvailableVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
|
||||
export const useVariableAssigner = () => {
|
||||
const store = useStoreApi()
|
||||
@ -123,11 +122,11 @@ export const useVariableAssigner = () => {
|
||||
}
|
||||
|
||||
export const useGetAvailableVars = () => {
|
||||
const { t } = useTranslation()
|
||||
const nodes: Node[] = useNodes()
|
||||
const { getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
|
||||
const { getNodeAvailableVars } = useWorkflowVariables()
|
||||
const isChatMode = useIsChatMode()
|
||||
const getAvailableVars = useCallback((nodeId: string, handleId: string, filterVar: (v: Var) => boolean) => {
|
||||
const getAvailableVars = useCallback((nodeId: string, handleId: string, filterVar: (v: Var) => boolean, hideEnv = false) => {
|
||||
const availableNodes: Node[] = []
|
||||
const currentNode = nodes.find(node => node.id === nodeId)!
|
||||
|
||||
@ -138,14 +137,28 @@ export const useGetAvailableVars = () => {
|
||||
availableNodes.push(...beforeNodes)
|
||||
const parentNode = nodes.find(node => node.id === currentNode.parentId)
|
||||
|
||||
return toNodeAvailableVars({
|
||||
if (hideEnv) {
|
||||
return getNodeAvailableVars({
|
||||
parentNode,
|
||||
beforeNodes: uniqBy(availableNodes, 'id').filter(node => node.id !== nodeId),
|
||||
isChatMode,
|
||||
hideEnv,
|
||||
filterVar,
|
||||
})
|
||||
.map(node => ({
|
||||
...node,
|
||||
vars: node.isStartNode ? node.vars.filter(v => !v.variable.startsWith('sys.')) : node.vars,
|
||||
}))
|
||||
.filter(item => item.vars.length > 0)
|
||||
}
|
||||
|
||||
return getNodeAvailableVars({
|
||||
parentNode,
|
||||
t,
|
||||
beforeNodes: uniqBy(availableNodes, 'id').filter(node => node.id !== nodeId),
|
||||
isChatMode,
|
||||
filterVar,
|
||||
})
|
||||
}, [nodes, t, isChatMode, getBeforeNodesInSameBranchIncludeParent])
|
||||
}, [nodes, getBeforeNodesInSameBranchIncludeParent, getNodeAvailableVars, isChatMode])
|
||||
|
||||
return getAvailableVars
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ const Panel: FC<NodePanelProps<VariableAssignerNodeType>> = ({
|
||||
}}
|
||||
onChange={handleListOrTypeChange}
|
||||
groupEnabled={false}
|
||||
availableVars={getAvailableVars(id, 'target', filterVar(inputs.output_type))}
|
||||
availableVars={getAvailableVars(id, 'target', filterVar(inputs.output_type), true)}
|
||||
/>
|
||||
)
|
||||
: (<div>
|
||||
@ -67,7 +67,7 @@ const Panel: FC<NodePanelProps<VariableAssignerNodeType>> = ({
|
||||
canRemove={!readOnly && inputs.advanced_settings?.groups.length > 1}
|
||||
onRemove={handleGroupRemoved(item.groupId)}
|
||||
onGroupNameChange={handleVarGroupNameChange(item.groupId)}
|
||||
availableVars={getAvailableVars(id, item.groupId, filterVar(item.output_type))}
|
||||
availableVars={getAvailableVars(id, item.groupId, filterVar(item.output_type), true)}
|
||||
/>
|
||||
{index !== inputs.advanced_settings?.groups.length - 1 && <Split className='my-4' />}
|
||||
</div>
|
||||
|
||||
@ -26,7 +26,7 @@ const PanelContextmenu = () => {
|
||||
const { handlePaneContextmenuCancel } = usePanelInteractions()
|
||||
const { handleStartWorkflowRun } = useWorkflowStartRun()
|
||||
const { handleAddNote } = useOperator()
|
||||
const { handleExportDSL } = useDSL()
|
||||
const { exportCheck } = useDSL()
|
||||
|
||||
useClickAway(() => {
|
||||
handlePaneContextmenuCancel()
|
||||
@ -105,7 +105,7 @@ const PanelContextmenu = () => {
|
||||
<div className='p-1'>
|
||||
<div
|
||||
className='flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
|
||||
onClick={() => handleExportDSL()}
|
||||
onClick={() => exportCheck()}
|
||||
>
|
||||
{t('app.export')}
|
||||
</div>
|
||||
|
||||
210
web/app/components/workflow/panel/env-panel/index.tsx
Normal file
210
web/app/components/workflow/panel/env-panel/index.tsx
Normal file
@ -0,0 +1,210 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { capitalize } from 'lodash-es'
|
||||
import {
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { RiCloseLine, RiDeleteBinLine, RiEditLine, RiLock2Line } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import VariableTrigger from '@/app/components/workflow/panel/env-panel/variable-trigger'
|
||||
import type {
|
||||
EnvironmentVariable,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { findUsedVarNodes, updateNodeVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import RemoveEffectVarConfirm from '@/app/components/workflow/nodes/_base/components/remove-effect-var-confirm'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft'
|
||||
|
||||
const EnvPanel = () => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const setShowEnvPanel = useStore(s => s.setShowEnvPanel)
|
||||
const envList = useStore(s => s.environmentVariables) as EnvironmentVariable[]
|
||||
const envSecrets = useStore(s => s.envSecrets)
|
||||
const updateEnvList = useStore(s => s.setEnvironmentVariables)
|
||||
const setEnvSecrets = useStore(s => s.setEnvSecrets)
|
||||
const { doSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
const [showVariableModal, setShowVariableModal] = useState(false)
|
||||
const [currentVar, setCurrentVar] = useState<EnvironmentVariable>()
|
||||
|
||||
const [showRemoveVarConfirm, setShowRemoveConfirm] = useState(false)
|
||||
const [cacheForDelete, setCacheForDelete] = useState<EnvironmentVariable>()
|
||||
|
||||
const formatSecret = (s: string) => {
|
||||
return s.length > 8 ? `${s.slice(0, 6)}************${s.slice(-2)}` : '********************'
|
||||
}
|
||||
|
||||
const getEffectedNodes = useCallback((env: EnvironmentVariable) => {
|
||||
const { getNodes } = store.getState()
|
||||
const allNodes = getNodes()
|
||||
return findUsedVarNodes(
|
||||
['env', env.name],
|
||||
allNodes,
|
||||
)
|
||||
}, [store])
|
||||
|
||||
const removeUsedVarInNodes = useCallback((env: EnvironmentVariable) => {
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const effectedNodes = getEffectedNodes(env)
|
||||
const newNodes = getNodes().map((node) => {
|
||||
if (effectedNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, ['env', env.name], [])
|
||||
|
||||
return node
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [getEffectedNodes, store])
|
||||
|
||||
const handleDelete = useCallback((env: EnvironmentVariable) => {
|
||||
removeUsedVarInNodes(env)
|
||||
updateEnvList(envList.filter(e => e.id !== env.id))
|
||||
setCacheForDelete(undefined)
|
||||
setShowRemoveConfirm(false)
|
||||
doSyncWorkflowDraft()
|
||||
if (env.value_type === 'secret') {
|
||||
const newMap = { ...envSecrets }
|
||||
delete newMap[env.id]
|
||||
setEnvSecrets(newMap)
|
||||
}
|
||||
}, [doSyncWorkflowDraft, envList, envSecrets, removeUsedVarInNodes, setEnvSecrets, updateEnvList])
|
||||
|
||||
const deleteCheck = useCallback((env: EnvironmentVariable) => {
|
||||
const effectedNodes = getEffectedNodes(env)
|
||||
if (effectedNodes.length > 0) {
|
||||
setCacheForDelete(env)
|
||||
setShowRemoveConfirm(true)
|
||||
}
|
||||
else {
|
||||
handleDelete(env)
|
||||
}
|
||||
}, [getEffectedNodes, handleDelete])
|
||||
|
||||
const handleSave = useCallback(async (env: EnvironmentVariable) => {
|
||||
// add env
|
||||
let newEnv = env
|
||||
if (!currentVar) {
|
||||
if (env.value_type === 'secret') {
|
||||
setEnvSecrets({
|
||||
...envSecrets,
|
||||
[env.id]: formatSecret(env.value),
|
||||
})
|
||||
}
|
||||
const newList = [env, ...envList]
|
||||
updateEnvList(newList)
|
||||
await doSyncWorkflowDraft()
|
||||
updateEnvList(newList.map(e => (e.id === env.id && env.value_type === 'secret') ? { ...e, value: '[__HIDDEN__]' } : e))
|
||||
return
|
||||
}
|
||||
else if (currentVar.value_type === 'secret') {
|
||||
if (env.value_type === 'secret') {
|
||||
if (envSecrets[currentVar.id] !== env.value) {
|
||||
newEnv = env
|
||||
setEnvSecrets({
|
||||
...envSecrets,
|
||||
[env.id]: formatSecret(env.value),
|
||||
})
|
||||
}
|
||||
else {
|
||||
newEnv = { ...env, value: '[__HIDDEN__]' }
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (env.value_type === 'secret') {
|
||||
newEnv = env
|
||||
setEnvSecrets({
|
||||
...envSecrets,
|
||||
[env.id]: formatSecret(env.value),
|
||||
})
|
||||
}
|
||||
}
|
||||
const newList = envList.map(e => e.id === currentVar.id ? newEnv : e)
|
||||
updateEnvList(newList)
|
||||
// side effects of rename env
|
||||
if (currentVar.name !== env.name) {
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const effectedNodes = getEffectedNodes(currentVar)
|
||||
const newNodes = getNodes().map((node) => {
|
||||
if (effectedNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, ['env', currentVar.name], ['env', env.name])
|
||||
|
||||
return node
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}
|
||||
await doSyncWorkflowDraft()
|
||||
updateEnvList(newList.map(e => (e.id === env.id && env.value_type === 'secret') ? { ...e, value: '[__HIDDEN__]' } : e))
|
||||
}, [currentVar, doSyncWorkflowDraft, envList, envSecrets, getEffectedNodes, setEnvSecrets, store, updateEnvList])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex flex-col w-[400px] bg-components-panel-bg-alt rounded-l-2xl h-full border border-components-panel-border',
|
||||
)}
|
||||
>
|
||||
<div className='shrink-0 flex items-center justify-between p-4 pb-0 text-text-primary system-xl-semibold'>
|
||||
{t('workflow.env.envPanelTitle')}
|
||||
<div className='flex items-center'>
|
||||
<div
|
||||
className='flex items-center justify-center w-6 h-6 cursor-pointer'
|
||||
onClick={() => setShowEnvPanel(false)}
|
||||
>
|
||||
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='shrink-0 py-1 px-4 system-sm-regular text-text-tertiary'>{t('workflow.env.envDescription')}</div>
|
||||
<div className='shrink-0 px-4 pt-2 pb-3'>
|
||||
<VariableTrigger
|
||||
open={showVariableModal}
|
||||
setOpen={setShowVariableModal}
|
||||
env={currentVar}
|
||||
onSave={handleSave}
|
||||
onClose={() => setCurrentVar(undefined)}
|
||||
/>
|
||||
</div>
|
||||
<div className='grow px-4 rounded-b-2xl overflow-y-auto'>
|
||||
{envList.map(env => (
|
||||
<div
|
||||
key={env.name}
|
||||
className='mb-1 px-2.5 py-2 bg-components-panel-on-panel-item-bg radius-md border-[0.5px] border-components-panel-border-subtle shadow-xs'
|
||||
>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='grow flex gap-1 items-center'>
|
||||
<Env className='w-4 h-4 text-util-colors-violet-violet-600' />
|
||||
<div className='text-text-primary system-sm-medium'>{env.name}</div>
|
||||
<div className='text-text-tertiary system-xs-medium'>{capitalize(env.value_type)}</div>
|
||||
{env.value_type === 'secret' && <RiLock2Line className='w-3 h-3 text-text-tertiary' />}
|
||||
</div>
|
||||
<div className='shrink-0 flex gap-1 items-center text-text-tertiary'>
|
||||
<div className='p-1 radius-md cursor-pointer hover:bg-state-base-hover hover:text-text-secondary'>
|
||||
<RiEditLine className='w-4 h-4' onClick={() => {
|
||||
setCurrentVar(env)
|
||||
setShowVariableModal(true)
|
||||
}}/>
|
||||
</div>
|
||||
<div className='p-1 radius-md cursor-pointer hover:bg-state-destructive-hover hover:text-text-destructive'>
|
||||
<RiDeleteBinLine className='w-4 h-4' onClick={() => deleteCheck(env)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='text-text-tertiary system-xs-regular truncate'>{env.value_type === 'secret' ? envSecrets[env.id] : env.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<RemoveEffectVarConfirm
|
||||
isShow={showRemoveVarConfirm}
|
||||
onCancel={() => setShowRemoveConfirm(false)}
|
||||
onConfirm={() => cacheForDelete && handleDelete(cacheForDelete)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(EnvPanel)
|
||||
151
web/app/components/workflow/panel/env-panel/variable-modal.tsx
Normal file
151
web/app/components/workflow/panel/env-panel/variable-modal.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
import { RiCloseLine, RiQuestionLine } from '@remixicon/react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import Button from '@/app/components/base/button'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type ModalPropsType = {
|
||||
env?: EnvironmentVariable
|
||||
onClose: () => void
|
||||
onSave: (env: EnvironmentVariable) => void
|
||||
}
|
||||
const VariableModal = ({
|
||||
env,
|
||||
onClose,
|
||||
onSave,
|
||||
}: ModalPropsType) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const envList = useStore(s => s.environmentVariables)
|
||||
const envSecrets = useStore(s => s.envSecrets)
|
||||
const [type, setType] = React.useState<'string' | 'number' | 'secret'>('string')
|
||||
const [name, setName] = React.useState('')
|
||||
const [value, setValue] = React.useState<any>()
|
||||
|
||||
const handleNameChange = (v: string) => {
|
||||
if (!v)
|
||||
return setName('')
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(v))
|
||||
return notify({ type: 'error', message: 'name is can only contain letters, numbers and underscores' })
|
||||
if (/^[0-9]/.test(v))
|
||||
return notify({ type: 'error', message: 'name can not start with a number' })
|
||||
setName(v)
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
if (!name)
|
||||
return notify({ type: 'error', message: 'name can not be empty' })
|
||||
if (!value)
|
||||
return notify({ type: 'error', message: 'value can not be empty' })
|
||||
if (!env && envList.some(env => env.name === name))
|
||||
return notify({ type: 'error', message: 'name is existed' })
|
||||
onSave({
|
||||
id: env ? env.id : uuid4(),
|
||||
value_type: type,
|
||||
name,
|
||||
value: type === 'number' ? Number(value) : value,
|
||||
})
|
||||
onClose()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (env) {
|
||||
setType(env.value_type)
|
||||
setName(env.name)
|
||||
setValue(env.value_type === 'secret' ? envSecrets[env.id] : env.value)
|
||||
}
|
||||
}, [env, envSecrets])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('flex flex-col w-[360px] bg-components-panel-bg rounded-2xl h-full border-[0.5px] border-components-panel-border shadow-2xl')}
|
||||
>
|
||||
<div className='shrink-0 flex items-center justify-between mb-3 p-4 pb-0 text-text-primary system-xl-semibold'>
|
||||
{!env ? t('workflow.env.modal.title') : t('workflow.env.modal.editTitle')}
|
||||
<div className='flex items-center'>
|
||||
<div
|
||||
className='flex items-center justify-center w-6 h-6 cursor-pointer'
|
||||
onClick={onClose}
|
||||
>
|
||||
<RiCloseLine className='w-4 h-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-4 py-2'>
|
||||
{/* type */}
|
||||
<div className='mb-4'>
|
||||
<div className='mb-1 text-text-secondary system-sm-semibold'>{t('workflow.env.modal.type')}</div>
|
||||
<div className='flex gap-2'>
|
||||
<div className={cn(
|
||||
'w-[106px] flex items-center justify-center p-2 radius-md bg-components-option-card-option-bg border border-components-option-card-option-border text-text-secondary system-sm-regular cursor-pointer hover:shadow-xs hover:bg-components-option-card-option-bg-hover hover:border-components-option-card-option-border-hover',
|
||||
type === 'string' && 'text-text-primary system-sm-medium border-[1.5px] shadow-xs bg-components-option-card-option-selected-bg border-components-option-card-option-selected-border hover:border-components-option-card-option-selected-border',
|
||||
)} onClick={() => setType('string')}>String</div>
|
||||
<div className={cn(
|
||||
'w-[106px] flex items-center justify-center p-2 radius-md bg-components-option-card-option-bg border border-components-option-card-option-border text-text-secondary system-sm-regular cursor-pointer hover:shadow-xs hover:bg-components-option-card-option-bg-hover hover:border-components-option-card-option-border-hover',
|
||||
type === 'number' && 'text-text-primary font-medium border-[1.5px] shadow-xs bg-components-option-card-option-selected-bg border-components-option-card-option-selected-border hover:border-components-option-card-option-selected-border',
|
||||
)} onClick={() => {
|
||||
setType('number')
|
||||
if (!(/^[0-9]$/).test(value))
|
||||
setValue('')
|
||||
}}>Number</div>
|
||||
<div className={cn(
|
||||
'w-[106px] flex items-center justify-center p-2 radius-md bg-components-option-card-option-bg border border-components-option-card-option-border text-text-secondary system-sm-regular cursor-pointer hover:shadow-xs hover:bg-components-option-card-option-bg-hover hover:border-components-option-card-option-border-hover',
|
||||
type === 'secret' && 'text-text-primary font-medium border-[1.5px] shadow-xs bg-components-option-card-option-selected-bg border-components-option-card-option-selected-border hover:border-components-option-card-option-selected-border',
|
||||
)} onClick={() => setType('secret')}>
|
||||
<span>Secret</span>
|
||||
<TooltipPlus popupContent={
|
||||
<div className='w-[240px]'>
|
||||
{t('workflow.env.modal.secretTip')}
|
||||
</div>
|
||||
}>
|
||||
<RiQuestionLine className='ml-0.5 w-[14px] h-[14px] text-text-quaternary' />
|
||||
</TooltipPlus>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* name */}
|
||||
<div className='mb-4'>
|
||||
<div className='mb-1 text-text-secondary system-sm-semibold'>{t('workflow.env.modal.name')}</div>
|
||||
<div className='flex'>
|
||||
<input
|
||||
tabIndex={0}
|
||||
className='block px-3 w-full h-9 bg-components-input-bg-normal system-sm-regular radius-md border border-transparent appearance-none outline-none caret-primary-600 hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:bg-components-input-bg-active focus:border-components-input-border-active focus:shadow-xs placeholder:system-sm-regular placeholder:text-components-input-text-placeholder'
|
||||
placeholder={t('workflow.env.modal.namePlaceholder') || ''}
|
||||
value={name}
|
||||
onChange={e => handleNameChange(e.target.value)}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* value */}
|
||||
<div className=''>
|
||||
<div className='mb-1 text-text-secondary system-sm-semibold'>{t('workflow.env.modal.value')}</div>
|
||||
<div className='flex'>
|
||||
<input
|
||||
tabIndex={0}
|
||||
className='block px-3 w-full h-9 bg-components-input-bg-normal system-sm-regular radius-md border border-transparent appearance-none outline-none caret-primary-600 hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:bg-components-input-bg-active focus:border-components-input-border-active focus:shadow-xs placeholder:system-sm-regular placeholder:text-components-input-text-placeholder'
|
||||
placeholder={t('workflow.env.modal.valuePlaceholder') || ''}
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
type={type !== 'number' ? 'text' : 'number'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='p-4 pt-2 flex flex-row-reverse rounded-b-2xl'>
|
||||
<div className='flex gap-2'>
|
||||
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
<Button variant='primary' onClick={handleSave}>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VariableModal
|
||||
@ -0,0 +1,68 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import VariableModal from '@/app/components/workflow/panel/env-panel/variable-modal'
|
||||
// import cn from '@/utils/classnames'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
setOpen: (value: React.SetStateAction<boolean>) => void
|
||||
env?: EnvironmentVariable
|
||||
onClose: () => void
|
||||
onSave: (env: EnvironmentVariable) => void
|
||||
}
|
||||
|
||||
const VariableTrigger = ({
|
||||
open,
|
||||
setOpen,
|
||||
env,
|
||||
onClose,
|
||||
onSave,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={() => {
|
||||
setOpen(v => !v)
|
||||
open && onClose()
|
||||
}}
|
||||
placement='left-start'
|
||||
offset={{
|
||||
mainAxis: 8,
|
||||
alignmentAxis: -104,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => {
|
||||
setOpen(v => !v)
|
||||
open && onClose()
|
||||
}}>
|
||||
<Button variant='primary'>
|
||||
<RiAddLine className='mr-1 w-4 h-4' />
|
||||
<span className='system-sm-medium'>{t('workflow.env.envPanelButton')}</span>
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[11]'>
|
||||
<VariableModal
|
||||
env={env}
|
||||
onSave={onSave}
|
||||
onClose={() => {
|
||||
onClose()
|
||||
setOpen(false)
|
||||
}}
|
||||
/>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default VariableTrigger
|
||||
@ -13,6 +13,7 @@ import DebugAndPreview from './debug-and-preview'
|
||||
import Record from './record'
|
||||
import WorkflowPreview from './workflow-preview'
|
||||
import ChatRecord from './chat-record'
|
||||
import EnvPanel from './env-panel'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import MessageLogModal from '@/app/components/base/message-log-modal'
|
||||
@ -23,6 +24,7 @@ const Panel: FC = () => {
|
||||
const selectedNode = nodes.find(node => node.data.selected)
|
||||
const historyWorkflowData = useStore(s => s.historyWorkflowData)
|
||||
const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel)
|
||||
const showEnvPanel = useStore(s => s.showEnvPanel)
|
||||
const isRestoring = useStore(s => s.isRestoring)
|
||||
const {
|
||||
enableShortcuts,
|
||||
@ -39,9 +41,7 @@ const Panel: FC = () => {
|
||||
return (
|
||||
<div
|
||||
tabIndex={-1}
|
||||
className={cn(
|
||||
'absolute top-14 right-0 bottom-2 flex z-10 outline-none',
|
||||
)}
|
||||
className={cn('absolute top-14 right-0 bottom-2 flex z-10 outline-none')}
|
||||
onFocus={disableShortcuts}
|
||||
onBlur={enableShortcuts}
|
||||
key={`${isRestoring}`}
|
||||
@ -85,6 +85,11 @@ const Panel: FC = () => {
|
||||
<WorkflowPreview />
|
||||
)
|
||||
}
|
||||
{
|
||||
showEnvPanel && (
|
||||
<EnvPanel />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -30,11 +30,7 @@ import cn from '@/utils/classnames'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
const WorkflowPreview = ({
|
||||
onShowIterationDetail,
|
||||
}: {
|
||||
onShowIterationDetail: (detail: NodeTracing[][]) => void
|
||||
}) => {
|
||||
const WorkflowPreview = () => {
|
||||
const { t } = useTranslation()
|
||||
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
|
||||
const workflowRunningData = useStore(s => s.workflowRunningData)
|
||||
|
||||
@ -12,6 +12,7 @@ import type {
|
||||
import type { VariableAssignerNodeType } from './nodes/variable-assigner/types'
|
||||
import type {
|
||||
Edge,
|
||||
EnvironmentVariable,
|
||||
HistoryWorkflowData,
|
||||
Node,
|
||||
RunFile,
|
||||
@ -59,6 +60,7 @@ type Shape = {
|
||||
edges: Edge[]
|
||||
viewport: Viewport
|
||||
features: Record<string, any>
|
||||
environmentVariables: EnvironmentVariable[]
|
||||
}
|
||||
setBackupDraft: (backupDraft?: Shape['backupDraft']) => void
|
||||
notInitialWorkflow: boolean
|
||||
@ -82,6 +84,12 @@ type Shape = {
|
||||
setShortcutsDisabled: (shortcutsDisabled: boolean) => void
|
||||
showDebugAndPreviewPanel: boolean
|
||||
setShowDebugAndPreviewPanel: (showDebugAndPreviewPanel: boolean) => void
|
||||
showEnvPanel: boolean
|
||||
setShowEnvPanel: (showEnvPanel: boolean) => void
|
||||
environmentVariables: EnvironmentVariable[]
|
||||
setEnvironmentVariables: (environmentVariables: EnvironmentVariable[]) => void
|
||||
envSecrets: Record<string, string>
|
||||
setEnvSecrets: (envSecrets: Record<string, string>) => void
|
||||
selection: null | { x1: number; y1: number; x2: number; y2: number }
|
||||
setSelection: (selection: Shape['selection']) => void
|
||||
bundleNodeSize: { width: number; height: number } | null
|
||||
@ -190,6 +198,12 @@ export const createWorkflowStore = () => {
|
||||
setShortcutsDisabled: shortcutsDisabled => set(() => ({ shortcutsDisabled })),
|
||||
showDebugAndPreviewPanel: false,
|
||||
setShowDebugAndPreviewPanel: showDebugAndPreviewPanel => set(() => ({ showDebugAndPreviewPanel })),
|
||||
showEnvPanel: false,
|
||||
setShowEnvPanel: showEnvPanel => set(() => ({ showEnvPanel })),
|
||||
environmentVariables: [],
|
||||
setEnvironmentVariables: environmentVariables => set(() => ({ environmentVariables })),
|
||||
envSecrets: {},
|
||||
setEnvSecrets: envSecrets => set(() => ({ envSecrets })),
|
||||
selection: null,
|
||||
setSelection: selection => set(() => ({ selection })),
|
||||
bundleNodeSize: null,
|
||||
|
||||
@ -102,6 +102,13 @@ export type Variable = {
|
||||
isParagraph?: boolean
|
||||
}
|
||||
|
||||
export type EnvironmentVariable = {
|
||||
id: string
|
||||
name: string
|
||||
value: any
|
||||
value_type: 'string' | 'number' | 'secret'
|
||||
}
|
||||
|
||||
export type VariableWithValue = {
|
||||
key: string
|
||||
value: string
|
||||
@ -183,6 +190,7 @@ export type Memory = {
|
||||
export enum VarType {
|
||||
string = 'string',
|
||||
number = 'number',
|
||||
secret = 'secret',
|
||||
boolean = 'boolean',
|
||||
object = 'object',
|
||||
array = 'array',
|
||||
|
||||
Reference in New Issue
Block a user