summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue6
-rw-r--r--app/assets/javascripts/jobs/components/log/log.vue45
-rw-r--r--app/assets/javascripts/jobs/store/actions.js8
-rw-r--r--app/assets/javascripts/jobs/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/jobs/store/mutations.js28
-rw-r--r--app/assets/javascripts/jobs/store/state.js5
-rw-r--r--app/assets/javascripts/jobs/store/utils.js54
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;