Skip to content

Commit 09f8f37

Browse files
caponettopaulovmr
andauthored
feat(ws): add WorkspaceKindSummary page and other improvements around it (#415)
* Minor refactorings and initial work for the Workspace Kind summary page Signed-off-by: Guilherme Caponetto <[email protected]> * feat(ws): added links from workspace kind details drawer to workspace kinds details page (#1) Signed-off-by: Paulo Rego <[email protected]> * Enable workspace filtering by namespace in the WorkspaceKind summary page Signed-off-by: Guilherme Caponetto <[email protected]> * Update Pause/Start action response types according to backend Signed-off-by: Guilherme Caponetto <[email protected]> * Fix WorkspaceKind logo href Signed-off-by: Guilherme Caponetto <[email protected]> * Replace placeholders for GPU data with real values in WorkspaceKind summary page Signed-off-by: Guilherme Caponetto <[email protected]> * Allow columns to be hidden in the WorkspaceTable Signed-off-by: Guilherme Caponetto <[email protected]> * feat(ws): added links from workspace kind details drawer namespace tab to workspace kinds details page (#2) Signed-off-by: Paulo Rego <[email protected]> * Improve types around Filter component Signed-off-by: Guilherme Caponetto <[email protected]> * feat: Add Workspace Actions Context and related components - Introduced WorkspaceActionsContext to manage workspace actions such as view, edit, delete, start, restart, and stop. - Created WorkspaceActionsContextProvider to encapsulate the context logic and provide it to child components. - Implemented WorkspaceKindSummary and Workspaces components to utilize the new context for handling workspace actions. - Added polling for refreshing workspaces at a default interval. - Enhanced WorkspaceTable to support row actions for workspaces. - Updated various components to include sortable and filterable data fields. - Refactored WorkspaceStartActionModal and WorkspaceStopActionModal to handle optional onActionDone callback. - Added loading and error handling components for better user experience. Signed-off-by: Guilherme Caponetto <[email protected]> * feat: Add buildWorkspaceList function and integrate into mockAllWorkspaces Signed-off-by: Guilherme Caponetto <[email protected]> * refactor: Update mock data and formatting for workspace activity timestamps Signed-off-by: Guilherme Caponetto <[email protected]> * feat: Implement usePolling hook and refactor workspace actions in Workspaces and WorkspaceKindSummary components Signed-off-by: Guilherme Caponetto <[email protected]> * refactor: Update column key usage in ExpandedWorkspaceRow and adjust workspace actions visibility in Workspaces component Signed-off-by: Guilherme Caponetto <[email protected]> * Make mocked workspace list deterministic Signed-off-by: Guilherme Caponetto <[email protected]> * feat: Enhance WorkspaceTable with additional columns and filtering capabilities - Added 'namespace', 'gpu', and 'idleGpu' columns to WorkspaceTable. - Updated filtering logic to support new columns in WorkspaceTable. - Refactored useWorkspaces hook to remove unnecessary parameters related to idle and GPU filtering. - Modified WorkspaceKindSummary and its expandable card to utilize new filtering functionality. - Updated WorkspaceUtils to include a method for formatting workspace idle state. - Adjusted Filter component to support generic filtered column types. - Updated Workspaces page to hide new columns as needed. Signed-off-by: Guilherme Caponetto <[email protected]> * refactor: Improve sorting functionality in WorkspaceTable by utilizing specific types for sortable columns Signed-off-by: Guilherme Caponetto <[email protected]> * Adjustments after rebase Signed-off-by: Guilherme Caponetto <[email protected]> * Format with prettier Signed-off-by: Guilherme Caponetto <[email protected]> --------- Signed-off-by: Guilherme Caponetto <[email protected]> Signed-off-by: Paulo Rego <[email protected]> Co-authored-by: Paulo Rego <[email protected]>
1 parent 6f12fa7 commit 09f8f37

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+2777
-1295
lines changed

workspaces/frontend/.eslintrc.js

Lines changed: 216 additions & 215 deletions
Large diffs are not rendered by default.

workspaces/frontend/eslint-local-rules/no-react-hook-namespace.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,24 @@ module.exports = {
1111
},
1212
create(context) {
1313
const hooks = new Set([
14-
'useState', 'useEffect', 'useContext', 'useReducer',
15-
'useCallback', 'useMemo', 'useRef', 'useLayoutEffect',
16-
'useImperativeHandle', 'useDebugValue', 'useDeferredValue',
17-
'useTransition', 'useId', 'useSyncExternalStore',
14+
'useState',
15+
'useEffect',
16+
'useContext',
17+
'useReducer',
18+
'useCallback',
19+
'useMemo',
20+
'useRef',
21+
'useLayoutEffect',
22+
'useImperativeHandle',
23+
'useDebugValue',
24+
'useDeferredValue',
25+
'useTransition',
26+
'useId',
27+
'useSyncExternalStore',
1828
]);
1929
return {
2030
MemberExpression(node) {
21-
if (
22-
node.object?.name === 'React' &&
23-
hooks.has(node.property?.name)
24-
) {
31+
if (node.object?.name === 'React' && hooks.has(node.property?.name)) {
2532
context.report({
2633
node,
2734
messageId: 'avoidNamespaceHook',

workspaces/frontend/package-lock.json

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

workspaces/frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
"@patternfly/react-icons": "^6.2.0",
104104
"@patternfly/react-styles": "^6.2.0",
105105
"@patternfly/react-table": "^6.2.0",
106+
"@patternfly/react-tokens": "^6.2.0",
106107
"@types/js-yaml": "^4.0.9",
107108
"date-fns": "^4.1.0",
108109
"js-yaml": "^4.1.0",

workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/application.cy.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@ import { pageNotfound } from '~/__tests__/cypress/cypress/pages/pageNotFound';
22
import { home } from '~/__tests__/cypress/cypress/pages/home';
33
import { mockNamespaces } from '~/__mocks__/mockNamespaces';
44
import { mockBFFResponse } from '~/__mocks__/utils';
5+
import { mockWorkspace1 } from '~/shared/mock/mockNotebookServiceData';
56

67
describe('Application', () => {
78
beforeEach(() => {
89
// Mock the namespaces API response
910
cy.intercept('GET', '/api/v1/namespaces', {
1011
body: mockBFFResponse(mockNamespaces),
1112
}).as('getNamespaces');
13+
cy.intercept('GET', `/api/v1/workspaces/${mockNamespaces[0].name}`, {
14+
body: mockBFFResponse({ mockWorkspace1 }),
15+
}).as('getWorkspaces');
1216
cy.visit('/');
1317
cy.wait('@getNamespaces');
18+
cy.wait('@getWorkspaces');
1419
});
1520

1621
it('Page not found should render', () => {

workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspace.mock.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,17 @@ const generateMockWorkspace = (
1212
podConfigDisplayName: string,
1313
pvcName: string,
1414
): Workspace => {
15-
const currentTime = Date.now();
16-
const lastActivityTime = currentTime - Math.floor(Math.random() * 1000000);
17-
const lastUpdateTime = currentTime - Math.floor(Math.random() * 100000);
15+
const pausedTime = new Date(2025, 0, 1).getTime();
16+
const lastActivityTime = new Date(2025, 0, 2).getTime();
17+
const lastUpdateTime = new Date(2025, 0, 3).getTime();
1818

1919
return {
2020
name,
2121
namespace,
2222
workspaceKind: { name: 'jupyterlab' } as WorkspaceKindInfo,
2323
deferUpdates: paused,
2424
paused,
25-
pausedTime: paused ? currentTime - Math.floor(Math.random() * 1000000) : 0,
25+
pausedTime,
2626
pendingRestart: Math.random() < 0.5, //to generate randomly True/False value
2727
state,
2828
stateMessage:

workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/WorkspaceDetailsActivity.cy.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { mockBFFResponse } from '~/__mocks__/utils';
22
import { mockWorkspaces } from '~/__tests__/cypress/cypress/tests/mocked/workspace.mock';
3-
import { formatTimestamp } from '~/shared/utilities/WorkspaceUtils';
43

54
describe('WorkspaceDetailsActivity Component', () => {
65
beforeEach(() => {
@@ -19,22 +18,16 @@ describe('WorkspaceDetailsActivity Component', () => {
1918
throw new Error('Intercepted response is undefined or empty');
2019
}
2120
const workspace = interception.response.body.data[0];
22-
cy.findByTestId('action-view-details').click();
21+
cy.findByTestId('action-viewDetails').click();
2322
cy.findByTestId('activityTab').click();
2423
cy.findByTestId('lastActivity')
2524
.invoke('text')
2625
.then((text) => {
2726
console.log('Rendered lastActivity:', text);
2827
});
29-
cy.findByTestId('lastActivity').should(
30-
'have.text',
31-
formatTimestamp(workspace.activity.lastActivity),
32-
);
33-
cy.findByTestId('lastUpdate').should(
34-
'have.text',
35-
formatTimestamp(workspace.activity.lastUpdate),
36-
);
37-
cy.findByTestId('pauseTime').should('have.text', formatTimestamp(workspace.pausedTime));
28+
cy.findByTestId('pauseTime').should('have.text', 'Jan 1, 2025, 12:00:00 AM');
29+
cy.findByTestId('lastActivity').should('have.text', 'Jan 2, 2025, 12:00:00 AM');
30+
cy.findByTestId('lastUpdate').should('have.text', 'Jan 3, 2025, 12:00:00 AM');
3831
cy.findByTestId('pendingRestart').should(
3932
'have.text',
4033
workspace.pendingRestart ? 'Yes' : 'No',

workspaces/frontend/src/__tests__/cypress/cypress/tests/mocked/workspaces/filterWorkspacesTest.cy.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { mockWorkspaces } from '~/__mocks__/mockWorkspaces';
33
import { mockBFFResponse } from '~/__mocks__/utils';
44
import { home } from '~/__tests__/cypress/cypress/pages/home';
55

6-
const useFilter = (filterName: string, searchValue: string) => {
6+
const useFilter = (filterKey: string, filterName: string, searchValue: string) => {
77
cy.get("[id$='filter-workspaces-dropdown']").click();
8-
cy.get(`[id$='filter-workspaces-dropdown-${filterName}']`).click();
8+
cy.get(`[id$='filter-workspaces-dropdown-${filterKey}']`).click();
99
cy.get("[data-testid='filter-workspaces-search-input']").type(searchValue);
1010
cy.get("[class$='pf-v6-c-toolbar__group']").contains(filterName);
1111
cy.get("[class$='pf-v6-c-toolbar__group']").contains(searchValue);
@@ -23,24 +23,24 @@ describe('Application', () => {
2323
});
2424
it('filter rows with single filter', () => {
2525
home.visit();
26-
useFilter('Name', 'My');
26+
useFilter('name', 'Name', 'My');
2727
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
2828
cy.get("[id$='workspaces-table-row-1']").contains('My First Jupyter Notebook');
2929
cy.get("[id$='workspaces-table-row-2']").contains('My Second Jupyter Notebook');
3030
});
3131

3232
it('filter rows with multiple filters', () => {
3333
home.visit();
34-
useFilter('Name', 'My');
35-
useFilter('Pod Config', 'Tiny');
34+
useFilter('name', 'Name', 'My');
35+
useFilter('podConfig', 'Pod Config', 'Tiny');
3636
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 1);
3737
cy.get("[id$='workspaces-table-row-1']").contains('My First Jupyter Notebook');
3838
});
3939

4040
it('filter rows with multiple filters and remove one', () => {
4141
home.visit();
42-
useFilter('Name', 'My');
43-
useFilter('Pod Config', 'Tiny');
42+
useFilter('name', 'Name', 'My');
43+
useFilter('podConfig', 'Pod Config', 'Tiny');
4444
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 1);
4545
cy.get("[id$='workspaces-table-row-1']").contains('My First Jupyter Notebook');
4646
cy.get("[class$='pf-v6-c-label-group__close']").eq(1).click();
@@ -52,8 +52,8 @@ describe('Application', () => {
5252

5353
it('filter rows with multiple filters and remove all', () => {
5454
home.visit();
55-
useFilter('Name', 'My');
56-
useFilter('Pod Config', 'Tiny');
55+
useFilter('name', 'Name', 'My');
56+
useFilter('podConfig', 'Pod Config', 'Tiny');
5757
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 1);
5858
cy.get("[id$='workspaces-table-row-1']").contains('My First Jupyter Notebook');
5959
cy.get('*').contains('Clear all filters').click();

workspaces/frontend/src/app/AppRoutes.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import React from 'react';
22
import { Route, Routes, Navigate } from 'react-router-dom';
33
import { AppRoutePaths } from '~/app/routes';
4+
import { WorkspaceKindSummaryWrapper } from '~/app/pages/WorkspaceKinds/summary/WorkspaceKindSummaryWrapper';
45
import { WorkspaceForm } from '~/app/pages/Workspaces/Form/WorkspaceForm';
5-
import { NotFound } from './pages/notFound/NotFound';
66
import { Debug } from './pages/Debug/Debug';
7-
import { Workspaces } from './pages/Workspaces/Workspaces';
8-
import '~/shared/style/MUI-theme.scss';
7+
import { NotFound } from './pages/notFound/NotFound';
98
import { WorkspaceKinds } from './pages/WorkspaceKinds/WorkspaceKinds';
9+
import { WorkspacesWrapper } from './pages/Workspaces/WorkspacesWrapper';
10+
import '~/shared/style/MUI-theme.scss';
1011

1112
export const isNavDataGroup = (navItem: NavDataItem): navItem is NavDataGroup =>
1213
'children' in navItem;
@@ -62,7 +63,8 @@ const AppRoutes: React.FC = () => {
6263
<Routes>
6364
<Route path={AppRoutePaths.workspaceCreate} element={<WorkspaceForm />} />
6465
<Route path={AppRoutePaths.workspaceEdit} element={<WorkspaceForm />} />
65-
<Route path={AppRoutePaths.workspaces} element={<Workspaces />} />
66+
<Route path={AppRoutePaths.workspaces} element={<WorkspacesWrapper />} />
67+
<Route path={AppRoutePaths.workspaceKindSummary} element={<WorkspaceKindSummaryWrapper />} />
6668
<Route path={AppRoutePaths.workspaceKinds} element={<WorkspaceKinds />} />
6769
<Route path="/" element={<Navigate to={AppRoutePaths.workspaces} replace />} />
6870
<Route path="*" element={<NotFound />} />
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as React from 'react';
2+
import { Alert, Bullseye } from '@patternfly/react-core';
3+
4+
interface LoadErrorProps {
5+
error: Error;
6+
}
7+
8+
// TODO: simple LoadError component -- we should improve this later
9+
10+
export const LoadError: React.FC<LoadErrorProps> = ({ error }) => (
11+
<Bullseye>
12+
<Alert variant="danger" title="Error loading data">
13+
Error details: {error.message}
14+
</Alert>
15+
</Bullseye>
16+
);

0 commit comments

Comments
 (0)