Commit 47aeacd7 authored by Jacob Schatz's avatar Jacob Schatz

Merge branch 'ipython-notebook-viewer' into 'master'

iPython notebook viewer

See merge request !10017
parents 5f6c32e1 0f790dbc
/* eslint-disable no-new */
import Vue from 'vue';
import VueResource from 'vue-resource';
import NotebookLab from 'vendor/notebooklab';
Vue.use(VueResource);
Vue.use(NotebookLab);
export default () => {
const el = document.getElementById('js-notebook-viewer');
new Vue({
el,
data() {
return {
error: false,
loadError: false,
loading: true,
json: {},
};
},
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 occured whilst loading the file. Please try again later.
</span>
<span v-else>
An error occured whilst parsing the file.
</span>
</p>
</div>
`,
methods: {
loadFile() {
this.$http.get(el.dataset.endpoint)
.then((res) => {
this.json = res.json();
this.loading = false;
})
.catch((e) => {
if (e.status) {
this.loadError = true;
}
this.error = true;
});
},
},
mounted() {
$('<link>', {
rel: 'stylesheet',
type: 'text/css',
href: gon.katex_css_url,
}).appendTo('head');
if (gon.katex_js_url) {
$.getScript(gon.katex_js_url, () => {
this.loadFile();
});
} else {
this.loadFile();
}
},
});
};
import renderNotebook from './notebook';
document.addEventListener('DOMContentLoaded', renderNotebook);
...@@ -46,6 +46,10 @@ class Blob < SimpleDelegator ...@@ -46,6 +46,10 @@ class Blob < SimpleDelegator
text? && language && language.name == 'SVG' text? && language && language.name == 'SVG'
end end
def ipython_notebook?
text? && language && language.name == 'Jupyter Notebook'
end
def size_within_svg_limits? def size_within_svg_limits?
size <= MAXIMUM_SVG_SIZE size <= MAXIMUM_SVG_SIZE
end end
...@@ -63,6 +67,8 @@ class Blob < SimpleDelegator ...@@ -63,6 +67,8 @@ class Blob < SimpleDelegator
end end
elsif image? || svg? elsif image? || svg?
'image' 'image'
elsif ipython_notebook?
'notebook'
elsif text? elsif text?
'text' 'text'
else else
......
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('notebook_viewer')
.file-content#js-notebook-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
...@@ -37,6 +37,7 @@ var config = { ...@@ -37,6 +37,7 @@ var config = {
merge_request_widget: './merge_request_widget/ci_bundle.js', merge_request_widget: './merge_request_widget/ci_bundle.js',
monitoring: './monitoring/monitoring_bundle.js', monitoring: './monitoring/monitoring_bundle.js',
network: './network/network_bundle.js', network: './network/network_bundle.js',
notebook_viewer: './blob/notebook_viewer.js',
profile: './profile/profile_bundle.js', profile: './profile/profile_bundle.js',
protected_branches: './protected_branches/protected_branches_bundle.js', protected_branches: './protected_branches/protected_branches_bundle.js',
snippet: './snippet/snippet_bundle.js', snippet: './snippet/snippet_bundle.js',
...@@ -105,6 +106,7 @@ var config = { ...@@ -105,6 +106,7 @@ var config = {
'environments_folder', 'environments_folder',
'issuable', 'issuable',
'merge_conflicts', 'merge_conflicts',
'notebook_viewer',
'vue_pipelines', 'vue_pipelines',
], ],
minChunks: function(module, count) { minChunks: function(module, count) {
......
import Vue from 'vue';
import renderNotebook from '~/blob/notebook';
describe('iPython notebook renderer', () => {
preloadFixtures('static/notebook_viewer.html.raw');
beforeEach(() => {
loadFixtures('static/notebook_viewer.html.raw');
});
it('shows loading icon', () => {
renderNotebook();
expect(
document.querySelector('.loading'),
).not.toBeNull();
});
describe('successful response', () => {
const response = (request, next) => {
next(request.respondWith(JSON.stringify({
cells: [{
cell_type: 'markdown',
source: ['# test'],
}, {
cell_type: 'code',
execution_count: 1,
source: [
'def test(str)',
' return str',
],
outputs: [],
}],
}), {
status: 200,
}));
};
beforeEach((done) => {
Vue.http.interceptors.push(response);
renderNotebook();
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, response,
);
});
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', () => {
const response = (request, next) => {
next(request.respondWith('{ "cells": [{"cell_type": "markdown"} }', {
status: 200,
}));
};
beforeEach((done) => {
Vue.http.interceptors.push(response);
renderNotebook();
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, response,
);
});
it('does not show loading icon', () => {
expect(
document.querySelector('.loading'),
).toBeNull();
});
it('shows error message', () => {
expect(
document.querySelector('.md').textContent.trim(),
).toBe('An error occured whilst parsing the file.');
});
});
describe('error getting file', () => {
const response = (request, next) => {
next(request.respondWith('', {
status: 500,
}));
};
beforeEach((done) => {
Vue.http.interceptors.push(response);
renderNotebook();
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, response,
);
});
it('does not show loading icon', () => {
expect(
document.querySelector('.loading'),
).toBeNull();
});
it('shows error message', () => {
expect(
document.querySelector('.md').textContent.trim(),
).toBe('An error occured whilst loading the file. Please try again later.');
});
});
});
.file-content#js-notebook-viewer{ data: { endpoint: '/test' } }
...@@ -53,6 +53,20 @@ describe Blob do ...@@ -53,6 +53,20 @@ describe Blob do
end end
end end
describe '#ipython_notebook?' do
it 'is falsey when language is not Jupyter Notebook' do
git_blob = double(text?: true, language: double(name: 'JSON'))
expect(described_class.decorate(git_blob)).not_to be_ipython_notebook
end
it 'is truthy when language is Jupyter Notebook' do
git_blob = double(text?: true, language: double(name: 'Jupyter Notebook'))
expect(described_class.decorate(git_blob)).to be_ipython_notebook
end
end
describe '#video?' do describe '#video?' do
it 'is falsey with image extension' do it 'is falsey with image extension' do
git_blob = Gitlab::Git::Blob.new(name: 'image.png') git_blob = Gitlab::Git::Blob.new(name: 'image.png')
...@@ -116,6 +130,11 @@ describe Blob do ...@@ -116,6 +130,11 @@ describe Blob do
blob = stubbed_blob blob = stubbed_blob
expect(blob.to_partial_path(project)).to eq 'download' expect(blob.to_partial_path(project)).to eq 'download'
end end
it 'handles iPython notebooks' do
blob = stubbed_blob(text?: true, ipython_notebook?: true)
expect(blob.to_partial_path(project)).to eq 'notebook'
end
end end
describe '#size_within_svg_limits?' do describe '#size_within_svg_limits?' do
......
This source diff could not be displayed because it is too large. You can view the blob instead.
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