summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/jobs
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/jobs')
-rw-r--r--app/assets/javascripts/jobs/components/empty_state.vue11
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue6
-rw-r--r--app/assets/javascripts/jobs/components/log/collapsible_section.vue33
-rw-r--r--app/assets/javascripts/jobs/components/log/line_number.vue6
-rw-r--r--app/assets/javascripts/jobs/components/manual_variables_form.vue39
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_job_details_container.vue2
-rw-r--r--app/assets/javascripts/jobs/constants.js2
-rw-r--r--app/assets/javascripts/jobs/index.js2
-rw-r--r--app/assets/javascripts/jobs/store/actions.js24
-rw-r--r--app/assets/javascripts/jobs/store/mutations.js28
-rw-r--r--app/assets/javascripts/jobs/store/state.js3
-rw-r--r--app/assets/javascripts/jobs/store/utils.js75
-rw-r--r--app/assets/javascripts/jobs/utils.js6
13 files changed, 176 insertions, 61 deletions
diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue
index 35b16d73cc7..e31c13f40b0 100644
--- a/app/assets/javascripts/jobs/components/empty_state.vue
+++ b/app/assets/javascripts/jobs/components/empty_state.vue
@@ -35,11 +35,6 @@ export default {
required: false,
default: false,
},
- variablesSettingsUrl: {
- type: String,
- required: false,
- default: null,
- },
action: {
type: Object,
required: false,
@@ -75,11 +70,7 @@ export default {
<p v-if="content" data-testid="job-empty-state-content">{{ content }}</p>
</div>
- <manual-variables-form
- v-if="shouldRenderManualVariables"
- :action="action"
- :variables-settings-url="variablesSettingsUrl"
- />
+ <manual-variables-form v-if="shouldRenderManualVariables" :action="action" />
<div class="text-content">
<div v-if="action && !shouldRenderManualVariables" class="text-center">
<gl-link
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index be95001a396..fa9ee56c049 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -50,11 +50,6 @@ export default {
required: false,
default: null,
},
- variablesSettingsUrl: {
- type: String,
- required: false,
- default: null,
- },
deploymentHelpUrl: {
type: String,
required: false,
@@ -315,7 +310,6 @@ export default {
:action="emptyStateAction"
:playable="job.playable"
:scheduled="job.scheduled"
- :variables-settings-url="variablesSettingsUrl"
/>
<!-- EO empty state -->
diff --git a/app/assets/javascripts/jobs/components/log/collapsible_section.vue b/app/assets/javascripts/jobs/components/log/collapsible_section.vue
index 55cdfb691f4..c0d5fac0e8d 100644
--- a/app/assets/javascripts/jobs/components/log/collapsible_section.vue
+++ b/app/assets/javascripts/jobs/components/log/collapsible_section.vue
@@ -1,4 +1,6 @@
<script>
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF } from '../../constants';
import LogLine from './line.vue';
import LogLineHeader from './line_header.vue';
@@ -7,7 +9,9 @@ export default {
components: {
LogLine,
LogLineHeader,
+ CollapsibleLogSection: () => import('./collapsible_section.vue'),
},
+ mixins: [glFeatureFlagsMixin()],
props: {
section: {
type: Object,
@@ -22,6 +26,9 @@ export default {
badgeDuration() {
return this.section.line && this.section.line.section_duration;
},
+ infinitelyCollapsibleSectionsFlag() {
+ return this.glFeatures?.[INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF];
+ },
},
methods: {
handleOnClickCollapsibleLine(section) {
@@ -40,12 +47,26 @@ export default {
@toggleLine="handleOnClickCollapsibleLine(section)"
/>
<template v-if="!section.isClosed">
- <log-line
- v-for="line in section.lines"
- :key="line.offset"
- :line="line"
- :path="traceEndpoint"
- />
+ <template v-if="infinitelyCollapsibleSectionsFlag">
+ <template v-for="line in section.lines">
+ <collapsible-log-section
+ v-if="line.isHeader"
+ :key="line.line.offset"
+ :section="line"
+ :trace-endpoint="traceEndpoint"
+ @onClickCollapsibleLine="handleOnClickCollapsibleLine"
+ />
+ <log-line v-else :key="line.offset" :line="line" :path="traceEndpoint" />
+ </template>
+ </template>
+ <template v-else>
+ <log-line
+ v-for="line in section.lines"
+ :key="line.offset"
+ :line="line"
+ :path="traceEndpoint"
+ />
+ </template>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/log/line_number.vue b/app/assets/javascripts/jobs/components/log/line_number.vue
index 7ca9154d2fe..c8ceac2c7ff 100644
--- a/app/assets/javascripts/jobs/components/log/line_number.vue
+++ b/app/assets/javascripts/jobs/components/log/line_number.vue
@@ -1,4 +1,6 @@
<script>
+import { INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF } from '../../constants';
+
export default {
functional: true,
props: {
@@ -14,7 +16,9 @@ export default {
render(h, { props }) {
const { lineNumber, path } = props;
- const parsedLineNumber = lineNumber + 1;
+ const parsedLineNumber = gon.features?.[INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF]
+ ? lineNumber
+ : lineNumber + 1;
const lineId = `L${parsedLineNumber}`;
const lineHref = `${path}#${lineId}`;
diff --git a/app/assets/javascripts/jobs/components/manual_variables_form.vue b/app/assets/javascripts/jobs/components/manual_variables_form.vue
index d45012d2023..269551ff9aa 100644
--- a/app/assets/javascripts/jobs/components/manual_variables_form.vue
+++ b/app/assets/javascripts/jobs/components/manual_variables_form.vue
@@ -1,14 +1,16 @@
<script>
-/* eslint-disable vue/no-v-html */
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlLink, GlSprintf } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { mapActions } from 'vuex';
-import { s__, sprintf } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { s__ } from '~/locale';
export default {
name: 'ManualVariablesForm',
components: {
GlButton,
+ GlLink,
+ GlSprintf,
},
props: {
action: {
@@ -24,11 +26,6 @@ export default {
);
},
},
- variablesSettingsUrl: {
- type: String,
- required: true,
- default: '',
- },
},
inputTypes: {
key: 'key',
@@ -37,6 +34,9 @@ export default {
i18n: {
keyPlaceholder: s__('CiVariables|Input variable key'),
valuePlaceholder: s__('CiVariables|Input variable value'),
+ formHelpText: s__(
+ 'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default',
+ ),
},
data() {
return {
@@ -47,17 +47,8 @@ export default {
};
},
computed: {
- helpText() {
- return sprintf(
- s__(
- 'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default',
- ),
- {
- linkStart: `<a href="${this.variablesSettingsUrl}">`,
- linkEnd: '</a>',
- },
- false,
- );
+ variableSettings() {
+ return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' });
},
},
watch: {
@@ -188,8 +179,14 @@ export default {
</div>
</div>
</div>
- <div class="d-flex gl-mt-3 justify-content-center">
- <p class="text-muted" data-testid="form-help-text" v-html="helpText"></p>
+ <div class="gl-text-center gl-mt-3">
+ <gl-sprintf :message="$options.i18n.formHelpText">
+ <template #link="{ content }">
+ <gl-link :href="variableSettings" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
</div>
<div class="d-flex justify-content-center">
<gl-button
diff --git a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
index 98badb96ed7..a6eff743ce9 100644
--- a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
@@ -46,7 +46,7 @@ export default {
return timeIntervalInWords(this.job.queued);
},
runnerHelpUrl() {
- return helpPagePath('ci/runners/README.html', {
+ return helpPagePath('ci/runners/index.html', {
anchor: 'set-maximum-job-timeout-for-a-runner',
});
},
diff --git a/app/assets/javascripts/jobs/constants.js b/app/assets/javascripts/jobs/constants.js
index 3040d4e2379..97f31eee57c 100644
--- a/app/assets/javascripts/jobs/constants.js
+++ b/app/assets/javascripts/jobs/constants.js
@@ -24,3 +24,5 @@ export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = {
};
export const SUCCESS_STATUS = 'SUCCESS';
+
+export const INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF = 'infinitelyCollapsibleSections';
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index 260190f5043..1fb6a6f9850 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -15,7 +15,6 @@ export default () => {
deploymentHelpUrl,
codeQualityHelpUrl,
runnerSettingsUrl,
- variablesSettingsUrl,
subscriptionsMoreMinutesUrl,
endpoint,
pagePath,
@@ -41,7 +40,6 @@ export default () => {
deploymentHelpUrl,
codeQualityHelpUrl,
runnerSettingsUrl,
- variablesSettingsUrl,
subscriptionsMoreMinutesUrl,
endpoint,
pagePath,
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js
index c89aeada69d..a8be5d8d039 100644
--- a/app/assets/javascripts/jobs/store/actions.js
+++ b/app/assets/javascripts/jobs/store/actions.js
@@ -1,5 +1,5 @@
import Visibility from 'visibilityjs';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { setFaviconOverlay, resetFavicon } from '~/lib/utils/favicon';
import httpStatusCodes from '~/lib/utils/http_status';
@@ -99,7 +99,9 @@ export const receiveJobSuccess = ({ commit }, data = {}) => {
};
export const receiveJobError = ({ commit }) => {
commit(types.RECEIVE_JOB_ERROR);
- flash(__('An error occurred while fetching the job.'));
+ createFlash({
+ message: __('An error occurred while fetching the job.'),
+ });
resetFavicon();
};
@@ -197,11 +199,15 @@ export const stopPollingTrace = ({ state, commit }) => {
export const receiveTraceSuccess = ({ commit }, log) => commit(types.RECEIVE_TRACE_SUCCESS, log);
export const receiveTraceError = ({ dispatch }) => {
dispatch('stopPollingTrace');
- flash(__('An error occurred while fetching the job log.'));
+ createFlash({
+ message: __('An error occurred while fetching the job log.'),
+ });
};
export const receiveTraceUnauthorizedError = ({ dispatch }) => {
dispatch('stopPollingTrace');
- flash(__('The current user is not authorized to access the job log.'));
+ createFlash({
+ message: __('The current user is not authorized to access the job log.'),
+ });
};
/**
* When the user clicks a collapsible line in the job
@@ -240,7 +246,9 @@ export const receiveJobsForStageSuccess = ({ commit }, data) =>
commit(types.RECEIVE_JOBS_FOR_STAGE_SUCCESS, data);
export const receiveJobsForStageError = ({ commit }) => {
commit(types.RECEIVE_JOBS_FOR_STAGE_ERROR);
- flash(__('An error occurred while fetching the jobs.'));
+ createFlash({
+ message: __('An error occurred while fetching the jobs.'),
+ });
};
export const triggerManualJob = ({ state }, variables) => {
@@ -254,5 +262,9 @@ export const triggerManualJob = ({ state }, variables) => {
.post(state.job.status.action.path, {
job_variables_attributes: parsedVariables,
})
- .catch(() => flash(__('An error occurred while triggering the job.')));
+ .catch(() =>
+ createFlash({
+ message: __('An error occurred while triggering the job.'),
+ }),
+ );
};
diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js
index 924b811d0d6..4045d8a0c16 100644
--- a/app/assets/javascripts/jobs/store/mutations.js
+++ b/app/assets/javascripts/jobs/store/mutations.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
+import { INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF } from '../constants';
import * as types from './mutation_types';
-import { logLinesParser, updateIncrementalTrace } from './utils';
+import { logLinesParser, logLinesParserLegacy, updateIncrementalTrace } from './utils';
export default {
[types.SET_JOB_ENDPOINT](state, endpoint) {
@@ -20,12 +21,26 @@ export default {
},
[types.RECEIVE_TRACE_SUCCESS](state, log = {}) {
+ const infinitelyCollapsibleSectionsFlag =
+ gon.features?.[INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF];
if (log.state) {
state.traceState = log.state;
}
if (log.append) {
- state.trace = log.lines ? updateIncrementalTrace(log.lines, state.trace) : state.trace;
+ if (infinitelyCollapsibleSectionsFlag) {
+ if (log.lines) {
+ const parsedResult = logLinesParser(
+ log.lines,
+ state.auxiliaryPartialTraceHelpers,
+ state.trace,
+ );
+ state.trace = parsedResult.parsedLines;
+ state.auxiliaryPartialTraceHelpers = parsedResult.auxiliaryPartialTraceHelpers;
+ }
+ } else {
+ state.trace = log.lines ? updateIncrementalTrace(log.lines, state.trace) : state.trace;
+ }
state.traceSize += log.size;
} else {
@@ -33,7 +48,14 @@ export default {
// the trace response will not have a defined
// html or size. We keep the old value otherwise these
// will be set to `null`
- state.trace = log.lines ? logLinesParser(log.lines) : state.trace;
+
+ if (infinitelyCollapsibleSectionsFlag) {
+ const parsedResult = logLinesParser(log.lines);
+ state.trace = parsedResult.parsedLines;
+ state.auxiliaryPartialTraceHelpers = parsedResult.auxiliaryPartialTraceHelpers;
+ } else {
+ state.trace = log.lines ? logLinesParserLegacy(log.lines) : state.trace;
+ }
state.traceSize = log.size || state.traceSize;
}
diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js
index 2fe945b2985..718324c8bad 100644
--- a/app/assets/javascripts/jobs/store/state.js
+++ b/app/assets/javascripts/jobs/store/state.js
@@ -30,4 +30,7 @@ export default () => ({
selectedStage: '',
stages: [],
jobs: [],
+
+ // to parse partial logs
+ auxiliaryPartialTraceHelpers: {},
});
diff --git a/app/assets/javascripts/jobs/store/utils.js b/app/assets/javascripts/jobs/store/utils.js
index a0e0a0fb8bd..36391a4d433 100644
--- a/app/assets/javascripts/jobs/store/utils.js
+++ b/app/assets/javascripts/jobs/store/utils.js
@@ -104,7 +104,7 @@ export const getIncrementalLineNumber = (acc) => {
* @param Array accumulator
* @returns Array parsed log lines
*/
-export const logLinesParser = (lines = [], accumulator = []) =>
+export const logLinesParserLegacy = (lines = [], accumulator = []) =>
lines.reduce(
(acc, line, index) => {
const lineNumber = accumulator.length > 0 ? getIncrementalLineNumber(acc) : index;
@@ -131,6 +131,77 @@ export const logLinesParser = (lines = [], accumulator = []) =>
[...accumulator],
);
+export const logLinesParser = (lines = [], previousTraceState = {}, prevParsedLines = []) => {
+ let currentLine = previousTraceState?.prevLineCount ?? 0;
+ let currentHeader = previousTraceState?.currentHeader;
+ let isPreviousLineHeader = previousTraceState?.isPreviousLineHeader ?? false;
+ const parsedLines = prevParsedLines.length > 0 ? prevParsedLines : [];
+ const sectionsQueue = previousTraceState?.sectionsQueue ?? [];
+
+ for (let i = 0; i < lines.length; i += 1) {
+ const line = lines[i];
+ // First run we can use the current index, later runs we have to retrieve the last number of lines
+ currentLine = previousTraceState?.prevLineCount ? currentLine + 1 : i + 1;
+
+ if (line.section_header && !isPreviousLineHeader) {
+ // If there's no previous line header that means we're at the root of the log
+
+ isPreviousLineHeader = true;
+ parsedLines.push(parseHeaderLine(line, currentLine));
+ currentHeader = { index: parsedLines.length - 1 };
+ } else if (line.section_header && isPreviousLineHeader) {
+ // If there's a current section, we can't push to the parsedLines array
+ sectionsQueue.push(currentHeader);
+ currentHeader = parseHeaderLine(line, currentLine); // Let's parse the incoming header line
+ } else if (line.section && !line.section_duration) {
+ // We're inside a collapsible section and want to parse a standard line
+ if (currentHeader?.index) {
+ // If the current section header is only an index, add the line as part of the lines
+ // array of the current collapsible section
+ parsedLines[currentHeader.index].lines.push(parseLine(line, currentLine));
+ } else {
+ // Otherwise add it to the innermost collapsible section lines array
+ currentHeader.lines.push(parseLine(line, currentLine));
+ }
+ } else if (line.section && line.section_duration) {
+ // NOTE: This marks the end of a section_header
+ const previousSection = sectionsQueue.pop();
+
+ // Add the duration to section header
+ // If at the root, just push the end to the current parsedLine,
+ // otherwise, push it to the previous sections queue
+ if (currentHeader?.index) {
+ parsedLines[currentHeader.index].line.section_duration = line.section_duration;
+ isPreviousLineHeader = false;
+ currentHeader = null;
+ } else {
+ currentHeader.line.section_duration = line.section_duration;
+
+ if (previousSection && previousSection?.index) {
+ // Is the previous section on root?
+ parsedLines[previousSection.index].lines.push(currentHeader);
+ } else if (previousSection && !previousSection?.index) {
+ previousSection.lines.push(currentHeader);
+ }
+
+ currentHeader = previousSection;
+ }
+ } else {
+ parsedLines.push(parseLine(line, currentLine));
+ }
+ }
+
+ return {
+ parsedLines,
+ auxiliaryPartialTraceHelpers: {
+ isPreviousLineHeader,
+ currentHeader,
+ sectionsQueue,
+ prevLineCount: lines.length,
+ },
+ };
+};
+
/**
* Finds the repeated offset, removes the old one
*
@@ -177,5 +248,5 @@ export const findOffsetAndRemove = (newLog = [], oldParsed = []) => {
export const updateIncrementalTrace = (newLog = [], oldParsed = []) => {
const parsedLog = findOffsetAndRemove(newLog, oldParsed);
- return logLinesParser(newLog, parsedLog);
+ return logLinesParserLegacy(newLog, parsedLog);
};
diff --git a/app/assets/javascripts/jobs/utils.js b/app/assets/javascripts/jobs/utils.js
index 122f23a5bb5..1ccecf3eb53 100644
--- a/app/assets/javascripts/jobs/utils.js
+++ b/app/assets/javascripts/jobs/utils.js
@@ -3,10 +3,10 @@
* https?:\/\/
*
* up until a disallowed character or whitespace
- * [^"<>\\^`{|}\s]+
+ * [^"<>()\\^`{|}\s]+
*
* and a disallowed character or whitespace, including non-ending chars .,:;!?
- * [^"<>\\^`{|}\s.,:;!?]
+ * [^"<>()\\^`{|}\s.,:;!?]
*/
-export const linkRegex = /(https?:\/\/[^"<>\\^`{|}\s]+[^"<>\\^`{|}\s.,:;!?])/g;
+export const linkRegex = /(https?:\/\/[^"<>()\\^`{|}\s]+[^"<>()\\^`{|}\s.,:;!?])/g;
export default { linkRegex };