Merge branch 'ipython-notebook-viewer' into 'master'
iPython notebook viewer See merge request !10017
This commit is contained in:
commit
47aeacd7f5
9 changed files with 6146 additions and 0 deletions
81
app/assets/javascripts/blob/notebook/index.js
Normal file
81
app/assets/javascripts/blob/notebook/index.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
/* 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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
3
app/assets/javascripts/blob/notebook_viewer.js
Normal file
3
app/assets/javascripts/blob/notebook_viewer.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import renderNotebook from './notebook';
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', renderNotebook);
|
|
@ -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
|
||||||
end
|
end
|
||||||
elsif image? || svg?
|
elsif image? || svg?
|
||||||
'image'
|
'image'
|
||||||
|
elsif ipython_notebook?
|
||||||
|
'notebook'
|
||||||
elsif text?
|
elsif text?
|
||||||
'text'
|
'text'
|
||||||
else
|
else
|
||||||
|
|
5
app/views/projects/blob/_notebook.html.haml
Normal file
5
app/views/projects/blob/_notebook.html.haml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
- 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 = {
|
||||||
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 = {
|
||||||
'environments_folder',
|
'environments_folder',
|
||||||
'issuable',
|
'issuable',
|
||||||
'merge_conflicts',
|
'merge_conflicts',
|
||||||
|
'notebook_viewer',
|
||||||
'vue_pipelines',
|
'vue_pipelines',
|
||||||
],
|
],
|
||||||
minChunks: function(module, count) {
|
minChunks: function(module, count) {
|
||||||
|
|
159
spec/javascripts/blob/notebook/index_spec.js
Normal file
159
spec/javascripts/blob/notebook/index_spec.js
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
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.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
1
spec/javascripts/fixtures/notebook_viewer.html.haml
Normal file
1
spec/javascripts/fixtures/notebook_viewer.html.haml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.file-content#js-notebook-viewer{ data: { endpoint: '/test' } }
|
|
@ -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
|
||||||
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
|
||||||
|
|
5870
vendor/assets/javascripts/notebooklab.js
vendored
Normal file
5870
vendor/assets/javascripts/notebooklab.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue