Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions workspaces/frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ This is the default setup for running the UI locally. Make sure you build the pr
npm run start:dev
```

The command above requires the backend to be active in order to serve data. To run the UI independently, without establishing a connection to the backend, use the following command to start the application with a mocked API:

```bash
npm run start:dev:mock
```

### Testing

Run all tests:
Expand Down
12 changes: 6 additions & 6 deletions workspaces/frontend/config/webpack.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,13 @@ module.exports = (env) => {
},
{
test: /\.css$/i,
use: [
'style-loader',
'css-loader',
],
use: ['style-loader', 'css-loader'],
include: [
path.resolve(relativeDir, 'node_modules/@patternfly/react-catalog-view-extension/dist/css/react-catalog-view-extension.css'),
]
path.resolve(
relativeDir,
'node_modules/@patternfly/react-catalog-view-extension/dist/css/react-catalog-view-extension.css',
),
],
},
],
},
Expand Down
7 changes: 7 additions & 0 deletions workspaces/frontend/config/webpack.dev.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-var-requires */

const path = require('path');
const { EnvironmentPlugin } = require('webpack');
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const { stylePaths } = require('./stylePaths');
Expand All @@ -9,6 +10,7 @@ const PORT = process.env.PORT || '9000';
const PROXY_HOST = process.env.PROXY_HOST || 'localhost';
const PROXY_PORT = process.env.PROXY_PORT || '4000';
const PROXY_PROTOCOL = process.env.PROXY_PROTOCOL || 'http:';
const MOCK_API_ENABLED = process.env.MOCK_API_ENABLED || 'false';
const relativeDir = path.resolve(__dirname, '..');

module.exports = merge(common('development'), {
Expand Down Expand Up @@ -45,4 +47,9 @@ module.exports = merge(common('development'), {
},
],
},
plugins: [
new EnvironmentPlugin({
WEBPACK_REPLACE__mockApiEnabled: MOCK_API_ENABLED,
}),
],
});
4 changes: 1 addition & 3 deletions workspaces/frontend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ module.exports = {
testEnvironment: 'jest-environment-jsdom',

// include projects from node_modules as required
transformIgnorePatterns: [
'node_modules/(?!yaml|lodash-es|uuid|@patternfly|delaunator)',
],
transformIgnorePatterns: ['node_modules/(?!yaml|lodash-es|uuid|@patternfly|delaunator)'],

// A list of paths to snapshot serializer modules Jest should use for snapshot testing
snapshotSerializers: [],
Expand Down
19 changes: 19 additions & 0 deletions workspaces/frontend/package-lock.json

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

4 changes: 3 additions & 1 deletion workspaces/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"build:clean": "rimraf ./dist",
"build:prod": "webpack --config ./config/webpack.prod.js",
"start:dev": "webpack serve --hot --color --config ./config/webpack.dev.js",
"start:dev:mock": "cross-env MOCK_API_ENABLED=true npm run start:dev",
"test": "run-s test:lint test:unit test:cypress-ci",
"test:cypress-ci": "npx concurrently -P -k -s first \"npm run cypress:server:build && npm run cypress:server\" \"npx wait-on tcp:127.0.0.1:9001 && npm run cypress:run:mock -- {@}\" -- ",
"test:jest": "jest --passWithNoTests",
Expand Down Expand Up @@ -52,6 +53,7 @@
"concurrently": "^9.1.0",
"copy-webpack-plugin": "^11.0.0",
"core-js": "^3.39.0",
"cross-env": "^7.0.3",
"css-loader": "^6.11.0",
"css-minimizer-webpack-plugin": "^5.0.1",
"cypress": "^13.16.1",
Expand Down Expand Up @@ -95,8 +97,8 @@
"webpack-merge": "^5.10.0"
},
"dependencies": {
"@patternfly/react-code-editor": "^6.2.0",
"@patternfly/react-catalog-view-extension": "^6.1.0",
"@patternfly/react-code-editor": "^6.2.0",
"@patternfly/react-core": "^6.2.0",
"@patternfly/react-icons": "^6.2.0",
"@patternfly/react-styles": "^6.2.0",
Expand Down
11 changes: 6 additions & 5 deletions workspaces/frontend/src/__mocks__/mockNamespaces.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { NamespacesList } from '~/app/types';
import { buildMockNamespace } from '~/shared/mock/mockBuilder';
import { Namespace } from '~/shared/api/backendApiTypes';

export const mockNamespaces: NamespacesList = [
{ name: 'default' },
{ name: 'kubeflow' },
{ name: 'custom-namespace' },
export const mockNamespaces: Namespace[] = [
buildMockNamespace({ name: 'default' }),
buildMockNamespace({ name: 'kubeflow' }),
buildMockNamespace({ name: 'custom-namespace' }),
];
55 changes: 55 additions & 0 deletions workspaces/frontend/src/__mocks__/mockWorkspaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { buildMockWorkspace } from '~/shared/mock/mockBuilder';

export const mockWorkspaces = [
buildMockWorkspace(),
buildMockWorkspace({
name: 'My Second Jupyter Notebook',
podTemplate: {
podMetadata: {
labels: {},
annotations: {},
},
volumes: {
home: {
pvcName: 'workspace-home-pvc',
mountPath: '/home',
readOnly: false,
},
data: [
{
pvcName: 'workspace-data-pvc',
mountPath: '/data',
readOnly: false,
},
],
},
options: {
imageConfig: {
current: {
id: 'jupyterlab_scipy_180',
displayName: 'jupyter-scipy:v1.9.0',
description: 'JupyterLab, with SciPy Packages',
labels: [
{
key: 'pythonVersion',
value: '3.11',
},
],
},
},
podConfig: {
current: {
id: 'large_cpu',
displayName: 'Large CPU',
description: 'Pod with 1 CPU, 16 Gb RAM',
labels: [
{ key: 'cpu', value: '4000m' },
{ key: 'memory', value: '16Gi' },
{ key: 'gpu', value: '1' },
],
},
},
},
},
}),
];
2 changes: 1 addition & 1 deletion workspaces/frontend/src/__mocks__/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ResponseBody } from '~/app/types';
import { ResponseBody } from '~/shared/api/types';

export const mockBFFResponse = <T>(data: T): ResponseBody<T> => ({
data,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { WorkspaceKind } from '~/shared/types';
import type { WorkspaceKind } from '~/shared/api/backendApiTypes';

// Factory function to create a valid WorkspaceKind
function createMockWorkspaceKind(overrides: Partial<WorkspaceKind> = {}): WorkspaceKind {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { mockNamespaces } from '~/__mocks__/mockNamespaces';
import { mockWorkspaces } from '~/__mocks__/mockWorkspaces';
import { mockBFFResponse } from '~/__mocks__/utils';
import { home } from '~/__tests__/cypress/cypress/pages/home';

const useFilter = (filterName: string, searchValue: string) => {
Expand All @@ -9,41 +12,49 @@ const useFilter = (filterName: string, searchValue: string) => {
};

describe('Application', () => {
beforeEach(() => {
cy.intercept('GET', '/api/v1/namespaces', {
body: mockBFFResponse(mockNamespaces),
});
cy.intercept('GET', '/api/v1/workspaces/default', {
body: mockBFFResponse(mockWorkspaces),
});
});
it('filter rows with single filter', () => {
home.visit();
useFilter('Name', 'My');
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
cy.get("[id$='workspaces-table-row-1']").contains('My Jupyter Notebook');
cy.get("[id$='workspaces-table-row-2']").contains('My Other Jupyter Notebook');
cy.get("[id$='workspaces-table-row-1']").contains('My First Jupyter Notebook');
cy.get("[id$='workspaces-table-row-2']").contains('My Second Jupyter Notebook');
});

it('filter rows with multiple filters', () => {
home.visit();
useFilter('Name', 'My');
useFilter('Pod Config', 'Small');
useFilter('Pod Config', 'Tiny');
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 1);
cy.get("[id$='workspaces-table-row-1']").contains('My Jupyter Notebook');
cy.get("[id$='workspaces-table-row-1']").contains('My First Jupyter Notebook');
});

it('filter rows with multiple filters and remove one', () => {
home.visit();
useFilter('Name', 'My');
useFilter('Pod Config', 'Small');
useFilter('Pod Config', 'Tiny');
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 1);
cy.get("[id$='workspaces-table-row-1']").contains('My Jupyter Notebook');
cy.get("[id$='workspaces-table-row-1']").contains('My First Jupyter Notebook');
cy.get("[class$='pf-v6-c-label-group__close']").eq(1).click();
cy.get("[class$='pf-v6-c-toolbar__group']").should('not.contain', 'Pod Config');
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
cy.get("[id$='workspaces-table-row-1']").contains('My Jupyter Notebook');
cy.get("[id$='workspaces-table-row-2']").contains('My Other Jupyter Notebook');
cy.get("[id$='workspaces-table-row-1']").contains('My First Jupyter Notebook');
cy.get("[id$='workspaces-table-row-2']").contains('My Second Jupyter Notebook');
});

it('filter rows with multiple filters and remove all', () => {
home.visit();
useFilter('Name', 'My');
useFilter('Pod Config', 'Small');
useFilter('Pod Config', 'Tiny');
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 1);
cy.get("[id$='workspaces-table-row-1']").contains('My Jupyter Notebook');
cy.get("[id$='workspaces-table-row-1']").contains('My First Jupyter Notebook');
cy.get('*').contains('Clear all filters').click();
cy.get("[class$='pf-v6-c-toolbar__group']").should('not.contain', 'Pod Config');
cy.get("[class$='pf-v6-c-toolbar__group']").should('not.contain', 'Name');
Expand Down
29 changes: 16 additions & 13 deletions workspaces/frontend/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
Title,
} from '@patternfly/react-core';
import { BarsIcon } from '@patternfly/react-icons';
import ErrorBoundary from '~/app/error/ErrorBoundary';
import NamespaceSelector from '~/shared/components/NamespaceSelector';
import logoDarkTheme from '~/images/logo-dark-theme.svg';
import { NamespaceContextProvider } from './context/NamespaceContextProvider';
Expand Down Expand Up @@ -63,19 +64,21 @@ const App: React.FC = () => {
);

return (
<NotebookContextProvider>
<NamespaceContextProvider>
<Page
mainContainerId="primary-app-container"
masthead={masthead}
isContentFilled
isManagedSidebar
sidebar={<NavSidebar />}
>
<AppRoutes />
</Page>
</NamespaceContextProvider>
</NotebookContextProvider>
<ErrorBoundary>
<NotebookContextProvider>
<NamespaceContextProvider>
<Page
mainContainerId="primary-app-container"
masthead={masthead}
isContentFilled
isManagedSidebar
sidebar={<NavSidebar />}
>
<AppRoutes />
</Page>
</NamespaceContextProvider>
</NotebookContextProvider>
</ErrorBoundary>
);
};

Expand Down
22 changes: 22 additions & 0 deletions workspaces/frontend/src/app/EnsureAPIAvailability.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from 'react';
import { Bullseye, Spinner } from '@patternfly/react-core';
import { useNotebookAPI } from './hooks/useNotebookAPI';

interface EnsureAPIAvailabilityProps {
children: React.ReactNode;
}

const EnsureAPIAvailability: React.FC<EnsureAPIAvailabilityProps> = ({ children }) => {
const { apiAvailable } = useNotebookAPI();
if (!apiAvailable) {
return (
<Bullseye style={{ minHeight: 150 }}>
<Spinner />
</Bullseye>
);
}

return <>{children}</>;
};

export default EnsureAPIAvailability;
22 changes: 6 additions & 16 deletions workspaces/frontend/src/app/actions/WorkspaceKindsActions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { WorkspaceKind } from '~/shared/types';
import { WorkspaceKind, WorkspaceOptionRedirect } from '~/shared/api/backendApiTypes';

type KindLogoDict = Record<string, string>;

Expand All @@ -20,10 +20,7 @@ export function buildKindLogoDictionary(workspaceKinds: WorkspaceKind[] | []): K
return kindLogoDict;
}

type WorkspaceRedirectStatus = Record<
string,
{ to: string; message: string; level: string } | null
>;
type WorkspaceRedirectStatus = Record<string, WorkspaceOptionRedirect | undefined>;

/**
* Builds a dictionary of workspace kinds to redirect statuses.
Expand All @@ -36,17 +33,10 @@ export function buildWorkspaceRedirectStatus(
const workspaceRedirectStatus: WorkspaceRedirectStatus = {};
for (const workspaceKind of workspaceKinds) {
// Loop through the `values` array inside `imageConfig`
const redirect = workspaceKind.podTemplate.options.imageConfig.values.find(
(value) => value.redirect,
)?.redirect;
// If redirect exists, extract the necessary properties
workspaceRedirectStatus[workspaceKind.name] = redirect
? {
to: redirect.to,
message: redirect.message.text,
level: redirect.message.level,
}
: null;
workspaceRedirectStatus[workspaceKind.name] =
workspaceKind.podTemplate.options.imageConfig.values.find(
(value) => value.redirect,
)?.redirect;
}
return workspaceRedirectStatus;
}
Loading