Commit f7699051 authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '194207-refactor-notebook-to-sfc' into 'master'

Refactor notebook_viewer to vue SFC

See merge request gitlab-org/gitlab!27090
parents 4b872ebc 09d02ed1
/* eslint-disable no-new */
import Vue from 'vue';
import axios from '../../lib/utils/axios_utils';
import notebookLab from '../../notebook/index.vue';
import NotebookViewer from './notebook_viewer.vue';
export default () => {
const el = document.getElementById('js-notebook-viewer');
new Vue({
return new Vue({
el,
components: {
notebookLab,
render(createElement) {
return createElement(NotebookViewer, {
props: {
endpoint: el.dataset.endpoint,
},
});
},
data() {
return {
error: false,
loadError: false,
loading: true,
json: {},
};
},
mounted() {
if (gon.katex_css_url) {
const katexStyles = document.createElement('link');
katexStyles.setAttribute('rel', 'stylesheet');
katexStyles.setAttribute('href', gon.katex_css_url);
document.head.appendChild(katexStyles);
}
if (gon.katex_js_url) {
const katexScript = document.createElement('script');
katexScript.addEventListener('load', () => {
this.loadFile();
});
katexScript.setAttribute('src', gon.katex_js_url);
document.head.appendChild(katexScript);
} else {
this.loadFile();
}
},
methods: {
loadFile() {
axios
.get(el.dataset.endpoint)
.then(res => res.data)
.then(data => {
this.json = data;
this.loading = false;
})
.catch(e => {
if (e.status !== 200) {
this.loadError = true;
}
this.error = true;
});
},
},
template: `
<div class="container-fluid md prepend-top-default append-bottom-default">
<div
class="text-center loading"
v-if="loading && !error">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="iPython notebook loading">
</i>
</div>
<notebook-lab
v-if="!loading && !error"
:notebook="json"
code-css-class="code white" />
<p
class="text-center"
v-if="error">
<span v-if="loadError">
An error occurred while loading the file. Please try again later.
</span>
<span v-else>
An error occurred while parsing the file.
</span>
</p>
</div>
`,
});
};
<script>
import axios from '~/lib/utils/axios_utils';
import notebookLab from '~/notebook/index.vue';
import { GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
notebookLab,
GlLoadingIcon,
},
props: {
endpoint: {
type: String,
required: true,
},
},
data() {
return {
error: false,
loadError: false,
loading: true,
json: {},
};
},
mounted() {
if (gon.katex_css_url) {
const katexStyles = document.createElement('link');
katexStyles.setAttribute('rel', 'stylesheet');
katexStyles.setAttribute('href', gon.katex_css_url);
document.head.appendChild(katexStyles);
}
if (gon.katex_js_url) {
const katexScript = document.createElement('script');
katexScript.addEventListener('load', () => {
this.loadFile();
});
katexScript.setAttribute('src', gon.katex_js_url);
document.head.appendChild(katexScript);
} else {
this.loadFile();
}
},
methods: {
loadFile() {
axios
.get(this.endpoint)
.then(res => res.data)
.then(data => {
this.json = data;
this.loading = false;
})
.catch(e => {
if (e.status !== 200) {
this.loadError = true;
}
this.error = true;
});
},
},
};
</script>
<template>
<div
class="js-notebook-viewer-mounted container-fluid md prepend-top-default append-bottom-default"
>
<div v-if="loading && !error" class="text-center loading">
<gl-loading-icon class="mt-5" size="lg" />
</div>
<notebook-lab v-if="!loading && !error" :notebook="json" code-css-class="code white" />
<p v-if="error" class="text-center">
<span v-if="loadError" ref="loadErrorMessage">{{
__('An error occurred while loading the file. Please try again later.')
}}</span>
<span v-else ref="parsingErrorMessage">{{
__('An error occurred while parsing the file.')
}}</span>
</p>
</div>
</template>
......@@ -1948,6 +1948,9 @@ msgstr ""
msgid "An error occurred while parsing recent searches"
msgstr ""
msgid "An error occurred while parsing the file."
msgstr ""
msgid "An error occurred while removing epics."
msgstr ""
......
......@@ -308,6 +308,48 @@ describe 'File blob', :js do
end
end
context 'Jupiter Notebook file' do
before do
project.add_maintainer(project.creator)
Files::CreateService.new(
project,
project.creator,
start_branch: 'master',
branch_name: 'master',
commit_message: "Add Jupiter Notebook",
file_path: 'files/basic.ipynb',
file_content: project.repository.blob_at('add-ipython-files', 'files/ipython/basic.ipynb').data
).execute
visit_blob('files/basic.ipynb')
wait_for_requests
end
it 'displays the blob' do
aggregate_failures do
# shows rendered notebook
expect(page).to have_selector('.js-notebook-viewer-mounted')
# does show a viewer switcher
expect(page).to have_selector('.js-blob-viewer-switcher')
# show a disabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
# shows a raw button
expect(page).to have_link('Open raw')
# shows a download button
expect(page).to have_link('Download')
# shows the rendered notebook
expect(page).to have_content('test')
end
end
end
context 'ISO file (stored in LFS)' do
context 'when LFS is enabled on the project' do
before do
......
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import component from '~/blob/notebook/notebook_viewer.vue';
import NotebookLab from '~/notebook/index.vue';
import waitForPromises from 'helpers/wait_for_promises';
describe('iPython notebook renderer', () => {
let wrapper;
let mock;
const endpoint = 'test';
const mockNotebook = {
cells: [
{
cell_type: 'markdown',
source: ['# test'],
},
{
cell_type: 'code',
execution_count: 1,
source: ['def test(str)', ' return str'],
outputs: [],
},
],
};
const mountComponent = () => {
wrapper = shallowMount(component, { propsData: { endpoint } });
};
const findLoading = () => wrapper.find(GlLoadingIcon);
const findNotebookLab = () => wrapper.find(NotebookLab);
const findLoadErrorMessage = () => wrapper.find({ ref: 'loadErrorMessage' });
const findParseErrorMessage = () => wrapper.find({ ref: 'parsingErrorMessage' });
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
mock.restore();
});
it('shows loading icon', () => {
mock.onGet(endpoint).reply(() => new Promise(() => {}));
mountComponent({ loadFile: jest.fn() });
expect(findLoading().exists()).toBe(true);
});
describe('successful response', () => {
beforeEach(() => {
mock.onGet(endpoint).reply(200, mockNotebook);
mountComponent();
return waitForPromises();
});
it('does not show loading icon', () => {
expect(findLoading().exists()).toBe(false);
});
it('renders the notebook', () => {
expect(findNotebookLab().exists()).toBe(true);
});
});
describe('error in JSON response', () => {
beforeEach(() => {
mock.onGet(endpoint).reply(() =>
// eslint-disable-next-line prefer-promise-reject-errors
Promise.reject({ status: 200 }),
);
mountComponent();
return waitForPromises();
});
it('does not show loading icon', () => {
expect(findLoading().exists()).toBe(false);
});
it('shows error message', () => {
expect(findParseErrorMessage().text()).toEqual('An error occurred while parsing the file.');
});
});
describe('error getting file', () => {
beforeEach(() => {
mock.onGet(endpoint).reply(500, '');
mountComponent();
return waitForPromises();
});
it('does not show loading icon', () => {
expect(findLoading().exists()).toBe(false);
});
it('shows error message', () => {
expect(findLoadErrorMessage().text()).toEqual(
'An error occurred while loading the file. Please try again later.',
);
});
});
});
<div class="file-content" data-endpoint="/test" id="js-notebook-viewer"></div>
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import renderNotebook from '~/blob/notebook';
describe('iPython notebook renderer', () => {
preloadFixtures('static/notebook_viewer.html');
beforeEach(() => {
loadFixtures('static/notebook_viewer.html');
});
it('shows loading icon', () => {
renderNotebook();
expect(document.querySelector('.loading')).not.toBeNull();
});
describe('successful response', () => {
let mock;
beforeEach(done => {
mock = new MockAdapter(axios);
mock.onGet('/test').reply(200, {
cells: [
{
cell_type: 'markdown',
source: ['# test'],
},
{
cell_type: 'code',
execution_count: 1,
source: ['def test(str)', ' return str'],
outputs: [],
},
],
});
renderNotebook();
setTimeout(() => {
done();
});
});
afterEach(() => {
mock.restore();
});
it('does not show loading icon', () => {
expect(document.querySelector('.loading')).toBeNull();
});
it('renders the notebook', () => {
expect(document.querySelector('.md')).not.toBeNull();
});
it('renders the markdown cell', () => {
expect(document.querySelector('h1')).not.toBeNull();
expect(document.querySelector('h1').textContent.trim()).toBe('test');
});
it('highlights code', () => {
expect(document.querySelector('.token')).not.toBeNull();
expect(document.querySelector('.language-python')).not.toBeNull();
});
});
describe('error in JSON response', () => {
let mock;
beforeEach(done => {
mock = new MockAdapter(axios);
mock.onGet('/test').reply(() =>
// eslint-disable-next-line prefer-promise-reject-errors
Promise.reject({ status: 200, data: '{ "cells": [{"cell_type": "markdown"} }' }),
);
renderNotebook();
setTimeout(() => {
done();
});
});
afterEach(() => {
mock.restore();
});
it('does not show loading icon', () => {
expect(document.querySelector('.loading')).toBeNull();
});
it('shows error message', () => {
expect(document.querySelector('.md').textContent.trim()).toBe(
'An error occurred while parsing the file.',
);
});
});
describe('error getting file', () => {
let mock;
beforeEach(done => {
mock = new MockAdapter(axios);
mock.onGet('/test').reply(500, '');
renderNotebook();
setTimeout(() => {
done();
});
});
afterEach(() => {
mock.restore();
});
it('does not show loading icon', () => {
expect(document.querySelector('.loading')).toBeNull();
});
it('shows error message', () => {
expect(document.querySelector('.md').textContent.trim()).toBe(
'An error occurred while loading the file. Please try again later.',
);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment