Skip to content

Commit 489ab80

Browse files
authored
4699 - render dataset items from api - P1 (#4725)
1 parent ddee7a4 commit 489ab80

15 files changed

+246
-74
lines changed

ui/src/features/annotator/annotator-canvas.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@ import { CSSProperties, MouseEvent } from 'react';
66
import { Grid, View } from '@geti/ui';
77
import { isEmpty } from 'lodash-es';
88

9-
import thumbnailUrl from '../../assets/mocked-project-thumbnail.png';
109
import { ZoomProvider } from '../../components/zoom/zoom';
1110
import { ZoomTransform } from '../../components/zoom/zoom-transform';
12-
import { response } from '../dataset/mock-response';
11+
import { useProjectIdentifier } from '../../hooks/use-project-identifier.hook';
12+
import { getImageUrl } from '../dataset/gallery/utils';
1313
import { Annotations } from './annotations/annotations.component';
1414
import { useSelectedAnnotations } from './select-annotation-provider.component';
1515
import { ToolManager } from './tools/tool-manager.component';
16-
17-
type Item = (typeof response.items)[number];
16+
import { Annotation, DatasetItem } from './types';
1817

1918
const DEFAULT_ANNOTATION_STYLES = {
2019
fillOpacity: 0.4,
@@ -27,10 +26,16 @@ const DEFAULT_ANNOTATION_STYLES = {
2726
strokeOpacity: 'var(--annotation-border-opacity, 1)',
2827
} satisfies CSSProperties;
2928

30-
export const AnnotatorCanvas = ({ mediaItem, isFocussed }: { mediaItem: Item; isFocussed: boolean }) => {
29+
type AnnotatorCanvasProps = {
30+
mediaItem: DatasetItem;
31+
isFocussed: boolean;
32+
};
33+
export const AnnotatorCanvas = ({ mediaItem, isFocussed }: AnnotatorCanvasProps) => {
3134
const { setSelectedAnnotations } = useSelectedAnnotations();
32-
35+
const project_id = useProjectIdentifier();
3336
const size = { width: mediaItem.width, height: mediaItem.height };
37+
// todo: pass media annotations
38+
const annotations: Annotation[] = [];
3439

3540
const handleClickOutside = (e: MouseEvent<SVGSVGElement>): void => {
3641
if (e.target === e.currentTarget) {
@@ -43,10 +48,10 @@ export const AnnotatorCanvas = ({ mediaItem, isFocussed }: { mediaItem: Item; is
4348
<ZoomTransform target={size}>
4449
<Grid areas={['innercanvas']} width={'100%'} height='100%'>
4550
<View gridArea={'innercanvas'}>
46-
<img src={thumbnailUrl} alt='Collected data' />
51+
<img src={getImageUrl(project_id, String(mediaItem.id))} alt='Collected data' />
4752
</View>
4853

49-
{!isEmpty(mediaItem.annotations) && (
54+
{!isEmpty(annotations) && (
5055
<View gridArea={'innercanvas'}>
5156
<svg
5257
width={size.width}

ui/src/features/annotator/annotator-provider.component.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { createContext, Dispatch, ReactNode, SetStateAction, useContext, useStat
66
import { v4 as uuid } from 'uuid';
77

88
import { ToolType } from '../../components/tool-selection-bar/tools/interface';
9-
import { Annotation, MediaItem, Shape } from './types';
9+
import { Annotation, DatasetItem, Shape } from './types';
1010

1111
type AnnotatorContext = {
1212
activeTool: ToolType | null;
@@ -15,15 +15,16 @@ type AnnotatorContext = {
1515
addAnnotation: (shape: Shape) => void;
1616
updateAnnotation: (updatedAnnotation: Annotation) => void;
1717

18-
mediaItem: MediaItem;
18+
mediaItem: DatasetItem;
1919
annotations: Annotation[];
2020
};
2121

2222
export const AnnotatorProviderContext = createContext<AnnotatorContext | null>(null);
2323

24-
export const AnnotatorProvider = ({ mediaItem, children }: { mediaItem: MediaItem; children: ReactNode }) => {
24+
export const AnnotatorProvider = ({ mediaItem, children }: { mediaItem: DatasetItem; children: ReactNode }) => {
2525
const [activeTool, setActiveTool] = useState<ToolType>('selection');
26-
const [annotations, setAnnotations] = useState<Annotation[]>(mediaItem.annotations);
26+
// todo: pass media annotations
27+
const [annotations, setAnnotations] = useState<Annotation[]>([]);
2728

2829
const updateAnnotation = (updatedAnnotation: Annotation) => {
2930
const { id } = updatedAnnotation;

ui/src/features/annotator/types.ts

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

4+
import { components } from '../../api/openapi-spec';
5+
46
export interface RegionOfInterest {
57
x: number;
68
y: number;
@@ -43,18 +45,6 @@ export type AnnotationState = {
4345
isLocked: boolean;
4446
};
4547

46-
export type MediaItem = {
47-
id: string;
48-
original_name: string;
49-
format: string;
50-
width: number;
51-
height: number;
52-
size: number;
53-
thumbhash: string;
54-
created_at: string;
55-
annotations: Annotation[];
56-
};
57-
5848
// Circle is only used for visual purposes on segment-anything tool
5949
export interface Circle {
6050
readonly shapeType: 'circle';
@@ -67,3 +57,4 @@ export interface ClipperPoint {
6757
X: number;
6858
Y: number;
6959
}
60+
export type DatasetItem = components['schemas']['DatasetItem'];

ui/src/features/dataset/gallery/gallery.component.tsx

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

4-
import { useState } from 'react';
4+
import { useRef, useState } from 'react';
55

6-
import { AriaComponentsListBox, DialogContainer, GridLayout, ListBoxItem, Size, View, Virtualizer } from '@geti/ui';
6+
import {
7+
AriaComponentsListBox,
8+
DialogContainer,
9+
GridLayout,
10+
ListBoxItem,
11+
Loading,
12+
Size,
13+
View,
14+
Virtualizer,
15+
} from '@geti/ui';
16+
import { useLoadMore } from '@react-aria/utils';
717

8-
import thumbnailUrl from '../../../assets/mocked-project-thumbnail.png';
18+
import { useProjectIdentifier } from '../../../hooks/use-project-identifier.hook';
919
import { useSelectedData } from '../../../routes/dataset/provider';
20+
import { DatasetItem } from '../../annotator/types';
1021
import { CheckboxInput } from '../checkbox-input';
1122
import { MediaPreview } from '../media-preview/media-preview.component';
12-
import { response } from '../mock-response';
1323
import { AnnotationStateIcon } from './annotation-state-icon.component';
1424
import { MediaItem } from './media-item.component';
25+
import { MediaThumbnail } from './media-thumbnail.component';
26+
import { getThumbnailUrl } from './utils';
1527

1628
import classes from './gallery.module.scss';
1729

30+
type GalleryProps = {
31+
items: DatasetItem[];
32+
fetchNextPage: () => void;
33+
hasNextPage: boolean;
34+
isFetchingNextPage: boolean;
35+
};
36+
1837
const layoutOptions = {
1938
minSpace: new Size(8, 8),
2039
maxColumns: 8,
2140
preserveAspectRatio: true,
2241
};
2342

24-
type Item = (typeof response.items)[number];
43+
export const Gallery = ({ items, hasNextPage, isFetchingNextPage, fetchNextPage }: GalleryProps) => {
44+
const ref = useRef<HTMLDivElement | null>(null);
45+
const project_id = useProjectIdentifier();
2546

26-
type MediaThumbnailProps = {
27-
onDoubleClick: () => void;
28-
url: string;
29-
alt: string;
30-
};
31-
const MediaThumbnail = ({ onDoubleClick, url, alt }: MediaThumbnailProps) => {
32-
return (
33-
<div onDoubleClick={onDoubleClick}>
34-
<img src={url} alt={alt} style={{ objectFit: 'cover', width: '100%', height: '100%' }} />
35-
</div>
36-
);
37-
};
38-
39-
export const Gallery = () => {
40-
const [selectedMediaItem, setSelectedMediaItem] = useState<null | Item>(null);
47+
const [selectedMediaItem, setSelectedMediaItem] = useState<null | DatasetItem>(null);
4148
const { selectedKeys, mediaState, setSelectedKeys } = useSelectedData();
49+
4250
const isSetSelectedKeys = selectedKeys instanceof Set;
4351

52+
useLoadMore(
53+
{
54+
isLoading: isFetchingNextPage,
55+
onLoadMore: () => hasNextPage && fetchNextPage(),
56+
},
57+
ref
58+
);
59+
4460
return (
4561
<View UNSAFE_className={classes.mainContainer}>
4662
<Virtualizer layout={GridLayout} layoutOptions={layoutOptions}>
4763
<AriaComponentsListBox
64+
ref={ref}
4865
layout='grid'
4966
aria-label='data-collection-grid'
5067
className={classes.container}
5168
selectedKeys={selectedKeys}
5269
selectionMode={'multiple'}
5370
onSelectionChange={setSelectedKeys}
5471
>
55-
{response.items.map((item) => (
72+
{items.map((item) => (
5673
<ListBoxItem
5774
id={item.id}
5875
key={item.id}
5976
textValue={item.id}
6077
className={classes.mediaItem}
61-
data-accepted={mediaState.get(item.id) === 'accepted'}
62-
data-rejected={mediaState.get(item.id) === 'rejected'}
78+
data-accepted={mediaState.get(String(item.id)) === 'accepted'}
79+
data-rejected={mediaState.get(String(item.id)) === 'rejected'}
6380
>
6481
<MediaItem
6582
contentElement={() => (
6683
<MediaThumbnail
84+
alt={item.name}
85+
url={getThumbnailUrl(project_id, String(item.id))}
6786
onDoubleClick={() => setSelectedMediaItem(item)}
68-
url={thumbnailUrl}
69-
alt={item.original_name}
7087
/>
7188
)}
72-
topRightElement={() => <AnnotationStateIcon state={mediaState.get(item.id)} />}
89+
topRightElement={() => <AnnotationStateIcon state={mediaState.get(String(item.id))} />}
7390
topLeftElement={() => (
7491
<CheckboxInput
7592
isReadOnly
7693
name={`select-${item.id}`}
77-
isChecked={isSetSelectedKeys && selectedKeys.has(item.id)}
94+
isChecked={isSetSelectedKeys && selectedKeys.has(String(item.id))}
7895
/>
7996
)}
8097
/>
8198
</ListBoxItem>
8299
))}
100+
{isFetchingNextPage && (
101+
<ListBoxItem id={'loader'} textValue={'loading'}>
102+
<Loading mode='overlay' />
103+
</ListBoxItem>
104+
)}
83105
</AriaComponentsListBox>
84106
</Virtualizer>
85107

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (C) 2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
type MediaThumbnailProps = {
5+
onDoubleClick: () => void;
6+
url: string;
7+
alt: string;
8+
};
9+
10+
export const MediaThumbnail = ({ onDoubleClick, url, alt }: MediaThumbnailProps) => {
11+
return (
12+
<div onDoubleClick={onDoubleClick}>
13+
<img src={url} alt={alt} style={{ objectFit: 'cover', width: '100%', height: '100%' }} />
14+
</div>
15+
);
16+
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (C) 2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { $api } from '../../../api/client';
5+
import { useProjectIdentifier } from '../../../hooks/use-project-identifier.hook';
6+
7+
const datasetItemsLimit = 20;
8+
9+
export const useGetDatasetItems = () => {
10+
const project_id = useProjectIdentifier();
11+
12+
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = $api.useInfiniteQuery(
13+
'get',
14+
'/api/projects/{project_id}/dataset/items',
15+
{
16+
params: {
17+
query: { offset: 0, limit: datasetItemsLimit },
18+
path: { project_id },
19+
},
20+
},
21+
{
22+
pageParamName: 'offset',
23+
getNextPageParam: ({
24+
pagination,
25+
}: {
26+
pagination: { offset: number; limit: number; count: number; total: number };
27+
}) => {
28+
const total = pagination.offset + pagination.count;
29+
30+
if (total >= pagination.total) {
31+
return undefined;
32+
}
33+
34+
return pagination.offset + datasetItemsLimit;
35+
},
36+
}
37+
);
38+
39+
const items = data?.pages.flatMap((page) => page.items) ?? [];
40+
41+
return { items, fetchNextPage, hasNextPage, isFetchingNextPage };
42+
};

ui/src/features/dataset/gallery/utils.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@
33

44
import { API_BASE_URL } from '../../../api/client';
55

6-
const getBaseUrl = (src: string) => `${API_BASE_URL}/api/data-collection/${src}`;
6+
const getBaseUrl = (projectId: string, itemId: string) =>
7+
`${API_BASE_URL}/api/projects/${projectId}/dataset/items/${itemId}`;
78

8-
export const getImageUrl = (src: string) => {
9-
return `${getBaseUrl(src)}/image`;
9+
export const getImageUrl = (projectId: string, itemId: string) => {
10+
return `${getBaseUrl(projectId, itemId)}/binary`;
1011
};
1112

12-
export const getThumbnailUrl = (src: string) => {
13-
return `${getImageUrl(src)}-thumbnail`;
13+
export const getThumbnailUrl = (projectId: string, itemId: string) => {
14+
return `${getBaseUrl(projectId, itemId)}/thumbnail`;
1415
};
1516

16-
export const getPredictionThumbnailUrl = (src: string) => {
17-
return `${getBaseUrl(src)}/prediction-thumbnail`;
17+
export const getPredictionThumbnailUrl = (projectId: string, itemId: string) => {
18+
return `${getBaseUrl(projectId, itemId)}/thumbnail`;
1819
};

ui/src/features/dataset/media-preview/media-preview.component.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import { AnnotatorCanvas } from '../../annotator/annotator-canvas';
99
import { AnnotatorProvider } from '../../annotator/annotator-provider.component';
1010
import { SelectAnnotationProvider } from '../../annotator/select-annotation-provider.component';
1111
import { ToolSelectionBar } from '../../annotator/tools/tool-selection-bar.component';
12-
import { response } from '../mock-response';
12+
import { DatasetItem } from '../../annotator/types';
1313

14-
type Item = (typeof response.items)[number];
15-
16-
export const MediaPreview = ({ mediaItem, close }: { mediaItem: Item; close: () => void }) => {
14+
type MediaPreviewProps = {
15+
mediaItem: DatasetItem;
16+
close: () => void;
17+
};
18+
export const MediaPreview = ({ mediaItem, close }: MediaPreviewProps) => {
1719
const [isFocussed, setIsFocussed] = useState(false);
1820

1921
return (
@@ -30,11 +32,11 @@ export const MediaPreview = ({ mediaItem, close }: { mediaItem: Item; close: ()
3032
areas={['toolbar canvas aside', 'toolbar canvas aside', 'toolbar footer aside']}
3133
width={'100%'}
3234
height='100%'
33-
columns={'auto 1fr auto'}
35+
columns={'100px calc(100% - 200px) 100px'}
3436
rows={'auto 1fr auto'}
3537
>
3638
<AnnotatorProvider mediaItem={mediaItem}>
37-
<View gridArea={'toolbar'} margin={'size-350'}>
39+
<View gridArea={'toolbar'}>
3840
<ToolSelectionBar />
3941
</View>
4042

0 commit comments

Comments
 (0)