Skip to content

Commit ea4d226

Browse files
camiloHimuramaxxgx
authored andcommitted
feat(inspect): Ux updates - p1 (#3142)
* update thubnail loader Signed-off-by: Colorado, Camilo <camilo.colorado@intel.com> * remove model selector from the toolbar Signed-off-by: Colorado, Camilo <camilo.colorado@intel.com> * move device picket to toolbar Signed-off-by: Colorado, Camilo <camilo.colorado@intel.com> * auto-activate pipeline Signed-off-by: Colorado, Camilo <camilo.colorado@intel.com> * update capture endpoint Signed-off-by: Colorado, Camilo <camilo.colorado@intel.com> --------- Signed-off-by: Colorado, Camilo <camilo.colorado@intel.com>
1 parent 840f82f commit ea4d226

File tree

15 files changed

+231
-229
lines changed

15 files changed

+231
-229
lines changed
Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { useEffect } from 'react';
1+
import { useState } from 'react';
22

33
import { Skeleton } from '@geti/ui';
4-
import { useQuery } from '@tanstack/react-query';
54
import { clsx } from 'clsx';
65

76
import { useInference } from '../../inference-provider.component';
@@ -15,31 +14,27 @@ interface DatasetItemProps {
1514
mediaItem: MediaItem;
1615
}
1716

17+
const RETRY_LIMIT = 3;
18+
1819
export const DatasetItem = ({ mediaItem }: DatasetItemProps) => {
19-
const { selectedMediaItem, onSetSelectedMediaItem } = useSelectedMediaItem();
20+
const [retry, setRetry] = useState(0);
21+
const [isLoading, setIsLoading] = useState(true);
2022
const { onInference, selectedModelId } = useInference();
23+
const { selectedMediaItem, onSetSelectedMediaItem } = useSelectedMediaItem();
2124

2225
const isSelected = selectedMediaItem?.id === mediaItem.id;
2326

24-
const { data: thumbnailBlob, isLoading } = useQuery({
25-
queryKey: ['media', mediaItem.id],
26-
queryFn: async () => {
27-
const response = await fetch(`/api/projects/${mediaItem.project_id}/images/${mediaItem.id}/thumbnail`);
27+
const mediaUrl = `/api/projects/${mediaItem.project_id}/images/${mediaItem.id}/thumbnail?retry=${retry}`;
2828

29-
if (!response.ok) {
30-
throw new Error('Network response was not ok');
31-
}
32-
return URL.createObjectURL(await response.blob());
33-
},
34-
retry: 3,
35-
retryDelay: (attemptIndex: number) => Math.min(1000 * 2 ** attemptIndex, 10000),
36-
});
29+
const handleError = () => {
30+
if (retry < RETRY_LIMIT) {
31+
setRetry((current) => current + 1);
32+
}
33+
};
3734

38-
useEffect(() => {
39-
return () => {
40-
thumbnailBlob && URL.revokeObjectURL(thumbnailBlob);
41-
};
42-
}, [thumbnailBlob]);
35+
const handleLoad = () => {
36+
setIsLoading(false);
37+
};
4338

4439
const handleClick = async () => {
4540
const selection = mediaItem.id === selectedMediaItem?.id ? undefined : mediaItem;
@@ -50,21 +45,17 @@ export const DatasetItem = ({ mediaItem }: DatasetItemProps) => {
5045

5146
return (
5247
<div className={clsx(styles.datasetItem, { [styles.datasetItemSelected]: isSelected })} onClick={handleClick}>
53-
{isLoading || !thumbnailBlob ? (
54-
<Skeleton width={'100%'} height={'100%'} />
55-
) : (
56-
<>
57-
<img src={thumbnailBlob} alt={mediaItem.filename} />
58-
<div className={clsx(styles.floatingContainer, styles.rightTopElement)}>
59-
<DeleteMediaItem
60-
itemsIds={[String(mediaItem.id)]}
61-
onDeleted={() => {
62-
selectedMediaItem?.id === mediaItem.id && onSetSelectedMediaItem(undefined);
63-
}}
64-
/>
65-
</div>
66-
</>
67-
)}
48+
{isLoading && <Skeleton width={'100%'} height={'100%'} UNSAFE_className={styles.loader} />}
49+
50+
<img src={mediaUrl} alt={mediaItem.filename} onError={handleError} onLoad={handleLoad} />
51+
<div className={clsx(styles.floatingContainer, styles.rightTopElement)}>
52+
<DeleteMediaItem
53+
itemsIds={[String(mediaItem.id)]}
54+
onDeleted={() => {
55+
selectedMediaItem?.id === mediaItem.id && onSetSelectedMediaItem(undefined);
56+
}}
57+
/>
58+
</div>
6859
</div>
6960
);
7061
};

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,10 @@
4343
top: var(--spectrum-global-dimension-size-50);
4444
right: var(--spectrum-global-dimension-size-50);
4545
}
46+
47+
.loader {
48+
top: 0px;
49+
left: 0px;
50+
position: absolute;
51+
background-color: var(--spectrum-global-color-gray-100);
52+
}

application/ui/src/features/inspect/dataset/delete-dataset-item/alert-dialog-content.component.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,11 @@
11
import { AlertDialog, Text } from '@geti/ui';
22

3-
import { useEventListener } from '../../../../hooks/event-listener/event-listener.hook';
4-
53
type AlertDialogContentProps = {
64
itemsIds: string[];
75
onPrimaryAction: () => void;
86
};
97

108
export const AlertDialogContent = ({ itemsIds, onPrimaryAction }: AlertDialogContentProps) => {
11-
useEventListener('keydown', (event) => {
12-
if (event.key === 'Enter') {
13-
event.preventDefault();
14-
onPrimaryAction();
15-
}
16-
});
17-
189
return (
1910
<AlertDialog
2011
maxHeight={'size-6000'}
@@ -23,6 +14,7 @@ export const AlertDialogContent = ({ itemsIds, onPrimaryAction }: AlertDialogCon
2314
primaryActionLabel='Confirm'
2415
secondaryActionLabel='Close'
2516
onPrimaryAction={onPrimaryAction}
17+
autoFocusButton='primary'
2618
>
2719
<Text>{`Are you sure you want to delete ${itemsIds.length} item(s)?`}</Text>
2820
</AlertDialog>

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

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

4-
import { Suspense } from 'react';
4+
import { Suspense, useEffect } from 'react';
55

66
import { $api } from '@geti-inspect/api';
7-
import { SchemaJob as Job, SchemaJob } from '@geti-inspect/api/spec';
7+
import { SchemaJob as Job } from '@geti-inspect/api/spec';
88
import { useProjectIdentifier } from '@geti-inspect/hooks';
9-
import { Flex, ProgressBar, Text, View } from '@geti/ui';
10-
import { CanceledIcon, WaitingIcon } from '@geti/ui/icons';
11-
import { queryOptions, experimental_streamedQuery as streamedQuery, useQuery } from '@tanstack/react-query';
12-
import { fetchSSE } from 'src/api/fetch-sse';
9+
import { Flex, Loading, Text, View } from '@geti/ui';
10+
import { WaitingIcon } from '@geti/ui/icons';
11+
12+
import { useTrainedModels } from '../../../hooks/use-model';
13+
import { useInference } from '../inference-provider.component';
14+
import { TrainingStatusItem } from './training-status-item.component';
1315

1416
const IdleItem = () => {
17+
const models = useTrainedModels();
18+
const { selectedModelId, onSetSelectedModelId } = useInference();
19+
20+
useEffect(() => {
21+
if (selectedModelId !== undefined || models.length === 0) {
22+
return;
23+
}
24+
25+
onSetSelectedModelId(models[0].id);
26+
}, [selectedModelId, models, onSetSelectedModelId]);
27+
1528
return (
1629
<Flex
1730
alignItems='center'
@@ -31,115 +44,6 @@ const IdleItem = () => {
3144
);
3245
};
3346

34-
const getStyleForMessage = (message: string) => {
35-
if (message.toLowerCase().includes('valid')) {
36-
return {
37-
backgroundColor: 'var(--spectrum-global-color-yellow-600)',
38-
color: '#000',
39-
};
40-
} else if (message.toLowerCase().includes('test')) {
41-
return {
42-
backgroundColor: 'var(--spectrum-global-color-green-600)',
43-
color: '#fff',
44-
};
45-
} else if (message.toLowerCase().includes('train') || message.toLowerCase().includes('fit')) {
46-
return {
47-
backgroundColor: 'var(--spectrum-global-color-blue-600)',
48-
color: '#fff',
49-
};
50-
}
51-
52-
return {
53-
backgroundColor: 'var(--spectrum-global-color-blue-600)',
54-
color: '#fff',
55-
};
56-
};
57-
58-
const TrainingStatusItem = ({ trainingJob }: { trainingJob: SchemaJob }) => {
59-
// Cancel training job
60-
const cancelJobMutation = $api.useMutation('post', '/api/jobs/{job_id}:cancel');
61-
const handleCancel = async () => {
62-
try {
63-
if (trainingJob.id === undefined) {
64-
throw Error('TODO: jobs should always have an ID');
65-
}
66-
67-
console.info('Cancel training');
68-
await cancelJobMutation.mutateAsync({
69-
params: {
70-
path: {
71-
job_id: trainingJob.id,
72-
},
73-
},
74-
});
75-
console.info('Job cancelled successfully');
76-
} catch (error) {
77-
console.error('Failed to cancel job:', error);
78-
}
79-
};
80-
81-
const progressQuery = useQuery(
82-
queryOptions({
83-
queryKey: ['get', '/api/jobs/{job_id}/progress', trainingJob.id],
84-
queryFn: streamedQuery({
85-
queryFn: () => fetchSSE(`/api/jobs/${trainingJob.id}/progress`),
86-
maxChunks: 1,
87-
}),
88-
staleTime: Infinity,
89-
})
90-
);
91-
92-
// Get the job progress and message from the last SSE message, or fallback
93-
const lastJobProgress = progressQuery.data?.at(-1);
94-
const progress = lastJobProgress?.progress ?? trainingJob.progress;
95-
const message = lastJobProgress?.message ?? trainingJob.message;
96-
97-
const { backgroundColor, color } = getStyleForMessage(message);
98-
99-
return (
100-
<div
101-
style={{
102-
height: '100%',
103-
display: 'flex',
104-
flexDirection: 'column',
105-
justifyContent: 'center',
106-
alignItems: 'center',
107-
backgroundColor,
108-
}}
109-
>
110-
<Flex direction='row' alignItems='center' width='100px' justifyContent='space-between'>
111-
<button
112-
onClick={() => {
113-
handleCancel();
114-
}}
115-
style={{
116-
background: 'none',
117-
border: 'none',
118-
cursor: 'pointer',
119-
}}
120-
>
121-
<CanceledIcon height='14px' width='14px' stroke={color} />
122-
</button>
123-
<Text
124-
UNSAFE_style={{
125-
fontSize: '12px',
126-
marginBottom: '4px',
127-
marginRight: '4px',
128-
textAlign: 'center',
129-
color,
130-
textOverflow: 'ellipsis',
131-
overflow: 'hidden',
132-
whiteSpace: 'nowrap',
133-
}}
134-
>
135-
{message}
136-
</Text>
137-
</Flex>
138-
<ProgressBar value={progress} aria-label={message} width='100px' showValueLabel={false} />
139-
</div>
140-
);
141-
};
142-
14347
const useCurrentJob = () => {
14448
const { data: jobsData } = $api.useSuspenseQuery('get', '/api/jobs', undefined, {
14549
refetchInterval: 5000,
@@ -166,7 +70,7 @@ export const ProgressBarItem = () => {
16670
export const Footer = () => {
16771
return (
16872
<View gridArea={'footer'} backgroundColor={'gray-100'} width={'100%'} height={'size-400'} overflow={'hidden'}>
169-
<Suspense>
73+
<Suspense fallback={<Loading mode={'inline'} size='S' />}>
17074
<ProgressBarItem />
17175
</Suspense>
17276
</View>

0 commit comments

Comments
 (0)