mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
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:
parent
eba25caa70
commit
fed8b1ec69
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user