Skip to content

Commit

Permalink
Merge notebooks-v2 into kind_logo_modification/#148 branch
Browse files Browse the repository at this point in the history
Signed-off-by: Liav Weiss (EXT-Nokia) <liav.weiss.ext@nokia.com>
  • Loading branch information
Liav Weiss (EXT-Nokia) committed Jan 14, 2025
1 parent d84621a commit 1869d2d
Show file tree
Hide file tree
Showing 11 changed files with 276 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { mockWorkspacekindsValid, mockWorkspacekindsInValid } from '../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 fallback when logo URL is invalid', () => {
const workspaceKinds = mockWorkspacekindsInValid.data; // Access mock data

cy.get('tbody tr').each(($row, index) => {
cy.wrap($row).find('td[data-label="Kind"]').within(() => {
cy.get('img')
.should('exist')
.then(($img) => {
// If the image src is invalid, it should not load
expect($img[0].naturalWidth).to.equal(0); // If the image is invalid, naturalWidth should be 0
});
});
});
});
});
});

Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { 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: 'https://invalid-url.example.com/invalid-logo.svg', // Broken URL
},
}),
],
};
20 changes: 20 additions & 0 deletions workspaces/frontend/src/app/actions/WorkspacekindsActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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) {
kindLogoDict[workspaceKind.name] = workspaceKind.logo.url;
}

return kindLogoDict;
}


4 changes: 3 additions & 1 deletion workspaces/frontend/src/app/context/useNotebookAPIState.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
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>;

const useNotebookAPIState = (
Expand All @@ -12,6 +13,7 @@ const useNotebookAPIState = (
const createAPI = React.useCallback(
(path: string) => ({
getNamespaces: getNamespaces(path),
getWorkspacekinds: getWorkspacekinds(path),
}),
[],
);
Expand Down
2 changes: 1 addition & 1 deletion workspaces/frontend/src/app/hooks/useNotebookAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type UseNotebookAPI = NotebookAPIState & {

export const useNotebookAPI = (): UseNotebookAPI => {
const { apiState, refreshAPIState: refreshAllAPI } = React.useContext(NotebookContext);

return {
refreshAllAPI,
...apiState,
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;
23 changes: 22 additions & 1 deletion workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
Button,
PaginationVariant,
Pagination,
Tooltip,
Brand,
} from '@patternfly/react-core';
import {
Table,
Expand All @@ -42,6 +44,8 @@ import { Workspace, WorkspacesColumnNames, WorkspaceState } from '~/shared/types
import { WorkspaceDetails } from '~/app/pages/Workspaces/Details/WorkspaceDetails';
import { ExpandedWorkspaceRow } from '~/app/pages/Workspaces/ExpandedWorkspaceRow';
import { formatRam } from 'shared/utilities/WorkspaceResources';
import { buildKindLogoDictionary } from '~/app/actions/WorkspacekindsActions';
import useWorkspacekinds from '~/app/hooks/useWorspacekinds';

export const Workspaces: React.FunctionComponent = () => {
/* Mocked workspaces, to be removed after fetching info from backend */
Expand Down Expand Up @@ -143,6 +147,15 @@ export const Workspaces: React.FunctionComponent = () => {
},
];

const [workspaceKinds, loaded, loadError] = useWorkspacekinds();
let kindLogoDict: Record<string, string> = {};

if (loaded && workspaceKinds) {
kindLogoDict = buildKindLogoDictionary(workspaceKinds);
} else {
console.error(loadError || 'Failed to load workspace kinds.');
}

// Table columns
const columnNames: WorkspacesColumnNames = {
name: 'Name',
Expand Down Expand Up @@ -548,7 +561,15 @@ export const Workspaces: React.FunctionComponent = () => {
}}
/>
<Td dataLabel={columnNames.name}>{workspace.name}</Td>
<Td dataLabel={columnNames.kind}>{workspace.kind}</Td>
<Td dataLabel={columnNames.kind}>
<Tooltip content={workspace.kind}>
<Brand
src={kindLogoDict[workspace.kind]}
alt={workspace.kind}
style={{ width: '20px', height: '20px', cursor: 'pointer' }}
/>
</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;
};
13 changes: 12 additions & 1 deletion workspaces/frontend/src/shared/api/notebookService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { NamespacesList } from '~/app/types';
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 '../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');
});
4 changes: 2 additions & 2 deletions workspaces/frontend/src/shared/api/useAPIState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const useAPIState = <T>(
createAPI: (path: string) => T,
): [apiState: APIState<T>, refreshAPIState: () => void] => {
const [internalAPIToggleState, setInternalAPIToggleState] = React.useState(false);

const refreshAPIState = React.useCallback(() => {
setInternalAPIToggleState((v) => !v);
}, []);
Expand All @@ -18,7 +18,7 @@ const useAPIState = <T>(
path = '';
}
const api = createAPI(path);

return {
apiAvailable: !!path,
api,
Expand Down
53 changes: 51 additions & 2 deletions workspaces/frontend/src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,60 @@ export interface WorkspaceKind {
name: string;
displayName: string;
description: string;
hidden: boolean;
deprecated: boolean;
deprecationWarning: string;
deprecationMessage: string,
hidden: boolean;
icon: WorkspaceIcon;
logo: WorkspaceLogo;
podTemplate: {
podMetadata: {
labels: {
myWorkspaceKindLabel: string
},
annotations: {
myWorkspaceKindAnnotation: string
}
},
volumeMounts: {
home: string
},
options: {
imageConfig: {
default: string,
values: [
{
id: string,
displayName: string,
labels: {
pythonVersion: string
},
hidden: true,
redirect: {
to: string,
message: {
text: string,
level: string
}
}
},
]
},
podConfig: {
default: string,
values: [
{
id: string,
displayName: string,
description: string,
labels: {
cpu: string,
memory: string
}
},
]
}
}
}
}

export enum WorkspaceState {
Expand Down

0 comments on commit 1869d2d

Please sign in to comment.