Skip to content

Commit dc399d1

Browse files
committed
add start/stop toggle and tests
1 parent 95d80a0 commit dc399d1

File tree

16 files changed

+234
-57
lines changed

16 files changed

+234
-57
lines changed

frontend/src/__tests__/cypress/cypress/pages/modelServing.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,10 @@ class KServeRow extends ModelMeshRow {
596596
findProjectScopedLabel() {
597597
return this.find().findByTestId('project-scoped-label');
598598
}
599+
600+
findStateActionToggle() {
601+
return this.find().findByTestId('state-action-toggle');
602+
}
599603
}
600604

601605
class InferenceServiceRow extends TableRow {

frontend/src/__tests__/cypress/cypress/pages/projects.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ class ProjectNotebookRow extends TableRow {
4848
}
4949

5050
findNotebookStart() {
51-
return this.find().findByTestId('notebook-start-action');
51+
return this.find().findByTestId('start-action-toggle');
5252
}
5353

5454
findNotebookStop() {
55-
return this.find().findByTestId('notebook-stop-action');
55+
return this.find().findByTestId('stop-action-toggle');
5656
}
5757
}
5858

frontend/src/__tests__/cypress/cypress/pages/workbench.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -242,12 +242,8 @@ class NotebookRow extends TableRow {
242242
this.findHaveNotebookStatusText(timeout).should('have.text', statusValue);
243243
}
244244

245-
findNotebookStart() {
246-
return this.find().findByTestId('notebook-start-action');
247-
}
248-
249-
findNotebookStop() {
250-
return this.find().findByTestId('notebook-stop-action');
245+
findNotebookStopToggle() {
246+
return this.find().findByTestId('state-action-toggle');
251247
}
252248

253249
findNotebookStatusModal() {

frontend/src/__tests__/cypress/cypress/tests/e2e/dataScienceProjects/workbenches/testWorkbenchCreation.cy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ describe('Create, Delete and Edit - Workbench Tests', () => {
114114

115115
// Stop workbench
116116
cy.step('Stop workbench and validate it has been stopped');
117-
notebookRow.findNotebookStop().click();
117+
notebookRow.findNotebookStopToggle().should('have.text', 'Stop').click();
118118
notebookConfirmModal.findStopWorkbenchButton().click();
119119
notebookRow.expectStatusLabelToBe('Stopped', 120000);
120120
cy.reload();

frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/runtime/servingRuntimeList.cy.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,6 +1324,86 @@ describe('Serving Runtime List', () => {
13241324
.should(be.sortDescending);
13251325
});
13261326

1327+
it('Stop and start model', () => {
1328+
initIntercepts({
1329+
projectEnableModelMesh: false,
1330+
disableKServeConfig: false,
1331+
disableModelMeshConfig: true,
1332+
inferenceServices: [
1333+
mockInferenceServiceK8sResource({
1334+
name: 'test-model',
1335+
displayName: 'test-model',
1336+
modelName: 'test-model',
1337+
isModelMesh: false,
1338+
activeModelState: 'Loaded',
1339+
}),
1340+
],
1341+
});
1342+
projectDetails.visitSection('test-project', 'model-server');
1343+
1344+
const kserveRow = modelServingSection.getKServeRow('test-model');
1345+
1346+
const stoppedInferenceService = mockInferenceServiceK8sResource({
1347+
name: 'test-model',
1348+
displayName: 'test-model',
1349+
modelName: 'test-model',
1350+
isModelMesh: false,
1351+
activeModelState: 'Unknown',
1352+
});
1353+
stoppedInferenceService.metadata.annotations = {
1354+
...stoppedInferenceService.metadata.annotations,
1355+
'serving.kserve.io/stop': 'true',
1356+
};
1357+
1358+
cy.intercept(
1359+
'PATCH',
1360+
'/api/k8s/apis/serving.kserve.io/v1beta1/namespaces/test-project/inferenceservices/test-model',
1361+
(req) => {
1362+
expect(req.body).to.deep.include({
1363+
op: 'add',
1364+
path: '/metadata/annotations/serving.kserve.io~1stop',
1365+
value: 'true',
1366+
});
1367+
req.reply(stoppedInferenceService);
1368+
},
1369+
).as('stopModelPatch');
1370+
cy.interceptK8sList(InferenceServiceModel, mockK8sResourceList([stoppedInferenceService])).as(
1371+
'getStoppedModel',
1372+
);
1373+
1374+
kserveRow.findStateActionToggle().should('have.text', 'Stop').click();
1375+
cy.wait(['@stopModelPatch', '@getStoppedModel']);
1376+
kserveRow.findStateActionToggle().should('have.text', 'Start');
1377+
1378+
const runningInferenceService = mockInferenceServiceK8sResource({
1379+
name: 'test-model',
1380+
displayName: 'test-model',
1381+
modelName: 'test-model',
1382+
isModelMesh: false,
1383+
activeModelState: 'Loaded',
1384+
});
1385+
1386+
cy.intercept(
1387+
'PATCH',
1388+
'/api/k8s/apis/serving.kserve.io/v1beta1/namespaces/test-project/inferenceservices/test-model',
1389+
(req) => {
1390+
expect(req.body).to.deep.include({
1391+
op: 'add',
1392+
path: '/metadata/annotations/serving.kserve.io~1stop',
1393+
value: 'false',
1394+
});
1395+
req.reply(runningInferenceService);
1396+
},
1397+
).as('startModelPatch');
1398+
cy.interceptK8sList(InferenceServiceModel, mockK8sResourceList([runningInferenceService])).as(
1399+
'getStartedModel',
1400+
);
1401+
1402+
kserveRow.findStateActionToggle().should('have.text', 'Start').click();
1403+
cy.wait(['@startModelPatch', '@getStartedModel']);
1404+
kserveRow.findStateActionToggle().should('have.text', 'Stop');
1405+
});
1406+
13271407
it('Check number of replicas of model', () => {
13281408
initIntercepts({
13291409
projectEnableModelMesh: false,

frontend/src/api/k8s/inferenceServices.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
k8sListResource,
77
K8sStatus,
88
k8sUpdateResource,
9+
k8sPatchResource,
910
} from '@openshift/dynamic-plugin-sdk-utils';
1011
import { InferenceServiceModel } from '#~/api/models';
1112
import { InferenceServiceKind, K8sAPIOptions, KnownLabels } from '#~/k8sTypes';
@@ -361,3 +362,22 @@ export const deleteInferenceService = (
361362
opts,
362363
),
363364
);
365+
366+
export const patchInferenceServiceStoppedStatus = (
367+
inferenceService: InferenceServiceKind,
368+
stoppedStatus: 'true' | 'false',
369+
): Promise<InferenceServiceKind> =>
370+
k8sPatchResource({
371+
model: InferenceServiceModel,
372+
queryOptions: {
373+
name: inferenceService.metadata.name,
374+
ns: inferenceService.metadata.namespace,
375+
},
376+
patches: [
377+
{
378+
op: 'add',
379+
path: '/metadata/annotations/serving.kserve.io~1stop',
380+
value: stoppedStatus,
381+
},
382+
],
383+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import * as React from 'react';
2+
import { Button } from '@patternfly/react-core';
3+
4+
export type ToggleState = {
5+
isStarting: boolean;
6+
isStopping: boolean;
7+
isRunning: boolean;
8+
isStopped: boolean;
9+
};
10+
11+
// Make the component generic, constrained to ToggleState
12+
export type StateActionToggleProps<T extends ToggleState> = {
13+
currentState: T;
14+
onStart: () => void;
15+
onStop: () => void;
16+
isDisabled?: boolean;
17+
};
18+
19+
const StateActionToggle = <T extends ToggleState>({
20+
currentState,
21+
onStart,
22+
onStop,
23+
isDisabled,
24+
}: StateActionToggleProps<T>): React.ReactElement => {
25+
const { isStarting, isRunning, isStopping } = currentState;
26+
const actionDisabled = isDisabled || isStopping || isStarting;
27+
const runningState = isRunning || isStarting;
28+
return (
29+
<Button
30+
data-testid="state-action-toggle"
31+
variant="link"
32+
isDisabled={actionDisabled}
33+
onClick={runningState ? onStop : onStart}
34+
isInline
35+
>
36+
{runningState ? 'Stop' : 'Start'}
37+
</Button>
38+
);
39+
};
40+
41+
export default StateActionToggle;

frontend/src/pages/modelServing/__tests__/utils.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
resourcesArePositive,
55
setUpTokenAuth,
66
isOciModelUri,
7+
getModelServingStatus,
78
} from '#~/pages/modelServing/utils';
89
import { mockServingRuntimeK8sResource } from '#~/__mocks__/mockServingRuntimeK8sResource';
910
import { ContainerResources } from '#~/types';
@@ -306,3 +307,29 @@ describe('isOciModelUri', () => {
306307
expect(isOciModelUri('')).toBe(false);
307308
});
308309
});
310+
311+
describe('getModelServingStatus', () => {
312+
it('should return correct status when model is running', () => {
313+
const inferenceService = mockInferenceServiceK8sResource({});
314+
expect(getModelServingStatus(inferenceService)).toEqual({
315+
inferenceService,
316+
isStopped: false,
317+
isRunning: true,
318+
isStopping: false,
319+
isStarting: false,
320+
});
321+
});
322+
323+
it('should return correct status when model is stopped', () => {
324+
const inferenceService = mockInferenceServiceK8sResource({});
325+
inferenceService.metadata.annotations ??= {};
326+
inferenceService.metadata.annotations['serving.kserve.io/stop'] = 'true';
327+
expect(getModelServingStatus(inferenceService)).toEqual({
328+
inferenceService,
329+
isStopped: true,
330+
isRunning: false,
331+
isStopping: false,
332+
isStarting: false,
333+
});
334+
});
335+
});

frontend/src/pages/modelServing/screens/global/InferenceServiceTable.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const InferenceServiceTable: React.FC<InferenceServiceTableProps> = ({
8484
columnNames={mappedColumns.map((column) => column.field)}
8585
onDeleteInferenceService={setDeleteInferenceService}
8686
onEditInferenceService={setEditInferenceService}
87+
refresh={refresh}
8788
/>
8889
</ResourceTr>
8990
)}

frontend/src/pages/modelServing/screens/global/InferenceServiceTableRow.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import ResourceActionsColumn from '#~/components/ResourceActionsColumn';
55
import ResourceNameTooltip from '#~/components/ResourceNameTooltip';
66
import useModelMetricsEnabled from '#~/pages/modelServing/useModelMetricsEnabled';
77
import { InferenceServiceKind, ServingRuntimeKind } from '#~/k8sTypes';
8-
import { isModelMesh } from '#~/pages/modelServing/utils';
8+
import { getModelServingStatus, isModelMesh } from '#~/pages/modelServing/utils';
99
import { SupportedArea } from '#~/concepts/areas';
1010
import useIsAreaAvailable from '#~/concepts/areas/useIsAreaAvailable';
1111
import { getDisplayNameFromK8sResource } from '#~/concepts/k8s/utils';
1212
import { byName, ProjectsContext } from '#~/concepts/projects/ProjectsContext';
1313
import { isProjectNIMSupported } from '#~/pages/modelServing/screens/projects/nimUtils';
1414
import useServingPlatformStatuses from '#~/pages/modelServing/useServingPlatformStatuses';
15+
import StateActionToggle from '#~/components/StateActionToggle';
16+
import { patchInferenceServiceStoppedStatus } from '#~/api/k8s/inferenceServices';
1517
import InferenceServiceEndpoint from './InferenceServiceEndpoint';
1618
import InferenceServiceProject from './InferenceServiceProject';
1719
import InferenceServiceStatus from './InferenceServiceStatus';
@@ -24,13 +26,15 @@ type InferenceServiceTableRowProps = {
2426
isGlobal?: boolean;
2527
servingRuntime?: ServingRuntimeKind;
2628
columnNames: string[];
29+
refresh?: () => void;
2730
onDeleteInferenceService: (obj: InferenceServiceKind) => void;
2831
onEditInferenceService: (obj: InferenceServiceKind) => void;
2932
};
3033

3134
const InferenceServiceTableRow: React.FC<InferenceServiceTableRowProps> = ({
3235
obj: inferenceService,
3336
servingRuntime,
37+
refresh = () => undefined,
3438
onDeleteInferenceService,
3539
onEditInferenceService,
3640
isGlobal,
@@ -51,6 +55,16 @@ const InferenceServiceTableRow: React.FC<InferenceServiceTableRowProps> = ({
5155
const kserveMetricsSupported = modelMetricsEnabled && kserveMetricsEnabled && !modelMesh;
5256
const displayName = getDisplayNameFromK8sResource(inferenceService);
5357

58+
const modelServingStatus = getModelServingStatus(inferenceService);
59+
60+
const onStart = React.useCallback(() => {
61+
patchInferenceServiceStoppedStatus(inferenceService, 'false').then(refresh);
62+
}, [inferenceService, refresh]);
63+
64+
const onStop = React.useCallback(() => {
65+
patchInferenceServiceStoppedStatus(inferenceService, 'true').then(refresh);
66+
}, [inferenceService, refresh]);
67+
5468
return (
5569
<>
5670
<Td dataLabel="Name">
@@ -107,6 +121,9 @@ const InferenceServiceTableRow: React.FC<InferenceServiceTableRowProps> = ({
107121
<Td dataLabel="Status">
108122
<InferenceServiceStatus inferenceService={inferenceService} isKserve={!modelMesh} />
109123
</Td>
124+
<Td>
125+
<StateActionToggle currentState={modelServingStatus} onStart={onStart} onStop={onStop} />
126+
</Td>
110127

111128
{columnNames.includes(ColumnField.Kebab) && (
112129
<Td isActionCell>

0 commit comments

Comments
 (0)