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