Skip to content

Commit f275246

Browse files
authored
RHOAIENG-24181: chore(jupyter/utils/addons): tree-shake the PatternFly CSS used to add spinner to JupyterLab-based workbenches (#1024)
* fixup, node 22 compatibility * fixup, remove linter for tsconfig.json, it is making unreasonable demands
1 parent 8fec6ce commit f275246

File tree

14 files changed

+3753
-4
lines changed

14 files changed

+3753
-4
lines changed

.github/workflows/code-quality.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,13 @@ jobs:
8282
- name: Validate JSON files (just syntax)
8383
id: validate-json-files
8484
run: |
85+
set -Eeuxo pipefail
86+
8587
type json_verify || sudo apt-get -y install yajl-tools
8688
shopt -s globstar
8789
ret_code=0
8890
echo "-- Checking a regular '*.json' files"
89-
for f in **/*.json; do echo "Checking: '${f}"; echo -n " > "; cat $f | json_verify || ret_code=1; done
91+
for f in **/*.json; do echo "Checking: '${f}"; echo -n " > "; [[ "$(basename "$f")" == "tsconfig.json" ]] && echo "Skipping ${f}" && continue; cat $f | json_verify || ret_code=1; done
9092
echo "-- Checking a 'Pipfile.lock' files"
9193
for f in **/Pipfile.lock; do echo "Checking: '${f}"; echo -n " > "; cat $f | json_verify || ret_code=1; done
9294
echo "-- Checking a '*.ipynb' Jupyter notebook files"

jupyter/utils/addons/.dockerignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Dependencies
2+
node_modules/
3+
.pnpm-store/
4+
5+
# Logs
6+
logs
7+
*.log
8+
npm-debug.log*
9+
pnpm-debug.log*
10+
yarn-debug.log*
11+
yarn-error.log*

jupyter/utils/addons/.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Dependencies
2+
node_modules/
3+
.pnpm-store/
4+
5+
# Logs
6+
logs
7+
*.log
8+
npm-debug.log*
9+
pnpm-debug.log*
10+
yarn-debug.log*
11+
yarn-error.log*

jupyter/utils/addons/README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Jupyter Addons
2+
3+
This package contains addons for JupyterLab workbenches.
4+
5+
## Features / Bugs solved here
6+
7+
(second level bullet points indicate features/bugs that appeared due to the first level bullet point solution)
8+
9+
* [RHOAIENG-11156](https://issues.redhat.com/browse/RHOAIENG-11156) - Better feedback for JupyterLab-based workbenches initial load (improve time to first contentful paint)
10+
* [RHOAIENG-20553](https://issues.redhat.com/browse/RHOAIENG-20553) - CSS is broken when loading the TensorBoard extension
11+
12+
## Usage
13+
14+
The project uses PurgeCSS to tree-shake the PatternFly CSS file, removing unused styles.
15+
The bundled output is generated in the `dist/` directory.
16+
17+
Code generation (generated code in `dist/` is committed to the repository)
18+
19+
```bash
20+
pnpm install
21+
pnpm build
22+
```
23+
24+
Image build (in a Dockerfile)
25+
26+
```Dockerfile
27+
ARG JUPYTER_REUSABLE_UTILS=jupyter/utils
28+
WORKDIR /opt/app-root/bin
29+
COPY ${JUPYTER_REUSABLE_UTILS} utils/
30+
RUN # Apply JupyterLab addons \
31+
/opt/app-root/bin/utils/addons/apply.sh
32+
```
33+
34+
## Development
35+
36+
### Example
37+
38+
Interactive demo of the spinner functionality:
39+
40+
1. Build the project: `pnpm build` or `pnpm build:dev`
41+
2. Open `dist/index.html` in a browser
42+
3. Clicking button simulates JupyterLab finished loading (spinner disappears)
43+
44+
### Build Process
45+
46+
The project uses webpack to bundle the JavaScript files and tree-shake the CSS:
47+
48+
- `pnpm build`: Creates a production build with minification
49+
- `pnpm build:dev`: Creates a development build with source maps
50+
- `pnpm build:clean`: Cleans the output directory and cache before building
51+
- `pnpm clean`: Removes the dist directory and build cache
52+
- `pnpm start`: Starts the webpack development server and opens `dist/index.html` (test page) in a browser
53+
- `pnpm watch`: Watches for file changes and rebuilds automatically
54+
- `pnpm test`: Runs the test-build.sh script to report tree-shaking effectiveness
55+
56+
## Files
57+
58+
- `apply.sh`: Script to apply the addons to a JupyterLab during Dockerfile build
59+
- `partial-head.html`, `partial-body.html`: HTML content to be injected into the head section of JupyterLab
60+
- `cleanup-webpack-plugin.mts`: Custom webpack plugin for asset cleanup (removes unnecessary files)
61+
- `webpack.config.ts`: Webpack configuration with enhanced tree-shaking
62+
- `dist/pf.css`: Tree-shaken PatternFly CSS file with only the necessary styles
63+
- `src/index.ejs`: Template for the example page built into `dist/index.html`
64+
- `dist/index.html`: Example HTML file demonstrating usage of the output
65+
- `test-build.sh`: Script to verify the tree-shaking effectiveness

jupyter/utils/addons/apply.sh

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55

66
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
77

8-
pf_url="https://unpkg.com/@patternfly/[email protected]/patternfly-no-globals.css"
9-
108
static_dir="/opt/app-root/share/jupyter/lab/static"
119
index_file="$static_dir/index.html"
1210

1311
head_file="$script_dir/partial-head.html"
1412
body_file="$script_dir/partial-body.html"
13+
css_file="$script_dir/dist/pf.css"
1514

1615
if [ ! -f "$index_file" ]; then
1716
echo "File '$index_file' not found"
@@ -28,7 +27,17 @@ if [ ! -f "$body_file" ]; then
2827
exit 1
2928
fi
3029

31-
curl -o "$static_dir/pf.css" "$pf_url"
30+
if [ ! -f "$css_file" ]; then
31+
echo "Tree-shaken CSS file not found. Building it now..."
32+
cd "$script_dir" && pnpm build
33+
if [ ! -f "$css_file" ]; then
34+
echo "Failed to build CSS file"
35+
exit 1
36+
fi
37+
fi
38+
39+
# Copy the tree-shaken CSS file to the static directory
40+
cp "$css_file" "$static_dir/pf.css"
3241

3342
head_content=$(tr -d '\n' <"$head_file" | sed 's/@/\\@/g')
3443
body_content=$(tr -d '\n' <"$body_file" | sed 's/@/\\@/g')
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// SPDX-License-Identifier: LicenseRef-Not-Copyrightable
2+
import webpack from 'webpack';
3+
import type { Compilation, Compiler, WebpackPluginInstance } from 'webpack';
4+
5+
interface CleanupPluginOptions {
6+
patterns: RegExp[];
7+
}
8+
9+
class WebpackCleanupPlugin implements WebpackPluginInstance {
10+
private options: CleanupPluginOptions;
11+
12+
constructor(options: CleanupPluginOptions) {
13+
this.options = {
14+
patterns: options.patterns || [],
15+
};
16+
}
17+
18+
apply(compiler: Compiler) {
19+
compiler.hooks.thisCompilation.tap('WebpackCleanupPlugin', (compilation: Compilation) => {
20+
// Process assets to remove files based on filename patterns
21+
compilation.hooks.processAssets.tap(
22+
{
23+
name: 'WebpackCleanupPlugin',
24+
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
25+
},
26+
(assets: Compilation['assets']) => {
27+
// Remove files based on filename patterns
28+
for (const filename in assets) {
29+
if (this.options.patterns.some(pattern => pattern.test(filename))) {
30+
compilation.deleteAsset(filename);
31+
}
32+
}
33+
},
34+
);
35+
});
36+
}
37+
}
38+
39+
export default WebpackCleanupPlugin;

jupyter/utils/addons/dist/index.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>Example spinner page</title><link rel="stylesheet" href="./pf.css"/><style>:where(html,body){height:100%}#loading-container{width:100%;height:100%;display:flex;align-items:center;justify-content:center}</style></head><body><p>Check the following</p><ol><li>the spinner is shown</li><li>the spinner is not too thin at the beginning of animation (missing CSS manifested this way)</li><li>if you scroll down as much as possible, the spinner is centered vertically and horizontally</li><li>the spinner is not shown after the button is clicked</li></ol><button>Finish loading</button><script>function htmlToNode(html) {
2+
const template = document.createElement('template');
3+
template.innerHTML = html;
4+
return template.content.firstChild;
5+
}
6+
document.querySelector('button').addEventListener('click', function () {
7+
document.querySelector('button').replaceWith(
8+
htmlToNode(`<div class="lm-Widget jp-LabShell" id="main">Jupyter is here.</div>`)
9+
);
10+
});</script><div id="loading-container"><svg class="pf-v6-c-spinner" role="progressbar" viewBox="0 0 100 100" aria-label="Loading..."><circle class="pf-v6-c-spinner__path" cx="50" cy="50" r="45" fill="none"/></svg></div><script>function checkForTargetElement(){const e=document.querySelector(".lm-Widget.jp-LabShell#main"),t=document.getElementById("loading-container");e&&t?t.remove():requestAnimationFrame(checkForTargetElement)}requestAnimationFrame(checkForTargetElement)</script></body></html>

jupyter/utils/addons/dist/pf.css

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
@charset "UTF-8";
2+
3+
:root {
4+
--pf-t--color--blue--50: #0066cc;
5+
--pf-t--global--icon--size--400: 3.5rem;
6+
--pf-t--global--color--brand--200: var(--pf-t--color--blue--50);
7+
--pf-t--global--icon--size--2xl: var(--pf-t--global--icon--size--400);
8+
--pf-t--global--color--brand--default: var(--pf-t--global--color--brand--200);
9+
--pf-t--global--icon--color--brand--default: var(--pf-t--global--color--brand--default);
10+
}
11+
12+
:root {
13+
}
14+
15+
.pf-v6-c-spinner {
16+
--pf-v6-c-spinner--diameter: var(--pf-t--global--icon--size--2xl);
17+
--pf-v6-c-spinner--Width: var(--pf-v6-c-spinner--diameter);
18+
--pf-v6-c-spinner--Height: var(--pf-v6-c-spinner--diameter);
19+
--pf-v6-c-spinner--Color: var(--pf-t--global--icon--color--brand--default);
20+
--pf-v6-c-spinner--AnimationDuration: 1.4s;
21+
--pf-v6-c-spinner--AnimationTimingFunction: linear;
22+
--pf-v6-c-spinner--StrokeWidth: 10;
23+
--pf-v6-c-spinner__path--StrokeWidth: var(--pf-v6-c-spinner--StrokeWidth);
24+
--pf-v6-c-spinner__path--AnimationTimingFunction: ease-in-out;
25+
--pf-v6-c-spinner--m-sm--diameter: var(--pf-t--global--icon--size--md);
26+
--pf-v6-c-spinner--m-md--diameter: var(--pf-t--global--icon--size--lg);
27+
--pf-v6-c-spinner--m-lg--diameter: var(--pf-t--global--icon--size--xl);
28+
--pf-v6-c-spinner--m-xl--diameter: var(--pf-t--global--icon--size--2xl);
29+
--pf-v6-c-spinner--m-inline--diameter: 1em;
30+
}
31+
32+
.pf-v6-c-spinner {
33+
width: var(--pf-v6-c-spinner--Width);
34+
height: var(--pf-v6-c-spinner--Height);
35+
overflow: hidden;
36+
animation: pf-v6-c-spinner-animation-rotate calc(var(--pf-v6-c-spinner--AnimationDuration) * 2) var(--pf-v6-c-spinner--AnimationTimingFunction) infinite;
37+
}
38+
39+
.pf-v6-c-spinner__path {
40+
width: 100%;
41+
height: 100%;
42+
stroke: var(--pf-v6-c-spinner--Color);
43+
stroke-dasharray: 283;
44+
stroke-dashoffset: 280;
45+
stroke-linecap: round;
46+
stroke-width: var(--pf-v6-c-spinner--StrokeWidth);
47+
transform-origin: 50% 50%;
48+
animation: pf-v6-c-spinner-animation-dash var(--pf-v6-c-spinner--AnimationDuration) var(--pf-v6-c-spinner__path--AnimationTimingFunction) infinite;
49+
}
50+
51+
@keyframes pf-v6-c-spinner-animation-rotate {
52+
0% {
53+
transform: rotate(0deg);
54+
}
55+
100% {
56+
transform: rotate(360deg);
57+
}
58+
}
59+
@keyframes pf-v6-c-spinner-animation-dash {
60+
0% {
61+
stroke-dashoffset: 280;
62+
transform: rotate(0);
63+
}
64+
15% {
65+
stroke-width: calc(var(--pf-v6-c-spinner__path--StrokeWidth) - 4);
66+
}
67+
40% {
68+
stroke-dasharray: 220;
69+
stroke-dashoffset: 150;
70+
}
71+
100% {
72+
stroke-dashoffset: 280;
73+
transform: rotate(720deg);
74+
}
75+
}

jupyter/utils/addons/package.json5

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// https://pnpm.io/package_json
2+
{
3+
type: "module",
4+
private: true,
5+
name: 'jupyter-utils-addons',
6+
version: '1.0.0',
7+
description: 'Addons for JupyterLab workbenches',
8+
license: 'Apache-2.0',
9+
scripts: {
10+
test: './test-build.sh',
11+
build: 'webpack --mode production',
12+
'build:dev': 'webpack --mode development',
13+
start: 'webpack serve --open',
14+
watch: 'webpack --watch',
15+
clean: 'rm -rf dist .cache',
16+
'build:clean': 'pnpm clean && pnpm build',
17+
},
18+
dependencies: {
19+
'@patternfly/patternfly': '6.0.0',
20+
},
21+
devDependencies: {
22+
// webpack to minify css file
23+
webpack: '^5.99.6',
24+
'webpack-cli': '^6.0.1',
25+
'webpack-dev-server': '^5.2.1',
26+
27+
// plugins for webpack
28+
'css-loader': '^7.1.2',
29+
'html-loader': '^5.1.0',
30+
'html-webpack-plugin': '^5.6.3',
31+
'mini-css-extract-plugin': '^2.9.2',
32+
'purgecss-webpack-plugin': '^7.0.2',
33+
34+
// https://webpack.js.org/configuration/configuration-languages/#typescript,
35+
'ts-node': '^10.9.2',
36+
typescript: '^5.8.3',
37+
'@types/node': '^22.14.1',
38+
'@types/webpack': '^5.28.5'
39+
},
40+
// "packageManager": "[email protected]" // forces install of a precise version, so annoying
41+
}

0 commit comments

Comments
 (0)