diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/jobs/components/job_app.vue | 6 | ||||
-rw-r--r-- | app/assets/javascripts/jobs/components/log/log.vue | 45 | ||||
-rw-r--r-- | app/assets/javascripts/jobs/store/actions.js | 8 | ||||
-rw-r--r-- | app/assets/javascripts/jobs/store/mutation_types.js | 1 | ||||
-rw-r--r-- | app/assets/javascripts/jobs/store/mutations.js | 28 | ||||
-rw-r--r-- | app/assets/javascripts/jobs/store/state.js | 5 | ||||
-rw-r--r-- | app/assets/javascripts/jobs/store/utils.js | 54 |
7 files changed, 136 insertions, 11 deletions
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index c7d4d7c4b9b..36701a95673 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -18,6 +18,7 @@ import UnmetPrerequisitesBlock from './unmet_prerequisites_block.vue'; import Sidebar from './sidebar.vue'; import { sprintf } from '~/locale'; import delayedJobMixin from '../mixins/delayed_job_mixin'; +import { isNewJobLogActive } from '../store/utils'; export default { name: 'JobPageApp', @@ -29,10 +30,7 @@ export default { EnvironmentsBlock, ErasedBlock, Icon, - Log: () => - gon && gon.features && gon.features.jobLogJson - ? import('./job_log_json.vue') - : import('./job_log.vue'), + Log: () => (isNewJobLogActive() ? import('./job_log_json.vue') : import('./job_log.vue')), LogTopBar, StuckBlock, UnmetPrerequisitesBlock, diff --git a/app/assets/javascripts/jobs/components/log/log.vue b/app/assets/javascripts/jobs/components/log/log.vue new file mode 100644 index 00000000000..5db866afe5a --- /dev/null +++ b/app/assets/javascripts/jobs/components/log/log.vue @@ -0,0 +1,45 @@ +<script> +import { mapState, mapActions } from 'vuex'; +import LogLine from './line.vue'; +import LogLineHeader from './line_header.vue'; + +export default { + components: { + LogLine, + LogLineHeader, + }, + computed: { + ...mapState(['traceEndpoint', 'trace']), + }, + methods: { + ...mapActions(['toggleCollapsibleLine']), + handleOnClickCollapsibleLine(section) { + this.toggleCollapsibleLine(section); + }, + }, +}; +</script> +<template> + <code class="job-log"> + <template v-for="(section, index) in trace"> + <template v-if="section.isHeader"> + <log-line-header + :key="`collapsible-${index}`" + :line="section.line" + :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> + <log-line v-else :key="section.offset" :line="section" :path="traceEndpoint" /> + </template> + </code> +</template> diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js index a2daef96a2d..41cc5a181dc 100644 --- a/app/assets/javascripts/jobs/store/actions.js +++ b/app/assets/javascripts/jobs/store/actions.js @@ -177,6 +177,14 @@ export const receiveTraceError = ({ commit }) => { clearTimeout(traceTimeout); flash(__('An error occurred while fetching the job log.')); }; +/** + * When the user clicks a collpasible line in the job + * log, we commit a mutation to update the state + * + * @param {Object} section + */ +export const toggleCollapsibleLine = ({ commit }, section) => + commit(types.TOGGLE_COLLAPSIBLE_LINE, section); /** * Jobs list on sidebar - depend on stages dropdown diff --git a/app/assets/javascripts/jobs/store/mutation_types.js b/app/assets/javascripts/jobs/store/mutation_types.js index 39146b2eefd..858fa3b73ab 100644 --- a/app/assets/javascripts/jobs/store/mutation_types.js +++ b/app/assets/javascripts/jobs/store/mutation_types.js @@ -23,6 +23,7 @@ export const REQUEST_TRACE = 'REQUEST_TRACE'; export const STOP_POLLING_TRACE = 'STOP_POLLING_TRACE'; export const RECEIVE_TRACE_SUCCESS = 'RECEIVE_TRACE_SUCCESS'; export const RECEIVE_TRACE_ERROR = 'RECEIVE_TRACE_ERROR'; +export const TOGGLE_COLLAPSIBLE_LINE = 'TOGGLE_COLLAPSIBLE_LINE'; export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE'; export const REQUEST_JOBS_FOR_STAGE = 'REQUEST_JOBS_FOR_STAGE'; diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js index ad08f27b147..540c3e2ad69 100644 --- a/app/assets/javascripts/jobs/store/mutations.js +++ b/app/assets/javascripts/jobs/store/mutations.js @@ -1,4 +1,6 @@ +import Vue from 'vue'; import * as types from './mutation_types'; +import { logLinesParser, updateIncrementalTrace, isNewJobLogActive } from './utils'; export default { [types.SET_JOB_ENDPOINT](state, endpoint) { @@ -23,14 +25,24 @@ export default { } if (log.append) { - state.trace += log.html; + if (isNewJobLogActive()) { + state.originalTrace = state.originalTrace.concat(log.trace); + state.trace = updateIncrementalTrace(state.originalTrace, state.trace, log.lines); + } else { + state.trace += log.html; + } state.traceSize += log.size; } else { // When the job still does not have a trace // the trace response will not have a defined // html or size. We keep the old value otherwise these // will be set to `undefined` - state.trace = log.html || state.trace; + if (isNewJobLogActive()) { + state.originalTrace = log.lines || state.trace; + state.trace = logLinesParser(log.lines) || state.trace; + } else { + state.trace = log.html || state.trace; + } state.traceSize = log.size || state.traceSize; } @@ -57,6 +69,18 @@ export default { state.isTraceComplete = true; }, + /** + * Instead of filtering the array of lines to find the one that must be updated + * we use Vue.set to make this process more performant + * + * https://vuex.vuejs.org/guide/mutations.html#mutations-follow-vue-s-reactivity-rules + * @param {Object} state + * @param {Object} section + */ + [types.TOGGLE_COLLAPSIBLE_LINE](state, section) { + Vue.set(section, 'isClosed', !section.isClosed); + }, + [types.REQUEST_JOB](state) { state.isLoading = true; }, diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js index 6019214e62c..585878f8240 100644 --- a/app/assets/javascripts/jobs/store/state.js +++ b/app/assets/javascripts/jobs/store/state.js @@ -1,3 +1,5 @@ +import { isNewJobLogActive } from '../store/utils'; + export default () => ({ jobEndpoint: null, traceEndpoint: null, @@ -16,7 +18,8 @@ export default () => ({ // Used to check if we should keep the automatic scroll isScrolledToBottomBeforeReceivingTrace: true, - trace: '', + trace: isNewJobLogActive() ? [] : '', + originalTrace: [], isTraceComplete: false, traceSize: 0, isTraceSizeVisible: false, diff --git a/app/assets/javascripts/jobs/store/utils.js b/app/assets/javascripts/jobs/store/utils.js index de7de92ed2e..f6a87b9a212 100644 --- a/app/assets/javascripts/jobs/store/utils.js +++ b/app/assets/javascripts/jobs/store/utils.js @@ -11,15 +11,16 @@ * @param {Array} lines * @returns {Array} */ -export default (lines = []) => +export const logLinesParser = (lines = [], lineNumberStart) => lines.reduce((acc, line, index) => { + const lineNumber = lineNumberStart ? lineNumberStart + index : index; if (line.section_header) { acc.push({ isClosed: true, isHeader: true, line: { ...line, - lineNumber: index, + lineNumber, }, lines: [], @@ -27,14 +28,59 @@ export default (lines = []) => } else if (acc.length && acc[acc.length - 1].isHeader) { acc[acc.length - 1].lines.push({ ...line, - lineNumber: index, + lineNumber, }); } else { acc.push({ ...line, - lineNumber: index, + lineNumber, }); } return acc; }, []); + +/** + * When the trace is not complete, backend may send the last received line + * in the new response. + * + * We need to check if that is the case by looking for the offset property + * before parsing the incremental part + * + * @param array originalTrace + * @param array oldLog + * @param array newLog + */ +export const updateIncrementalTrace = (originalTrace = [], oldLog = [], newLog = []) => { + const firstLine = newLog[0]; + const firstLineOffset = firstLine.offset; + + // We are going to return a new array, + // let's make a shallow copy to make sure we + // are not updating the state outside of a mutation first. + const cloneOldLog = [...oldLog]; + + const lastIndex = cloneOldLog.length - 1; + const lastLine = cloneOldLog[lastIndex]; + + // The last line may be inside a collpasible section + // If it is, we use the not parsed saved log, remove the last element + // and parse the first received part togheter with the incremental log + if ( + lastLine.isHeader && + (lastLine.line.offset === firstLineOffset || + (lastLine.lines.length && + lastLine.lines[lastLine.lines.length - 1].offset === firstLineOffset)) + ) { + const cloneOriginal = [...originalTrace]; + cloneOriginal.splice(cloneOriginal.length - 1); + return logLinesParser(cloneOriginal.concat(newLog)); + } else if (lastLine.offset === firstLineOffset) { + cloneOldLog.splice(lastIndex); + return cloneOldLog.concat(logLinesParser(newLog, cloneOldLog.length)); + } + // there are no matches, let's parse the new log and return them together + return cloneOldLog.concat(logLinesParser(newLog, cloneOldLog.length)); +}; + +export const isNewJobLogActive = () => gon && gon.features && gon.features.jobLogJson; |