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:
parent
b682a6f898
commit
8d1683f7b0
10 changed files with 119 additions and 88 deletions
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: {
|
||||
|
|
5
changelogs/unreleased/notebook-multiple-outputs.yml
Normal file
5
changelogs/unreleased/notebook-multiple-outputs.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
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', () => {
|
||||
|
|
Loading…
Reference in a new issue