summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/blob/notebook/index.js80
-rw-r--r--app/assets/javascripts/blob/notebook_viewer.js76
-rw-r--r--spec/javascripts/blob/notebook/index_spec.js159
-rw-r--r--spec/javascripts/fixtures/notebook_viewer.html.haml1
-rw-r--r--spec/models/blob_spec.rb19
-rw-r--r--vendor/assets/javascripts/notebooklab.js8
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
+});