Skip to content

Commit 09d9f27

Browse files
authored
feat(inspect): enhance ModelsView (#3135)
* feat(inspect): enhance ModelsView Signed-off-by: Dmitry Kalinin <dmitry.kalinin@intel.com> * Adjusted styling Signed-off-by: Dmitry Kalinin <dmitry.kalinin@intel.com> * Fixed comments Signed-off-by: Dmitry Kalinin <dmitry.kalinin@intel.com> * Fixed format Signed-off-by: Dmitry Kalinin <dmitry.kalinin@intel.com> --------- Signed-off-by: Dmitry Kalinin <dmitry.kalinin@intel.com>
1 parent 9990aca commit 09d9f27

File tree

10 files changed

+393
-77
lines changed

10 files changed

+393
-77
lines changed
Lines changed: 5 additions & 0 deletions
Loading

application/ui/src/assets/icons/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@ export { ReactComponent as Fireworks } from './fire-works.svg';
2323
export { ReactComponent as PipelineLink } from './pipeline-link.svg';
2424
export { ReactComponent as Folder } from './folder.svg';
2525
export { ReactComponent as LinkExpired } from './link-expired.svg';
26+
export { ReactComponent as ActiveIcon } from './active-icon.svg';
27+
export { ReactComponent as LoadingIcon } from './loading-icon.svg';
Lines changed: 3 additions & 0 deletions
Loading

application/ui/src/features/inspect/jobs/show-job-logs.component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const JobLogsDialogContent = ({ jobId }: { jobId: string }) => {
3737
);
3838
};
3939

40-
const JobLogsDialog = ({ close, jobId }: { close: () => void; jobId: string }) => {
40+
export const JobLogsDialog = ({ close, jobId }: { close: () => void; jobId: string }) => {
4141
return (
4242
<Dialog>
4343
<Heading>Logs</Heading>
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { useState } from 'react';
2+
3+
import { $api } from '@geti-inspect/api';
4+
import { useProjectIdentifier } from '@geti-inspect/hooks';
5+
import { ActionButton, AlertDialog, DialogContainer, Item, Menu, MenuTrigger, toast, type Key } from '@geti/ui';
6+
import { MoreMenu } from '@geti/ui/icons';
7+
8+
import { JobLogsDialog } from '../jobs/show-job-logs.component';
9+
import type { ModelData } from './model-types';
10+
11+
interface ModelActionsMenuProps {
12+
model: ModelData;
13+
selectedModelId: string | undefined;
14+
onSetSelectedModelId: (modelId: string | undefined) => void;
15+
}
16+
17+
export const ModelActionsMenu = ({ model, selectedModelId, onSetSelectedModelId }: ModelActionsMenuProps) => {
18+
const { projectId } = useProjectIdentifier();
19+
const [isLogsDialogOpen, setIsLogsDialogOpen] = useState(false);
20+
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
21+
22+
const cancelJobMutation = $api.useMutation('post', '/api/jobs/{job_id}:cancel');
23+
const deleteModelMutation = $api.useMutation('delete', '/api/projects/{project_id}/models/{model_id}', {
24+
meta: {
25+
invalidates: [
26+
['get', '/api/projects/{project_id}/models', { params: { path: { project_id: projectId } } }],
27+
['get', '/api/projects/{project_id}/pipeline', { params: { path: { project_id: projectId } } }],
28+
['get', '/api/jobs'],
29+
],
30+
},
31+
});
32+
33+
const hasJobActions = Boolean(model.job?.id);
34+
const canDeleteModel = model.status === 'Completed' && model.id !== selectedModelId;
35+
const shouldShowMenu = hasJobActions || canDeleteModel;
36+
37+
if (!shouldShowMenu) {
38+
return null;
39+
}
40+
41+
const disabledMenuKeys: Key[] = [];
42+
if (cancelJobMutation.isPending) {
43+
disabledMenuKeys.push('cancel');
44+
}
45+
if (deleteModelMutation.isPending) {
46+
disabledMenuKeys.push('delete');
47+
}
48+
49+
const handleCancelJob = () => {
50+
if (!model.job?.id) {
51+
return;
52+
}
53+
54+
void cancelJobMutation.mutateAsync(
55+
{
56+
params: {
57+
path: {
58+
job_id: model.job.id,
59+
},
60+
},
61+
},
62+
{
63+
onError: () => {
64+
toast({ type: 'error', message: 'Failed to cancel training job.' });
65+
},
66+
}
67+
);
68+
};
69+
70+
const handleDeleteModel = () => {
71+
void deleteModelMutation.mutateAsync(
72+
{
73+
params: {
74+
path: {
75+
project_id: projectId,
76+
model_id: model.id,
77+
},
78+
},
79+
},
80+
{
81+
onSuccess: () => {
82+
if (selectedModelId === model.id) {
83+
onSetSelectedModelId(undefined);
84+
}
85+
86+
toast({ type: 'success', message: `Model "${model.name}" has been deleted.` });
87+
},
88+
onError: () => {
89+
toast({ type: 'error', message: `Failed to delete "${model.name}".` });
90+
},
91+
onSettled: () => {
92+
setIsDeleteDialogOpen(false);
93+
},
94+
}
95+
);
96+
};
97+
98+
return (
99+
<>
100+
<MenuTrigger>
101+
<ActionButton isQuiet aria-label='model actions'>
102+
<MoreMenu />
103+
</ActionButton>
104+
<Menu
105+
disabledKeys={disabledMenuKeys}
106+
onAction={(actionKey) => {
107+
if (actionKey === 'logs' && model.job?.id) {
108+
setIsLogsDialogOpen(true);
109+
}
110+
if (actionKey === 'cancel' && model.job?.id) {
111+
void handleCancelJob();
112+
}
113+
if (actionKey === 'delete' && canDeleteModel) {
114+
setIsDeleteDialogOpen(true);
115+
}
116+
}}
117+
>
118+
{hasJobActions ? <Item key='logs'>View logs</Item> : null}
119+
{model.job?.status === 'pending' || model.job?.status === 'running' ? (
120+
<Item key='cancel'>Cancel training</Item>
121+
) : null}
122+
{canDeleteModel ? <Item key='delete'>Delete model</Item> : null}
123+
</Menu>
124+
</MenuTrigger>
125+
126+
<DialogContainer type='fullscreen' onDismiss={() => setIsLogsDialogOpen(false)}>
127+
{isLogsDialogOpen && model.job?.id ? (
128+
<JobLogsDialog close={() => setIsLogsDialogOpen(false)} jobId={model.job.id} />
129+
) : null}
130+
</DialogContainer>
131+
132+
<DialogContainer onDismiss={() => setIsDeleteDialogOpen(false)}>
133+
{!isDeleteDialogOpen || !canDeleteModel ? null : (
134+
<AlertDialog
135+
variant='destructive'
136+
cancelLabel='Cancel'
137+
title={`Delete model "${model.name}"?`}
138+
primaryActionLabel={deleteModelMutation.isPending ? 'Deleting...' : 'Delete model'}
139+
isPrimaryActionDisabled={deleteModelMutation.isPending}
140+
onPrimaryAction={() => {
141+
void handleDeleteModel();
142+
}}
143+
>
144+
Deleting a model removes any exported artifacts and cannot be undone.
145+
</AlertDialog>
146+
)}
147+
</DialogContainer>
148+
</>
149+
);
150+
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Badge } from '@adobe/react-spectrum';
2+
import { ActiveIcon, LoadingIcon } from '@geti-inspect/icons';
3+
import { Alert, Cancel, Pending } from '@geti/ui/icons';
4+
import { SchemaJob } from 'src/api/openapi-spec';
5+
6+
import classes from './models-view.module.scss';
7+
8+
interface ModelStatusBadgesProps {
9+
isSelected: boolean;
10+
jobStatus?: SchemaJob['status'];
11+
}
12+
13+
export const ModelStatusBadges = ({ isSelected, jobStatus }: ModelStatusBadgesProps) => {
14+
if (!isSelected && !jobStatus) {
15+
return null;
16+
}
17+
18+
return (
19+
<>
20+
{isSelected && (
21+
<Badge variant='info' UNSAFE_className={classes.badge}>
22+
<ActiveIcon />
23+
Active
24+
</Badge>
25+
)}
26+
{jobStatus === 'pending' && (
27+
<Badge variant='neutral' UNSAFE_className={classes.badge}>
28+
<Pending />
29+
Pending
30+
</Badge>
31+
)}
32+
{jobStatus === 'running' && (
33+
<Badge variant='info' UNSAFE_className={classes.badge}>
34+
<LoadingIcon />
35+
Training...
36+
</Badge>
37+
)}
38+
{jobStatus === 'failed' && (
39+
<Badge variant='negative' UNSAFE_className={classes.badge}>
40+
<Alert />
41+
Failed
42+
</Badge>
43+
)}
44+
{jobStatus === 'canceled' && (
45+
<Badge variant='neutral' UNSAFE_className={classes.badge}>
46+
<Cancel />
47+
Canceled
48+
</Badge>
49+
)}
50+
</>
51+
);
52+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { SchemaJob } from 'src/api/openapi-spec';
2+
3+
export interface ModelData {
4+
id: string;
5+
name: string;
6+
timestamp: string;
7+
startTime: number;
8+
durationInSeconds: number | null;
9+
status: 'Training' | 'Completed' | 'Failed';
10+
architecture: string;
11+
progress: number;
12+
job: SchemaJob | undefined;
13+
sizeBytes: number | null;
14+
}

0 commit comments

Comments
 (0)