mariobuikhuizen / voila-embed-vuetify

A Vue component to embed ipyvuetify widgets served by Voila
MIT License
1 stars 1 forks source link

releaase a version compatible with Vue3 #10

Open havok2063 opened 8 months ago

havok2063 commented 8 months ago

Hi @mariobuikhuizen is it possible to release a version of this onto npm that is compatible with Vue 3. I'd like to use this for a new project. I tried bumping the dependencies to

    "vue": "^3.3.4",
    "vuetify": "^3.4.6",

which is what I'm using in the new project and npm install on the local repo, but I ran into issues. I'm also not sure what this dependency is, "@mariobuikhuizen/vue-compiler-addon": "^2.6.10-alpha.1" and if we still need it?

havok2063 commented 8 months ago

I tried installing with npm install [local_path]. It seemed to install ok with "voila-embed-vuetify": "file:../../github_projects/voila-embed-vuetify" added as a dependency in my project. But I get the following error when loading the component.

[plugin:vite:import-analysis] Failed to resolve import "vuetify/lib" from "../../github_projects/voila-embed-vuetify/src/JupyterWidgetEmbed.js". Does the file exist?

/Users/Brian/Work/github_projects/voila-embed-vuetify/src/JupyterWidgetEmbed.js:2:46

1  |  import Vue from 'vue'; // eslint-disable-line import/no-unresolved
2  |  import Vuetify, * as vuetifyComponents from 'vuetify/lib'; // eslint-disable-line import/no-unresolved
   |                                               ^
3  |  import { addCompiler } from '@mariobuikhuizen/vue-compiler-addon/dist/vue-compiler-addon.esm';
4  |  import { provideWidget, requestWidget } from './widgetLocator';

My component is

<template>
    <div>
        <jupyter-widget-embed
                voila-url="http://localhost:14050"
                notebook="jdaviz.ipynb"
                mount-id="jdaviz"
                :request-options=requestOptions
        ></jupyter-widget-embed>
    </div>
</template>

<script lang="ts" setup>

import { JupyterWidgetEmbed } from 'voila-embed-vuetify'

let requestOptions = {"credentials": 'include'}

</script>
havok2063 commented 8 months ago

I tried redefining the component directly in my frontend. The page at least renders now and does not produce any errors in the console but the spinner spins forever and no content loads. I see Starting WebSocket: ws://localhost:14050/api/kernels/b8444461-72f9-471f-b0dc-84b4482d23ae in the console. Any ideas on what could be going on?

Screen Shot 2024-01-04 at 1 30 08 PM

The component JupyterWidgetEmbed is defined as

<template>
    <div v-if="renderFn">
      <!-- Rendered through Vue's render function -->
    </div>
    <div v-else>
      <!-- Default slot or loading indicator -->
      <v-chip style="white-space: initial">
        [{{ notebook }} - {{ mountId }}]
        <v-progress-circular indeterminate></v-progress-circular>
      </v-chip>
    </div>
</template>

<script lang="ts" setup>

import { ref, Ref, onMounted } from 'vue'

// Declaring types for better TypeScript integration
interface RequestOptions {
  // Define the structure of requestOptions
}

interface NotebookLoaded {
  [key: string]: boolean;
}

// define which properties are passed in from the parent, i.e. ":xxx"
const props = defineProps<{
    voilaUrl: String,
    notebook: String,
    mountId: String,
    requestOptions: Object
}>()

const notebooksLoaded: Ref<NotebookLoaded> = ref({})
let voilaLoaded: Ref<boolean> = ref(false)
let elem = null
let renderFn = ref(null)
const widgetResolveFns = {}
const widgetPromises = {}

function keyFromMountPath(obj: object) {
    return `${obj.voilaUrl}${obj.notebook}${obj.mountId}`;
}

function provideWidget(mountPath: string, widgetModel: object) {
    const key = keyFromMountPath(mountPath);
    if (widgetResolveFns[key]) {
        widgetResolveFns[key](widgetModel);
    } else {
        widgetPromises[key] = Promise.resolve(widgetModel);
    }
}

function requestWidget(mountPath: string) {
    const key = keyFromMountPath(mountPath);

    if (!widgetPromises[key]) {
        widgetPromises[key] = new Promise((resolve) => { widgetResolveFns[key] = resolve; });
    }
    return widgetPromises[key];
}

function getWidgetManager(voila, kernel) {
            /* voila >= 0.1.8 */
            const context = {
                saveState: {
                    connect: () => {
                    }
                },
                /* voila >= 0.2.8 */
                sessionContext: {
                    session: {
                        kernel
                    },
                    kernelChanged: {
                        connect: () => {
                        }
                    },
                    statusChanged: {
                        connect: () => {
                        }
                    },
                    connectionStatusChanged: {
                        connect: () => {
                        }
                    },
                },
            };

            const settings = {
                saveState: false
            };

            const rendermime = new voila.RenderMimeRegistry({
                initialFactories: voila.standardRendererFactories
            });

            return new voila.WidgetManager(context, rendermime, settings);
}

async function init(voilaUrl: string, notebook: string, requestOptions: RequestOptions) {
  const requireJsPromise = loadRequireJs(voilaUrl);
  addVoilaCss(voilaUrl);

  const notebookKey = `${voilaUrl}${notebook}`;
  if (notebooksLoaded.value[notebookKey]) {
    return;
  }
  notebooksLoaded.value[notebookKey] = true;

  const res = await fetch(`${voilaUrl}/voila/render/${notebook}`, requestOptions);
  const json = await res.json();

  await requireJsPromise;
  if (!voilaLoaded.value) {
    window.requirejs.config({
      // Configuration...
      baseUrl: `${voilaUrl}${json.baseUrl}voila`,
      waitSeconds: 3000,
        map: {
            '*': {
                'jupyter-vue': `${voilaUrl}/voila/nbextensions/jupyter-vue/nodeps.js`,
                'jupyter-vuetify': `${voilaUrl}/voila/nbextensions/jupyter-vuetify/nodeps.js`,
            },
        },
    });

    const extensions = json.extensions
      .filter((extension) => !extension.includes('jupyter-vue'))
      .map((extension) => `${voilaUrl}${extension}`);

    window.requirejs(extensions);

    voilaLoaded.value = true;
  }

  window.requirejs(['static/voila'], (voila) => {
    window.define('vue', [], () => Vue);
    (async () => {
      // Kernel and WidgetManager handling code...
      const kernel = await voila.connectKernel(`${voilaUrl}${json.baseUrl}`, json.kernelId);

const widgetManager = getWidgetManager(voila, kernel);
await widgetManager._loadFromKernel();

Object.values(widgetManager._models)
    .map(async (modelPromise) => {
        const model = await modelPromise;
        const meta = model.get('_metadata');
        const mountId = meta && meta.mount_id;
        if (mountId) {
            provideWidget({ voilaUrl, notebook, mountId }, model);
        }
    });
    })();
  });
}

function loadRequireJs(voilaUrl: string): Promise<void> {
  if (document.getElementById('tag-requirejs')) {
    return Promise.resolve();
  }
  return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = `${voilaUrl}/voila/static/require.min.js`;
    script.id = 'tag-requirejs';
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
}

function addVoilaCss(voilaUrl: string): void {
  if (!document.getElementById('tag-index.css')) {
    const link = document.createElement('link');
    link.href = `${voilaUrl}/voila/static/index.css`;
    link.type = 'text/css';
    link.rel = 'stylesheet';
    link.id = 'tag-index.css';
    document.head.appendChild(link);
  }
  if (!document.getElementById('tag-theme-light.css')) {
    const link = document.createElement('link');
    link.href = `${voilaUrl}/voila/static/theme-light.css`;
    link.type = 'text/css';
    link.rel = 'stylesheet';
    link.id = 'tag-theme-light.css';
    document.head.appendChild(link);
  }
}

onMounted(() => {
    init(props.voilaUrl, props.notebook, props.requestOptions)

    const model = requestWidget(props)
    console.log('model', model)
    model
    .then((model) => model.widget_manager.create_view(model))
    .then((widgetView) => {
        console.log('view', widgetView)
        if (['VuetifyView', 'VuetifyTemplateView'].includes(widgetView.model.get('_view_name'))) {
        renderFn.value = (createElement) => widgetView.vueRender(createElement)
      } else {
        // Handle other widget types
      }
    })

})

</script>
havok2063 commented 8 months ago

I think the requestWidget promise is never resolving. Neither of the next .then statements are being run. I'll keep poking around at it.

mariobuikhuizen commented 8 months ago

You will also need a version of ipyvue and ipyvuetify that's compatible wit Vue3 and Vuetify3. We started working on this in https://github.com/widgetti/ipyvue/pull/82 and https://github.com/widgetti/ipyvuetify/pull/283. Available as ipyvue==3.0.0a2 and ipyvuetify==3.0.0a2.

I haven't tested it yet with this project, it might need changes as well.

havok2063 commented 8 months ago

Ahh ok. After updating those dependencies, I see 404 errors in the console for the following:

GET http://localhost:14050/voila/vue.js
GET http://localhost:14050/voila/vuetify.js

and a Error: Script error for "vuetify", needed by: http://localhost:14050/voila/nbextensions/jupyter-vuetify/nodeps.js

So likely some things need updating in the init function? In my voila conda env, I do see the right nodeps files, e..g share/jupyter/nbextensions/jupyter-vuetify/nodeps.js. And this loads in my browser http://localhost:14050/voila/nbextensions/jupyter-vuetify/nodeps.js. I'm not too familiar with how these dependencies need to be loaded into the front-end component.