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 { useStoreApi, } from 'reactflow' import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager' 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 { updateEnvironmentVariables } from '@/service/workflow' 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 stringifySecretValue = (value: unknown) => { return typeof value === 'string' ? value : JSON.stringify(value) } const sanitizeSecretValue = (env: EnvironmentVariable) => { return env.value_type === 'secret' ? { ...env, value: HIDDEN_SECRET_VALUE } : env } const broadcastEnvUpdate = (appId: string) => { const socket = webSocketClient.getSocket(appId) if (!socket?.connected) return socket.emit('collaboration_event', { type: 'vars_and_features_update', timestamp: Date.now(), }) } const useEnvPanelActions = ({ appId, store, envSecrets, updateEnvList, setEnvSecrets, }: { appId: string store: ReturnType envSecrets: Record updateEnvList: (envList: EnvironmentVariable[]) => void setEnvSecrets: (envSecrets: Record) => void }) => { const getAffectedNodes = useCallback((env: EnvironmentVariable) => { const allNodes = store.getState().getNodes() return findUsedVarNodes( ['env', env.name], allNodes, ) }, [store]) const updateAffectedNodes = useCallback((currentEnv: EnvironmentVariable, nextSelector: string[]) => { const { getNodes, setNodes } = store.getState() const affectedNodes = getAffectedNodes(currentEnv) const nextNodes = getNodes().map((node) => { if (affectedNodes.find(affectedNode => affectedNode.id === node.id)) return updateNodeVars(node, ['env', currentEnv.name], nextSelector) return node }) setNodes(nextNodes) }, [getAffectedNodes, store]) const syncEnvList = useCallback(async ( nextEnvList: EnvironmentVariable[], rollbackEnvList: EnvironmentVariable[], ) => { updateEnvList(nextEnvList) try { await updateEnvironmentVariables({ appId, environmentVariables: nextEnvList, }) broadcastEnvUpdate(appId) updateEnvList(nextEnvList.map(sanitizeSecretValue)) return true } catch (error) { console.error('Failed to update environment variables:', error) updateEnvList(rollbackEnvList) return false } }, [appId, updateEnvList]) const saveSecretValue = useCallback((env: EnvironmentVariable) => { setEnvSecrets({ ...envSecrets, [env.id]: formatSecret(stringifySecretValue(env.value)), }) }, [envSecrets, setEnvSecrets]) const removeEnvSecret = useCallback((envId: string) => { const nextSecrets = { ...envSecrets } delete nextSecrets[envId] setEnvSecrets(nextSecrets) }, [envSecrets, setEnvSecrets]) return { getAffectedNodes, updateAffectedNodes, syncEnvList, saveSecretValue, removeEnvSecret, } } 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 appId = useStore(s => s.appId) as string const { getAffectedNodes, updateAffectedNodes, syncEnvList, saveSecretValue, removeEnvSecret, } = useEnvPanelActions({ appId, store, envSecrets, updateEnvList, setEnvSecrets, }) 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) => { updateAffectedNodes(env, []) const nextEnvList = envList.filter(e => e.id !== env.id) setCacheForDelete(undefined) setShowRemoveVarConfirm(false) const synced = await syncEnvList(nextEnvList, envList) if (synced && env.value_type === 'secret') removeEnvSecret(env.id) }, [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], 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 newList = envList.map(e => e.id === currentVar.id ? newEnv : e) if (currentVar.name !== env.name) updateAffectedNodes(currentVar, ['env', env.name]) const synced = await syncEnvList(newList, envList) if (!synced && currentVar.value_type === 'secret' && env.value_type !== 'secret') saveSecretValue(currentVar) }, [currentVar, envList, envSecrets, removeEnvSecret, saveSecretValue, syncEnvList, updateAffectedNodes]) const handleVariableModalClose = () => { setCurrentVar(undefined) } return (
{t('env.envPanelTitle', { ns: 'workflow' })}
setShowEnvPanel(false)} >
{t('env.envDescription', { ns: 'workflow' })}
{envList.map(env => ( ))}
setShowRemoveVarConfirm(false)} onConfirm={() => cacheForDelete && handleDelete(cacheForDelete)} />
) } export default memo(EnvPanel)