Skip to content

Commit fac8b31

Browse files
authored
✨ Upgrade sidepanel extension to JupyterLab 4.x compatibility (#34495)
* Upgrade package from JupyterLab 3.x to 4.x * Update CHANGES.md * Upgrade to adapt React 18 * Update testsuite dependencies to 4.x * Update sidepanel testsuite to adapt React 18+ * Add jest.setup.js to enable async steps * Add license to jest.setup.js * Pass eslint * Fix eslint * Use waitFor() instead of setTimeout * Use "as Element" instead of ": Element" in somewhere that can improve efficiency. * Update CHANGES.md * Update README.md
1 parent e27c927 commit fac8b31

File tree

24 files changed

+487
-288
lines changed

24 files changed

+487
-288
lines changed

.github/gh-actions-self-hosted-runners/arc/images/Dockerfile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ RUN docker buildx install && docker buildx version
2525

2626
USER root
2727
#Install Node
28-
RUN curl -OL https://nodejs.org/dist/v18.16.0/node-v18.16.0-linux-x64.tar.xz && \
29-
tar -C /usr/local -xf node-v18.16.0-linux-x64.tar.xz && \
30-
rm node-v18.16.0-linux-x64.tar.xz && \
31-
mv /usr/local/node-v18.16.0-linux-x64 /usr/local/node
28+
RUN curl -OL https://nodejs.org/dist/v22.14.0/node-v22.14.0-linux-x64.tar.xz && \
29+
tar -C /usr/local -xf node-v22.14.0-linux-x64.tar.xz && \
30+
rm node-v22.14.0-linux-x64.tar.xz && \
31+
mv /usr/local/node-v22.14.0-linux-x64 /usr/local/node
3232
ENV PATH="${PATH}:/usr/local/node/bin"
3333
#Install Go
3434
ARG go_version=1.24.0

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,13 @@
7777

7878
* X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)).
7979
* [YAML] WriteToTFRecord and ReadFromTFRecord Beam YAML support
80+
* Python: Added JupyterLab 4.x extension compatibility for enhanced notebook integration ([#34495](https://github.com/apache/beam/pull/34495)).
8081

8182
## Breaking Changes
8283

8384
* X behavior was changed ([#X](https://github.com/apache/beam/issues/X)).
8485
* Yapf version upgraded to 0.43.0 for formatting (Python) ([#34801](https://github.com/apache/beam/pull/34801/)).
86+
* Python: Added JupyterLab 4.x extension compatibility for enhanced notebook integration ([#34495](https://github.com/apache/beam/pull/34495)).
8587

8688
## Deprecations
8789

sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ Includes two different side panels:
2525

2626
| JupyterLab version | Extension version |
2727
| ------------------ | ----------------- |
28-
| v3 | >=v2.0.0 |
28+
| v4 | >=v4.0.0 |
29+
| v3 | v2.0.0-v3.0.0 |
2930
| v2 | v1.0.0 |
3031

3132
## Install

sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/jest.config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,7 @@ module.exports = {
2828
// Use identity-obj-proxy to load css and less files in tests.
2929
"moduleNameMapper": {
3030
"\\.(css|less)$": "identity-obj-proxy"
31-
}
31+
},
32+
"testEnvironment": "jsdom",
33+
"setupFilesAfterEnv": ['<rootDir>/jest.setup.js']
3234
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
/**
20+
* Configures jest async performance.
21+
*/
22+
23+
const { configure } = require('@testing-library/react');
24+
require('@testing-library/jest-dom');
25+
26+
configure({
27+
asyncUtilTimeout: 5000,
28+
react: { version: 'detect' }
29+
});
30+
31+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;

sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/package.json

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "apache-beam-jupyterlab-sidepanel",
3-
"version": "3.0.0",
3+
"version": "4.0.0",
44
"description": "A side panel providing information and controls to run Apache Beam notebooks interactively.",
55
"keywords": [
66
"jupyter",
@@ -43,38 +43,47 @@
4343
"watch:src": "tsc -w"
4444
},
4545
"dependencies": {
46-
"@jupyterlab/application": "^3.1.17",
47-
"@jupyterlab/launcher": "^3.1.17",
48-
"@jupyterlab/mainmenu": "^3.1.17",
49-
"@rmwc/button": "^6.1.3",
50-
"@rmwc/fab": "^6.1.4",
51-
"@rmwc/data-table": "^6.0.14",
52-
"@rmwc/dialog": "^7.0.2",
53-
"@rmwc/drawer": "^6.0.14",
54-
"@rmwc/list": "^6.1.3",
55-
"@rmwc/textfield": "^6.1.4",
56-
"@rmwc/tooltip": "^6.1.4",
57-
"@rmwc/top-app-bar": "^6.1.3",
58-
"material-design-icons": "^3.0.1"
46+
"@jupyterlab/application": "^4.3.6",
47+
"@jupyterlab/launcher": "^4.3.6",
48+
"@jupyterlab/mainmenu": "^4.3.6",
49+
"@lumino/widgets": "^2.2.1",
50+
"@rmwc/base": "^14.0.0",
51+
"@rmwc/button": "^8.0.6",
52+
"@rmwc/data-table": "^8.0.6",
53+
"@rmwc/dialog": "^8.0.6",
54+
"@rmwc/drawer": "^8.0.6",
55+
"@rmwc/fab": "^8.0.6",
56+
"@rmwc/list": "^8.0.6",
57+
"@rmwc/ripple": "^14.0.0",
58+
"@rmwc/textfield": "^8.0.6",
59+
"@rmwc/tooltip": "^8.0.6",
60+
"@rmwc/top-app-bar": "^8.0.6",
61+
"material-design-icons": "^3.0.1",
62+
"react": "^18.2.0",
63+
"react-dom": "^18.2.0"
5964
},
6065
"devDependencies": {
61-
"@jupyterlab/builder": "^3.1.0",
62-
"@types/jest": "^26.0.7",
63-
"@types/react-dom": "^16.9.8",
64-
"@typescript-eslint/eslint-plugin": "^4.8.1",
65-
"@typescript-eslint/parser": "^4.8.1",
66-
"eslint": "^7.14.0",
67-
"eslint-config-prettier": "^6.15.0",
68-
"eslint-plugin-prettier": "^3.1.4",
69-
"eslint-plugin-react": "^7.20.5",
66+
"@jupyterlab/builder": "^4.3.6",
67+
"@testing-library/dom": "^9.3.0",
68+
"@testing-library/jest-dom": "^6.1.4",
69+
"@testing-library/react": "^14.0.0",
70+
"@types/jest": "^29.5.14",
71+
"@types/react": "^18.2.0",
72+
"@types/react-dom": "^18.2.0",
73+
"@typescript-eslint/eslint-plugin": "^7.3.1",
74+
"@typescript-eslint/parser": "^7.3.1",
75+
"eslint": "^8.56.0",
76+
"eslint-config-prettier": "^9.1.0",
77+
"eslint-plugin-prettier": "^5.1.3",
78+
"eslint-plugin-react": "^7.33.2",
7079
"identity-obj-proxy": "^3.0.0",
71-
"jest": "^26.1.0",
80+
"jest": "^29.7.0",
81+
"jest-environment-jsdom": "^29.0.0",
7282
"npm-run-all": "^4.1.5",
73-
"prettier": "^2.1.1",
74-
"react-dom": "^17.0.1",
75-
"rimraf": "^3.0.2",
76-
"ts-jest": "^26.1.3",
77-
"typescript": "~4.1.3"
83+
"prettier": "^3.2.4",
84+
"rimraf": "^5.0.5",
85+
"ts-jest": "^29.1.2",
86+
"typescript": "~5.3.3"
7887
},
7988
"sideEffects": [
8089
"style/*.css",
@@ -86,6 +95,6 @@
8695
},
8796
"test": "jest",
8897
"resolutions": {
89-
"@types/react": "~16.9.16"
98+
"@types/react": "^18.2.0"
9099
}
91100
}

sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/SidePanel.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
ReactWidget,
1515
SessionContext,
1616
ISessionContext,
17-
sessionContextDialogs
17+
SessionContextDialogs
1818
} from '@jupyterlab/apputils';
1919
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
2020
import { ServiceManager } from '@jupyterlab/services';
@@ -64,7 +64,7 @@ export class SidePanel extends BoxPanel {
6464
} else {
6565
let sessionModel = sessionModelItr.next();
6666
while (sessionModel !== undefined) {
67-
if (sessionModel.kernel.id !== firstModel.kernel.id) {
67+
if (sessionModel.value.kernel.id !== firstModel.value.kernel.id) {
6868
// There is more than one unique running kernel.
6969
onlyOneUniqueKernelExists = false;
7070
break;
@@ -78,18 +78,19 @@ export class SidePanel extends BoxPanel {
7878
// kernel.
7979
if (onlyOneUniqueKernelExists) {
8080
this._sessionContext.sessionManager.connectTo({
81-
model: firstModel,
81+
model: firstModel.value,
8282
kernelConnectionOptions: {
8383
// Only one connection can handleComms. Leave it to the connection
8484
// established by the opened notebook.
8585
handleComms: false
8686
}
8787
});
8888
// Connect to the unique kernel.
89-
this._sessionContext.changeKernel(firstModel.kernel);
89+
this._sessionContext.changeKernel(firstModel.value.kernel);
9090
} else {
9191
// Let the user choose among sessions and kernels when there is no
9292
// or more than 1 running kernels.
93+
const sessionContextDialogs = new SessionContextDialogs();
9394
await sessionContextDialogs.selectKernel(this._sessionContext);
9495
}
9596
} catch (err) {

sdks/python/apache_beam/runners/interactive/extensions/apache-beam-jupyterlab-sidepanel/src/__tests__/clusters/Clusters.test.tsx

Lines changed: 50 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,42 +12,55 @@
1212

1313
import * as React from 'react';
1414

15-
import { render, unmountComponentAtNode } from 'react-dom';
15+
import { createRoot, Root } from 'react-dom/client';
1616

17-
import { act } from 'react-dom/test-utils';
17+
import { act } from 'react';
1818

1919
import { Clusters } from '../../clusters/Clusters';
2020

21+
import { waitFor } from '@testing-library/dom';
22+
2123
let container: null | Element = null;
24+
let root: Root | null = null;
2225
beforeEach(() => {
2326
container = document.createElement('div');
2427
document.body.appendChild(container);
28+
root = createRoot(container);
2529
});
2630

27-
afterEach(() => {
28-
unmountComponentAtNode(container);
29-
container.remove();
30-
container = null;
31+
afterEach(async () => {
32+
try {
33+
if (root) {
34+
await act(async () => {
35+
root.unmount();
36+
await new Promise(resolve => setTimeout(resolve, 0));
37+
});
38+
}
39+
} catch (error) {
40+
console.warn('During unmount:', error);
41+
} finally {
42+
if (container?.parentNode) {
43+
container.remove();
44+
}
45+
container = null;
46+
root = null;
47+
}
3148
});
32-
33-
it('renders info message about no clusters being available', () => {
49+
it('renders info message about no clusters being available', async () => {
3450
const clustersRef: React.RefObject<Clusters> = React.createRef<Clusters>();
35-
act(() => {
36-
render(
37-
<Clusters sessionContext={{} as any} ref={clustersRef} />,
38-
container
39-
);
51+
await act(async () => {
52+
root.render(<Clusters sessionContext={{} as any} ref={clustersRef} />);
4053
const clusters = clustersRef.current;
4154
if (clusters) {
4255
clusters.setState({ clusters: {} });
4356
}
4457
});
45-
const infoElement: Element = container.firstElementChild;
58+
const infoElement = container.firstElementChild as Element;
4659
expect(infoElement.tagName).toBe('DIV');
4760
expect(infoElement.textContent).toBe('No clusters detected.');
4861
});
4962

50-
it('renders a data-table', () => {
63+
it('renders a data-table', async () => {
5164
const clustersRef: React.RefObject<Clusters> = React.createRef<Clusters>();
5265
const testData = {
5366
key: {
@@ -59,17 +72,19 @@ it('renders a data-table', () => {
5972
dashboard: 'test-dashboard'
6073
}
6174
};
62-
act(() => {
63-
render(
64-
<Clusters sessionContext={{} as any} ref={clustersRef} />,
65-
container
66-
);
67-
const clusters = clustersRef.current;
68-
if (clusters) {
69-
clusters.setState({ clusters: testData });
70-
}
75+
await act(async () => {
76+
root.render(<Clusters sessionContext={{} as any} ref={clustersRef} />);
77+
});
78+
79+
await act(async () => {
80+
clustersRef.current?.setState({ clusters: testData });
7181
});
72-
const topAppBarHeader: Element = container.firstElementChild;
82+
83+
await waitFor(() =>
84+
expect(container.querySelector('.mdc-data-table__table')).toBeTruthy()
85+
);
86+
87+
const topAppBarHeader = container.firstElementChild as Element;
7388
expect(topAppBarHeader.tagName).toBe('HEADER');
7489
expect(topAppBarHeader.getAttribute('class')).toContain('mdc-top-app-bar');
7590
expect(topAppBarHeader.getAttribute('class')).toContain(
@@ -79,42 +94,42 @@ it('renders a data-table', () => {
7994
'mdc-top-app-bar--dense'
8095
);
8196
expect(topAppBarHeader.innerHTML).toContain('Clusters [kernel:no kernel]');
82-
const topAppBarFixedAdjust: Element = container.children[1];
97+
const topAppBarFixedAdjust = container.children[1] as Element;
8398
expect(topAppBarFixedAdjust.tagName).toBe('DIV');
8499
expect(topAppBarFixedAdjust.getAttribute('class')).toContain(
85100
'mdc-top-app-bar--fixed-adjust'
86101
);
87-
const selectBar: Element = container.children[2];
102+
const selectBar = container.children[2] as Element;
88103
expect(selectBar.tagName).toBe('DIV');
89104
expect(selectBar.getAttribute('class')).toContain('mdc-select');
90-
const dialogBox: Element = container.children[3];
105+
const dialogBox = container.children[3] as Element;
91106
expect(dialogBox.tagName).toBe('DIV');
92107
expect(dialogBox.getAttribute('class')).toContain('mdc-dialog');
93-
const clustersComponent: Element = container.children[4];
108+
const clustersComponent = container.children[4] as Element;
94109
expect(clustersComponent.tagName).toBe('DIV');
95110
expect(clustersComponent.getAttribute('class')).toContain('Clusters');
96-
const dataTableDiv: Element = clustersComponent.children[0];
111+
const dataTableDiv = clustersComponent.children[0] as Element;
97112
expect(dataTableDiv.tagName).toBe('DIV');
98113
expect(dataTableDiv.getAttribute('class')).toContain('mdc-data-table');
99-
const dataTable: Element = dataTableDiv.children[0];
114+
const dataTable = dataTableDiv.children[0].firstElementChild as Element;
100115
expect(dataTable.tagName).toBe('TABLE');
101116
expect(dataTable.getAttribute('class')).toContain('mdc-data-table__table');
102-
const dataTableHead: Element = dataTable.children[0];
117+
const dataTableHead = dataTable.children[0] as Element;
103118
expect(dataTableHead.tagName).toBe('THEAD');
104119
expect(dataTableHead.getAttribute('class')).toContain(
105120
'rmwc-data-table__head'
106121
);
107-
const dataTableHeaderRow: Element = dataTableHead.children[0];
122+
const dataTableHeaderRow = dataTableHead.children[0] as Element;
108123
expect(dataTableHeaderRow.tagName).toBe('TR');
109124
expect(dataTableHeaderRow.getAttribute('class')).toContain(
110125
'mdc-data-table__header-row'
111126
);
112-
const dataTableBody: Element = dataTable.children[1];
127+
const dataTableBody = dataTable.children[1] as Element;
113128
expect(dataTableBody.tagName).toBe('TBODY');
114129
expect(dataTableBody.getAttribute('class')).toContain(
115130
'mdc-data-table__content'
116131
);
117-
const dataTableBodyRow: Element = dataTableBody.children[0];
132+
const dataTableBodyRow = dataTableBody.children[0] as Element;
118133
expect(dataTableBodyRow.tagName).toBe('TR');
119134
expect(dataTableBodyRow.getAttribute('class')).toContain(
120135
'mdc-data-table__row'

0 commit comments

Comments
 (0)