From 28d4a04c12da0309f5a00b37c0f225239df714e6 Mon Sep 17 00:00:00 2001 From: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> Date: Fri, 2 May 2025 15:24:55 -0300 Subject: [PATCH 01/12] Enable start:dev with real data and start:dev:mock with mocked data Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> --- workspaces/frontend/config/webpack.dev.js | 7 + workspaces/frontend/package-lock.json | 19 ++ workspaces/frontend/package.json | 4 +- .../frontend/src/__mocks__/mockNamespaces.ts | 4 +- .../frontend/src/__mocks__/mockWorkspaces.ts | 123 ++++++++ .../workspaces/filterWorkspacesTest.cy.ts | 11 + .../src/app/context/useNotebookAPIState.tsx | 88 +++++- .../frontend/src/app/hooks/useNamespaces.ts | 8 +- .../src/app/hooks/useWorkspaceCountPerKind.ts | 25 ++ .../src/app/hooks/useWorkspaceKindByName.ts | 26 ++ .../src/app/hooks/useWorkspaceKinds.ts | 2 +- .../frontend/src/app/hooks/useWorkspaces.ts | 26 ++ .../pages/WorkspaceKinds/WorkspaceKinds.tsx | 98 ++---- .../kind/WorkspaceCreationKindSelection.tsx | 141 +-------- .../src/app/pages/Workspaces/Workspaces.tsx | 154 +--------- .../src/app/pages/Workspaces/utils.ts | 2 +- .../WorkspaceRedirectInformationView.tsx | 128 +++----- .../WorkspaceRestartActionModal.tsx | 2 +- .../WorkspaceStartActionModal.tsx | 2 +- .../WorkspaceStopActionModal.tsx | 2 +- workspaces/frontend/src/app/types.ts | 91 +++++- .../api/__tests__/notebookService.spec.ts | 7 +- .../frontend/src/shared/api/apiUtils.ts | 7 + .../frontend/src/shared/api/callTypes.ts | 45 +++ .../frontend/src/shared/api/mockedData.ts | 289 ++++++++++++++++++ .../src/shared/api/mockedNotebookService.ts | 64 ++++ .../src/shared/api/notebookService.ts | 138 ++++++--- workspaces/frontend/src/shared/types.ts | 11 + 28 files changed, 1012 insertions(+), 512 deletions(-) create mode 100644 workspaces/frontend/src/__mocks__/mockWorkspaces.ts create mode 100644 workspaces/frontend/src/app/hooks/useWorkspaceCountPerKind.ts create mode 100644 workspaces/frontend/src/app/hooks/useWorkspaceKindByName.ts create mode 100644 workspaces/frontend/src/app/hooks/useWorkspaces.ts create mode 100644 workspaces/frontend/src/shared/api/callTypes.ts create mode 100644 workspaces/frontend/src/shared/api/mockedData.ts create mode 100644 workspaces/frontend/src/shared/api/mockedNotebookService.ts diff --git a/workspaces/frontend/config/webpack.dev.js b/workspaces/frontend/config/webpack.dev.js index d419ccd58..9283bfc15 100644 --- a/workspaces/frontend/config/webpack.dev.js +++ b/workspaces/frontend/config/webpack.dev.js @@ -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'); @@ -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'), { @@ -45,4 +47,9 @@ module.exports = merge(common('development'), { }, ], }, + plugins: [ + new EnvironmentPlugin({ + WEBPACK_REPLACE__mockApiEnabled: MOCK_API_ENABLED, + }), + ], }); diff --git a/workspaces/frontend/package-lock.json b/workspaces/frontend/package-lock.json index ccf1348a5..97a1a675d 100644 --- a/workspaces/frontend/package-lock.json +++ b/workspaces/frontend/package-lock.json @@ -41,6 +41,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", @@ -7750,6 +7751,24 @@ "node": ">=8" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", diff --git a/workspaces/frontend/package.json b/workspaces/frontend/package.json index ed2c1326c..6af360145 100644 --- a/workspaces/frontend/package.json +++ b/workspaces/frontend/package.json @@ -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", @@ -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", @@ -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", diff --git a/workspaces/frontend/src/__mocks__/mockNamespaces.ts b/workspaces/frontend/src/__mocks__/mockNamespaces.ts index 6b6761fb6..ea7ec4337 100644 --- a/workspaces/frontend/src/__mocks__/mockNamespaces.ts +++ b/workspaces/frontend/src/__mocks__/mockNamespaces.ts @@ -1,6 +1,6 @@ -import { NamespacesList } from '~/app/types'; +import { Namespace } from '~/shared/types'; -export const mockNamespaces: NamespacesList = [ +export const mockNamespaces: Namespace[] = [ { name: 'default' }, { name: 'kubeflow' }, { name: 'custom-namespace' }, diff --git a/workspaces/frontend/src/__mocks__/mockWorkspaces.ts b/workspaces/frontend/src/__mocks__/mockWorkspaces.ts new file mode 100644 index 000000000..90990def7 --- /dev/null +++ b/workspaces/frontend/src/__mocks__/mockWorkspaces.ts @@ -0,0 +1,123 @@ +import { Workspace, WorkspaceState } from '~/shared/types'; + +export const mockWorkspaces: Workspace[] = [ + { + name: 'My Jupyter Notebook', + namespace: 'namespace1', + paused: true, + deferUpdates: true, + kind: 'jupyter-lab', + cpu: 3, + ram: 500, + podTemplate: { + podMetadata: { + labels: ['label1', 'label2'], + annotations: ['annotation1', 'annotation2'], + }, + volumes: { + home: '/home', + data: [ + { + pvcName: 'Volume-1', + mountPath: '/data', + readOnly: true, + }, + { + pvcName: 'Volume-2', + mountPath: '/data', + readOnly: false, + }, + ], + }, + endpoints: [ + { + displayName: 'JupyterLab', + port: '7777', + }, + ], + }, + options: { + imageConfig: 'jupyterlab_scipy_180', + podConfig: 'Small CPU', + }, + status: { + activity: { + lastActivity: 1739673600, + lastUpdate: 1739673700, + }, + pauseTime: 1739673500, + pendingRestart: false, + podTemplateOptions: { + imageConfig: { + desired: '', + redirectChain: [], + }, + }, + state: WorkspaceState.Paused, + stateMessage: 'It is paused.', + }, + redirectStatus: { + level: 'Info', + text: 'This is informational', + }, + }, + { + name: 'My Other Jupyter Notebook', + namespace: 'namespace1', + paused: false, + deferUpdates: false, + kind: 'jupyter-lab', + cpu: 1, + ram: 12540, + podTemplate: { + podMetadata: { + labels: ['label1', 'label2'], + annotations: ['annotation1', 'annotation2'], + }, + volumes: { + home: '/home', + data: [ + { + pvcName: 'PVC-1', + mountPath: '/data', + readOnly: false, + }, + ], + }, + endpoints: [ + { + displayName: 'JupyterLab', + port: '8888', + }, + { + displayName: 'Spark Master', + port: '9999', + }, + ], + }, + options: { + imageConfig: 'jupyterlab_scipy_180', + podConfig: 'Large CPU', + }, + status: { + activity: { + lastActivity: 0, + lastUpdate: 0, + }, + pauseTime: 0, + pendingRestart: false, + podTemplateOptions: { + imageConfig: { + desired: '', + redirectChain: [], + }, + }, + state: WorkspaceState.Running, + stateMessage: 'It is running.', + }, + redirectStatus: { + level: 'Danger', + text: 'This is dangerous', + }, + }, +]; diff --git a/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/filterWorkspacesTest.cy.ts b/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/filterWorkspacesTest.cy.ts index ebff315c8..4d7b5eb5a 100644 --- a/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/filterWorkspacesTest.cy.ts +++ b/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/filterWorkspacesTest.cy.ts @@ -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) => { @@ -9,6 +12,14 @@ 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'); diff --git a/workspaces/frontend/src/app/context/useNotebookAPIState.tsx b/workspaces/frontend/src/app/context/useNotebookAPIState.tsx index 6347618ce..6471183d0 100644 --- a/workspaces/frontend/src/app/context/useNotebookAPIState.tsx +++ b/workspaces/frontend/src/app/context/useNotebookAPIState.tsx @@ -1,24 +1,100 @@ import React from 'react'; -import { APIState } from '~/shared/api/types'; import { NotebookAPIs } from '~/app/types'; -import { getNamespaces, getWorkspaceKinds, createWorkspace } from '~/shared/api/notebookService'; +import { + mockCreateWorkspace, + mockCreateWorkspaceKind, + mockDeleteWorkspace, + mockDeleteWorkspaceKind, + mockGetHealthCheck, + mockGetWorkspace, + mockGetWorkspaceKind, + mockListAllWorkspaces, + mockListNamespaces, + mockListWorkspaceKinds, + mockListWorkspaces, + mockPatchWorkspace, + mockPatchWorkspaceKind, + mockUpdateWorkspace, + mockUpdateWorkspaceKind, +} from '~/shared/api/mockedNotebookService'; +import { + createWorkspace, + createWorkspaceKind, + deleteWorkspace, + deleteWorkspaceKind, + getHealthCheck, + getWorkspace, + getWorkspaceKind, + listAllWorkspaces, + listNamespaces, + listWorkspaceKinds, + listWorkspaces, + patchWorkspace, + patchWorkspaceKind, + updateWorkspace, + updateWorkspaceKind, +} from '~/shared/api/notebookService'; +import { APIState } from '~/shared/api/types'; import useAPIState from '~/shared/api/useAPIState'; export type NotebookAPIState = APIState; +const MOCK_API_ENABLED = process.env.WEBPACK_REPLACE__mockApiEnabled === 'true'; + const useNotebookAPIState = ( hostPath: string | null, ): [apiState: NotebookAPIState, refreshAPIState: () => void] => { - const createAPI = React.useCallback( + const createApi = React.useCallback( (path: string) => ({ - getNamespaces: getNamespaces(path), - getWorkspaceKinds: getWorkspaceKinds(path), + // Health + getHealthCheck: getHealthCheck(path), + // Namespace + listNamespaces: listNamespaces(path), + // Workspace + listAllWorkspaces: listAllWorkspaces(path), + listWorkspaces: listWorkspaces(path), createWorkspace: createWorkspace(path), + getWorkspace: getWorkspace(path), + updateWorkspace: updateWorkspace(path), + patchWorkspace: patchWorkspace(path), + deleteWorkspace: deleteWorkspace(path), + // WorkspaceKind + listWorkspaceKinds: listWorkspaceKinds(path), + createWorkspaceKind: createWorkspaceKind(path), + getWorkspaceKind: getWorkspaceKind(path), + patchWorkspaceKind: patchWorkspaceKind(path), + deleteWorkspaceKind: deleteWorkspaceKind(path), + updateWorkspaceKind: updateWorkspaceKind(path), + }), + [], + ); + + const createMockApi = React.useCallback( + (path: string) => ({ + // Health + getHealthCheck: mockGetHealthCheck(path), + // Namespace + listNamespaces: mockListNamespaces(path), + // Workspace + listAllWorkspaces: mockListAllWorkspaces(path), + listWorkspaces: mockListWorkspaces(path), + createWorkspace: mockCreateWorkspace(path), + getWorkspace: mockGetWorkspace(path), + updateWorkspace: mockUpdateWorkspace(path), + patchWorkspace: mockPatchWorkspace(path), + deleteWorkspace: mockDeleteWorkspace(path), + // WorkspaceKind + listWorkspaceKinds: mockListWorkspaceKinds(path), + createWorkspaceKind: mockCreateWorkspaceKind(path), + getWorkspaceKind: mockGetWorkspaceKind(path), + patchWorkspaceKind: mockPatchWorkspaceKind(path), + deleteWorkspaceKind: mockDeleteWorkspaceKind(path), + updateWorkspaceKind: mockUpdateWorkspaceKind(path), }), [], ); - return useAPIState(hostPath, createAPI); + return useAPIState(hostPath, MOCK_API_ENABLED ? createMockApi : createApi); }; export default useNotebookAPIState; diff --git a/workspaces/frontend/src/app/hooks/useNamespaces.ts b/workspaces/frontend/src/app/hooks/useNamespaces.ts index 8b84fd33e..e8d5c4349 100644 --- a/workspaces/frontend/src/app/hooks/useNamespaces.ts +++ b/workspaces/frontend/src/app/hooks/useNamespaces.ts @@ -3,19 +3,19 @@ import useFetchState, { FetchState, FetchStateCallbackPromise, } from '~/shared/utilities/useFetchState'; -import { NamespacesList } from '~/app/types'; import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; +import { Namespace } from '~/shared/types'; -const useNamespaces = (): FetchState => { +const useNamespaces = (): FetchState => { const { api, apiAvailable } = useNotebookAPI(); - const call = React.useCallback>( + const call = React.useCallback>( (opts) => { if (!apiAvailable) { return Promise.reject(new Error('API not yet available')); } - return api.getNamespaces(opts); + return api.listNamespaces(opts); }, [api, apiAvailable], ); diff --git a/workspaces/frontend/src/app/hooks/useWorkspaceCountPerKind.ts b/workspaces/frontend/src/app/hooks/useWorkspaceCountPerKind.ts new file mode 100644 index 000000000..675c55e38 --- /dev/null +++ b/workspaces/frontend/src/app/hooks/useWorkspaceCountPerKind.ts @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; +import { Workspace, WorkspaceKind } from '~/shared/types'; + +type WorkspaceCountPerKind = Record; + +export const useWorkspaceCountPerKind = (): WorkspaceCountPerKind => { + const { api } = useNotebookAPI(); + + const [workspaceCountPerKind, setWorkspaceCountPerKind] = React.useState( + {}, + ); + + React.useEffect(() => { + api.listAllWorkspaces({}).then((workspaces) => { + const countPerKind = workspaces.reduce((acc: WorkspaceCountPerKind, workspace: Workspace) => { + acc[workspace.kind] = (acc[workspace.kind] || 0) + 1; + return acc; + }, {}); + setWorkspaceCountPerKind(countPerKind); + }); + }, [api]); + + return workspaceCountPerKind; +}; diff --git a/workspaces/frontend/src/app/hooks/useWorkspaceKindByName.ts b/workspaces/frontend/src/app/hooks/useWorkspaceKindByName.ts new file mode 100644 index 000000000..f5715e637 --- /dev/null +++ b/workspaces/frontend/src/app/hooks/useWorkspaceKindByName.ts @@ -0,0 +1,26 @@ +import * as React from 'react'; +import useFetchState, { + FetchState, + FetchStateCallbackPromise, +} from '~/shared/utilities/useFetchState'; +import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; +import { WorkspaceKind } from '~/shared/types'; + +const useWorkspaceKindByName = (kind: string): FetchState => { + const { api, apiAvailable } = useNotebookAPI(); + + const call = React.useCallback>( + (opts) => { + if (!apiAvailable) { + return Promise.reject(new Error('API not yet available')); + } + + return api.getWorkspaceKind(opts, kind); + }, + [api, apiAvailable, kind], + ); + + return useFetchState(call, null); +}; + +export default useWorkspaceKindByName; diff --git a/workspaces/frontend/src/app/hooks/useWorkspaceKinds.ts b/workspaces/frontend/src/app/hooks/useWorkspaceKinds.ts index f0ad18462..90c21ab1d 100644 --- a/workspaces/frontend/src/app/hooks/useWorkspaceKinds.ts +++ b/workspaces/frontend/src/app/hooks/useWorkspaceKinds.ts @@ -13,7 +13,7 @@ const useWorkspaceKinds = (): FetchState => { if (!apiAvailable) { return Promise.reject(new Error('API not yet available')); } - return api.getWorkspaceKinds(opts); + return api.listWorkspaceKinds(opts); }, [api, apiAvailable], ); diff --git a/workspaces/frontend/src/app/hooks/useWorkspaces.ts b/workspaces/frontend/src/app/hooks/useWorkspaces.ts new file mode 100644 index 000000000..fa0f4838f --- /dev/null +++ b/workspaces/frontend/src/app/hooks/useWorkspaces.ts @@ -0,0 +1,26 @@ +import * as React from 'react'; +import useFetchState, { + FetchState, + FetchStateCallbackPromise, +} from '~/shared/utilities/useFetchState'; +import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; +import { Workspace } from '~/shared/types'; + +const useWorkspaces = (namespace: string): FetchState => { + const { api, apiAvailable } = useNotebookAPI(); + + const call = React.useCallback>( + (opts) => { + if (!apiAvailable) { + return Promise.reject(new Error('API not yet available')); + } + + return api.listWorkspaces(opts, namespace); + }, + [api, apiAvailable, namespace], + ); + + return useFetchState(call, null); +}; + +export default useWorkspaces; diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx index 9becc15e6..6e6d7f773 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx @@ -41,83 +41,14 @@ import { } from '@patternfly/react-table'; import { CodeIcon, FilterIcon, SearchIcon } from '@patternfly/react-icons'; import { WorkspaceKind, WorkspaceKindsColumnNames } from '~/shared/types'; +import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds'; +import { useWorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind'; export enum ActionType { ViewDetails, } export const WorkspaceKinds: React.FunctionComponent = () => { - // Todo: Remove mock and use useWorkspaceKinds API instead. - const mockWorkspaceKinds: WorkspaceKind[] = [ - { - name: 'jupyterlab', - displayName: 'JupyterLab Notebook', - description: - 'Example of a description for JupyterLab a Workspace which runs JupyterLab in a Pod.', - deprecated: true, - deprecationMessage: - 'This WorkspaceKind was removed on 20XX-XX-XX, please use another WorkspaceKind.', - 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', - }, - }, - ], - }, - }, - }, - }, - ]; - - const mockNumberOfWorkspaces = 1; // Todo: Create a function to calculate number of workspaces for each workspace kind. - // Table columns const columnNames: WorkspaceKindsColumnNames = { icon: '', @@ -127,7 +58,8 @@ export const WorkspaceKinds: React.FunctionComponent = () => { numberOfWorkspaces: 'Number of workspaces', }; - const initialWorkspaceKinds = mockWorkspaceKinds; + const [workspaceKinds, workspaceKindsLoaded, workspaceKindsError] = useWorkspaceKinds(); + const workspaceCountPerKind = useWorkspaceCountPerKind(); const [selectedWorkspaceKind, setSelectedWorkspaceKind] = React.useState( null, ); @@ -150,19 +82,19 @@ export const WorkspaceKinds: React.FunctionComponent = () => { name: workspaceKind.name, description: workspaceKind.description, deprecated: workspaceKind.deprecated, - numOfWorkspaces: mockNumberOfWorkspaces, + numOfWorkspaces: workspaceCountPerKind[workspaceKind.name] ?? 0, }; return [icon, name, description, deprecated, numberOfWorkspaces]; }, - [], + [workspaceCountPerKind], ); const sortedWorkspaceKinds = React.useMemo(() => { if (activeSortIndex === null) { - return initialWorkspaceKinds; + return workspaceKinds; } - return [...initialWorkspaceKinds].sort((a, b) => { + return [...workspaceKinds].sort((a, b) => { const aValue = getSortableRowValues(a)[activeSortIndex]; const bValue = getSortableRowValues(b)[activeSortIndex]; if (typeof aValue === 'boolean' && typeof bValue === 'boolean') { @@ -174,7 +106,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => { ? (aValue as string).localeCompare(bValue as string) : (bValue as string).localeCompare(aValue as string); }); - }, [initialWorkspaceKinds, activeSortIndex, activeSortDirection, getSortableRowValues]); + }, [workspaceKinds, activeSortIndex, activeSortDirection, getSortableRowValues]); const getSortParams = React.useCallback( (columnIndex: number): ThProps['sort'] => ({ @@ -535,6 +467,14 @@ export const WorkspaceKinds: React.FunctionComponent = () => { const DESCRIPTION_CHAR_LIMIT = 50; + if (workspaceKindsError) { + return

Error loading workspace kinds: {workspaceKindsError.message}

; // TODO: UX for error state + } + + if (!workspaceKindsLoaded) { + return

Loading...

; // TODO: UX for loading state + } + return ( { )} - {mockNumberOfWorkspaces} + + {workspaceCountPerKind[workspaceKind.name] ?? 0} + = ({ selectedKind, onSelect }) => { - /* Replace mocks below for BFF call */ - const mockedWorkspaceKind: WorkspaceKind = useMemo( - () => ({ - name: 'jupyter-lab1', - 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', jupyterlabVersion: '1.8.0' }, - hidden: true, - redirect: { - to: 'jupyterlab_scipy_190', - message: { - text: 'This update will change...', - level: 'Info', - }, - }, - }, - { - id: 'jupyterlab_scipy_190', - displayName: 'jupyter-scipy:v1.9.0', - labels: { pythonVersion: '3.12', jupyterlabVersion: '1.9.0' }, - hidden: true, - redirect: { - to: 'jupyterlab_scipy_200', - message: { - text: 'This update will change...', - level: 'Warning', - }, - }, - }, - { - id: 'jupyterlab_scipy_200', - displayName: 'jupyter-scipy:v2.0.0', - labels: { pythonVersion: '3.12', jupyterlabVersion: '2.0.0' }, - hidden: true, - redirect: { - to: 'jupyterlab_scipy_210', - message: { - text: 'This update will change...', - level: 'Warning', - }, - }, - }, - { - id: 'jupyterlab_scipy_210', - displayName: 'jupyter-scipy:v2.1.0', - labels: { pythonVersion: '3.13', jupyterlabVersion: '2.1.0' }, - hidden: true, - redirect: { - to: 'jupyterlab_scipy_220', - message: { - text: 'This update will change...', - level: 'Warning', - }, - }, - }, - ], - }, - 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' }, - redirect: { - to: 'small_cpu', - message: { - text: 'This update will change...', - level: 'Danger', - }, - }, - }, - { - id: 'large_cpu', - displayName: 'Large CPU', - description: 'Pod with 1 CPU, 1 Gb RAM', - labels: { cpu: '1000m', memory: '1Gi' }, - }, - ], - }, - }, - }, - }), - [], - ); - - /* Replace mocks below for BFF call */ - const allWorkspaceKinds = useMemo(() => { - const kinds: WorkspaceKind[] = []; - - for (let i = 1; i <= 15; i++) { - const kind = { ...mockedWorkspaceKind }; - kind.name += i; - kind.displayName += ` ${i}`; - kind.podTemplate = { ...mockedWorkspaceKind.podTemplate }; - kind.podTemplate.podMetadata = { ...mockedWorkspaceKind.podTemplate.podMetadata }; - kind.podTemplate.podMetadata.labels = { - ...mockedWorkspaceKind.podTemplate.podMetadata.labels, - }; - kind.podTemplate.podMetadata.labels[`my-label-key-${Math.ceil(i / 4)}`] = - `my-label-value-${Math.ceil(i)}`; - kinds.push(kind); - } - - return kinds; - }, [mockedWorkspaceKind]); + const [workspaceKinds, loaded, error] = useWorkspaceKinds(); const kindDetailsContent = useMemo( () => , [selectedKind], ); + if (error) { + return

Error loading workspace kinds: {error.message}

; // TODO: UX for error state + } + + if (!loaded) { + return

Loading...

; // TODO: UX for loading state + } + return (

Select a workspace kind to use for the workspace.

@@ -155,7 +36,7 @@ const WorkspaceCreationKindSelection: React.FunctionComponent< diff --git a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx index 45275951a..a9d06c891 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx @@ -43,10 +43,12 @@ import { buildWorkspaceRedirectStatus, } from '~/app/actions/WorkspaceKindsActions'; import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds'; +import useWorkspaces from '~/app/hooks/useWorkspaces'; import { WorkspaceConnectAction } from '~/app/pages/Workspaces/WorkspaceConnectAction'; import { WorkspaceStartActionModal } from '~/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal'; import { WorkspaceRestartActionModal } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal'; import { WorkspaceStopActionModal } from '~/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal'; +import { useNamespaceContext } from '~/app/context/NamespaceContextProvider'; import Filter, { FilteredColumn } from 'shared/components/Filter'; import { formatRam } from 'shared/utilities/WorkspaceUtils'; @@ -60,143 +62,6 @@ export enum ActionType { } export const Workspaces: React.FunctionComponent = () => { - /* Mocked workspaces, to be removed after fetching info from backend */ - const mockWorkspaces: Workspace[] = [ - { - name: 'My Jupyter Notebook', - namespace: 'namespace1', - paused: true, - deferUpdates: true, - kind: 'jupyter-lab', - cpu: 3, - ram: 500, - podTemplate: { - podMetadata: { - labels: ['label1', 'label2'], - annotations: ['annotation1', 'annotation2'], - }, - volumes: { - home: '/home', - data: [ - { - pvcName: 'Volume-1', - mountPath: '/data', - readOnly: true, - }, - { - pvcName: 'Volume-2', - mountPath: '/data', - readOnly: false, - }, - ], - secrets: [ - { - secretName: 'Secret-2', - mountPath: '/data', - defaultMode: 420, - }, - ], - }, - endpoints: [ - { - displayName: 'JupyterLab', - port: '7777', - }, - ], - }, - options: { - imageConfig: 'jupyterlab_scipy_180', - podConfig: 'Small CPU', - }, - status: { - activity: { - lastActivity: 1739673600, - lastUpdate: 1739673700, - }, - pauseTime: 1739673500, - pendingRestart: false, - podTemplateOptions: { - imageConfig: { - desired: '', - redirectChain: [], - }, - }, - state: WorkspaceState.Paused, - stateMessage: 'It is paused.', - }, - redirectStatus: { - level: 'Info', - text: 'This is informational', // Tooltip text - }, - }, - { - name: 'My Other Jupyter Notebook', - namespace: 'namespace1', - paused: false, - deferUpdates: false, - kind: 'jupyter-lab', - cpu: 1, - ram: 12540, - podTemplate: { - podMetadata: { - labels: ['label1', 'label2'], - annotations: ['annotation1', 'annotation2'], - }, - volumes: { - home: '/home', - data: [ - { - pvcName: 'PVC-1', - mountPath: '/data', - readOnly: false, - }, - ], - secrets: [ - { - secretName: 'workspace-secret', - mountPath: '/secrets/my-secret', - defaultMode: 420, - }, - ], - }, - endpoints: [ - { - displayName: 'JupyterLab', - port: '8888', - }, - { - displayName: 'Spark Master', - port: '9999', - }, - ], - }, - options: { - imageConfig: 'jupyterlab_scipy_180', - podConfig: 'Large CPU', - }, - status: { - activity: { - lastActivity: 0, - lastUpdate: 0, - }, - pauseTime: 0, - pendingRestart: false, - podTemplateOptions: { - imageConfig: { - desired: '', - redirectChain: [], - }, - }, - state: WorkspaceState.Running, - stateMessage: 'It is running.', - }, - redirectStatus: { - level: 'Danger', - text: 'This is dangerous', - }, - }, - ]; - const navigate = useNavigate(); const createWorkspace = useCallback(() => { navigate('/workspaces/create'); @@ -236,15 +101,22 @@ export const Workspaces: React.FunctionComponent = () => { lastActivity: 'Last Activity', }; - // change when fetch workspaces is implemented - const initialWorkspaces = mockWorkspaces; - const [workspaces, setWorkspaces] = useState(initialWorkspaces); + const { selectedNamespace } = useNamespaceContext(); + const [initialWorkspaces, initialWorkspacesLoaded] = useWorkspaces(selectedNamespace); + const [workspaces, setWorkspaces] = useState([]); const [expandedWorkspacesNames, setExpandedWorkspacesNames] = React.useState([]); const [selectedWorkspace, setSelectedWorkspace] = React.useState(null); const [workspaceToDelete, setWorkspaceToDelete] = React.useState(null); const [isActionAlertModalOpen, setIsActionAlertModalOpen] = React.useState(false); const [activeActionType, setActiveActionType] = React.useState(null); + React.useEffect(() => { + if (!initialWorkspacesLoaded) { + return; + } + setWorkspaces(initialWorkspaces ?? []); + }, [initialWorkspaces, initialWorkspacesLoaded]); + const selectWorkspace = React.useCallback( (newSelectedWorkspace: Workspace | null) => { if (selectedWorkspace?.name === newSelectedWorkspace?.name) { @@ -270,7 +142,7 @@ export const Workspaces: React.FunctionComponent = () => { // filter function to pass to the filter component const onFilter = (filters: FilteredColumn[]) => { // Search name with search value - let filteredWorkspaces = initialWorkspaces; + let filteredWorkspaces = initialWorkspaces ?? []; filters.forEach((filter) => { let searchValueInput: RegExp; try { diff --git a/workspaces/frontend/src/app/pages/Workspaces/utils.ts b/workspaces/frontend/src/app/pages/Workspaces/utils.ts index a87fb3a2b..e260d02c3 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/utils.ts +++ b/workspaces/frontend/src/app/pages/Workspaces/utils.ts @@ -13,6 +13,6 @@ export const createWorkspaceCall = async ( formData: CreateWorkspaceData, namespace: string, ): Promise => { - const workspace = await api.createWorkspace(opts, formData, namespace); + const workspace = await api.createWorkspace(opts, namespace, formData); return { workspace }; }; diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView.tsx b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView.tsx index 033dfd81e..d4c0eb451 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView.tsx @@ -5,82 +5,10 @@ import { InfoCircleIcon, } from '@patternfly/react-icons'; import * as React from 'react'; +import useWorkspaceKindByName from '~/app/hooks/useWorkspaceKindByName'; +import { WorkspaceKind } from '~/shared/types'; -// remove when changing to fetch data from BE -const mockedWorkspaceKind = { - 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', - }, - }, - }, - { - id: 'jupyterlab_scipy_190', - displayName: 'jupyter-scipy:v1.9.0', - labels: { pythonVersion: '3.11' }, - hidden: true, - redirect: { - to: 'jupyterlab_scipy_200', - message: { - text: 'This update will change...', - level: 'Warning', - }, - }, - }, - ], - }, - 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' }, - redirect: { - to: 'small_cpu', - message: { - text: 'This update will change...', - level: 'Danger', - }, - }, - }, - ], - }, - }, - }, -}; - -const getLevelIcon = (level: string) => { +const getLevelIcon = (level: string | undefined) => { switch (level) { case 'Info': return ( @@ -109,30 +37,42 @@ const getLevelIcon = (level: string) => { } }; -export const WorkspaceRedirectInformationView: React.FC = () => { +interface WorkspaceRedirectInformationViewProps { + kind: string; +} + +export const WorkspaceRedirectInformationView: React.FC = ({ + kind, +}) => { const [activeKey, setActiveKey] = React.useState(0); - // change this to get from BE, and use the workspaceKinds API - const workspaceKind = mockedWorkspaceKind; + const [workspaceKind, workspaceKindLoaded] = useWorkspaceKindByName(kind); + const [imageConfig, setImageConfig] = + React.useState(); + const [podConfig, setPodConfig] = + React.useState(); - const { imageConfig } = workspaceKind.podTemplate.options; - const { podConfig } = workspaceKind.podTemplate.options; + React.useEffect(() => { + if (!workspaceKindLoaded) { + return; + } + setImageConfig(workspaceKind?.podTemplate.options.imageConfig); + setPodConfig(workspaceKind?.podTemplate.options.podConfig); + }, [workspaceKindLoaded, workspaceKind]); - const imageConfigRedirects = imageConfig.values.map((value) => ({ + const imageConfigRedirects = imageConfig?.values.map((value) => ({ src: value.id, - dest: value.redirect.to, - message: value.redirect.message.text, - level: value.redirect.message.level, + dest: value.redirect?.to, + message: value.redirect?.message.text, + level: value.redirect?.message.level, })); - const podConfigRedirects = podConfig.values.map((value) => ({ + const podConfigRedirects = podConfig?.values.map((value) => ({ src: value.id, - dest: value.redirect.to, - message: value.redirect.message.text, - level: value.redirect.message.level, + dest: value.redirect?.to, + message: value.redirect?.message.text, + level: value.redirect?.message.level, })); - const getMaxLevel = ( - redirects: { dest: string; level: string; message: string; src: string }[], - ) => { + const getMaxLevel = (redirects: NonNullable) => { let maxLevel = redirects[0].level; redirects.forEach((redirect) => { if ( @@ -146,8 +86,8 @@ export const WorkspaceRedirectInformationView: React.FC = () => { }; return ( - setActiveKey(eventKey)}> - {imageConfigRedirects.length > 0 && ( + setActiveKey(eventKey)}> + {imageConfigRedirects && imageConfigRedirects.length > 0 && ( { ))} )} - {podConfigRedirects.length > 0 && ( + {podConfigRedirects && podConfigRedirects.length > 0 && ( = ({ There are pending redirect updates for that workspace. Are you sure you want to proceed? - + ) : ( Are you sure you want to restart the workspace? diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx index 8e065e776..8563a5a1e 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx @@ -41,7 +41,7 @@ export const WorkspaceStartActionModal: React.FC = ({ There are pending redirect updates for that workspace. Are you sure you want to proceed? - + {workspace && } diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx index 992044f61..56d88cd77 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx @@ -46,7 +46,7 @@ export const WorkspaceStopActionModal: React.FC = ({ There are pending redirect updates for that workspace. Are you sure you want to proceed? - + ) : ( Are you sure you want to stop the workspace? diff --git a/workspaces/frontend/src/app/types.ts b/workspaces/frontend/src/app/types.ts index d3c081720..89f364f17 100644 --- a/workspaces/frontend/src/app/types.ts +++ b/workspaces/frontend/src/app/types.ts @@ -1,5 +1,11 @@ import { APIOptions } from '~/shared/api/types'; -import { Workspace, WorkspaceKind, WorkspacePodTemplateMutate } from '~/shared/types'; +import { + HealthCheckResponse, + Namespace, + Workspace, + WorkspaceKind, + WorkspacePodTemplateMutate, +} from '~/shared/types'; export type ResponseBody = { data: T; @@ -57,26 +63,87 @@ export type ResponseCustomProperty = export type ResponseCustomProperties = Record; export type ResponseStringCustomProperties = Record; -export type Namespace = { - name: string; -}; - -export type NamespacesList = Namespace[]; - -export type GetNamespaces = (opts: APIOptions) => Promise; +// Health +export type GetHealthCheck = (opts: APIOptions) => Promise; -export type GetWorkspaceKinds = (opts: APIOptions) => Promise; +// Namespace +export type ListNamespaces = (opts: APIOptions) => Promise; +// Workspace +export type ListAllWorkspaces = (opts: APIOptions) => Promise; +export type ListWorkspaces = (opts: APIOptions, namespace: string) => Promise; +export type GetWorkspace = ( + opts: APIOptions, + namespace: string, + workspace: string, +) => Promise; export type CreateWorkspace = ( opts: APIOptions, - data: CreateWorkspaceData, namespace: string, + data: CreateWorkspaceData, ) => Promise; +export type UpdateWorkspace = ( + opts: APIOptions, + namespace: string, + workspace: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it + data: any, +) => Promise; +export type PatchWorkspace = ( + opts: APIOptions, + namespace: string, + workspace: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it + data: any, +) => Promise; +export type DeleteWorkspace = ( + opts: APIOptions, + namespace: string, + workspace: string, +) => Promise; + +// WorkspaceKind +export type ListWorkspaceKinds = (opts: APIOptions) => Promise; +export type GetWorkspaceKind = (opts: APIOptions, kind: string) => Promise; +export type CreateWorkspaceKind = ( + opts: APIOptions, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it + data: any, +) => Promise; +export type UpdateWorkspaceKind = ( + opts: APIOptions, + kind: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it + data: any, +) => Promise; +export type PatchWorkspaceKind = ( + opts: APIOptions, + kind: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it + data: any, +) => Promise; +export type DeleteWorkspaceKind = (opts: APIOptions, kind: string) => Promise; export type NotebookAPIs = { - getNamespaces: GetNamespaces; - getWorkspaceKinds: GetWorkspaceKinds; + // Health + getHealthCheck: GetHealthCheck; + // Namespace + listNamespaces: ListNamespaces; + // Workspace + listAllWorkspaces: ListAllWorkspaces; + listWorkspaces: ListWorkspaces; + getWorkspace: GetWorkspace; createWorkspace: CreateWorkspace; + updateWorkspace: UpdateWorkspace; + patchWorkspace: PatchWorkspace; + deleteWorkspace: DeleteWorkspace; + // WorkspaceKind + listWorkspaceKinds: ListWorkspaceKinds; + getWorkspaceKind: GetWorkspaceKind; + createWorkspaceKind: CreateWorkspaceKind; + updateWorkspaceKind: UpdateWorkspaceKind; + patchWorkspaceKind: PatchWorkspaceKind; + deleteWorkspaceKind: DeleteWorkspaceKind; }; export type CreateWorkspaceData = { diff --git a/workspaces/frontend/src/shared/api/__tests__/notebookService.spec.ts b/workspaces/frontend/src/shared/api/__tests__/notebookService.spec.ts index 50f12a9e9..d84aa9a98 100644 --- a/workspaces/frontend/src/shared/api/__tests__/notebookService.spec.ts +++ b/workspaces/frontend/src/shared/api/__tests__/notebookService.spec.ts @@ -1,7 +1,7 @@ +import { BFF_API_VERSION } from '~/app/const'; import { restGET } from '~/shared/api/apiUtils'; import { handleRestFailures } from '~/shared/api/errorUtils'; -import { getNamespaces } from '~/shared/api/notebookService'; -import { BFF_API_VERSION } from '~/app/const'; +import { listNamespaces } from '~/shared/api/notebookService'; const mockRestPromise = Promise.resolve({ data: {} }); const mockRestResponse = {}; @@ -11,6 +11,7 @@ jest.mock('~/shared/api/apiUtils', () => ({ restGET: jest.fn(() => mockRestPromise), restPATCH: jest.fn(() => mockRestPromise), isNotebookResponse: jest.fn(() => true), + extractNotebookResponse: jest.fn(() => mockRestResponse), })); jest.mock('~/shared/api/errorUtils', () => ({ @@ -23,7 +24,7 @@ const APIOptionsMock = {}; describe('getNamespaces', () => { it('should call restGET and handleRestFailures to fetch namespaces', async () => { - const response = await getNamespaces(`/api/${BFF_API_VERSION}/namespaces`)(APIOptionsMock); + const response = await listNamespaces(`/api/${BFF_API_VERSION}/namespaces`)(APIOptionsMock); expect(response).toEqual(mockRestResponse); expect(restGETMock).toHaveBeenCalledTimes(1); expect(restGETMock).toHaveBeenCalledWith( diff --git a/workspaces/frontend/src/shared/api/apiUtils.ts b/workspaces/frontend/src/shared/api/apiUtils.ts index c51058269..10735494f 100644 --- a/workspaces/frontend/src/shared/api/apiUtils.ts +++ b/workspaces/frontend/src/shared/api/apiUtils.ts @@ -180,3 +180,10 @@ export const isNotebookResponse = (response: unknown): response is ResponseBo } return false; }; + +export function extractNotebookResponse(response: unknown): T { + if (isNotebookResponse(response)) { + return response.data; + } + throw new Error('Invalid response format'); +} diff --git a/workspaces/frontend/src/shared/api/callTypes.ts b/workspaces/frontend/src/shared/api/callTypes.ts new file mode 100644 index 000000000..d927489ed --- /dev/null +++ b/workspaces/frontend/src/shared/api/callTypes.ts @@ -0,0 +1,45 @@ +import { + CreateWorkspace, + CreateWorkspaceKind, + DeleteWorkspace, + DeleteWorkspaceKind, + GetWorkspace, + GetWorkspaceKind, + GetHealthCheck, + ListAllWorkspaces, + ListNamespaces, + ListWorkspaceKinds, + ListWorkspaces, + PatchWorkspace, + PatchWorkspaceKind, + UpdateWorkspace, + UpdateWorkspaceKind, +} from '~/app/types'; +import { APIOptions } from './types'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type KubeflowSpecificAPICall = (opts: APIOptions, ...args: any[]) => Promise; +type KubeflowAPICall = (hostPath: string) => ActualCall; + +// Health +export type GetHealthCheckAPI = KubeflowAPICall; + +// Namespace +export type ListNamespacesAPI = KubeflowAPICall; + +// Workspace +export type ListAllWorkspacesAPI = KubeflowAPICall; +export type ListWorkspacesAPI = KubeflowAPICall; +export type CreateWorkspaceAPI = KubeflowAPICall; +export type GetWorkspaceAPI = KubeflowAPICall; +export type UpdateWorkspaceAPI = KubeflowAPICall; +export type PatchWorkspaceAPI = KubeflowAPICall; +export type DeleteWorkspaceAPI = KubeflowAPICall; + +// WorkspaceKind +export type ListWorkspaceKindsAPI = KubeflowAPICall; +export type CreateWorkspaceKindAPI = KubeflowAPICall; +export type GetWorkspaceKindAPI = KubeflowAPICall; +export type UpdateWorkspaceKindAPI = KubeflowAPICall; +export type PatchWorkspaceKindAPI = KubeflowAPICall; +export type DeleteWorkspaceKindAPI = KubeflowAPICall; diff --git a/workspaces/frontend/src/shared/api/mockedData.ts b/workspaces/frontend/src/shared/api/mockedData.ts new file mode 100644 index 000000000..da3b2583b --- /dev/null +++ b/workspaces/frontend/src/shared/api/mockedData.ts @@ -0,0 +1,289 @@ +import { + HealthCheckResponse, + Namespace, + Workspace, + WorkspaceKind, + WorkspaceState, +} from '~/shared/types'; + +// Health +export const mockedHealthCheck: HealthCheckResponse = { + status: 'Healthy', + systemInfo: { version: '1.0.0' }, +}; + +// Namespace +export const mockNamespace1: Namespace = { name: 'workspace-test-1' }; +export const mockNamespace2: Namespace = { name: 'workspace-test-2' }; +export const mockNamespace3: Namespace = { name: 'workspace-test-3' }; +export const mockNamespaces = [mockNamespace1, mockNamespace2, mockNamespace3]; + +// WorkspaceKind +export const mockWorkspaceKindBase1: WorkspaceKind = { + name: 'jupyterlab1', + displayName: 'JupyterLab Notebook 1', + description: + 'Example of a description for JupyterLab a Workspace which runs JupyterLab in a Pod.', + deprecated: true, + deprecationMessage: + 'This WorkspaceKind was removed on 20XX-XX-XX, please use another WorkspaceKind.', + 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', jupyterlabVersion: '1.8.0' }, + hidden: true, + redirect: { + to: 'jupyterlab_scipy_190', + message: { + text: 'This update will change...', + level: 'Info', + }, + }, + }, + { + id: 'jupyterlab_scipy_190', + displayName: 'jupyter-scipy:v1.9.0', + labels: { pythonVersion: '3.12', jupyterlabVersion: '1.9.0' }, + hidden: true, + redirect: { + to: 'jupyterlab_scipy_200', + message: { + text: 'This update will change...', + level: 'Warning', + }, + }, + }, + { + id: 'jupyterlab_scipy_200', + displayName: 'jupyter-scipy:v2.0.0', + labels: { pythonVersion: '3.12', jupyterlabVersion: '2.0.0' }, + hidden: true, + redirect: { + to: 'jupyterlab_scipy_210', + message: { + text: 'This update will change...', + level: 'Warning', + }, + }, + }, + { + id: 'jupyterlab_scipy_210', + displayName: 'jupyter-scipy:v2.1.0', + labels: { pythonVersion: '3.13', jupyterlabVersion: '2.1.0' }, + hidden: true, + redirect: { + to: 'jupyterlab_scipy_220', + message: { + text: 'This update will change...', + level: 'Warning', + }, + }, + }, + ], + }, + 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' }, + redirect: { + to: 'small_cpu', + message: { + text: 'This update will change...', + level: 'Danger', + }, + }, + }, + { + id: 'large_cpu', + displayName: 'Large CPU', + description: 'Pod with 1 CPU, 1 Gb RAM', + labels: { cpu: '1000m', memory: '1Gi' }, + }, + ], + }, + }, + }, +}; + +const mockWorkspaceKind2: WorkspaceKind = { + ...mockWorkspaceKindBase1, + name: 'jupyterlab2', + displayName: 'JupyterLab Notebook 2', +}; +const mockWorkspaceKind3: WorkspaceKind = { + ...mockWorkspaceKindBase1, + name: 'jupyterlab3', + displayName: 'JupyterLab Notebook 3', +}; + +export const mockWorkspaceKinds = [mockWorkspaceKindBase1, mockWorkspaceKind2, mockWorkspaceKind3]; + +// Workspace +export const mockWorkspaceBase1: Workspace = { + name: 'My Jupyter Notebook', + namespace: mockNamespace1.name, + paused: true, + deferUpdates: true, + kind: mockWorkspaceKindBase1.name, + cpu: 3, + ram: 500, + podTemplate: { + podMetadata: { + labels: ['label1', 'label2'], + annotations: ['annotation1', 'annotation2'], + }, + volumes: { + home: '/home', + data: [ + { + pvcName: 'Volume-1', + mountPath: '/data', + readOnly: true, + }, + { + pvcName: 'Volume-2', + mountPath: '/data', + readOnly: false, + }, + ], + }, + endpoints: [ + { + displayName: 'JupyterLab', + port: '7777', + }, + ], + }, + options: { + imageConfig: 'jupyterlab_scipy_180', + podConfig: 'Small CPU', + }, + status: { + activity: { + lastActivity: 1739673600, + lastUpdate: 1739673700, + }, + pauseTime: 1739673500, + pendingRestart: false, + podTemplateOptions: { + imageConfig: { + desired: '', + redirectChain: [], + }, + }, + state: WorkspaceState.Paused, + stateMessage: 'It is paused.', + }, + redirectStatus: { + level: 'Info', + text: 'This is informational', + }, +}; + +export const mockWorkspaceBase2: Workspace = { + name: 'My Other Jupyter Notebook', + namespace: mockNamespace1.name, + paused: false, + deferUpdates: false, + kind: mockWorkspaceKindBase1.name, + cpu: 1, + ram: 12540, + podTemplate: { + podMetadata: { + labels: ['label1', 'label2'], + annotations: ['annotation1', 'annotation2'], + }, + volumes: { + home: '/home', + data: [ + { + pvcName: 'PVC-1', + mountPath: '/data', + readOnly: false, + }, + ], + }, + endpoints: [ + { + displayName: 'JupyterLab', + port: '8888', + }, + { + displayName: 'Spark Master', + port: '9999', + }, + ], + }, + options: { + imageConfig: 'jupyterlab_scipy_180', + podConfig: 'Large CPU', + }, + status: { + activity: { + lastActivity: 0, + lastUpdate: 0, + }, + pauseTime: 0, + pendingRestart: true, + podTemplateOptions: { + imageConfig: { + desired: '', + redirectChain: [], + }, + }, + state: WorkspaceState.Running, + stateMessage: 'It is running.', + }, + redirectStatus: { + level: 'Danger', + text: 'This is dangerous', + }, +}; + +export const mockWorkspace3: Workspace = { + ...mockWorkspaceBase1, + name: 'My Third Jupyter Notebook', + namespace: mockNamespace2.name, + kind: mockWorkspaceKind2.name, +}; + +export const mockWorkspace4: Workspace = { + ...mockWorkspaceBase2, + name: 'My Fourth Jupyter Notebook', + namespace: mockNamespace2.name, + kind: mockWorkspaceKind2.name, +}; + +export const mockAllWorkspaces = [ + mockWorkspaceBase1, + mockWorkspaceBase2, + mockWorkspace3, + mockWorkspace4, +]; diff --git a/workspaces/frontend/src/shared/api/mockedNotebookService.ts b/workspaces/frontend/src/shared/api/mockedNotebookService.ts new file mode 100644 index 000000000..fa01a1366 --- /dev/null +++ b/workspaces/frontend/src/shared/api/mockedNotebookService.ts @@ -0,0 +1,64 @@ +import { + CreateWorkspaceAPI, + CreateWorkspaceKindAPI, + DeleteWorkspaceAPI, + DeleteWorkspaceKindAPI, + GetHealthCheckAPI, + GetWorkspaceAPI, + GetWorkspaceKindAPI, + ListAllWorkspacesAPI, + ListNamespacesAPI, + ListWorkspaceKindsAPI, + ListWorkspacesAPI, + PatchWorkspaceAPI, + PatchWorkspaceKindAPI, + UpdateWorkspaceAPI, + UpdateWorkspaceKindAPI, +} from './callTypes'; +import { + mockedHealthCheck, + mockNamespaces, + mockWorkspaceKinds, + mockAllWorkspaces, + mockWorkspaceBase1, +} from './mockedData'; + +export const mockGetHealthCheck: GetHealthCheckAPI = () => async () => mockedHealthCheck; + +export const mockListNamespaces: ListNamespacesAPI = () => async () => mockNamespaces; + +export const mockListAllWorkspaces: ListAllWorkspacesAPI = () => async () => mockAllWorkspaces; + +export const mockListWorkspaces: ListWorkspacesAPI = () => async (_opts, namespace) => + mockAllWorkspaces.filter((workspace) => workspace.namespace === namespace); + +export const mockGetWorkspace: GetWorkspaceAPI = () => async (_opts, namespace, workspace) => + mockAllWorkspaces.find((w) => w.name === workspace && w.namespace === namespace)!; + +export const mockCreateWorkspace: CreateWorkspaceAPI = () => async () => mockWorkspaceBase1; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +export const mockUpdateWorkspace: UpdateWorkspaceAPI = () => async () => {}; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +export const mockPatchWorkspace: PatchWorkspaceAPI = () => async () => {}; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +export const mockDeleteWorkspace: DeleteWorkspaceAPI = () => async () => {}; + +export const mockListWorkspaceKinds: ListWorkspaceKindsAPI = () => async () => mockWorkspaceKinds; + +export const mockGetWorkspaceKind: GetWorkspaceKindAPI = () => async (_opts, kind) => + mockWorkspaceKinds.find((w) => w.name === kind)!; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +export const mockCreateWorkspaceKind: CreateWorkspaceKindAPI = () => async () => {}; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +export const mockUpdateWorkspaceKind: UpdateWorkspaceKindAPI = () => async () => {}; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +export const mockPatchWorkspaceKind: PatchWorkspaceKindAPI = () => async () => {}; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +export const mockDeleteWorkspaceKind: DeleteWorkspaceKindAPI = () => async () => {}; diff --git a/workspaces/frontend/src/shared/api/notebookService.ts b/workspaces/frontend/src/shared/api/notebookService.ts index 81017369e..afdbed9f7 100644 --- a/workspaces/frontend/src/shared/api/notebookService.ts +++ b/workspaces/frontend/src/shared/api/notebookService.ts @@ -1,37 +1,103 @@ -import { NamespacesList, CreateWorkspaceData } from '~/app/types'; -import { isNotebookResponse, restGET, restCREATE } from '~/shared/api/apiUtils'; -import { APIOptions } from '~/shared/api/types'; +import { + extractNotebookResponse, + restCREATE, + restDELETE, + restGET, + restPATCH, + restUPDATE, +} from '~/shared/api/apiUtils'; import { handleRestFailures } from '~/shared/api/errorUtils'; -import { Workspace, WorkspaceKind } from '~/shared/types'; - -export const getNamespaces = - (hostPath: string) => - (opts: APIOptions): Promise => - handleRestFailures(restGET(hostPath, `/namespaces`, {}, opts)).then((response) => { - if (isNotebookResponse(response)) { - return response.data; - } - throw new Error('Invalid response format'); - }); - -export const getWorkspaceKinds = - (hostPath: string) => - (opts: APIOptions): Promise => - handleRestFailures(restGET(hostPath, `/workspacekinds`, {}, opts)).then((response) => { - if (isNotebookResponse(response)) { - return response.data; - } - throw new Error('Invalid response format'); - }); - -export const createWorkspace = - (hostPath: string) => - (opts: APIOptions, data: CreateWorkspaceData, namespace = ''): Promise => - handleRestFailures(restCREATE(hostPath, `/workspaces/${namespace}`, data, opts)).then( - (response) => { - if (isNotebookResponse(response)) { - return response.data; - } - throw new Error('Invalid response format'); - }, - ); +import { HealthCheckResponse, Namespace, Workspace, WorkspaceKind } from '~/shared/types'; +import { + CreateWorkspaceAPI, + CreateWorkspaceKindAPI, + DeleteWorkspaceAPI, + DeleteWorkspaceKindAPI, + GetHealthCheckAPI, + GetWorkspaceAPI, + GetWorkspaceKindAPI, + ListAllWorkspacesAPI, + ListNamespacesAPI, + ListWorkspaceKindsAPI, + ListWorkspacesAPI, + PatchWorkspaceAPI, + PatchWorkspaceKindAPI, + UpdateWorkspaceAPI, + UpdateWorkspaceKindAPI, +} from './callTypes'; + +export const getHealthCheck: GetHealthCheckAPI = (hostPath) => (opts) => + handleRestFailures(restGET(hostPath, `/healthcheck`, {}, opts)).then((response) => + extractNotebookResponse(response), + ); + +export const listNamespaces: ListNamespacesAPI = (hostPath) => (opts) => + handleRestFailures(restGET(hostPath, `/namespaces`, {}, opts)).then((response) => + extractNotebookResponse(response), + ); + +export const listAllWorkspaces: ListAllWorkspacesAPI = (hostPath) => (opts) => + handleRestFailures(restGET(hostPath, `/workspaces`, {}, opts)).then((response) => + extractNotebookResponse(response), + ); + +export const listWorkspaces: ListWorkspacesAPI = (hostPath) => (opts, namespace) => + handleRestFailures(restGET(hostPath, `/workspaces/${namespace}`, {}, opts)).then((response) => + extractNotebookResponse(response), + ); + +export const getWorkspace: GetWorkspaceAPI = (hostPath) => (opts, namespace, workspace) => + handleRestFailures(restGET(hostPath, `/workspaces/${namespace}/${workspace}`, {}, opts)).then( + (response) => extractNotebookResponse(response), + ); + +export const createWorkspace: CreateWorkspaceAPI = (hostPath) => (opts, namespace, data) => + handleRestFailures(restCREATE(hostPath, `/workspaces/${namespace}`, data, {}, opts)).then( + (response) => extractNotebookResponse(response), + ); + +export const updateWorkspace: UpdateWorkspaceAPI = + (hostPath) => (opts, namespace, workspace, data) => + handleRestFailures( + restUPDATE(hostPath, `/workspaces/${namespace}/${workspace}`, data, {}, opts), + ).then((response) => extractNotebookResponse(response)); + +export const patchWorkspace: PatchWorkspaceAPI = (hostPath) => (opts, namespace, workspace, data) => + handleRestFailures(restPATCH(hostPath, `/workspaces/${namespace}/${workspace}`, data, opts)).then( + (response) => extractNotebookResponse(response), + ); + +export const deleteWorkspace: DeleteWorkspaceAPI = (hostPath) => (opts, namespace, workspace) => + handleRestFailures( + restDELETE(hostPath, `/workspaces/${namespace}/${workspace}`, {}, {}, opts), + ).then((response) => extractNotebookResponse(response)); + +export const listWorkspaceKinds: ListWorkspaceKindsAPI = (hostPath) => (opts) => + handleRestFailures(restGET(hostPath, `/workspacekinds`, {}, opts)).then((response) => + extractNotebookResponse(response), + ); + +export const getWorkspaceKind: GetWorkspaceKindAPI = (hostPath) => (opts, kind) => + handleRestFailures(restGET(hostPath, `/workspacekinds/${kind}`, {}, opts)).then((response) => + extractNotebookResponse(response), + ); + +export const createWorkspaceKind: CreateWorkspaceKindAPI = (hostPath) => (opts, data) => + handleRestFailures(restCREATE(hostPath, `/workspacekinds`, data, {}, opts)).then((response) => + extractNotebookResponse(response), + ); + +export const updateWorkspaceKind: UpdateWorkspaceKindAPI = (hostPath) => (opts, kind, data) => + handleRestFailures(restUPDATE(hostPath, `/workspacekinds/${kind}`, data, {}, opts)).then( + (response) => extractNotebookResponse(response), + ); + +export const patchWorkspaceKind: PatchWorkspaceKindAPI = (hostPath) => (opts, kind, data) => + handleRestFailures(restPATCH(hostPath, `/workspacekinds/${kind}`, data, opts)).then((response) => + extractNotebookResponse(response), + ); + +export const deleteWorkspaceKind: DeleteWorkspaceKindAPI = (hostPath) => (opts, kind) => + handleRestFailures(restDELETE(hostPath, `/workspacekinds/${kind}`, {}, {}, opts)).then( + (response) => extractNotebookResponse(response), + ); diff --git a/workspaces/frontend/src/shared/types.ts b/workspaces/frontend/src/shared/types.ts index f775a2ea8..e56b546a2 100644 --- a/workspaces/frontend/src/shared/types.ts +++ b/workspaces/frontend/src/shared/types.ts @@ -203,3 +203,14 @@ export type WorkspaceKindsColumnNames = { deprecated: string; numberOfWorkspaces: string; }; + +export type Namespace = { + name: string; +}; + +export type HealthCheckResponse = { + status: 'Healthy' | 'Unhealthy'; + systemInfo: { + version: string; + }; +}; From 2eaf652e99c9193f8618a106d9eec58f7d71a7a8 Mon Sep 17 00:00:00 2001 From: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> Date: Mon, 5 May 2025 10:46:05 -0300 Subject: [PATCH 02/12] Reorganize mock-related code Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> --- .../frontend/src/__mocks__/mockNamespaces.ts | 7 +- .../frontend/src/__mocks__/mockWorkspaces.ts | 121 +-------- .../src/app/context/useNotebookAPIState.tsx | 2 +- .../mockedData.ts => mock/mockBuilder.ts} | 245 ++++++------------ .../mockNotebookService.ts} | 14 +- .../shared/mock/mockNotebookServiceData.ts | 115 ++++++++ 6 files changed, 208 insertions(+), 296 deletions(-) rename workspaces/frontend/src/shared/{api/mockedData.ts => mock/mockBuilder.ts} (62%) rename workspaces/frontend/src/shared/{api/mockedNotebookService.ts => mock/mockNotebookService.ts} (92%) create mode 100644 workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts diff --git a/workspaces/frontend/src/__mocks__/mockNamespaces.ts b/workspaces/frontend/src/__mocks__/mockNamespaces.ts index ea7ec4337..cde3cfbd6 100644 --- a/workspaces/frontend/src/__mocks__/mockNamespaces.ts +++ b/workspaces/frontend/src/__mocks__/mockNamespaces.ts @@ -1,7 +1,8 @@ +import { buildMockNamespace } from '~/shared/mock/mockBuilder'; import { Namespace } from '~/shared/types'; export const mockNamespaces: Namespace[] = [ - { name: 'default' }, - { name: 'kubeflow' }, - { name: 'custom-namespace' }, + buildMockNamespace({ name: 'default' }), + buildMockNamespace({ name: 'kubeflow' }), + buildMockNamespace({ name: 'custom-namespace' }), ]; diff --git a/workspaces/frontend/src/__mocks__/mockWorkspaces.ts b/workspaces/frontend/src/__mocks__/mockWorkspaces.ts index 90990def7..f16ea457c 100644 --- a/workspaces/frontend/src/__mocks__/mockWorkspaces.ts +++ b/workspaces/frontend/src/__mocks__/mockWorkspaces.ts @@ -1,123 +1,12 @@ -import { Workspace, WorkspaceState } from '~/shared/types'; +import { buildMockWorkspace } from '~/shared/mock/mockBuilder'; -export const mockWorkspaces: Workspace[] = [ - { - name: 'My Jupyter Notebook', - namespace: 'namespace1', - paused: true, - deferUpdates: true, - kind: 'jupyter-lab', - cpu: 3, - ram: 500, - podTemplate: { - podMetadata: { - labels: ['label1', 'label2'], - annotations: ['annotation1', 'annotation2'], - }, - volumes: { - home: '/home', - data: [ - { - pvcName: 'Volume-1', - mountPath: '/data', - readOnly: true, - }, - { - pvcName: 'Volume-2', - mountPath: '/data', - readOnly: false, - }, - ], - }, - endpoints: [ - { - displayName: 'JupyterLab', - port: '7777', - }, - ], - }, - options: { - imageConfig: 'jupyterlab_scipy_180', - podConfig: 'Small CPU', - }, - status: { - activity: { - lastActivity: 1739673600, - lastUpdate: 1739673700, - }, - pauseTime: 1739673500, - pendingRestart: false, - podTemplateOptions: { - imageConfig: { - desired: '', - redirectChain: [], - }, - }, - state: WorkspaceState.Paused, - stateMessage: 'It is paused.', - }, - redirectStatus: { - level: 'Info', - text: 'This is informational', - }, - }, - { +export const mockWorkspaces = [ + buildMockWorkspace(), + buildMockWorkspace({ name: 'My Other Jupyter Notebook', - namespace: 'namespace1', - paused: false, - deferUpdates: false, - kind: 'jupyter-lab', - cpu: 1, - ram: 12540, - podTemplate: { - podMetadata: { - labels: ['label1', 'label2'], - annotations: ['annotation1', 'annotation2'], - }, - volumes: { - home: '/home', - data: [ - { - pvcName: 'PVC-1', - mountPath: '/data', - readOnly: false, - }, - ], - }, - endpoints: [ - { - displayName: 'JupyterLab', - port: '8888', - }, - { - displayName: 'Spark Master', - port: '9999', - }, - ], - }, options: { imageConfig: 'jupyterlab_scipy_180', podConfig: 'Large CPU', }, - status: { - activity: { - lastActivity: 0, - lastUpdate: 0, - }, - pauseTime: 0, - pendingRestart: false, - podTemplateOptions: { - imageConfig: { - desired: '', - redirectChain: [], - }, - }, - state: WorkspaceState.Running, - stateMessage: 'It is running.', - }, - redirectStatus: { - level: 'Danger', - text: 'This is dangerous', - }, - }, + }), ]; diff --git a/workspaces/frontend/src/app/context/useNotebookAPIState.tsx b/workspaces/frontend/src/app/context/useNotebookAPIState.tsx index 6471183d0..f4673e74d 100644 --- a/workspaces/frontend/src/app/context/useNotebookAPIState.tsx +++ b/workspaces/frontend/src/app/context/useNotebookAPIState.tsx @@ -16,7 +16,7 @@ import { mockPatchWorkspaceKind, mockUpdateWorkspace, mockUpdateWorkspaceKind, -} from '~/shared/api/mockedNotebookService'; +} from '~/shared/mock/mockNotebookService'; import { createWorkspace, createWorkspaceKind, diff --git a/workspaces/frontend/src/shared/api/mockedData.ts b/workspaces/frontend/src/shared/mock/mockBuilder.ts similarity index 62% rename from workspaces/frontend/src/shared/api/mockedData.ts rename to workspaces/frontend/src/shared/mock/mockBuilder.ts index da3b2583b..31a6da148 100644 --- a/workspaces/frontend/src/shared/api/mockedData.ts +++ b/workspaces/frontend/src/shared/mock/mockBuilder.ts @@ -6,22 +6,84 @@ import { WorkspaceState, } from '~/shared/types'; -// Health -export const mockedHealthCheck: HealthCheckResponse = { +export const buildMockHealthCheckResponse = ( + healthCheckResponse?: Partial, +): HealthCheckResponse => ({ status: 'Healthy', systemInfo: { version: '1.0.0' }, -}; + ...healthCheckResponse, +}); -// Namespace -export const mockNamespace1: Namespace = { name: 'workspace-test-1' }; -export const mockNamespace2: Namespace = { name: 'workspace-test-2' }; -export const mockNamespace3: Namespace = { name: 'workspace-test-3' }; -export const mockNamespaces = [mockNamespace1, mockNamespace2, mockNamespace3]; +export const buildMockNamespace = (namespace?: Partial): Namespace => ({ + name: 'default', + ...namespace, +}); -// WorkspaceKind -export const mockWorkspaceKindBase1: WorkspaceKind = { - name: 'jupyterlab1', - displayName: 'JupyterLab Notebook 1', +export const buildMockWorkspace = (workspace?: Partial): Workspace => ({ + name: 'My Jupyter Notebook', + namespace: 'default', + paused: true, + deferUpdates: true, + kind: 'jupyterlab', + cpu: 3, + ram: 500, + podTemplate: { + podMetadata: { + labels: ['label1', 'label2'], + annotations: ['annotation1', 'annotation2'], + }, + volumes: { + home: '/home', + data: [ + { + pvcName: 'Volume-1', + mountPath: '/data', + readOnly: true, + }, + { + pvcName: 'Volume-2', + mountPath: '/data', + readOnly: false, + }, + ], + }, + endpoints: [ + { + displayName: 'JupyterLab', + port: '7777', + }, + ], + }, + options: { + imageConfig: 'jupyterlab_scipy_180', + podConfig: 'Small CPU', + }, + status: { + activity: { + lastActivity: 1739673600, + lastUpdate: 1739673700, + }, + pauseTime: 1739673500, + pendingRestart: false, + podTemplateOptions: { + imageConfig: { + desired: '', + redirectChain: [], + }, + }, + state: WorkspaceState.Paused, + stateMessage: 'It is paused.', + }, + redirectStatus: { + level: 'Info', + text: 'This is informational', + }, + ...workspace, +}); + +export const buildMockWorkspaceKind = (workspaceKind?: Partial): WorkspaceKind => ({ + name: 'jupyterlab', + displayName: 'JupyterLab Notebook', description: 'Example of a description for JupyterLab a Workspace which runs JupyterLab in a Pod.', deprecated: true, @@ -130,160 +192,5 @@ export const mockWorkspaceKindBase1: WorkspaceKind = { }, }, }, -}; - -const mockWorkspaceKind2: WorkspaceKind = { - ...mockWorkspaceKindBase1, - name: 'jupyterlab2', - displayName: 'JupyterLab Notebook 2', -}; -const mockWorkspaceKind3: WorkspaceKind = { - ...mockWorkspaceKindBase1, - name: 'jupyterlab3', - displayName: 'JupyterLab Notebook 3', -}; - -export const mockWorkspaceKinds = [mockWorkspaceKindBase1, mockWorkspaceKind2, mockWorkspaceKind3]; - -// Workspace -export const mockWorkspaceBase1: Workspace = { - name: 'My Jupyter Notebook', - namespace: mockNamespace1.name, - paused: true, - deferUpdates: true, - kind: mockWorkspaceKindBase1.name, - cpu: 3, - ram: 500, - podTemplate: { - podMetadata: { - labels: ['label1', 'label2'], - annotations: ['annotation1', 'annotation2'], - }, - volumes: { - home: '/home', - data: [ - { - pvcName: 'Volume-1', - mountPath: '/data', - readOnly: true, - }, - { - pvcName: 'Volume-2', - mountPath: '/data', - readOnly: false, - }, - ], - }, - endpoints: [ - { - displayName: 'JupyterLab', - port: '7777', - }, - ], - }, - options: { - imageConfig: 'jupyterlab_scipy_180', - podConfig: 'Small CPU', - }, - status: { - activity: { - lastActivity: 1739673600, - lastUpdate: 1739673700, - }, - pauseTime: 1739673500, - pendingRestart: false, - podTemplateOptions: { - imageConfig: { - desired: '', - redirectChain: [], - }, - }, - state: WorkspaceState.Paused, - stateMessage: 'It is paused.', - }, - redirectStatus: { - level: 'Info', - text: 'This is informational', - }, -}; - -export const mockWorkspaceBase2: Workspace = { - name: 'My Other Jupyter Notebook', - namespace: mockNamespace1.name, - paused: false, - deferUpdates: false, - kind: mockWorkspaceKindBase1.name, - cpu: 1, - ram: 12540, - podTemplate: { - podMetadata: { - labels: ['label1', 'label2'], - annotations: ['annotation1', 'annotation2'], - }, - volumes: { - home: '/home', - data: [ - { - pvcName: 'PVC-1', - mountPath: '/data', - readOnly: false, - }, - ], - }, - endpoints: [ - { - displayName: 'JupyterLab', - port: '8888', - }, - { - displayName: 'Spark Master', - port: '9999', - }, - ], - }, - options: { - imageConfig: 'jupyterlab_scipy_180', - podConfig: 'Large CPU', - }, - status: { - activity: { - lastActivity: 0, - lastUpdate: 0, - }, - pauseTime: 0, - pendingRestart: true, - podTemplateOptions: { - imageConfig: { - desired: '', - redirectChain: [], - }, - }, - state: WorkspaceState.Running, - stateMessage: 'It is running.', - }, - redirectStatus: { - level: 'Danger', - text: 'This is dangerous', - }, -}; - -export const mockWorkspace3: Workspace = { - ...mockWorkspaceBase1, - name: 'My Third Jupyter Notebook', - namespace: mockNamespace2.name, - kind: mockWorkspaceKind2.name, -}; - -export const mockWorkspace4: Workspace = { - ...mockWorkspaceBase2, - name: 'My Fourth Jupyter Notebook', - namespace: mockNamespace2.name, - kind: mockWorkspaceKind2.name, -}; - -export const mockAllWorkspaces = [ - mockWorkspaceBase1, - mockWorkspaceBase2, - mockWorkspace3, - mockWorkspace4, -]; + ...workspaceKind, +}); diff --git a/workspaces/frontend/src/shared/api/mockedNotebookService.ts b/workspaces/frontend/src/shared/mock/mockNotebookService.ts similarity index 92% rename from workspaces/frontend/src/shared/api/mockedNotebookService.ts rename to workspaces/frontend/src/shared/mock/mockNotebookService.ts index fa01a1366..786c454a5 100644 --- a/workspaces/frontend/src/shared/api/mockedNotebookService.ts +++ b/workspaces/frontend/src/shared/mock/mockNotebookService.ts @@ -14,16 +14,16 @@ import { PatchWorkspaceKindAPI, UpdateWorkspaceAPI, UpdateWorkspaceKindAPI, -} from './callTypes'; +} from '~/shared/api/callTypes'; import { - mockedHealthCheck, + mockAllWorkspaces, + mockedHealthCheckResponse, mockNamespaces, + mockWorkspace1, mockWorkspaceKinds, - mockAllWorkspaces, - mockWorkspaceBase1, -} from './mockedData'; +} from '~/shared/mock/mockNotebookServiceData'; -export const mockGetHealthCheck: GetHealthCheckAPI = () => async () => mockedHealthCheck; +export const mockGetHealthCheck: GetHealthCheckAPI = () => async () => mockedHealthCheckResponse; export const mockListNamespaces: ListNamespacesAPI = () => async () => mockNamespaces; @@ -35,7 +35,7 @@ export const mockListWorkspaces: ListWorkspacesAPI = () => async (_opts, namespa export const mockGetWorkspace: GetWorkspaceAPI = () => async (_opts, namespace, workspace) => mockAllWorkspaces.find((w) => w.name === workspace && w.namespace === namespace)!; -export const mockCreateWorkspace: CreateWorkspaceAPI = () => async () => mockWorkspaceBase1; +export const mockCreateWorkspace: CreateWorkspaceAPI = () => async () => mockWorkspace1; // eslint-disable-next-line @typescript-eslint/no-empty-function export const mockUpdateWorkspace: UpdateWorkspaceAPI = () => async () => {}; diff --git a/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts b/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts new file mode 100644 index 000000000..4ceab09b2 --- /dev/null +++ b/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts @@ -0,0 +1,115 @@ +import { + buildMockHealthCheckResponse, + buildMockNamespace, + buildMockWorkspace, + buildMockWorkspaceKind, +} from '~/shared/mock/mockBuilder'; +import { Workspace, WorkspaceKind, WorkspaceState } from '~/shared/types'; + +// Health +export const mockedHealthCheckResponse = buildMockHealthCheckResponse(); + +// Namespace +export const mockNamespace1 = buildMockNamespace({ name: 'workspace-test-1' }); +export const mockNamespace2 = buildMockNamespace({ name: 'workspace-test-2' }); +export const mockNamespace3 = buildMockNamespace({ name: 'workspace-test-3' }); + +export const mockNamespaces = [mockNamespace1, mockNamespace2, mockNamespace3]; + +// WorkspaceKind +export const mockWorkspaceKind1: WorkspaceKind = buildMockWorkspaceKind({ + name: 'jupyterlab1', + displayName: 'JupyterLab Notebook 1', +}); + +export const mockWorkspaceKind2: WorkspaceKind = buildMockWorkspaceKind({ + name: 'jupyterlab2', + displayName: 'JupyterLab Notebook 2', +}); + +export const mockWorkspaceKind3: WorkspaceKind = buildMockWorkspaceKind({ + name: 'jupyterlab3', + displayName: 'JupyterLab Notebook 3', +}); + +export const mockWorkspaceKinds = [mockWorkspaceKind1, mockWorkspaceKind2, mockWorkspaceKind3]; + +// Workspace +export const mockWorkspace1: Workspace = buildMockWorkspace({ + kind: mockWorkspaceKind1.name, + namespace: mockNamespace1.name, +}); + +export const mockWorkspace2: Workspace = buildMockWorkspace({ + name: 'My Other Jupyter Notebook', + kind: mockWorkspaceKind1.name, + namespace: mockNamespace1.name, + paused: false, + deferUpdates: false, + cpu: 1, + ram: 12540, + podTemplate: { + podMetadata: { + labels: ['label1', 'label2'], + annotations: ['annotation1', 'annotation2'], + }, + volumes: { + home: '/home', + data: [ + { + pvcName: 'PVC-1', + mountPath: '/data', + readOnly: false, + }, + ], + }, + endpoints: [ + { + displayName: 'JupyterLab', + port: '8888', + }, + { + displayName: 'Spark Master', + port: '9999', + }, + ], + }, + options: { + imageConfig: 'jupyterlab_scipy_180', + podConfig: 'Large CPU', + }, + status: { + activity: { + lastActivity: 0, + lastUpdate: 0, + }, + pauseTime: 1739673500, + pendingRestart: false, + podTemplateOptions: { + imageConfig: { + desired: '', + redirectChain: [], + }, + }, + state: WorkspaceState.Running, + stateMessage: 'It is running.', + }, + redirectStatus: { + level: 'Danger', + text: 'This is dangerous', + }, +}); + +export const mockWorkspace3 = buildMockWorkspace({ + name: 'My Third Jupyter Notebook', + namespace: mockNamespace2.name, + kind: mockWorkspaceKind2.name, +}); + +export const mockWorkspace4 = buildMockWorkspace({ + name: 'My Fourth Jupyter Notebook', + namespace: mockNamespace2.name, + kind: mockWorkspaceKind2.name, +}); + +export const mockAllWorkspaces = [mockWorkspace1, mockWorkspace2, mockWorkspace3, mockWorkspace4]; From 877c02fa7e81a5ce0c980c8be5a4ab09d58b05fa Mon Sep 17 00:00:00 2001 From: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> Date: Tue, 6 May 2025 14:41:24 -0300 Subject: [PATCH 03/12] Refactor FE types according to BE types and adapt the FE code accordingly Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> --- .../frontend/src/__mocks__/mockWorkspaces.ts | 51 ++- .../workspaces/filterWorkspacesTest.cy.ts | 20 +- .../src/app/actions/WorkspaceKindsActions.tsx | 22 +- .../src/app/hooks/useWorkspaceCountPerKind.ts | 2 +- .../Workspaces/Creation/WorkspaceCreation.tsx | 8 +- .../image/WorkspaceCreationImageDetails.tsx | 10 +- .../image/WorkspaceCreationImageList.tsx | 21 +- .../image/WorkspaceCreationImageSelection.tsx | 10 +- .../Creation/labelFilter/FilterByLabels.tsx | 14 +- .../WorkspaceCreationPodConfigDetails.tsx | 10 +- .../WorkspaceCreationPodConfigList.tsx | 22 +- .../WorkspaceCreationPodConfigSelection.tsx | 10 +- .../WorkspaceCreationPropertiesSelection.tsx | 29 +- .../WorkspaceCreationPropertiesVolumes.tsx | 8 +- .../Details/WorkspaceDetailsActivity.tsx | 4 +- .../Details/WorkspaceDetailsOverview.tsx | 10 +- .../Workspaces/WorkspaceConnectAction.tsx | 31 +- .../src/app/pages/Workspaces/Workspaces.tsx | 122 ++++--- .../WorkspaceRedirectInformationView.tsx | 8 +- .../WorkspaceRestartActionModal.tsx | 4 +- .../WorkspaceStartActionModal.tsx | 2 +- .../WorkspaceStopActionModal.tsx | 4 +- workspaces/frontend/src/app/types.ts | 51 --- .../frontend/src/shared/mock/mockBuilder.ts | 174 +++++++--- .../shared/mock/mockNotebookServiceData.ts | 116 ++++--- workspaces/frontend/src/shared/types.ts | 317 +++++++++++------- .../src/shared/utilities/WorkspaceUtils.ts | 10 + 27 files changed, 642 insertions(+), 448 deletions(-) diff --git a/workspaces/frontend/src/__mocks__/mockWorkspaces.ts b/workspaces/frontend/src/__mocks__/mockWorkspaces.ts index f16ea457c..83093df48 100644 --- a/workspaces/frontend/src/__mocks__/mockWorkspaces.ts +++ b/workspaces/frontend/src/__mocks__/mockWorkspaces.ts @@ -3,10 +3,53 @@ import { buildMockWorkspace } from '~/shared/mock/mockBuilder'; export const mockWorkspaces = [ buildMockWorkspace(), buildMockWorkspace({ - name: 'My Other Jupyter Notebook', - options: { - imageConfig: 'jupyterlab_scipy_180', - podConfig: 'Large CPU', + 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' }, + ], + }, + }, + }, }, }), ]; diff --git a/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/filterWorkspacesTest.cy.ts b/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/filterWorkspacesTest.cy.ts index 4d7b5eb5a..075c8c3ca 100644 --- a/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/filterWorkspacesTest.cy.ts +++ b/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/filterWorkspacesTest.cy.ts @@ -24,37 +24,37 @@ describe('Application', () => { 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'); diff --git a/workspaces/frontend/src/app/actions/WorkspaceKindsActions.tsx b/workspaces/frontend/src/app/actions/WorkspaceKindsActions.tsx index 49bb9ec97..49170787b 100644 --- a/workspaces/frontend/src/app/actions/WorkspaceKindsActions.tsx +++ b/workspaces/frontend/src/app/actions/WorkspaceKindsActions.tsx @@ -1,4 +1,4 @@ -import { WorkspaceKind } from '~/shared/types'; +import { WorkspaceKind, WorkspaceOptionRedirect } from '~/shared/types'; type KindLogoDict = Record; @@ -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; /** * Builds a dictionary of workspace kinds to redirect statuses. @@ -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; } diff --git a/workspaces/frontend/src/app/hooks/useWorkspaceCountPerKind.ts b/workspaces/frontend/src/app/hooks/useWorkspaceCountPerKind.ts index 675c55e38..d6dac8af5 100644 --- a/workspaces/frontend/src/app/hooks/useWorkspaceCountPerKind.ts +++ b/workspaces/frontend/src/app/hooks/useWorkspaceCountPerKind.ts @@ -14,7 +14,7 @@ export const useWorkspaceCountPerKind = (): WorkspaceCountPerKind => { React.useEffect(() => { api.listAllWorkspaces({}).then((workspaces) => { const countPerKind = workspaces.reduce((acc: WorkspaceCountPerKind, workspace: Workspace) => { - acc[workspace.kind] = (acc[workspace.kind] || 0) + 1; + acc[workspace.workspaceKind.name] = (acc[workspace.workspaceKind.name] || 0) + 1; return acc; }, {}); setWorkspaceCountPerKind(countPerKind); diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx index a69abda5e..245dd4194 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx @@ -19,9 +19,9 @@ import { WorkspaceCreationKindSelection } from '~/app/pages/Workspaces/Creation/ import { WorkspaceCreationPropertiesSelection } from '~/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection'; import { WorkspaceCreationPodConfigSelection } from '~/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigSelection'; import { - WorkspaceImage, + WorkspaceImageConfigValue, WorkspaceKind, - WorkspacePodConfig, + WorkspacePodConfigValue, WorkspaceProperties, } from '~/shared/types'; @@ -37,8 +37,8 @@ const WorkspaceCreation: React.FunctionComponent = () => { const [currentStep, setCurrentStep] = useState(WorkspaceCreationSteps.KindSelection); const [selectedKind, setSelectedKind] = useState(); - const [selectedImage, setSelectedImage] = useState(); - const [selectedPodConfig, setSelectedPodConfig] = useState(); + const [selectedImage, setSelectedImage] = useState(); + const [selectedPodConfig, setSelectedPodConfig] = useState(); const [, setSelectedProperties] = useState(); const getStepVariant = useCallback( diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageDetails.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageDetails.tsx index df59bedcd..19ebcf7a4 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageDetails.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageDetails.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { List, ListItem, Title } from '@patternfly/react-core'; -import { WorkspaceImage } from '~/shared/types'; +import { WorkspacePodConfigValue } from '~/shared/types'; type WorkspaceCreationImageDetailsProps = { - workspaceImage?: WorkspaceImage; + workspaceImage?: WorkspacePodConfigValue; }; export const WorkspaceCreationImageDetails: React.FunctionComponent< @@ -18,9 +18,9 @@ export const WorkspaceCreationImageDetails: React.FunctionComponent< {workspaceImage.displayName}
- {Object.keys(workspaceImage.labels).map((labelKey) => ( - - {labelKey}={workspaceImage.labels[labelKey]} + {workspaceImage.labels.map((label) => ( + + {label.key}={label.value} ))} diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageList.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageList.tsx index 7b22b65c5..c573bad47 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageList.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageList.tsx @@ -12,20 +12,20 @@ import { CardBody, } from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons/dist/esm/icons/search-icon'; -import { WorkspaceImage } from '~/shared/types'; import Filter, { FilteredColumn } from '~/shared/components/Filter'; +import { WorkspaceImageConfigValue } from '~/shared/types'; type WorkspaceCreationImageListProps = { - images: WorkspaceImage[]; + images: WorkspaceImageConfigValue[]; selectedLabels: Map>; - selectedImage: WorkspaceImage | undefined; - onSelect: (workspaceImage: WorkspaceImage | undefined) => void; + selectedImage: WorkspaceImageConfigValue | undefined; + onSelect: (workspaceImage: WorkspaceImageConfigValue | undefined) => void; }; export const WorkspaceCreationImageList: React.FunctionComponent< WorkspaceCreationImageListProps > = ({ images, selectedLabels, selectedImage, onSelect }) => { - const [workspaceImages, setWorkspaceImages] = useState(images); + const [workspaceImages, setWorkspaceImages] = useState(images); const [filters, setFilters] = useState([]); const filterableColumns = useMemo( @@ -36,13 +36,12 @@ export const WorkspaceCreationImageList: React.FunctionComponent< ); const getFilteredWorkspaceImagesByLabels = useCallback( - (unfilteredImages: WorkspaceImage[]) => + (unfilteredImages: WorkspaceImageConfigValue[]) => unfilteredImages.filter((image) => - Object.keys(image.labels).reduce((accumulator, labelKey) => { - const labelValue = image.labels[labelKey]; - if (selectedLabels.has(labelKey)) { - const labelValues: Set | undefined = selectedLabels.get(labelKey); - return accumulator && labelValues !== undefined && labelValues.has(labelValue); + image.labels.reduce((accumulator, label) => { + if (selectedLabels.has(label.key)) { + const labelValues: Set | undefined = selectedLabels.get(label.key); + return accumulator && labelValues !== undefined && labelValues.has(label.value); } return accumulator; }, true), diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageSelection.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageSelection.tsx index cdfb1e953..dab896297 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageSelection.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageSelection.tsx @@ -1,15 +1,15 @@ import * as React from 'react'; import { Content, Divider, Split, SplitItem } from '@patternfly/react-core'; import { useMemo, useState } from 'react'; -import { WorkspaceImage } from '~/shared/types'; import { WorkspaceCreationImageDetails } from '~/app/pages/Workspaces/Creation/image/WorkspaceCreationImageDetails'; import { WorkspaceCreationImageList } from '~/app/pages/Workspaces/Creation/image/WorkspaceCreationImageList'; import { FilterByLabels } from '~/app/pages/Workspaces/Creation/labelFilter/FilterByLabels'; +import { WorkspaceImageConfigValue } from '~/shared/types'; interface WorkspaceCreationImageSelectionProps { - images: WorkspaceImage[]; - selectedImage: WorkspaceImage | undefined; - onSelect: (image: WorkspaceImage | undefined) => void; + images: WorkspaceImageConfigValue[]; + selectedImage: WorkspaceImageConfigValue | undefined; + onSelect: (image: WorkspaceImageConfigValue | undefined) => void; } const WorkspaceCreationImageSelection: React.FunctionComponent< @@ -20,7 +20,7 @@ const WorkspaceCreationImageSelection: React.FunctionComponent< const imageFilterContent = useMemo( () => ( image.labels)} + labelledObjects={images.flatMap((image) => image.labels)} selectedLabels={selectedLabels} onSelect={setSelectedLabels} /> diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/labelFilter/FilterByLabels.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/labelFilter/FilterByLabels.tsx index 14051ac77..40de77be1 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/labelFilter/FilterByLabels.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/labelFilter/FilterByLabels.tsx @@ -5,9 +5,10 @@ import { FilterSidePanelCategoryItem, } from '@patternfly/react-catalog-view-extension'; import '@patternfly/react-catalog-view-extension/dist/css/react-catalog-view-extension.css'; +import { WorkspaceOptionLabel } from '~/shared/types'; type FilterByLabelsProps = { - labelledObjects: any[]; // eslint-disable-line @typescript-eslint/no-explicit-any + labelledObjects: WorkspaceOptionLabel[]; selectedLabels: Map>; onSelect: (labels: Map>) => void; }; @@ -20,13 +21,10 @@ export const FilterByLabels: React.FunctionComponent = ({ const filterMap = useMemo(() => { const labelsMap = new Map>(); labelledObjects.forEach((labelledObject) => { - Object.keys(labelledObject).forEach((labelKey) => { - const labelValue = labelledObject[labelKey]; - if (!labelsMap.has(labelKey)) { - labelsMap.set(labelKey, new Set()); - } - labelsMap.get(labelKey)?.add(labelValue); - }); + if (!labelsMap.has(labelledObject.key)) { + labelsMap.set(labelledObject.key, new Set()); + } + labelsMap.get(labelledObject.key)?.add(labelledObject.value); }); return labelsMap; }, [labelledObjects]); diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigDetails.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigDetails.tsx index 0d45ca92d..fab250d09 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigDetails.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigDetails.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { List, ListItem, Title } from '@patternfly/react-core'; -import { WorkspacePodConfig } from '~/shared/types'; +import { WorkspacePodConfigValue } from '~/shared/types'; type WorkspaceCreationPodConfigDetailsProps = { - workspacePodConfig?: WorkspacePodConfig; + workspacePodConfig?: WorkspacePodConfigValue; }; export const WorkspaceCreationPodConfigDetails: React.FunctionComponent< @@ -18,9 +18,9 @@ export const WorkspaceCreationPodConfigDetails: React.FunctionComponent< {workspacePodConfig.displayName}

{workspacePodConfig.description}

- {Object.keys(workspacePodConfig.labels).map((labelKey) => ( - - {labelKey}={workspacePodConfig.labels[labelKey]} + {workspacePodConfig.labels.map((label) => ( + + {label.key}={label.value} ))} diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigList.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigList.tsx index cb383f3ed..2c811677d 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigList.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigList.tsx @@ -12,20 +12,21 @@ import { CardBody, } from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons/dist/esm/icons/search-icon'; -import { WorkspacePodConfig } from '~/shared/types'; +import { WorkspacePodConfigValue } from '~/shared/types'; import Filter, { FilteredColumn } from '~/shared/components/Filter'; type WorkspaceCreationPodConfigListProps = { - podConfigs: WorkspacePodConfig[]; + podConfigs: WorkspacePodConfigValue[]; selectedLabels: Map>; - selectedPodConfig: WorkspacePodConfig | undefined; - onSelect: (workspacePodConfig: WorkspacePodConfig | undefined) => void; + selectedPodConfig: WorkspacePodConfigValue | undefined; + onSelect: (workspacePodConfig: WorkspacePodConfigValue | undefined) => void; }; export const WorkspaceCreationPodConfigList: React.FunctionComponent< WorkspaceCreationPodConfigListProps > = ({ podConfigs, selectedLabels, selectedPodConfig, onSelect }) => { - const [workspacePodConfigs, setWorkspacePodConfigs] = useState(podConfigs); + const [workspacePodConfigs, setWorkspacePodConfigs] = + useState(podConfigs); const [filters, setFilters] = useState([]); const filterableColumns = useMemo( @@ -36,13 +37,12 @@ export const WorkspaceCreationPodConfigList: React.FunctionComponent< ); const getFilteredWorkspacePodConfigsByLabels = useCallback( - (unfilteredPodConfigs: WorkspacePodConfig[]) => + (unfilteredPodConfigs: WorkspacePodConfigValue[]) => unfilteredPodConfigs.filter((podConfig) => - Object.keys(podConfig.labels).reduce((accumulator, labelKey) => { - const labelValue = podConfig.labels[labelKey]; - if (selectedLabels.has(labelKey)) { - const labelValues: Set | undefined = selectedLabels.get(labelKey); - return accumulator && labelValues !== undefined && labelValues.has(labelValue); + podConfig.labels.reduce((accumulator, label) => { + if (selectedLabels.has(label.key)) { + const labelValues: Set | undefined = selectedLabels.get(label.key); + return accumulator && labelValues !== undefined && labelValues.has(label.value); } return accumulator; }, true), diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigSelection.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigSelection.tsx index 6ac201a5e..e525f042d 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigSelection.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigSelection.tsx @@ -1,15 +1,15 @@ import * as React from 'react'; import { Content, Divider, Split, SplitItem } from '@patternfly/react-core'; import { useMemo, useState } from 'react'; -import { WorkspacePodConfig } from '~/shared/types'; +import { WorkspacePodConfigValue } from '~/shared/types'; import { WorkspaceCreationPodConfigDetails } from '~/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigDetails'; import { WorkspaceCreationPodConfigList } from '~/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigList'; import { FilterByLabels } from '~/app/pages/Workspaces/Creation/labelFilter/FilterByLabels'; interface WorkspaceCreationPodConfigSelectionProps { - podConfigs: WorkspacePodConfig[]; - selectedPodConfig: WorkspacePodConfig | undefined; - onSelect: (podConfig: WorkspacePodConfig | undefined) => void; + podConfigs: WorkspacePodConfigValue[]; + selectedPodConfig: WorkspacePodConfigValue | undefined; + onSelect: (podConfig: WorkspacePodConfigValue | undefined) => void; } const WorkspaceCreationPodConfigSelection: React.FunctionComponent< @@ -20,7 +20,7 @@ const WorkspaceCreationPodConfigSelection: React.FunctionComponent< const podConfigFilterContent = useMemo( () => ( podConfig.labels)} + labelledObjects={podConfigs.flatMap((podConfig) => podConfig.labels)} selectedLabels={selectedLabels} onSelect={setSelectedLabels} /> diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection.tsx index 963ba9c39..55f7a4ca2 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection.tsx @@ -1,23 +1,28 @@ -import * as React from 'react'; -import { useEffect, useMemo, useState } from 'react'; import { - TextInput, Checkbox, + Content, + Divider, + ExpandableSection, Form, FormGroup, - ExpandableSection, - Divider, Split, SplitItem, - Content, + TextInput, } from '@patternfly/react-core'; +import * as React from 'react'; +import { useEffect, useMemo, useState } from 'react'; +import { + WorkspaceImageConfigValue, + WorkspacePodVolumeInfo, + WorkspacePodVolumesMutate, + WorkspaceSecret, +} from '~/shared/types'; import { WorkspaceCreationImageDetails } from '~/app/pages/Workspaces/Creation/image/WorkspaceCreationImageDetails'; import { WorkspaceCreationPropertiesVolumes } from '~/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesVolumes'; -import { WorkspaceImage, WorkspaceVolumes, WorkspaceVolume, WorkspaceSecret } from '~/shared/types'; import { WorkspaceCreationPropertiesSecrets } from './WorkspaceCreationPropertiesSecrets'; interface WorkspaceCreationPropertiesSelectionProps { - selectedImage: WorkspaceImage | undefined; + selectedImage: WorkspaceImageConfigValue | undefined; } const WorkspaceCreationPropertiesSelection: React.FunctionComponent< @@ -26,8 +31,12 @@ const WorkspaceCreationPropertiesSelection: React.FunctionComponent< const [workspaceName, setWorkspaceName] = useState(''); const [deferUpdates, setDeferUpdates] = useState(false); const [homeDirectory, setHomeDirectory] = useState(''); - const [volumes, setVolumes] = useState({ home: '', data: [], secrets: [] }); - const [volumesData, setVolumesData] = useState([]); + const [volumes, setVolumes] = useState({ + home: '', + data: [], + secrets: [], + }); + const [volumesData, setVolumesData] = useState([]); const [secrets, setSecrets] = useState( volumes.secrets.length ? volumes.secrets : [], ); diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesVolumes.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesVolumes.tsx index b047ad020..ea79d87b3 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesVolumes.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesVolumes.tsx @@ -16,11 +16,11 @@ import { FormGroup, ModalHeader, } from '@patternfly/react-core'; -import { WorkspaceVolume } from '~/shared/types'; +import { WorkspacePodVolumeInfo } from '~/shared/types'; interface WorkspaceCreationPropertiesVolumesProps { - volumes: WorkspaceVolume[]; - setVolumes: React.Dispatch>; + volumes: WorkspacePodVolumeInfo[]; + setVolumes: React.Dispatch>; } export const WorkspaceCreationPropertiesVolumes: React.FC< @@ -28,7 +28,7 @@ export const WorkspaceCreationPropertiesVolumes: React.FC< > = ({ volumes, setVolumes }) => { const [isModalOpen, setIsModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [formData, setFormData] = useState({ + const [formData, setFormData] = useState({ pvcName: '', mountPath: '', readOnly: false, diff --git a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsActivity.tsx b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsActivity.tsx index 5832d2a3e..59254bae6 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsActivity.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsActivity.tsx @@ -16,7 +16,7 @@ type WorkspaceDetailsActivityProps = { export const WorkspaceDetailsActivity: React.FunctionComponent = ({ workspace, }) => { - const { activity, pauseTime, pendingRestart } = workspace.status; + const { activity, pausedTime, pendingRestart } = workspace; return ( @@ -37,7 +37,7 @@ export const WorkspaceDetailsActivity: React.FunctionComponent Pause Time - {formatTimestamp(pauseTime)} + {formatTimestamp(pausedTime)} diff --git a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsOverview.tsx b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsOverview.tsx index 7456b9813..2057f0ce9 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsOverview.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsOverview.tsx @@ -23,19 +23,23 @@ export const WorkspaceDetailsOverview: React.FunctionComponent Kind - {workspace.kind} + {workspace.workspaceKind.name} Labels - {workspace.podTemplate.podMetadata.labels.join(', ')} + {Object.entries(workspace.podTemplate.podMetadata.labels) + .map(([key, value]) => `${key}=${value}`) + .join(', ')} Pod config - {workspace.options.podConfig} + + {workspace.podTemplate.options.podConfig.current.displayName} + diff --git a/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConnectAction.tsx b/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConnectAction.tsx index 8528a1309..8022c41b3 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConnectAction.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConnectAction.tsx @@ -33,11 +33,15 @@ export const WorkspaceConnectAction: React.FunctionComponent { - openEndpoint(workspace.podTemplate.endpoints[0].port); + if (workspace.services.length === 0 || !workspace.services[0].httpService) { + return; + } + + openEndpoint(workspace.services[0].httpService.httpPath); }; - const openEndpoint = (port: string) => { - window.open(`workspace/${workspace.namespace}/${workspace.name}/${port}`, '_blank'); + const openEndpoint = (value: string) => { + window.open(value, '_blank'); }; return ( @@ -50,7 +54,8 @@ export const WorkspaceConnectAction: React.FunctionComponent - {workspace.podTemplate.endpoints.map((endpoint) => ( - - {endpoint.displayName} - - ))} + {workspace.services.map((service) => { + if (!service.httpService) { + return null; + } + return ( + + {service.httpService.displayName} + + ); + })} ); diff --git a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx index a9d06c891..6672ed02c 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx @@ -50,7 +50,7 @@ import { WorkspaceRestartActionModal } from '~/app/pages/Workspaces/workspaceAct import { WorkspaceStopActionModal } from '~/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal'; import { useNamespaceContext } from '~/app/context/NamespaceContextProvider'; import Filter, { FilteredColumn } from 'shared/components/Filter'; -import { formatRam } from 'shared/utilities/WorkspaceUtils'; +import { extractCpuValue, extractMemoryValue } from 'shared/utilities/WorkspaceUtils'; export enum ActionType { ViewDetails, @@ -68,14 +68,8 @@ export const Workspaces: React.FunctionComponent = () => { }, [navigate]); const [workspaceKinds] = useWorkspaceKinds(); - let kindLogoDict: Record = {}; - kindLogoDict = buildKindLogoDictionary(workspaceKinds); - - let workspaceRedirectStatus: Record< - string, - { to: string; message: string; level: string } | null - > = {}; // Initialize the redirect status dictionary - workspaceRedirectStatus = buildWorkspaceRedirectStatus(workspaceKinds); // Populate the dictionary + const kindLogoDict = buildKindLogoDictionary(workspaceKinds); + const workspaceRedirectStatus = buildWorkspaceRedirectStatus(workspaceKinds); // Table columns const columnNames: WorkspacesColumnNames = { @@ -159,15 +153,26 @@ export const Workspaces: React.FunctionComponent = () => { case columnNames.name: return workspace.name.search(searchValueInput) >= 0; case columnNames.kind: - return workspace.kind.search(searchValueInput) >= 0; + return workspace.workspaceKind.name.search(searchValueInput) >= 0; case columnNames.image: - return workspace.options.imageConfig.search(searchValueInput) >= 0; + return ( + workspace.podTemplate.options.imageConfig.current.displayName.search( + searchValueInput, + ) >= 0 + ); case columnNames.podConfig: - return workspace.options.podConfig.search(searchValueInput) >= 0; + return ( + workspace.podTemplate.options.podConfig.current.displayName.search( + searchValueInput, + ) >= 0 + ); case columnNames.state: - return WorkspaceState[workspace.status.state].search(searchValueInput) >= 0; + return workspace.state.search(searchValueInput) >= 0; case columnNames.homeVol: - return workspace.podTemplate.volumes.home.search(searchValueInput) >= 0; + if (!workspace.podTemplate.volumes.home) { + return false; + } + return workspace.podTemplate.volumes.home.mountPath.search(searchValueInput) >= 0; default: return true; } @@ -186,14 +191,14 @@ export const Workspaces: React.FunctionComponent = () => { { redirectStatus: '', name: workspace.name, - kind: workspace.kind, - image: workspace.options.imageConfig, - podConfig: workspace.options.podConfig, - state: WorkspaceState[workspace.status.state], - homeVol: workspace.podTemplate.volumes.home, - cpu: workspace.cpu, - ram: workspace.ram, - lastActivity: workspace.status.activity.lastActivity, + kind: workspace.workspaceKind.name, + image: workspace.podTemplate.options.imageConfig.current.displayName, + podConfig: workspace.podTemplate.options.podConfig.current.displayName, + state: workspace.state, + homeVol: workspace.podTemplate.volumes.home?.pvcName ?? '', + cpu: extractCpuValue(workspace), + ram: extractMemoryValue(workspace), + lastActivity: workspace.activity.lastActivity, }; return [redirectStatus, name, kind, image, podConfig, state, homeVol, cpu, ram, lastActivity]; }; @@ -273,7 +278,6 @@ export const Workspaces: React.FunctionComponent = () => { }; const workspaceDefaultActions = (workspace: Workspace): IActions => { - const workspaceState = workspace.status.state; const workspaceActions = [ { id: 'view-details', @@ -293,7 +297,7 @@ export const Workspaces: React.FunctionComponent = () => { { isSeparator: true, }, - workspaceState !== WorkspaceState.Running + workspace.state !== WorkspaceState.WorkspaceStateRunning ? { id: 'start', title: 'Start', @@ -306,7 +310,7 @@ export const Workspaces: React.FunctionComponent = () => { }, ] as IActions; - if (workspaceState === WorkspaceState.Running) { + if (workspace.state === WorkspaceState.WorkspaceStateRunning) { workspaceActions.push({ id: 'stop', title: 'Stop', @@ -346,19 +350,23 @@ export const Workspaces: React.FunctionComponent = () => { return undefined; }; - // States - - const stateColors: ( - | 'blue' - | 'teal' - | 'green' - | 'orange' - | 'purple' - | 'red' - | 'orangered' - | 'grey' - | 'yellow' - )[] = ['green', 'orange', 'yellow', 'blue', 'red', 'purple']; + const extractStateColor = (state: WorkspaceState) => { + switch (state) { + case WorkspaceState.WorkspaceStateRunning: + return 'green'; + case WorkspaceState.WorkspaceStatePending: + return 'orange'; + case WorkspaceState.WorkspaceStateTerminating: + return 'yellow'; + case WorkspaceState.WorkspaceStateError: + return 'red'; + case WorkspaceState.WorkspaceStatePaused: + return 'purple'; + case WorkspaceState.WorkspaceStateUnknown: + default: + return 'grey'; + } + }; // Redirect Status Icons @@ -482,43 +490,47 @@ export const Workspaces: React.FunctionComponent = () => { }} /> - {workspaceRedirectStatus[workspace.kind] + {workspaceRedirectStatus[workspace.workspaceKind.name] ? getRedirectStatusIcon( - workspaceRedirectStatus[workspace.kind]?.level, - workspaceRedirectStatus[workspace.kind]?.message || + workspaceRedirectStatus[workspace.workspaceKind.name]?.message?.level, + workspaceRedirectStatus[workspace.workspaceKind.name]?.message?.text || 'No API response available', ) : getRedirectStatusIcon(undefined, 'No API response available')} {workspace.name} - {kindLogoDict[workspace.kind] ? ( - + {kindLogoDict[workspace.workspaceKind.name] ? ( + ) : ( - + )} - {workspace.options.imageConfig} - {workspace.options.podConfig} + + {workspace.podTemplate.options.imageConfig.current.displayName} + + + {workspace.podTemplate.options.podConfig.current.displayName} + - + + + + {workspace.podTemplate.volumes.home?.pvcName ?? ''} - {workspace.podTemplate.volumes.home} - {`${workspace.cpu}%`} - {formatRam(workspace.ram)} + {extractCpuValue(workspace)} + {extractMemoryValue(workspace)} 1 hour ago diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView.tsx b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView.tsx index d4c0eb451..3827fdc10 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView.tsx @@ -62,14 +62,14 @@ export const WorkspaceRedirectInformationView: React.FC ({ src: value.id, dest: value.redirect?.to, - message: value.redirect?.message.text, - level: value.redirect?.message.level, + message: value.redirect?.message?.text, + level: value.redirect?.message?.level, })); const podConfigRedirects = podConfig?.values.map((value) => ({ src: value.id, dest: value.redirect?.to, - message: value.redirect?.message.text, - level: value.redirect?.message.level, + message: value.redirect?.message?.text, + level: value.redirect?.message?.level, })); const getMaxLevel = (redirects: NonNullable) => { diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal.tsx b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal.tsx index e092ce3ec..d5edf030a 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal.tsx @@ -22,7 +22,7 @@ export const WorkspaceRestartActionModal: React.FC = ({ isOpen, workspace, }) => { - const workspacePendingUpdate = workspace?.status.pendingRestart; + const workspacePendingUpdate = workspace?.pendingRestart; const handleClick = (isUpdate = false) => { if (isUpdate) { console.log(`Update ${workspace?.name}`); @@ -46,7 +46,7 @@ export const WorkspaceRestartActionModal: React.FC = ({ There are pending redirect updates for that workspace. Are you sure you want to proceed? - + ) : ( Are you sure you want to restart the workspace? diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx index 8563a5a1e..54215d42f 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx @@ -41,7 +41,7 @@ export const WorkspaceStartActionModal: React.FC = ({ There are pending redirect updates for that workspace. Are you sure you want to proceed? - {workspace && } + {workspace && } diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx index 56d88cd77..4b2fb9a22 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx @@ -22,7 +22,7 @@ export const WorkspaceStopActionModal: React.FC = ({ isOpen, workspace, }) => { - const workspacePendingUpdate = workspace?.status.pendingRestart; + const workspacePendingUpdate = workspace?.pendingRestart; const handleClick = (isUpdate = false) => { if (isUpdate) { console.log(`Update ${workspace?.name}`); @@ -46,7 +46,7 @@ export const WorkspaceStopActionModal: React.FC = ({ There are pending redirect updates for that workspace. Are you sure you want to proceed? - + ) : ( Are you sure you want to stop the workspace? diff --git a/workspaces/frontend/src/app/types.ts b/workspaces/frontend/src/app/types.ts index 89f364f17..d7d4327e5 100644 --- a/workspaces/frontend/src/app/types.ts +++ b/workspaces/frontend/src/app/types.ts @@ -12,57 +12,6 @@ export type ResponseBody = { metadata?: Record; }; -export enum ResponseMetadataType { - INT = 'MetadataIntValue', - DOUBLE = 'MetadataDoubleValue', - STRING = 'MetadataStringValue', - STRUCT = 'MetadataStructValue', - PROTO = 'MetadataProtoValue', - BOOL = 'MetadataBoolValue', -} - -export type ResponseCustomPropertyInt = { - metadataType: ResponseMetadataType.INT; - int_value: string; // int64-formatted string -}; - -export type ResponseCustomPropertyDouble = { - metadataType: ResponseMetadataType.DOUBLE; - double_value: number; -}; - -export type ResponseCustomPropertyString = { - metadataType: ResponseMetadataType.STRING; - string_value: string; -}; - -export type ResponseCustomPropertyStruct = { - metadataType: ResponseMetadataType.STRUCT; - struct_value: string; // Base64 encoded bytes for struct value -}; - -export type ResponseCustomPropertyProto = { - metadataType: ResponseMetadataType.PROTO; - type: string; // url describing proto value - proto_value: string; // Base64 encoded bytes for proto value -}; - -export type ResponseCustomPropertyBool = { - metadataType: ResponseMetadataType.BOOL; - bool_value: boolean; -}; - -export type ResponseCustomProperty = - | ResponseCustomPropertyInt - | ResponseCustomPropertyDouble - | ResponseCustomPropertyString - | ResponseCustomPropertyStruct - | ResponseCustomPropertyProto - | ResponseCustomPropertyBool; - -export type ResponseCustomProperties = Record; -export type ResponseStringCustomProperties = Record; - // Health export type GetHealthCheck = (opts: APIOptions) => Promise; diff --git a/workspaces/frontend/src/shared/mock/mockBuilder.ts b/workspaces/frontend/src/shared/mock/mockBuilder.ts index 31a6da148..324a6c058 100644 --- a/workspaces/frontend/src/shared/mock/mockBuilder.ts +++ b/workspaces/frontend/src/shared/mock/mockBuilder.ts @@ -3,13 +3,16 @@ import { Namespace, Workspace, WorkspaceKind, + WorkspaceKindInfo, + WorkspaceRedirectMessageLevel, + WorkspaceServiceStatus, WorkspaceState, } from '~/shared/types'; export const buildMockHealthCheckResponse = ( healthCheckResponse?: Partial, ): HealthCheckResponse => ({ - status: 'Healthy', + status: WorkspaceServiceStatus.ServiceStatusHealthy, systemInfo: { version: '1.0.0' }, ...healthCheckResponse, }); @@ -19,76 +22,115 @@ export const buildMockNamespace = (namespace?: Partial): Namespace => ...namespace, }); +export const buildMockWorkspaceKindInfo = ( + workspaceKindInfo?: Partial, +): WorkspaceKindInfo => ({ + name: 'jupyterlab', + missing: 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', + }, + ...workspaceKindInfo, +}); + export const buildMockWorkspace = (workspace?: Partial): Workspace => ({ - name: 'My Jupyter Notebook', + name: 'My First Jupyter Notebook', namespace: 'default', + workspaceKind: buildMockWorkspaceKindInfo(), paused: true, deferUpdates: true, - kind: 'jupyterlab', - cpu: 3, - ram: 500, + pausedTime: 1739673500, + state: WorkspaceState.WorkspaceStateRunning, + stateMessage: 'Workspace is running', podTemplate: { podMetadata: { - labels: ['label1', 'label2'], - annotations: ['annotation1', 'annotation2'], + labels: { labelKey1: 'labelValue1', labelKey2: 'labelValue2' }, + annotations: { annotationKey1: 'annotationValue1', annotationKey2: 'annotationValue2' }, }, volumes: { - home: '/home', + home: { + pvcName: 'Volume-Home', + mountPath: '/home', + readOnly: false, + }, data: [ { - pvcName: 'Volume-1', + pvcName: 'Volume-Data1', mountPath: '/data', readOnly: true, }, { - pvcName: 'Volume-2', + pvcName: 'Volume-Data2', mountPath: '/data', readOnly: false, }, ], }, - endpoints: [ - { - displayName: 'JupyterLab', - port: '7777', + options: { + imageConfig: { + current: { + id: 'jupyterlab_scipy_190', + displayName: 'jupyter-scipy:v1.9.0', + description: 'JupyterLab, with SciPy Packages', + labels: [ + { + key: 'pythonVersion', + value: '3.11', + }, + ], + }, }, - ], + podConfig: { + current: { + id: 'tiny_cpu', + displayName: 'Tiny CPU', + description: 'Pod with 0.1 CPU, 128 Mb RAM', + labels: [ + { + key: 'cpu', + value: '100m', + }, + { + key: 'memory', + value: '128Mi', + }, + ], + }, + }, + }, }, - options: { - imageConfig: 'jupyterlab_scipy_180', - podConfig: 'Small CPU', + activity: { + lastActivity: 1746551485113, + lastUpdate: 1746551485113, }, - status: { - activity: { - lastActivity: 1739673600, - lastUpdate: 1739673700, + pendingRestart: false, + services: [ + { + httpService: { + displayName: 'JupyterLab', + httpPath: 'https://jupyterlab.example.com', + }, }, - pauseTime: 1739673500, - pendingRestart: false, - podTemplateOptions: { - imageConfig: { - desired: '', - redirectChain: [], + { + httpService: { + displayName: 'Spark Master', + httpPath: 'https://spark-master.example.com', }, }, - state: WorkspaceState.Paused, - stateMessage: 'It is paused.', - }, - redirectStatus: { - level: 'Info', - text: 'This is informational', - }, + ], ...workspace, }); export const buildMockWorkspaceKind = (workspaceKind?: Partial): WorkspaceKind => ({ name: 'jupyterlab', displayName: 'JupyterLab Notebook', - description: - 'Example of a description for JupyterLab a Workspace which runs JupyterLab in a Pod.', - deprecated: true, + description: 'A Workspace which runs JupyterLab in a Pod', + deprecated: false, deprecationMessage: - 'This WorkspaceKind was removed on 20XX-XX-XX, please use another WorkspaceKind.', + 'This WorkspaceKind will be removed on 20XX-XX-XX, please use another WorkspaceKind.', hidden: false, icon: { url: 'https://jupyter.org/assets/favicons/apple-touch-icon-152x152.png', @@ -115,52 +157,68 @@ export const buildMockWorkspaceKind = (workspaceKind?: Partial): { id: 'jupyterlab_scipy_180', displayName: 'jupyter-scipy:v1.8.0', - labels: { pythonVersion: '3.11', jupyterlabVersion: '1.8.0' }, + description: 'JupyterLab, with SciPy Packages', + labels: [ + { key: 'pythonVersion', value: '3.11' }, + { key: 'jupyterlabVersion', value: '1.8.0' }, + ], hidden: true, redirect: { to: 'jupyterlab_scipy_190', message: { text: 'This update will change...', - level: 'Info', + level: WorkspaceRedirectMessageLevel.RedirectMessageLevelInfo, }, }, }, { id: 'jupyterlab_scipy_190', displayName: 'jupyter-scipy:v1.9.0', - labels: { pythonVersion: '3.12', jupyterlabVersion: '1.9.0' }, + description: 'JupyterLab, with SciPy Packages', + labels: [ + { key: 'pythonVersion', value: '3.12' }, + { key: 'jupyterlabVersion', value: '1.9.0' }, + ], hidden: true, redirect: { to: 'jupyterlab_scipy_200', message: { text: 'This update will change...', - level: 'Warning', + level: WorkspaceRedirectMessageLevel.RedirectMessageLevelWarning, }, }, }, { id: 'jupyterlab_scipy_200', displayName: 'jupyter-scipy:v2.0.0', - labels: { pythonVersion: '3.12', jupyterlabVersion: '2.0.0' }, + description: 'JupyterLab, with SciPy Packages', + labels: [ + { key: 'pythonVersion', value: '3.12' }, + { key: 'jupyterlabVersion', value: '2.0.0' }, + ], hidden: true, redirect: { to: 'jupyterlab_scipy_210', message: { text: 'This update will change...', - level: 'Warning', + level: WorkspaceRedirectMessageLevel.RedirectMessageLevelWarning, }, }, }, { id: 'jupyterlab_scipy_210', displayName: 'jupyter-scipy:v2.1.0', - labels: { pythonVersion: '3.13', jupyterlabVersion: '2.1.0' }, + description: 'JupyterLab, with SciPy Packages', + labels: [ + { key: 'pythonVersion', value: '3.13' }, + { key: 'jupyterlabVersion', value: '2.1.0' }, + ], hidden: true, redirect: { to: 'jupyterlab_scipy_220', message: { text: 'This update will change...', - level: 'Warning', + level: WorkspaceRedirectMessageLevel.RedirectMessageLevelWarning, }, }, }, @@ -173,12 +231,16 @@ export const buildMockWorkspaceKind = (workspaceKind?: Partial): id: 'tiny_cpu', displayName: 'Tiny CPU', description: 'Pod with 0.1 CPU, 128 Mb RAM', - labels: { cpu: '100m', memory: '128Mi' }, + hidden: false, + labels: [ + { key: 'cpu', value: '100m' }, + { key: 'memory', value: '128Mi' }, + ], redirect: { to: 'small_cpu', message: { text: 'This update will change...', - level: 'Danger', + level: WorkspaceRedirectMessageLevel.RedirectMessageLevelDanger, }, }, }, @@ -186,7 +248,19 @@ export const buildMockWorkspaceKind = (workspaceKind?: Partial): id: 'large_cpu', displayName: 'Large CPU', description: 'Pod with 1 CPU, 1 Gb RAM', - labels: { cpu: '1000m', memory: '1Gi' }, + hidden: false, + labels: [ + { key: 'cpu', value: '1000m' }, + { key: 'memory', value: '1Gi' }, + { key: 'gpu', value: '1' }, + ], + redirect: { + to: 'large_cpu', + message: { + text: 'This update will change...', + level: WorkspaceRedirectMessageLevel.RedirectMessageLevelDanger, + }, + }, }, ], }, diff --git a/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts b/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts index 4ceab09b2..029935538 100644 --- a/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts +++ b/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts @@ -3,8 +3,9 @@ import { buildMockNamespace, buildMockWorkspace, buildMockWorkspaceKind, + buildMockWorkspaceKindInfo, } from '~/shared/mock/mockBuilder'; -import { Workspace, WorkspaceKind, WorkspaceState } from '~/shared/types'; +import { Workspace, WorkspaceKind, WorkspaceKindInfo, WorkspaceState } from '~/shared/types'; // Health export const mockedHealthCheckResponse = buildMockHealthCheckResponse(); @@ -34,27 +35,38 @@ export const mockWorkspaceKind3: WorkspaceKind = buildMockWorkspaceKind({ export const mockWorkspaceKinds = [mockWorkspaceKind1, mockWorkspaceKind2, mockWorkspaceKind3]; +export const mockWorkspaceKindInfo1: WorkspaceKindInfo = buildMockWorkspaceKindInfo({ + name: mockWorkspaceKind1.name, +}); + +export const mockWorkspaceKindInfo2: WorkspaceKindInfo = buildMockWorkspaceKindInfo({ + name: mockWorkspaceKind2.name, +}); + // Workspace export const mockWorkspace1: Workspace = buildMockWorkspace({ - kind: mockWorkspaceKind1.name, + workspaceKind: mockWorkspaceKindInfo1, namespace: mockNamespace1.name, }); export const mockWorkspace2: Workspace = buildMockWorkspace({ - name: 'My Other Jupyter Notebook', - kind: mockWorkspaceKind1.name, + name: 'My Second Jupyter Notebook', + workspaceKind: mockWorkspaceKindInfo1, namespace: mockNamespace1.name, + state: WorkspaceState.WorkspaceStatePaused, paused: false, deferUpdates: false, - cpu: 1, - ram: 12540, podTemplate: { podMetadata: { - labels: ['label1', 'label2'], - annotations: ['annotation1', 'annotation2'], + labels: { labelKey1: 'labelValue1', labelKey2: 'labelValue2' }, + annotations: { annotationKey1: 'annotationValue1', annotationKey2: 'annotationValue2' }, }, volumes: { - home: '/home', + home: { + pvcName: 'Volume-Home', + mountPath: '/home', + readOnly: false, + }, data: [ { pvcName: 'PVC-1', @@ -63,53 +75,67 @@ export const mockWorkspace2: Workspace = buildMockWorkspace({ }, ], }, - endpoints: [ - { - displayName: 'JupyterLab', - port: '8888', - }, - { - displayName: 'Spark Master', - port: '9999', - }, - ], - }, - options: { - imageConfig: 'jupyterlab_scipy_180', - podConfig: 'Large CPU', - }, - status: { - activity: { - lastActivity: 0, - lastUpdate: 0, - }, - pauseTime: 1739673500, - pendingRestart: false, - podTemplateOptions: { + options: { imageConfig: { - desired: '', - redirectChain: [], + current: { + id: 'jupyterlab_scipy_190', + 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 4 CPU, 16 Gb RAM', + labels: [ + { + key: 'cpu', + value: '4000m', + }, + { + key: 'memory', + value: '16Gi', + }, + ], + }, }, }, - state: WorkspaceState.Running, - stateMessage: 'It is running.', - }, - redirectStatus: { - level: 'Danger', - text: 'This is dangerous', }, }); -export const mockWorkspace3 = buildMockWorkspace({ +export const mockWorkspace3: Workspace = buildMockWorkspace({ name: 'My Third Jupyter Notebook', - namespace: mockNamespace2.name, - kind: mockWorkspaceKind2.name, + namespace: mockNamespace1.name, + workspaceKind: mockWorkspaceKindInfo1, + state: WorkspaceState.WorkspaceStateRunning, + pendingRestart: true, }); export const mockWorkspace4 = buildMockWorkspace({ name: 'My Fourth Jupyter Notebook', namespace: mockNamespace2.name, - kind: mockWorkspaceKind2.name, + state: WorkspaceState.WorkspaceStateError, + workspaceKind: mockWorkspaceKindInfo2, +}); + +export const mockWorkspace5 = buildMockWorkspace({ + name: 'My Fifth Jupyter Notebook', + namespace: mockNamespace2.name, + state: WorkspaceState.WorkspaceStateTerminating, + workspaceKind: mockWorkspaceKindInfo2, }); -export const mockAllWorkspaces = [mockWorkspace1, mockWorkspace2, mockWorkspace3, mockWorkspace4]; +export const mockAllWorkspaces = [ + mockWorkspace1, + mockWorkspace2, + mockWorkspace3, + mockWorkspace4, + mockWorkspace5, +]; diff --git a/workspaces/frontend/src/shared/types.ts b/workspaces/frontend/src/shared/types.ts index e56b546a2..6b9847f9b 100644 --- a/workspaces/frontend/src/shared/types.ts +++ b/workspaces/frontend/src/shared/types.ts @@ -1,70 +1,92 @@ -export interface WorkspaceIcon { - url: string; +export enum WorkspaceServiceStatus { + ServiceStatusHealthy = 'Healthy', + ServiceStatusUnhealthy = 'Unhealthy', +} + +export interface WorkspaceSystemInfo { + version: string; } -export interface WorkspaceLogo { +export type HealthCheckResponse = { + status: WorkspaceServiceStatus; + systemInfo: WorkspaceSystemInfo; +}; + +export type Namespace = { + name: string; +}; + +export interface WorkspaceImageRef { url: string; } -export interface WorkspaceImage { +export interface WorkspacePodConfigValue { id: string; displayName: string; - labels: any; // eslint-disable-line @typescript-eslint/no-explicit-any + description: string; + labels: WorkspaceOptionLabel[]; hidden: boolean; - redirect?: { - to: string; - message: { - text: string; - level: string; - }; - }; + redirect?: WorkspaceOptionRedirect; } -export interface WorkspaceVolume { - pvcName: string; - mountPath: string; - readOnly: boolean; +export interface WorkspaceKindPodConfig { + default: string; + values: WorkspacePodConfigValue[]; +} + +export interface WorkspaceKindPodMetadata { + labels: Record; + annotations: Record; } -export interface WorkspaceVolumes { +export interface WorkspacePodVolumeMounts { home: string; - data: WorkspaceVolume[]; - secrets: WorkspaceSecret[]; } -export interface WorkspaceSecret { - defaultMode: number; - secretName: string; - mountPath: string; +export interface WorkspaceOptionLabel { + key: string; + value: string; } -export interface WorkspaceProperties { - workspaceName: string; - deferUpdates: boolean; - homeDirectory: string; - volumes: boolean; - isVolumesExpanded: boolean; - redirect?: { - to: string; - message: { - text: string; - level: string; - }; - }; +export enum WorkspaceRedirectMessageLevel { + RedirectMessageLevelInfo = 'Info', + RedirectMessageLevelWarning = 'Warning', + RedirectMessageLevelDanger = 'Danger', } -export interface WorkspacePodConfig { +export interface WorkspaceRedirectMessage { + text: string; + level: WorkspaceRedirectMessageLevel; +} + +export interface WorkspaceOptionRedirect { + to: string; + message?: WorkspaceRedirectMessage; +} + +export interface WorkspaceImageConfigValue { id: string; displayName: string; description: string; - labels: any; // eslint-disable-line @typescript-eslint/no-explicit-any - redirect?: { - to: string; - message: { - text: string; - level: string; - }; - }; + labels: WorkspaceOptionLabel[]; + hidden: boolean; + redirect?: WorkspaceOptionRedirect; +} + +export interface WorkspaceKindImageConfig { + default: string; + values: WorkspaceImageConfigValue[]; +} + +export interface WorkspaceKindPodTemplateOptions { + imageConfig: WorkspaceKindImageConfig; + podConfig: WorkspaceKindPodConfig; +} + +export interface WorkspaceKindPodTemplate { + podMetadata: WorkspaceKindPodMetadata; + volumeMounts: WorkspacePodVolumeMounts; + options: WorkspaceKindPodTemplateOptions; } export interface WorkspaceKind { @@ -74,56 +96,105 @@ export interface WorkspaceKind { deprecated: boolean; deprecationMessage: string; hidden: boolean; - icon: WorkspaceIcon; - logo: WorkspaceLogo; - podTemplate: { - podMetadata: { - labels: any; // eslint-disable-line @typescript-eslint/no-explicit-any - annotations: any; // eslint-disable-line @typescript-eslint/no-explicit-any - }; - volumeMounts: { - home: string; - }; - options: { - imageConfig: { - default: string; - values: WorkspaceImage[]; - }; - podConfig: { - default: string; - values: WorkspacePodConfig[]; - }; - }; - }; + icon: WorkspaceImageRef; + logo: WorkspaceImageRef; + podTemplate: WorkspaceKindPodTemplate; } export enum WorkspaceState { - Running, - Terminating, - Paused, - Pending, - Error, - Unknown, -} - -export interface WorkspaceStatus { - activity: { - lastActivity: number; - lastUpdate: number; - }; - pauseTime: number; - pendingRestart: boolean; - podTemplateOptions: { - imageConfig: { - desired: string; - redirectChain: { - source: string; - target: string; - }[]; - }; - }; - state: WorkspaceState; - stateMessage: string; + WorkspaceStateRunning = 'Running', + WorkspaceStateTerminating = 'Terminating', + WorkspaceStatePaused = 'Paused', + WorkspaceStatePending = 'Pending', + WorkspaceStateError = 'Error', + WorkspaceStateUnknown = 'Unknown', +} + +export interface WorkspaceKindInfo { + name: string; + missing: boolean; + icon: WorkspaceImageRef; + logo: WorkspaceImageRef; +} + +export interface WorkspacePodMetadata { + labels: Record; + annotations: Record; +} + +export interface WorkspacePodVolumeInfo { + pvcName: string; + mountPath: string; + readOnly: boolean; +} + +export interface WorkspaceOptionInfo { + id: string; + displayName: string; + description: string; + labels: WorkspaceOptionLabel[]; +} + +export interface WorkspaceRedirectStep { + sourceId: string; + targetId: string; + message?: WorkspaceRedirectMessage; +} + +export interface WorkspaceImageConfig { + current: WorkspaceOptionInfo; + desired?: WorkspaceOptionInfo; + redirectChain?: WorkspaceRedirectStep[]; +} + +export interface WorkspacePodConfig { + current: WorkspaceOptionInfo; + desired?: WorkspaceOptionInfo; + redirectChain?: WorkspaceRedirectStep[]; +} + +export interface WorkspacePodTemplateOptions { + imageConfig: WorkspaceImageConfig; + podConfig: WorkspacePodConfig; +} + +export interface WorkspacePodVolumes { + home?: WorkspacePodVolumeInfo; + data: WorkspacePodVolumeInfo[]; +} + +export interface WorkspacePodTemplate { + podMetadata: WorkspacePodMetadata; + volumes: WorkspacePodVolumes; + options: WorkspacePodTemplateOptions; +} + +export enum WorkspaceProbeResult { + ProbeResultSuccess = 'Success', + ProbeResultFailure = 'Failure', + ProbeResultTimeout = 'Timeout', +} + +export interface WorkspaceLastProbeInfo { + startTimeMs: number; + endTimeMs: number; + result: WorkspaceProbeResult; + message: string; +} + +export interface WorkspaceActivity { + lastActivity: number; + lastUpdate: number; + lastProbe?: WorkspaceLastProbeInfo; +} + +export interface WorkspaceHttpService { + displayName: string; + httpPath: string; +} + +export interface WorkspaceService { + httpService?: WorkspaceHttpService; } export interface WorkspacePodMetadataMutate { @@ -137,9 +208,16 @@ export interface WorkspacePodVolumeMount { readOnly?: boolean; } +export interface WorkspaceSecret { + defaultMode: number; + secretName: string; + mountPath: string; +} + export interface WorkspacePodVolumesMutate { home?: string; data: WorkspacePodVolumeMount[]; + secrets: WorkspaceSecret[]; } export interface WorkspacePodTemplateOptionsMutate { @@ -156,31 +234,16 @@ export interface WorkspacePodTemplateMutate { export interface Workspace { name: string; namespace: string; - paused: boolean; + workspaceKind: WorkspaceKindInfo; deferUpdates: boolean; - kind: string; - cpu: number; - ram: number; - podTemplate: { - podMetadata: { - labels: string[]; - annotations: string[]; - }; - volumes: WorkspaceVolumes; - endpoints: { - displayName: string; - port: string; - }[]; - }; - options: { - imageConfig: string; - podConfig: string; - }; - status: WorkspaceStatus; - redirectStatus: { - level: 'Info' | 'Warning' | 'Danger'; - text: string; - }; + paused: boolean; + pausedTime: number; + pendingRestart: boolean; + state: WorkspaceState; + stateMessage: string; + podTemplate: WorkspacePodTemplate; + activity: WorkspaceActivity; + services: WorkspaceService[]; } export type WorkspacesColumnNames = { @@ -204,13 +267,17 @@ export type WorkspaceKindsColumnNames = { numberOfWorkspaces: string; }; -export type Namespace = { - name: string; -}; - -export type HealthCheckResponse = { - status: 'Healthy' | 'Unhealthy'; - systemInfo: { - version: string; +export interface WorkspaceProperties { + workspaceName: string; + deferUpdates: boolean; + homeDirectory: string; + volumes: boolean; + isVolumesExpanded: boolean; + redirect?: { + to: string; + message: { + text: string; + level: string; + }; }; -}; +} diff --git a/workspaces/frontend/src/shared/utilities/WorkspaceUtils.ts b/workspaces/frontend/src/shared/utilities/WorkspaceUtils.ts index f89fa221a..94636b3b7 100644 --- a/workspaces/frontend/src/shared/utilities/WorkspaceUtils.ts +++ b/workspaces/frontend/src/shared/utilities/WorkspaceUtils.ts @@ -1,3 +1,5 @@ +import { Workspace } from '~/shared/types'; + export const formatRam = (valueInKb: number): string => { const units = ['KB', 'MB', 'GB', 'TB']; let index = 0; @@ -14,3 +16,11 @@ export const formatRam = (valueInKb: number): string => { // Helper function to format UNIX timestamps export const formatTimestamp = (timestamp: number): string => timestamp && timestamp > 0 ? new Date(timestamp * 1000).toLocaleString() : '-'; + +export const extractCpuValue = (workspace: Workspace): string => + workspace.podTemplate.options.podConfig.current.labels.find((label) => label.key === 'cpu') + ?.value || 'N/A'; + +export const extractMemoryValue = (workspace: Workspace): string => + workspace.podTemplate.options.podConfig.current.labels.find((label) => label.key === 'memory') + ?.value || 'N/A'; From b4765915198ebdd952403fe11fc05717cb5ac1d5 Mon Sep 17 00:00:00 2001 From: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> Date: Tue, 6 May 2025 19:07:40 +0000 Subject: [PATCH 04/12] Reorganize types Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> --- .../frontend/src/__mocks__/mockNamespaces.ts | 2 +- workspaces/frontend/src/__mocks__/utils.ts | 2 +- .../tests/mocked/workspaceKinds.mock.ts | 2 +- .../src/app/actions/WorkspaceKindsActions.tsx | 2 +- .../src/app/context/useNotebookAPIState.tsx | 36 +++--- .../src/app/hooks/useCreateWorkspace.ts | 34 ----- .../frontend/src/app/hooks/useNamespaces.ts | 2 +- .../src/app/hooks/useWorkspaceCountPerKind.ts | 2 +- .../src/app/hooks/useWorkspaceKindByName.ts | 2 +- .../src/app/hooks/useWorkspaceKinds.ts | 2 +- .../frontend/src/app/hooks/useWorkspaces.ts | 2 +- .../pages/WorkspaceKinds/WorkspaceKinds.tsx | 3 +- .../Workspaces/Creation/WorkspaceCreation.tsx | 4 +- .../image/WorkspaceCreationImageDetails.tsx | 2 +- .../image/WorkspaceCreationImageList.tsx | 2 +- .../image/WorkspaceCreationImageSelection.tsx | 2 +- .../kind/WorkspaceCreationKindDetails.tsx | 2 +- .../kind/WorkspaceCreationKindList.tsx | 2 +- .../kind/WorkspaceCreationKindSelection.tsx | 2 +- .../Creation/labelFilter/FilterByLabels.tsx | 2 +- .../WorkspaceCreationPodConfigDetails.tsx | 2 +- .../WorkspaceCreationPodConfigList.tsx | 2 +- .../WorkspaceCreationPodConfigSelection.tsx | 2 +- .../WorkspaceCreationPropertiesSecrets.tsx | 2 +- .../WorkspaceCreationPropertiesSelection.tsx | 8 +- .../WorkspaceCreationPropertiesVolumes.tsx | 2 +- .../app/pages/Workspaces/DataVolumesList.tsx | 2 +- .../Workspaces/Details/WorkspaceDetails.tsx | 2 +- .../Details/WorkspaceDetailsActivity.tsx | 2 +- .../Details/WorkspaceDetailsOverview.tsx | 2 +- .../pages/Workspaces/ExpandedWorkspaceRow.tsx | 3 +- .../Workspaces/WorkspaceConnectAction.tsx | 2 +- .../src/app/pages/Workspaces/Workspaces.tsx | 3 +- .../src/app/pages/Workspaces/utils.ts | 18 --- .../WorkspaceRedirectInformationView.tsx | 2 +- .../WorkspaceRestartActionModal.tsx | 2 +- .../WorkspaceStartActionModal.tsx | 2 +- .../WorkspaceStopActionModal.tsx | 2 +- workspaces/frontend/src/app/types.ts | 120 +++--------------- .../frontend/src/shared/api/apiUtils.ts | 5 +- .../{types.ts => api/backendApiTypes.ts} | 31 ++--- .../frontend/src/shared/api/callTypes.ts | 6 +- .../frontend/src/shared/api/notebookApi.ts | 91 +++++++++++++ .../src/shared/api/notebookService.ts | 7 +- workspaces/frontend/src/shared/api/types.ts | 9 ++ .../frontend/src/shared/mock/mockBuilder.ts | 2 +- .../shared/mock/mockNotebookServiceData.ts | 7 +- .../src/shared/utilities/WorkspaceUtils.ts | 2 +- 48 files changed, 205 insertions(+), 244 deletions(-) delete mode 100644 workspaces/frontend/src/app/hooks/useCreateWorkspace.ts delete mode 100644 workspaces/frontend/src/app/pages/Workspaces/utils.ts rename workspaces/frontend/src/shared/{types.ts => api/backendApiTypes.ts} (93%) create mode 100644 workspaces/frontend/src/shared/api/notebookApi.ts diff --git a/workspaces/frontend/src/__mocks__/mockNamespaces.ts b/workspaces/frontend/src/__mocks__/mockNamespaces.ts index cde3cfbd6..6265e681b 100644 --- a/workspaces/frontend/src/__mocks__/mockNamespaces.ts +++ b/workspaces/frontend/src/__mocks__/mockNamespaces.ts @@ -1,5 +1,5 @@ import { buildMockNamespace } from '~/shared/mock/mockBuilder'; -import { Namespace } from '~/shared/types'; +import { Namespace } from '~/shared/api/backendApiTypes'; export const mockNamespaces: Namespace[] = [ buildMockNamespace({ name: 'default' }), diff --git a/workspaces/frontend/src/__mocks__/utils.ts b/workspaces/frontend/src/__mocks__/utils.ts index f808fe7cb..d4af2e359 100644 --- a/workspaces/frontend/src/__mocks__/utils.ts +++ b/workspaces/frontend/src/__mocks__/utils.ts @@ -1,4 +1,4 @@ -import { ResponseBody } from '~/app/types'; +import { ResponseBody } from '~/shared/api/types'; export const mockBFFResponse = (data: T): ResponseBody => ({ data, diff --git a/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaceKinds.mock.ts b/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaceKinds.mock.ts index 03c0b05fa..4d0a48615 100644 --- a/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaceKinds.mock.ts +++ b/workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaceKinds.mock.ts @@ -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 { diff --git a/workspaces/frontend/src/app/actions/WorkspaceKindsActions.tsx b/workspaces/frontend/src/app/actions/WorkspaceKindsActions.tsx index 49170787b..b9db0aae3 100644 --- a/workspaces/frontend/src/app/actions/WorkspaceKindsActions.tsx +++ b/workspaces/frontend/src/app/actions/WorkspaceKindsActions.tsx @@ -1,4 +1,4 @@ -import { WorkspaceKind, WorkspaceOptionRedirect } from '~/shared/types'; +import { WorkspaceKind, WorkspaceOptionRedirect } from '~/shared/api/backendApiTypes'; type KindLogoDict = Record; diff --git a/workspaces/frontend/src/app/context/useNotebookAPIState.tsx b/workspaces/frontend/src/app/context/useNotebookAPIState.tsx index f4673e74d..2af754c65 100644 --- a/workspaces/frontend/src/app/context/useNotebookAPIState.tsx +++ b/workspaces/frontend/src/app/context/useNotebookAPIState.tsx @@ -1,22 +1,5 @@ import React from 'react'; -import { NotebookAPIs } from '~/app/types'; -import { - mockCreateWorkspace, - mockCreateWorkspaceKind, - mockDeleteWorkspace, - mockDeleteWorkspaceKind, - mockGetHealthCheck, - mockGetWorkspace, - mockGetWorkspaceKind, - mockListAllWorkspaces, - mockListNamespaces, - mockListWorkspaceKinds, - mockListWorkspaces, - mockPatchWorkspace, - mockPatchWorkspaceKind, - mockUpdateWorkspace, - mockUpdateWorkspaceKind, -} from '~/shared/mock/mockNotebookService'; +import { NotebookAPIs } from '~/shared/api/notebookApi'; import { createWorkspace, createWorkspaceKind, @@ -36,6 +19,23 @@ import { } from '~/shared/api/notebookService'; import { APIState } from '~/shared/api/types'; import useAPIState from '~/shared/api/useAPIState'; +import { + mockCreateWorkspace, + mockCreateWorkspaceKind, + mockDeleteWorkspace, + mockDeleteWorkspaceKind, + mockGetHealthCheck, + mockGetWorkspace, + mockGetWorkspaceKind, + mockListAllWorkspaces, + mockListNamespaces, + mockListWorkspaceKinds, + mockListWorkspaces, + mockPatchWorkspace, + mockPatchWorkspaceKind, + mockUpdateWorkspace, + mockUpdateWorkspaceKind, +} from '~/shared/mock/mockNotebookService'; export type NotebookAPIState = APIState; diff --git a/workspaces/frontend/src/app/hooks/useCreateWorkspace.ts b/workspaces/frontend/src/app/hooks/useCreateWorkspace.ts deleted file mode 100644 index 0e2cfc1b3..000000000 --- a/workspaces/frontend/src/app/hooks/useCreateWorkspace.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from 'react'; -import useFetchState, { - FetchState, - FetchStateCallbackPromise, -} from '~/shared/utilities/useFetchState'; -import { CreateWorkspaceData } from '~/app/types'; -import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; -import { Workspace } from '~/shared/types'; -import { createWorkspaceCall } from '~/app/pages/Workspaces/utils'; -import { APIOptions } from '~/shared/api/types'; - -const useCreateWorkspace = ( - namespace: string, - formData: CreateWorkspaceData, -): FetchState => { - const { api, apiAvailable } = useNotebookAPI(); - - const call = React.useCallback>( - (opts: APIOptions) => { - if (!apiAvailable) { - return Promise.reject(new Error('API not yet available')); - } - if (!namespace) { - return Promise.reject(new Error('namespace is not available yet')); - } - return createWorkspaceCall(opts, api, formData, namespace).then((result) => result.workspace); - }, - [api, apiAvailable, namespace, formData], - ); - - return useFetchState(call, null); -}; - -export default useCreateWorkspace; diff --git a/workspaces/frontend/src/app/hooks/useNamespaces.ts b/workspaces/frontend/src/app/hooks/useNamespaces.ts index e8d5c4349..d07c0ba1b 100644 --- a/workspaces/frontend/src/app/hooks/useNamespaces.ts +++ b/workspaces/frontend/src/app/hooks/useNamespaces.ts @@ -4,7 +4,7 @@ import useFetchState, { FetchStateCallbackPromise, } from '~/shared/utilities/useFetchState'; import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; -import { Namespace } from '~/shared/types'; +import { Namespace } from '~/shared/api/backendApiTypes'; const useNamespaces = (): FetchState => { const { api, apiAvailable } = useNotebookAPI(); diff --git a/workspaces/frontend/src/app/hooks/useWorkspaceCountPerKind.ts b/workspaces/frontend/src/app/hooks/useWorkspaceCountPerKind.ts index d6dac8af5..c627de362 100644 --- a/workspaces/frontend/src/app/hooks/useWorkspaceCountPerKind.ts +++ b/workspaces/frontend/src/app/hooks/useWorkspaceCountPerKind.ts @@ -1,6 +1,6 @@ import * as React from 'react'; import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; -import { Workspace, WorkspaceKind } from '~/shared/types'; +import { Workspace, WorkspaceKind } from '~/shared/api/backendApiTypes'; type WorkspaceCountPerKind = Record; diff --git a/workspaces/frontend/src/app/hooks/useWorkspaceKindByName.ts b/workspaces/frontend/src/app/hooks/useWorkspaceKindByName.ts index f5715e637..942f24b6e 100644 --- a/workspaces/frontend/src/app/hooks/useWorkspaceKindByName.ts +++ b/workspaces/frontend/src/app/hooks/useWorkspaceKindByName.ts @@ -4,7 +4,7 @@ import useFetchState, { FetchStateCallbackPromise, } from '~/shared/utilities/useFetchState'; import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; -import { WorkspaceKind } from '~/shared/types'; +import { WorkspaceKind } from '~/shared/api/backendApiTypes'; const useWorkspaceKindByName = (kind: string): FetchState => { const { api, apiAvailable } = useNotebookAPI(); diff --git a/workspaces/frontend/src/app/hooks/useWorkspaceKinds.ts b/workspaces/frontend/src/app/hooks/useWorkspaceKinds.ts index 90c21ab1d..4db6cb2a6 100644 --- a/workspaces/frontend/src/app/hooks/useWorkspaceKinds.ts +++ b/workspaces/frontend/src/app/hooks/useWorkspaceKinds.ts @@ -3,7 +3,7 @@ import useFetchState, { FetchState, FetchStateCallbackPromise, } from '~/shared/utilities/useFetchState'; -import { WorkspaceKind } from '~/shared/types'; +import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; const useWorkspaceKinds = (): FetchState => { diff --git a/workspaces/frontend/src/app/hooks/useWorkspaces.ts b/workspaces/frontend/src/app/hooks/useWorkspaces.ts index fa0f4838f..b6347f9d3 100644 --- a/workspaces/frontend/src/app/hooks/useWorkspaces.ts +++ b/workspaces/frontend/src/app/hooks/useWorkspaces.ts @@ -4,7 +4,7 @@ import useFetchState, { FetchStateCallbackPromise, } from '~/shared/utilities/useFetchState'; import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; -import { Workspace } from '~/shared/types'; +import { Workspace } from '~/shared/api/backendApiTypes'; const useWorkspaces = (namespace: string): FetchState => { const { api, apiAvailable } = useNotebookAPI(); diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx index 6e6d7f773..805a79214 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx @@ -40,9 +40,10 @@ import { IActions, } from '@patternfly/react-table'; import { CodeIcon, FilterIcon, SearchIcon } from '@patternfly/react-icons'; -import { WorkspaceKind, WorkspaceKindsColumnNames } from '~/shared/types'; +import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds'; import { useWorkspaceCountPerKind } from '~/app/hooks/useWorkspaceCountPerKind'; +import { WorkspaceKindsColumnNames } from '~/app/types'; export enum ActionType { ViewDetails, diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx index 245dd4194..03efd9b61 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx @@ -11,9 +11,9 @@ import { Stack, StackItem, } from '@patternfly/react-core'; +import { CheckIcon } from '@patternfly/react-icons'; import { useCallback, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { CheckIcon } from '@patternfly/react-icons'; import { WorkspaceCreationImageSelection } from '~/app/pages/Workspaces/Creation/image/WorkspaceCreationImageSelection'; import { WorkspaceCreationKindSelection } from '~/app/pages/Workspaces/Creation/kind/WorkspaceCreationKindSelection'; import { WorkspaceCreationPropertiesSelection } from '~/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection'; @@ -23,7 +23,7 @@ import { WorkspaceKind, WorkspacePodConfigValue, WorkspaceProperties, -} from '~/shared/types'; +} from '~/shared/api/backendApiTypes'; enum WorkspaceCreationSteps { KindSelection, diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageDetails.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageDetails.tsx index 19ebcf7a4..a1607c738 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageDetails.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageDetails.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { List, ListItem, Title } from '@patternfly/react-core'; -import { WorkspacePodConfigValue } from '~/shared/types'; +import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes'; type WorkspaceCreationImageDetailsProps = { workspaceImage?: WorkspacePodConfigValue; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageList.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageList.tsx index c573bad47..55c0cd419 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageList.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageList.tsx @@ -13,7 +13,7 @@ import { } from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons/dist/esm/icons/search-icon'; import Filter, { FilteredColumn } from '~/shared/components/Filter'; -import { WorkspaceImageConfigValue } from '~/shared/types'; +import { WorkspaceImageConfigValue } from '~/shared/api/backendApiTypes'; type WorkspaceCreationImageListProps = { images: WorkspaceImageConfigValue[]; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageSelection.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageSelection.tsx index dab896297..e51580f10 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageSelection.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/image/WorkspaceCreationImageSelection.tsx @@ -4,7 +4,7 @@ import { useMemo, useState } from 'react'; import { WorkspaceCreationImageDetails } from '~/app/pages/Workspaces/Creation/image/WorkspaceCreationImageDetails'; import { WorkspaceCreationImageList } from '~/app/pages/Workspaces/Creation/image/WorkspaceCreationImageList'; import { FilterByLabels } from '~/app/pages/Workspaces/Creation/labelFilter/FilterByLabels'; -import { WorkspaceImageConfigValue } from '~/shared/types'; +import { WorkspaceImageConfigValue } from '~/shared/api/backendApiTypes'; interface WorkspaceCreationImageSelectionProps { images: WorkspaceImageConfigValue[]; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/kind/WorkspaceCreationKindDetails.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/kind/WorkspaceCreationKindDetails.tsx index 1fc6088cc..09897d04f 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/kind/WorkspaceCreationKindDetails.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/kind/WorkspaceCreationKindDetails.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Title } from '@patternfly/react-core'; -import { WorkspaceKind } from '~/shared/types'; +import { WorkspaceKind } from '~/shared/api/backendApiTypes'; type WorkspaceCreationKindDetailsProps = { workspaceKind?: WorkspaceKind; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/kind/WorkspaceCreationKindList.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/kind/WorkspaceCreationKindList.tsx index 23ef90f80..ba78a6bc9 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/kind/WorkspaceCreationKindList.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/kind/WorkspaceCreationKindList.tsx @@ -12,7 +12,7 @@ import { EmptyStateBody, } from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons/dist/esm/icons/search-icon'; -import { WorkspaceKind } from '~/shared/types'; +import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import Filter, { FilteredColumn } from '~/shared/components/Filter'; type WorkspaceCreationKindListProps = { diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/kind/WorkspaceCreationKindSelection.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/kind/WorkspaceCreationKindSelection.tsx index ee7e07149..24a567d88 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/kind/WorkspaceCreationKindSelection.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/kind/WorkspaceCreationKindSelection.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Content, Divider, Split, SplitItem } from '@patternfly/react-core'; import { useMemo } from 'react'; -import { WorkspaceKind } from '~/shared/types'; +import { WorkspaceKind } from '~/shared/api/backendApiTypes'; import { WorkspaceCreationKindDetails } from '~/app/pages/Workspaces/Creation/kind/WorkspaceCreationKindDetails'; import { WorkspaceCreationKindList } from '~/app/pages/Workspaces/Creation/kind/WorkspaceCreationKindList'; import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds'; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/labelFilter/FilterByLabels.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/labelFilter/FilterByLabels.tsx index 40de77be1..d540b855b 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/labelFilter/FilterByLabels.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/labelFilter/FilterByLabels.tsx @@ -5,7 +5,7 @@ import { FilterSidePanelCategoryItem, } from '@patternfly/react-catalog-view-extension'; import '@patternfly/react-catalog-view-extension/dist/css/react-catalog-view-extension.css'; -import { WorkspaceOptionLabel } from '~/shared/types'; +import { WorkspaceOptionLabel } from '~/shared/api/backendApiTypes'; type FilterByLabelsProps = { labelledObjects: WorkspaceOptionLabel[]; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigDetails.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigDetails.tsx index fab250d09..86413d28f 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigDetails.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigDetails.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { List, ListItem, Title } from '@patternfly/react-core'; -import { WorkspacePodConfigValue } from '~/shared/types'; +import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes'; type WorkspaceCreationPodConfigDetailsProps = { workspacePodConfig?: WorkspacePodConfigValue; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigList.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigList.tsx index 2c811677d..112c66d45 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigList.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigList.tsx @@ -12,7 +12,7 @@ import { CardBody, } from '@patternfly/react-core'; import { SearchIcon } from '@patternfly/react-icons/dist/esm/icons/search-icon'; -import { WorkspacePodConfigValue } from '~/shared/types'; +import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes'; import Filter, { FilteredColumn } from '~/shared/components/Filter'; type WorkspaceCreationPodConfigListProps = { diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigSelection.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigSelection.tsx index e525f042d..63317cd10 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigSelection.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigSelection.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Content, Divider, Split, SplitItem } from '@patternfly/react-core'; import { useMemo, useState } from 'react'; -import { WorkspacePodConfigValue } from '~/shared/types'; +import { WorkspacePodConfigValue } from '~/shared/api/backendApiTypes'; import { WorkspaceCreationPodConfigDetails } from '~/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigDetails'; import { WorkspaceCreationPodConfigList } from '~/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigList'; import { FilterByLabels } from '~/app/pages/Workspaces/Creation/labelFilter/FilterByLabels'; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSecrets.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSecrets.tsx index bdd4c4281..fa76dd2f0 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSecrets.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSecrets.tsx @@ -18,7 +18,7 @@ import { HelperText, HelperTextItem, } from '@patternfly/react-core'; -import { WorkspaceSecret } from '~/shared/types'; +import { WorkspaceSecret } from '~/shared/api/backendApiTypes'; interface WorkspaceCreationPropertiesSecretsProps { secrets: WorkspaceSecret[]; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection.tsx index 55f7a4ca2..c88508edc 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection.tsx @@ -1,3 +1,4 @@ +import * as React from 'react'; import { Checkbox, Content, @@ -9,16 +10,15 @@ import { SplitItem, TextInput, } from '@patternfly/react-core'; -import * as React from 'react'; import { useEffect, useMemo, useState } from 'react'; +import { WorkspaceCreationImageDetails } from '~/app/pages/Workspaces/Creation/image/WorkspaceCreationImageDetails'; +import { WorkspaceCreationPropertiesVolumes } from '~/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesVolumes'; import { WorkspaceImageConfigValue, WorkspacePodVolumeInfo, WorkspacePodVolumesMutate, WorkspaceSecret, -} from '~/shared/types'; -import { WorkspaceCreationImageDetails } from '~/app/pages/Workspaces/Creation/image/WorkspaceCreationImageDetails'; -import { WorkspaceCreationPropertiesVolumes } from '~/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesVolumes'; +} from '~/shared/api/backendApiTypes'; import { WorkspaceCreationPropertiesSecrets } from './WorkspaceCreationPropertiesSecrets'; interface WorkspaceCreationPropertiesSelectionProps { diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesVolumes.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesVolumes.tsx index ea79d87b3..dbc7478a5 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesVolumes.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesVolumes.tsx @@ -16,7 +16,7 @@ import { FormGroup, ModalHeader, } from '@patternfly/react-core'; -import { WorkspacePodVolumeInfo } from '~/shared/types'; +import { WorkspacePodVolumeInfo } from '~/shared/api/backendApiTypes'; interface WorkspaceCreationPropertiesVolumesProps { volumes: WorkspacePodVolumeInfo[]; diff --git a/workspaces/frontend/src/app/pages/Workspaces/DataVolumesList.tsx b/workspaces/frontend/src/app/pages/Workspaces/DataVolumesList.tsx index 426bf6605..dd847eeed 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/DataVolumesList.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/DataVolumesList.tsx @@ -12,7 +12,7 @@ import { } from '@patternfly/react-core'; import { DatabaseIcon, LockedIcon } from '@patternfly/react-icons'; import * as React from 'react'; -import { Workspace } from '~/shared/types'; +import { Workspace } from '~/shared/api/backendApiTypes'; interface DataVolumesListProps { workspace: Workspace; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetails.tsx b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetails.tsx index 05990f3c3..6122c6553 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetails.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetails.tsx @@ -12,7 +12,7 @@ import { TabContentBody, TabContent, } from '@patternfly/react-core'; -import { Workspace } from '~/shared/types'; +import { Workspace } from '~/shared/api/backendApiTypes'; import { WorkspaceDetailsOverview } from '~/app/pages/Workspaces/Details/WorkspaceDetailsOverview'; import { WorkspaceDetailsActions } from '~/app/pages/Workspaces/Details/WorkspaceDetailsActions'; import { WorkspaceDetailsActivity } from '~/app/pages/Workspaces/Details/WorkspaceDetailsActivity'; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsActivity.tsx b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsActivity.tsx index 59254bae6..44dcdfe89 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsActivity.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsActivity.tsx @@ -6,7 +6,7 @@ import { DescriptionListDescription, Divider, } from '@patternfly/react-core'; -import { Workspace } from '~/shared/types'; +import { Workspace } from '~/shared/api/backendApiTypes'; import { formatTimestamp } from '~/shared/utilities/WorkspaceUtils'; type WorkspaceDetailsActivityProps = { diff --git a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsOverview.tsx b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsOverview.tsx index 2057f0ce9..6b00b3833 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsOverview.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsOverview.tsx @@ -6,7 +6,7 @@ import { DescriptionListDescription, Divider, } from '@patternfly/react-core'; -import { Workspace } from '~/shared/types'; +import { Workspace } from '~/shared/api/backendApiTypes'; type WorkspaceDetailsOverviewProps = { workspace: Workspace; diff --git a/workspaces/frontend/src/app/pages/Workspaces/ExpandedWorkspaceRow.tsx b/workspaces/frontend/src/app/pages/Workspaces/ExpandedWorkspaceRow.tsx index 8caa05546..5fd2e38c2 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/ExpandedWorkspaceRow.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/ExpandedWorkspaceRow.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; import { ExpandableRowContent, Td, Tr } from '@patternfly/react-table'; -import { Workspace, WorkspacesColumnNames } from '~/shared/types'; +import { Workspace } from '~/shared/api/backendApiTypes'; import { DataVolumesList } from '~/app/pages/Workspaces/DataVolumesList'; +import { WorkspacesColumnNames } from '~/app/types'; interface ExpandedWorkspaceRowProps { workspace: Workspace; diff --git a/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConnectAction.tsx b/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConnectAction.tsx index 8022c41b3..43d7de016 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConnectAction.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConnectAction.tsx @@ -7,7 +7,7 @@ import { MenuToggleElement, MenuToggleAction, } from '@patternfly/react-core'; -import { Workspace, WorkspaceState } from '~/shared/types'; +import { Workspace, WorkspaceState } from '~/shared/api/backendApiTypes'; type WorkspaceConnectActionProps = { workspace: Workspace; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx index 6672ed02c..52e016b3f 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx @@ -34,7 +34,7 @@ import { } from '@patternfly/react-icons'; import { useCallback, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Workspace, WorkspacesColumnNames, WorkspaceState } from '~/shared/types'; +import { Workspace, WorkspaceState } from '~/shared/api/backendApiTypes'; import { WorkspaceDetails } from '~/app/pages/Workspaces/Details/WorkspaceDetails'; import { ExpandedWorkspaceRow } from '~/app/pages/Workspaces/ExpandedWorkspaceRow'; import DeleteModal from '~/shared/components/DeleteModal'; @@ -49,6 +49,7 @@ import { WorkspaceStartActionModal } from '~/app/pages/Workspaces/workspaceActio import { WorkspaceRestartActionModal } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal'; import { WorkspaceStopActionModal } from '~/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal'; import { useNamespaceContext } from '~/app/context/NamespaceContextProvider'; +import { WorkspacesColumnNames } from '~/app/types'; import Filter, { FilteredColumn } from 'shared/components/Filter'; import { extractCpuValue, extractMemoryValue } from 'shared/utilities/WorkspaceUtils'; diff --git a/workspaces/frontend/src/app/pages/Workspaces/utils.ts b/workspaces/frontend/src/app/pages/Workspaces/utils.ts deleted file mode 100644 index e260d02c3..000000000 --- a/workspaces/frontend/src/app/pages/Workspaces/utils.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Workspace } from '~/shared/types'; -import { CreateWorkspaceData } from '~/app/types'; -import { NotebookAPIState } from '~/app/context/useNotebookAPIState'; -import { APIOptions } from '~/shared/api/types'; - -export type RegisterWorkspaceCreatedResources = { - workspace: Workspace; -}; - -export const createWorkspaceCall = async ( - opts: APIOptions, - api: NotebookAPIState['api'], - formData: CreateWorkspaceData, - namespace: string, -): Promise => { - const workspace = await api.createWorkspace(opts, namespace, formData); - return { workspace }; -}; diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView.tsx b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView.tsx index 3827fdc10..cd277b278 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView.tsx @@ -6,7 +6,7 @@ import { } from '@patternfly/react-icons'; import * as React from 'react'; import useWorkspaceKindByName from '~/app/hooks/useWorkspaceKindByName'; -import { WorkspaceKind } from '~/shared/types'; +import { WorkspaceKind } from '~/shared/api/backendApiTypes'; const getLevelIcon = (level: string | undefined) => { switch (level) { diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal.tsx b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal.tsx index d5edf030a..aa012a88c 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal.tsx @@ -8,7 +8,7 @@ import { ModalHeader, TabTitleText, } from '@patternfly/react-core'; -import { Workspace } from '~/shared/types'; +import { Workspace } from '~/shared/api/backendApiTypes'; import { WorkspaceRedirectInformationView } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView'; interface RestartActionAlertProps { diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx index 54215d42f..ec7b848ab 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx @@ -7,7 +7,7 @@ import { ModalHeader, TabTitleText, } from '@patternfly/react-core'; -import { Workspace } from '~/shared/types'; +import { Workspace } from '~/shared/api/backendApiTypes'; import { WorkspaceRedirectInformationView } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView'; interface StartActionAlertProps { diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx index 4b2fb9a22..981b3fbbf 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx @@ -8,7 +8,7 @@ import { TabTitleText, Content, } from '@patternfly/react-core'; -import { Workspace } from '~/shared/types'; +import { Workspace } from '~/shared/api/backendApiTypes'; import { WorkspaceRedirectInformationView } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView'; interface StopActionAlertProps { diff --git a/workspaces/frontend/src/app/types.ts b/workspaces/frontend/src/app/types.ts index d7d4327e5..2614e4461 100644 --- a/workspaces/frontend/src/app/types.ts +++ b/workspaces/frontend/src/app/types.ts @@ -1,106 +1,20 @@ -import { APIOptions } from '~/shared/api/types'; -import { - HealthCheckResponse, - Namespace, - Workspace, - WorkspaceKind, - WorkspacePodTemplateMutate, -} from '~/shared/types'; - -export type ResponseBody = { - data: T; - metadata?: Record; -}; - -// Health -export type GetHealthCheck = (opts: APIOptions) => Promise; - -// Namespace -export type ListNamespaces = (opts: APIOptions) => Promise; - -// Workspace -export type ListAllWorkspaces = (opts: APIOptions) => Promise; -export type ListWorkspaces = (opts: APIOptions, namespace: string) => Promise; -export type GetWorkspace = ( - opts: APIOptions, - namespace: string, - workspace: string, -) => Promise; -export type CreateWorkspace = ( - opts: APIOptions, - namespace: string, - data: CreateWorkspaceData, -) => Promise; -export type UpdateWorkspace = ( - opts: APIOptions, - namespace: string, - workspace: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it - data: any, -) => Promise; -export type PatchWorkspace = ( - opts: APIOptions, - namespace: string, - workspace: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it - data: any, -) => Promise; -export type DeleteWorkspace = ( - opts: APIOptions, - namespace: string, - workspace: string, -) => Promise; - -// WorkspaceKind -export type ListWorkspaceKinds = (opts: APIOptions) => Promise; -export type GetWorkspaceKind = (opts: APIOptions, kind: string) => Promise; -export type CreateWorkspaceKind = ( - opts: APIOptions, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it - data: any, -) => Promise; -export type UpdateWorkspaceKind = ( - opts: APIOptions, - kind: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it - data: any, -) => Promise; -export type PatchWorkspaceKind = ( - opts: APIOptions, - kind: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it - data: any, -) => Promise; -export type DeleteWorkspaceKind = (opts: APIOptions, kind: string) => Promise; - -export type NotebookAPIs = { - // Health - getHealthCheck: GetHealthCheck; - // Namespace - listNamespaces: ListNamespaces; - // Workspace - listAllWorkspaces: ListAllWorkspaces; - listWorkspaces: ListWorkspaces; - getWorkspace: GetWorkspace; - createWorkspace: CreateWorkspace; - updateWorkspace: UpdateWorkspace; - patchWorkspace: PatchWorkspace; - deleteWorkspace: DeleteWorkspace; - // WorkspaceKind - listWorkspaceKinds: ListWorkspaceKinds; - getWorkspaceKind: GetWorkspaceKind; - createWorkspaceKind: CreateWorkspaceKind; - updateWorkspaceKind: UpdateWorkspaceKind; - patchWorkspaceKind: PatchWorkspaceKind; - deleteWorkspaceKind: DeleteWorkspaceKind; +export type WorkspacesColumnNames = { + name: string; + kind: string; + image: string; + podConfig: string; + state: string; + homeVol: string; + cpu: string; + ram: string; + lastActivity: string; + redirectStatus: string; }; -export type CreateWorkspaceData = { - data: { - name: string; - kind: string; - paused: boolean; - deferUpdates: boolean; - podTemplate: WorkspacePodTemplateMutate; - }; +export type WorkspaceKindsColumnNames = { + icon: string; + name: string; + description: string; + deprecated: string; + numberOfWorkspaces: string; }; diff --git a/workspaces/frontend/src/shared/api/apiUtils.ts b/workspaces/frontend/src/shared/api/apiUtils.ts index 10735494f..3d90488b5 100644 --- a/workspaces/frontend/src/shared/api/apiUtils.ts +++ b/workspaces/frontend/src/shared/api/apiUtils.ts @@ -1,7 +1,6 @@ -import { APIOptions } from '~/shared/api/types'; +import { APIOptions, ResponseBody } from '~/shared/api/types'; import { EitherOrNone } from '~/shared/typeHelpers'; -import { ResponseBody } from '~/app/types'; -import { DEV_MODE, AUTH_HEADER } from '~/shared/utilities/const'; +import { AUTH_HEADER, DEV_MODE } from '~/shared/utilities/const'; export const mergeRequestInit = ( opts: APIOptions = {}, diff --git a/workspaces/frontend/src/shared/types.ts b/workspaces/frontend/src/shared/api/backendApiTypes.ts similarity index 93% rename from workspaces/frontend/src/shared/types.ts rename to workspaces/frontend/src/shared/api/backendApiTypes.ts index 6b9847f9b..e796d08c4 100644 --- a/workspaces/frontend/src/shared/types.ts +++ b/workspaces/frontend/src/shared/api/backendApiTypes.ts @@ -7,14 +7,14 @@ export interface WorkspaceSystemInfo { version: string; } -export type HealthCheckResponse = { +export interface HealthCheckResponse { status: WorkspaceServiceStatus; systemInfo: WorkspaceSystemInfo; -}; +} -export type Namespace = { +export interface Namespace { name: string; -}; +} export interface WorkspaceImageRef { url: string; @@ -246,26 +246,13 @@ export interface Workspace { services: WorkspaceService[]; } -export type WorkspacesColumnNames = { +export interface WorkspaceCreate { name: string; kind: string; - image: string; - podConfig: string; - state: string; - homeVol: string; - cpu: string; - ram: string; - lastActivity: string; - redirectStatus: string; -}; - -export type WorkspaceKindsColumnNames = { - icon: string; - name: string; - description: string; - deprecated: string; - numberOfWorkspaces: string; -}; + paused: boolean; + deferUpdates: boolean; + podTemplate: WorkspacePodTemplateMutate; +} export interface WorkspaceProperties { workspaceName: string; diff --git a/workspaces/frontend/src/shared/api/callTypes.ts b/workspaces/frontend/src/shared/api/callTypes.ts index d927489ed..924a6c425 100644 --- a/workspaces/frontend/src/shared/api/callTypes.ts +++ b/workspaces/frontend/src/shared/api/callTypes.ts @@ -3,9 +3,9 @@ import { CreateWorkspaceKind, DeleteWorkspace, DeleteWorkspaceKind, + GetHealthCheck, GetWorkspace, GetWorkspaceKind, - GetHealthCheck, ListAllWorkspaces, ListNamespaces, ListWorkspaceKinds, @@ -14,8 +14,8 @@ import { PatchWorkspaceKind, UpdateWorkspace, UpdateWorkspaceKind, -} from '~/app/types'; -import { APIOptions } from './types'; +} from '~/shared/api/notebookApi'; +import { APIOptions } from '~/shared/api/types'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type KubeflowSpecificAPICall = (opts: APIOptions, ...args: any[]) => Promise; diff --git a/workspaces/frontend/src/shared/api/notebookApi.ts b/workspaces/frontend/src/shared/api/notebookApi.ts new file mode 100644 index 000000000..227a4ff20 --- /dev/null +++ b/workspaces/frontend/src/shared/api/notebookApi.ts @@ -0,0 +1,91 @@ +import { + HealthCheckResponse, + Namespace, + Workspace, + WorkspaceCreate, + WorkspaceKind, +} from '~/shared/api/backendApiTypes'; +import { APIOptions, CreateData } from '~/shared/api/types'; + +// Health +export type GetHealthCheck = (opts: APIOptions) => Promise; + +// Namespace +export type ListNamespaces = (opts: APIOptions) => Promise; + +// Workspace +export type ListAllWorkspaces = (opts: APIOptions) => Promise; +export type ListWorkspaces = (opts: APIOptions, namespace: string) => Promise; +export type GetWorkspace = ( + opts: APIOptions, + namespace: string, + workspace: string, +) => Promise; +export type CreateWorkspace = ( + opts: APIOptions, + namespace: string, + data: CreateData, +) => Promise; +export type UpdateWorkspace = ( + opts: APIOptions, + namespace: string, + workspace: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it + data: any, +) => Promise; +export type PatchWorkspace = ( + opts: APIOptions, + namespace: string, + workspace: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it + data: any, +) => Promise; +export type DeleteWorkspace = ( + opts: APIOptions, + namespace: string, + workspace: string, +) => Promise; + +// WorkspaceKind +export type ListWorkspaceKinds = (opts: APIOptions) => Promise; +export type GetWorkspaceKind = (opts: APIOptions, kind: string) => Promise; +export type CreateWorkspaceKind = ( + opts: APIOptions, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it + data: any, +) => Promise; +export type UpdateWorkspaceKind = ( + opts: APIOptions, + kind: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it + data: any, +) => Promise; +export type PatchWorkspaceKind = ( + opts: APIOptions, + kind: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it + data: any, +) => Promise; +export type DeleteWorkspaceKind = (opts: APIOptions, kind: string) => Promise; + +export type NotebookAPIs = { + // Health + getHealthCheck: GetHealthCheck; + // Namespace + listNamespaces: ListNamespaces; + // Workspace + listAllWorkspaces: ListAllWorkspaces; + listWorkspaces: ListWorkspaces; + getWorkspace: GetWorkspace; + createWorkspace: CreateWorkspace; + updateWorkspace: UpdateWorkspace; + patchWorkspace: PatchWorkspace; + deleteWorkspace: DeleteWorkspace; + // WorkspaceKind + listWorkspaceKinds: ListWorkspaceKinds; + getWorkspaceKind: GetWorkspaceKind; + createWorkspaceKind: CreateWorkspaceKind; + updateWorkspaceKind: UpdateWorkspaceKind; + patchWorkspaceKind: PatchWorkspaceKind; + deleteWorkspaceKind: DeleteWorkspaceKind; +}; diff --git a/workspaces/frontend/src/shared/api/notebookService.ts b/workspaces/frontend/src/shared/api/notebookService.ts index afdbed9f7..28e2a02e3 100644 --- a/workspaces/frontend/src/shared/api/notebookService.ts +++ b/workspaces/frontend/src/shared/api/notebookService.ts @@ -7,7 +7,12 @@ import { restUPDATE, } from '~/shared/api/apiUtils'; import { handleRestFailures } from '~/shared/api/errorUtils'; -import { HealthCheckResponse, Namespace, Workspace, WorkspaceKind } from '~/shared/types'; +import { + HealthCheckResponse, + Namespace, + Workspace, + WorkspaceKind, +} from '~/shared/api/backendApiTypes'; import { CreateWorkspaceAPI, CreateWorkspaceKindAPI, diff --git a/workspaces/frontend/src/shared/api/types.ts b/workspaces/frontend/src/shared/api/types.ts index bb0055bc8..bb80ac165 100644 --- a/workspaces/frontend/src/shared/api/types.ts +++ b/workspaces/frontend/src/shared/api/types.ts @@ -18,3 +18,12 @@ export type APIState = { /** The available API functions */ api: T; }; + +export type ResponseBody = { + data: T; + metadata?: Record; +}; + +export type CreateData = { + data: T; +}; diff --git a/workspaces/frontend/src/shared/mock/mockBuilder.ts b/workspaces/frontend/src/shared/mock/mockBuilder.ts index 324a6c058..9187df278 100644 --- a/workspaces/frontend/src/shared/mock/mockBuilder.ts +++ b/workspaces/frontend/src/shared/mock/mockBuilder.ts @@ -7,7 +7,7 @@ import { WorkspaceRedirectMessageLevel, WorkspaceServiceStatus, WorkspaceState, -} from '~/shared/types'; +} from '~/shared/api/backendApiTypes'; export const buildMockHealthCheckResponse = ( healthCheckResponse?: Partial, diff --git a/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts b/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts index 029935538..093835c71 100644 --- a/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts +++ b/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts @@ -1,3 +1,9 @@ +import { + Workspace, + WorkspaceKind, + WorkspaceKindInfo, + WorkspaceState, +} from '~/shared/api/backendApiTypes'; import { buildMockHealthCheckResponse, buildMockNamespace, @@ -5,7 +11,6 @@ import { buildMockWorkspaceKind, buildMockWorkspaceKindInfo, } from '~/shared/mock/mockBuilder'; -import { Workspace, WorkspaceKind, WorkspaceKindInfo, WorkspaceState } from '~/shared/types'; // Health export const mockedHealthCheckResponse = buildMockHealthCheckResponse(); diff --git a/workspaces/frontend/src/shared/utilities/WorkspaceUtils.ts b/workspaces/frontend/src/shared/utilities/WorkspaceUtils.ts index 94636b3b7..52da1fcc8 100644 --- a/workspaces/frontend/src/shared/utilities/WorkspaceUtils.ts +++ b/workspaces/frontend/src/shared/utilities/WorkspaceUtils.ts @@ -1,4 +1,4 @@ -import { Workspace } from '~/shared/types'; +import { Workspace } from '~/shared/api/backendApiTypes'; export const formatRam = (valueInKb: number): string => { const units = ['KB', 'MB', 'GB', 'TB']; From 76d00ff3f4a04986d769d6fc2b553fd1ee5128c8 Mon Sep 17 00:00:00 2001 From: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> Date: Tue, 6 May 2025 17:15:13 -0300 Subject: [PATCH 05/12] Prepare code for create/update/patch Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> --- .../src/shared/api/backendApiTypes.ts | 15 ++++++++ .../frontend/src/shared/api/notebookApi.ts | 34 +++++++++---------- .../src/shared/api/notebookService.ts | 10 +++--- workspaces/frontend/src/shared/api/types.ts | 2 +- .../src/shared/mock/mockNotebookService.ts | 11 +++--- 5 files changed, 44 insertions(+), 28 deletions(-) diff --git a/workspaces/frontend/src/shared/api/backendApiTypes.ts b/workspaces/frontend/src/shared/api/backendApiTypes.ts index e796d08c4..e73fd29f9 100644 --- a/workspaces/frontend/src/shared/api/backendApiTypes.ts +++ b/workspaces/frontend/src/shared/api/backendApiTypes.ts @@ -89,6 +89,15 @@ export interface WorkspaceKindPodTemplate { options: WorkspaceKindPodTemplateOptions; } +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface WorkspaceKindCreate {} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface WorkspaceKindUpdate {} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface WorkspaceKindPatch {} + export interface WorkspaceKind { name: string; displayName: string; @@ -254,6 +263,12 @@ export interface WorkspaceCreate { podTemplate: WorkspacePodTemplateMutate; } +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface WorkspaceUpdate {} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface WorkspacePatch {} + export interface WorkspaceProperties { workspaceName: string; deferUpdates: boolean; diff --git a/workspaces/frontend/src/shared/api/notebookApi.ts b/workspaces/frontend/src/shared/api/notebookApi.ts index 227a4ff20..8b0341c07 100644 --- a/workspaces/frontend/src/shared/api/notebookApi.ts +++ b/workspaces/frontend/src/shared/api/notebookApi.ts @@ -4,8 +4,13 @@ import { Workspace, WorkspaceCreate, WorkspaceKind, + WorkspaceKindCreate, + WorkspaceKindPatch, + WorkspaceKindUpdate, + WorkspacePatch, + WorkspaceUpdate, } from '~/shared/api/backendApiTypes'; -import { APIOptions, CreateData } from '~/shared/api/types'; +import { APIOptions, RequestData } from '~/shared/api/types'; // Health export type GetHealthCheck = (opts: APIOptions) => Promise; @@ -24,22 +29,20 @@ export type GetWorkspace = ( export type CreateWorkspace = ( opts: APIOptions, namespace: string, - data: CreateData, + data: RequestData, ) => Promise; export type UpdateWorkspace = ( opts: APIOptions, namespace: string, workspace: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it - data: any, -) => Promise; + data: RequestData, +) => Promise; export type PatchWorkspace = ( opts: APIOptions, namespace: string, workspace: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it - data: any, -) => Promise; + data: RequestData, +) => Promise; export type DeleteWorkspace = ( opts: APIOptions, namespace: string, @@ -51,21 +54,18 @@ export type ListWorkspaceKinds = (opts: APIOptions) => Promise; export type GetWorkspaceKind = (opts: APIOptions, kind: string) => Promise; export type CreateWorkspaceKind = ( opts: APIOptions, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it - data: any, -) => Promise; + data: RequestData, +) => Promise; export type UpdateWorkspaceKind = ( opts: APIOptions, kind: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it - data: any, -) => Promise; + data: RequestData, +) => Promise; export type PatchWorkspaceKind = ( opts: APIOptions, kind: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Review data and response when start using it - data: any, -) => Promise; + data: RequestData, +) => Promise; export type DeleteWorkspaceKind = (opts: APIOptions, kind: string) => Promise; export type NotebookAPIs = { diff --git a/workspaces/frontend/src/shared/api/notebookService.ts b/workspaces/frontend/src/shared/api/notebookService.ts index 28e2a02e3..a5cdf859e 100644 --- a/workspaces/frontend/src/shared/api/notebookService.ts +++ b/workspaces/frontend/src/shared/api/notebookService.ts @@ -65,11 +65,11 @@ export const updateWorkspace: UpdateWorkspaceAPI = (hostPath) => (opts, namespace, workspace, data) => handleRestFailures( restUPDATE(hostPath, `/workspaces/${namespace}/${workspace}`, data, {}, opts), - ).then((response) => extractNotebookResponse(response)); + ).then((response) => extractNotebookResponse(response)); export const patchWorkspace: PatchWorkspaceAPI = (hostPath) => (opts, namespace, workspace, data) => handleRestFailures(restPATCH(hostPath, `/workspaces/${namespace}/${workspace}`, data, opts)).then( - (response) => extractNotebookResponse(response), + (response) => extractNotebookResponse(response), ); export const deleteWorkspace: DeleteWorkspaceAPI = (hostPath) => (opts, namespace, workspace) => @@ -89,17 +89,17 @@ export const getWorkspaceKind: GetWorkspaceKindAPI = (hostPath) => (opts, kind) export const createWorkspaceKind: CreateWorkspaceKindAPI = (hostPath) => (opts, data) => handleRestFailures(restCREATE(hostPath, `/workspacekinds`, data, {}, opts)).then((response) => - extractNotebookResponse(response), + extractNotebookResponse(response), ); export const updateWorkspaceKind: UpdateWorkspaceKindAPI = (hostPath) => (opts, kind, data) => handleRestFailures(restUPDATE(hostPath, `/workspacekinds/${kind}`, data, {}, opts)).then( - (response) => extractNotebookResponse(response), + (response) => extractNotebookResponse(response), ); export const patchWorkspaceKind: PatchWorkspaceKindAPI = (hostPath) => (opts, kind, data) => handleRestFailures(restPATCH(hostPath, `/workspacekinds/${kind}`, data, opts)).then((response) => - extractNotebookResponse(response), + extractNotebookResponse(response), ); export const deleteWorkspaceKind: DeleteWorkspaceKindAPI = (hostPath) => (opts, kind) => diff --git a/workspaces/frontend/src/shared/api/types.ts b/workspaces/frontend/src/shared/api/types.ts index bb80ac165..6f1ac507d 100644 --- a/workspaces/frontend/src/shared/api/types.ts +++ b/workspaces/frontend/src/shared/api/types.ts @@ -24,6 +24,6 @@ export type ResponseBody = { metadata?: Record; }; -export type CreateData = { +export type RequestData = { data: T; }; diff --git a/workspaces/frontend/src/shared/mock/mockNotebookService.ts b/workspaces/frontend/src/shared/mock/mockNotebookService.ts index 786c454a5..b60c7fc36 100644 --- a/workspaces/frontend/src/shared/mock/mockNotebookService.ts +++ b/workspaces/frontend/src/shared/mock/mockNotebookService.ts @@ -20,6 +20,7 @@ import { mockedHealthCheckResponse, mockNamespaces, mockWorkspace1, + mockWorkspaceKind1, mockWorkspaceKinds, } from '~/shared/mock/mockNotebookServiceData'; @@ -38,10 +39,10 @@ export const mockGetWorkspace: GetWorkspaceAPI = () => async (_opts, namespace, export const mockCreateWorkspace: CreateWorkspaceAPI = () => async () => mockWorkspace1; // eslint-disable-next-line @typescript-eslint/no-empty-function -export const mockUpdateWorkspace: UpdateWorkspaceAPI = () => async () => {}; +export const mockUpdateWorkspace: UpdateWorkspaceAPI = () => async () => mockWorkspace1; // eslint-disable-next-line @typescript-eslint/no-empty-function -export const mockPatchWorkspace: PatchWorkspaceAPI = () => async () => {}; +export const mockPatchWorkspace: PatchWorkspaceAPI = () => async () => mockWorkspace1; // eslint-disable-next-line @typescript-eslint/no-empty-function export const mockDeleteWorkspace: DeleteWorkspaceAPI = () => async () => {}; @@ -52,13 +53,13 @@ export const mockGetWorkspaceKind: GetWorkspaceKindAPI = () => async (_opts, kin mockWorkspaceKinds.find((w) => w.name === kind)!; // eslint-disable-next-line @typescript-eslint/no-empty-function -export const mockCreateWorkspaceKind: CreateWorkspaceKindAPI = () => async () => {}; +export const mockCreateWorkspaceKind: CreateWorkspaceKindAPI = () => async () => mockWorkspaceKind1; // eslint-disable-next-line @typescript-eslint/no-empty-function -export const mockUpdateWorkspaceKind: UpdateWorkspaceKindAPI = () => async () => {}; +export const mockUpdateWorkspaceKind: UpdateWorkspaceKindAPI = () => async () => mockWorkspaceKind1; // eslint-disable-next-line @typescript-eslint/no-empty-function -export const mockPatchWorkspaceKind: PatchWorkspaceKindAPI = () => async () => {}; +export const mockPatchWorkspaceKind: PatchWorkspaceKindAPI = () => async () => mockWorkspaceKind1; // eslint-disable-next-line @typescript-eslint/no-empty-function export const mockDeleteWorkspaceKind: DeleteWorkspaceKindAPI = () => async () => {}; From 5410d8fc1568726f2a26a7bf330e8a50c6b44cdb Mon Sep 17 00:00:00 2001 From: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> Date: Wed, 7 May 2025 09:34:44 -0300 Subject: [PATCH 06/12] Format files with prettier Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> --- workspaces/frontend/config/webpack.common.js | 12 +++++----- workspaces/frontend/jest.config.js | 4 +--- workspaces/frontend/tsconfig.json | 24 ++++---------------- 3 files changed, 12 insertions(+), 28 deletions(-) diff --git a/workspaces/frontend/config/webpack.common.js b/workspaces/frontend/config/webpack.common.js index a78bd03cc..cfa03d366 100644 --- a/workspaces/frontend/config/webpack.common.js +++ b/workspaces/frontend/config/webpack.common.js @@ -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', + ), + ], }, ], }, diff --git a/workspaces/frontend/jest.config.js b/workspaces/frontend/jest.config.js index e8e27cc64..01c94bcd0 100644 --- a/workspaces/frontend/jest.config.js +++ b/workspaces/frontend/jest.config.js @@ -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: [], diff --git a/workspaces/frontend/tsconfig.json b/workspaces/frontend/tsconfig.json index b7e5ca628..1a9726b8f 100644 --- a/workspaces/frontend/tsconfig.json +++ b/workspaces/frontend/tsconfig.json @@ -5,10 +5,7 @@ "outDir": "dist", "module": "esnext", "target": "ES6", - "lib": [ - "es6", - "dom" - ], + "lib": ["es6", "dom"], "sourceMap": true, "jsx": "react", "moduleResolution": "node", @@ -22,22 +19,11 @@ "allowSyntheticDefaultImports": true, "strict": true, "paths": { - "~/*": [ - "./*" - ] + "~/*": ["./*"] }, "importHelpers": true, "skipLibCheck": true }, - "include": [ - "**/*.ts", - "**/*.tsx", - "**/*.jsx", - "**/*.js" - ], - "exclude": [ - "node_modules", - "src/__tests__/cypress" - ] - -} \ No newline at end of file + "include": ["**/*.ts", "**/*.tsx", "**/*.jsx", "**/*.js"], + "exclude": ["node_modules", "src/__tests__/cypress"] +} From 9c217a927aaae7a874b01b0f663b6929f8974e17 Mon Sep 17 00:00:00 2001 From: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> Date: Wed, 7 May 2025 10:04:09 -0300 Subject: [PATCH 07/12] Clean up unnecessary eslint comments Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> --- .../src/shared/mock/mockNotebookService.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/workspaces/frontend/src/shared/mock/mockNotebookService.ts b/workspaces/frontend/src/shared/mock/mockNotebookService.ts index b60c7fc36..6148bbd8f 100644 --- a/workspaces/frontend/src/shared/mock/mockNotebookService.ts +++ b/workspaces/frontend/src/shared/mock/mockNotebookService.ts @@ -38,28 +38,25 @@ export const mockGetWorkspace: GetWorkspaceAPI = () => async (_opts, namespace, export const mockCreateWorkspace: CreateWorkspaceAPI = () => async () => mockWorkspace1; -// eslint-disable-next-line @typescript-eslint/no-empty-function export const mockUpdateWorkspace: UpdateWorkspaceAPI = () => async () => mockWorkspace1; -// eslint-disable-next-line @typescript-eslint/no-empty-function export const mockPatchWorkspace: PatchWorkspaceAPI = () => async () => mockWorkspace1; -// eslint-disable-next-line @typescript-eslint/no-empty-function -export const mockDeleteWorkspace: DeleteWorkspaceAPI = () => async () => {}; +export const mockDeleteWorkspace: DeleteWorkspaceAPI = () => async () => { + /* no-op */ +}; export const mockListWorkspaceKinds: ListWorkspaceKindsAPI = () => async () => mockWorkspaceKinds; export const mockGetWorkspaceKind: GetWorkspaceKindAPI = () => async (_opts, kind) => mockWorkspaceKinds.find((w) => w.name === kind)!; -// eslint-disable-next-line @typescript-eslint/no-empty-function export const mockCreateWorkspaceKind: CreateWorkspaceKindAPI = () => async () => mockWorkspaceKind1; -// eslint-disable-next-line @typescript-eslint/no-empty-function export const mockUpdateWorkspaceKind: UpdateWorkspaceKindAPI = () => async () => mockWorkspaceKind1; -// eslint-disable-next-line @typescript-eslint/no-empty-function export const mockPatchWorkspaceKind: PatchWorkspaceKindAPI = () => async () => mockWorkspaceKind1; -// eslint-disable-next-line @typescript-eslint/no-empty-function -export const mockDeleteWorkspaceKind: DeleteWorkspaceKindAPI = () => async () => {}; +export const mockDeleteWorkspaceKind: DeleteWorkspaceKindAPI = () => async () => { + /* no-op */ +}; From abd5ac7ea6a20a0548c82c5b3c7836b21604f05c Mon Sep 17 00:00:00 2001 From: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> Date: Fri, 9 May 2025 10:17:43 +0000 Subject: [PATCH 08/12] Minor adjustments after rebase Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> --- .../Workspaces/Creation/WorkspaceCreation.tsx | 2 +- .../pages/Workspaces/WorkspaceConnectAction.tsx | 1 - workspaces/frontend/src/app/types.ts | 16 ++++++++++++++++ .../frontend/src/shared/api/backendApiTypes.ts | 15 --------------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx index 03efd9b61..0362cce16 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx @@ -22,8 +22,8 @@ import { WorkspaceImageConfigValue, WorkspaceKind, WorkspacePodConfigValue, - WorkspaceProperties, } from '~/shared/api/backendApiTypes'; +import { WorkspaceProperties } from '~/app/types'; enum WorkspaceCreationSteps { KindSelection, diff --git a/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConnectAction.tsx b/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConnectAction.tsx index 43d7de016..fa87ea033 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConnectAction.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/WorkspaceConnectAction.tsx @@ -54,7 +54,6 @@ export const WorkspaceConnectAction: React.FunctionComponent Date: Fri, 9 May 2025 07:20:53 -0300 Subject: [PATCH 09/12] Add error boundary Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> --- workspaces/frontend/src/app/App.tsx | 29 +++--- .../frontend/src/app/error/ErrorBoundary.tsx | 98 +++++++++++++++++++ .../frontend/src/app/error/ErrorDetails.tsx | 72 ++++++++++++++ .../frontend/src/app/error/UpdateState.tsx | 46 +++++++++ 4 files changed, 232 insertions(+), 13 deletions(-) create mode 100644 workspaces/frontend/src/app/error/ErrorBoundary.tsx create mode 100644 workspaces/frontend/src/app/error/ErrorDetails.tsx create mode 100644 workspaces/frontend/src/app/error/UpdateState.tsx diff --git a/workspaces/frontend/src/app/App.tsx b/workspaces/frontend/src/app/App.tsx index 098b393c2..81b487236 100644 --- a/workspaces/frontend/src/app/App.tsx +++ b/workspaces/frontend/src/app/App.tsx @@ -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'; @@ -63,19 +64,21 @@ const App: React.FC = () => { ); return ( - - - } - > - - - - + + + + } + > + + + + + ); }; diff --git a/workspaces/frontend/src/app/error/ErrorBoundary.tsx b/workspaces/frontend/src/app/error/ErrorBoundary.tsx new file mode 100644 index 000000000..d59b85e2e --- /dev/null +++ b/workspaces/frontend/src/app/error/ErrorBoundary.tsx @@ -0,0 +1,98 @@ +import * as React from 'react'; +import { Button, Split, SplitItem, Title } from '@patternfly/react-core'; +import { TimesIcon } from '@patternfly/react-icons'; +import ErrorDetails from '~/app/error/ErrorDetails'; +import UpdateState from '~/app/error/UpdateState'; + +type ErrorBoundaryProps = { + children?: React.ReactNode; +}; + +type ErrorBoundaryState = + | { hasError: false } + | { + hasError: true; + error: Error; + errorInfo: React.ErrorInfo; + isUpdateState: boolean; + }; + +class ErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { + hasError: false, + }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { + this.setState({ + hasError: true, + error, + errorInfo, + isUpdateState: error.name === 'ChunkLoadError', + }); + // eslint-disable-next-line no-console + console.error('Caught error:', error, errorInfo); + } + + render(): React.ReactNode { + const { children } = this.props; + const { hasError } = this.state; + + if (hasError) { + const { error, errorInfo, isUpdateState } = this.state; + if (isUpdateState) { + return ( + this.setState((prevState) => ({ ...prevState, isUpdateState: false }))} + /> + ); + } + return ( +
+ + + + An error occurred + +

+ Try{' '} + {' '} + the page if there was a recent update. +

+
+ +
+ ); + } + + return children; + } +} + +export default ErrorBoundary; diff --git a/workspaces/frontend/src/app/error/ErrorDetails.tsx b/workspaces/frontend/src/app/error/ErrorDetails.tsx new file mode 100644 index 000000000..66d84a20b --- /dev/null +++ b/workspaces/frontend/src/app/error/ErrorDetails.tsx @@ -0,0 +1,72 @@ +import * as React from 'react'; +import { + ClipboardCopy, + ClipboardCopyVariant, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Title, +} from '@patternfly/react-core'; + +type ErrorDetailsProps = { + title: string; + errorMessage?: string; + componentStack: string; + stack?: string; +}; + +const ErrorDetails: React.FC = ({ + title, + errorMessage, + componentStack, + stack, +}) => ( + <> + + {title} + + + {errorMessage ? ( + + Description: + {errorMessage} + + ) : null} + + + Component trace: + + + {componentStack.trim()} + + + + + {stack ? ( + + Stack trace: + + + {stack.trim()} + + + + ) : null} + + +); + +export default ErrorDetails; diff --git a/workspaces/frontend/src/app/error/UpdateState.tsx b/workspaces/frontend/src/app/error/UpdateState.tsx new file mode 100644 index 000000000..dce1252b9 --- /dev/null +++ b/workspaces/frontend/src/app/error/UpdateState.tsx @@ -0,0 +1,46 @@ +import * as React from 'react'; +import { + Button, + EmptyState, + EmptyStateActions, + EmptyStateBody, + EmptyStateFooter, + EmptyStateVariant, + PageSection, +} from '@patternfly/react-core'; +import { PathMissingIcon } from '@patternfly/react-icons'; + +type Props = { + onClose: () => void; +}; + +const UpdateState: React.FC = ({ onClose }) => ( + + + This is likely the result of a recent update. + + + + + + + + + + +); + +export default UpdateState; From 511cf405409f5b233fcbf3832e21d3ecd1c63b34 Mon Sep 17 00:00:00 2001 From: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> Date: Fri, 9 May 2025 07:21:52 -0300 Subject: [PATCH 10/12] Add EnsureAPIAvailability component Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> --- .../src/app/EnsureAPIAvailability.tsx | 22 +++++++++++++++++++ .../src/app/context/NotebookContext.tsx | 3 ++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 workspaces/frontend/src/app/EnsureAPIAvailability.tsx diff --git a/workspaces/frontend/src/app/EnsureAPIAvailability.tsx b/workspaces/frontend/src/app/EnsureAPIAvailability.tsx new file mode 100644 index 000000000..53dbeeaf2 --- /dev/null +++ b/workspaces/frontend/src/app/EnsureAPIAvailability.tsx @@ -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 = ({ children }) => { + const { apiAvailable } = useNotebookAPI(); + if (!apiAvailable) { + return ( + + + + ); + } + + return <>{children}; +}; + +export default EnsureAPIAvailability; diff --git a/workspaces/frontend/src/app/context/NotebookContext.tsx b/workspaces/frontend/src/app/context/NotebookContext.tsx index 34787e47b..df17db283 100644 --- a/workspaces/frontend/src/app/context/NotebookContext.tsx +++ b/workspaces/frontend/src/app/context/NotebookContext.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { ReactNode } from 'react'; import { BFF_API_VERSION } from '~/app/const'; +import EnsureAPIAvailability from '~/app/EnsureAPIAvailability'; import useNotebookAPIState, { NotebookAPIState } from './useNotebookAPIState'; export type NotebookContextType = { @@ -33,7 +34,7 @@ export const NotebookContextProvider: React.FC = ( [apiState, refreshAPIState], )} > - {children} + {children} ); }; From cc77caec5024e1b77c81df9ef20ee7cb377f4561 Mon Sep 17 00:00:00 2001 From: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> Date: Mon, 12 May 2025 10:42:07 -0300 Subject: [PATCH 11/12] Add instructions for running the UI with a mocked API Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> --- workspaces/frontend/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/workspaces/frontend/README.md b/workspaces/frontend/README.md index 7e5ed38d3..b43fbf1d2 100644 --- a/workspaces/frontend/README.md +++ b/workspaces/frontend/README.md @@ -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: From faf12c96a8fc7efc882a1e1e4baa129990098243 Mon Sep 17 00:00:00 2001 From: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> Date: Mon, 12 May 2025 13:44:21 +0000 Subject: [PATCH 12/12] Enable create workspace Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> --- .../src/app/hooks/useGenericObjectState.ts | 34 +++++ .../Workspaces/Creation/WorkspaceCreation.tsx | 139 ++++++++++++++---- .../WorkspaceCreationPropertiesSecrets.tsx | 50 ++++--- .../WorkspaceCreationPropertiesSelection.tsx | 66 ++++----- .../WorkspaceCreationPropertiesVolumes.tsx | 35 ++--- workspaces/frontend/src/app/types.ts | 37 +++-- .../src/shared/api/backendApiTypes.ts | 8 +- 7 files changed, 239 insertions(+), 130 deletions(-) create mode 100644 workspaces/frontend/src/app/hooks/useGenericObjectState.ts diff --git a/workspaces/frontend/src/app/hooks/useGenericObjectState.ts b/workspaces/frontend/src/app/hooks/useGenericObjectState.ts new file mode 100644 index 000000000..7c6098aaa --- /dev/null +++ b/workspaces/frontend/src/app/hooks/useGenericObjectState.ts @@ -0,0 +1,34 @@ +import * as React from 'react'; + +export type UpdateObjectAtPropAndValue = ( + propKey: K, + propValue: T[K], +) => void; + +export type GenericObjectState = [ + data: T, + setData: UpdateObjectAtPropAndValue, + resetDefault: () => void, +]; + +const useGenericObjectState = (defaultData: T | (() => T)): GenericObjectState => { + const [value, setValue] = React.useState(defaultData); + + const setPropValue = React.useCallback>((propKey, propValue) => { + setValue((oldValue) => { + if (oldValue[propKey] !== propValue) { + return { ...oldValue, [propKey]: propValue }; + } + return oldValue; + }); + }, []); + + const defaultDataRef = React.useRef(value); + const resetToDefault = React.useCallback(() => { + setValue(defaultDataRef.current); + }, []); + + return [value, setPropValue, resetToDefault]; +}; + +export default useGenericObjectState; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx b/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx index 0362cce16..2d1a37a8e 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Creation/WorkspaceCreation.tsx @@ -12,18 +12,17 @@ import { StackItem, } from '@patternfly/react-core'; import { CheckIcon } from '@patternfly/react-icons'; -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { useNamespaceContext } from '~/app/context/NamespaceContextProvider'; +import useGenericObjectState from '~/app/hooks/useGenericObjectState'; +import { useNotebookAPI } from '~/app/hooks/useNotebookAPI'; import { WorkspaceCreationImageSelection } from '~/app/pages/Workspaces/Creation/image/WorkspaceCreationImageSelection'; import { WorkspaceCreationKindSelection } from '~/app/pages/Workspaces/Creation/kind/WorkspaceCreationKindSelection'; -import { WorkspaceCreationPropertiesSelection } from '~/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection'; import { WorkspaceCreationPodConfigSelection } from '~/app/pages/Workspaces/Creation/podConfig/WorkspaceCreationPodConfigSelection'; -import { - WorkspaceImageConfigValue, - WorkspaceKind, - WorkspacePodConfigValue, -} from '~/shared/api/backendApiTypes'; -import { WorkspaceProperties } from '~/app/types'; +import { WorkspaceCreationPropertiesSelection } from '~/app/pages/Workspaces/Creation/properties/WorkspaceCreationPropertiesSelection'; +import { WorkspaceCreateFormData } from '~/app/types'; +import { WorkspaceCreate } from '~/shared/api/backendApiTypes'; enum WorkspaceCreationSteps { KindSelection, @@ -34,12 +33,24 @@ enum WorkspaceCreationSteps { const WorkspaceCreation: React.FunctionComponent = () => { const navigate = useNavigate(); + const { api } = useNotebookAPI(); + const { selectedNamespace } = useNamespaceContext(); + const [isSubmitting, setIsSubmitting] = React.useState(false); const [currentStep, setCurrentStep] = useState(WorkspaceCreationSteps.KindSelection); - const [selectedKind, setSelectedKind] = useState(); - const [selectedImage, setSelectedImage] = useState(); - const [selectedPodConfig, setSelectedPodConfig] = useState(); - const [, setSelectedProperties] = useState(); + + const [data, setData, resetData] = useGenericObjectState({ + kind: undefined, + image: undefined, + podConfig: undefined, + properties: { + deferUpdates: false, + homeDirectory: '', + volumes: [], + secrets: [], + workspaceName: '', + }, + }); const getStepVariant = useCallback( (step: WorkspaceCreationSteps) => { @@ -62,17 +73,68 @@ const WorkspaceCreation: React.FunctionComponent = () => { setCurrentStep(currentStep + 1); }, [currentStep]); + const canGoToPreviousStep = useMemo(() => currentStep > 0, [currentStep]); + + const canGoToNextStep = useMemo( + () => currentStep < Object.keys(WorkspaceCreationSteps).length / 2 - 1, + [currentStep], + ); + + const canSubmit = useMemo( + () => !isSubmitting && !canGoToNextStep, + [canGoToNextStep, isSubmitting], + ); + + const handleCreate = useCallback(() => { + // TODO: properly validate data before submitting + if (!data.kind || !data.image || !data.podConfig) { + return; + } + + const workspaceCreate: WorkspaceCreate = { + name: data.properties.workspaceName, + kind: data.kind.name, + deferUpdates: data.properties.deferUpdates, + paused: false, + podTemplate: { + podMetadata: { + labels: {}, + annotations: {}, + }, + options: { + imageConfig: data.image.id, + podConfig: data.podConfig.id, + }, + volumes: { + home: data.properties.homeDirectory, + data: data.properties.volumes, + secrets: data.properties.secrets, + }, + }, + }; + + setIsSubmitting(true); + + api + .createWorkspace({}, selectedNamespace, { data: workspaceCreate }) + .then((newWorkspace) => { + // TODO: alert user about success + console.info('New workspace created:', JSON.stringify(newWorkspace)); + navigate('/workspaces'); + }) + .catch((err) => { + // TODO: alert user about error + console.error('Error creating workspace:', err); + }) + .finally(() => { + setIsSubmitting(false); + }); + }, [api, data, navigate, selectedNamespace]); + const cancel = useCallback(() => { navigate('/workspaces'); }, [navigate]); - const onSelectWorkspaceKind = useCallback((newWorkspaceKind: WorkspaceKind | undefined) => { - setSelectedKind(newWorkspaceKind); - setSelectedImage(undefined); - setSelectedPodConfig(undefined); - setSelectedProperties(undefined); - }, []); - return ( <> @@ -157,26 +219,33 @@ const WorkspaceCreation: React.FunctionComponent = () => { {currentStep === WorkspaceCreationSteps.KindSelection && ( { + resetData(); + setData('kind', kind); + }} /> )} {currentStep === WorkspaceCreationSteps.ImageSelection && ( setData('image', image)} + images={data.kind?.podTemplate.options.imageConfig.values ?? []} /> )} {currentStep === WorkspaceCreationSteps.PodConfigSelection && ( setData('podConfig', podConfig)} + podConfigs={data.kind?.podTemplate.options.podConfig.values ?? []} /> )} {currentStep === WorkspaceCreationSteps.Properties && ( - + setData('properties', properties)} + selectedImage={data.image} + /> )} @@ -186,7 +255,7 @@ const WorkspaceCreation: React.FunctionComponent = () => { variant="primary" ouiaId="Primary" onClick={previousStep} - isDisabled={currentStep === 0} + isDisabled={!canGoToPreviousStep} > Previous @@ -196,11 +265,21 @@ const WorkspaceCreation: React.FunctionComponent = () => { variant="primary" ouiaId="Primary" onClick={nextStep} - isDisabled={currentStep === Object.keys(WorkspaceCreationSteps).length / 2 - 1} + isDisabled={!canGoToNextStep} > Next + + +