Feat/environment variables in workflow (#6515)

Co-authored-by: JzoNg <jzongcode@gmail.com>
This commit is contained in:
-LAN-
2024-07-22 15:29:39 +08:00
committed by GitHub
parent 87d583f454
commit 5e6fc58db3
146 changed files with 2486 additions and 746 deletions

View File

@ -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'

View 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

View File

@ -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>
{

View 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)

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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)

View File

@ -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'

View File

@ -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,
},
}

View File

@ -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,
}
}

View File

@ -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])

View File

@ -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

View 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,
}
}

View File

@ -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)

View File

@ -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}

View File

@ -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) {

View File

@ -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>)

View File

@ -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>

View File

@ -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: {

View File

@ -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>

View File

@ -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,

View File

@ -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,

View File

@ -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 {

View File

@ -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

View File

@ -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'>

View File

@ -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)
},
})

View File

@ -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>
</>
)}

View File

@ -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)
},
})

View File

@ -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)
},
})

View File

@ -125,6 +125,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
</div>
{(isShowAuthorization && !readOnly) && (
<AuthorizationModal
nodeId={id}
isShow
onHide={hideAuthorization}
payload={inputs.authorization}

View File

@ -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

View File

@ -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',

View File

@ -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 {

View File

@ -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) => {

View File

@ -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}

View File

@ -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>
)

View File

@ -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
}

View File

@ -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>

View File

@ -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>

View 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)

View 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

View File

@ -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

View File

@ -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>
)
}

View File

@ -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)

View File

@ -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,

View File

@ -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',