Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions application/ui/mocks/mock-metrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { SchemaPipelineMetrics } from './../src/api/openapi-spec.d';

export const getMockedMetrics = (partial: Partial<SchemaPipelineMetrics>): SchemaPipelineMetrics => {
return {
time_window: { start: '2025-11-28T15:32:51.030059Z', end: '2025-11-28T15:33:51.030059Z', time_window: 60 },
inference: {
latency: {
avg_ms: 76.51289340585073,
min_ms: 59.030749995145015,
max_ms: 260.70933300070465,
p95_ms: 92.98036629625129,
latest_ms: 71.58341699687298,
},
throughput: { avg_requests_per_second: 0.45, total_requests: 27, max_requests_per_second: 8.0 },
},
...partial,
};
};
13 changes: 13 additions & 0 deletions application/ui/mocks/mock-model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { SchemaModel } from '../src/api/openapi-spec';

export const getMockedModel = (partial: Partial<SchemaModel>): SchemaModel => ({
id: 'model-1',
name: 'Model 1',
format: 'openvino' as const,
project_id: '123',
threshold: 0.5,
is_ready: true,
dataset_snapshot_id: 'dataset-1',
train_job_id: 'job-1',
...partial,
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface DatasetItemProps {

export const DatasetList = ({ mediaItems }: DatasetItemProps) => {
const [selectedMediaItemId, setSelectedMediaItem] = useQueryState('selectedMediaItem');
//Todo: revisit implementation when dataset loading is paginated
//TODO: revisit implementation when dataset loading is paginated
const selectedMediaItem = mediaItems.find((item) => item.id === selectedMediaItemId);

const mediaItemsToRender = [
Expand Down
76 changes: 40 additions & 36 deletions application/ui/src/features/inspect/footer/footer.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,70 +5,74 @@ import { Suspense, useEffect } from 'react';

import { $api } from '@geti-inspect/api';
import { SchemaJob as Job } from '@geti-inspect/api/spec';
import { usePipeline, useProjectIdentifier, useSetModelToPipeline } from '@geti-inspect/hooks';
import { usePatchPipeline, usePipeline, useProjectIdentifier } from '@geti-inspect/hooks';
import { Flex, Loading, Text, View } from '@geti/ui';
import { WaitingIcon } from '@geti/ui/icons';

import { useTrainedModels } from '../../../hooks/use-model';
import { TrainingStatusItem } from './training-status-item.component';

const IdleItem = () => {
const models = useTrainedModels();
const { data: pipeline } = usePipeline();
const setModelToPipelineMutation = useSetModelToPipeline();
const selectedModelId = pipeline?.model?.id;

useEffect(() => {
if (selectedModelId !== undefined || models.length === 0) {
return;
}

setModelToPipelineMutation(models[0].id);
}, [selectedModelId, models, setModelToPipelineMutation]);

return (
<Flex
alignItems='center'
width='size-3000'
justifyContent='start'
gap='size-100'
height='100%'
UNSAFE_style={{
padding: '0 var(--spectrum-global-dimension-size-200)',
}}
>
<WaitingIcon height='14px' width='14px' stroke='var(--spectrum-global-color-gray-600)' />
<Text marginStart={'5px'} UNSAFE_style={{ color: 'var(--spectrum-global-color-gray-600)' }}>
Idle
</Text>
</Flex>
);
};

const useCurrentJob = () => {
const { projectId } = useProjectIdentifier();
const { data: jobsData } = $api.useSuspenseQuery('get', '/api/jobs', undefined, {
refetchInterval: 5000,
});

const { projectId } = useProjectIdentifier();
const runningJob = jobsData.jobs.find(
(job: Job) => job.project_id === projectId && (job.status === 'running' || job.status === 'pending')
);

return runningJob;
};

const useDefaultModel = () => {
const models = useTrainedModels();
const { data: pipeline } = usePipeline();
const { projectId } = useProjectIdentifier();
const patchPipeline = usePatchPipeline(projectId);

const hasSelectedModel = pipeline?.model?.id !== undefined;
const hasNonAvailableModels = models.length === 0;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we filter the models with is_ready or status==Completed here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, I'll update it on my next PR


useEffect(() => {
if (hasSelectedModel || hasNonAvailableModels || patchPipeline.isPending) {
return;
}

patchPipeline.mutate({
params: { path: { project_id: projectId } },
body: { model_id: models[0].id },
});
}, [hasNonAvailableModels, hasSelectedModel, models, patchPipeline, projectId]);
};

export const ProgressBarItem = () => {
const trainingJob = useCurrentJob();

if (trainingJob !== undefined) {
return <TrainingStatusItem trainingJob={trainingJob} />;
}

return <IdleItem />;
return (
<Flex
gap='size-100'
height='100%'
width='size-3000'
alignItems='center'
justifyContent='start'
UNSAFE_style={{ padding: '0 var(--spectrum-global-dimension-size-200)' }}
>
<WaitingIcon height='14px' width='14px' stroke='var(--spectrum-global-color-gray-600)' />
<Text marginStart={'5px'} UNSAFE_style={{ color: 'var(--spectrum-global-color-gray-600)' }}>
Idle
</Text>
</Flex>
);
};

export const Footer = () => {
useDefaultModel();

return (
<View gridArea={'footer'} backgroundColor={'gray-100'} width={'100%'} height={'size-400'} overflow={'hidden'}>
<Suspense fallback={<Loading mode={'inline'} size='S' />}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useState } from 'react';

import { $api } from '@geti-inspect/api';
import { useProjectIdentifier } from '@geti-inspect/hooks';
import { usePatchPipeline, useProjectIdentifier } from '@geti-inspect/hooks';
import { ActionButton, AlertDialog, DialogContainer, Item, Menu, MenuTrigger, toast, type Key } from '@geti/ui';
import { MoreMenu } from '@geti/ui/icons';

Expand All @@ -12,13 +12,13 @@ import type { ModelData } from './model-types';
interface ModelActionsMenuProps {
model: ModelData;
selectedModelId: string | undefined;
onSetSelectedModelId: (modelId: string | undefined) => void;
}

type DialogType = 'logs' | 'delete' | 'export' | null;

export const ModelActionsMenu = ({ model, selectedModelId, onSetSelectedModelId }: ModelActionsMenuProps) => {
export const ModelActionsMenu = ({ model, selectedModelId }: ModelActionsMenuProps) => {
const { projectId } = useProjectIdentifier();
const patchPipeline = usePatchPipeline(projectId);
const [openDialog, setOpenDialog] = useState<DialogType>(null);

const cancelJobMutation = $api.useMutation('post', '/api/jobs/{job_id}:cancel');
Expand All @@ -33,8 +33,9 @@ export const ModelActionsMenu = ({ model, selectedModelId, onSetSelectedModelId
});

const hasJobActions = Boolean(model.job?.id);
const canDeleteModel = model.status === 'Completed' && model.id !== selectedModelId;
const canExportModel = model.status === 'Completed';
const hasCompletedStatus = model.status === 'Completed';
const canDeleteModel = hasCompletedStatus && model.id !== selectedModelId;
const canExportModel = hasCompletedStatus;
const shouldShowMenu = hasJobActions || canDeleteModel || canExportModel;

if (!shouldShowMenu) {
Expand All @@ -48,20 +49,17 @@ export const ModelActionsMenu = ({ model, selectedModelId, onSetSelectedModelId
if (deleteModelMutation.isPending) {
disabledMenuKeys.push('delete');
}
if (model.id === selectedModelId || patchPipeline.isPending) {
disabledMenuKeys.push('activate');
}

const handleCancelJob = () => {
if (!model.job?.id) {
return;
}

void cancelJobMutation.mutateAsync(
{
params: {
path: {
job_id: model.job.id,
},
},
},
{ params: { path: { job_id: model.job.id } } },
{
onError: () => {
toast({ type: 'error', message: 'Failed to cancel training job.' });
Expand All @@ -70,20 +68,17 @@ export const ModelActionsMenu = ({ model, selectedModelId, onSetSelectedModelId
);
};

const handleSetModel = (modelId?: string) => {
patchPipeline.mutateAsync({ params: { path: { project_id: projectId } }, body: { model_id: modelId } });
};

const handleDeleteModel = () => {
void deleteModelMutation.mutateAsync(
{
params: {
path: {
project_id: projectId,
model_id: model.id,
},
},
},
{ params: { path: { project_id: projectId, model_id: model.id } } },
{
onSuccess: () => {
if (selectedModelId === model.id) {
onSetSelectedModelId(undefined);
handleSetModel(undefined);
}

toast({ type: 'success', message: `Model "${model.name}" has been deleted.` });
Expand Down Expand Up @@ -119,8 +114,12 @@ export const ModelActionsMenu = ({ model, selectedModelId, onSetSelectedModelId
if (actionKey === 'delete' && canDeleteModel) {
setOpenDialog('delete');
}
if (actionKey === 'activate' && hasCompletedStatus) {
handleSetModel(model.id);
}
}}
>
{hasCompletedStatus ? <Item key='activate'>Activate</Item> : null}
{hasJobActions ? <Item key='logs'>View logs</Item> : null}
{model.job?.status === 'pending' || model.job?.status === 'running' ? (
<Item key='cancel'>Cancel training</Item>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMemo } from 'react';

import { $api } from '@geti-inspect/api';
import { usePipeline, useProjectIdentifier, useSetModelToPipeline } from '@geti-inspect/hooks';
import { usePipeline, useProjectIdentifier } from '@geti-inspect/hooks';
import {
Cell,
Column,
Expand Down Expand Up @@ -39,7 +39,7 @@ const useModels = () => {
export const ModelsView = () => {
const { data: pipeline } = usePipeline();
const { jobs = [] } = useProjectTrainingJobs();
const setModelToPipelineMutation = useSetModelToPipeline();

const dateFormatter = useDateFormatter({ dateStyle: 'medium', timeStyle: 'short' });
const selectedModelId = pipeline.model?.id;
useRefreshModelsOnJobUpdates(jobs);
Expand Down Expand Up @@ -109,10 +109,6 @@ export const ModelsView = () => {
return new Set<string>([selectedModelId]);
}, [selectedModelId]);

const handleSetModel = (modelId?: string) => {
setModelToPipelineMutation(modelId);
};

return (
<View backgroundColor='gray-100' height='100%'>
{/* Models Table */}
Expand All @@ -122,22 +118,6 @@ export const ModelsView = () => {
selectionStyle='highlight'
selectionMode='single'
selectedKeys={tableSelectedKeys}
onSelectionChange={(key) => {
if (typeof key === 'string') {
return;
}

const selectedId = key.values().next().value;
if (selectedId === selectedModelId) {
return;
}

const selectedModel = models.find((model) => model.id === selectedId);

if (selectedModel?.status === 'Completed') {
handleSetModel(selectedModel.id);
}
}}
UNSAFE_className={classes.table}
renderEmptyState={() => (
<IllustratedMessage>
Expand Down Expand Up @@ -178,11 +158,7 @@ export const ModelsView = () => {
<Cell>
<Flex justifyContent='end' alignItems='center'>
<Flex alignItems='center' gap='size-200'>
<ModelActionsMenu
model={model}
selectedModelId={selectedModelId}
onSetSelectedModelId={handleSetModel}
/>
<ModelActionsMenu model={model} selectedModelId={selectedModelId} />
</Flex>
</Flex>
</Cell>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0

import { $api } from '@geti-inspect/api';
import { usePipeline } from '@geti-inspect/hooks';
import { dimensionValue, View } from '@geti/ui';
import { isEmpty, isNil } from 'lodash-es';

interface FpsProp {
projectId: string;
}

export const Fps = ({ projectId }: FpsProp) => {
const { data: pipeline } = usePipeline();
const isRunning = pipeline?.status === 'running';
const formatter = new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 });

const { data: metrics } = $api.useQuery(
'get',
'/api/projects/{project_id}/pipeline/metrics',
{ params: { path: { project_id: projectId } } },
{ enabled: isRunning }
);

const requestsPerSecond = metrics?.inference.throughput.avg_requests_per_second;

if (isEmpty(metrics) || isNil(requestsPerSecond)) {
return null;
}

return (
<View
position={'absolute'}
top={'size-200'}
right={'size-200'}
backgroundColor={'gray-100'}
UNSAFE_style={{
fontSize: dimensionValue('size-130'),
padding: `${dimensionValue('size-85')} ${dimensionValue('size-65')}`,
borderRadius: dimensionValue('size-25'),
}}
>
{formatter.format(requestsPerSecond)} fps
</View>
);
};
Loading
Loading