Skip to content

Commit 27d5e58

Browse files
fix(data-table): use DropdownMenuTrigger wrapper in view options (#2297)
* fix(license): use useApiStoreHook to fix react-doctor errors Replace useStore(useApiStore, ...) with useApiStoreHook(...) in license notification components. Passing a hook as a value argument to another hook violates React Compiler rules. useApiStoreHook is an existing wrapper that encapsulates the same call pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(data-table): use DropdownMenuTrigger wrapper in view options Replace raw radix DropdownMenuPrimitive.Trigger with the project's DropdownMenuTrigger wrapper in DataTableViewOptions. The raw primitive doesn't handle re-renders correctly with React Compiler. Fixes UX-967 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(data-table): add integration test for column toggle dropdown Verify DataTableViewOptions dropdown can be reopened after toggling columns, preventing regression of the broken trigger. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(data-table): add 'use no memo' to opt out of React Compiler The DataTable shared components (data-table.tsx, data-table-filter.tsx) were being compiled by React Compiler, which incorrectly memoized callbacks and state in DataTableFacetedFilter (Set mutation in event handlers) and other table-bound components. This broke search inputs and filter dropdowns on Knowledge Base, Secret Store, AI Agent, and Remote MCP list pages. Adding the 'use no memo' directive opts these files out of React Compiler, matching the pattern already used by the list page files. Also adds integration tests verifying the search input value updates on keystroke and can be cleared and reused. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(list-pages): add search and filter integration tests Add tests for search input and faceted filter functionality on Knowledge Base, AI Agent, and Remote MCP list pages to prevent regressions from React Compiler memoization issues. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(secrets): add search and filter integration tests for list page Add tests for search input and faceted filter functionality on the Secret Store list page to prevent regressions from React Compiler memoization issues. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(mcp): add Playwright E2E test for search and filter Add E2E tests for the Remote MCP servers list page that run against a production build with React Compiler active. These tests verify that the search input accepts keystrokes, filters table rows, and that the status faceted filter opens and is selectable — all of which were broken when React Compiler memoized shared DataTable component callbacks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: skip MCP search E2E tests when feature unavailable in variant The MCP servers page isn't available in the OSS console variant, causing E2E tests to fail when the search input isn't rendered. Use test.skip() to gracefully skip when the page doesn't have the UI. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: replace Playwright E2E with integration tests for DataTable Delete the MCP search Playwright test (MCP page unavailable in OSS variant). Replace with comprehensive integration tests for DataTable search input, faceted filter rendering, and view options toggle. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c4ce059 commit 27d5e58

File tree

7 files changed

+890
-3
lines changed

7 files changed

+890
-3
lines changed

frontend/src/components/pages/agents/list/ai-agent-list-page.test.tsx

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,4 +629,151 @@ describe('AIAgentsListPage', () => {
629629

630630
expect(screen.getByRole('button', { name: 'Next Page' })).toBeDisabled();
631631
});
632+
633+
test('search input updates value on keystrokes', async () => {
634+
const user = userEvent.setup();
635+
636+
const agent1 = create(AIAgentSchema, {
637+
id: 'agent-1',
638+
displayName: 'Test Agent',
639+
description: '',
640+
state: AIAgent_State.RUNNING,
641+
provider: { provider: { case: 'openai', value: { apiKey: 'key' } } },
642+
model: 'gpt-4',
643+
systemPrompt: '',
644+
mcpServers: {},
645+
tags: {},
646+
});
647+
648+
const transport = createAIAgentsTransport({
649+
listAIAgentsMock: vi
650+
.fn()
651+
.mockReturnValue(create(ListAIAgentsResponseSchema, { aiAgents: [agent1], nextPageToken: '' })),
652+
});
653+
654+
renderWithFileRoutes(<AIAgentsListPage />, { transport });
655+
656+
await waitFor(() => {
657+
expect(screen.getByText('Test Agent')).toBeVisible();
658+
});
659+
660+
const filterInput = screen.getByPlaceholderText('Filter agents...');
661+
await user.type(filterInput, 'hello');
662+
663+
// Input value must reflect typed text — a React Compiler memoization
664+
// bug would freeze it at the initial empty string.
665+
expect(filterInput).toHaveValue('hello');
666+
});
667+
668+
test('filters agents by name via search input', async () => {
669+
const user = userEvent.setup();
670+
671+
const agent1 = create(AIAgentSchema, {
672+
id: 'agent-1',
673+
displayName: 'Alpha Agent',
674+
description: '',
675+
state: AIAgent_State.RUNNING,
676+
provider: { provider: { case: 'openai', value: { apiKey: 'key' } } },
677+
model: 'gpt-4',
678+
systemPrompt: '',
679+
mcpServers: {},
680+
tags: {},
681+
});
682+
683+
const agent2 = create(AIAgentSchema, {
684+
id: 'agent-2',
685+
displayName: 'Beta Agent',
686+
description: '',
687+
state: AIAgent_State.STOPPED,
688+
provider: { provider: { case: 'openai', value: { apiKey: 'key' } } },
689+
model: 'gpt-4',
690+
systemPrompt: '',
691+
mcpServers: {},
692+
tags: {},
693+
});
694+
695+
const transport = createAIAgentsTransport({
696+
listAIAgentsMock: vi
697+
.fn()
698+
.mockReturnValue(create(ListAIAgentsResponseSchema, { aiAgents: [agent1, agent2], nextPageToken: '' })),
699+
});
700+
701+
renderWithFileRoutes(<AIAgentsListPage />, { transport });
702+
703+
await waitFor(() => {
704+
expect(screen.getByText('Alpha Agent')).toBeVisible();
705+
expect(screen.getByText('Beta Agent')).toBeVisible();
706+
});
707+
708+
const filterInput = screen.getByPlaceholderText('Filter agents...');
709+
await user.type(filterInput, 'Beta');
710+
711+
await waitFor(() => {
712+
expect(screen.getByText('Beta Agent')).toBeVisible();
713+
expect(screen.queryByText('Alpha Agent')).not.toBeInTheDocument();
714+
});
715+
716+
// Clear and type again to verify the input remains interactive
717+
await user.clear(filterInput);
718+
719+
await waitFor(() => {
720+
expect(screen.getByText('Alpha Agent')).toBeVisible();
721+
expect(screen.getByText('Beta Agent')).toBeVisible();
722+
});
723+
});
724+
725+
test('status faceted filter filters results', async () => {
726+
const user = userEvent.setup();
727+
728+
const agent1 = create(AIAgentSchema, {
729+
id: 'agent-1',
730+
displayName: 'Running Agent',
731+
description: '',
732+
state: AIAgent_State.RUNNING,
733+
provider: { provider: { case: 'openai', value: { apiKey: 'key' } } },
734+
model: 'gpt-4',
735+
systemPrompt: '',
736+
mcpServers: {},
737+
tags: {},
738+
});
739+
740+
const agent2 = create(AIAgentSchema, {
741+
id: 'agent-2',
742+
displayName: 'Stopped Agent',
743+
description: '',
744+
state: AIAgent_State.STOPPED,
745+
provider: { provider: { case: 'openai', value: { apiKey: 'key' } } },
746+
model: 'gpt-4',
747+
systemPrompt: '',
748+
mcpServers: {},
749+
tags: {},
750+
});
751+
752+
const transport = createAIAgentsTransport({
753+
listAIAgentsMock: vi
754+
.fn()
755+
.mockReturnValue(create(ListAIAgentsResponseSchema, { aiAgents: [agent1, agent2], nextPageToken: '' })),
756+
});
757+
758+
renderWithFileRoutes(<AIAgentsListPage />, { transport });
759+
760+
await waitFor(() => {
761+
expect(screen.getByText('Running Agent')).toBeVisible();
762+
expect(screen.getByText('Stopped Agent')).toBeVisible();
763+
});
764+
765+
// Click the "Status" faceted filter button (not the column header one in <thead>)
766+
const statusFilterButton = screen.getAllByRole('button', { name: /status/i }).find((btn) => !btn.closest('thead'))!;
767+
await user.click(statusFilterButton);
768+
769+
// Select the "Stopped" option from the filter popover
770+
const stoppedOption = await screen.findByRole('option', { name: /stopped/i });
771+
await user.click(stoppedOption);
772+
773+
// Only the stopped agent should remain visible
774+
await waitFor(() => {
775+
expect(screen.getByText('Stopped Agent')).toBeVisible();
776+
expect(screen.queryByText('Running Agent')).not.toBeInTheDocument();
777+
});
778+
});
632779
});

0 commit comments

Comments
 (0)