feat(web): only one evaluation workflow can be added

This commit is contained in:
JzoNg 2026-04-09 17:43:34 +08:00
parent 5069694bba
commit c29245c1cb
6 changed files with 46 additions and 4 deletions

View File

@ -41,6 +41,24 @@ describe('evaluation store', () => {
expect(configuredMetric!.customConfig!.workflowName).toBe(config.workflowOptions[0].label)
})
it('should only add one custom metric', () => {
const resourceType = 'apps'
const resourceId = 'app-custom-limit'
const store = useEvaluationStore.getState()
store.ensureResource(resourceType, resourceId)
store.addCustomMetric(resourceType, resourceId)
store.addCustomMetric(resourceType, resourceId)
expect(
useEvaluationStore
.getState()
.resources['apps:app-custom-limit']
.metrics
.filter(metric => metric.kind === 'custom-workflow'),
).toHaveLength(1)
})
it('should add and remove builtin metrics', () => {
const resourceType = 'apps'
const resourceId = 'app-2'

View File

@ -210,5 +210,20 @@ describe('MetricSection', () => {
expect(screen.getByText('evaluation.metrics.custom.workflowPlaceholder')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'evaluation.metrics.custom.addMapping' })).toBeInTheDocument()
})
it('should disable adding another custom metric when one already exists', () => {
// Arrange
act(() => {
useEvaluationStore.getState().addCustomMetric(resourceType, resourceId)
})
// Act
renderMetricSection()
fireEvent.click(screen.getByRole('button', { name: 'evaluation.metrics.add' }))
// Assert
expect(screen.getByRole('button', { name: /evaluation.metrics.custom.footerTitle/i })).toBeDisabled()
expect(screen.getByText('evaluation.metrics.custom.limitDescription')).toBeInTheDocument()
})
})
})

View File

@ -12,7 +12,7 @@ import {
PopoverTrigger,
} from '@/app/components/base/ui/popover'
import { cn } from '@/utils/classnames'
import { useEvaluationStore } from '../../store'
import { useEvaluationResource, useEvaluationStore } from '../../store'
import SelectorEmptyState from './selector-empty-state'
import SelectorFooter from './selector-footer'
import SelectorMetricSection from './selector-metric-section'
@ -26,12 +26,14 @@ const MetricSelector = ({
triggerStyle = 'button',
}: MetricSelectorProps) => {
const { t } = useTranslation('evaluation')
const resource = useEvaluationResource(resourceType, resourceId)
const addCustomMetric = useEvaluationStore(state => state.addCustomMetric)
const [open, setOpen] = useState(false)
const [query, setQuery] = useState('')
const [nodeInfoMap, setNodeInfoMap] = useState<Record<string, Array<{ node_id: string, title: string, type: string }>>>({})
const [collapsedMetricMap, setCollapsedMetricMap] = useState<Record<string, boolean>>({})
const [expandedMetricNodesMap, setExpandedMetricNodesMap] = useState<Record<string, boolean>>({})
const hasCustomMetric = resource.metrics.some(metric => metric.kind === 'custom-workflow')
const {
builtinMetricMap,
@ -134,7 +136,8 @@ const MetricSelector = ({
<SelectorFooter
title={t('metrics.custom.footerTitle')}
description={t('metrics.custom.footerDescription')}
description={hasCustomMetric ? t('metrics.custom.limitDescription') : t('metrics.custom.footerDescription')}
disabled={hasCustomMetric}
onClick={() => {
addCustomMetric(resourceType, resourceId)
setOpen(false)

View File

@ -1,18 +1,21 @@
type SelectorFooterProps = {
title: string
description: string
disabled?: boolean
onClick: () => void
}
const SelectorFooter = ({
title,
description,
disabled = false,
onClick,
}: SelectorFooterProps) => {
return (
<button
type="button"
className="relative flex items-center gap-3 overflow-hidden border-t border-divider-subtle bg-background-default-subtle px-4 py-5 text-left hover:bg-state-base-hover-alt"
disabled={disabled}
className="relative flex items-center gap-3 overflow-hidden border-t border-divider-subtle bg-background-default-subtle px-4 py-5 text-left enabled:hover:bg-state-base-hover-alt disabled:cursor-not-allowed disabled:opacity-60"
onClick={onClick}
>
<div className="absolute -left-6 -top-6 h-28 w-28 rounded-full bg-util-colors-indigo-indigo-100 opacity-50 blur-2xl" />

View File

@ -142,7 +142,9 @@ export const useEvaluationStore = create<EvaluationStore>((set, get) => ({
set(state => ({
resources: updateResourceState(state.resources, resourceType, resourceId, resource => ({
...resource,
metrics: [...resource.metrics, createCustomMetric()],
metrics: resource.metrics.some(metric => metric.kind === 'custom-workflow')
? resource.metrics
: [...resource.metrics, createCustomMetric()],
})),
}))
},

View File

@ -67,6 +67,7 @@
"metrics.custom.description": "Select an evaluation workflow and map your variables before running tests.",
"metrics.custom.footerDescription": "Connect your published evaluation workflows",
"metrics.custom.footerTitle": "Custom metrics",
"metrics.custom.limitDescription": "Only one custom metric can be added.",
"metrics.custom.mappingTitle": "Variable Mapping",
"metrics.custom.mappingWarning": "Complete the workflow selection and each variable mapping to enable batch tests.",
"metrics.custom.sourcePlaceholder": "Source variable",