Skip to content

Commit c8d4d22

Browse files
committed
Serving runtime version status label and tests
1 parent 9def685 commit c8d4d22

File tree

16 files changed

+442
-124
lines changed

16 files changed

+442
-124
lines changed

frontend/src/__mocks__/mockServingRuntimeK8sResource.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type MockResourceConfigType = {
2424
acceleratorProfileNamespace?: string;
2525
isNonDashboardItem?: boolean;
2626
version?: string;
27+
templateName?: string;
2728
};
2829

2930
export const mockServingRuntimeK8sResourceLegacy = ({
@@ -136,6 +137,7 @@ export const mockServingRuntimeK8sResource = ({
136137
hardwareProfileNamespace = undefined,
137138
isNonDashboardItem = false,
138139
version,
140+
templateName = 'ovms',
139141
}: MockResourceConfigType): ServingRuntimeKind => ({
140142
apiVersion: 'serving.kserve.io/v1alpha1',
141143
kind: 'ServingRuntime',
@@ -150,7 +152,7 @@ export const mockServingRuntimeK8sResource = ({
150152
'opendatahub.io/accelerator-name': acceleratorName,
151153
'opendatahub.io/hardware-profile-name': hardwareProfileName,
152154

153-
'opendatahub.io/template-name': 'ovms',
155+
'opendatahub.io/template-name': templateName,
154156
'openshift.io/display-name': displayName,
155157
'opendatahub.io/apiProtocol': apiProtocol,
156158
...(version && {

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

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,6 @@ class ModelServingGlobal {
108108
findServingRuntime(name: string) {
109109
return this.findModelsTable().find(`[data-label=Serving Runtime]`).contains(name);
110110
}
111-
112-
findServingRuntimeVersionLabel() {
113-
return cy.findByTestId('serving-runtime-version-label');
114-
}
115111
}
116112

117113
class ServingRuntimeGroup extends Contextual<HTMLElement> {}
@@ -564,6 +560,18 @@ class ModelServingRow extends TableRow {
564560
findInternalServicePopover() {
565561
return cy.findByTestId('internal-service-popover');
566562
}
563+
564+
findConfirmStopModal() {
565+
return cy.findByTestId('stop-model-modal');
566+
}
567+
568+
findConfirmStopModalButton() {
569+
return this.findConfirmStopModal().findByTestId('stop-model-button');
570+
}
571+
572+
findConfirmStopModalCheckbox() {
573+
return this.findConfirmStopModal().findByTestId('dont-show-again-checkbox');
574+
}
567575
}
568576

569577
class ModelMeshRow extends ModelServingRow {
@@ -603,6 +611,14 @@ class KServeRow extends ModelMeshRow {
603611
}
604612

605613
class InferenceServiceRow extends TableRow {
614+
findServingRuntimeVersionLabel() {
615+
return this.find().findByTestId('serving-runtime-version-label');
616+
}
617+
618+
findServingRuntimeVersionStatusLabel() {
619+
return this.find().findByTestId('serving-runtime-version-status-label');
620+
}
621+
606622
findStatusTooltip() {
607623
return this.find()
608624
.findByTestId('status-tooltip')

frontend/src/__tests__/cypress/cypress/tests/mocked/modelServing/modelServingGlobal.cy.ts

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ import {
2828
ServingRuntimeModel,
2929
TemplateModel,
3030
} from '#~/__tests__/cypress/cypress/utils/models';
31-
import { DeploymentMode, type InferenceServiceKind, type ServingRuntimeKind } from '#~/k8sTypes';
31+
import {
32+
DeploymentMode,
33+
type TemplateKind,
34+
type InferenceServiceKind,
35+
type ServingRuntimeKind,
36+
} from '#~/k8sTypes';
3237
import { ServingRuntimePlatform } from '#~/types';
3338
import { be } from '#~/__tests__/cypress/cypress/utils/should';
3439
import { asClusterAdminUser } from '#~/__tests__/cypress/cypress/utils/mockUsers';
@@ -63,6 +68,7 @@ type HandlersProps = {
6368
disableServingRuntimeParamsConfig?: boolean;
6469
disableProjectScoped?: boolean;
6570
disableHardwareProfiles?: boolean;
71+
servingRuntimesTemplates?: TemplateKind[];
6672
};
6773

6874
const initIntercepts = ({
@@ -947,19 +953,46 @@ describe('Model Serving Global', () => {
947953
modelServingGlobal.getModelMetricLink('Test Inference Service').click();
948954
cy.findByTestId('app-page-title').should('have.text', 'Test Inference Service metrics');
949955
});
950-
it('Display the version label if the annotation is present', () => {
951-
const servingRuntimeWithVersion = mockServingRuntimeK8sResource({});
952-
servingRuntimeWithVersion.metadata.annotations =
953-
servingRuntimeWithVersion.metadata.annotations || {};
954-
servingRuntimeWithVersion.metadata.annotations['opendatahub.io/runtime-version'] = '1.2.3';
956+
it('Display the version label and status label correctly', () => {
957+
const servingRuntimeWithLatestVersion = mockServingRuntimeK8sResource({
958+
namespace: 'test-project',
959+
name: 'test-inference-service-latest',
960+
templateName: 'template-2',
961+
version: '1.0.0',
962+
});
963+
const servingRuntimeWithOutdatedVersion = mockServingRuntimeK8sResource({
964+
namespace: 'test-project',
965+
name: 'test-inference-service-outdated',
966+
templateName: 'template-2',
967+
version: '0.5.0',
968+
});
969+
const inferenceServiceLatest = mockInferenceServiceK8sResource({
970+
name: 'test-inference-service-latest',
971+
namespace: 'test-project',
972+
displayName: 'Latest Model',
973+
modelName: 'test-inference-service-latest',
974+
});
975+
const inferenceServiceOutdated = mockInferenceServiceK8sResource({
976+
name: 'test-inference-service-outdated',
977+
namespace: 'test-project',
978+
displayName: 'Outdated Model',
979+
modelName: 'test-inference-service-outdated',
980+
});
955981

956982
initIntercepts({
957-
servingRuntimes: [servingRuntimeWithVersion],
983+
servingRuntimes: [servingRuntimeWithLatestVersion, servingRuntimeWithOutdatedVersion],
984+
inferenceServices: [inferenceServiceLatest, inferenceServiceOutdated],
958985
});
959986

960-
modelServingGlobal.visit();
961-
modelServingGlobal.findServingRuntimeVersionLabel().should('exist');
962-
modelServingGlobal.findServingRuntimeVersionLabel().should('contain.text', '1.2.3');
987+
modelServingGlobal.visit('test-project');
988+
989+
const latestRow = modelServingSection.getInferenceServiceRow('Latest Model');
990+
latestRow.findServingRuntimeVersionLabel().should('contain.text', '1.0.0');
991+
latestRow.findServingRuntimeVersionStatusLabel().should('have.text', 'Latest');
992+
993+
const outdatedRow = modelServingSection.getInferenceServiceRow('Outdated Model');
994+
outdatedRow.findServingRuntimeVersionLabel().should('contain.text', '0.5.0');
995+
outdatedRow.findServingRuntimeVersionStatusLabel().should('have.text', 'Outdated');
963996
});
964997

965998
it('Not display the version label if the annotation is absent', () => {
@@ -969,8 +1002,15 @@ describe('Model Serving Global', () => {
9691002
servingRuntimes: [servingRuntimeWithoutVersion],
9701003
});
9711004

972-
modelServingGlobal.visit();
973-
modelServingGlobal.findServingRuntimeVersionLabel().should('not.exist');
1005+
modelServingGlobal.visit('test-project');
1006+
modelServingSection
1007+
.getInferenceServiceRow('Test Inference Service')
1008+
.findServingRuntimeVersionLabel()
1009+
.should('not.exist');
1010+
modelServingSection
1011+
.getInferenceServiceRow('Test Inference Service')
1012+
.findServingRuntimeVersionStatusLabel()
1013+
.should('not.exist');
9741014
});
9751015

9761016
it('Should display env vars from a valueFrom secret', () => {

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ type HandlersProps = {
9898
disableProjectScoped?: boolean;
9999
disableHardwareProfiles?: boolean;
100100
};
101+
import { STOP_MODAL_PREFERENCE_KEY } from '#~/pages/modelServing/useStopModalPreference';
101102

102103
const initIntercepts = ({
103104
disableKServeConfig,
@@ -1340,6 +1341,8 @@ describe('Serving Runtime List', () => {
13401341
}),
13411342
],
13421343
});
1344+
cy.clearLocalStorage(STOP_MODAL_PREFERENCE_KEY);
1345+
cy.window().then((win) => win.localStorage.setItem(STOP_MODAL_PREFERENCE_KEY, 'false'));
13431346
projectDetails.visitSection('test-project', 'model-server');
13441347

13451348
const kserveRow = modelServingSection.getKServeRow('test-model');
@@ -1373,8 +1376,18 @@ describe('Serving Runtime List', () => {
13731376
);
13741377

13751378
kserveRow.findStateActionToggle().should('have.text', 'Stop').click();
1379+
kserveRow.findConfirmStopModal().should('exist');
1380+
kserveRow.findConfirmStopModalCheckbox().should('exist');
1381+
kserveRow.findConfirmStopModalCheckbox().should('not.be.checked');
1382+
kserveRow.findConfirmStopModalCheckbox().click();
1383+
kserveRow.findConfirmStopModalCheckbox().should('be.checked');
1384+
kserveRow.findConfirmStopModalButton().click();
13761385
cy.wait(['@stopModelPatch', '@getStoppedModel']);
13771386
kserveRow.findStateActionToggle().should('have.text', 'Start');
1387+
cy.window().then((win) => {
1388+
const preference = win.localStorage.getItem(STOP_MODAL_PREFERENCE_KEY);
1389+
expect(preference).to.equal('true');
1390+
});
13781391

13791392
const runningInferenceService = mockInferenceServiceK8sResource({
13801393
name: 'test-model',
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React from 'react';
2+
import { Button } from '@patternfly/react-core';
3+
import ConfirmStopModal from '#~/pages/projects/components/ConfirmStopModal';
4+
import useStopModalPreference from './useStopModalPreference';
5+
6+
type ModelServingStopModalProps = {
7+
modelName: string;
8+
title: string;
9+
onClose: (confirmStatus: boolean) => void;
10+
};
11+
12+
const ModelServingStopModal: React.FC<ModelServingStopModalProps> = ({
13+
modelName,
14+
title,
15+
onClose,
16+
}) => {
17+
const [dontShowModalValue, setDontShowModalValue] = useStopModalPreference();
18+
19+
const onBeforeClose = (confirmStatus: boolean) => {
20+
if (!confirmStatus) {
21+
setDontShowModalValue(false);
22+
}
23+
onClose(confirmStatus);
24+
};
25+
const modalActions = [
26+
<Button
27+
key="confirm"
28+
variant="primary"
29+
onClick={() => onBeforeClose(true)}
30+
data-testid="stop-model-button"
31+
>
32+
Stop
33+
</Button>,
34+
<Button
35+
key="cancel"
36+
variant="secondary"
37+
onClick={() => onBeforeClose(false)}
38+
data-testid="cancel-stop-model-button"
39+
>
40+
Cancel
41+
</Button>,
42+
];
43+
return (
44+
<ConfirmStopModal
45+
message={
46+
<>
47+
Are you sure you want to stop <strong>{modelName}</strong>?
48+
</>
49+
}
50+
modalActions={modalActions}
51+
onBeforeClose={onBeforeClose}
52+
title={title}
53+
dataTestId="stop-model-modal"
54+
dontShowModalValue={dontShowModalValue}
55+
setDontShowModalValue={setDontShowModalValue}
56+
/>
57+
);
58+
};
59+
60+
export default ModelServingStopModal;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as React from 'react';
2+
import { TemplateKind } from '#~/k8sTypes';
3+
import { useTemplates } from '#~/api';
4+
import { useDashboardNamespace } from '#~/redux/selectors';
5+
import { findTemplateByName } from './utils';
6+
7+
export const useTemplateByName = (
8+
templateName?: string,
9+
): [TemplateKind | undefined, boolean, Error | undefined] => {
10+
const { dashboardNamespace } = useDashboardNamespace();
11+
const [templates, loaded, error] = useTemplates(dashboardNamespace);
12+
13+
const template = React.useMemo(() => {
14+
if (!templateName || !loaded || error) {
15+
return undefined;
16+
}
17+
return findTemplateByName(templates, templateName);
18+
}, [templates, templateName, loaded, error]);
19+
20+
return [template, loaded, error];
21+
};

frontend/src/pages/modelServing/customServingRuntimes/utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,16 @@ export const getServingRuntimeVersion = (
140140
return resource.metadata.annotations?.['opendatahub.io/runtime-version'] || undefined;
141141
};
142142

143+
export const getTemplateNameFromServingRuntime = (
144+
resource: ServingRuntimeKind,
145+
): string | undefined => resource.metadata.annotations?.['opendatahub.io/template-name'];
146+
147+
export const findTemplateByName = (
148+
templates: TemplateKind[],
149+
templateName: string,
150+
): TemplateKind | undefined =>
151+
templates.find((t) => getServingRuntimeNameFromTemplate(t) === templateName);
152+
143153
export const isTemplateKind = (
144154
resource: ServingRuntimeKind | TemplateKind,
145155
): resource is TemplateKind => resource.kind === 'Template';
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Alert, AlertProps, Label, Popover } from '@patternfly/react-core';
2+
import * as React from 'react';
3+
import { CheckCircleIcon, ExclamationTriangleIcon } from '@patternfly/react-icons';
4+
import { ServingRuntimeVersionStatusLabel } from './const';
5+
6+
type ServingRuntimeVersionStatusProps = {
7+
isOutdated?: boolean;
8+
version: string;
9+
templateVersion: string;
10+
};
11+
12+
const ServingRuntimeVersionStatus: React.FC<ServingRuntimeVersionStatusProps> = ({
13+
isOutdated,
14+
version,
15+
templateVersion,
16+
}) => {
17+
const [isPopoverVisible, setIsPopoverVisible] = React.useState(false);
18+
const getPopoverText = ():
19+
| Record<string, never>
20+
| {
21+
title: string;
22+
body: React.ReactNode;
23+
variant: AlertProps['variant'];
24+
footer?: React.ReactNode;
25+
} => {
26+
if (isOutdated) {
27+
return {
28+
title: 'Serving runtime outdated',
29+
body: (
30+
<p>
31+
This serving runtime is outdated.
32+
<br />
33+
<b>Your version:</b> {version}
34+
<br />
35+
<b>Latest version:</b> {templateVersion}
36+
</p>
37+
),
38+
variant: 'warning',
39+
};
40+
}
41+
return {
42+
title: 'Serving runtime up to date',
43+
body: <p>This serving runtime is up to date.</p>,
44+
variant: 'success',
45+
};
46+
};
47+
const { title, body, variant } = getPopoverText();
48+
return (
49+
<>
50+
<Popover
51+
isVisible={isPopoverVisible}
52+
shouldOpen={() => setIsPopoverVisible(true)}
53+
shouldClose={() => setIsPopoverVisible(false)}
54+
headerContent={<Alert variant={variant} isInline isPlain title={title} />}
55+
bodyContent={body}
56+
>
57+
<Label
58+
data-testid="serving-runtime-version-status-label"
59+
color={isOutdated ? 'yellow' : 'green'}
60+
icon={isOutdated ? <ExclamationTriangleIcon /> : <CheckCircleIcon />}
61+
isCompact
62+
>
63+
{isOutdated
64+
? ServingRuntimeVersionStatusLabel.OUTDATED
65+
: ServingRuntimeVersionStatusLabel.LATEST}
66+
</Label>
67+
</Popover>
68+
</>
69+
);
70+
};
71+
export default ServingRuntimeVersionStatus;

frontend/src/pages/modelServing/screens/const.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,8 @@ export enum ScopedType {
7272
Project = 'Project-scoped',
7373
Global = 'Global-scoped',
7474
}
75+
76+
export enum ServingRuntimeVersionStatusLabel {
77+
LATEST = 'Latest',
78+
OUTDATED = 'Outdated',
79+
}

0 commit comments

Comments
 (0)