import type { EnvironmentVariable, } from '@/app/components/workflow/types' import { RiCloseLine } from '@remixicon/react' import { memo, useCallback, useState, } from 'react' import { useTranslation } from 'react-i18next' import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow' import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft' import RemoveEffectVarConfirm from '@/app/components/workflow/nodes/_base/components/remove-effect-var-confirm' import { findUsedVarNodes, updateNodeVars } from '@/app/components/workflow/nodes/_base/components/variable/utils' import EnvItem from '@/app/components/workflow/panel/env-panel/env-item' import VariableTrigger from '@/app/components/workflow/panel/env-panel/variable-trigger' import { useStore } from '@/app/components/workflow/store' import { cn } from '@/utils/classnames' const HIDDEN_SECRET_VALUE = '[__HIDDEN__]' const formatSecret = (secret: string) => { return secret.length > 8 ? `${secret.slice(0, 6)}************${secret.slice(-2)}` : '********************' } const sanitizeSecretValue = (env: EnvironmentVariable) => { return env.value_type === 'secret' ? { ...env, value: HIDDEN_SECRET_VALUE } : env } const removeSecretFromMap = (secretMap: Record, envId: string) => { const nextSecretMap = { ...secretMap } delete nextSecretMap[envId] return nextSecretMap } const useEnvPanelActions = ({ collaborativeWorkflow, appId, envSecrets, updateEnvList, setEnvSecrets, setControlPromptEditorRerenderKey, doSyncWorkflowDraft, }: { collaborativeWorkflow: ReturnType appId: string envSecrets: Record updateEnvList: (envList: EnvironmentVariable[]) => void setEnvSecrets: (envSecrets: Record) => void setControlPromptEditorRerenderKey: (controlPromptEditorRerenderKey: number) => void doSyncWorkflowDraft: () => Promise }) => { const emitVarsAndFeaturesUpdate = useCallback(async () => { try { const { webSocketClient } = await import('@/app/components/workflow/collaboration/core/websocket-manager') const socket = webSocketClient.getSocket(appId) if (socket) { socket.emit('collaboration_event', { type: 'vars_and_features_update', }) } } catch (error) { console.error('Failed to emit vars_and_features_update event:', error) } }, [appId]) const persistEnvironmentVariables = useCallback(async (nextEnvList: EnvironmentVariable[]) => { try { const { updateEnvironmentVariables } = await import('@/service/workflow') await updateEnvironmentVariables({ appId, environmentVariables: nextEnvList, }) await emitVarsAndFeaturesUpdate() return true } catch (error) { console.error('Failed to update environment variables:', error) return false } }, [appId, emitVarsAndFeaturesUpdate]) const getAffectedNodes = useCallback((env: EnvironmentVariable) => { const { nodes: allNodes } = collaborativeWorkflow.getState() return findUsedVarNodes( ['env', env.name], allNodes, ) }, [collaborativeWorkflow]) const updateAffectedNodes = useCallback((currentEnv: EnvironmentVariable, nextSelector: string[]) => { const { nodes, setNodes } = collaborativeWorkflow.getState() const affectedNodes = getAffectedNodes(currentEnv) const nextNodes = nodes.map((node) => { if (affectedNodes.find(affectedNode => affectedNode.id === node.id)) return updateNodeVars(node, ['env', currentEnv.name], nextSelector) return node }) setNodes(nextNodes) setControlPromptEditorRerenderKey(Date.now()) }, [collaborativeWorkflow, getAffectedNodes, setControlPromptEditorRerenderKey]) const syncEnvList = useCallback(async ( nextEnvList: EnvironmentVariable[], options?: { syncDraft?: boolean }, ) => { updateEnvList(nextEnvList) const shouldSyncDraft = options?.syncDraft ?? true let persisted = false if (shouldSyncDraft) { const syncDraftPromise = doSyncWorkflowDraft() persisted = await persistEnvironmentVariables(nextEnvList) await syncDraftPromise } else { persisted = await persistEnvironmentVariables(nextEnvList) } updateEnvList(nextEnvList.map(sanitizeSecretValue)) return persisted }, [doSyncWorkflowDraft, persistEnvironmentVariables, updateEnvList]) const saveSecretValue = useCallback((env: EnvironmentVariable) => { setEnvSecrets({ ...envSecrets, [env.id]: formatSecret(String(env.value)), }) }, [envSecrets, setEnvSecrets]) const removeEnvSecret = useCallback((envId: string) => { setEnvSecrets(removeSecretFromMap(envSecrets, envId)) }, [envSecrets, setEnvSecrets]) return { getAffectedNodes, updateAffectedNodes, syncEnvList, saveSecretValue, removeEnvSecret, } } const EnvPanel = () => { const { t } = useTranslation() const collaborativeWorkflow = useCollaborativeWorkflow() 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 setControlPromptEditorRerenderKey = useStore(s => s.setControlPromptEditorRerenderKey) const appId = useStore(s => s.appId) as string const { doSyncWorkflowDraft } = useNodesSyncDraft() const { getAffectedNodes, updateAffectedNodes, syncEnvList, saveSecretValue, removeEnvSecret, } = useEnvPanelActions({ collaborativeWorkflow, appId, envSecrets, updateEnvList, setEnvSecrets, setControlPromptEditorRerenderKey, doSyncWorkflowDraft, }) const [showVariableModal, setShowVariableModal] = useState(false) const [currentVar, setCurrentVar] = useState() const [showRemoveVarConfirm, setShowRemoveVarConfirm] = useState(false) const [cacheForDelete, setCacheForDelete] = useState() const handleEdit = (env: EnvironmentVariable) => { setCurrentVar(env) setShowVariableModal(true) } const handleDelete = useCallback(async (env: EnvironmentVariable) => { const nextEnvList = envList.filter(e => e.id !== env.id) setCacheForDelete(undefined) setShowRemoveVarConfirm(false) updateAffectedNodes(env, []) if (env.value_type === 'secret') removeEnvSecret(env.id) await syncEnvList(nextEnvList) }, [envList, removeEnvSecret, syncEnvList, updateAffectedNodes]) const deleteCheck = useCallback((env: EnvironmentVariable) => { const affectedNodes = getAffectedNodes(env) if (affectedNodes.length > 0) { setCacheForDelete(env) setShowRemoveVarConfirm(true) } else { handleDelete(env) } }, [getAffectedNodes, handleDelete]) const handleSave = useCallback(async (env: EnvironmentVariable) => { let newEnv = env if (!currentVar) { if (env.value_type === 'secret') saveSecretValue(env) await syncEnvList([env, ...envList]) return } if (currentVar.value_type === 'secret') { if (env.value_type === 'secret') { if (envSecrets[currentVar.id] !== env.value) { newEnv = env saveSecretValue(env) } else { newEnv = sanitizeSecretValue(env) } } else { removeEnvSecret(currentVar.id) } } else if (env.value_type === 'secret') { saveSecretValue(env) } const nextEnvList = envList.map(e => e.id === currentVar.id ? newEnv : e) const hasEnvNameChanged = currentVar.name !== env.name if (hasEnvNameChanged) updateAffectedNodes(currentVar, ['env', env.name]) const persisted = await syncEnvList(nextEnvList, { syncDraft: hasEnvNameChanged }) if (!persisted && !hasEnvNameChanged) await doSyncWorkflowDraft() }, [currentVar, doSyncWorkflowDraft, envList, envSecrets, removeEnvSecret, saveSecretValue, syncEnvList, updateAffectedNodes]) const handleVariableModalClose = () => { setCurrentVar(undefined) } return (
{t('env.envPanelTitle', { ns: 'workflow' })}
setShowEnvPanel(false)} > {/* eslint-disable-next-line hyoban/prefer-tailwind-icons */}
{t('env.envDescription', { ns: 'workflow' })}
{envList.map(env => ( ))}
setShowRemoveVarConfirm(false)} onConfirm={() => cacheForDelete && handleDelete(cacheForDelete)} />
) } export default memo(EnvPanel)