Skip to content

Commit

Permalink
feat(ws): Notebooks 2.0 // Frontend // Workspaces table // Workspace …
Browse files Browse the repository at this point in the history
…Kind column kubeflow#148 (kubeflow#177)

* Merge notebooks-v2 into kind_logo_modification/kubeflow#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 kubeflow#148

Signed-off-by: Liav Weiss (EXT-Nokia) <liav.weiss.ext@nokia.com>

* feat(ws): Notebooks 2.0 // Frontend // Workspaces table // Workspace Kind column kubeflow#148

Signed-off-by: Liav Weiss (EXT-Nokia) <liav.weiss.ext@nokia.com>

* feat(ws): Notebooks 2.0 // Frontend // Workspaces table // Workspace Kind column kubeflow#148

Signed-off-by: Liav Weiss (EXT-Nokia) <liav.weiss.ext@nokia.com>

* feat(ws): Notebooks 2.0 // Frontend // Workspaces table // Workspace Kind column kubeflow#148

Signed-off-by: Liav Weiss (EXT-Nokia) <liav.weiss.ext@nokia.com>

* feat(ws): Notebooks 2.0 // Frontend // Workspaces table // Workspace Kind column kubeflow#148

Signed-off-by: Liav Weiss (EXT-Nokia) <liav.weiss.ext@nokia.com>

* feat(ws): Notebooks 2.0 // Frontend // Workspaces table // Workspace Kind column kubeflow#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>
  • Loading branch information
liavweiss and Liav Weiss (EXT-Nokia) authored Jan 29, 2025
1 parent 2c05c38 commit 055150b
Show file tree
Hide file tree
Showing 11 changed files with 283 additions and 9 deletions.
10 changes: 6 additions & 4 deletions workspaces/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion workspaces/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"react-router-dom": "^6.26.1",
"regenerator-runtime": "^0.13.11",
"rimraf": "^6.0.1",
"sass": "^1.83.1",
"sass": "^1.83.4",
"sass-loader": "^16.0.4",
"serve": "^14.2.1",
"style-loader": "^3.3.4",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
mockWorkspaceKindsInValid,
mockWorkspaceKindsValid,
} from '~/__tests__/cypress/cypress/tests/mocked/workspaceKinds.mock';

describe('Test buildKindLogoDictionary Functionality', () => {
// Mock valid workspace kinds
context('With Valid Data', () => {
before(() => {
// Mock the API response
cy.intercept('GET', '/api/v1/workspacekinds', {
statusCode: 200,
body: mockWorkspaceKindsValid,
});

// Visit the page
cy.visit('/');
});

it('should fetch and populate kind logos', () => {
// Check that the logos are rendered in the table
cy.get('tbody tr').each(($row) => {
cy.wrap($row)
.find('td[data-label="Kind"]')
.within(() => {
cy.get('img')
.should('exist')
.then(($img) => {
// Ensure the image is fully loaded
cy.wrap($img[0]).should('have.prop', 'complete', true);
});
});
});
});
});

// Mock invalid workspace kinds
context('With Invalid Data', () => {
before(() => {
// Mock the API response for invalid workspace kinds
cy.intercept('GET', '/api/v1/workspacekinds', {
statusCode: 200,
body: mockWorkspaceKindsInValid,
});

// Visit the page
cy.visit('/');
});

it('should show a fallback icon when the logo URL is missing', () => {
cy.get('tbody tr').each(($row) => {
cy.wrap($row)
.find('td[data-label="Kind"]')
.within(() => {
// Ensure that the image is NOT rendered (because it's invalid or missing)
cy.get('img').should('not.exist'); // No images should be displayed

// Check if the fallback icon (TimesCircleIcon) is displayed
cy.get('svg').should('exist'); // Look for the SVG (TimesCircleIcon)
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)
});
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { WorkspaceKind } from '~/shared/types';

// Factory function to create a valid WorkspaceKind
function createMockWorkspaceKind(overrides: Partial<WorkspaceKind> = {}): WorkspaceKind {
return {
name: 'jupyter-lab',
displayName: 'JupyterLab Notebook',
description: 'A Workspace which runs JupyterLab in a Pod',
deprecated: false,
deprecationMessage: '',
hidden: false,
icon: {
url: 'https://jupyter.org/assets/favicons/apple-touch-icon-152x152.png',
},
logo: {
url: 'https://upload.wikimedia.org/wikipedia/commons/3/38/Jupyter_logo.svg',
},
podTemplate: {
podMetadata: {
labels: { myWorkspaceKindLabel: 'my-value' },
annotations: { myWorkspaceKindAnnotation: 'my-value' },
},
volumeMounts: { home: '/home/jovyan' },
options: {
imageConfig: {
default: 'jupyterlab_scipy_190',
values: [
{
id: 'jupyterlab_scipy_180',
displayName: 'jupyter-scipy:v1.8.0',
labels: { pythonVersion: '3.11' },
hidden: true,
redirect: {
to: 'jupyterlab_scipy_190',
message: {
text: 'This update will change...',
level: 'Info',
},
},
},
],
},
podConfig: {
default: 'tiny_cpu',
values: [
{
id: 'tiny_cpu',
displayName: 'Tiny CPU',
description: 'Pod with 0.1 CPU, 128 Mb RAM',
labels: { cpu: '100m', memory: '128Mi' },
},
],
},
},
},
...overrides, // Allows customization
};
}

// Generate valid mock data with "data" property
export const mockWorkspaceKindsValid = {
data: [createMockWorkspaceKind()],
};

// Generate invalid mock data with "data" property
export const mockWorkspaceKindsInValid = {
data: [
createMockWorkspaceKind({
logo: {
url: '',
},
}),
],
};
21 changes: 21 additions & 0 deletions workspaces/frontend/src/app/actions/WorkspaceKindsActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { WorkspaceKind } from '~/shared/types';

type KindLogoDict = Record<string, string>;

/**
* Builds a dictionary of kind names to logos, and returns it.
* @param {WorkspaceKind[]} workspaceKinds - The list of workspace kinds.
* @returns {KindLogoDict} A dictionary with kind names as keys and logo URLs as values.
*/
export function buildKindLogoDictionary(workspaceKinds: WorkspaceKind[] | []): KindLogoDict {
const kindLogoDict: KindLogoDict = {};

for (const workspaceKind of workspaceKinds) {
try {
kindLogoDict[workspaceKind.name] = workspaceKind.logo.url;
} catch {
kindLogoDict[workspaceKind.name] = '';
}
}
return kindLogoDict;
}
3 changes: 2 additions & 1 deletion workspaces/frontend/src/app/context/useNotebookAPIState.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { APIState } from '~/shared/api/types';
import { NotebookAPIs } from '~/app/types';
import { getNamespaces } from '~/shared/api/notebookService';
import { getNamespaces, getWorkspaceKinds } from '~/shared/api/notebookService';
import useAPIState from '~/shared/api/useAPIState';

export type NotebookAPIState = APIState<NotebookAPIs>;
Expand All @@ -12,6 +12,7 @@ const useNotebookAPIState = (
const createAPI = React.useCallback(
(path: string) => ({
getNamespaces: getNamespaces(path),
getWorkspaceKinds: getWorkspaceKinds(path),
}),
[],
);
Expand Down
24 changes: 24 additions & 0 deletions workspaces/frontend/src/app/hooks/useWorkspaceKinds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from 'react';
import useFetchState, {
FetchState,
FetchStateCallbackPromise,
} from '~/shared/utilities/useFetchState';
import { WorkspaceKind } from '~/shared/types';
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';

const useWorkspaceKinds = (): FetchState<WorkspaceKind[]> => {
const { api, apiAvailable } = useNotebookAPI();
const call = React.useCallback<FetchStateCallbackPromise<WorkspaceKind[]>>(
(opts) => {
if (!apiAvailable) {
return Promise.reject(new Error('API not yet available'));
}
return api.getWorkspaceKinds(opts);
},
[api, apiAvailable],
);

return useFetchState(call, []);
};

export default useWorkspaceKinds;
25 changes: 24 additions & 1 deletion workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
Pagination,
Button,
Content,
Tooltip,
Brand,
} from '@patternfly/react-core';
import {
Table,
Expand All @@ -24,10 +26,13 @@ import {
IActions,
} from '@patternfly/react-table';
import { useState } from 'react';
import { CodeIcon } from '@patternfly/react-icons';
import { Workspace, WorkspacesColumnNames, WorkspaceState } from '~/shared/types';
import { WorkspaceDetails } from '~/app/pages/Workspaces/Details/WorkspaceDetails';
import { ExpandedWorkspaceRow } from '~/app/pages/Workspaces/ExpandedWorkspaceRow';
import DeleteModal from '~/shared/components/DeleteModal';
import { buildKindLogoDictionary } from '~/app/actions/WorkspaceKindsActions';
import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds';
import Filter, { FilteredColumn } from 'shared/components/Filter';
import { formatRam } from 'shared/utilities/WorkspaceResources';

Expand Down Expand Up @@ -131,6 +136,10 @@ export const Workspaces: React.FunctionComponent = () => {
},
];

const [workspaceKinds] = useWorkspaceKinds();
let kindLogoDict: Record<string, string> = {};
kindLogoDict = buildKindLogoDictionary(workspaceKinds);

// Table columns
const columnNames: WorkspacesColumnNames = {
name: 'Name',
Expand Down Expand Up @@ -419,7 +428,21 @@ export const Workspaces: React.FunctionComponent = () => {
}}
/>
<Td dataLabel={columnNames.name}>{workspace.name}</Td>
<Td dataLabel={columnNames.kind}>{workspace.kind}</Td>
<Td dataLabel={columnNames.kind}>
{kindLogoDict[workspace.kind] ? (
<Tooltip content={workspace.kind}>
<Brand
src={kindLogoDict[workspace.kind]}
alt={workspace.kind}
style={{ width: '20px', height: '20px', cursor: 'pointer' }}
/>
</Tooltip>
) : (
<Tooltip content={workspace.kind}>
<CodeIcon />
</Tooltip>
)}
</Td>
<Td dataLabel={columnNames.image}>{workspace.options.imageConfig}</Td>
<Td dataLabel={columnNames.podConfig}>{workspace.options.podConfig}</Td>
<Td dataLabel={columnNames.state}>
Expand Down
4 changes: 4 additions & 0 deletions workspaces/frontend/src/app/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { APIOptions } from '~/shared/api/types';
import { WorkspaceKind } from '~/shared/types';

export type ResponseBody<T> = {
data: T;
Expand Down Expand Up @@ -64,6 +65,9 @@ export type NamespacesList = Namespace[];

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

export type GetWorkspaceKinds = (opts: APIOptions) => Promise<WorkspaceKind[]>;

export type NotebookAPIs = {
getNamespaces: GetNamespaces;
getWorkspaceKinds: GetWorkspaceKinds;
};
11 changes: 11 additions & 0 deletions workspaces/frontend/src/shared/api/notebookService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NamespacesList } from '~/app/types';
import { isNotebookResponse, restGET } from '~/shared/api/apiUtils';
import { APIOptions } from '~/shared/api/types';
import { handleRestFailures } from '~/shared/api/errorUtils';
import { WorkspaceKind } from '~/shared/types';

export const getNamespaces =
(hostPath: string) =>
Expand All @@ -12,3 +13,13 @@ export const getNamespaces =
}
throw new Error('Invalid response format');
});

export const getWorkspaceKinds =
(hostPath: string) =>
(opts: APIOptions): Promise<WorkspaceKind[]> =>
handleRestFailures(restGET(hostPath, `/workspacekinds`, {}, opts)).then((response) => {
if (isNotebookResponse<WorkspaceKind[]>(response)) {
return response.data;
}
throw new Error('Invalid response format');
});
Loading

0 comments on commit 055150b

Please sign in to comment.