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

@@ -28,6 +28,9 @@ import type { CreateAppModalProps } from '@/app/components/explore/create-app-mo
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { getRedirection } from '@/utils/app-redirection'
import UpdateDSLModal from '@/app/components/workflow/update-dsl-modal'
import type { EnvironmentVariable } from '@/app/components/workflow/types'
import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal'
import { fetchWorkflowDraft } from '@/service/workflow'
export type IAppInfoProps = {
expand: boolean
@@ -47,6 +50,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
const [showSwitchTip, setShowSwitchTip] = useState<string>('')
const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
const [showImportDSLModal, setShowImportDSLModal] = useState<boolean>(false)
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
const mutateApps = useContextSelector(
AppsContext,
@@ -108,11 +112,14 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
}
}
const onExport = async () => {
const onExport = async (include = false) => {
if (!appDetail)
return
try {
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)
@@ -124,6 +131,27 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
}
}
const exportCheck = async () => {
if (!appDetail)
return
if (appDetail.mode !== 'workflow' && appDetail.mode !== 'advanced-chat') {
onExport()
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) {
onExport()
return
}
setSecretEnvList(list)
}
catch (e) {
notify({ type: 'error', message: t('app.exportFailed') })
}
}
const onConfirmDelete = useCallback(async () => {
if (!appDetail)
return
@@ -314,7 +342,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
</>
)}
<Divider className="!my-1" />
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={onExport}>
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={exportCheck}>
<span className='text-gray-700 text-sm leading-5'>{t('app.export')}</span>
</div>
{
@@ -403,14 +431,19 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
onCancel={() => setShowConfirmDelete(false)}
/>
)}
{
showImportDSLModal && (
<UpdateDSLModal
onCancel={() => setShowImportDSLModal(false)}
onBackup={onExport}
/>
)
}
{showImportDSLModal && (
<UpdateDSLModal
onCancel={() => setShowImportDSLModal(false)}
onBackup={onExport}
/>
)}
{secretEnvList.length > 0 && (
<DSLExportConfirmModal
envList={secretEnvList}
onConfirm={onExport}
onClose={() => setSecretEnvList([])}
/>
)}
</div>
</PortalToFollowElem>
)

View File

@@ -119,11 +119,11 @@ const AppPublisher = ({
<PortalToFollowElemTrigger onClick={handleTrigger}>
<Button
variant='primary'
className='pl-3 pr-1'
className='pl-3 pr-2'
disabled={disabled}
>
{t('workflow.common.publish')}
<RiArrowDownSLine className='ml-0.5' />
<RiArrowDownSLine className='w-4 h-4 ml-0.5' />
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[11]'>

View File

@@ -1,5 +0,0 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Left Icon">
<path id="Vector" d="M7.83333 2.66683H5.7C4.5799 2.66683 4.01984 2.66683 3.59202 2.88482C3.21569 3.07656 2.90973 3.38252 2.71799 3.75885C2.5 4.18667 2.5 4.74672 2.5 5.86683V9.3335C2.5 9.95348 2.5 10.2635 2.56815 10.5178C2.75308 11.208 3.29218 11.7471 3.98236 11.932C4.2367 12.0002 4.54669 12.0002 5.16667 12.0002V13.5572C5.16667 13.9124 5.16667 14.09 5.23949 14.1812C5.30282 14.2606 5.39885 14.3067 5.50036 14.3066C5.61708 14.3065 5.75578 14.1955 6.03317 13.9736L7.62348 12.7014C7.94834 12.4415 8.11078 12.3115 8.29166 12.2191C8.45213 12.1371 8.62295 12.0772 8.79948 12.041C8.99845 12.0002 9.20646 12.0002 9.6225 12.0002H10.6333C11.7534 12.0002 12.3135 12.0002 12.7413 11.7822C13.1176 11.5904 13.4236 11.2845 13.6153 10.9081C13.8333 10.4803 13.8333 9.92027 13.8333 8.80016V8.66683M11.6551 6.472L14.8021 4.44889C15.0344 4.29958 15.1505 4.22493 15.1906 4.13C15.2257 4.04706 15.2257 3.95347 15.1906 3.87052C15.1505 3.7756 15.0344 3.70094 14.8021 3.55163L11.6551 1.52852C11.3874 1.35646 11.2536 1.27043 11.1429 1.27833C11.0465 1.28522 10.9578 1.33365 10.8998 1.41105C10.8333 1.49987 10.8333 1.65896 10.8333 1.97715V6.02337C10.8333 6.34156 10.8333 6.50066 10.8998 6.58948C10.9578 6.66688 11.0465 6.71531 11.1429 6.72219C11.2536 6.7301 11.3874 6.64407 11.6551 6.472Z" stroke="#155EEF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="env">
<g id="Vector">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.33325 3.33325C1.33325 2.22868 2.22868 1.33325 3.33325 1.33325H12.6666C13.7712 1.33325 14.6666 2.22869 14.6666 3.33325V3.66659C14.6666 4.03478 14.3681 4.33325 13.9999 4.33325C13.6317 4.33325 13.3333 4.03478 13.3333 3.66659V3.33325C13.3333 2.96506 13.0348 2.66659 12.6666 2.66659H3.33325C2.96506 2.66659 2.66659 2.96506 2.66659 3.33325V3.66659C2.66659 4.03478 2.36811 4.33325 1.99992 4.33325C1.63173 4.33325 1.33325 4.03478 1.33325 3.66659V3.33325Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6666 12.6666C14.6666 13.7712 13.7712 14.6666 12.6666 14.6666L3.33325 14.6666C2.22866 14.6666 1.33325 13.7711 1.33325 12.6666L1.33325 12.3333C1.33325 11.9651 1.63173 11.6666 1.99992 11.6666C2.36811 11.6666 2.66659 11.9651 2.66659 12.3333V12.6666C2.66659 13.0348 2.96505 13.3333 3.33325 13.3333L12.6666 13.3333C13.0348 13.3333 13.3333 13.0348 13.3333 12.6666V12.3333C13.3333 11.9651 13.6317 11.6666 13.9999 11.6666C14.3681 11.6666 14.6666 11.9651 14.6666 12.3333V12.6666Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.33325 5.99992C1.33325 5.63173 1.63173 5.33325 1.99992 5.33325H4.33325C4.70144 5.33325 4.99992 5.63173 4.99992 5.99992C4.99992 6.36811 4.70144 6.66658 4.33325 6.66658H2.66659V7.33325H3.99992C4.36811 7.33325 4.66659 7.63173 4.66659 7.99992C4.66659 8.36811 4.36811 8.66658 3.99992 8.66658H2.66659V9.33325H4.33325C4.70144 9.33325 4.99992 9.63173 4.99992 9.99992C4.99992 10.3681 4.70144 10.6666 4.33325 10.6666H1.99992C1.63173 10.6666 1.33325 10.3681 1.33325 9.99992V5.99992Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.4734 5.36186C6.75457 5.27673 7.05833 5.38568 7.22129 5.63012L8.66659 7.79807V5.99992C8.66659 5.63173 8.96506 5.33325 9.33325 5.33325C9.70144 5.33325 9.99992 5.63173 9.99992 5.99992V9.99992C9.99992 10.2937 9.80761 10.5528 9.52644 10.638C9.24527 10.7231 8.94151 10.6142 8.77855 10.3697L7.33325 8.20177V9.99992C7.33325 10.3681 7.03478 10.6666 6.66659 10.6666C6.2984 10.6666 5.99992 10.3681 5.99992 9.99992V5.99992C5.99992 5.70614 6.19222 5.44699 6.4734 5.36186Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0768 5.38453C11.4167 5.24292 11.807 5.40364 11.9486 5.74351L12.9999 8.26658L14.0512 5.74351C14.1928 5.40364 14.5831 5.24292 14.923 5.38453C15.2629 5.52614 15.4236 5.91646 15.282 6.25633L13.6153 10.2563C13.5118 10.5048 13.2691 10.6666 12.9999 10.6666C12.7308 10.6666 12.488 10.5048 12.3845 10.2563L10.7179 6.25633C10.5763 5.91646 10.737 5.52614 11.0768 5.38453Z" fill="black"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,39 +0,0 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "17",
"height": "16",
"viewBox": "0 0 17 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Left Icon"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector",
"d": "M7.83333 2.66683H5.7C4.5799 2.66683 4.01984 2.66683 3.59202 2.88482C3.21569 3.07656 2.90973 3.38252 2.71799 3.75885C2.5 4.18667 2.5 4.74672 2.5 5.86683V9.3335C2.5 9.95348 2.5 10.2635 2.56815 10.5178C2.75308 11.208 3.29218 11.7471 3.98236 11.932C4.2367 12.0002 4.54669 12.0002 5.16667 12.0002V13.5572C5.16667 13.9124 5.16667 14.09 5.23949 14.1812C5.30282 14.2606 5.39885 14.3067 5.50036 14.3066C5.61708 14.3065 5.75578 14.1955 6.03317 13.9736L7.62348 12.7014C7.94834 12.4415 8.11078 12.3115 8.29166 12.2191C8.45213 12.1371 8.62295 12.0772 8.79948 12.041C8.99845 12.0002 9.20646 12.0002 9.6225 12.0002H10.6333C11.7534 12.0002 12.3135 12.0002 12.7413 11.7822C13.1176 11.5904 13.4236 11.2845 13.6153 10.9081C13.8333 10.4803 13.8333 9.92027 13.8333 8.80016V8.66683M11.6551 6.472L14.8021 4.44889C15.0344 4.29958 15.1505 4.22493 15.1906 4.13C15.2257 4.04706 15.2257 3.95347 15.1906 3.87052C15.1505 3.7756 15.0344 3.70094 14.8021 3.55163L11.6551 1.52852C11.3874 1.35646 11.2536 1.27043 11.1429 1.27833C11.0465 1.28522 10.9578 1.33365 10.8998 1.41105C10.8333 1.49987 10.8333 1.65896 10.8333 1.97715V6.02337C10.8333 6.34156 10.8333 6.50066 10.8998 6.58948C10.9578 6.66688 11.0465 6.71531 11.1429 6.72219C11.2536 6.7301 11.3874 6.64407 11.6551 6.472Z",
"stroke": "currentColor",
"stroke-width": "1.5",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
}
]
},
"name": "MessagePlay"
}

View File

@@ -4,4 +4,3 @@ export { default as ChatBot } from './ChatBot'
export { default as CuteRobot } from './CuteRobot'
export { default as MessageCheckRemove } from './MessageCheckRemove'
export { default as MessageFastPlus } from './MessageFastPlus'
export { default as MessagePlay } from './MessagePlay'

View File

@@ -0,0 +1,90 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "env"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Vector"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M1.33325 3.33325C1.33325 2.22868 2.22868 1.33325 3.33325 1.33325H12.6666C13.7712 1.33325 14.6666 2.22869 14.6666 3.33325V3.66659C14.6666 4.03478 14.3681 4.33325 13.9999 4.33325C13.6317 4.33325 13.3333 4.03478 13.3333 3.66659V3.33325C13.3333 2.96506 13.0348 2.66659 12.6666 2.66659H3.33325C2.96506 2.66659 2.66659 2.96506 2.66659 3.33325V3.66659C2.66659 4.03478 2.36811 4.33325 1.99992 4.33325C1.63173 4.33325 1.33325 4.03478 1.33325 3.66659V3.33325Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M14.6666 12.6666C14.6666 13.7712 13.7712 14.6666 12.6666 14.6666L3.33325 14.6666C2.22866 14.6666 1.33325 13.7711 1.33325 12.6666L1.33325 12.3333C1.33325 11.9651 1.63173 11.6666 1.99992 11.6666C2.36811 11.6666 2.66659 11.9651 2.66659 12.3333V12.6666C2.66659 13.0348 2.96505 13.3333 3.33325 13.3333L12.6666 13.3333C13.0348 13.3333 13.3333 13.0348 13.3333 12.6666V12.3333C13.3333 11.9651 13.6317 11.6666 13.9999 11.6666C14.3681 11.6666 14.6666 11.9651 14.6666 12.3333V12.6666Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M1.33325 5.99992C1.33325 5.63173 1.63173 5.33325 1.99992 5.33325H4.33325C4.70144 5.33325 4.99992 5.63173 4.99992 5.99992C4.99992 6.36811 4.70144 6.66658 4.33325 6.66658H2.66659V7.33325H3.99992C4.36811 7.33325 4.66659 7.63173 4.66659 7.99992C4.66659 8.36811 4.36811 8.66658 3.99992 8.66658H2.66659V9.33325H4.33325C4.70144 9.33325 4.99992 9.63173 4.99992 9.99992C4.99992 10.3681 4.70144 10.6666 4.33325 10.6666H1.99992C1.63173 10.6666 1.33325 10.3681 1.33325 9.99992V5.99992Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M6.4734 5.36186C6.75457 5.27673 7.05833 5.38568 7.22129 5.63012L8.66659 7.79807V5.99992C8.66659 5.63173 8.96506 5.33325 9.33325 5.33325C9.70144 5.33325 9.99992 5.63173 9.99992 5.99992V9.99992C9.99992 10.2937 9.80761 10.5528 9.52644 10.638C9.24527 10.7231 8.94151 10.6142 8.77855 10.3697L7.33325 8.20177V9.99992C7.33325 10.3681 7.03478 10.6666 6.66659 10.6666C6.2984 10.6666 5.99992 10.3681 5.99992 9.99992V5.99992C5.99992 5.70614 6.19222 5.44699 6.4734 5.36186Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M11.0768 5.38453C11.4167 5.24292 11.807 5.40364 11.9486 5.74351L12.9999 8.26658L14.0512 5.74351C14.1928 5.40364 14.5831 5.24292 14.923 5.38453C15.2629 5.52614 15.4236 5.91646 15.282 6.25633L13.6153 10.2563C13.5118 10.5048 13.2691 10.6666 12.9999 10.6666C12.7308 10.6666 12.488 10.5048 12.3845 10.2563L10.7179 6.25633C10.5763 5.91646 10.737 5.52614 11.0768 5.38453Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
}
]
},
"name": "Env"
}

View File

@@ -2,7 +2,7 @@
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './MessagePlay.json'
import data from './Env.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
@@ -11,6 +11,6 @@ const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseP
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'MessagePlay'
Icon.displayName = 'Env'
export default Icon

View File

@@ -1,6 +1,7 @@
export { default as Apps02 } from './Apps02'
export { default as Colors } from './Colors'
export { default as DragHandle } from './DragHandle'
export { default as Env } from './Env'
export { default as Exchange02 } from './Exchange02'
export { default as FileCode } from './FileCode'
export { default as Icon3Dots } from './Icon3Dots'

View File

@@ -21,9 +21,10 @@ import {
} from './index'
import cn from '@/utils/classnames'
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
import { Env } from '@/app/components/base/icons/src/vender/line/others'
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
import { Line3 } from '@/app/components/base/icons/src/public/common'
import { isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import { isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
import TooltipPlus from '@/app/components/base/tooltip-plus'
type WorkflowVariableBlockComponentProps = {
@@ -50,6 +51,7 @@ const WorkflowVariableBlockComponent = ({
)()
const [localWorkflowNodesMap, setLocalWorkflowNodesMap] = useState<WorkflowNodesMap>(workflowNodesMap)
const node = localWorkflowNodesMap![variables[0]]
const isEnv = isENV(variables)
useEffect(() => {
if (!editor.hasNodes([WorkflowVariableBlockNode]))
@@ -73,30 +75,33 @@ const WorkflowVariableBlockComponent = ({
className={cn(
'mx-0.5 relative group/wrap flex items-center h-[18px] pl-0.5 pr-[3px] rounded-[5px] border select-none',
isSelected ? ' border-[#84ADFF] bg-[#F5F8FF]' : ' border-black/5 bg-white',
!node && '!border-[#F04438] !bg-[#FEF3F2]',
!node && !isEnv && '!border-[#F04438] !bg-[#FEF3F2]',
)}
ref={ref}
>
<div className='flex items-center'>
{
node?.type && (
<div className='p-[1px]'>
<VarBlockIcon
className='!text-gray-500'
type={node?.type}
/>
</div>
)
}
<div className='shrink-0 mx-0.5 max-w-[60px] text-xs font-medium text-gray-500 truncate' title={node?.title} style={{
}}>{node?.title}</div>
<Line3 className='mr-0.5 text-gray-300'></Line3>
</div>
{!isEnv && (
<div className='flex items-center'>
{
node?.type && (
<div className='p-[1px]'>
<VarBlockIcon
className='!text-gray-500'
type={node?.type}
/>
</div>
)
}
<div className='shrink-0 mx-0.5 max-w-[60px] text-xs font-medium text-gray-500 truncate' title={node?.title} style={{
}}>{node?.title}</div>
<Line3 className='mr-0.5 text-gray-300'></Line3>
</div>
)}
<div className='flex items-center text-primary-600'>
<Variable02 className='w-3.5 h-3.5' />
<div className='shrink-0 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('shrink-0 ml-0.5 text-xs font-medium truncate', isEnv && 'text-gray-900')} title={varName}>{varName}</div>
{
!node && (
!node && !isEnv && (
<RiErrorWarningFill className='ml-0.5 w-3 h-3 text-[#D92D20]' />
)
}
@@ -104,7 +109,7 @@ const WorkflowVariableBlockComponent = ({
</div>
)
if (!node) {
if (!node && !isEnv) {
return (
<TooltipPlus popupContent={t('workflow.errorMsg.invalidVariable')}>
{Item}

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