diff --git a/app/assets/javascripts/jobs/components/log/collapsible_section.vue b/app/assets/javascripts/jobs/components/log/collapsible_section.vue new file mode 100644 index 0000000000000000000000000000000000000000..7c0deb08488a09543910becc4875d00bf599bf5b --- /dev/null +++ b/app/assets/javascripts/jobs/components/log/collapsible_section.vue @@ -0,0 +1,55 @@ +<script> +import LogLine from './line.vue'; +import LogLineHeader from './line_header.vue'; + +export default { + name: 'CollpasibleLogSection', + components: { + LogLine, + LogLineHeader, + }, + props: { + section: { + type: Object, + required: true, + }, + traceEndpoint: { + type: String, + required: true, + }, + }, + computed: { + badgeDuration() { + return this.section.line && this.section.line.section_duration; + }, + }, + methods: { + handleOnClickCollapsibleLine(section) { + this.$emit('onClickCollapsibleLine', section); + }, + }, +}; +</script> +<template> + <div> + <log-line-header + :line="section.line" + :duration="badgeDuration" + :path="traceEndpoint" + :is-closed="section.isClosed" + @toggleLine="handleOnClickCollapsibleLine(section)" + /> + <template v-if="!section.isClosed"> + <template v-for="line in section.lines"> + <collpasible-log-section + v-if="line.isHeader" + :key="`collapsible-nested-${line.offset}`" + :section="line" + :trace-endpoint="traceEndpoint" + @toggleLine="handleOnClickCollapsibleLine" + /> + <log-line v-else :key="line.offset" :line="line" :path="traceEndpoint" /> + </template> + </template> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/log/log.vue b/app/assets/javascripts/jobs/components/log/log.vue index 429796aeb4e5188cdf11cd32f9b536a1a56b4675..ef126166e8bd9c1520d4d1466425c0b5ef414b7b 100644 --- a/app/assets/javascripts/jobs/components/log/log.vue +++ b/app/assets/javascripts/jobs/components/log/log.vue @@ -1,12 +1,12 @@ <script> import { mapState, mapActions } from 'vuex'; +import CollpasibleLogSection from './collapsible_section.vue'; import LogLine from './line.vue'; -import LogLineHeader from './line_header.vue'; export default { components: { + CollpasibleLogSection, LogLine, - LogLineHeader, }, computed: { ...mapState(['traceEndpoint', 'trace', 'isTraceComplete']), @@ -22,24 +22,13 @@ export default { <template> <code class="job-log d-block"> <template v-for="(section, index) in trace"> - <template v-if="section.isHeader"> - <log-line-header - :key="`collapsible-${index}`" - :line="section.line" - :duration="section.section_duration" - :path="traceEndpoint" - :is-closed="section.isClosed" - @toggleLine="handleOnClickCollapsibleLine(section)" - /> - <template v-if="!section.isClosed"> - <log-line - v-for="line in section.lines" - :key="line.offset" - :line="line" - :path="traceEndpoint" - /> - </template> - </template> + <collpasible-log-section + v-if="section.isHeader" + :key="`collapsible-${index}`" + :section="section" + :trace-endpoint="traceEndpoint" + @onClickCollapsibleLine="handleOnClickCollapsibleLine" + /> <log-line v-else :key="section.offset" :line="section" :path="traceEndpoint" /> </template> diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 82b4ec750ff28d0eb1669e69c29c8f945e49ce87..c4a0ed63080a7eeb529ab240d92dc88837e2dd4b 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -28,6 +28,7 @@ @import 'framework/issue_box'; @import 'framework/lists'; @import 'framework/logo'; +@import 'framework/job_log'; @import 'framework/markdown_area'; @import 'framework/media_object'; @import 'framework/modal'; diff --git a/spec/frontend/jobs/components/log/collapsible_section_spec.js b/spec/frontend/jobs/components/log/collapsible_section_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..6c1ebf0a7c1f9df5fb50c3b73f529255a5ea556b --- /dev/null +++ b/spec/frontend/jobs/components/log/collapsible_section_spec.js @@ -0,0 +1,60 @@ +import { mount } from '@vue/test-utils'; +import CollpasibleSection from '~/jobs/components/log/collapsible_section.vue'; +import { nestedSectionOpened, nestedSectionClosed } from './mock_data'; + +describe('Job Log Collapsible Section', () => { + let wrapper; + + const traceEndpoint = 'jobs/335'; + + const findCollapsibleLine = () => wrapper.find('.collapsible-line'); + + const createComponent = (props = {}) => { + wrapper = mount(CollpasibleSection, { + sync: true, + propsData: { + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('with closed nested section', () => { + beforeEach(() => { + createComponent({ + section: nestedSectionClosed, + traceEndpoint, + }); + }); + + it('renders clickable header line', () => { + expect(findCollapsibleLine().attributes('role')).toBe('button'); + }); + }); + + describe('with opened nested section', () => { + beforeEach(() => { + createComponent({ + section: nestedSectionOpened, + traceEndpoint, + }); + }); + + it('renders all sections opened', () => { + expect(wrapper.findAll('.collapsible-line').length).toBe(2); + }); + }); + + it('emits onClickCollapsibleLine on click', () => { + createComponent({ + section: nestedSectionOpened, + traceEndpoint, + }); + + findCollapsibleLine().trigger('click'); + expect(wrapper.emitted('onClickCollapsibleLine').length).toBe(1); + }); +}); diff --git a/spec/frontend/jobs/components/log/mock_data.js b/spec/frontend/jobs/components/log/mock_data.js index db42644de77b0175e5feb3cf9352a6bba49b6c9a..0dae306dcc7f64e48323710a3588f91919736aa1 100644 --- a/spec/frontend/jobs/components/log/mock_data.js +++ b/spec/frontend/jobs/components/log/mock_data.js @@ -150,3 +150,73 @@ export const collapsibleTraceIncremental = [ sections: ['section'], }, ]; + +export const nestedSectionClosed = { + offset: 5, + section_header: true, + isHeader: true, + isClosed: true, + line: { + content: [{ text: 'foo' }], + sections: ['prepare-script'], + lineNumber: 1, + }, + section_duration: '00:03', + lines: [ + { + section_header: true, + section_duration: '00:02', + isHeader: true, + isClosed: true, + line: { + offset: 52, + content: [{ text: 'bar' }], + sections: ['prepare-script', 'prepare-script-nested'], + lineNumber: 2, + }, + lines: [ + { + offset: 80, + content: [{ text: 'this is a collapsible nested section' }], + sections: ['prepare-script', 'prepare-script-nested'], + lineNumber: 3, + }, + ], + }, + ], +}; + +export const nestedSectionOpened = { + offset: 5, + section_header: true, + isHeader: true, + isClosed: false, + line: { + content: [{ text: 'foo' }], + sections: ['prepare-script'], + lineNumber: 1, + }, + section_duration: '00:03', + lines: [ + { + section_header: true, + section_duration: '00:02', + isHeader: true, + isClosed: false, + line: { + offset: 52, + content: [{ text: 'bar' }], + sections: ['prepare-script', 'prepare-script-nested'], + lineNumber: 2, + }, + lines: [ + { + offset: 80, + content: [{ text: 'this is a collapsible nested section' }], + sections: ['prepare-script', 'prepare-script-nested'], + lineNumber: 3, + }, + ], + }, + ], +};