Skip to content

Commit 3c79f7d

Browse files
authored
feat(inspect): dataset list virtualization (#3191)
* 3173 remove statistics tab Signed-off-by: Colorado, Camilo <[email protected]> * rename project Signed-off-by: Colorado, Camilo <[email protected]> * dataset virtualization Signed-off-by: Colorado, Camilo <[email protected]> * dataset infinite scroll Signed-off-by: Colorado, Camilo <[email protected]> * eslint fixes Signed-off-by: Colorado, Camilo <[email protected]> --------- Signed-off-by: Colorado, Camilo <[email protected]>
1 parent 211baba commit 3c79f7d

30 files changed

+620
-348
lines changed

application/ui/src/components/virtualizer-grid-layout/grid-media-item/grid-media-item.module.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
line-height: 0px;
1111
padding: var(--spectrum-global-dimension-size-125);
1212
border-radius: var(--spectrum-global-dimension-size-50);
13+
background-color: var(--spectrum-gray-100);
1314

1415
&:empty {
1516
display: none;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Image } from '@geti-inspect/icons';
2+
import { Flex } from '@geti/ui';
3+
import { clsx } from 'clsx';
4+
5+
import styles from './dataset-item-placeholder.module.scss';
6+
7+
export const DatasetItemPlaceholder = () => {
8+
return (
9+
<Flex justifyContent={'center'} alignItems={'center'} UNSAFE_className={clsx(styles.datasetItemPlaceholder)}>
10+
<Image />
11+
</Flex>
12+
);
13+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.datasetItemPlaceholder {
2+
width: 100%;
3+
height: 100%;
4+
aspect-ratio: auto;
5+
border: 1px dashed var(--spectrum-global-color-gray-700);
6+
background-color: var(--spectrum-global-color-gray-200);
7+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { MediaItem } from '../types';
2+
3+
const PLACEHOLDER_FILENAME = 'placeholder';
4+
5+
export const isPlaceholderItem = (name: string): boolean => {
6+
return name.includes(PLACEHOLDER_FILENAME);
7+
};
8+
9+
export const getPlaceholderItem = (index: number): MediaItem => {
10+
return {
11+
id: `${PLACEHOLDER_FILENAME}-${index}`,
12+
filename: PLACEHOLDER_FILENAME,
13+
project_id: '',
14+
size: 0,
15+
is_anomalous: false,
16+
width: 0,
17+
height: 0,
18+
};
19+
};

application/ui/src/features/inspect/dataset/dataset-item/dataset-item-placeholder.component.tsx

Lines changed: 0 additions & 19 deletions
This file was deleted.

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

Lines changed: 0 additions & 49 deletions
This file was deleted.

application/ui/src/features/inspect/dataset/dataset-item/dataset-item.module.scss

Lines changed: 0 additions & 52 deletions
This file was deleted.

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

Lines changed: 61 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
1-
import { DialogContainer, Flex, Grid, Heading, minmax, repeat } from '@geti/ui';
1+
import { DialogContainer, Flex, Heading, Selection, Size, View } from '@geti/ui';
2+
import { isNil } from 'lodash-es';
23
import isEmpty from 'lodash-es/isEmpty';
34
import { useQueryState } from 'nuqs';
5+
import { MediaThumbnail } from 'src/components/media-thumbnail/media-thumbnail.component';
6+
import { GridMediaItem } from 'src/components/virtualizer-grid-layout/grid-media-item/grid-media-item.component';
7+
import { VirtualizerGridLayout } from 'src/components/virtualizer-grid-layout/virtualizer-grid-layout.component';
48

5-
import { DatasetItemPlaceholder } from './dataset-item/dataset-item-placeholder.component';
6-
import { DatasetItem } from './dataset-item/dataset-item.component';
9+
import { getThumbnailUrl } from '../utils';
10+
import { DatasetItemPlaceholder } from './dataset-item-placeholder/dataset-item-placeholder.component';
11+
import { getPlaceholderItem, isPlaceholderItem } from './dataset-item-placeholder/util';
12+
import { DeleteMediaItem } from './delete-dataset-item/delete-dataset-item.component';
13+
import { useGetMediaItems } from './hooks/use-get-media-items.hook';
714
import { MediaPreview } from './media-preview/media-preview.component';
815
import { InferenceOpacityProvider } from './media-preview/providers/inference-opacity-provider.component';
916
import { MediaItem } from './types';
1017
import { REQUIRED_NUMBER_OF_NORMAL_IMAGES_TO_TRIGGER_TRAINING } from './utils';
1118

12-
interface DatasetItemProps {
13-
mediaItems: MediaItem[];
14-
}
19+
const layoutOptions = {
20+
maxColumns: 4,
21+
minSpace: new Size(8, 8),
22+
minItemSize: new Size(120, 120),
23+
preserveAspectRatio: true,
24+
};
1525

16-
export const DatasetList = ({ mediaItems }: DatasetItemProps) => {
26+
export const DatasetList = () => {
27+
const { mediaItems, isFetchingNextPage, fetchNextPage } = useGetMediaItems();
1728
const [selectedMediaItemId, setSelectedMediaItem] = useQueryState('selectedMediaItem');
1829
//TODO: revisit implementation when dataset loading is paginated
1930
const selectedMediaItem = mediaItems.find((item) => item.id === selectedMediaItemId);
@@ -22,34 +33,55 @@ export const DatasetList = ({ mediaItems }: DatasetItemProps) => {
2233
...mediaItems,
2334
...Array.from({
2435
length: Math.max(0, REQUIRED_NUMBER_OF_NORMAL_IMAGES_TO_TRIGGER_TRAINING - mediaItems.length),
25-
}).map(() => undefined),
36+
}).map((_, index): MediaItem => getPlaceholderItem(index)),
2637
];
2738

39+
const handleSelectionChange = (newKeys: Selection) => {
40+
const updatedSelectedKeys = new Set(newKeys);
41+
const firstKey = updatedSelectedKeys.values().next().value;
42+
const itemId = String(firstKey);
43+
44+
if (!isNil(firstKey) && !isPlaceholderItem(itemId)) {
45+
setSelectedMediaItem(itemId);
46+
}
47+
};
48+
2849
return (
2950
<Flex gap='size-200' direction={'column'} height={'100%'}>
3051
<Heading>Normal images</Heading>
3152

32-
<Grid
33-
flex={1}
34-
gap={'size-100'}
35-
rows={['max-content', '1fr']}
36-
alignContent={'start'}
37-
columns={repeat('auto-fill', minmax('size-1600', '1fr'))}
38-
>
39-
{mediaItemsToRender.map((mediaItem, index) =>
40-
isEmpty(mediaItem) ? (
41-
<DatasetItemPlaceholder key={index} />
42-
) : (
43-
<DatasetItem
44-
key={mediaItem.id}
45-
mediaItem={mediaItem}
46-
isSelected={selectedMediaItem?.id === mediaItem.id}
47-
onClick={() => setSelectedMediaItem(mediaItem?.id ?? null)}
48-
onDeleted={() => selectedMediaItem?.id === mediaItem.id && setSelectedMediaItem(null)}
49-
/>
50-
)
51-
)}
52-
</Grid>
53+
<View width={'100%'} height={'100%'}>
54+
<VirtualizerGridLayout
55+
items={mediaItemsToRender}
56+
ariaLabel='sidebar-items'
57+
selectionMode='single'
58+
layoutOptions={layoutOptions}
59+
isLoadingMore={isFetchingNextPage}
60+
onLoadMore={fetchNextPage}
61+
onSelectionChange={handleSelectionChange}
62+
contentItem={(mediaItem) =>
63+
mediaItem.filename === 'placeholder' ? (
64+
<DatasetItemPlaceholder />
65+
) : (
66+
<GridMediaItem
67+
contentElement={() => (
68+
<MediaThumbnail
69+
alt={mediaItem.filename}
70+
url={getThumbnailUrl(mediaItem)}
71+
onClick={() => setSelectedMediaItem(mediaItem.id ?? null)}
72+
/>
73+
)}
74+
topRightElement={() => (
75+
<DeleteMediaItem
76+
itemsIds={[String(mediaItem.id)]}
77+
onDeleted={() => setSelectedMediaItem(null)}
78+
/>
79+
)}
80+
/>
81+
)
82+
}
83+
/>
84+
</View>
5385

5486
<DialogContainer onDismiss={() => setSelectedMediaItem(null)}>
5587
{!isEmpty(selectedMediaItem) && (

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+
};

0 commit comments

Comments
 (0)