Skip to content
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

Support progressive rendering #1489

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .github/workflows/check-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
version_spec: next

- name: Upload Distributions
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: voila-releaser-dist-${{ github.run_number }}
path: .jupyter_releaser_checkout/dist
4 changes: 2 additions & 2 deletions .github/workflows/packaging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
sha256sum * | tee SHA256SUMS

- name: Upload distributions
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: dist ${{ github.run_number }}
path: ./dist
Expand Down Expand Up @@ -76,7 +76,7 @@ jobs:
with:
python-version: ${{ matrix.python }}
architecture: 'x64'
- uses: actions/download-artifact@v2
- uses: actions/download-artifact@v4
with:
name: dist ${{ github.run_number }}
path: ./dist
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ui-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,23 @@ jobs:

- name: Upload Playwright Test assets
if: always()
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: voila-test-assets
path: |
ui-tests/test-results

- name: Upload Playwright Benchmark report
if: always()
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: voila-benchmark-report
path: |
ui-tests/benchmark-results

- name: Upload Playwright Test report
if: always()
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: voila-test-report
path: |
Expand Down
12 changes: 6 additions & 6 deletions packages/voila/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { PageConfig } from '@jupyterlab/coreutils';

import { IRenderMime } from '@jupyterlab/rendermime';

import { KernelWidgetManager } from '@jupyter-widgets/jupyterlab-manager';
import { VoilaWidgetManager } from './plugins/widget';

import { IShell, VoilaShell } from './shell';

Expand Down Expand Up @@ -121,23 +121,23 @@ export class VoilaApp extends JupyterFrontEnd<IShell> {
/**
* A promise that resolves when the Voila Widget Manager is created
*/
get widgetManagerPromise(): PromiseDelegate<KernelWidgetManager> {
get widgetManagerPromise(): PromiseDelegate<VoilaWidgetManager> {
return this._widgetManagerPromise;
}

set widgetManager(manager: KernelWidgetManager | null) {
set widgetManager(manager: VoilaWidgetManager | null) {
this._widgetManager = manager;
if (this._widgetManager) {
this._widgetManagerPromise.resolve(this._widgetManager);
}
}

get widgetManager(): KernelWidgetManager | null {
get widgetManager(): VoilaWidgetManager | null {
return this._widgetManager;
}

protected _widgetManager: KernelWidgetManager | null = null;
protected _widgetManagerPromise = new PromiseDelegate<KernelWidgetManager>();
protected _widgetManager: VoilaWidgetManager | null = null;
protected _widgetManagerPromise = new PromiseDelegate<VoilaWidgetManager>();
}

/**
Expand Down
153 changes: 135 additions & 18 deletions packages/voila/src/plugins/widget/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,25 @@
* *
* The full license is in the file LICENSE, distributed with this software. *
****************************************************************************/

import {
IJupyterWidgetRegistry,
IWidgetRegistryData
} from '@jupyter-widgets/base';
import { WidgetRenderer } from '@jupyter-widgets/jupyterlab-manager';
import {
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '@jupyterlab/application';

import { PageConfig } from '@jupyterlab/coreutils';

import { IOutput } from '@jupyterlab/nbformat';
import { OutputAreaModel, SimplifiedOutputArea } from '@jupyterlab/outputarea';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';

import { KernelAPI, ServerConnection } from '@jupyterlab/services';

import { KernelConnection } from '@jupyterlab/services/lib/kernel/default';

import {
WidgetRenderer,
KernelWidgetManager
} from '@jupyter-widgets/jupyterlab-manager';

import {
IJupyterWidgetRegistry,
IWidgetRegistryData
} from '@jupyter-widgets/base';
import { Widget } from '@lumino/widgets';

import { VoilaApp } from '../../app';

import { Widget } from '@lumino/widgets';
import { VoilaWidgetManager } from './manager';
import { RenderedCells } from './renderedcells';

const WIDGET_MIMETYPE = 'application/vnd.jupyter.widget-view+json';
Expand Down Expand Up @@ -67,7 +59,7 @@ export const widgetManager: JupyterFrontEndPlugin<IJupyterWidgetRegistry> = {
};
}
const kernel = new KernelConnection({ model, serverSettings });
const manager = new KernelWidgetManager(kernel, rendermime);
const manager = new VoilaWidgetManager(kernel, rendermime);
app.widgetManager = manager;

rendermime.removeMimeType(WIDGET_MIMETYPE);
Expand Down Expand Up @@ -157,10 +149,135 @@ export const renderOutputsPlugin: JupyterFrontEndPlugin<void> = {
Widget.attach(output, container);
}
});

const node = document.getElementById('rendered_cells');
if (node) {
const cells = new RenderedCells({ node });
app.shell.add(cells, 'main');
}
}
};

function createOutputArea({
rendermime,
parent
}: {
rendermime: IRenderMimeRegistry;
parent: Element;
}): OutputAreaModel {
const model = new OutputAreaModel({ trusted: true });
const area = new SimplifiedOutputArea({
model,
rendermime
});

const wrapper = document.createElement('div');
wrapper.classList.add('jp-Cell-outputWrapper');
const collapser = document.createElement('div');
collapser.classList.add(
'jp-Collapser',
'jp-OutputCollapser',
'jp-Cell-outputCollapser'
);
wrapper.appendChild(collapser);
parent.lastElementChild?.appendChild(wrapper);
area.node.classList.add('jp-Cell-outputArea');

area.node.style.display = 'flex';
area.node.style.flexDirection = 'column';

Widget.attach(area, wrapper);
return model;
}

/**
* The plugin that renders outputs.
*/
export const renderOutputsProgressivelyPlugin: JupyterFrontEndPlugin<void> = {
id: '@voila-dashboards/voila:render-outputs-progressively',
autoStart: true,
requires: [IRenderMimeRegistry, IJupyterWidgetRegistry],
activate: async (
app: JupyterFrontEnd,
rendermime: IRenderMimeRegistry
): Promise<void> => {
const widgetManager = (app as VoilaApp).widgetManager;
if (!widgetManager) {
return;
}

const kernelId = (app as VoilaApp).widgetManager?.kernel.id;

const receivedWidgetModel: {
[modelId: string]: {
outputModel: OutputAreaModel;
executionModel: IOutput;
};
} = {};
const modelRegisteredHandler = (_: VoilaWidgetManager, modelId: string) => {
if (receivedWidgetModel[modelId]) {
const { outputModel, executionModel } = receivedWidgetModel[modelId];
console.log('render later');
outputModel.add(executionModel);
widgetManager.removeRegisteredModel(modelId);
}
};
widgetManager.modelRegistered.connect(modelRegisteredHandler);

const ws = new WebSocket(`ws://localhost:8866/voila/execution/${kernelId}`);

ws.onmessage = async (msg) => {
const { action, payload } = JSON.parse(msg.data);
if (action === 'execution_result') {
const { cell_index, output_cell } = payload;
const element = document.querySelector(
`[cell-index="${cell_index + 1}"]`
);
if (element) {
const skeleton = element
.getElementsByClassName('voila-skeleton-container')
.item(0);
if (skeleton) {
element.removeChild(skeleton);
}
const model = createOutputArea({ rendermime, parent: element });

if (output_cell.outputs.length > 0) {
element.lastElementChild?.classList.remove(
'jp-mod-noOutputs',
'jp-mod-noInput'
);
}
for (const outputData of output_cell.outputs) {
const modelId =
outputData?.data?.['application/vnd.jupyter.widget-view+json']
?.model_id;
if (modelId) {
if (widgetManager.has_model(modelId)) {
console.log('render immediatly');
model.add(outputData);
} else {
receivedWidgetModel[modelId] = {
outputModel: model,
executionModel: outputData
};
}
} else {
model.add(outputData);
}
}
}
} else if (action === 'finished') {
widgetManager.modelRegistered.disconnect(modelRegisteredHandler);
ws.close();
}
};
ws.onopen = () => {
ws.send(
JSON.stringify({ action: 'execute', payload: { kernel_id: kernelId } })
);
};
}
};

export { VoilaWidgetManager };
22 changes: 22 additions & 0 deletions packages/voila/src/plugins/widget/manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { WidgetModel } from '@jupyter-widgets/base';
import { KernelWidgetManager } from '@jupyter-widgets/jupyterlab-manager';
import { ISignal, Signal } from '@lumino/signaling';

export class VoilaWidgetManager extends KernelWidgetManager {
register_model(model_id: string, modelPromise: Promise<WidgetModel>): void {
super.register_model(model_id, modelPromise);
this._registeredModels.add(model_id);
this._modelRegistered.emit(model_id);
}
get registeredModels(): ReadonlySet<string> {
return this._registeredModels;
}
get modelRegistered(): ISignal<VoilaWidgetManager, string> {
return this._modelRegistered;
}
removeRegisteredModel(modelId: string) {
this._registeredModels.delete(modelId);
}
private _modelRegistered = new Signal<VoilaWidgetManager, string>(this);
private _registeredModels = new Set<string>();
}
3 changes: 3 additions & 0 deletions packages/voila/src/voilaplugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { pathsPlugin } from './plugins/path';
import { translatorPlugin } from './plugins/translator';
import { renderOutputsPlugin, widgetManager } from './plugins/widget';
import { themePlugin, themesManagerPlugin } from './plugins/themes';
import { renderOutputsProgressivelyPlugin } from './plugins/widget/index';

/**
* Export the plugins as default.
Expand All @@ -21,6 +22,7 @@ const plugins: JupyterFrontEndPlugin<any>[] = [
translatorPlugin,
widgetManager,
renderOutputsPlugin,
renderOutputsProgressivelyPlugin,
themesManagerPlugin,
themePlugin
];
Expand All @@ -32,6 +34,7 @@ export {
translatorPlugin,
widgetManager,
renderOutputsPlugin,
renderOutputsProgressivelyPlugin,
themesManagerPlugin,
themePlugin
};
61 changes: 61 additions & 0 deletions share/jupyter/voila/templates/base/skeleton.macro.html.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{% macro css() %}
<!-- voila skeleton -->
<style>
.voila-skeleton-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

.voila-skeleton-post {
width: 220px;
height: 80px;
}
.voila-skeleton-post .voila-skeleton-avatar {
float: left;
width: 52px;
height: 52px;
background-color: #ccc;
border-radius: 25%;
margin: 8px;
background-image: linear-gradient(90deg, #ddd 0px, #e8e8e8 40px, #ddd 80px);
background-size: 600px;
animation: shine-avatar 1.6s infinite linear;
}
.voila-skeleton-post .voila-skeleton-line {
float: left;
width: 140px;
height: 16px;
margin-top: 12px;
border-radius: 7px;
background-image: linear-gradient(90deg, #ddd 0px, #e8e8e8 40px, #ddd 80px);
background-size: 600px;
animation: shine-lines 1.6s infinite linear;
}
.voila-skeleton-post .voila-skeleton-avatar + .voila-skeleton-line {
margin-top: 11px;
width: 100px;
}
.voila-skeleton-post .voila-skeleton-line ~ .voila-skeleton-line {
background-color: #ddd;
}

@keyframes shine-lines {
0% {
background-position: -100px;
}
40%, 100% {
background-position: 140px;
}
}
@keyframes shine-avatar {
0% {
background-position: -32px;
}
40%, 100% {
background-position: 208px;
}
}
</style>
{% endmacro %}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
spinner.style.display="flex";
}
var el = document.getElementById("loading_text");
innterText = text ?? `Executing ${cell_index} of ${cell_count}`
innterText = text ?? `Reading ${cell_index} of ${cell_count}`
if(el){
el.innerHTML = innterText;
}
Expand Down
Loading
Loading