Skip to content

Commit 0773110

Browse files
authored
feat(inspect): Enable/Disable inference (#3127)
* update api definitions Signed-off-by: Colorado, Camilo <camilo.colorado@intel.com> * add unit tests Signed-off-by: Colorado, Camilo <camilo.colorado@intel.com> * switch component calls :stop pipeline Signed-off-by: Colorado, Camilo <camilo.colorado@intel.com> * source sink message Signed-off-by: Colorado, Camilo <camilo.colorado@intel.com> * update unit test description Signed-off-by: Colorado, Camilo <camilo.colorado@intel.com> --------- Signed-off-by: Colorado, Camilo <camilo.colorado@intel.com>
1 parent 8adc3a1 commit 0773110

24 files changed

+522
-99
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { SchemaMediaList } from './../src/api/openapi-spec.d';
2+
3+
export const getMockedMediaItem = (
4+
data: Partial<SchemaMediaList['media'][number]>
5+
): SchemaMediaList['media'][number] => {
6+
return {
7+
id: '1',
8+
project_id: '123',
9+
filename: 'test-image.jpg',
10+
size: 1024,
11+
is_anomalous: false,
12+
width: 1920,
13+
height: 1080,
14+
created_at: '2024-01-01T00:00:00Z',
15+
...data,
16+
};
17+
};

application/ui/src/features/inspect/inference-provider.component.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,13 @@ interface InferenceProviderProps {
7070
export const InferenceProvider = ({ children }: InferenceProviderProps) => {
7171
const { data: pipeline } = usePipeline();
7272
const { projectId } = useProjectIdentifier();
73-
const updatePipeline = $api.useMutation('patch', '/api/projects/{project_id}/pipeline');
73+
const updatePipeline = $api.useMutation('patch', '/api/projects/{project_id}/pipeline', {
74+
meta: {
75+
invalidates: [
76+
['get', '/api/projects/{project_id}/pipeline', { params: { path: { project_id: projectId } } }],
77+
],
78+
},
79+
});
7480

7581
const { selectedMediaItem } = useSelectedMediaItem();
7682
const [inferenceOpacity, setInferenceOpacity] = useState<number>(0.75);
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { $api } from '@geti-inspect/api';
2-
import { useDisablePipeline, useEnablePipeline } from '@geti-inspect/hooks';
2+
import { useActivatePipeline, useDisablePipeline } from '@geti-inspect/hooks';
33
import { AlertDialog } from '@geti/ui';
44

55
interface ConfirmationDialogProps {
@@ -8,7 +8,7 @@ interface ConfirmationDialogProps {
88
}
99

1010
export const ConfirmationDialog = ({ activeProjectId, currentProjectId }: ConfirmationDialogProps) => {
11-
const enablePipeline = useEnablePipeline({});
11+
const activePipeline = useActivatePipeline({});
1212
const disablePipeline = useDisablePipeline(activeProjectId);
1313

1414
const activeProject = $api.useSuspenseQuery('get', '/api/projects/{project_id}', {
@@ -26,7 +26,7 @@ export const ConfirmationDialog = ({ activeProjectId, currentProjectId }: Confir
2626
params: { path: { project_id: activeProjectId } },
2727
});
2828

29-
await enablePipeline.mutateAsync({
29+
await activePipeline.mutateAsync({
3030
params: { path: { project_id: currentProjectId } },
3131
});
3232
};
Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { Suspense, useEffect } from 'react';
1+
import { Suspense, useEffect, useState } from 'react';
22

33
import { LinkExpired } from '@geti-inspect/icons';
4-
import { Button, DialogTrigger, Flex, Loading, Text } from '@geti/ui';
4+
import { Button, DialogContainer, Flex, Loading, Text } from '@geti/ui';
55

66
import { useWebRTCConnection } from '../../../../components/stream/web-rtc-connection-provider';
77
import { ConfirmationDialog } from './confirmation-dialog.component';
@@ -22,6 +22,7 @@ const useStopCurrentWebRtcConnection = () => {
2222
};
2323

2424
export const EnableProject = ({ activeProjectId, currentProjectId }: EnableProjectProps) => {
25+
const [isOpen, setIsOpen] = useState(false);
2526
useStopCurrentWebRtcConnection();
2627

2728
return (
@@ -36,12 +37,15 @@ export const EnableProject = ({ activeProjectId, currentProjectId }: EnableProje
3637

3738
<Text UNSAFE_className={classes.title}>Would you like to activate this project?</Text>
3839

39-
<DialogTrigger>
40-
<Button>Activate project</Button>
41-
<Suspense fallback={<Loading mode={'inline'} />}>
42-
<ConfirmationDialog activeProjectId={activeProjectId} currentProjectId={currentProjectId} />
43-
</Suspense>
44-
</DialogTrigger>
40+
<Button onPress={() => setIsOpen(true)}>Activate project</Button>
41+
42+
<DialogContainer onDismiss={() => setIsOpen(false)}>
43+
{isOpen && (
44+
<Suspense fallback={<Loading mode={'inline'} />}>
45+
<ConfirmationDialog activeProjectId={activeProjectId} currentProjectId={currentProjectId} />
46+
</Suspense>
47+
)}
48+
</DialogContainer>
4549
</Flex>
4650
</Flex>
4751
);

application/ui/src/features/inspect/media-actions/media-actions.component.tsx renamed to application/ui/src/features/inspect/main-content/main-content.component.tsx

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { $api } from '@geti-inspect/api';
21
import { useActivePipeline, usePipeline, useProjectIdentifier } from '@geti-inspect/hooks';
32
import isEmpty from 'lodash-es/isEmpty';
43

@@ -7,37 +6,20 @@ import { StreamContainer } from '../stream/stream-container';
76
import { EnableProject } from './enable-project/enable-project.component';
87
import { InferenceResult } from './inference-result/inference-result.component';
98
import { SourceSinkMessage } from './source-sink-message/source-sink-message.component';
10-
import { TrainModelMessage } from './train-model-message/train-model-message.component';
119

12-
const useIsInferenceAvailable = () => {
13-
const { projectId } = useProjectIdentifier();
14-
const { data } = $api.useQuery('get', '/api/projects/{project_id}/models', {
15-
params: { path: { project_id: projectId } },
16-
});
17-
18-
return data?.models.length !== 0;
19-
};
20-
21-
export const MediaActions = () => {
10+
export const MainContent = () => {
2211
const { data: pipeline } = usePipeline();
2312
const { projectId } = useProjectIdentifier();
2413
const { selectedMediaItem } = useSelectedMediaItem();
25-
const isInferenceAvailable = useIsInferenceAvailable();
2614
const { data: activeProjectPipeline } = useActivePipeline();
2715

28-
const hasSink = !isEmpty(pipeline.sink?.id);
29-
const hasSource = !isEmpty(pipeline.source?.id);
3016
const hasActiveProject = !isEmpty(activeProjectPipeline);
3117
const isCurrentProjectActive = activeProjectPipeline?.project_id === projectId;
3218

33-
if (isEmpty(selectedMediaItem) && (!hasSource || !hasSink)) {
19+
if (isEmpty(selectedMediaItem) && isEmpty(pipeline.source?.id)) {
3420
return <SourceSinkMessage />;
3521
}
3622

37-
if (isEmpty(selectedMediaItem) && !isInferenceAvailable) {
38-
return <TrainModelMessage />;
39-
}
40-
4123
if (isEmpty(selectedMediaItem) && hasActiveProject && !isCurrentProjectActive) {
4224
return <EnableProject currentProjectId={projectId} activeProjectId={activeProjectPipeline.project_id} />;
4325
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2+
import { render, screen } from '@testing-library/react';
3+
import { MemoryRouter, Route, Routes } from 'react-router-dom';
4+
import { SchemaPipeline } from 'src/api/openapi-spec';
5+
import { http } from 'src/api/utils';
6+
import { ZoomProvider } from 'src/components/zoom/zoom';
7+
import { server } from 'src/msw-node-setup';
8+
9+
import { getMockedMediaItem } from '../../../../mocks/mock-media-item';
10+
import { getMockedPipeline } from '../../../../mocks/mock-pipeline';
11+
import { useWebRTCConnection, WebRTCConnectionState } from '../../../components/stream/web-rtc-connection-provider';
12+
import { MediaItem } from '../dataset/types';
13+
import { useSelectedMediaItem } from '../selected-media-item-provider.component';
14+
import { MainContent } from './main-content.component';
15+
import { SOURCE_MESSAGE } from './source-sink-message/source-sink-message.component';
16+
17+
vi.mock('../../../components/stream/web-rtc-connection-provider', () => ({
18+
useWebRTCConnection: vi.fn(),
19+
}));
20+
21+
vi.mock('../selected-media-item-provider.component', () => ({
22+
useSelectedMediaItem: vi.fn(),
23+
}));
24+
25+
describe('MainContent', () => {
26+
const mockMediaItem = getMockedMediaItem({});
27+
28+
const renderApp = ({
29+
webRtcConfig = {},
30+
pipelineConfig = {},
31+
selectedMediaItem,
32+
activePipelineConfig = {},
33+
}: {
34+
webRtcConfig?: Partial<WebRTCConnectionState>;
35+
pipelineConfig?: Partial<SchemaPipeline>;
36+
selectedMediaItem?: MediaItem;
37+
activePipelineConfig?: Partial<SchemaPipeline> | null;
38+
}) => {
39+
vi.mocked(useWebRTCConnection).mockReturnValue({
40+
start: vi.fn(),
41+
status: 'idle',
42+
stop: vi.fn(),
43+
webRTCConnectionRef: { current: null },
44+
...webRtcConfig,
45+
});
46+
47+
vi.mocked(useSelectedMediaItem).mockReturnValue({ selectedMediaItem, onSetSelectedMediaItem: vi.fn() });
48+
49+
server.use(
50+
http.get('/api/projects/{project_id}/pipeline', ({ response }) =>
51+
response(200).json(getMockedPipeline(pipelineConfig))
52+
),
53+
http.get('/api/active-pipeline', ({ response }) => {
54+
if (activePipelineConfig === null) {
55+
return response(200).json(null);
56+
}
57+
return response(200).json(getMockedPipeline({ project_id: '123', ...activePipelineConfig }));
58+
})
59+
);
60+
61+
return render(
62+
<QueryClientProvider client={new QueryClient()}>
63+
<ZoomProvider>
64+
<MemoryRouter initialEntries={['/projects/123/inspect']}>
65+
<Routes>
66+
<Route path='/projects/:projectId/inspect' element={<MainContent />} />
67+
</Routes>
68+
</MemoryRouter>
69+
</ZoomProvider>
70+
</QueryClientProvider>
71+
);
72+
};
73+
74+
describe('SinkMessage', () => {
75+
it('renders when no source is configured and no media item selected', async () => {
76+
renderApp({ pipelineConfig: { source: undefined } });
77+
78+
expect(await screen.findByText(SOURCE_MESSAGE)).toBeVisible();
79+
});
80+
81+
it('does not render SinkMessage when no sink is configured', async () => {
82+
renderApp({ pipelineConfig: { sink: undefined } });
83+
84+
expect(screen.queryByText(SOURCE_MESSAGE)).not.toBeInTheDocument();
85+
});
86+
87+
it('renders when both source and sink are missing', async () => {
88+
renderApp({ pipelineConfig: { source: undefined, sink: undefined } });
89+
90+
expect(await screen.findByText(SOURCE_MESSAGE)).toBeVisible();
91+
});
92+
});
93+
94+
describe('EnableProject', () => {
95+
it('renders when another project is active and no media item selected', async () => {
96+
renderApp({
97+
pipelineConfig: { project_id: '123' },
98+
activePipelineConfig: { project_id: '456' },
99+
});
100+
101+
expect(await screen.findByRole('button', { name: /Activate project/i })).toBeVisible();
102+
});
103+
104+
it('does not render EnableProject when current project is active', async () => {
105+
renderApp({
106+
pipelineConfig: { project_id: '123' },
107+
activePipelineConfig: { project_id: '123' },
108+
});
109+
110+
expect(screen.queryByRole('button', { name: /Activate project/i })).not.toBeInTheDocument();
111+
});
112+
113+
it('does not render EnableProject when no active pipeline', async () => {
114+
renderApp({
115+
pipelineConfig: { project_id: '123' },
116+
activePipelineConfig: null,
117+
});
118+
119+
expect(screen.queryByRole('button', { name: /Activate project/i })).not.toBeInTheDocument();
120+
});
121+
});
122+
123+
describe('StreamContainer', () => {
124+
it('renders when no media item selected and source/sink are configured', async () => {
125+
renderApp({
126+
pipelineConfig: { status: 'idle' },
127+
webRtcConfig: { status: 'idle' },
128+
});
129+
130+
expect(await screen.findByRole('button', { name: /Start stream/i })).toBeVisible();
131+
});
132+
133+
it('renders when no media item selected and no other project is active', async () => {
134+
renderApp({
135+
pipelineConfig: { project_id: '123' },
136+
activePipelineConfig: null,
137+
});
138+
139+
expect(await screen.findByRole('button', { name: /Start stream/i })).toBeVisible();
140+
});
141+
142+
it('renders when current project is active', async () => {
143+
renderApp({
144+
webRtcConfig: { status: 'idle' },
145+
pipelineConfig: { project_id: '123', status: 'running' },
146+
activePipelineConfig: { project_id: '123' },
147+
});
148+
149+
expect(await screen.findByRole('button', { name: /Start stream/i })).toBeVisible();
150+
});
151+
});
152+
153+
describe('InferenceResult', () => {
154+
it('renders when media item is selected', async () => {
155+
renderApp({ selectedMediaItem: mockMediaItem });
156+
157+
expect(screen.queryByText(SOURCE_MESSAGE)).not.toBeInTheDocument();
158+
expect(screen.queryByRole('button', { name: /Start stream/i })).not.toBeInTheDocument();
159+
});
160+
161+
it('renders InferenceResult even when source/sink missing if media item selected', async () => {
162+
renderApp({
163+
pipelineConfig: { source: undefined, sink: undefined },
164+
selectedMediaItem: mockMediaItem,
165+
});
166+
167+
expect(screen.queryByText(SOURCE_MESSAGE)).not.toBeInTheDocument();
168+
});
169+
170+
it('renders InferenceResult even when another project is active if media item selected', async () => {
171+
renderApp({
172+
pipelineConfig: { project_id: '123' },
173+
activePipelineConfig: { project_id: '456' },
174+
selectedMediaItem: mockMediaItem,
175+
});
176+
177+
expect(screen.queryByRole('button', { name: /Activate project/i })).not.toBeInTheDocument();
178+
});
179+
});
180+
});
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Grid, Heading } from '@geti/ui';
22

33
import styles from './source-sink-message.module.scss';
44

5+
export const SOURCE_MESSAGE = 'No source configured. Please set it before starting the stream.';
6+
57
export const SourceSinkMessage = () => {
68
return (
79
<Grid
@@ -10,7 +12,7 @@ export const SourceSinkMessage = () => {
1012
justifyContent={'center'}
1113
alignContent={'center'}
1214
>
13-
<Heading>No source or sink is configured. Please set both before starting the stream.</Heading>
15+
<Heading>{SOURCE_MESSAGE}</Heading>
1416
</Grid>
1517
);
1618
};

0 commit comments

Comments
 (0)