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'
|
||||
end
|
||||
|
||||
def ipython_notebook?
|
||||
text? && language && language.name == 'Jupyter Notebook'
|
||||
end
|
||||
|
||||
def size_within_svg_limits?
|
||||
size <= MAXIMUM_SVG_SIZE
|
||||
end
|
||||
|
@ -63,6 +67,8 @@ class Blob < SimpleDelegator
|
|||
end
|
||||
elsif image? || svg?
|
||||
'image'
|
||||
elsif ipython_notebook?
|
||||
'notebook'
|
||||
elsif text?
|
||||
'text'
|
||||
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',
|
||||
monitoring: './monitoring/monitoring_bundle.js',
|
||||
network: './network/network_bundle.js',
|
||||
notebook_viewer: './blob/notebook_viewer.js',
|
||||
profile: './profile/profile_bundle.js',
|
||||
protected_branches: './protected_branches/protected_branches_bundle.js',
|
||||
snippet: './snippet/snippet_bundle.js',
|
||||
|
@ -105,6 +106,7 @@ var config = {
|
|||
'environments_folder',
|
||||
'issuable',
|
||||
'merge_conflicts',
|
||||
'notebook_viewer',
|
||||
'vue_pipelines',
|
||||
],
|
||||
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
|
||||
|
||||
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
|
||||
it 'is falsey with image extension' do
|
||||
git_blob = Gitlab::Git::Blob.new(name: 'image.png')
|
||||
|
@ -116,6 +130,11 @@ describe Blob do
|
|||
blob = stubbed_blob
|
||||
expect(blob.to_partial_path(project)).to eq 'download'
|
||||
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
|
||||
|
||||
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