feat(editor): Use a segment control for the agent checks status filter (no-changelog) (#41169)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Emre Caglar 2026-10-16 10:09:00 +00:00 committed by GitHub
parent eba25caa70
commit fed8b1ec69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 78 additions and 54 deletions

View File

@ -443,28 +443,6 @@ describe('AgentEvalChecksPanel', () => {
expect(getAllByTestId(/agent-eval-check-/)).toHaveLength(4);
});
it('marks the selected status filter', async () => {
const user = userEvent.setup();
const { getByTestId } = renderFour();
const all = getByTestId('agent-eval-checks-filter-all');
const pass = getByTestId('agent-eval-checks-filter-pass');
expect(all).toHaveAttribute('aria-pressed', 'true');
expect(all.className).toContain('brand');
expect(pass).toHaveAttribute('aria-pressed', 'false');
expect(pass.className).toContain('outline');
expect(pass.className).not.toContain('brand');
await user.click(pass);
expect(pass).toHaveAttribute('aria-pressed', 'true');
expect(pass.className).toContain('brand');
expect(all).toHaveAttribute('aria-pressed', 'false');
expect(all.className).toContain('outline');
expect(all.className).not.toContain('brand');
});
it('filters down to only passing rows', async () => {
const user = userEvent.setup();
const { getAllByTestId, getByTestId } = renderFour();
@ -482,6 +460,35 @@ describe('AgentEvalChecksPanel', () => {
expect(getAllByTestId(/agent-eval-check-/)).toHaveLength(2);
});
it('shows the filter as one segment control, with the chosen option checked', async () => {
const user = userEvent.setup();
const { getAllByRole, getByTestId } = renderFour();
const segment = (key: string) =>
getByTestId(`agent-eval-checks-filter-${key}`).closest('[role="radio"]');
expect(getAllByRole('radio')).toHaveLength(3);
expect(segment('all')).toHaveAttribute('aria-checked', 'true');
await user.click(getByTestId('agent-eval-checks-filter-pass'));
expect(segment('pass')).toHaveAttribute('aria-checked', 'true');
expect(segment('all')).toHaveAttribute('aria-checked', 'false');
});
it('shows a status avatar on each status option, and none on "All"', () => {
const { getByTestId } = renderFour();
expect(
within(getByTestId('agent-eval-checks-filter-needs-work')).getByRole('img'),
).toBeInTheDocument();
expect(
within(getByTestId('agent-eval-checks-filter-pass')).getByRole('img'),
).toBeInTheDocument();
expect(
within(getByTestId('agent-eval-checks-filter-all')).queryByRole('img'),
).not.toBeInTheDocument();
});
});
it('emits rerun when "Run all checks" is clicked', async () => {
@ -1469,7 +1476,10 @@ describe('AgentEvalChecksPanel', () => {
await vi.waitFor(() =>
expect(queryByTestId('agent-eval-checks-filter-needs-work')).not.toBeInTheDocument(),
);
expect(getByTestId('agent-eval-checks-filter-all')).toHaveAttribute('aria-pressed', 'true');
expect(getByTestId('agent-eval-checks-filter-all').closest('[role="radio"]')).toHaveAttribute(
'aria-checked',
'true',
);
expect(getAllByTestId(/agent-eval-check-/)).toHaveLength(2);
});
});

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
/**
* The checks-have-run view behind `useTestAgentPreviewFlag`: status
* filter pills plus "Add a check" and "Run all checks" buttons up top, then each case as an
* filter segment control plus "Add a check" and "Run all checks" buttons up top, then each case as an
* `AgentEvalTryRow` — replaces `AgentEvalResultsPanel`'s vote/comment review
* model with the simpler row the rest of the preview flow already uses.
*
@ -10,7 +10,7 @@
* never passes `label` to `AgentEvalTryRow`.
*/
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { N8nButton, N8nIcon } from '@n8n/design-system';
import { N8nButton, N8nIcon, N8nSegmentControl } from '@n8n/design-system';
import { useToast } from '@n8n/composables/useToast';
import { useI18n, type BaseTextKey } from '@n8n/i18n';
@ -118,10 +118,15 @@ const needsWorkCount = computed(
type StatusFilter = 'all' | 'pass' | 'needs-work';
const statusFilter = ref<StatusFilter>('all');
/** No avatar for "All" — it isn't one of `AgentAvatar`'s statuses. */
const STATUS_FILTER_AVATARS: Record<StatusFilter, AgentAvatarKind | undefined> = {
'needs-work': 'work',
pass: 'pass',
all: undefined,
};
type StatusFilterOption = {
key: StatusFilter;
/** No avatar for "All" — it isn't one of `AgentAvatar`'s statuses. */
avatarKind?: AgentAvatarKind;
count: number;
labelKey: BaseTextKey;
/** "All" stays up regardless of count; the status pills only earn their
@ -135,13 +140,11 @@ type StatusFilterOption = {
const statusFilterOptions = computed<StatusFilterOption[]>(() => [
{
key: 'needs-work',
avatarKind: 'work',
count: needsWorkCount.value,
labelKey: 'agents.builder.agentEvals.checks.needsWork',
},
{
key: 'pass',
avatarKind: 'pass',
count: passedCount.value,
labelKey: 'agents.builder.agentEvals.checks.pass',
},
@ -157,6 +160,16 @@ const visibleStatusFilters = computed(() =>
statusFilterOptions.value.filter((option) => option.alwaysShown || option.count > 0),
);
const statusFilterSegments = computed(() =>
visibleStatusFilters.value.map((option) => ({
value: option.key,
label: i18n.baseText(option.labelKey, {
adjustToNumber: option.count,
interpolate: { count: String(option.count) },
}),
})),
);
// Needs-work first, then pass, so the rows a reviewer should act on are
// never buried below the ones that already look fine — regardless of filter.
const STATUS_RANK: Record<AgentAvatarKind, number> = {
@ -448,28 +461,24 @@ onBeforeUnmount(store.stopPollingRun);
<template>
<section :class="$style.panel" data-testid="agent-eval-checks-panel">
<header :class="$style.header">
<div v-if="rows.length > 0" :class="$style.filters">
<N8nButton
v-for="option in visibleStatusFilters"
:key="option.key"
type="button"
size="small"
:variant="statusFilter === option.key ? 'brand' : 'outline'"
:aria-pressed="statusFilter === option.key"
:data-testid="`agent-eval-checks-filter-${option.key}`"
@click="setStatusFilter(option.key)"
>
<template v-if="option.avatarKind" #icon>
<AgentAvatar :kind="option.avatarKind" size="xs" />
</template>
{{
i18n.baseText(option.labelKey, {
adjustToNumber: option.count,
interpolate: { count: String(option.count) },
})
}}
</N8nButton>
</div>
<N8nSegmentControl
v-if="rows.length > 0"
:model-value="statusFilter"
:options="statusFilterSegments"
@update:model-value="setStatusFilter"
>
<template #option="{ value, label }">
<span :class="$style.filterOption" :data-testid="`agent-eval-checks-filter-${value}`">
<AgentAvatar
v-if="STATUS_FILTER_AVATARS[value]"
:kind="STATUS_FILTER_AVATARS[value]"
size="xs"
:class="$style.filterAvatar"
/>
{{ label }}
</span>
</template>
</N8nSegmentControl>
<div :class="$style.actions">
<N8nButton
v-if="applicableSuggestionIds.length > 0 && !showingPreviousRun"
@ -576,10 +585,15 @@ onBeforeUnmount(store.stopPollingRun);
gap: var(--spacing--sm);
}
.filters {
display: flex;
.filterOption {
display: inline-flex;
align-items: center;
gap: var(--spacing--2xs);
gap: var(--spacing--4xs);
}
/* 16px, between the avatar's xs and row sizes, so it fits the segment height. */
.filterOption > .filterAvatar {
--avatar-size: 16px;
}
.actions {