* 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>
This commit is contained in:
parent
2c05c38aaa
commit
055150bb2e
|
|
@ -60,7 +60,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",
|
||||
|
|
@ -18869,10 +18869,11 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.83.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.83.1.tgz",
|
||||
"integrity": "sha512-EVJbDaEs4Rr3F0glJzFSOvtg2/oy2V/YrGFPqPY24UqcLDWcI9ZY5sN+qyO3c/QCZwzgfirvhXvINiJCE/OLcA==",
|
||||
"version": "1.83.4",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz",
|
||||
"integrity": "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
|
|
@ -18893,6 +18894,7 @@
|
|||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz",
|
||||
"integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"neo-async": "^2.6.2"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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: '',
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>;
|
||||
|
|
@ -12,6 +12,7 @@ const useNotebookAPIState = (
|
|||
const createAPI = React.useCallback(
|
||||
(path: string) => ({
|
||||
getNamespaces: getNamespaces(path),
|
||||
getWorkspaceKinds: getWorkspaceKinds(path),
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -11,6 +11,8 @@ import {
|
|||
Pagination,
|
||||
Button,
|
||||
Content,
|
||||
Tooltip,
|
||||
Brand,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
Table,
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
@ -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',
|
||||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { APIOptions } from '~/shared/api/types';
|
||||
import { WorkspaceKind } from '~/shared/types';
|
||||
|
||||
export type ResponseBody<T> = {
|
||||
data: T;
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue