feat(env-panel): add description field to environment variables
Add description field to environment variable modal and display it in the env item component. This allows users to provide additional context for each environment variable.pull/21556/head
parent
7b7a506abd
commit
7c097fe6eb
File diff suppressed because it is too large
Load Diff
@ -1,53 +1,56 @@
|
|||||||
import { memo, useState } from 'react'
|
import { memo, useState } from 'react'
|
||||||
import { capitalize } from 'lodash-es'
|
import { capitalize } from 'lodash-es'
|
||||||
import { RiDeleteBinLine, RiEditLine, RiLock2Line } from '@remixicon/react'
|
import { RiDeleteBinLine, RiEditLine, RiLock2Line } from '@remixicon/react'
|
||||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||||
import { useStore } from '@/app/components/workflow/store'
|
import { useStore } from '@/app/components/workflow/store'
|
||||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
import { useTranslation } from 'react-i18next'
|
||||||
import cn from '@/utils/classnames'
|
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
type EnvItemProps = {
|
|
||||||
env: EnvironmentVariable
|
type EnvItemProps = {
|
||||||
onEdit: (env: EnvironmentVariable) => void
|
env: EnvironmentVariable
|
||||||
onDelete: (env: EnvironmentVariable) => void
|
onEdit: (env: EnvironmentVariable) => void
|
||||||
}
|
onDelete: (env: EnvironmentVariable) => void
|
||||||
|
}
|
||||||
const EnvItem = ({
|
|
||||||
env,
|
const EnvItem = ({
|
||||||
onEdit,
|
env,
|
||||||
onDelete,
|
onEdit,
|
||||||
}: EnvItemProps) => {
|
onDelete,
|
||||||
const envSecrets = useStore(s => s.envSecrets)
|
}: EnvItemProps) => {
|
||||||
const [destructive, setDestructive] = useState(false)
|
const { t } = useTranslation()
|
||||||
|
const envSecrets = useStore(s => s.envSecrets)
|
||||||
return (
|
const [destructive, setDestructive] = useState(false)
|
||||||
<div className={cn(
|
|
||||||
'radius-md mb-1 border border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-2.5 py-2 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover',
|
return (
|
||||||
destructive && 'border-state-destructive-border hover:bg-state-destructive-hover',
|
<div className={cn(
|
||||||
)}>
|
'radius-md mb-1 border border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-2.5 py-2 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover',
|
||||||
<div className='flex items-center justify-between'>
|
destructive && 'border-state-destructive-border hover:bg-state-destructive-hover',
|
||||||
<div className='flex grow items-center gap-1'>
|
)}>
|
||||||
<Env className='h-4 w-4 text-util-colors-violet-violet-600' />
|
<div className='flex items-center justify-between'>
|
||||||
<div className='system-sm-medium text-text-primary'>{env.name}</div>
|
<div className='flex grow items-center gap-1'>
|
||||||
<div className='system-xs-medium text-text-tertiary'>{capitalize(env.value_type)}</div>
|
<Env className='h-4 w-4 text-util-colors-violet-violet-600' />
|
||||||
{env.value_type === 'secret' && <RiLock2Line className='h-3 w-3 text-text-tertiary' />}
|
<div className='system-sm-medium text-text-primary'>{env.name}</div>
|
||||||
</div>
|
<div className='system-xs-medium text-text-tertiary'>{capitalize(env.value_type)}</div>
|
||||||
<div className='flex shrink-0 items-center gap-1 text-text-tertiary'>
|
{env.value_type === 'secret' && <RiLock2Line className='h-3 w-3 text-text-tertiary' />}
|
||||||
<div className='radius-md cursor-pointer p-1 hover:bg-state-base-hover hover:text-text-secondary'>
|
</div>
|
||||||
<RiEditLine className='h-4 w-4' onClick={() => onEdit(env)}/>
|
<div className='flex shrink-0 items-center gap-1 text-text-tertiary'>
|
||||||
</div>
|
<div className='radius-md cursor-pointer p-1 hover:bg-state-base-hover hover:text-text-secondary'>
|
||||||
<div
|
<RiEditLine className='h-4 w-4' onClick={() => onEdit(env)}/>
|
||||||
className='radius-md cursor-pointer p-1 hover:bg-state-destructive-hover hover:text-text-destructive'
|
</div>
|
||||||
onMouseOver={() => setDestructive(true)}
|
<div
|
||||||
onMouseOut={() => setDestructive(false)}
|
className='radius-md cursor-pointer p-1 hover:bg-state-destructive-hover hover:text-text-destructive'
|
||||||
>
|
onMouseOver={() => setDestructive(true)}
|
||||||
<RiDeleteBinLine className='h-4 w-4' onClick={() => onDelete(env)} />
|
onMouseOut={() => setDestructive(false)}
|
||||||
</div>
|
>
|
||||||
</div>
|
<RiDeleteBinLine className='h-4 w-4' onClick={() => onDelete(env)} />
|
||||||
</div>
|
</div>
|
||||||
<div className='system-xs-regular truncate text-text-tertiary'>{env.value_type === 'secret' ? envSecrets[env.id] : env.value}</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
<div className='system-xs-regular truncate text-text-tertiary'>{env.description ? (`${t('workflow.env.modal.description')}: ${env.description}`) : ''}</div>
|
||||||
}
|
<div className='system-xs-regular truncate text-text-tertiary'>{env.value_type === 'secret' ? envSecrets[env.id] : env.value}</div>
|
||||||
|
</div>
|
||||||
export default memo(EnvItem)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(EnvItem)
|
||||||
|
|||||||
@ -1,167 +1,182 @@
|
|||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { v4 as uuid4 } from 'uuid'
|
import { v4 as uuid4 } from 'uuid'
|
||||||
import { RiCloseLine } from '@remixicon/react'
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
import { useContext } from 'use-context-selector'
|
import { useContext } from 'use-context-selector'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
import { useStore } from '@/app/components/workflow/store'
|
import { useStore } from '@/app/components/workflow/store'
|
||||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import { checkKeys } from '@/utils/var'
|
import { checkKeys } from '@/utils/var'
|
||||||
|
|
||||||
export type ModalPropsType = {
|
export type ModalPropsType = {
|
||||||
env?: EnvironmentVariable
|
env?: EnvironmentVariable
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSave: (env: EnvironmentVariable) => void
|
onSave: (env: EnvironmentVariable) => void
|
||||||
}
|
}
|
||||||
const VariableModal = ({
|
const VariableModal = ({
|
||||||
env,
|
env,
|
||||||
onClose,
|
onClose,
|
||||||
onSave,
|
onSave,
|
||||||
}: ModalPropsType) => {
|
}: ModalPropsType) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { notify } = useContext(ToastContext)
|
const { notify } = useContext(ToastContext)
|
||||||
const envList = useStore(s => s.environmentVariables)
|
const envList = useStore(s => s.environmentVariables)
|
||||||
const envSecrets = useStore(s => s.envSecrets)
|
const envSecrets = useStore(s => s.envSecrets)
|
||||||
const [type, setType] = React.useState<'string' | 'number' | 'secret'>('string')
|
const [type, setType] = React.useState<'string' | 'number' | 'secret'>('string')
|
||||||
const [name, setName] = React.useState('')
|
const [name, setName] = React.useState('')
|
||||||
const [value, setValue] = React.useState<any>()
|
const [value, setValue] = React.useState<any>()
|
||||||
|
const [des, setDes] = React.useState<string>('')
|
||||||
const checkVariableName = (value: string) => {
|
|
||||||
const { isValid, errorMessageKey } = checkKeys([value], false)
|
const checkVariableName = (value: string) => {
|
||||||
if (!isValid) {
|
const { isValid, errorMessageKey } = checkKeys([value], false)
|
||||||
notify({
|
if (!isValid) {
|
||||||
type: 'error',
|
notify({
|
||||||
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: t('workflow.env.modal.name') }),
|
type: 'error',
|
||||||
})
|
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: t('workflow.env.modal.name') }),
|
||||||
return false
|
})
|
||||||
}
|
return false
|
||||||
return true
|
}
|
||||||
}
|
return true
|
||||||
|
}
|
||||||
const handleSave = () => {
|
|
||||||
if (!checkVariableName(name))
|
const handleSave = () => {
|
||||||
return
|
if (!checkVariableName(name))
|
||||||
if (!value)
|
return
|
||||||
return notify({ type: 'error', message: 'value can not be empty' })
|
if (!value)
|
||||||
|
return notify({ type: 'error', message: 'value can not be empty' })
|
||||||
// Add check for duplicate name when editing
|
|
||||||
if (env && env.name !== name && envList.some(e => e.name === name))
|
// Add check for duplicate name when editing
|
||||||
return notify({ type: 'error', message: 'name is existed' })
|
if (env && env.name !== name && envList.some(e => e.name === name))
|
||||||
// Original check for create new variable
|
return notify({ type: 'error', message: 'name is existed' })
|
||||||
if (!env && envList.some(e => e.name === name))
|
// Original check for create new variable
|
||||||
return notify({ type: 'error', message: 'name is existed' })
|
if (!env && envList.some(e => e.name === name))
|
||||||
|
return notify({ type: 'error', message: 'name is existed' })
|
||||||
onSave({
|
|
||||||
id: env ? env.id : uuid4(),
|
onSave({
|
||||||
value_type: type,
|
id: env ? env.id : uuid4(),
|
||||||
name,
|
value_type: type,
|
||||||
value: type === 'number' ? Number(value) : value,
|
name,
|
||||||
})
|
value: type === 'number' ? Number(value) : value,
|
||||||
onClose()
|
description: des,
|
||||||
}
|
})
|
||||||
|
onClose()
|
||||||
useEffect(() => {
|
}
|
||||||
if (env) {
|
|
||||||
setType(env.value_type)
|
useEffect(() => {
|
||||||
setName(env.name)
|
if (env) {
|
||||||
setValue(env.value_type === 'secret' ? envSecrets[env.id] : env.value)
|
setType(env.value_type)
|
||||||
}
|
setName(env.name)
|
||||||
}, [env, envSecrets])
|
setValue(env.value_type === 'secret' ? envSecrets[env.id] : env.value)
|
||||||
|
setDes(env.description)
|
||||||
return (
|
}
|
||||||
<div
|
}, [env, envSecrets])
|
||||||
className={cn('flex h-full w-[360px] flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-2xl')}
|
|
||||||
>
|
return (
|
||||||
<div className='system-xl-semibold mb-3 flex shrink-0 items-center justify-between p-4 pb-0 text-text-primary'>
|
<div
|
||||||
{!env ? t('workflow.env.modal.title') : t('workflow.env.modal.editTitle')}
|
className={cn('flex h-full w-[360px] flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-2xl')}
|
||||||
<div className='flex items-center'>
|
>
|
||||||
<div
|
<div className='system-xl-semibold mb-3 flex shrink-0 items-center justify-between p-4 pb-0 text-text-primary'>
|
||||||
className='flex h-6 w-6 cursor-pointer items-center justify-center'
|
{!env ? t('workflow.env.modal.title') : t('workflow.env.modal.editTitle')}
|
||||||
onClick={onClose}
|
<div className='flex items-center'>
|
||||||
>
|
<div
|
||||||
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
|
className='flex h-6 w-6 cursor-pointer items-center justify-center'
|
||||||
</div>
|
onClick={onClose}
|
||||||
</div>
|
>
|
||||||
</div>
|
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
|
||||||
<div className='px-4 py-2'>
|
</div>
|
||||||
{/* type */}
|
</div>
|
||||||
<div className='mb-4'>
|
</div>
|
||||||
<div className='system-sm-semibold mb-1 flex h-6 items-center text-text-secondary'>{t('workflow.env.modal.type')}</div>
|
<div className='px-4 py-2'>
|
||||||
<div className='flex gap-2'>
|
{/* type */}
|
||||||
<div className={cn(
|
<div className='mb-4'>
|
||||||
'radius-md system-sm-regular flex w-[106px] cursor-pointer items-center justify-center border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
|
<div className='system-sm-semibold mb-1 flex h-6 items-center text-text-secondary'>{t('workflow.env.modal.type')}</div>
|
||||||
type === 'string' && 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary shadow-xs hover:border-components-option-card-option-selected-border',
|
<div className='flex gap-2'>
|
||||||
)} onClick={() => setType('string')}>String</div>
|
<div className={cn(
|
||||||
<div className={cn(
|
'radius-md system-sm-regular flex w-[106px] cursor-pointer items-center justify-center border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
|
||||||
'radius-md system-sm-regular flex w-[106px] cursor-pointer items-center justify-center border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
|
type === 'string' && 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary shadow-xs hover:border-components-option-card-option-selected-border',
|
||||||
type === 'number' && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg font-medium text-text-primary shadow-xs hover:border-components-option-card-option-selected-border',
|
)} onClick={() => setType('string')}>String</div>
|
||||||
)} onClick={() => {
|
<div className={cn(
|
||||||
setType('number')
|
'radius-md system-sm-regular flex w-[106px] cursor-pointer items-center justify-center border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
|
||||||
if (!(/^\d$/).test(value))
|
type === 'number' && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg font-medium text-text-primary shadow-xs hover:border-components-option-card-option-selected-border',
|
||||||
setValue('')
|
)} onClick={() => {
|
||||||
}}>Number</div>
|
setType('number')
|
||||||
<div className={cn(
|
if (!(/^\d$/).test(value))
|
||||||
'radius-md system-sm-regular flex w-[106px] cursor-pointer items-center justify-center border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
|
setValue('')
|
||||||
type === 'secret' && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg font-medium text-text-primary shadow-xs hover:border-components-option-card-option-selected-border',
|
}}>Number</div>
|
||||||
)} onClick={() => setType('secret')}>
|
<div className={cn(
|
||||||
<span>Secret</span>
|
'radius-md system-sm-regular flex w-[106px] cursor-pointer items-center justify-center border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
|
||||||
<Tooltip
|
type === 'secret' && 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg font-medium text-text-primary shadow-xs hover:border-components-option-card-option-selected-border',
|
||||||
popupContent={
|
)} onClick={() => setType('secret')}>
|
||||||
<div className='w-[240px]'>
|
<span>Secret</span>
|
||||||
{t('workflow.env.modal.secretTip')}
|
<Tooltip
|
||||||
</div>
|
popupContent={
|
||||||
}
|
<div className='w-[240px]'>
|
||||||
triggerClassName='ml-0.5 w-3.5 h-3.5'
|
{t('workflow.env.modal.secretTip')}
|
||||||
/>
|
</div>
|
||||||
</div>
|
}
|
||||||
</div>
|
triggerClassName='ml-0.5 w-3.5 h-3.5'
|
||||||
</div>
|
/>
|
||||||
{/* name */}
|
</div>
|
||||||
<div className='mb-4'>
|
</div>
|
||||||
<div className='system-sm-semibold mb-1 flex h-6 items-center text-text-secondary'>{t('workflow.env.modal.name')}</div>
|
</div>
|
||||||
<div className='flex'>
|
{/* name */}
|
||||||
<Input
|
<div className='mb-4'>
|
||||||
placeholder={t('workflow.env.modal.namePlaceholder') || ''}
|
<div className='system-sm-semibold mb-1 flex h-6 items-center text-text-secondary'>{t('workflow.env.modal.name')}</div>
|
||||||
value={name}
|
<div className='flex'>
|
||||||
onChange={e => setName(e.target.value || '')}
|
<Input
|
||||||
onBlur={e => checkVariableName(e.target.value)}
|
placeholder={t('workflow.env.modal.namePlaceholder') || ''}
|
||||||
type='text'
|
value={name}
|
||||||
/>
|
onChange={e => setName(e.target.value || '')}
|
||||||
</div>
|
onBlur={e => checkVariableName(e.target.value)}
|
||||||
</div>
|
type='text'
|
||||||
{/* value */}
|
/>
|
||||||
<div className=''>
|
</div>
|
||||||
<div className='system-sm-semibold mb-1 flex h-6 items-center text-text-secondary'>{t('workflow.env.modal.value')}</div>
|
</div>
|
||||||
<div className='flex'>
|
{/* value */}
|
||||||
{
|
<div className=''>
|
||||||
type !== 'number' ? <textarea
|
<div className='system-sm-semibold mb-1 flex h-6 items-center text-text-secondary'>{t('workflow.env.modal.value')}</div>
|
||||||
className='system-sm-regular placeholder:system-sm-regular block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs'
|
<div className='flex'>
|
||||||
value={value}
|
{
|
||||||
placeholder={t('workflow.env.modal.valuePlaceholder') || ''}
|
type !== 'number' ? <textarea
|
||||||
onChange={e => setValue(e.target.value)}
|
className='system-sm-regular placeholder:system-sm-regular block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs'
|
||||||
/>
|
value={value}
|
||||||
: <Input
|
placeholder={t('workflow.env.modal.valuePlaceholder') || ''}
|
||||||
placeholder={t('workflow.env.modal.valuePlaceholder') || ''}
|
onChange={e => setValue(e.target.value)}
|
||||||
value={value}
|
/>
|
||||||
onChange={e => setValue(e.target.value)}
|
: <Input
|
||||||
type="number"
|
placeholder={t('workflow.env.modal.valuePlaceholder') || ''}
|
||||||
/>
|
value={value}
|
||||||
}
|
onChange={e => setValue(e.target.value)}
|
||||||
</div>
|
type="number"
|
||||||
</div>
|
/>
|
||||||
</div>
|
}
|
||||||
<div className='flex flex-row-reverse rounded-b-2xl p-4 pt-2'>
|
</div>
|
||||||
<div className='flex gap-2'>
|
</div>
|
||||||
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
|
{/* description */}
|
||||||
<Button variant='primary' onClick={handleSave}>{t('common.operation.save')}</Button>
|
<div className=''>
|
||||||
</div>
|
<div className='system-sm-semibold mb-1 flex h-6 items-center text-text-secondary'>{t('workflow.env.modal.description')}</div>
|
||||||
</div>
|
<div className='flex'>
|
||||||
</div>
|
<textarea
|
||||||
)
|
className='system-sm-regular placeholder:system-sm-regular block h-20 w-full resize-none appearance-none rounded-lg border border-transparent bg-components-input-bg-normal p-2 text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs'
|
||||||
}
|
value={des}
|
||||||
|
placeholder={t('workflow.env.modal.descriptionPlaceholder') || ''}
|
||||||
export default VariableModal
|
onChange={e => setDes(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-row-reverse rounded-b-2xl p-4 pt-2'>
|
||||||
|
<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
|
||||||
|
|||||||
Loading…
Reference in New Issue