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
Mminamiyama 11 months ago
parent 7b7a506abd
commit 7c097fe6eb

@ -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…
Cancel
Save