Skip to content

Commit b4448db

Browse files
author
Yehudit Kerido
committed
feat(ws): Notebooks 2.0 // Frontend // Fetch workspaces
Signed-off-by: Yehudit Kerido <yehudit.kerido@nokia.com>
1 parent 683e6d3 commit b4448db

File tree

5 files changed

+123
-99
lines changed

5 files changed

+123
-99
lines changed

workspaces/frontend/src/__tests__/cypress/cypress/tests/e2e/Workspaces.cy.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,33 @@
1-
import { WorkspaceState } from '~/shared/types';
21
import { home } from '~/__tests__/cypress/cypress/pages/home';
32
import {
43
mockWorkspaces,
54
mockWorkspacesByNS,
65
} from '~/__tests__/cypress/cypress/tests/mocked/workspace.mock';
6+
import type { Workspace } from '~/shared/types';
77
import { mockNamespaces } from '~/__mocks__/mockNamespaces';
88
import { mockBFFResponse } from '~/__mocks__/utils';
99

1010
// Helper function to validate the content of a single workspace row in the table
11-
const validateWorkspaceRow = (workspace: any, index: number) => {
11+
const validateWorkspaceRow = (workspace: Workspace, index: number) => {
1212
// Validate the workspace name
1313
cy.findByTestId(`workspace-row-${index}`)
1414
.find('[data-testid="workspace-name"]')
1515
.should('have.text', workspace.name);
1616

17-
// Map workspace state to the expected label
18-
const expectedLabel = WorkspaceState[workspace.status.state];
19-
20-
// Validate the state label and pod configuration
21-
cy.findByTestId(`workspace-row-${index}`)
22-
.find('[data-testid="state-label"]')
23-
.should('have.text', expectedLabel);
24-
2517
cy.findByTestId(`workspace-row-${index}`)
2618
.find('[data-testid="pod-config"]')
27-
.should('have.text', workspace.options.podConfig);
19+
.should('have.text', workspace.pod_template.pod_config.current);
2820
};
2921

3022
// Test suite for workspace-related tests
3123
describe('Workspaces Tests', () => {
3224
beforeEach(() => {
3325
home.visit();
26+
27+
cy.intercept('GET', '/api/v1/namespaces', {
28+
body: mockBFFResponse(mockNamespaces),
29+
}).as('getNamespaces');
30+
3431
cy.intercept('GET', '/api/v1/workspaces', {
3532
body: mockBFFResponse(mockWorkspaces),
3633
}).as('getWorkspaces');
@@ -103,11 +100,18 @@ describe('Workspace by namespace functionality', () => {
103100
describe('Workspaces Component', () => {
104101
beforeEach(() => {
105102
// Mock the namespaces API response
103+
104+
cy.visit('/');
106105
cy.intercept('GET', '/api/v1/namespaces', {
107106
body: mockBFFResponse(mockNamespaces),
108107
}).as('getNamespaces');
109-
cy.visit('/');
110108
cy.wait('@getNamespaces');
109+
cy.intercept('GET', 'api/v1/workspaces', {
110+
body: mockBFFResponse(mockWorkspaces),
111+
}).as('getWorkspaces');
112+
cy.intercept('GET', 'api/v1/workspaces/kubeflow', {
113+
body: mockBFFResponse(mockWorkspacesByNS),
114+
});
111115
});
112116

113117
function openDeleteModal() {
@@ -122,6 +126,10 @@ describe('Workspaces Component', () => {
122126
() => cy.get('[aria-label="Close"]').click(),
123127
];
124128

129+
// Change namespace to "kubeflow"
130+
cy.findByTestId('namespace-toggle').click();
131+
cy.findByTestId('dropdown-item-kubeflow').click();
132+
125133
closeModalActions.forEach((closeAction) => {
126134
openDeleteModal();
127135
cy.findByTestId('delete-modal-input').type('Some Text');
@@ -138,6 +146,9 @@ describe('Workspaces Component', () => {
138146
});
139147

140148
it('should verify the delete modal verification mechanism', () => {
149+
// Change namespace to "kubeflow"
150+
cy.findByTestId('namespace-toggle').click();
151+
cy.findByTestId('dropdown-item-kubeflow').click();
141152
openDeleteModal();
142153
cy.findByTestId('delete-modal').within(() => {
143154
cy.get('strong')

workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspace.mock.ts

Lines changed: 48 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Workspace } from '~/shared/types';
12
import { WorkspaceState } from '~/shared/types';
23

34
const generateMockWorkspace = (
@@ -8,60 +9,66 @@ const generateMockWorkspace = (
89
imageConfig: string,
910
podConfig: string,
1011
pvcName: string,
11-
) => {
12+
): Workspace => {
1213
const currentTime = Date.now();
13-
const lastActivity = currentTime - Math.floor(Math.random() * 1000000); // Random last activity time
14-
const lastUpdate = currentTime - Math.floor(Math.random() * 100000); // Random last update time
14+
const lastActivityTime = currentTime - Math.floor(Math.random() * 1000000); // Random last activity time
15+
const lastUpdateTime = currentTime - Math.floor(Math.random() * 100000); // Random last update time
1516

1617
return {
1718
name,
1819
namespace,
20+
workspace_kind: {
21+
name: 'jupyterlab',
22+
},
23+
defer_updates: paused,
1924
paused,
20-
deferUpdates: !!paused,
21-
kind: 'jupyter-lab',
22-
cpu: 3,
23-
ram: 500,
24-
podTemplate: {
25+
paused_time: paused ? currentTime - Math.floor(Math.random() * 1000000) : 0,
26+
state,
27+
state_message:
28+
state === WorkspaceState.Running
29+
? 'Workspace is running smoothly.'
30+
: state === WorkspaceState.Paused
31+
? 'Workspace is paused.'
32+
: 'Workspace is operational.',
33+
pod_template: {
34+
pod_metadata: {
35+
labels: {},
36+
annotations: {},
37+
},
2538
volumes: {
26-
home: '/home',
39+
home: {
40+
pvc_name: `${pvcName}-home`,
41+
mount_path: '/home/jovyan',
42+
readOnly: false,
43+
},
2744
data: [
2845
{
29-
pvcName,
30-
mountPath: '/data',
31-
readOnly: paused, // Randomize based on paused state
46+
pvc_name: pvcName,
47+
mount_path: '/data/my-data',
48+
readOnly: paused, // Set based on the paused state
3249
},
3350
],
3451
},
35-
},
36-
options: {
37-
imageConfig,
38-
podConfig,
39-
},
40-
status: {
41-
activity: {
42-
lastActivity,
43-
lastUpdate,
52+
image_config: {
53+
current: imageConfig,
54+
desired: '',
55+
redirect_chain: [],
4456
},
45-
pauseTime: paused ? currentTime - Math.floor(Math.random() * 1000000) : 0,
46-
pendingRestart: !!paused,
47-
podTemplateOptions: {
48-
imageConfig: {
49-
desired: imageConfig,
50-
redirectChain: [
51-
{
52-
source: 'base-image',
53-
target: `optimized-${Math.floor(Math.random() * 100)}`,
54-
},
55-
],
56-
},
57+
pod_config: {
58+
current: podConfig,
59+
desired: podConfig,
60+
redirect_chain: [],
61+
},
62+
},
63+
activity: {
64+
last_activity: lastActivityTime,
65+
last_update: lastUpdateTime,
66+
last_probe: {
67+
start_time_ms: lastUpdateTime - 1000, // Simulated probe timing
68+
end_time_ms: lastUpdateTime,
69+
result: 'default_result',
70+
message: 'default_message',
5771
},
58-
state,
59-
stateMessage:
60-
state === WorkspaceState.Running
61-
? 'Workspace is running smoothly.'
62-
: state === WorkspaceState.Paused
63-
? 'Workspace is paused.'
64-
: 'Workspace is operational.',
6572
},
6673
};
6774
};
@@ -74,7 +81,7 @@ const generateMockWorkspaces = (numWorkspaces: number, byNamespace = false) => {
7481
'jupyterlab_tensorflow_230',
7582
'jupyterlab_pytorch_120',
7683
];
77-
const namespaces = byNamespace ? ['kubeflow'] : ['kubeflow', 'system', 'user-example'];
84+
const namespaces = byNamespace ? ['kubeflow'] : ['kubeflow', 'system', 'user-example', 'default'];
7885

7986
for (let i = 1; i <= numWorkspaces; i++) {
8087
const state =

workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from 'react';
22
import {
3+
Spinner,
34
Drawer,
45
DrawerContent,
56
DrawerContentBody,
@@ -63,7 +64,7 @@ export enum ActionType {
6364

6465
export const Workspaces: React.FunctionComponent = () => {
6566
const { selectedNamespace } = useNamespaceContext();
66-
const [initialWorkspaces, loading, error] = useWorkspaces(selectedNamespace);
67+
const [initialWorkspaces, loaded, error] = useWorkspaces(selectedNamespace);
6768
if (error) {
6869
console.log(error.message); // TODO: Verify how to handle errors display.
6970
}
@@ -103,12 +104,15 @@ export const Workspaces: React.FunctionComponent = () => {
103104
};
104105

105106
// change when fetch workspaces is implemented
106-
const [workspaces, setWorkspaces] = useState(initialWorkspaces);
107+
const [workspaces, setWorkspaces] = React.useState<Workspace[]>([]);
107108
const [expandedWorkspacesNames, setExpandedWorkspacesNames] = React.useState<string[]>([]);
108109
const [selectedWorkspace, setSelectedWorkspace] = React.useState<Workspace | null>(null);
109110
const [workspaceToDelete, setWorkspaceToDelete] = React.useState<Workspace | null>(null);
110111
const [isActionAlertModalOpen, setIsActionAlertModalOpen] = React.useState(false);
111112
const [activeActionType, setActiveActionType] = React.useState<ActionType | null>(null);
113+
React.useEffect(() => {
114+
setWorkspaces(initialWorkspaces);
115+
}, [initialWorkspaces]);
112116

113117
const selectWorkspace = React.useCallback(
114118
(newSelectedWorkspace) => {
@@ -152,15 +156,15 @@ export const Workspaces: React.FunctionComponent = () => {
152156
case columnNames.name:
153157
return workspace.name.search(searchValueInput) >= 0;
154158
case columnNames.kind:
155-
return workspace.kind.search(searchValueInput) >= 0;
159+
return workspace.workspace_kind.name.search(searchValueInput) >= 0;
156160
case columnNames.image:
157-
return workspace.options.imageConfig.search(searchValueInput) >= 0;
161+
return workspace.pod_template.image_config.current.search(searchValueInput) >= 0;
158162
case columnNames.podConfig:
159-
return workspace.options.podConfig.search(searchValueInput) >= 0;
163+
return workspace.pod_template.pod_config.current.search(searchValueInput) >= 0;
160164
case columnNames.state:
161-
return WorkspaceState[workspace.status.state].search(searchValueInput) >= 0;
165+
return WorkspaceState[workspace.state].search(searchValueInput) >= 0;
162166
case columnNames.homeVol:
163-
return workspace.podTemplate.volumes.home.search(searchValueInput) >= 0;
167+
return workspace.pod_template.volumes.home.pvc_name.search(searchValueInput) >= 0;
164168
default:
165169
return true;
166170
}
@@ -174,20 +178,19 @@ export const Workspaces: React.FunctionComponent = () => {
174178
const [activeSortIndex, setActiveSortIndex] = React.useState<number | null>(null);
175179
const [activeSortDirection, setActiveSortDirection] = React.useState<'asc' | 'desc' | null>(null);
176180

177-
const getSortableRowValues = (workspace: Workspace): (string | number)[] => {
178-
const { redirectStatus, name, kind, image, podConfig, state, homeVol, cpu, ram, lastActivity } =
179-
{
181+
const getSortableRowValues = (workspace: Workspace): (string | number | undefined)[] => {
182+
const { name, kind, image, podConfig, state, homeVol, cpu, ram, lastActivity } = {
180183
redirectStatus: '',
181-
name: workspace.name,
182-
kind: workspace.kind,
183-
image: workspace.options.imageConfig,
184-
podConfig: workspace.options.podConfig,
185-
state: WorkspaceState[workspace.status.state],
186-
homeVol: workspace.podTemplate.volumes.home,
187-
cpu: workspace.cpu,
188-
ram: workspace.ram,
189-
lastActivity: workspace.status.activity.lastActivity,
190-
};
184+
name: workspace.name,
185+
kind: workspace.workspace_kind.name,
186+
image: workspace.pod_template.image_config.current,
187+
podConfig: workspace.pod_template.pod_config.current,
188+
state: workspace.state,
189+
homeVol: workspace.pod_template.volumes.home.pvc_name,
190+
cpu: workspace.cpu,
191+
ram: workspace.ram,
192+
lastActivity: workspace.activity.last_activity,
193+
};
191194
return [redirectStatus, name, kind, image, podConfig, state, homeVol, cpu, ram, lastActivity];
192195
};
193196

workspaces/frontend/src/shared/types.ts

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
export interface WorkspaceIcon {
2-
url: string;
2+
url: string;
33
}
44

55
export interface WorkspaceLogo {
6-
url: string;
6+
url: string;
77
}
88

99
export interface WorkspaceKind {
@@ -67,32 +67,32 @@ export interface WorkspaceKind {
6767
}
6868

6969
export enum WorkspaceState {
70-
Running,
71-
Terminating,
72-
Paused,
73-
Pending,
74-
Error,
75-
Unknown,
70+
Running,
71+
Terminating,
72+
Paused,
73+
Pending,
74+
Error,
75+
Unknown,
7676
}
7777

7878
export interface WorkspaceStatus {
79-
activity: {
80-
lastActivity: number;
81-
lastUpdate: number;
82-
};
83-
pauseTime: number;
84-
pendingRestart: boolean;
85-
podTemplateOptions: {
86-
imageConfig: {
87-
desired: string;
88-
redirectChain: {
89-
source: string;
90-
target: string;
91-
}[];
79+
activity: {
80+
lastActivity: number;
81+
lastUpdate: number;
9282
};
93-
};
94-
state: WorkspaceState;
95-
stateMessage: string;
83+
pauseTime: number;
84+
pendingRestart: boolean;
85+
podTemplateOptions: {
86+
imageConfig: {
87+
desired: string;
88+
redirectChain: {
89+
source: string;
90+
target: string;
91+
}[];
92+
};
93+
};
94+
state: WorkspaceState;
95+
stateMessage: string;
9696
}
9797

9898
export interface Workspace {
@@ -143,4 +143,4 @@ export type WorkspacesColumnNames = {
143143
ram: string;
144144
lastActivity: string;
145145
redirectStatus: string;
146-
};
146+
};

workspaces/frontend/src/shared/utilities/WorkspaceUtils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
export const formatRam = (valueInKb: number): string => {
1+
export const formatRam = (valueInKb: number | undefined | null): string => {
2+
if (valueInKb === undefined || valueInKb === null) {
3+
return 'N/A';
4+
}
25
const units = ['KB', 'MB', 'GB', 'TB'];
36
let index = 0;
47
let value = valueInKb;

0 commit comments

Comments
 (0)