Skip to content

Commit 055150b

Browse files
liavweissLiav Weiss (EXT-Nokia)
andauthored
feat(ws): Notebooks 2.0 // Frontend // Workspaces table // Workspace Kind column #148 (#177)
* Merge notebooks-v2 into kind_logo_modification/#148 branch Signed-off-by: Liav Weiss (EXT-Nokia) <liav.weiss.ext@nokia.com> * feat(ws): Notebooks 2.0 // Frontend // Workspaces table // Workspace Kind column #148 Signed-off-by: Liav Weiss (EXT-Nokia) <liav.weiss.ext@nokia.com> * feat(ws): Notebooks 2.0 // Frontend // Workspaces table // Workspace Kind column #148 Signed-off-by: Liav Weiss (EXT-Nokia) <liav.weiss.ext@nokia.com> * feat(ws): Notebooks 2.0 // Frontend // Workspaces table // Workspace Kind column #148 Signed-off-by: Liav Weiss (EXT-Nokia) <liav.weiss.ext@nokia.com> * feat(ws): Notebooks 2.0 // Frontend // Workspaces table // Workspace Kind column #148 Signed-off-by: Liav Weiss (EXT-Nokia) <liav.weiss.ext@nokia.com> * feat(ws): Notebooks 2.0 // Frontend // Workspaces table // Workspace Kind column #148 Signed-off-by: Liav Weiss (EXT-Nokia) <liav.weiss.ext@nokia.com> * feat(ws): Notebooks 2.0 // Frontend // Workspaces table // Workspace Kind column #148 Signed-off-by: Liav Weiss (EXT-Nokia) <liav.weiss.ext@nokia.com> --------- Signed-off-by: Liav Weiss (EXT-Nokia) <liav.weiss.ext@nokia.com> Co-authored-by: Liav Weiss (EXT-Nokia) <liav.weiss.ext@nokia.com>
1 parent 2c05c38 commit 055150b

File tree

11 files changed

+283
-9
lines changed

11 files changed

+283
-9
lines changed

workspaces/frontend/package-lock.json

Lines changed: 6 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

workspaces/frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
"react-router-dom": "^6.26.1",
7676
"regenerator-runtime": "^0.13.11",
7777
"rimraf": "^6.0.1",
78-
"sass": "^1.83.1",
78+
"sass": "^1.83.4",
7979
"sass-loader": "^16.0.4",
8080
"serve": "^14.2.1",
8181
"style-loader": "^3.3.4",
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {
2+
mockWorkspaceKindsInValid,
3+
mockWorkspaceKindsValid,
4+
} from '~/__tests__/cypress/cypress/tests/mocked/workspaceKinds.mock';
5+
6+
describe('Test buildKindLogoDictionary Functionality', () => {
7+
// Mock valid workspace kinds
8+
context('With Valid Data', () => {
9+
before(() => {
10+
// Mock the API response
11+
cy.intercept('GET', '/api/v1/workspacekinds', {
12+
statusCode: 200,
13+
body: mockWorkspaceKindsValid,
14+
});
15+
16+
// Visit the page
17+
cy.visit('/');
18+
});
19+
20+
it('should fetch and populate kind logos', () => {
21+
// Check that the logos are rendered in the table
22+
cy.get('tbody tr').each(($row) => {
23+
cy.wrap($row)
24+
.find('td[data-label="Kind"]')
25+
.within(() => {
26+
cy.get('img')
27+
.should('exist')
28+
.then(($img) => {
29+
// Ensure the image is fully loaded
30+
cy.wrap($img[0]).should('have.prop', 'complete', true);
31+
});
32+
});
33+
});
34+
});
35+
});
36+
37+
// Mock invalid workspace kinds
38+
context('With Invalid Data', () => {
39+
before(() => {
40+
// Mock the API response for invalid workspace kinds
41+
cy.intercept('GET', '/api/v1/workspacekinds', {
42+
statusCode: 200,
43+
body: mockWorkspaceKindsInValid,
44+
});
45+
46+
// Visit the page
47+
cy.visit('/');
48+
});
49+
50+
it('should show a fallback icon when the logo URL is missing', () => {
51+
cy.get('tbody tr').each(($row) => {
52+
cy.wrap($row)
53+
.find('td[data-label="Kind"]')
54+
.within(() => {
55+
// Ensure that the image is NOT rendered (because it's invalid or missing)
56+
cy.get('img').should('not.exist'); // No images should be displayed
57+
58+
// Check if the fallback icon (TimesCircleIcon) is displayed
59+
cy.get('svg').should('exist'); // Look for the SVG (TimesCircleIcon)
60+
cy.get('svg').should('have.class', 'pf-v6-svg'); // Ensure the correct fallback icon class is applied (update the class name based on your icon library)
61+
});
62+
});
63+
});
64+
});
65+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import type { WorkspaceKind } from '~/shared/types';
2+
3+
// Factory function to create a valid WorkspaceKind
4+
function createMockWorkspaceKind(overrides: Partial<WorkspaceKind> = {}): WorkspaceKind {
5+
return {
6+
name: 'jupyter-lab',
7+
displayName: 'JupyterLab Notebook',
8+
description: 'A Workspace which runs JupyterLab in a Pod',
9+
deprecated: false,
10+
deprecationMessage: '',
11+
hidden: false,
12+
icon: {
13+
url: 'https://jupyter.org/assets/favicons/apple-touch-icon-152x152.png',
14+
},
15+
logo: {
16+
url: 'https://upload.wikimedia.org/wikipedia/commons/3/38/Jupyter_logo.svg',
17+
},
18+
podTemplate: {
19+
podMetadata: {
20+
labels: { myWorkspaceKindLabel: 'my-value' },
21+
annotations: { myWorkspaceKindAnnotation: 'my-value' },
22+
},
23+
volumeMounts: { home: '/home/jovyan' },
24+
options: {
25+
imageConfig: {
26+
default: 'jupyterlab_scipy_190',
27+
values: [
28+
{
29+
id: 'jupyterlab_scipy_180',
30+
displayName: 'jupyter-scipy:v1.8.0',
31+
labels: { pythonVersion: '3.11' },
32+
hidden: true,
33+
redirect: {
34+
to: 'jupyterlab_scipy_190',
35+
message: {
36+
text: 'This update will change...',
37+
level: 'Info',
38+
},
39+
},
40+
},
41+
],
42+
},
43+
podConfig: {
44+
default: 'tiny_cpu',
45+
values: [
46+
{
47+
id: 'tiny_cpu',
48+
displayName: 'Tiny CPU',
49+
description: 'Pod with 0.1 CPU, 128 Mb RAM',
50+
labels: { cpu: '100m', memory: '128Mi' },
51+
},
52+
],
53+
},
54+
},
55+
},
56+
...overrides, // Allows customization
57+
};
58+
}
59+
60+
// Generate valid mock data with "data" property
61+
export const mockWorkspaceKindsValid = {
62+
data: [createMockWorkspaceKind()],
63+
};
64+
65+
// Generate invalid mock data with "data" property
66+
export const mockWorkspaceKindsInValid = {
67+
data: [
68+
createMockWorkspaceKind({
69+
logo: {
70+
url: '',
71+
},
72+
}),
73+
],
74+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { WorkspaceKind } from '~/shared/types';
2+
3+
type KindLogoDict = Record<string, string>;
4+
5+
/**
6+
* Builds a dictionary of kind names to logos, and returns it.
7+
* @param {WorkspaceKind[]} workspaceKinds - The list of workspace kinds.
8+
* @returns {KindLogoDict} A dictionary with kind names as keys and logo URLs as values.
9+
*/
10+
export function buildKindLogoDictionary(workspaceKinds: WorkspaceKind[] | []): KindLogoDict {
11+
const kindLogoDict: KindLogoDict = {};
12+
13+
for (const workspaceKind of workspaceKinds) {
14+
try {
15+
kindLogoDict[workspaceKind.name] = workspaceKind.logo.url;
16+
} catch {
17+
kindLogoDict[workspaceKind.name] = '';
18+
}
19+
}
20+
return kindLogoDict;
21+
}

workspaces/frontend/src/app/context/useNotebookAPIState.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import { APIState } from '~/shared/api/types';
33
import { NotebookAPIs } from '~/app/types';
4-
import { getNamespaces } from '~/shared/api/notebookService';
4+
import { getNamespaces, getWorkspaceKinds } from '~/shared/api/notebookService';
55
import useAPIState from '~/shared/api/useAPIState';
66

77
export type NotebookAPIState = APIState<NotebookAPIs>;
@@ -12,6 +12,7 @@ const useNotebookAPIState = (
1212
const createAPI = React.useCallback(
1313
(path: string) => ({
1414
getNamespaces: getNamespaces(path),
15+
getWorkspaceKinds: getWorkspaceKinds(path),
1516
}),
1617
[],
1718
);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as React from 'react';
2+
import useFetchState, {
3+
FetchState,
4+
FetchStateCallbackPromise,
5+
} from '~/shared/utilities/useFetchState';
6+
import { WorkspaceKind } from '~/shared/types';
7+
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
8+
9+
const useWorkspaceKinds = (): FetchState<WorkspaceKind[]> => {
10+
const { api, apiAvailable } = useNotebookAPI();
11+
const call = React.useCallback<FetchStateCallbackPromise<WorkspaceKind[]>>(
12+
(opts) => {
13+
if (!apiAvailable) {
14+
return Promise.reject(new Error('API not yet available'));
15+
}
16+
return api.getWorkspaceKinds(opts);
17+
},
18+
[api, apiAvailable],
19+
);
20+
21+
return useFetchState(call, []);
22+
};
23+
24+
export default useWorkspaceKinds;

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
Pagination,
1212
Button,
1313
Content,
14+
Tooltip,
15+
Brand,
1416
} from '@patternfly/react-core';
1517
import {
1618
Table,
@@ -24,10 +26,13 @@ import {
2426
IActions,
2527
} from '@patternfly/react-table';
2628
import { useState } from 'react';
29+
import { CodeIcon } from '@patternfly/react-icons';
2730
import { Workspace, WorkspacesColumnNames, WorkspaceState } from '~/shared/types';
2831
import { WorkspaceDetails } from '~/app/pages/Workspaces/Details/WorkspaceDetails';
2932
import { ExpandedWorkspaceRow } from '~/app/pages/Workspaces/ExpandedWorkspaceRow';
3033
import DeleteModal from '~/shared/components/DeleteModal';
34+
import { buildKindLogoDictionary } from '~/app/actions/WorkspaceKindsActions';
35+
import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds';
3136
import Filter, { FilteredColumn } from 'shared/components/Filter';
3237
import { formatRam } from 'shared/utilities/WorkspaceResources';
3338

@@ -131,6 +136,10 @@ export const Workspaces: React.FunctionComponent = () => {
131136
},
132137
];
133138

139+
const [workspaceKinds] = useWorkspaceKinds();
140+
let kindLogoDict: Record<string, string> = {};
141+
kindLogoDict = buildKindLogoDictionary(workspaceKinds);
142+
134143
// Table columns
135144
const columnNames: WorkspacesColumnNames = {
136145
name: 'Name',
@@ -419,7 +428,21 @@ export const Workspaces: React.FunctionComponent = () => {
419428
}}
420429
/>
421430
<Td dataLabel={columnNames.name}>{workspace.name}</Td>
422-
<Td dataLabel={columnNames.kind}>{workspace.kind}</Td>
431+
<Td dataLabel={columnNames.kind}>
432+
{kindLogoDict[workspace.kind] ? (
433+
<Tooltip content={workspace.kind}>
434+
<Brand
435+
src={kindLogoDict[workspace.kind]}
436+
alt={workspace.kind}
437+
style={{ width: '20px', height: '20px', cursor: 'pointer' }}
438+
/>
439+
</Tooltip>
440+
) : (
441+
<Tooltip content={workspace.kind}>
442+
<CodeIcon />
443+
</Tooltip>
444+
)}
445+
</Td>
423446
<Td dataLabel={columnNames.image}>{workspace.options.imageConfig}</Td>
424447
<Td dataLabel={columnNames.podConfig}>{workspace.options.podConfig}</Td>
425448
<Td dataLabel={columnNames.state}>

workspaces/frontend/src/app/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { APIOptions } from '~/shared/api/types';
2+
import { WorkspaceKind } from '~/shared/types';
23

34
export type ResponseBody<T> = {
45
data: T;
@@ -64,6 +65,9 @@ export type NamespacesList = Namespace[];
6465

6566
export type GetNamespaces = (opts: APIOptions) => Promise<NamespacesList>;
6667

68+
export type GetWorkspaceKinds = (opts: APIOptions) => Promise<WorkspaceKind[]>;
69+
6770
export type NotebookAPIs = {
6871
getNamespaces: GetNamespaces;
72+
getWorkspaceKinds: GetWorkspaceKinds;
6973
};

workspaces/frontend/src/shared/api/notebookService.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { NamespacesList } from '~/app/types';
22
import { isNotebookResponse, restGET } from '~/shared/api/apiUtils';
33
import { APIOptions } from '~/shared/api/types';
44
import { handleRestFailures } from '~/shared/api/errorUtils';
5+
import { WorkspaceKind } from '~/shared/types';
56

67
export const getNamespaces =
78
(hostPath: string) =>
@@ -12,3 +13,13 @@ export const getNamespaces =
1213
}
1314
throw new Error('Invalid response format');
1415
});
16+
17+
export const getWorkspaceKinds =
18+
(hostPath: string) =>
19+
(opts: APIOptions): Promise<WorkspaceKind[]> =>
20+
handleRestFailures(restGET(hostPath, `/workspacekinds`, {}, opts)).then((response) => {
21+
if (isNotebookResponse<WorkspaceKind[]>(response)) {
22+
return response.data;
23+
}
24+
throw new Error('Invalid response format');
25+
});

0 commit comments

Comments
 (0)