Commit e593e14a authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'notebook-multiple-outputs' into 'master'

Support multiple outputs in Jupyter notebooks

Closes #32588 and #31910

See merge request gitlab-org/gitlab-ce!24263
parents 7e1b5f42 8d1683f7
<script> <script>
import CodeCell from './code/index.vue'; import CodeOutput from './code/index.vue';
import OutputCell from './output/index.vue'; import OutputCell from './output/index.vue';
export default { export default {
name: 'CodeCell',
components: { components: {
'code-cell': CodeCell, CodeOutput,
'output-cell': OutputCell, OutputCell,
}, },
props: { props: {
cell: { cell: {
...@@ -29,8 +30,8 @@ export default { ...@@ -29,8 +30,8 @@ export default {
hasOutput() { hasOutput() {
return this.cell.outputs.length; return this.cell.outputs.length;
}, },
output() { outputs() {
return this.cell.outputs[0]; return this.cell.outputs;
}, },
}, },
}; };
...@@ -38,7 +39,7 @@ export default { ...@@ -38,7 +39,7 @@ export default {
<template> <template>
<div class="cell"> <div class="cell">
<code-cell <code-output
:raw-code="rawInputCode" :raw-code="rawInputCode"
:count="cell.execution_count" :count="cell.execution_count"
:code-css-class="codeCssClass" :code-css-class="codeCssClass"
...@@ -47,7 +48,7 @@ export default { ...@@ -47,7 +48,7 @@ export default {
<output-cell <output-cell
v-if="hasOutput" v-if="hasOutput"
:count="cell.execution_count" :count="cell.execution_count"
:output="output" :outputs="outputs"
:code-css-class="codeCssClass" :code-css-class="codeCssClass"
/> />
</div> </div>
......
...@@ -3,8 +3,9 @@ import Prism from '../../lib/highlight'; ...@@ -3,8 +3,9 @@ import Prism from '../../lib/highlight';
import Prompt from '../prompt.vue'; import Prompt from '../prompt.vue';
export default { export default {
name: 'CodeOutput',
components: { components: {
prompt: Prompt, Prompt,
}, },
props: { props: {
count: { count: {
......
...@@ -4,13 +4,21 @@ import Prompt from '../prompt.vue'; ...@@ -4,13 +4,21 @@ import Prompt from '../prompt.vue';
export default { export default {
components: { components: {
prompt: Prompt, Prompt,
}, },
props: { props: {
count: {
type: Number,
required: true,
},
rawCode: { rawCode: {
type: String, type: String,
required: true, required: true,
}, },
index: {
type: Number,
required: true,
},
}, },
computed: { computed: {
sanitizedOutput() { sanitizedOutput() {
...@@ -21,13 +29,16 @@ export default { ...@@ -21,13 +29,16 @@ export default {
}, },
}); });
}, },
showOutput() {
return this.index === 0;
},
}, },
}; };
</script> </script>
<template> <template>
<div class="output"> <div class="output">
<prompt /> <prompt type="Out" :count="count" :show-output="showOutput" />
<div v-html="sanitizedOutput"></div> <div v-html="sanitizedOutput"></div>
</div> </div>
</template> </template>
...@@ -6,6 +6,10 @@ export default { ...@@ -6,6 +6,10 @@ export default {
prompt: Prompt, prompt: Prompt,
}, },
props: { props: {
count: {
type: Number,
required: true,
},
outputType: { outputType: {
type: String, type: String,
required: true, required: true,
...@@ -14,10 +18,24 @@ export default { ...@@ -14,10 +18,24 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
index: {
type: Number,
required: true,
},
},
computed: {
imgSrc() {
return `data:${this.outputType};base64,${this.rawCode}`;
},
showOutput() {
return this.index === 0;
},
}, },
}; };
</script> </script>
<template> <template>
<div class="output"><prompt /> <img :src="'data:' + outputType + ';base64,' + rawCode" /></div> <div class="output">
<prompt type="out" :count="count" :show-output="showOutput" /> <img :src="imgSrc" />
</div>
</template> </template>
<script> <script>
import CodeCell from '../code/index.vue'; import CodeOutput from '../code/index.vue';
import Html from './html.vue'; import HtmlOutput from './html.vue';
import Image from './image.vue'; import ImageOutput from './image.vue';
export default { export default {
components: {
'code-cell': CodeCell,
'html-output': Html,
'image-output': Image,
},
props: { props: {
codeCssClass: { codeCssClass: {
type: String, type: String,
...@@ -20,68 +15,69 @@ export default { ...@@ -20,68 +15,69 @@ export default {
required: false, required: false,
default: 0, default: 0,
}, },
output: { outputs: {
type: Object, type: Array,
required: true, required: true,
default: () => ({}),
}, },
}, },
computed: { data() {
componentName() { return {
if (this.output.text) { outputType: '',
return 'code-cell'; };
} else if (this.output.data['image/png']) { },
return 'image-output'; methods: {
} else if (this.output.data['text/html']) { dataForType(output, type) {
return 'html-output'; let data = output.data[type];
} else if (this.output.data['image/svg+xml']) {
return 'html-output';
}
return 'code-cell'; if (typeof data === 'object') {
}, data = data.join('');
rawCode() {
if (this.output.text) {
return this.output.text.join('');
} }
return this.dataForType(this.outputType); return data;
}, },
outputType() { getComponent(output) {
if (this.output.text) { if (output.text) {
return ''; return CodeOutput;
} else if (this.output.data['image/png']) { } else if (output.data['image/png']) {
return 'image/png'; this.outputType = 'image/png';
} else if (this.output.data['text/html']) {
return 'text/html'; return ImageOutput;
} else if (this.output.data['image/svg+xml']) { } else if (output.data['text/html']) {
return 'image/svg+xml'; this.outputType = 'text/html';
return HtmlOutput;
} else if (output.data['image/svg+xml']) {
this.outputType = 'image/svg+xml';
return HtmlOutput;
} }
return 'text/plain'; this.outputType = 'text/plain';
return CodeOutput;
}, },
}, rawCode(output) {
methods: { if (output.text) {
dataForType(type) { return output.text.join('');
let data = this.output.data[type];
if (typeof data === 'object') {
data = data.join('');
} }
return data; return this.dataForType(output, this.outputType);
}, },
}, },
}; };
</script> </script>
<template> <template>
<component <div>
:is="componentName" <component
:output-type="outputType" :is="getComponent(output)"
:count="count" v-for="(output, index) in outputs"
:raw-code="rawCode" :key="index"
:code-css-class="codeCssClass" type="output"
type="output" :output-type="outputType"
/> :count="count"
:index="index"
:raw-code="rawCode(output)"
:code-css-class="codeCssClass"
/>
</div>
</template> </template>
...@@ -11,18 +11,26 @@ export default { ...@@ -11,18 +11,26 @@ export default {
required: false, required: false,
default: 0, default: 0,
}, },
showOutput: {
type: Boolean,
required: false,
default: true,
},
}, },
computed: { computed: {
hasKeys() { hasKeys() {
return this.type !== '' && this.count; return this.type !== '' && this.count;
}, },
showTypeText() {
return this.type && this.count && this.showOutput;
},
}, },
}; };
</script> </script>
<template> <template>
<div class="prompt"> <div class="prompt">
<span v-if="hasKeys"> {{ type }} [{{ count }}]: </span> <span v-if="showTypeText"> {{ type }} [{{ count }}]: </span>
</div> </div>
</template> </template>
......
...@@ -3,8 +3,8 @@ import { MarkdownCell, CodeCell } from './cells'; ...@@ -3,8 +3,8 @@ import { MarkdownCell, CodeCell } from './cells';
export default { export default {
components: { components: {
'code-cell': CodeCell, CodeCell,
'markdown-cell': MarkdownCell, MarkdownCell,
}, },
props: { props: {
notebook: { notebook: {
......
---
title: Support multiple outputs in jupyter notebooks
merge_request:
author:
type: changed
...@@ -9,6 +9,8 @@ describe('html output cell', () => { ...@@ -9,6 +9,8 @@ describe('html output cell', () => {
return new Component({ return new Component({
propsData: { propsData: {
rawCode, rawCode,
count: 0,
index: 0,
}, },
}).$mount(); }).$mount();
} }
......
...@@ -10,7 +10,7 @@ describe('Output component', () => { ...@@ -10,7 +10,7 @@ describe('Output component', () => {
const createComponent = output => { const createComponent = output => {
vm = new Component({ vm = new Component({
propsData: { propsData: {
output, outputs: [].concat(output),
count: 1, count: 1,
}, },
}); });
...@@ -51,28 +51,21 @@ describe('Output component', () => { ...@@ -51,28 +51,21 @@ describe('Output component', () => {
it('renders as an image', () => { it('renders as an image', () => {
expect(vm.$el.querySelector('img')).not.toBeNull(); expect(vm.$el.querySelector('img')).not.toBeNull();
}); });
it('does not render the prompt', () => {
expect(vm.$el.querySelector('.prompt span')).toBeNull();
});
}); });
describe('html output', () => { describe('html output', () => {
beforeEach(done => { it('renders raw HTML', () => {
createComponent(json.cells[4].outputs[0]); createComponent(json.cells[4].outputs[0]);
setTimeout(() => {
done();
});
});
it('renders raw HTML', () => {
expect(vm.$el.querySelector('p')).not.toBeNull(); expect(vm.$el.querySelector('p')).not.toBeNull();
expect(vm.$el.textContent.trim()).toBe('test'); expect(vm.$el.querySelectorAll('p').length).toBe(1);
expect(vm.$el.textContent.trim()).toContain('test');
}); });
it('does not render the prompt', () => { it('renders multiple raw HTML outputs', () => {
expect(vm.$el.querySelector('.prompt span')).toBeNull(); createComponent([json.cells[4].outputs[0], json.cells[4].outputs[0]]);
expect(vm.$el.querySelectorAll('p').length).toBe(2);
}); });
}); });
...@@ -88,10 +81,6 @@ describe('Output component', () => { ...@@ -88,10 +81,6 @@ describe('Output component', () => {
it('renders as an svg', () => { it('renders as an svg', () => {
expect(vm.$el.querySelector('svg')).not.toBeNull(); expect(vm.$el.querySelector('svg')).not.toBeNull();
}); });
it('does not render the prompt', () => {
expect(vm.$el.querySelector('.prompt span')).toBeNull();
});
}); });
describe('default to plain text', () => { describe('default to plain text', () => {
......
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