Skip to content

✨ Upgrade sidepanel extension to JupyterLab 4.x compatibility [DO NOT MERGE] #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

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
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)).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also a breaking change if we require 4.x now.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much! I'm not rushing for this PR to be merged—just checking in to make sure it wasn't forgotten."


## 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