Skip to content

Commit 003a47b

Browse files
Add search bar to all pages (#59)
* Refactor: Move 'Add New' buttons to fixed top position This commit refactors the following pages: - tables - models - datasets - workflows The "Add New {category}" button on these pages has been moved to the top of the page, below the header, and is now fixed (not in the scrollable area). Specific changes: - Table page: "Add new table" and "Import" buttons are now at the top. - Dataset page: "Add new dataset" and "Help" buttons are now at the top. - Models page: "Add new provider" button is now at the top. - Workflows page: "Add new workflow" button is now at the top. All related TypeScript checks, tests, and the build process pass successfully. * Refactor: Update styles for fixed 'Add New' buttons This commit updates the styles for the fixed "Add New {category}" buttons on the tables, models, datasets, and workflows pages. Specific changes: - Removed the bottom border (separator) from the div containing the buttons. - Reduced the bottom padding of the div to decrease space below the buttons. These changes address the feedback to remove the separator and reduce the space below the buttons. All related TypeScript checks, tests, and the build process pass successfully. * Feat: Add search functionality to list pages This commit introduces search functionality to the following pages: - Tables: Search by table name. - Datasets: Search by dataset name. - Models: Search by provider name, model name, or model ID. - Workflows: Search by workflow name. Key changes include: - Added a search input field to the fixed header of each page. - Implemented filtering logic to update the displayed items based on the search query. - Added "No results found" messages when applicable. - Included comprehensive unit tests for the new search functionality on all affected pages. - Performed code cleanup by removing temporary comments. All TypeScript checks, tests, and the build process pass successfully. * Fix: Ensure dataset list refreshes on create/delete and add tests This commit addresses an issue where the dataset list page was not reliably updating after a dataset was created or deleted. Changes: - Implemented a `refreshKey` mechanism in `DatasetListPage` and `DatasetList` to ensure the list component re-fetches data when a dataset is created, updated, or deleted. - Added new tests to `dataset-list-page.test.tsx` to specifically verify dataset creation and deletion scenarios, confirming that the list refreshes as expected. - Corrected mock setups in tests to ensure stability and accuracy, including the `useToast` mock. All related TypeScript checks, tests, and the build process pass successfully. * add search bar to each page --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent 67cb251 commit 003a47b

10 files changed

Lines changed: 1129 additions & 383 deletions
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
import { render, screen, waitFor } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
3+
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
4+
import { DatasetListPage } from './dataset-list-page';
5+
import * as actions from '@/actions';
6+
import { MemoryRouter, useLocation, useNavigate } from 'react-router-dom';
7+
import { TestProvider } from '@/test/helpers/test-provider';
8+
9+
vi.mock('@/actions');
10+
vi.mock('react-router-dom', async () => {
11+
const originalModule = await vi.importActual('react-router-dom');
12+
return {
13+
...originalModule,
14+
useNavigate: vi.fn(),
15+
useLocation: vi.fn(),
16+
};
17+
});
18+
19+
import * as ActualToastHook from '@/hooks/use-toast';
20+
vi.mock('@/hooks/use-toast', async (importOriginal) => {
21+
const actual = await importOriginal<typeof ActualToastHook>();
22+
return {
23+
...actual,
24+
useToast: vi.fn(() => ({
25+
toast: vi.fn(),
26+
})),
27+
};
28+
});
29+
30+
31+
describe('DatasetListPage Search Functionality', () => {
32+
const sampleDatasets = [
33+
{ id: 'ds1', name: 'Dataset Alpha', description: 'First one', type: 'csv' },
34+
{ id: 'ds2', name: 'Dataset Beta', description: 'Second one', type: 'list' },
35+
{ id: 'ds3', name: 'Gamma Dataset', description: 'Third one', type: 'csv' },
36+
];
37+
38+
beforeEach(async () => {
39+
vi.resetAllMocks();
40+
41+
(actions.getDatasets as Mock).mockResolvedValue({
42+
datasets: sampleDatasets,
43+
});
44+
(actions.deleteDataset as Mock).mockResolvedValue({});
45+
(actions.createDataset as Mock).mockResolvedValue({});
46+
(actions.updateDataset as Mock).mockResolvedValue({});
47+
48+
49+
(useNavigate as Mock).mockReturnValue(vi.fn());
50+
(useLocation as Mock).mockReturnValue({
51+
key: 'testKey',
52+
pathname: '/datasets',
53+
search: '',
54+
hash: '',
55+
state: null,
56+
});
57+
58+
render(
59+
<MemoryRouter>
60+
<TestProvider>
61+
<DatasetListPage />
62+
</TestProvider>
63+
</MemoryRouter>
64+
);
65+
66+
await waitFor(() => expect(screen.getByText('Dataset Alpha')).toBeInTheDocument());
67+
await waitFor(() => expect(screen.getByText('Dataset Beta')).toBeInTheDocument());
68+
await waitFor(() => expect(screen.getByText('Gamma Dataset')).toBeInTheDocument());
69+
});
70+
71+
it('should render all datasets initially', () => {
72+
expect(screen.getByText('Dataset Alpha')).toBeInTheDocument();
73+
expect(screen.getByText('Dataset Beta')).toBeInTheDocument();
74+
expect(screen.getByText('Gamma Dataset')).toBeInTheDocument();
75+
});
76+
77+
it('should filter datasets based on search query', async () => {
78+
const searchInput = screen.getByPlaceholderText('Search datasets...');
79+
await userEvent.type(searchInput, 'Alpha');
80+
81+
await waitFor(() => {
82+
expect(screen.getByText('Dataset Alpha')).toBeInTheDocument();
83+
expect(screen.queryByText('Dataset Beta')).not.toBeInTheDocument();
84+
expect(screen.queryByText('Gamma Dataset')).not.toBeInTheDocument();
85+
});
86+
});
87+
88+
it('should be case-insensitive', async () => {
89+
const searchInput = screen.getByPlaceholderText('Search datasets...');
90+
await userEvent.type(searchInput, 'gamma');
91+
92+
await waitFor(() => {
93+
expect(screen.queryByText('Dataset Alpha')).not.toBeInTheDocument();
94+
expect(screen.queryByText('Dataset Beta')).not.toBeInTheDocument();
95+
expect(screen.getByText('Gamma Dataset')).toBeInTheDocument();
96+
});
97+
});
98+
99+
it('should show no results message if search matches nothing', async () => {
100+
const searchInput = screen.getByPlaceholderText('Search datasets...');
101+
await userEvent.type(searchInput, 'NonExistentDataset');
102+
103+
// The component currently does not implement a specific "no results" message.
104+
// It would render an empty list.
105+
await waitFor(() => {
106+
expect(screen.queryByText('Dataset Alpha')).not.toBeInTheDocument();
107+
expect(screen.queryByText('Dataset Beta')).not.toBeInTheDocument();
108+
expect(screen.queryByText('Gamma Dataset')).not.toBeInTheDocument();
109+
});
110+
// If a "No datasets found..." message were implemented, we'd assert its presence here.
111+
});
112+
113+
it('should show all datasets when search query is cleared', async () => {
114+
const searchInput = screen.getByPlaceholderText('Search datasets...');
115+
await userEvent.type(searchInput, 'Alpha');
116+
117+
await waitFor(() => expect(screen.getByText('Dataset Alpha')).toBeInTheDocument());
118+
await waitFor(() => expect(screen.queryByText('Dataset Beta')).not.toBeInTheDocument());
119+
120+
await userEvent.clear(searchInput);
121+
122+
await waitFor(() => {
123+
expect(screen.getByText('Dataset Alpha')).toBeInTheDocument();
124+
expect(screen.getByText('Dataset Beta')).toBeInTheDocument();
125+
expect(screen.getByText('Gamma Dataset')).toBeInTheDocument();
126+
});
127+
});
128+
});
129+
130+
vi.mock('@/components/dialog/dataset/dataset', () => ({
131+
CreateDatasetDialog: vi.fn(({ isOpen, onClose, onCreate, dataset }: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
132+
if (!isOpen) return null;
133+
return (
134+
<div data-testid="mock-create-dataset-dialog">
135+
<button
136+
data-testid="mock-create-dataset-submit"
137+
onClick={() => {
138+
const type = dataset?.type || 'csv';
139+
onCreate({
140+
name: dataset?.name || 'New Mocked Dataset',
141+
description: dataset?.description || 'Mocked description',
142+
type: type,
143+
files: type === 'csv' ? [new File([''], 'mock.csv', { type: 'text/csv' })] : undefined,
144+
options: type === 'list' ? ['opt1'] : undefined,
145+
});
146+
}}
147+
>
148+
Create/Update
149+
</button>
150+
<button data-testid="mock-create-dataset-close" onClick={onClose}>Close</button>
151+
</div>
152+
);
153+
}),
154+
}));
155+
vi.mock('@/components/dialog/dataset/info', () => ({
156+
DatasetInfoDialog: vi.fn(() => <div data-testid="mock-dataset-info-dialog" />),
157+
}));
158+
vi.mock('@/components/dialog/dataset/preview', () => ({
159+
DatasetPreviewDialog: vi.fn(() => <div data-testid="mock-dataset-preview-dialog" />),
160+
}));
161+
162+
vi.mock('@/components/ui/common-card', () => ({
163+
CommonCard: vi.fn(({ name, children, onDelete, onEdit, onClick, badgeText }: {
164+
name: string;
165+
children: React.ReactNode;
166+
onDelete?: () => void;
167+
onEdit?: () => void;
168+
onClick: () => void;
169+
badgeText?: string;
170+
}) => (
171+
<div data-testid={`common-card-${name.replace(/\s+/g, "-")}`}>
172+
<button onClick={onClick} data-testid={`view-${name.replace(/\s+/g, "-")}`}>{name}</button>
173+
<div>{children}</div>
174+
{badgeText && <span>{badgeText}</span>}
175+
{onDelete && <button onClick={onDelete} data-testid={`delete-${name.replace(/\s+/g, "-")}`}>Delete</button>}
176+
{onEdit && <button onClick={onEdit} data-testid={`edit-${name.replace(/\s+/g, "-")}`}>Edit</button>}
177+
</div>
178+
)),
179+
}));
180+
181+
182+
describe('DatasetListPage Create, Delete and Refresh', () => {
183+
const initialDatasets = [
184+
{ id: '1', name: 'Dataset Alpha', description: 'First one', type: 'csv' as const },
185+
{ id: '2', name: 'Dataset Beta', description: 'Second one', type: 'list' as const },
186+
];
187+
188+
const newDataset = { id: '3', name: 'New Mocked Dataset', description: 'Mocked description', type: 'csv' as const };
189+
190+
const mockGetDatasets = actions.getDatasets as Mock;
191+
const mockCreateDataset = actions.createDataset as Mock;
192+
const mockDeleteDataset = actions.deleteDataset as Mock;
193+
194+
beforeEach(() => {
195+
vi.resetAllMocks();
196+
(useNavigate as Mock).mockReturnValue(vi.fn());
197+
(useLocation as Mock).mockReturnValue({
198+
key: 'testKeyCUD',
199+
pathname: '/datasets',
200+
search: '',
201+
hash: '',
202+
state: null,
203+
});
204+
205+
mockCreateDataset.mockResolvedValue({ ...newDataset });
206+
mockDeleteDataset.mockResolvedValue(undefined);
207+
});
208+
209+
it('should refresh the list after creating a new dataset', async () => {
210+
mockGetDatasets
211+
.mockResolvedValueOnce({ datasets: initialDatasets })
212+
.mockResolvedValueOnce({ datasets: [...initialDatasets, newDataset] });
213+
214+
render(
215+
<MemoryRouter>
216+
<TestProvider>
217+
<DatasetListPage />
218+
</TestProvider>
219+
</MemoryRouter>
220+
);
221+
222+
await waitFor(() => expect(screen.getByText('Dataset Alpha')).toBeInTheDocument());
223+
expect(screen.getByText('Dataset Beta')).toBeInTheDocument();
224+
225+
const addNewButton = screen.getByRole('button', { name: /Add New Dataset/i });
226+
await userEvent.click(addNewButton);
227+
228+
await waitFor(() => expect(screen.getByTestId('mock-create-dataset-dialog')).toBeInTheDocument());
229+
230+
const submitButton = screen.getByTestId('mock-create-dataset-submit');
231+
await userEvent.click(submitButton);
232+
233+
await waitFor(() => expect(screen.getByText('New Mocked Dataset')).toBeInTheDocument(), { timeout: 2000 });
234+
235+
expect(mockCreateDataset).toHaveBeenCalledTimes(1);
236+
await waitFor(() => expect(mockGetDatasets).toHaveBeenCalledTimes(2)); // Initial load + load after create
237+
});
238+
239+
it('should refresh the list after deleting a dataset', async () => {
240+
mockGetDatasets
241+
.mockResolvedValueOnce({ datasets: initialDatasets })
242+
.mockResolvedValueOnce({ datasets: [initialDatasets[1]] }); // Dataset Alpha (id: '1') is removed
243+
244+
render(
245+
<MemoryRouter>
246+
<TestProvider>
247+
<DatasetListPage />
248+
</TestProvider>
249+
</MemoryRouter>
250+
);
251+
252+
await waitFor(() => expect(screen.getByText('Dataset Alpha')).toBeInTheDocument());
253+
expect(screen.getByText('Dataset Beta')).toBeInTheDocument();
254+
255+
const deleteButtonAlpha = screen.getByTestId('delete-Dataset-Alpha');
256+
await userEvent.click(deleteButtonAlpha);
257+
// Note: Mocked CommonCard's onDelete is called directly, no confirmation dialog step here.
258+
259+
await waitFor(() => expect(screen.queryByText('Dataset Alpha')).not.toBeInTheDocument(), { timeout: 2000 });
260+
expect(screen.getByText('Dataset Beta')).toBeInTheDocument();
261+
262+
expect(mockDeleteDataset).toHaveBeenCalledWith('1');
263+
await waitFor(() => expect(mockGetDatasets).toHaveBeenCalledTimes(2)); // Initial load + load after delete
264+
});
265+
});

0 commit comments

Comments
 (0)