Skip to content

Commit e593e14

Browse files
author
Filipa Lacerda
committed
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
2 parents 7e1b5f4 + 8d1683f commit e593e14

File tree

10 files changed

+118
-87
lines changed

10 files changed

+118
-87
lines changed

app/assets/javascripts/notebook/cells/code.vue

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
<script>
2-
import CodeCell from './code/index.vue';
2+
import CodeOutput from './code/index.vue';
33
import OutputCell from './output/index.vue';
44
55
export default {
6+
name: 'CodeCell',
67
components: {
7-
'code-cell': CodeCell,
8-
'output-cell': OutputCell,
8+
CodeOutput,
9+
OutputCell,
910
},
1011
props: {
1112
cell: {
@@ -29,16 +30,16 @@ export default {
2930
hasOutput() {
3031
return this.cell.outputs.length;
3132
},
32-
output() {
33-
return this.cell.outputs[0];
33+
outputs() {
34+
return this.cell.outputs;
3435
},
3536
},
3637
};
3738
</script>
3839

3940
<template>
4041
<div class="cell">
41-
<code-cell
42+
<code-output
4243
:raw-code="rawInputCode"
4344
:count="cell.execution_count"
4445
:code-css-class="codeCssClass"
@@ -47,7 +48,7 @@ export default {
4748
<output-cell
4849
v-if="hasOutput"
4950
:count="cell.execution_count"
50-
:output="output"
51+
:outputs="outputs"
5152
:code-css-class="codeCssClass"
5253
/>
5354
</div>

app/assets/javascripts/notebook/cells/code/index.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import Prism from '../../lib/highlight';
33
import Prompt from '../prompt.vue';
44
55
export default {
6+
name: 'CodeOutput',
67
components: {
7-
prompt: Prompt,
8+
Prompt,
89
},
910
props: {
1011
count: {

app/assets/javascripts/notebook/cells/output/html.vue

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,21 @@ import Prompt from '../prompt.vue';
44
55
export default {
66
components: {
7-
prompt: Prompt,
7+
Prompt,
88
},
99
props: {
10+
count: {
11+
type: Number,
12+
required: true,
13+
},
1014
rawCode: {
1115
type: String,
1216
required: true,
1317
},
18+
index: {
19+
type: Number,
20+
required: true,
21+
},
1422
},
1523
computed: {
1624
sanitizedOutput() {
@@ -21,13 +29,16 @@ export default {
2129
},
2230
});
2331
},
32+
showOutput() {
33+
return this.index === 0;
34+
},
2435
},
2536
};
2637
</script>
2738

2839
<template>
2940
<div class="output">
30-
<prompt />
41+
<prompt type="Out" :count="count" :show-output="showOutput" />
3142
<div v-html="sanitizedOutput"></div>
3243
</div>
3344
</template>

app/assets/javascripts/notebook/cells/output/image.vue

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ export default {
66
prompt: Prompt,
77
},
88
props: {
9+
count: {
10+
type: Number,
11+
required: true,
12+
},
913
outputType: {
1014
type: String,
1115
required: true,
@@ -14,10 +18,24 @@ export default {
1418
type: String,
1519
required: true,
1620
},
21+
index: {
22+
type: Number,
23+
required: true,
24+
},
25+
},
26+
computed: {
27+
imgSrc() {
28+
return `data:${this.outputType};base64,${this.rawCode}`;
29+
},
30+
showOutput() {
31+
return this.index === 0;
32+
},
1733
},
1834
};
1935
</script>
2036

2137
<template>
22-
<div class="output"><prompt /> <img :src="'data:' + outputType + ';base64,' + rawCode" /></div>
38+
<div class="output">
39+
<prompt type="out" :count="count" :show-output="showOutput" /> <img :src="imgSrc" />
40+
</div>
2341
</template>
Lines changed: 50 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
<script>
2-
import CodeCell from '../code/index.vue';
3-
import Html from './html.vue';
4-
import Image from './image.vue';
2+
import CodeOutput from '../code/index.vue';
3+
import HtmlOutput from './html.vue';
4+
import ImageOutput from './image.vue';
55
66
export default {
7-
components: {
8-
'code-cell': CodeCell,
9-
'html-output': Html,
10-
'image-output': Image,
11-
},
127
props: {
138
codeCssClass: {
149
type: String,
@@ -20,68 +15,69 @@ export default {
2015
required: false,
2116
default: 0,
2217
},
23-
output: {
24-
type: Object,
18+
outputs: {
19+
type: Array,
2520
required: true,
26-
default: () => ({}),
2721
},
2822
},
29-
computed: {
30-
componentName() {
31-
if (this.output.text) {
32-
return 'code-cell';
33-
} else if (this.output.data['image/png']) {
34-
return 'image-output';
35-
} else if (this.output.data['text/html']) {
36-
return 'html-output';
37-
} else if (this.output.data['image/svg+xml']) {
38-
return 'html-output';
39-
}
23+
data() {
24+
return {
25+
outputType: '',
26+
};
27+
},
28+
methods: {
29+
dataForType(output, type) {
30+
let data = output.data[type];
4031
41-
return 'code-cell';
42-
},
43-
rawCode() {
44-
if (this.output.text) {
45-
return this.output.text.join('');
32+
if (typeof data === 'object') {
33+
data = data.join('');
4634
}
4735
48-
return this.dataForType(this.outputType);
36+
return data;
4937
},
50-
outputType() {
51-
if (this.output.text) {
52-
return '';
53-
} else if (this.output.data['image/png']) {
54-
return 'image/png';
55-
} else if (this.output.data['text/html']) {
56-
return 'text/html';
57-
} else if (this.output.data['image/svg+xml']) {
58-
return 'image/svg+xml';
38+
getComponent(output) {
39+
if (output.text) {
40+
return CodeOutput;
41+
} else if (output.data['image/png']) {
42+
this.outputType = 'image/png';
43+
44+
return ImageOutput;
45+
} else if (output.data['text/html']) {
46+
this.outputType = 'text/html';
47+
48+
return HtmlOutput;
49+
} else if (output.data['image/svg+xml']) {
50+
this.outputType = 'image/svg+xml';
51+
52+
return HtmlOutput;
5953
}
6054
61-
return 'text/plain';
55+
this.outputType = 'text/plain';
56+
return CodeOutput;
6257
},
63-
},
64-
methods: {
65-
dataForType(type) {
66-
let data = this.output.data[type];
67-
68-
if (typeof data === 'object') {
69-
data = data.join('');
58+
rawCode(output) {
59+
if (output.text) {
60+
return output.text.join('');
7061
}
7162
72-
return data;
63+
return this.dataForType(output, this.outputType);
7364
},
7465
},
7566
};
7667
</script>
7768

7869
<template>
79-
<component
80-
:is="componentName"
81-
:output-type="outputType"
82-
:count="count"
83-
:raw-code="rawCode"
84-
:code-css-class="codeCssClass"
85-
type="output"
86-
/>
70+
<div>
71+
<component
72+
:is="getComponent(output)"
73+
v-for="(output, index) in outputs"
74+
:key="index"
75+
type="output"
76+
:output-type="outputType"
77+
:count="count"
78+
:index="index"
79+
:raw-code="rawCode(output)"
80+
:code-css-class="codeCssClass"
81+
/>
82+
</div>
8783
</template>

app/assets/javascripts/notebook/cells/prompt.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,26 @@ export default {
1111
required: false,
1212
default: 0,
1313
},
14+
showOutput: {
15+
type: Boolean,
16+
required: false,
17+
default: true,
18+
},
1419
},
1520
computed: {
1621
hasKeys() {
1722
return this.type !== '' && this.count;
1823
},
24+
showTypeText() {
25+
return this.type && this.count && this.showOutput;
26+
},
1927
},
2028
};
2129
</script>
2230

2331
<template>
2432
<div class="prompt">
25-
<span v-if="hasKeys"> {{ type }} [{{ count }}]: </span>
33+
<span v-if="showTypeText"> {{ type }} [{{ count }}]: </span>
2634
</div>
2735
</template>
2836

app/assets/javascripts/notebook/index.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { MarkdownCell, CodeCell } from './cells';
33
44
export default {
55
components: {
6-
'code-cell': CodeCell,
7-
'markdown-cell': MarkdownCell,
6+
CodeCell,
7+
MarkdownCell,
88
},
99
props: {
1010
notebook: {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
title: Support multiple outputs in jupyter notebooks
3+
merge_request:
4+
author:
5+
type: changed

spec/javascripts/notebook/cells/output/html_spec.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ describe('html output cell', () => {
99
return new Component({
1010
propsData: {
1111
rawCode,
12+
count: 0,
13+
index: 0,
1214
},
1315
}).$mount();
1416
}

spec/javascripts/notebook/cells/output/index_spec.js

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe('Output component', () => {
1010
const createComponent = output => {
1111
vm = new Component({
1212
propsData: {
13-
output,
13+
outputs: [].concat(output),
1414
count: 1,
1515
},
1616
});
@@ -51,28 +51,21 @@ describe('Output component', () => {
5151
it('renders as an image', () => {
5252
expect(vm.$el.querySelector('img')).not.toBeNull();
5353
});
54-
55-
it('does not render the prompt', () => {
56-
expect(vm.$el.querySelector('.prompt span')).toBeNull();
57-
});
5854
});
5955

6056
describe('html output', () => {
61-
beforeEach(done => {
57+
it('renders raw HTML', () => {
6258
createComponent(json.cells[4].outputs[0]);
6359

64-
setTimeout(() => {
65-
done();
66-
});
67-
});
68-
69-
it('renders raw HTML', () => {
7060
expect(vm.$el.querySelector('p')).not.toBeNull();
71-
expect(vm.$el.textContent.trim()).toBe('test');
61+
expect(vm.$el.querySelectorAll('p').length).toBe(1);
62+
expect(vm.$el.textContent.trim()).toContain('test');
7263
});
7364

74-
it('does not render the prompt', () => {
75-
expect(vm.$el.querySelector('.prompt span')).toBeNull();
65+
it('renders multiple raw HTML outputs', () => {
66+
createComponent([json.cells[4].outputs[0], json.cells[4].outputs[0]]);
67+
68+
expect(vm.$el.querySelectorAll('p').length).toBe(2);
7669
});
7770
});
7871

@@ -88,10 +81,6 @@ describe('Output component', () => {
8881
it('renders as an svg', () => {
8982
expect(vm.$el.querySelector('svg')).not.toBeNull();
9083
});
91-
92-
it('does not render the prompt', () => {
93-
expect(vm.$el.querySelector('.prompt span')).toBeNull();
94-
});
9584
});
9685

9786
describe('default to plain text', () => {

0 commit comments

Comments
 (0)