Skip to content

✨ Upgrade sidepanel extension to JupyterLab 4.x compatibility #34495

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/gh-actions-self-hosted-runners/arc/images/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ RUN docker buildx install && docker buildx version

USER root
#Install Node
RUN curl -OL https://nodejs.org/dist/v18.16.0/node-v18.16.0-linux-x64.tar.xz && \
tar -C /usr/local -xf node-v18.16.0-linux-x64.tar.xz && \
rm node-v18.16.0-linux-x64.tar.xz && \
mv /usr/local/node-v18.16.0-linux-x64 /usr/local/node
RUN curl -OL https://nodejs.org/dist/v22.14.0/node-v22.14.0-linux-x64.tar.xz && \
tar -C /usr/local -xf node-v22.14.0-linux-x64.tar.xz && \
rm node-v22.14.0-linux-x64.tar.xz && \
mv /usr/local/node-v22.14.0-linux-x64 /usr/local/node
ENV PATH="${PATH}:/usr/local/node/bin"
#Install Go
ARG go_version=1.24.0
Expand Down
2 changes: 1 addition & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@

## New Features / Improvements

* X feature added (Java/Python) ([#X](https://github.com/apache/beam/issues/X)).
* Python: Added JupyterLab 4.x extension compatibility for enhanced notebook integration ([#34495](https://github.com/apache/beam/pull/34495)).

## Breaking Changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,7 @@ module.exports = {
// Use identity-obj-proxy to load css and less files in tests.
"moduleNameMapper": {
"\\.(css|less)$": "identity-obj-proxy"
}
},
"testEnvironment": "jsdom",
"setupFilesAfterEnv": ['<rootDir>/jest.setup.js']
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* Configures jest async performance.
*/

const { configure } = require('@testing-library/react');
require('@testing-library/jest-dom');

configure({
asyncUtilTimeout: 5000,
react: { version: 'detect' }
});

globalThis.IS_REACT_ACT_ENVIRONMENT = true;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apache-beam-jupyterlab-sidepanel",
"version": "3.0.0",
"version": "4.0.0",
"description": "A side panel providing information and controls to run Apache Beam notebooks interactively.",
"keywords": [
"jupyter",
Expand Down Expand Up @@ -43,38 +43,47 @@
"watch:src": "tsc -w"
},
"dependencies": {
"@jupyterlab/application": "^3.1.17",
"@jupyterlab/launcher": "^3.1.17",
"@jupyterlab/mainmenu": "^3.1.17",
"@rmwc/button": "^6.1.3",
"@rmwc/fab": "^6.1.4",
"@rmwc/data-table": "^6.0.14",
"@rmwc/dialog": "^7.0.2",
"@rmwc/drawer": "^6.0.14",
"@rmwc/list": "^6.1.3",
"@rmwc/textfield": "^6.1.4",
"@rmwc/tooltip": "^6.1.4",
"@rmwc/top-app-bar": "^6.1.3",
"material-design-icons": "^3.0.1"
"@jupyterlab/application": "^4.3.6",
"@jupyterlab/launcher": "^4.3.6",
"@jupyterlab/mainmenu": "^4.3.6",
"@lumino/widgets": "^2.2.1",
"@rmwc/base": "^14.0.0",
"@rmwc/button": "^8.0.6",
"@rmwc/data-table": "^8.0.6",
"@rmwc/dialog": "^8.0.6",
"@rmwc/drawer": "^8.0.6",
"@rmwc/fab": "^8.0.6",
"@rmwc/list": "^8.0.6",
"@rmwc/ripple": "^14.0.0",
"@rmwc/textfield": "^8.0.6",
"@rmwc/tooltip": "^8.0.6",
"@rmwc/top-app-bar": "^8.0.6",
"material-design-icons": "^3.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@jupyterlab/builder": "^3.1.0",
"@types/jest": "^26.0.7",
"@types/react-dom": "^16.9.8",
"@typescript-eslint/eslint-plugin": "^4.8.1",
"@typescript-eslint/parser": "^4.8.1",
"eslint": "^7.14.0",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.5",
"@jupyterlab/builder": "^4.3.6",
"@testing-library/dom": "^9.3.0",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.0.0",
"@types/jest": "^29.5.14",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@typescript-eslint/eslint-plugin": "^7.3.1",
"@typescript-eslint/parser": "^7.3.1",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.33.2",
"identity-obj-proxy": "^3.0.0",
"jest": "^26.1.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.0.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.1",
"react-dom": "^17.0.1",
"rimraf": "^3.0.2",
"ts-jest": "^26.1.3",
"typescript": "~4.1.3"
"prettier": "^3.2.4",
"rimraf": "^5.0.5",
"ts-jest": "^29.1.2",
"typescript": "~5.3.3"
},
"sideEffects": [
"style/*.css",
Expand All @@ -86,6 +95,6 @@
},
"test": "jest",
"resolutions": {
"@types/react": "~16.9.16"
"@types/react": "^18.2.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
ReactWidget,
SessionContext,
ISessionContext,
sessionContextDialogs
SessionContextDialogs
} from '@jupyterlab/apputils';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { ServiceManager } from '@jupyterlab/services';
Expand Down Expand Up @@ -64,7 +64,7 @@ export class SidePanel extends BoxPanel {
} else {
let sessionModel = sessionModelItr.next();
while (sessionModel !== undefined) {
if (sessionModel.kernel.id !== firstModel.kernel.id) {
if (sessionModel.value.kernel.id !== firstModel.value.kernel.id) {
// There is more than one unique running kernel.
onlyOneUniqueKernelExists = false;
break;
Expand All @@ -78,18 +78,19 @@ export class SidePanel extends BoxPanel {
// kernel.
if (onlyOneUniqueKernelExists) {
this._sessionContext.sessionManager.connectTo({
model: firstModel,
model: firstModel.value,
kernelConnectionOptions: {
// Only one connection can handleComms. Leave it to the connection
// established by the opened notebook.
handleComms: false
}
});
// Connect to the unique kernel.
this._sessionContext.changeKernel(firstModel.kernel);
this._sessionContext.changeKernel(firstModel.value.kernel);
} else {
// Let the user choose among sessions and kernels when there is no
// or more than 1 running kernels.
const sessionContextDialogs = new SessionContextDialogs();
await sessionContextDialogs.selectKernel(this._sessionContext);
}
} catch (err) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,42 +12,55 @@

import * as React from 'react';

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

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

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

import { waitFor } from '@testing-library/dom';

let container: null | Element = null;
let root: Root | null = null;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});

afterEach(() => {
unmountComponentAtNode(container);
container.remove();
container = null;
afterEach(async () => {
try {
if (root) {
await act(async () => {
root.unmount();
await new Promise(resolve => setTimeout(resolve, 0));
});
}
} catch (error) {
console.warn('During unmount:', error);
} finally {
if (container?.parentNode) {
container.remove();
}
container = null;
root = null;
}
});

it('renders info message about no clusters being available', () => {
it('renders info message about no clusters being available', async () => {
const clustersRef: React.RefObject<Clusters> = React.createRef<Clusters>();
act(() => {
render(
<Clusters sessionContext={{} as any} ref={clustersRef} />,
container
);
await act(async () => {
root.render(<Clusters sessionContext={{} as any} ref={clustersRef} />);
const clusters = clustersRef.current;
if (clusters) {
clusters.setState({ clusters: {} });
}
});
const infoElement: Element = container.firstElementChild;
const infoElement = container.firstElementChild as Element;
expect(infoElement.tagName).toBe('DIV');
expect(infoElement.textContent).toBe('No clusters detected.');
});

it('renders a data-table', () => {
it('renders a data-table', async () => {
const clustersRef: React.RefObject<Clusters> = React.createRef<Clusters>();
const testData = {
key: {
Expand All @@ -59,17 +72,19 @@ it('renders a data-table', () => {
dashboard: 'test-dashboard'
}
};
act(() => {
render(
<Clusters sessionContext={{} as any} ref={clustersRef} />,
container
);
const clusters = clustersRef.current;
if (clusters) {
clusters.setState({ clusters: testData });
}
await act(async () => {
root.render(<Clusters sessionContext={{} as any} ref={clustersRef} />);
});

await act(async () => {
clustersRef.current?.setState({ clusters: testData });
});
const topAppBarHeader: Element = container.firstElementChild;

await waitFor(() =>
expect(container.querySelector('.mdc-data-table__table')).toBeTruthy()
);

const topAppBarHeader = container.firstElementChild as Element;
expect(topAppBarHeader.tagName).toBe('HEADER');
expect(topAppBarHeader.getAttribute('class')).toContain('mdc-top-app-bar');
expect(topAppBarHeader.getAttribute('class')).toContain(
Expand All @@ -79,42 +94,42 @@ it('renders a data-table', () => {
'mdc-top-app-bar--dense'
);
expect(topAppBarHeader.innerHTML).toContain('Clusters [kernel:no kernel]');
const topAppBarFixedAdjust: Element = container.children[1];
const topAppBarFixedAdjust = container.children[1] as Element;
expect(topAppBarFixedAdjust.tagName).toBe('DIV');
expect(topAppBarFixedAdjust.getAttribute('class')).toContain(
'mdc-top-app-bar--fixed-adjust'
);
const selectBar: Element = container.children[2];
const selectBar = container.children[2] as Element;
expect(selectBar.tagName).toBe('DIV');
expect(selectBar.getAttribute('class')).toContain('mdc-select');
const dialogBox: Element = container.children[3];
const dialogBox = container.children[3] as Element;
expect(dialogBox.tagName).toBe('DIV');
expect(dialogBox.getAttribute('class')).toContain('mdc-dialog');
const clustersComponent: Element = container.children[4];
const clustersComponent = container.children[4] as Element;
expect(clustersComponent.tagName).toBe('DIV');
expect(clustersComponent.getAttribute('class')).toContain('Clusters');
const dataTableDiv: Element = clustersComponent.children[0];
const dataTableDiv = clustersComponent.children[0] as Element;
expect(dataTableDiv.tagName).toBe('DIV');
expect(dataTableDiv.getAttribute('class')).toContain('mdc-data-table');
const dataTable: Element = dataTableDiv.children[0];
const dataTable = dataTableDiv.children[0].firstElementChild as Element;
expect(dataTable.tagName).toBe('TABLE');
expect(dataTable.getAttribute('class')).toContain('mdc-data-table__table');
const dataTableHead: Element = dataTable.children[0];
const dataTableHead = dataTable.children[0] as Element;
expect(dataTableHead.tagName).toBe('THEAD');
expect(dataTableHead.getAttribute('class')).toContain(
'rmwc-data-table__head'
);
const dataTableHeaderRow: Element = dataTableHead.children[0];
const dataTableHeaderRow = dataTableHead.children[0] as Element;
expect(dataTableHeaderRow.tagName).toBe('TR');
expect(dataTableHeaderRow.getAttribute('class')).toContain(
'mdc-data-table__header-row'
);
const dataTableBody: Element = dataTable.children[1];
const dataTableBody = dataTable.children[1] as Element;
expect(dataTableBody.tagName).toBe('TBODY');
expect(dataTableBody.getAttribute('class')).toContain(
'mdc-data-table__content'
);
const dataTableBodyRow: Element = dataTableBody.children[0];
const dataTableBodyRow = dataTableBody.children[0] as Element;
expect(dataTableBodyRow.tagName).toBe('TR');
expect(dataTableBodyRow.getAttribute('class')).toContain(
'mdc-data-table__row'
Expand Down
Loading
Loading