Skip to content

Commit 9dd23f1

Browse files
committed
dataset infinite scroll
Signed-off-by: Colorado, Camilo <camilo.colorado@intel.com>
1 parent 8ab09b7 commit 9dd23f1

File tree

8 files changed

+124
-29
lines changed

8 files changed

+124
-29
lines changed

application/ui/src/features/inspect/dataset/dataset-list.component.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,21 @@ import { getThumbnailUrl } from '../utils';
1010
import { DatasetItemPlaceholder } from './dataset-item-placeholder/dataset-item-placeholder.component';
1111
import { getPlaceholderItem, isPlaceholderItem } from './dataset-item-placeholder/util';
1212
import { DeleteMediaItem } from './delete-dataset-item/delete-dataset-item.component';
13+
import { useGetMediaItems } from './hooks/use-get-media-items.hook';
1314
import { MediaPreview } from './media-preview/media-preview.component';
1415
import { InferenceOpacityProvider } from './media-preview/providers/inference-opacity-provider.component';
1516
import { MediaItem } from './types';
1617
import { REQUIRED_NUMBER_OF_NORMAL_IMAGES_TO_TRIGGER_TRAINING } from './utils';
1718

18-
interface DatasetItemProps {
19-
mediaItems: MediaItem[];
20-
}
21-
2219
const layoutOptions = {
2320
maxColumns: 4,
2421
minSpace: new Size(8, 8),
2522
minItemSize: new Size(120, 120),
2623
preserveAspectRatio: true,
2724
};
2825

29-
export const DatasetList = ({ mediaItems }: DatasetItemProps) => {
26+
export const DatasetList = () => {
27+
const { mediaItems, isFetchingNextPage, fetchNextPage } = useGetMediaItems();
3028
const [selectedMediaItemId, setSelectedMediaItem] = useQueryState('selectedMediaItem');
3129
//TODO: revisit implementation when dataset loading is paginated
3230
const selectedMediaItem = mediaItems.find((item) => item.id === selectedMediaItemId);
@@ -58,6 +56,8 @@ export const DatasetList = ({ mediaItems }: DatasetItemProps) => {
5856
ariaLabel='sidebar-items'
5957
selectionMode='single'
6058
layoutOptions={layoutOptions}
59+
isLoadingMore={isFetchingNextPage}
60+
onLoadMore={fetchNextPage}
6161
onSelectionChange={handleSelectionChange}
6262
contentItem={(mediaItem) =>
6363
mediaItem.filename === 'placeholder' ? (

application/ui/src/features/inspect/dataset/dataset.component.tsx

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,12 @@
11
import { Suspense } from 'react';
22

3-
import { $api } from '@geti-inspect/api';
4-
import { useProjectIdentifier } from '@geti-inspect/hooks';
53
import { Flex, Heading, Loading, View } from '@geti/ui';
64

75
import { TrainModelButton } from '../train-model/train-model-button.component';
86
import { DatasetList } from './dataset-list.component';
97
import { UploadImages } from './upload-images.component';
108

11-
const useMediaItems = () => {
12-
const { projectId } = useProjectIdentifier();
13-
14-
const { data } = $api.useSuspenseQuery('get', '/api/projects/{project_id}/images', {
15-
params: { path: { project_id: projectId } },
16-
});
17-
18-
return {
19-
mediaItems: data.media,
20-
};
21-
};
22-
239
export const Dataset = () => {
24-
const { mediaItems } = useMediaItems();
2510
return (
2611
<Flex direction={'column'} height={'100%'}>
2712
<Heading margin={0}>
@@ -36,7 +21,7 @@ export const Dataset = () => {
3621
<Suspense fallback={<Loading mode={'inline'} />}>
3722
<View flex={1} padding={'size-300'}>
3823
<Flex direction={'column'} height={'100%'} gap={'size-300'}>
39-
<DatasetList mediaItems={mediaItems} />
24+
<DatasetList />
4025
</Flex>
4126
</View>
4227
</Suspense>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { $api } from '@geti-inspect/api';
2+
import { useProjectIdentifier } from '@geti-inspect/hooks';
3+
4+
const mediaItemsLimit = 20;
5+
6+
export const useGetMediaItems = () => {
7+
const { projectId } = useProjectIdentifier();
8+
9+
const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = $api.useInfiniteQuery(
10+
'get',
11+
'/api/projects/{project_id}/images',
12+
{
13+
params: {
14+
query: { offset: 0, limit: mediaItemsLimit },
15+
path: { project_id: projectId },
16+
},
17+
},
18+
{
19+
pageParamName: 'offset',
20+
getNextPageParam: ({
21+
pagination,
22+
}: {
23+
pagination: { offset: number; limit: number; count: number; total: number };
24+
}) => {
25+
const total = pagination.offset + pagination.count;
26+
27+
if (total >= pagination.total) {
28+
return undefined;
29+
}
30+
31+
return pagination.offset + mediaItemsLimit;
32+
},
33+
}
34+
);
35+
36+
const mediaItems = data?.pages.flatMap((page) => page.media) ?? [];
37+
38+
return { mediaItems, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage };
39+
};

application/ui/src/features/inspect/projects-management/project-list-item/project-list-item.component.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { Flex, PhotoPlaceholder, Text } from '@geti/ui';
99
import { clsx } from 'clsx';
1010
import { useNavigate } from 'react-router';
1111

12-
import { useWebRTCConnection } from '../../../../components/stream/web-rtc-connection-provider';
1312
import { paths } from '../../../../routes/paths';
1413
import { ProjectEdition } from './project-edition/project-edition.component';
1514
import { ProjectActions } from './project-list-actions/project-list-actions.component';
@@ -27,7 +26,6 @@ interface ProjectListItemProps {
2726

2827
export const ProjectListItem = ({ project, isActive, isInEditMode, setProjectInEdition }: ProjectListItemProps) => {
2928
const navigate = useNavigate();
30-
const { stop } = useWebRTCConnection();
3129

3230
const updateProject = $api.useMutation('patch', '/api/projects/{project_id}', {
3331
onSettled: () => setProjectInEdition(null),
@@ -50,7 +48,6 @@ export const ProjectListItem = ({ project, isActive, isInEditMode, setProjectInE
5048
return;
5149
}
5250

53-
stop();
5451
navigate(`${paths.project({ projectId: project.id })}?mode=Dataset`);
5552
};
5653

application/ui/src/features/inspect/projects-management/project-list-item/project-list-item.test.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ vi.mock('react-router', async () => {
2020
};
2121
});
2222

23-
const mockStop = vi.fn();
2423
const mockNavigate = vi.fn();
2524

2625
describe('ProjectListItem', () => {
@@ -33,7 +32,7 @@ describe('ProjectListItem', () => {
3332
vi.clearAllMocks();
3433
vi.mocked(useNavigate).mockReturnValue(mockNavigate);
3534
vi.mocked(useWebRTCConnection).mockReturnValue({
36-
stop: mockStop,
35+
stop: vi.fn(),
3736
start: vi.fn(),
3837
status: 'idle',
3938
webRTCConnectionRef: { current: null },
@@ -54,7 +53,6 @@ describe('ProjectListItem', () => {
5453

5554
await userEvent.click(screen.getByRole('listitem'));
5655

57-
expect(mockStop).toHaveBeenCalled();
5856
expect(mockNavigate).toHaveBeenCalledWith('/projects/project-123?mode=Dataset');
5957
});
6058

application/ui/src/features/inspect/toolbar/inference-devices/inference-devices.component.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const InferenceDevices = () => {
4545
isQuiet
4646
width='auto'
4747
label='Inference devices: '
48+
aria-label='inference devices'
4849
labelAlign='end'
4950
labelPosition='side'
5051
items={options}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { ThemeProvider } from '@geti/ui/theme';
2+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3+
import { render, screen, waitFor } from '@testing-library/react';
4+
import { MemoryRouter, Route, Routes } from 'react-router-dom';
5+
import { SchemaPipeline } from 'src/api/openapi-spec';
6+
import { http } from 'src/api/utils';
7+
import { server } from 'src/msw-node-setup';
8+
9+
import { getMockedPipeline } from '../../../../mocks/mock-pipeline';
10+
import { Toolbar } from './toolbar';
11+
12+
describe('Toolbar', () => {
13+
const renderApp = ({ pipelineConfig = {} }: { pipelineConfig?: Partial<SchemaPipeline> }) => {
14+
server.use(
15+
http.get('/api/projects/{project_id}/pipeline', ({ response }) =>
16+
response(200).json(getMockedPipeline(pipelineConfig))
17+
)
18+
);
19+
20+
return render(
21+
<ThemeProvider>
22+
<QueryClientProvider client={new QueryClient()}>
23+
<MemoryRouter initialEntries={['/projects/123/inspect']}>
24+
<Routes>
25+
<Route path='/projects/:projectId/inspect' element={<Toolbar />} />
26+
</Routes>
27+
</MemoryRouter>
28+
</QueryClientProvider>
29+
</ThemeProvider>
30+
);
31+
};
32+
33+
describe('InferenceDevices', () => {
34+
it.only('renders when model is configured', async () => {
35+
renderApp({
36+
pipelineConfig: {
37+
model: {
38+
id: '1',
39+
name: 'test-model',
40+
format: 'onnx',
41+
project_id: '123',
42+
threshold: 0.5,
43+
is_ready: true,
44+
train_job_id: 'train-job-1',
45+
dataset_snapshot_id: '',
46+
},
47+
},
48+
});
49+
50+
expect(await screen.findByRole('button', { name: /inference devices/i })).toBeVisible();
51+
});
52+
53+
it('does not render when no model is configured', async () => {
54+
renderApp({
55+
pipelineConfig: {
56+
model: undefined,
57+
},
58+
});
59+
60+
await waitFor(() => {
61+
expect(screen.queryByRole('button', { name: /inference devices/i })).not.toBeInTheDocument();
62+
});
63+
});
64+
});
65+
});

application/ui/src/features/inspect/toolbar/toolbar.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
// Copyright (C) 2025 Intel Corporation
22
// SPDX-License-Identifier: Apache-2.0
33

4+
import { usePipeline } from '@geti-inspect/hooks';
45
import { dimensionValue, Divider, Flex, View } from '@geti/ui';
6+
import { isNil } from 'lodash-es';
57

68
import { InferenceDevices } from './inference-devices/inference-devices.component';
79
import { PipelineConfiguration } from './pipeline-configuration.component';
810

911
export const Toolbar = () => {
12+
const { data: pipeline } = usePipeline();
13+
14+
const hasModel = !isNil(pipeline?.model?.id);
15+
1016
return (
1117
<View
1218
gridArea='toolbar'
@@ -16,8 +22,12 @@ export const Toolbar = () => {
1622
>
1723
<Flex height='100%' gap='size-200' alignItems={'center'}>
1824
<Flex marginStart='auto' alignItems={'center'} gap={'size-200'}>
19-
<Divider size={'S'} orientation={'vertical'} />
20-
<InferenceDevices />
25+
{hasModel && (
26+
<>
27+
<Divider size={'S'} orientation={'vertical'} />
28+
<InferenceDevices />
29+
</>
30+
)}
2131
<Divider size={'S'} orientation={'vertical'} />
2232
<PipelineConfiguration />
2333
</Flex>

0 commit comments

Comments
 (0)