From c29245c1cb41f1e443db417800ae02d4fd0977e6 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Thu, 9 Apr 2026 17:43:34 +0800 Subject: [PATCH] feat(web): only one evaluation workflow can be added --- .../evaluation/__tests__/store.spec.ts | 18 ++++++++++++++++++ .../metric-section/__tests__/index.spec.tsx | 15 +++++++++++++++ .../components/metric-selector/index.tsx | 7 +++++-- .../metric-selector/selector-footer.tsx | 5 ++++- web/app/components/evaluation/store.ts | 4 +++- web/i18n/en-US/evaluation.json | 1 + 6 files changed, 46 insertions(+), 4 deletions(-) diff --git a/web/app/components/evaluation/__tests__/store.spec.ts b/web/app/components/evaluation/__tests__/store.spec.ts index 843a0107e8..18bd31a24c 100644 --- a/web/app/components/evaluation/__tests__/store.spec.ts +++ b/web/app/components/evaluation/__tests__/store.spec.ts @@ -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' diff --git a/web/app/components/evaluation/components/metric-section/__tests__/index.spec.tsx b/web/app/components/evaluation/components/metric-section/__tests__/index.spec.tsx index 03343d2a1c..0b76e4387b 100644 --- a/web/app/components/evaluation/components/metric-section/__tests__/index.spec.tsx +++ b/web/app/components/evaluation/components/metric-section/__tests__/index.spec.tsx @@ -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() + }) }) }) diff --git a/web/app/components/evaluation/components/metric-selector/index.tsx b/web/app/components/evaluation/components/metric-selector/index.tsx index a01256bb8d..4870952b9b 100644 --- a/web/app/components/evaluation/components/metric-selector/index.tsx +++ b/web/app/components/evaluation/components/metric-selector/index.tsx @@ -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>>({}) const [collapsedMetricMap, setCollapsedMetricMap] = useState>({}) const [expandedMetricNodesMap, setExpandedMetricNodesMap] = useState>({}) + const hasCustomMetric = resource.metrics.some(metric => metric.kind === 'custom-workflow') const { builtinMetricMap, @@ -134,7 +136,8 @@ const MetricSelector = ({ { addCustomMetric(resourceType, resourceId) setOpen(false) diff --git a/web/app/components/evaluation/components/metric-selector/selector-footer.tsx b/web/app/components/evaluation/components/metric-selector/selector-footer.tsx index 3b8d04474a..74163c275d 100644 --- a/web/app/components/evaluation/components/metric-selector/selector-footer.tsx +++ b/web/app/components/evaluation/components/metric-selector/selector-footer.tsx @@ -1,18 +1,21 @@ type SelectorFooterProps = { title: string description: string + disabled?: boolean onClick: () => void } const SelectorFooter = ({ title, description, + disabled = false, onClick, }: SelectorFooterProps) => { return (