Support multiple outputs in Jupyter notebooks

Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/31910, https://gitlab.com/gitlab-org/gitlab-ce/issues/32588
This commit is contained in:
Phil Hughes 2019-01-11 10:14:51 +00:00
parent b682a6f898
commit 8d1683f7b0
No known key found for this signature in database
GPG key ID: 32245528C52E0F9F
10 changed files with 119 additions and 88 deletions

View file

@ -1,11 +1,12 @@
<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>

View file

@ -3,8 +3,9 @@ import Prism from '../../lib/highlight';
import Prompt from '../prompt.vue';
export default {
name: 'CodeOutput',
components: {
prompt: Prompt,
Prompt,
},
props: {
count: {

View file

@ -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>

View file

@ -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>

View file

@ -1,14 +1,9 @@
<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,50 +15,19 @@ 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';
}
return 'code-cell';
},
rawCode() {
if (this.output.text) {
return this.output.text.join('');
}
return this.dataForType(this.outputType);
},
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';
}
return 'text/plain';
},
data() {
return {
outputType: '',
};
},
methods: {
dataForType(type) {
let data = this.output.data[type];
dataForType(output, type) {
let data = output.data[type];
if (typeof data === 'object') {
data = data.join('');
@ -71,17 +35,49 @@ export default {
return data;
},
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;
}
this.outputType = 'text/plain';
return CodeOutput;
},
rawCode(output) {
if (output.text) {
return output.text.join('');
}
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>

View file

@ -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>

View file

@ -3,8 +3,8 @@ import { MarkdownCell, CodeCell } from './cells';
export default {
components: {
'code-cell': CodeCell,
'markdown-cell': MarkdownCell,
CodeCell,
MarkdownCell,
},
props: {
notebook: {

View file

@ -0,0 +1,5 @@
---
title: Support multiple outputs in jupyter notebooks
merge_request:
author:
type: changed

View file

@ -9,6 +9,8 @@ describe('html output cell', () => {
return new Component({
propsData: {
rawCode,
count: 0,
index: 0,
},
}).$mount();
}

View file

@ -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', () => {