Skip to content

Commit d4ef1ca

Browse files
authored
feat: use listValues to render Image and Pod Config steps in Workspac… (#1098)
* feat: use listValues to render Image and Pod Config steps in Workspace Form Signed-off-by: Charles Thao <cthao@redhat.com> * fix: make error message display more explicit Signed-off-by: Charles Thao <cthao@redhat.com> --------- Signed-off-by: Charles Thao <cthao@redhat.com>
1 parent d225446 commit d4ef1ca

26 files changed

Lines changed: 637 additions & 186 deletions

workspaces/frontend/config/cspell-ignore-words.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@ unhover
1818
unhovering
1919
toleration
2020
tolerations
21+
podtemplate
22+
listvalues
23+
cuda

workspaces/frontend/src/__tests__/cypress/cypress/support/commands/api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { UserSettings } from 'mod-arch-core';
22
import type {
33
ApiErrorEnvelope,
44
ApiNamespaceListEnvelope,
5+
ApiPodTemplateOptionsEnvelope,
56
ApiPVCCreateEnvelope,
67
ApiPVCListEnvelope,
78
ApiSecretCreateEnvelope,
@@ -103,6 +104,11 @@ declare global {
103104
options: { path: { apiVersion: string; kind: string } },
104105
response: ApiWorkspaceKindEnvelope | ApiErrorEnvelope,
105106
) => Cypress.Chainable<null>) &
107+
((
108+
type: 'POST /api/:apiVersion/workspacekinds/:kind/podtemplate/options/listvalues',
109+
options: { path: { apiVersion: string; kind: string } },
110+
response: ApiPodTemplateOptionsEnvelope | ApiErrorEnvelope,
111+
) => Cypress.Chainable<null>) &
106112
((
107113
type: 'GET /api/:apiVersion/persistentvolumeclaims/:namespace',
108114
options: { path: { apiVersion: string; namespace: string } },

workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/createWorkspace.cy.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
buildMockWorkspaceCreate,
1212
buildMockWorkspaceKind,
1313
} from '~/shared/mock/mockBuilder';
14+
import { interceptListValues } from '~/__tests__/cypress/cypress/utils/testBuilders';
1415
import { navBar } from '~/__tests__/cypress/cypress/pages/components/navBar';
1516
import {
1617
secretsDetachModal,
@@ -119,6 +120,8 @@ describe('Create workspace', () => {
119120
mockModArchResponse([mockWorkspaceKind]),
120121
).as('getWorkspaceKinds');
121122

123+
interceptListValues(mockWorkspaceKind);
124+
122125
workspaces.visit();
123126
cy.wait('@getNamespaces');
124127

@@ -364,6 +367,7 @@ describe('Create workspace', () => {
364367
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
365368
mockModArchResponse([mockWorkspaceKindWithMultipleImages]),
366369
).as('getWorkspaceKindsMultiple');
370+
interceptListValues(mockWorkspaceKindWithMultipleImages);
367371

368372
createWorkspace.visit();
369373
cy.wait('@getWorkspaceKindsMultiple');
@@ -414,6 +418,7 @@ describe('Create workspace', () => {
414418
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
415419
mockModArchResponse([mockWorkspaceKindWithMultiplePodConfigs]),
416420
).as('getWorkspaceKindsMultiplePodConfigs');
421+
interceptListValues(mockWorkspaceKindWithMultiplePodConfigs);
417422

418423
createWorkspace.visit();
419424
cy.wait('@getWorkspaceKindsMultiplePodConfigs');
@@ -451,6 +456,8 @@ describe('Create workspace', () => {
451456
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
452457
mockModArchResponse([mockWorkspaceKind, mockWorkspaceKind2]),
453458
).as('getMultipleWorkspaceKinds');
459+
interceptListValues(mockWorkspaceKind);
460+
interceptListValues(mockWorkspaceKind2);
454461

455462
createWorkspace.visit();
456463
cy.wait('@getMultipleWorkspaceKinds');
@@ -528,6 +535,7 @@ describe('Create workspace', () => {
528535
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
529536
mockModArchResponse([mockWorkspaceKindSingleImage]),
530537
).as('getWorkspaceKindsSingleImage');
538+
interceptListValues(mockWorkspaceKindSingleImage);
531539

532540
createWorkspace.visit();
533541
cy.wait('@getWorkspaceKindsSingleImage');
@@ -564,6 +572,7 @@ describe('Create workspace', () => {
564572
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
565573
mockModArchResponse([mockWorkspaceKindSinglePodConfig]),
566574
).as('getWorkspaceKindsSinglePodConfig');
575+
interceptListValues(mockWorkspaceKindSinglePodConfig);
567576

568577
createWorkspace.visit();
569578
cy.wait('@getWorkspaceKindsSinglePodConfig');
@@ -691,6 +700,7 @@ describe('Create workspace', () => {
691700
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
692701
mockModArchResponse([mockWorkspaceKindWithMultipleOptions]),
693702
).as('getWorkspaceKindsWithOptions');
703+
interceptListValues(mockWorkspaceKindWithMultipleOptions);
694704
});
695705

696706
it('should filter images by name', () => {
@@ -742,6 +752,7 @@ describe('Create workspace', () => {
742752
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
743753
mockModArchResponse([mockWorkspaceKindWithMultipleOptions]),
744754
).as('getWorkspaceKindsWithOptions');
755+
interceptListValues(mockWorkspaceKindWithMultipleOptions);
745756
});
746757

747758
it('should filter pod configs by name', () => {
@@ -1431,4 +1442,145 @@ describe('Create workspace', () => {
14311442
});
14321443
});
14331444
});
1445+
1446+
describe('ListValues options', () => {
1447+
const cpuImage: OptionsImageConfigValue = {
1448+
id: 'jupyter_cpu',
1449+
displayName: 'jupyter-scipy:v2.1.0',
1450+
description: 'JupyterLab, CPU only',
1451+
labels: [{ key: 'pythonVersion', value: '3.13' }],
1452+
hidden: false,
1453+
};
1454+
1455+
const cudaImage: OptionsImageConfigValue = {
1456+
id: 'jupyter_cuda',
1457+
displayName: 'jupyter-scipy:v2.1.0-cuda',
1458+
description: 'JupyterLab with CUDA support',
1459+
labels: [
1460+
{ key: 'pythonVersion', value: '3.13' },
1461+
{ key: 'cuda', value: '12.4' },
1462+
{ key: 'gpuRequired', value: 'true' },
1463+
],
1464+
hidden: false,
1465+
};
1466+
1467+
const smallCpuPod: OptionsPodConfigValue = {
1468+
id: 'small_cpu',
1469+
displayName: 'Small CPU',
1470+
description: 'Pod with 0.5 CPU, 512 Mb RAM',
1471+
labels: [
1472+
{ key: 'cpu', value: '500m' },
1473+
{ key: 'memory', value: '512Mi' },
1474+
],
1475+
hidden: false,
1476+
};
1477+
1478+
const bigGpuPod: OptionsPodConfigValue = {
1479+
id: 'big_gpu',
1480+
displayName: 'Big GPU',
1481+
description: 'Pod with 4 CPU, 16 GB RAM, and 1 GPU',
1482+
labels: [
1483+
{ key: 'cpu', value: '4000m' },
1484+
{ key: 'memory', value: '16Gi' },
1485+
{ key: 'gpu', value: '1' },
1486+
],
1487+
hidden: false,
1488+
};
1489+
1490+
it('should show all images and pod configs from listValues response', () => {
1491+
const kind = buildMockWorkspaceKind({
1492+
podTemplate: {
1493+
...mockWorkspaceKind.podTemplate,
1494+
options: {
1495+
imageConfig: { default: cpuImage.id, values: [cpuImage, cudaImage] },
1496+
podConfig: { default: smallCpuPod.id, values: [smallCpuPod, bigGpuPod] },
1497+
},
1498+
},
1499+
});
1500+
1501+
cy.interceptApi(
1502+
'GET /api/:apiVersion/workspacekinds',
1503+
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
1504+
mockModArchResponse([kind]),
1505+
).as('getWorkspaceKindsListValues');
1506+
interceptListValues(kind);
1507+
1508+
createWorkspace.visit();
1509+
cy.wait('@getWorkspaceKindsListValues');
1510+
1511+
createWorkspace.selectKind(kind.name);
1512+
createWorkspace.clickNext();
1513+
1514+
createWorkspace.findImageCard(cpuImage.id).should('be.visible');
1515+
createWorkspace.findImageCard(cudaImage.id).should('be.visible');
1516+
1517+
createWorkspace.selectImage(cudaImage.id);
1518+
createWorkspace.clickNext();
1519+
1520+
createWorkspace.findPodConfigCard(smallCpuPod.id).should('be.visible');
1521+
createWorkspace.findPodConfigCard(bigGpuPod.id).should('be.visible');
1522+
});
1523+
1524+
it('should show only CPU pod configs when listValues returns no GPU options', () => {
1525+
const kind = buildMockWorkspaceKind({
1526+
podTemplate: {
1527+
...mockWorkspaceKind.podTemplate,
1528+
options: {
1529+
imageConfig: { default: cpuImage.id, values: [cpuImage] },
1530+
podConfig: { default: smallCpuPod.id, values: [smallCpuPod] },
1531+
},
1532+
},
1533+
});
1534+
1535+
cy.interceptApi(
1536+
'GET /api/:apiVersion/workspacekinds',
1537+
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
1538+
mockModArchResponse([kind]),
1539+
).as('getWorkspaceKindsListValues');
1540+
interceptListValues(kind);
1541+
1542+
createWorkspace.visit();
1543+
cy.wait('@getWorkspaceKindsListValues');
1544+
1545+
createWorkspace.selectKind(kind.name);
1546+
createWorkspace.clickNext();
1547+
1548+
createWorkspace.selectImage(cpuImage.id);
1549+
createWorkspace.clickNext();
1550+
1551+
createWorkspace.findPodConfigCard(smallCpuPod.id).should('be.visible');
1552+
createWorkspace.findPodConfigCard(bigGpuPod.id).should('not.exist');
1553+
});
1554+
1555+
it('should auto-select default image and pod config from listValues', () => {
1556+
const kind = buildMockWorkspaceKind({
1557+
podTemplate: {
1558+
...mockWorkspaceKind.podTemplate,
1559+
options: {
1560+
imageConfig: { default: cudaImage.id, values: [cpuImage, cudaImage] },
1561+
podConfig: { default: bigGpuPod.id, values: [smallCpuPod, bigGpuPod] },
1562+
},
1563+
},
1564+
});
1565+
1566+
cy.interceptApi(
1567+
'GET /api/:apiVersion/workspacekinds',
1568+
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
1569+
mockModArchResponse([kind]),
1570+
).as('getWorkspaceKindsListValues');
1571+
interceptListValues(kind);
1572+
1573+
createWorkspace.visit();
1574+
cy.wait('@getWorkspaceKindsListValues');
1575+
1576+
createWorkspace.selectKind(kind.name);
1577+
createWorkspace.clickNext();
1578+
1579+
createWorkspace.assertImageSelected(cudaImage.id);
1580+
1581+
createWorkspace.clickNext();
1582+
1583+
createWorkspace.assertPodConfigSelected(bigGpuPod.id);
1584+
});
1585+
});
14341586
});

workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/defaultSelection.cy.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { mockModArchResponse } from 'mod-arch-core';
22
import { createWorkspace } from '~/__tests__/cypress/cypress/pages/workspaces/createWorkspace';
33
import { NOTEBOOKS_API_VERSION } from '~/__tests__/cypress/cypress/support/commands/api';
44
import { buildMockNamespace, buildMockWorkspaceKind } from '~/shared/mock/mockBuilder';
5+
import { interceptListValues } from '~/__tests__/cypress/cypress/utils/testBuilders';
56

67
type ImageConfigOption = {
78
id: string;
@@ -100,6 +101,7 @@ describe('Workspace Form - Default Selection', () => {
100101
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
101102
mockModArchResponse([mockWorkspaceKind]),
102103
).as('getWorkspaceKinds');
104+
interceptListValues(mockWorkspaceKind);
103105

104106
createWorkspace.visit();
105107
cy.wait('@getWorkspaceKinds');
@@ -133,6 +135,7 @@ describe('Workspace Form - Default Selection', () => {
133135
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
134136
mockModArchResponse([mockWorkspaceKind]),
135137
).as('getWorkspaceKinds');
138+
interceptListValues(mockWorkspaceKind);
136139

137140
createWorkspace.visit();
138141
cy.wait('@getWorkspaceKinds');
@@ -172,6 +175,7 @@ describe('Workspace Form - Default Selection', () => {
172175
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
173176
mockModArchResponse([mockWorkspaceKind]),
174177
).as('getWorkspaceKinds');
178+
interceptListValues(mockWorkspaceKind);
175179

176180
createWorkspace.visit();
177181
cy.wait('@getWorkspaceKinds');
@@ -210,6 +214,7 @@ describe('Workspace Form - Default Selection', () => {
210214
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
211215
mockModArchResponse([mockWorkspaceKind]),
212216
).as('getWorkspaceKinds');
217+
interceptListValues(mockWorkspaceKind);
213218

214219
createWorkspace.visit();
215220
cy.wait('@getWorkspaceKinds');
@@ -253,6 +258,7 @@ describe('Workspace Form - Default Selection', () => {
253258
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
254259
mockModArchResponse([mockWorkspaceKind]),
255260
).as('getWorkspaceKinds');
261+
interceptListValues(mockWorkspaceKind);
256262

257263
createWorkspace.visit();
258264
cy.wait('@getWorkspaceKinds');
@@ -288,6 +294,7 @@ describe('Workspace Form - Default Selection', () => {
288294
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
289295
mockModArchResponse([mockWorkspaceKind]),
290296
).as('getWorkspaceKinds');
297+
interceptListValues(mockWorkspaceKind);
291298

292299
createWorkspace.visit();
293300
cy.wait('@getWorkspaceKinds');

workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/editWorkspace.cy.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
buildMockWorkspaceUpdate,
1414
} from '~/shared/mock/mockBuilder';
1515
import { NOTEBOOKS_API_VERSION } from '~/__tests__/cypress/cypress/support/commands/api';
16+
import { interceptListValues } from '~/__tests__/cypress/cypress/utils/testBuilders';
1617
import { V1Beta1WorkspaceState } from '~/generated/data-contracts';
1718
import { toastNotification } from '~/__tests__/cypress/cypress/pages/components/toastNotification';
1819

@@ -116,6 +117,7 @@ const setupEditWorkspace = (): EditWorkspaceSetup => {
116117
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
117118
mockModArchResponse([mockWorkspaceKind]),
118119
).as('getWorkspaceKinds');
120+
interceptListValues(mockWorkspaceKind);
119121

120122
return { mockNamespace, mockWorkspace, mockWorkspaceKind };
121123
};
@@ -372,6 +374,8 @@ describe('Edit workspace', () => {
372374
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
373375
mockModArchResponse([mockWorkspaceKind, differentWorkspaceKind]),
374376
).as('getWorkspaceKinds');
377+
interceptListValues(mockWorkspaceKind);
378+
interceptListValues(differentWorkspaceKind);
375379

376380
visitEditWorkspace();
377381

workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/filterImagesByLabels.cy.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import {
88
buildMockWorkspaceKind,
99
} from '~/shared/mock/mockBuilder';
1010
import { navBar } from '~/__tests__/cypress/cypress/pages/components/navBar';
11-
import { buildMockImageWithLabels } from '~/__tests__/cypress/cypress/utils/testBuilders';
11+
import {
12+
buildMockImageWithLabels,
13+
interceptListValues,
14+
} from '~/__tests__/cypress/cypress/utils/testBuilders';
1215
import type { WorkspacekindsWorkspaceKindListItem } from '~/generated/data-contracts';
1316

1417
const STEP_NAMES = {
@@ -137,6 +140,7 @@ describe('Filter Images by Labels', () => {
137140
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
138141
mockModArchResponse([mockWorkspaceKind]),
139142
).as('getWorkspaceKinds');
143+
interceptListValues(mockWorkspaceKind);
140144

141145
workspaces.visit();
142146
cy.wait('@getNamespaces');

workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/filterPodConfigsByLabels.cy.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import {
88
buildMockWorkspaceKind,
99
} from '~/shared/mock/mockBuilder';
1010
import { navBar } from '~/__tests__/cypress/cypress/pages/components/navBar';
11-
import { buildMockPodConfigWithLabels } from '~/__tests__/cypress/cypress/utils/testBuilders';
11+
import {
12+
buildMockPodConfigWithLabels,
13+
interceptListValues,
14+
} from '~/__tests__/cypress/cypress/utils/testBuilders';
1215
import type { WorkspacekindsWorkspaceKindListItem } from '~/generated/data-contracts';
1316

1417
const STEP_NAMES = {
@@ -98,6 +101,7 @@ describe('Filter Pod Configs by Labels', () => {
98101
{ path: { apiVersion: NOTEBOOKS_API_VERSION } },
99102
mockModArchResponse([mockWorkspaceKind]),
100103
).as('getWorkspaceKinds');
104+
interceptListValues(mockWorkspaceKind);
101105

102106
workspaces.visit();
103107
cy.wait('@getNamespaces');

0 commit comments

Comments
 (0)