diff options
-rw-r--r-- | app/assets/javascripts/blob/notebook/index.js | 80 | ||||
-rw-r--r-- | app/assets/javascripts/blob/notebook_viewer.js | 76 | ||||
-rw-r--r-- | spec/javascripts/blob/notebook/index_spec.js | 159 | ||||
-rw-r--r-- | spec/javascripts/fixtures/notebook_viewer.html.haml | 1 | ||||
-rw-r--r-- | spec/models/blob_spec.rb | 19 | ||||
-rw-r--r-- | vendor/assets/javascripts/notebooklab.js | 8 |
6 files changed, 265 insertions, 78 deletions
diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js new file mode 100644 index 00000000000..c910ed5b76b --- /dev/null +++ b/app/assets/javascripts/blob/notebook/index.js @@ -0,0 +1,80 @@ +/* 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" /> + <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(); + } + }, + }); +}; diff --git a/app/assets/javascripts/blob/notebook_viewer.js b/app/assets/javascripts/blob/notebook_viewer.js index 45b838c700f..b7a0a195a92 100644 --- a/app/assets/javascripts/blob/notebook_viewer.js +++ b/app/assets/javascripts/blob/notebook_viewer.js @@ -1,75 +1,3 @@ -import Vue from 'vue'; -import VueResource from 'vue-resource'; -import NotebookLab from 'vendor/notebooklab'; +import renderNotebook from './notebook'; -Vue.use(VueResource); -Vue.use(NotebookLab); - -document.addEventListener('DOMContentLoaded', () => { - 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" /> - <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'); - - $.getScript(gon.katex_js_url, () => { - this.loadFile(); - }); - }, - }); -}); +document.addEventListener('DOMContentLoaded', renderNotebook); diff --git a/spec/javascripts/blob/notebook/index_spec.js b/spec/javascripts/blob/notebook/index_spec.js new file mode 100644 index 00000000000..03539bead29 --- /dev/null +++ b/spec/javascripts/blob/notebook/index_spec.js @@ -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('.hljs'), + ).not.toBeNull(); + + expect( + document.querySelector('.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.'); + }); + }); +}); diff --git a/spec/javascripts/fixtures/notebook_viewer.html.haml b/spec/javascripts/fixtures/notebook_viewer.html.haml new file mode 100644 index 00000000000..17a7a9d8f31 --- /dev/null +++ b/spec/javascripts/fixtures/notebook_viewer.html.haml @@ -0,0 +1 @@ +.file-content#js-notebook-viewer{ data: { endpoint: '/test' } } diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb index 94c25a454aa..552229e9b07 100644 --- a/spec/models/blob_spec.rb +++ b/spec/models/blob_spec.rb @@ -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 diff --git a/vendor/assets/javascripts/notebooklab.js b/vendor/assets/javascripts/notebooklab.js index 35e845657bd..9d2aea18c5b 100644 --- a/vendor/assets/javascripts/notebooklab.js +++ b/vendor/assets/javascripts/notebooklab.js @@ -414,13 +414,13 @@ exports.default = { }, computed: { markdown: function markdown() { - var regex = new RegExp(/^\$\$(.*)\$\$$/, 'g'); + var regex = new RegExp('^\$\$(.*)\$\$$', 'g'); var source = this.cell.source.map(function (line) { var matches = regex.exec(line.trim()); // Only render use the Katex library if it is actually loaded - if (matches && matches.length > 0 && katex) { + if (matches && matches.length > 0 && typeof katex !== 'undefined') { return katex.renderToString(matches[1]); } else { return line; @@ -3047,7 +3047,7 @@ function escape(html, encode) { } function unescape(html) { - // explicitly match decimal, hex, and named HTML entities + // explicitly match decimal, hex, and named HTML entities return html.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/g, function(_, n) { n = n.toLowerCase(); if (n === 'colon') return ':'; @@ -3636,4 +3636,4 @@ module.exports = { /***/ }) /******/ ]); -});
\ No newline at end of file +}); |